Files
houseplan-card/test/resize-controller.test.mjs
2026-09-06 17:38:47 +03:00

236 lines
8.9 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { ResizeController } from '../test-build/resize-controller.js';
import {
resizeLiveCandidateSpace, resizeLiveJunctionRoomIds, resizeLiveRoomIds,
} from '../test-build/resize-live-preflight.js';
import { checkSpacePhysicalGeometry } from '../test-build/plan-geometry-preflight.js';
import { resolveSafeResize } from '../test-build/resize.js';
const room = () => ({
id: 'room', wall_ids: ['w0', 'w1', 'w2', 'w3'],
poly: [[100, 100], [400, 100], [400, 400], [100, 400]],
});
const options = { minDim: 25, eps: 0.5, step: 5 };
const setup = () => {
const controller = new ResizeController();
const rooms = [room()];
const resolution = resolveSafeResize(rooms, [], 'room', 1, options);
assert.equal(resolution.enabled, true);
const beforeWalls = [{ cm: 0 }, { cm: 15 }, { cm: 15 }];
assert.equal(controller.begin({
pointerId: 7,
start: [400, 250],
roomId: 'room',
plan: resolution.plan,
options,
rooms,
openings: [],
snapshotIdentity: 'snapshot-a',
before: { id: 'before' },
wallUnionBefore: { path: 'old' },
epochBefore: 42,
}), true);
return { controller, beforeWalls };
};
const move = (controller, beforeWalls, overrides = {}) => controller.move({
pointerId: 7,
point: [450, 250],
step: 5,
snap: (point) => point,
project: (_snapshot, polys) => ({
ok: true,
value: {
preview: { polys }, beforeWalls, afterWalls: [...beforeWalls], artifact: { path: 'new' },
},
}),
publish: () => {},
measure: () => ['50 cm'],
...overrides,
});
test('#264 controller owns latent selection and bounded eligibility cache', () => {
const controller = new ResizeController();
controller.selectRoom('room');
assert.equal(controller.escapeIdle(), 'selection-cleared');
assert.equal(controller.escapeIdle(), 'exit-tool');
let calls = 0;
const resolution = { enabled: false, reason: 'diagonal' };
assert.equal(controller.resolve('a', 'room:0', () => (++calls, resolution)), resolution);
assert.equal(controller.resolve('a', 'room:0', () => (++calls, resolution)), resolution);
assert.equal(calls, 1);
controller.resolve('b', 'room:0', () => (++calls, resolution));
assert.equal(calls, 2);
});
test('#264 accepted preview commits once and late/wrong pointer events are no-ops', () => {
const { controller, beforeWalls } = setup();
assert.deepEqual(move(controller, beforeWalls, { pointerId: 99 }), { kind: 'no-op' });
const accepted = move(controller, beforeWalls);
assert.equal(accepted.kind, 'accepted');
assert.deepEqual(controller.liveLabels, ['50 cm']);
const done = controller.finish({
pointerId: 7,
currentSnapshotIdentity: 'snapshot-a',
validatePreview: () => true,
});
assert.equal(done.kind, 'commit');
assert.deepEqual(done.before, { id: 'before' });
assert.deepEqual(controller.finish({
pointerId: 7, currentSnapshotIdentity: 'snapshot-a', validatePreview: () => true,
}), { kind: 'no-op' });
});
test('#264 failed projection retains the last complete preview and notifies once', () => {
const { controller, beforeWalls } = setup();
const accepted = move(controller, beforeWalls);
assert.equal(accepted.kind, 'accepted');
const preview = controller.preview;
const labels = controller.liveLabels;
const fail = { project: () => ({ ok: false, reason: 'wall-metadata' }) };
assert.deepEqual(move(controller, beforeWalls, { point: [460, 250], ...fail }), {
kind: 'rejected', notify: true,
});
assert.equal(controller.preview, preview);
assert.equal(controller.liveLabels, labels);
assert.deepEqual(move(controller, beforeWalls, { point: [460, 250], ...fail }), {
kind: 'rejected', notify: false,
});
});
test('#264 exact wall profile fails closed on preview and is rechecked on finish', () => {
const first = setup();
assert.deepEqual(move(first.controller, first.beforeWalls, {
project: (_snapshot, polys) => ({
ok: true,
value: {
preview: { polys }, beforeWalls: first.beforeWalls,
afterWalls: [{ cm: 0 }, { cm: 15 }], artifact: null,
},
}),
}), { kind: 'rejected', notify: true });
assert.equal(first.controller.preview, null);
const second = setup();
const afterWalls = [...second.beforeWalls];
assert.equal(move(second.controller, second.beforeWalls, {
project: (_snapshot, polys) => ({
ok: true,
value: { preview: { polys }, beforeWalls: second.beforeWalls, afterWalls, artifact: null },
}),
}).kind, 'accepted');
afterWalls.pop();
assert.deepEqual(second.controller.finish({
pointerId: 7,
currentSnapshotIdentity: 'snapshot-a',
validatePreview: () => true,
}), { kind: 'rejected', reason: 'wall-records' });
});
test('#264 cancel restores retained masonry only for the same snapshot', () => {
const same = setup().controller;
assert.deepEqual(same.cancel('snapshot-a'), {
kind: 'cancelled', restoreWallUnion: { path: 'old' }, restoreEpoch: 42,
});
assert.deepEqual(same.cancel('snapshot-a'), { kind: 'no-op' });
const stale = setup().controller;
assert.deepEqual(stale.cancel('snapshot-b'), {
kind: 'cancelled', restoreWallUnion: null, restoreEpoch: null,
});
});
test('#451 live resize preflight keeps only affected rooms at any plan size', () => {
const rect = (id, x, y, w = 1, h = 1) => ({
id, poly: [[x, y], [x + w, y], [x + w, y + h], [x, y + h]],
});
const rooms = Array.from({ length: 100 }, (_, index) =>
rect(`room-${index}`, index % 10, Math.floor(index / 10)));
assert.deepEqual(
resizeLiveRoomIds(rooms, ['room-44']),
['room-44'],
);
assert.deepEqual(resizeLiveRoomIds([
rect('changed', 0, 0, 2, 2), rect('nested', 0.5, 0.5, 0.5, 0.5),
rect('endpoint', 2, 2), rect('remote', 10, 10),
], ['changed']), ['changed']);
assert.deepEqual(resizeLiveJunctionRoomIds([
rect('changed', 0, 0, 2, 2), rect('nested', 0.5, 0.5, 0.5, 0.5),
rect('endpoint', 2, 2), rect('remote', 10, 10),
], ['changed']), ['changed', 'nested', 'endpoint']);
});
test('#451 live resize candidate keeps nearby physical bodies and excludes remote work', () => {
const rect = (id, x, y) => ({
id, poly: [[x, y], [x + 1, y], [x + 1, y + 1], [x, y + 1]],
});
const rooms = Array.from({ length: 100 }, (_, index) =>
rect(`room-${index}`, index % 10, Math.floor(index / 10)));
const candidate = resizeLiveCandidateSpace({
id: 'large', cell_cm: 5, rooms,
walls: [
{ id: 'near', key: '4.5,4.0@0.0000', a: [4, 4], b: [5, 4] },
{ id: 'remote', key: '90.5,90.0@0.0000', a: [90, 90], b: [91, 90] },
{ id: 'unknown-legacy-data', cm: 15 },
],
wall_segments: [
{ id: 'near', a: [4, 4], b: [5, 4] },
{ id: 'remote', a: [90, 90], b: [91, 90] },
],
open_spans: [
{ id: 'near', a: [4, 4], b: [5, 4] },
{ id: 'remote', a: [90, 90], b: [91, 90] },
],
partitions: [
{ id: 'near', a: [4.5, 4.5], b: [5, 5] },
{ id: 'remote', a: [90, 90], b: [91, 91] },
],
wall_columns: [
{ id: 'near', center: [4, 4.5], cm: 15 },
{ id: 'remote', center: [90, 90], cm: 15 },
],
openings: [
{ id: 'near', x: 4.5, y: 4, length: 1 },
{ id: 'remote', x: 90, y: 90, length: 1 },
],
}, ['room-44']);
assert.ok(candidate);
assert.equal(candidate.rooms.length, 1);
assert.deepEqual(candidate.walls.map((item) => item.id), ['near', 'unknown-legacy-data']);
assert.deepEqual(candidate.wall_segments.map((item) => item.id), ['near']);
assert.deepEqual(candidate.open_spans.map((item) => item.id), ['near']);
assert.deepEqual(candidate.partitions.map((item) => item.id), ['near']);
assert.deepEqual(candidate.wall_columns.map((item) => item.id), ['near']);
assert.deepEqual(candidate.openings.map((item) => item.id), ['near']);
});
test('#451 live resize physical preflight retains the adjacent room of a degraded wall', () => {
const fixture = JSON.parse(readFileSync(
new URL('./fixtures/278-wall-union-isolation.json', import.meta.url), 'utf8',
));
const baseSpace = fixture.config.spaces[0];
const remoteRooms = Array.from({ length: 80 }, (_, index) => {
const x = 10 + index * 2;
return {
id: `remote-${index}`,
poly: [[x, 10], [x + 1, 10], [x + 1, 11], [x, 11]],
};
});
const largeSpace = { ...baseSpace, rooms: [...baseSpace.rooms, ...remoteRooms] };
const full = checkSpacePhysicalGeometry({ spaces: [largeSpace] }, largeSpace.id);
assert.equal(full.ok, false);
assert.equal(full.reason, 'wall-degraded-extra');
const liveRoomIds = resizeLiveJunctionRoomIds(largeSpace.rooms, ['r1']);
assert.deepEqual(liveRoomIds, ['r1', 'r2']);
const liveSpace = resizeLiveCandidateSpace(largeSpace, liveRoomIds);
assert.ok(liveSpace);
assert.deepEqual(liveSpace.rooms.map((room) => room.id), ['r1', 'r2']);
const live = checkSpacePhysicalGeometry({ spaces: [liveSpace] }, liveSpace.id);
assert.equal(live.ok, false);
assert.equal(live.reason, 'wall-degraded-extra');
});