mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 22:29:05 +00:00
Merge issue #298 into dev
Owner-approved review exception: the external reviewer is unavailable. Both r1 High findings were fixed, the exact branch SHA passed all recorded gates, and generated bundles were rebuilt after conflict resolution with #296. Issue: #298 User-Visible: no
This commit is contained in:
@@ -76,6 +76,16 @@ test('#277 a lossy persistence rekey stops at the last complete preview', () =>
|
||||
assert.match(card, /g\.d = previousD/);
|
||||
});
|
||||
|
||||
test('#298 production carrier proof uses room edges, never independent partitions', () => {
|
||||
const carrierBlock = card.match(
|
||||
/const wallCarriers: \[number\[\], number\[\]\]\[\] = \[\];[\s\S]*?const wallSignature/,
|
||||
);
|
||||
assert.ok(carrierBlock, 'production wall-carrier preflight is missing');
|
||||
assert.match(carrierBlock[0], /for \(const room of sp\.rooms \|\| \[\]\)/);
|
||||
assert.doesNotMatch(carrierBlock[0], /sp\.partitions/,
|
||||
'independent partition geometry cannot carry space.walls metadata');
|
||||
});
|
||||
|
||||
test('#277 Resize render fingerprints geometry once for the whole handle layer', () => {
|
||||
assert.match(card, /const renderSnapshot = this\._rszDrag\?\.snap \|\| this\._rszSnapshot\(\)/);
|
||||
assert.match(card, /this\._rszResolution\(r\.id, i, renderSnapshot\)/);
|
||||
|
||||
@@ -4,6 +4,8 @@ import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import {
|
||||
wallKey, lookupWall, thicknessCmAt, degradeWalls, rekeyWallsAfterMove,
|
||||
rekeyWallsAfterMoveChecked,
|
||||
wallRecordsHaveCarrierCoverage,
|
||||
setWallThickness, setWallThicknessForRoom, applyWallThicknessToNewRoom,
|
||||
drawWallPreviewD, linearWallBody, linearWallJoinPatches,
|
||||
DRAW_WALL_DEFAULT_CM, clampWallCm, cmToField, fieldToCm,
|
||||
@@ -607,6 +609,192 @@ test('issue 293 moving a shared seam keeps one continuous side-wall record', ()
|
||||
assert.equal(bent.length, 2, 'meeting atoms with different directions must remain losslessly split');
|
||||
});
|
||||
|
||||
test('issue 298 fixed-topology rekey never scales an interior side-wall endpoint', () => {
|
||||
const wall = setWallThickness([], [-85, 304], [577, 304], 20, pitch, 1000);
|
||||
const next = rekeyWallsAfterMove(
|
||||
wall,
|
||||
[[[-100, 304], [100, 304]]],
|
||||
[[[-96, 304], [100, 304]]],
|
||||
pitch,
|
||||
1000,
|
||||
'fixed-topology',
|
||||
);
|
||||
assert.equal(next.length, 1);
|
||||
assert.deepEqual([next[0].a, next[0].b], [wall[0].a, wall[0].b]);
|
||||
assert.equal(next[0].key, wall[0].key);
|
||||
|
||||
const affine = rekeyWallsAfterMove(
|
||||
wall,
|
||||
[[[-100, 304], [100, 304]]],
|
||||
[[[-96, 304], [100, 304]]],
|
||||
pitch,
|
||||
1000,
|
||||
);
|
||||
assert.notDeepEqual(affine[0].a, wall[0].a,
|
||||
'regression fixture must kill the historical proportional mapping');
|
||||
});
|
||||
|
||||
test('issue 298 fixed-topology rekey translates every breakpoint of the moving wall', () => {
|
||||
const wall = setWallThickness([], [20, 0], [80, 0], 25, pitch, 1000);
|
||||
const next = rekeyWallsAfterMove(
|
||||
wall,
|
||||
[[[0, 0], [100, 0]]],
|
||||
[[[0, 5], [100, 5]]],
|
||||
pitch,
|
||||
1000,
|
||||
'fixed-topology',
|
||||
);
|
||||
assert.deepEqual([next[0].a, next[0].b], [[0.02, 0.005], [0.08, 0.005]]);
|
||||
assert.equal(next[0].cm, 25);
|
||||
});
|
||||
|
||||
test('issue 298 fixed-topology rekey preserves an unrelated exact record byte-semantically', () => {
|
||||
const wall = {
|
||||
key: 'compatibility-key-that-must-not-change', cm: 21,
|
||||
a: [0, 0], b: [0.1, 0],
|
||||
};
|
||||
const next = rekeyWallsAfterMove(
|
||||
[wall],
|
||||
[[[0, 200], [100, 200]]],
|
||||
[[[0, 205], [100, 205]]],
|
||||
pitch,
|
||||
1000,
|
||||
'fixed-topology',
|
||||
);
|
||||
assert.deepEqual(next, [wall]);
|
||||
});
|
||||
|
||||
test('issue 298 carrier proof covers collinear chains and rejects gaps or off-grid endpoints', () => {
|
||||
const exact = setWallThickness([], [0, 0], [100, 0], 20, pitch, 1000);
|
||||
assert.equal(wallRecordsHaveCarrierCoverage(
|
||||
exact, [[[0, 0], [40, 0]], [[40, 0], [100, 0]]], pitch, 1000,
|
||||
), true);
|
||||
assert.equal(wallRecordsHaveCarrierCoverage(
|
||||
exact, [[[0, 0], [40, 0]], [[41, 0], [100, 0]]], pitch, 1000,
|
||||
), false, 'endpoint-only validation must not accept a carrier gap');
|
||||
|
||||
const offGrid = [{ ...exact[0], a: [0.00001, 0] }];
|
||||
assert.equal(wallRecordsHaveCarrierCoverage(
|
||||
offGrid, [[[0.01, 0], [100, 0]]], pitch, 1000,
|
||||
), false, 'a true off-grid coordinate is not storage noise');
|
||||
assert.equal(wallRecordsHaveCarrierCoverage(
|
||||
offGrid, [[[0.01, 0], [100, 0]]], pitch, 1000, offGrid,
|
||||
), true, 'an identical historical endpoint is debt, not a new Resize coordinate');
|
||||
assert.equal(wallRecordsHaveCarrierCoverage(
|
||||
[{ ...offGrid[0], a: [0.00002, 0] }],
|
||||
[[[0.02, 0], [100, 0]]], pitch, 1000, offGrid,
|
||||
), false, 'Resize may not replace old debt with a different off-grid endpoint');
|
||||
});
|
||||
|
||||
test('issue 298 fixed-topology legacy records move only by unambiguous whole-edge identity', () => {
|
||||
const oldEdge = [[0, 0], [80, 0]];
|
||||
const newEdge = [[0, 0], [92, 0]];
|
||||
const whole = { key: wallKey(...oldEdge, pitch), cm: 22 };
|
||||
const moved = rekeyWallsAfterMoveChecked(
|
||||
[whole], [oldEdge], [newEdge], pitch, 1, 'fixed-topology',
|
||||
);
|
||||
assert.equal(moved.rejected, false);
|
||||
assert.deepEqual(moved.walls, [{ ...whole, key: wallKey(...newEdge, pitch) }]);
|
||||
|
||||
const renderSpaceWhole = {
|
||||
key: wallKey([1 / 480, 0], [80 - 1 / 480, 0], pitch), cm: 22,
|
||||
};
|
||||
const renderMoved = rekeyWallsAfterMoveChecked(
|
||||
[renderSpaceWhole], [oldEdge], [newEdge], pitch, 1000, 'fixed-topology',
|
||||
);
|
||||
assert.equal(renderMoved.rejected, false, 'historical render-space whole-edge key is unambiguous');
|
||||
assert.equal(renderMoved.walls[0].key, wallKey(...newEdge, pitch));
|
||||
|
||||
const untouched = { key: wallKey([100, 10], [120, 10], pitch), cm: 19, future: 'kept' };
|
||||
const untouchedResult = rekeyWallsAfterMoveChecked(
|
||||
[untouched], [oldEdge], [newEdge], pitch, 1, 'fixed-topology',
|
||||
);
|
||||
assert.equal(untouchedResult.rejected, false);
|
||||
assert.deepEqual(untouchedResult.walls, [untouched]);
|
||||
|
||||
const partial = { key: wallKey([10, 0], [30, 0], pitch), cm: 21 };
|
||||
const ambiguous = rekeyWallsAfterMoveChecked(
|
||||
[partial], [oldEdge], [newEdge], pitch, 1, 'fixed-topology',
|
||||
);
|
||||
assert.equal(ambiguous.rejected, true);
|
||||
assert.deepEqual(ambiguous.walls, [partial], 'a rejected candidate cannot partially rekey storage');
|
||||
|
||||
const conflictingWhole = rekeyWallsAfterMoveChecked(
|
||||
[whole], [oldEdge, oldEdge], [newEdge, [[0, 0], [96, 0]]],
|
||||
pitch, 1, 'fixed-topology',
|
||||
);
|
||||
assert.equal(conflictingWhole.rejected, true,
|
||||
'one whole-edge key with conflicting destinations must reject atomically');
|
||||
assert.deepEqual(conflictingWhole.walls, [whole]);
|
||||
|
||||
const exactWithBadCompatibilityKey = {
|
||||
key: 'stale-compatibility-key', cm: 24, a: [0, 0], b: [0.08, 0],
|
||||
};
|
||||
const exact = rekeyWallsAfterMoveChecked(
|
||||
[exactWithBadCompatibilityKey], [oldEdge], [newEdge], pitch, 1000, 'fixed-topology',
|
||||
);
|
||||
assert.equal(exact.rejected, false);
|
||||
assert.deepEqual(exact.walls[0].b, [0.092, 0]);
|
||||
assert.equal(exact.walls[0].key, wallKey([0, 0], [0.092, 0], pitch));
|
||||
});
|
||||
|
||||
test('issue 298 first-floor fixture keeps the room-b seam on 17/52/57/101 boundaries', () => {
|
||||
const fixture = JSON.parse(readFileSync(new URL(
|
||||
'./fixtures/real-plan-first-floor.json', import.meta.url,
|
||||
), 'utf8')).space;
|
||||
const changedIds = new Set(['room-b', 'room-g']);
|
||||
const nextRooms = fixture.rooms.map((room) => {
|
||||
const copy = structuredClone(room);
|
||||
if (copy.id === 'room-b') copy.poly[0][0] = copy.poly[1][0] = 52 / 240;
|
||||
if (copy.id === 'room-g') copy.poly[2][0] = copy.poly[3][0] = 52 / 240;
|
||||
return copy;
|
||||
});
|
||||
const oldSpans = [];
|
||||
const newSpans = [];
|
||||
for (const oldRoom of fixture.rooms.filter((room) => changedIds.has(room.id))) {
|
||||
const nextRoom = nextRooms.find((room) => room.id === oldRoom.id);
|
||||
const oldPoly = oldRoom.poly.map((point) => point.map((value) => value * 1000));
|
||||
const newPoly = nextRoom.poly.map((point) => point.map((value) => value * 1000));
|
||||
for (let index = 0; index < oldPoly.length; index++) {
|
||||
oldSpans.push([oldPoly[index], oldPoly[(index + 1) % oldPoly.length]]);
|
||||
newSpans.push([newPoly[index], newPoly[(index + 1) % newPoly.length]]);
|
||||
}
|
||||
}
|
||||
|
||||
const result = rekeyWallsAfterMoveChecked(
|
||||
fixture.walls, oldSpans, newSpans, pitch, 1000, 'fixed-topology',
|
||||
);
|
||||
assert.equal(result.rejected, false);
|
||||
const next = result.walls;
|
||||
const step = (value) => Number((value * 240).toFixed(6));
|
||||
const y155 = next.filter((wall) => wall.a && wall.b
|
||||
&& Math.abs(step(wall.a[1]) - 155) < 0.001
|
||||
&& Math.abs(step(wall.b[1]) - 155) < 0.001);
|
||||
const spans = y155.map((wall) => [step(wall.a[0]), step(wall.b[0])].sort((a, b) => a - b))
|
||||
.sort((a, b) => a[0] - b[0]);
|
||||
assert.deepEqual(spans, [[17, 57], [52, 101]]);
|
||||
assert.equal(spans.flat().some((value) => Math.abs(value - 59.538461) < 0.001), false);
|
||||
|
||||
const untouched = fixture.walls.find((wall) => wall.key === '0.479167,0.050000@0.0000');
|
||||
assert.ok(next.some((wall) => JSON.stringify(wall) === JSON.stringify(untouched)),
|
||||
'an unrelated first-floor record stays byte-semantic');
|
||||
|
||||
const carriers = nextRooms.flatMap((room) => room.poly.map((point, index) => [
|
||||
point.map((value) => value * 1000),
|
||||
room.poly[(index + 1) % room.poly.length].map((value) => value * 1000),
|
||||
]));
|
||||
assert.equal(wallRecordsHaveCarrierCoverage(
|
||||
y155, carriers, pitch, 1000, fixture.walls,
|
||||
), true, 'all surviving first-floor endpoints remain lattice/carrier safe');
|
||||
|
||||
const affine = rekeyWallsAfterMove(
|
||||
fixture.walls, oldSpans, newSpans, pitch, 1000, 'affine',
|
||||
);
|
||||
assert.equal(affine.some((wall) => [wall.a, wall.b].flat()
|
||||
.some((value) => Math.abs(step(value) - 59.538461) < 0.001)), true,
|
||||
'fixture must kill the historical proportional endpoint mapping');
|
||||
});
|
||||
|
||||
test('issue 253 key collisions never erase different exact or legacy records', () => {
|
||||
const exact = [
|
||||
{ key: wallKey([-1, 0], [1, 0], pitch), cm: 20, a: [-1, 0], b: [1, 0] },
|
||||
|
||||
Reference in New Issue
Block a user