mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
210 lines
8.5 KiB
JavaScript
210 lines
8.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import {
|
|
checkSpacePhysicalGeometry,
|
|
checkOptimizeGeometry,
|
|
prepareSpacePhysicalGeometryInputs,
|
|
spacePhysicalGeometryFingerprint,
|
|
} from '../test-build/plan-geometry-preflight.js';
|
|
import { GRID_STEP_N, spaceModels } from '../test-build/space-geometry.js';
|
|
import { contentFingerprint } from '../test-build/visual-continuity.js';
|
|
import { wallKey } from '../test-build/wall-thickness.js';
|
|
|
|
const clone = (value) => structuredClone(value);
|
|
const room = (id = 'room') => ({
|
|
id,
|
|
poly: [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]],
|
|
});
|
|
const wall = (cm = 20) => ({
|
|
key: wallKey([0.1, 0.1], [0.9, 0.1], GRID_STEP_N),
|
|
a: [0.1, 0.1], b: [0.9, 0.1], cm,
|
|
});
|
|
const base = (spaces) => ({ model_version: 6, spaces, markers: [], settings: {} });
|
|
|
|
test('Optimize geometry preflight covers the production input matrix without mutating config', () => {
|
|
const config = base([
|
|
{
|
|
id: 'room-wall', title: 'Room wall', view_box: [0, 0, 1, 1], cell_cm: 5,
|
|
rooms: [room()], walls: [wall()],
|
|
openings: [{
|
|
id: 'ordinary', type: 'door', x: 0.5, y: 0.1,
|
|
angle: 0, length: 0.1,
|
|
}],
|
|
},
|
|
{
|
|
id: 'hosted', title: 'Hosted', view_box: [0, 0, 1, 1], cell_cm: 5,
|
|
rooms: [room('host-room')], walls: [wall(18)],
|
|
partitions: [{ id: 'partition', a: [0.1, 0.1], b: [0.9, 0.1], cm: 15 }],
|
|
openings: [{
|
|
id: 'hosted-opening', type: 'passage', x: 0, y: 0,
|
|
angle: 0, length: 0.1,
|
|
host: { kind: 'partition', id: 'partition', t: 0.5 },
|
|
}],
|
|
},
|
|
{
|
|
id: 'physical-only', title: 'Physical only', view_box: [0, 0, 1, 1], cell_cm: 5,
|
|
rooms: [],
|
|
partitions: [{ id: 'p', a: [0.1, 0.2], b: [0.8, 0.2], cm: 15 }],
|
|
room_drafts: [{
|
|
id: 'draft', points: [[0.1, 0.4], [0.8, 0.4]], segments: [{ cm: 12 }],
|
|
}],
|
|
wall_columns: [{ id: 'column', shape: 'square', center: [0.5, 0.6], cm: 20 }],
|
|
},
|
|
{
|
|
id: 'image-only', title: 'Image only', view_box: [0, 0, 1, 1],
|
|
plan_url: '/local/plan.png', rooms: [],
|
|
},
|
|
{ id: 'empty', title: 'Empty', view_box: [0, 0, 1, 1], rooms: [] },
|
|
{ id: 'floor-only', title: 'Floor only', view_box: [0, 0, 1, 1], rooms: [room('floor')] },
|
|
]);
|
|
const before = clone(config);
|
|
|
|
const result = checkOptimizeGeometry(config);
|
|
|
|
assert.deepEqual(config, before);
|
|
assert.equal(result.fingerprint, contentFingerprint(config));
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(result.spaces.map(({ spaceId, status }) => ({ spaceId, status })), [
|
|
{ spaceId: 'room-wall', status: 'ok' },
|
|
{ spaceId: 'hosted', status: 'ok' },
|
|
{ spaceId: 'physical-only', status: 'ok' },
|
|
{ spaceId: 'image-only', status: 'not-applicable' },
|
|
{ spaceId: 'empty', status: 'not-applicable' },
|
|
{ spaceId: 'floor-only', status: 'ok' },
|
|
]);
|
|
});
|
|
|
|
test('production preparation resolves ordinary and hosted openings once for masonry and bodies', () => {
|
|
const raw = {
|
|
id: 'parity', title: 'Parity', view_box: [0, 0, 1, 1], cell_cm: 5,
|
|
rooms: [room()], walls: [wall()],
|
|
partitions: [{ id: 'partition', a: [0.1, 0.1], b: [0.9, 0.1], cm: 15 }],
|
|
openings: [
|
|
{ id: 'ordinary', type: 'door', x: 0.3, y: 0.1, angle: 0, length: 0.08 },
|
|
{
|
|
id: 'wall-hosted', type: 'window', x: 0.5, y: 0.1, angle: 0, length: 0.08,
|
|
host: { kind: 'wall', id: 'wall-segment', t: 0.5 },
|
|
},
|
|
{
|
|
id: 'hosted', type: 'passage', x: 0, y: 0, angle: 0, length: 0.08,
|
|
host: { kind: 'partition', id: 'partition', t: 0.7 },
|
|
},
|
|
{
|
|
id: 'orphan', type: 'door', x: 0.4, y: 0.4, angle: 0, length: 0.08,
|
|
host: { kind: 'partition', id: 'missing', t: 0.5 },
|
|
},
|
|
],
|
|
};
|
|
const model = spaceModels(base([raw]))[0];
|
|
const input = prepareSpacePhysicalGeometryInputs(raw, model);
|
|
|
|
assert.deepEqual(input.openings.map((opening) => opening.id), ['ordinary', 'wall-hosted', 'hosted']);
|
|
assert.equal(input.partitionCuts.length, 1);
|
|
assert.equal(input.roomOpenings.length, 3, 'wall hosts remain room cuts; coincident partition hosts cut masonry too');
|
|
assert.ok(input.physicalBodies.length >= 2, 'partition and its opening jamb bodies are retained');
|
|
assert.equal(input.wallKeyPitch, GRID_STEP_N);
|
|
assert.equal(input.cellCm, 5);
|
|
});
|
|
|
|
test('null, exceptions and floor failure are bounded while successful empty geometry stays green', () => {
|
|
const wallConfig = base([{
|
|
id: 'wall-space', title: 'Wall space', view_box: [0, 0, 1, 1],
|
|
rooms: [room()], walls: [wall()],
|
|
}]);
|
|
const floorConfig = base([{
|
|
id: 'floor-space', title: 'Floor space', view_box: [0, 0, 1, 1], rooms: [room()],
|
|
}]);
|
|
|
|
const wallNull = checkOptimizeGeometry(wallConfig, { wallPass: () => null });
|
|
assert.equal(wallNull.ok, false);
|
|
assert.equal(wallNull.failures[0].reason, 'wall-null');
|
|
|
|
const wallThrows = checkOptimizeGeometry(wallConfig, { wallPass: () => { throw new Error('secret'); } });
|
|
assert.equal(wallThrows.failures[0].reason, 'wall-exception');
|
|
assert.doesNotMatch(JSON.stringify(wallThrows), /secret/);
|
|
// #295: the detail field carries the error CLASS only — diagnosable without
|
|
// breaking the privacy contract above.
|
|
assert.equal(wallThrows.failures[0].detail, 'Error');
|
|
|
|
const successfulEmpty = checkOptimizeGeometry(wallConfig, {
|
|
wallPass: () => ({ geom: [], paperGeom: [], depthUnits: 0, openingIndex: null }),
|
|
floorPass: () => { throw new Error('must not run'); },
|
|
});
|
|
assert.equal(successfulEmpty.ok, true);
|
|
|
|
const floorNull = checkOptimizeGeometry(floorConfig, { floorPass: () => null });
|
|
assert.equal(floorNull.failures[0].reason, 'floor-null');
|
|
const floorThrows = checkOptimizeGeometry(floorConfig, {
|
|
floorPass: () => { throw new Error('private floor detail'); },
|
|
});
|
|
assert.equal(floorThrows.failures[0].reason, 'floor-exception');
|
|
assert.doesNotMatch(JSON.stringify(floorThrows), /private floor detail/);
|
|
|
|
const prepareThrows = checkOptimizeGeometry(wallConfig, {
|
|
prepareSpace: () => { throw new Error('private preparation detail'); },
|
|
});
|
|
assert.equal(prepareThrows.failures[0].reason, 'prepare-exception');
|
|
assert.doesNotMatch(JSON.stringify(prepareThrows), /private preparation detail/);
|
|
});
|
|
|
|
test('#278 strict one-space barrier rejects degraded render-safe geometry', () => {
|
|
const config = base([{
|
|
id: 'strict', title: 'Strict', view_box: [0, 0, 1, 1],
|
|
rooms: [room()], walls: [wall()],
|
|
}]);
|
|
let captured = null;
|
|
const accepted = checkSpacePhysicalGeometry(config, 'strict', {
|
|
captureWallGeometry: (input, geometry) => { captured = { input, geometry }; },
|
|
});
|
|
assert.equal(accepted.ok, true);
|
|
assert.equal(captured?.input?.space?.id, 'strict');
|
|
assert.equal(captured?.geometry?.status, 'ok');
|
|
assert.ok(captured?.geometry?.multiWallNodes);
|
|
|
|
const degraded = checkSpacePhysicalGeometry(config, 'strict', {
|
|
wallPass: () => ({
|
|
status: 'degraded-extra', geom: [], components: [], roomGeom: [], paperGeom: [],
|
|
depthUnits: 0, openingIndex: null, degradedExtraCount: 1,
|
|
}),
|
|
});
|
|
assert.equal(degraded.ok, false);
|
|
assert.equal(degraded.reason, 'wall-degraded-extra');
|
|
assert.equal(degraded.fingerprint, spacePhysicalGeometryFingerprint(config.spaces[0]));
|
|
|
|
const failedCore = checkSpacePhysicalGeometry(config, 'strict', {
|
|
wallPass: () => ({
|
|
status: 'failed-core', geom: [], components: [], roomGeom: [], paperGeom: [],
|
|
depthUnits: 0, openingIndex: null, degradedExtraCount: 0,
|
|
}),
|
|
});
|
|
assert.equal(failedCore.reason, 'wall-failed-core');
|
|
|
|
const missing = checkSpacePhysicalGeometry(config, 'missing');
|
|
assert.equal(missing.ok, false);
|
|
assert.equal(missing.reason, 'prepare-exception');
|
|
});
|
|
|
|
test('one failed space blocks the ordered whole-plan result and uses safe display fallbacks', () => {
|
|
const config = base([
|
|
{ id: 'good', title: ' Good floor ', view_box: [0, 0, 1, 1], rooms: [] },
|
|
{ id: 'bad-id', title: ' ', view_box: [0, 0, 1, 1], rooms: [room()] },
|
|
{ id: '', title: '', view_box: [0, 0, 1, 1], rooms: [room()] },
|
|
]);
|
|
const result = checkOptimizeGeometry(config, {
|
|
fallbackSpaceName: (index) => `Fallback ${index}`,
|
|
floorPass: (rooms) => rooms[0]?.id === 'room' ? null : [],
|
|
});
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.deepEqual(result.spaces.map((space) => space.displayName), [
|
|
'Good floor', 'bad-id', 'Fallback 3',
|
|
]);
|
|
assert.deepEqual(result.failures.map(({ spaceId, displayName, reason }) => ({
|
|
spaceId, displayName, reason,
|
|
})), [
|
|
{ spaceId: 'bad-id', displayName: 'bad-id', reason: 'floor-null' },
|
|
{ spaceId: '', displayName: 'Fallback 3', reason: 'floor-null' },
|
|
]);
|
|
});
|