- 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]>
95 lines
4.2 KiB
TypeScript
95 lines
4.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { FLOOR_GRID_DEPTH, hiveLayout } from "./layout";
|
|
import { inlineCatalog, part, templateState } from "./test-utils";
|
|
|
|
describe("hiveLayout", () => {
|
|
const catalog = inlineCatalog();
|
|
|
|
it("computes the template height with the roof overlapping by 35 mm", () => {
|
|
const layout = hiveLayout(templateState(), catalog);
|
|
expect(layout.bounds.h).toBeCloseTo(4.31, 9);
|
|
expect(layout.bounds.w).toBeCloseTo(5.25, 9);
|
|
expect(layout.bounds.d).toBeCloseTo(4.3, 9);
|
|
});
|
|
|
|
it("stacks parts on +y from 0 with centres and sizes in units of 100 mm", () => {
|
|
const [floor, box, cover] = hiveLayout(templateState(), catalog).parts;
|
|
expect(floor.center).toEqual([0, 0.6, 0]);
|
|
expect(floor.size).toEqual([5.25, 1.2, 4.3]);
|
|
expect(floor.stackIndex).toBe(0);
|
|
expect(box.center[1]).toBeCloseTo(1.2 + 1.15, 9);
|
|
expect(cover.center[1]).toBeCloseTo(3.5 + 0.005, 9);
|
|
expect(box.stackIndex).toBe(1);
|
|
});
|
|
|
|
it("lets the roof overlap the part below while using its own height", () => {
|
|
const layout = hiveLayout(templateState(), catalog);
|
|
const roof = layout.parts[layout.parts.length - 1];
|
|
const below = layout.parts[layout.parts.length - 2];
|
|
const belowTop = below.center[1] + below.size[1] / 2;
|
|
const roofBottom = roof.center[1] - roof.size[1] / 2;
|
|
expect(belowTop - roofBottom).toBeCloseTo(0.35, 9);
|
|
expect(roof.size[1]).toBeCloseTo(0.6, 9);
|
|
});
|
|
|
|
it("places 10 frames evenly across the inner width of the box", () => {
|
|
const layout = hiveLayout(templateState(), catalog);
|
|
expect(layout.frames).toHaveLength(10);
|
|
const inner = 5.25 - 0.4;
|
|
const xs = layout.frames.map((f) => f.center[0]);
|
|
expect(xs[0]).toBeCloseTo(-inner / 2 + inner / 20, 9);
|
|
expect(xs[9]).toBeCloseTo(inner / 2 - inner / 20, 9);
|
|
const pitch = inner / 10;
|
|
xs.slice(1).forEach((x, i) => expect(x - xs[i]).toBeCloseTo(pitch, 9));
|
|
layout.frames.forEach((f, i) => {
|
|
expect(f.boxId).toBe("b");
|
|
expect(f.slot).toBe(i + 1);
|
|
expect(f.size[0]).toBeCloseTo(0.25, 9);
|
|
expect(f.size[1]).toBeCloseTo(2.2, 9);
|
|
});
|
|
});
|
|
|
|
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")!;
|
|
// 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);
|
|
});
|
|
|
|
it("returns an empty layout for an empty stack", () => {
|
|
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);
|
|
});
|
|
});
|