feat: holographic 3D hive scene and Futtertasche (Beute L3, first cut)
- 3D dependencies (three, react-three-fiber, drei, postprocessing, motion), exact versions, supply-chain checked; scene loaded only on demand on the colony page (283 kB gzip), SVG stays as fallback - glass hologram per owner design 2026-10-16: true Liebig Zander shapes (grips, rebate, Wanderboden foot), cool blue edge glow, click any part -> it flies to the front while the rest stays as a faint ghost; focus shows content (feed levels, Bausperre, pockets); frames rise on hover and take their type look (colour + comb pattern) - full-bleed card with frosted overlays and glass buttons - Futtertasche (FRAME_FEEDER): new part kind occupying frame slots with a fill level, catalog row, validator rules in both ports, outline, sheets, follow-ups, settings Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
c608f5d405
commit
6e13236d49
100 files changed
+9432
-209
No files matched your search
@@ -37,10 +37,14 @@ const ROOF_OVERLAP_MM = 35;
|
||||
const BOX_WALL_MM = 20;
|
||||
const FRAME_THICKNESS_MM = 25;
|
||||
const FRAME_HEIGHT_MARGIN_MM = 10;
|
||||
const FLOOR_INSERT_LIFT_MM = 5;
|
||||
/** 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)];
|
||||
}
|
||||
@@ -52,11 +56,11 @@ function insertBox(
|
||||
): PartBox {
|
||||
const size = sizeOf(type);
|
||||
const hostBottom = host.center[1] - host.size[1] / 2;
|
||||
// FLOOR_INSERT lies just above the floor's bottom area (its centre y = floor
|
||||
// bottom + 5 mm, y is always a centre); every other insert
|
||||
// (the EVAPORATOR) stands on the bottom of the box it sits in.
|
||||
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" ? hostBottom + mm(FLOOR_INSERT_LIFT_MM) : hostBottom + size[1] / 2;
|
||||
type.kind === "FLOOR_INSERT" ? hostTop - FLOOR_GRID_DEPTH + size[1] / 2 : hostBottom + size[1] / 2;
|
||||
return {
|
||||
id: insert.id,
|
||||
partTypeId: insert.partTypeId,
|
||||
@@ -67,22 +71,42 @@ function insertBox(
|
||||
};
|
||||
}
|
||||
|
||||
function frameBoxes(part: HivePart, type: CatalogPartType, box: PartBox): FrameBox[] {
|
||||
const frames = part.frames ?? [];
|
||||
if (frames.length === 0) return [];
|
||||
/** 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);
|
||||
const pitch = inner / frames.length;
|
||||
const size: Vec3 = [
|
||||
mm(FRAME_THICKNESS_MM),
|
||||
mm(type.heightMm - FRAME_HEIGHT_MARGIN_MM),
|
||||
box.size[2] - 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: [-inner / 2 + pitch * (index + 0.5), box.center[1], 0],
|
||||
center: [left + pitch * (index + 0.5), box.center[1], 0],
|
||||
size,
|
||||
}));
|
||||
}
|
||||
@@ -116,11 +140,15 @@ export function hiveLayout(state: HiveState, catalog: HiveCatalog): HiveLayout {
|
||||
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) inserts.push(insertBox(insert, insertType, box));
|
||||
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, type, box));
|
||||
if (type.kind === "BOX") frames.push(...frameBoxes(part, box, grid));
|
||||
});
|
||||
|
||||
return { parts, inserts, frames, bounds: { w: width, h: maxTop, d: depth } };
|
||||
|
||||
Reference in new issue
Block a user