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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-16 09:12:03 +02:00
1 parent c608f5d405
commit 6e13236d49
100 files changed
+9432 -209

No files matched your search

+28 -3
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { hiveLayout } from "./layout";
import { FLOOR_GRID_DEPTH, hiveLayout } from "./layout";
import { inlineCatalog, part, templateState } from "./test-utils";
describe("hiveLayout", () => {
@@ -49,14 +49,16 @@ describe("hiveLayout", () => {
});
});
it("puts the floor insert 5 mm above the floor bottom and the evaporator on the box bottom", () => {
it("lays the floor insert on the floor's grid (40 mm below the floor top) and the evaporator on the box bottom", () => {
const state = templateState();
state.stack[0].inserts = [part("fi", "floor-insert")];
state.stack[1].inserts = [part("ev", "evaporator")];
const layout = hiveLayout(state, catalog);
const insert = layout.inserts.find((i) => i.id === "fi")!;
const evaporator = layout.inserts.find((i) => i.id === "ev")!;
expect(insert.center[1]).toBeCloseTo(0.05, 9);
// Floor top 1.2, grid 40 mm (0.4 units) below it, Bausperre 5 mm thick lying on the grid.
expect(insert.center[1]).toBeCloseTo(1.2 - FLOOR_GRID_DEPTH + 0.025, 9);
expect(FLOOR_GRID_DEPTH).toBeCloseTo(0.4, 9);
expect(insert.kind).toBe("FLOOR_INSERT");
expect(evaporator.center[1]).toBeCloseTo(1.2 + 0.6, 9);
expect(evaporator.stackIndex).toBe(1);
@@ -66,4 +68,27 @@ describe("hiveLayout", () => {
const layout = hiveLayout({ schemaVersion: 1, stack: [] }, catalog);
expect(layout).toEqual({ parts: [], inserts: [], frames: [], bounds: { w: 0, h: 0, d: 0 } });
});
it("lays a frame feeder over its slots with the frame height", () => {
const state = templateState();
const frames = state.stack[1].frames!;
frames[2] = { slot: 3, type: "NONE" };
frames[3] = { slot: 4, type: "NONE" };
state.stack[1].inserts = [part("ff", "frame-feeder", { slot: 3 })];
const layout = hiveLayout(state, catalog);
const pocket = layout.inserts.find((i) => i.id === "ff")!;
const inner = 5.25 - 0.4;
const pitch = inner / 10;
const box = layout.parts[1];
expect(pocket.kind).toBe("FRAME_FEEDER");
expect(pocket.stackIndex).toBe(1);
expect(pocket.size[0]).toBeCloseTo(2 * pitch, 9);
expect(pocket.size[1]).toBeCloseTo(2.2, 9);
expect(pocket.size[2]).toBeCloseTo(4.3 - 0.4, 9);
expect(pocket.center[0]).toBeCloseTo(-inner / 2 + pitch * 3, 9);
expect(pocket.center[1]).toBeCloseTo(box.center[1], 9);
// The pocket sits exactly where frames 3 and 4 would hang.
const covered = layout.frames.filter((f) => f.slot === 3 || f.slot === 4).map((f) => f.center[0]);
expect(pocket.center[0]).toBeCloseTo((covered[0] + covered[1]) / 2, 9);
});
});