Files
ApiCore/ApiCore-Frontend/src/lib/hive/layout.ts
T
Nick SzaunigandClaude Opus 5.5 9f6ab4ea3f refactor: Beute-Frontend vereinfacht (findPart, subVec3, seededRandom)
Gemeinsame Helfer statt vier Kopien der Teilsuche, zwei Vektor-
Subtraktionen und zwei Zufallsgeneratoren; hasPartSheet an einer
Stelle; ungenutzte setOpacity und points-Ref entfernt. Verhalten
unverändert (1050 Tests).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 10:16:35 +02:00

161 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { CatalogPartType, FrameType, HiveCatalog, HivePart, HivePartKind, HiveState } from "./types";
// Layout math shared by the 3D scene, the SVG fallback and the tests.
// 1 unit = 100 mm; x = length, y = height (stack grows on +y from 0), z = width.
// The model is centred at x = z = 0.
export type Vec3 = [number, number, number];
/** a − b, component by component. */
export function subVec3(a: Vec3, b: Vec3): Vec3 {
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}
export interface PartBox {
id: string;
partTypeId: string;
kind: HivePartKind;
center: Vec3;
size: Vec3;
/** Stack index; an insert carries the index of its host. */
stackIndex: number;
}
export interface FrameBox {
boxId: string;
slot: number;
type: FrameType;
name?: string;
center: Vec3;
size: Vec3;
}
export interface HiveLayout {
parts: PartBox[];
inserts: PartBox[];
frames: FrameBox[];
bounds: { w: number; h: number; d: number };
}
const UNIT_MM = 100;
const ROOF_OVERLAP_MM = 35;
const BOX_WALL_MM = 20;
const FRAME_THICKNESS_MM = 25;
const FRAME_HEIGHT_MARGIN_MM = 10;
/** The stainless grid of the floor (Liebig Wanderboden) lies this far below its top edge. */
const FLOOR_GRID_DEPTH_MM = 40;
const mm = (value: number): number => value / UNIT_MM;
/** Depth of the floor's grid below the floor's top edge, in layout units (shared with the 3D floor). */
export const FLOOR_GRID_DEPTH = mm(FLOOR_GRID_DEPTH_MM);
function sizeOf(type: CatalogPartType): Vec3 {
return [mm(type.lengthMm), mm(type.heightMm), mm(type.widthMm)];
}
function insertBox(
insert: HivePart,
type: CatalogPartType,
host: PartBox,
): PartBox {
const size = sizeOf(type);
const hostBottom = host.center[1] - host.size[1] / 2;
const hostTop = host.center[1] + host.size[1] / 2;
// FLOOR_INSERT (Bausperre) lies on the floor's grid near its top edge; every
// other insert (the EVAPORATOR) stands on the bottom of the box it sits in.
const centerY =
type.kind === "FLOOR_INSERT" ? hostTop - FLOOR_GRID_DEPTH + size[1] / 2 : hostBottom + size[1] / 2;
return {
id: insert.id,
partTypeId: insert.partTypeId,
kind: type.kind,
center: [0, centerY, 0],
size,
stackIndex: host.stackIndex,
};
}
/** Frame grid of a box: x of the first slot's left edge, slot pitch, frame size. */
function frameGrid(part: HivePart, type: CatalogPartType, box: PartBox): { left: number; pitch: number; size: Vec3 } | null {
const count = part.frames?.length ?? 0;
if (count === 0) return null;
const inner = box.size[0] - 2 * mm(BOX_WALL_MM);
return {
left: -inner / 2,
pitch: inner / count,
size: [mm(FRAME_THICKNESS_MM), mm(type.heightMm - FRAME_HEIGHT_MARGIN_MM), box.size[2] - 2 * mm(BOX_WALL_MM)],
};
}
/** A frame feeder hangs where its slots' frames would: width = slotWidth x pitch, frame height. */
function feederBox(insert: HivePart, type: CatalogPartType, host: PartBox, grid: ReturnType<typeof frameGrid>): PartBox | null {
if (!grid || typeof insert.slot !== "number" || !type.slotWidth) return null;
const width = grid.pitch * type.slotWidth;
return {
id: insert.id,
partTypeId: insert.partTypeId,
kind: type.kind,
center: [grid.left + grid.pitch * (insert.slot - 1) + width / 2, host.center[1], 0],
size: [width, grid.size[1], grid.size[2]],
stackIndex: host.stackIndex,
};
}
function frameBoxes(part: HivePart, box: PartBox, grid: ReturnType<typeof frameGrid>): FrameBox[] {
const frames = part.frames ?? [];
if (!grid) return [];
const { left, pitch, size } = grid;
return frames.map((frame, index) => ({
boxId: part.id,
slot: frame.slot,
type: frame.type,
...(frame.name !== undefined ? { name: frame.name } : {}),
center: [left + pitch * (index + 0.5), box.center[1], 0],
size,
}));
}
/** Pure layout of a hive state in 3D units (1 = 100 mm). Unknown part types are skipped. */
export function hiveLayout(state: HiveState, catalog: HiveCatalog): HiveLayout {
const parts: PartBox[] = [];
const inserts: PartBox[] = [];
const frames: FrameBox[] = [];
let top = 0;
let maxTop = 0;
let width = 0;
let depth = 0;
state.stack.forEach((part, stackIndex) => {
const type = catalog.partTypes.get(part.partTypeId);
if (!type) return;
const size = sizeOf(type);
const bottom = type.kind === "ROOF" ? top - mm(ROOF_OVERLAP_MM) : top;
const box: PartBox = {
id: part.id,
partTypeId: part.partTypeId,
kind: type.kind,
center: [0, bottom + size[1] / 2, 0],
size,
stackIndex,
};
parts.push(box);
top = bottom + size[1];
maxTop = Math.max(maxTop, top);
width = Math.max(width, size[0]);
depth = Math.max(depth, size[2]);
const grid = type.kind === "BOX" ? frameGrid(part, type, box) : null;
for (const insert of part.inserts ?? []) {
const insertType = catalog.partTypes.get(insert.partTypeId);
if (!insertType) continue;
const placed =
insertType.kind === "FRAME_FEEDER" ? feederBox(insert, insertType, box, grid) : insertBox(insert, insertType, box);
if (placed) inserts.push(placed);
}
if (type.kind === "BOX") frames.push(...frameBoxes(part, box, grid));
});
return { parts, inserts, frames, bounds: { w: width, h: maxTop, d: depth } };
}