fix: preserve legacy resize lattice endpoints

Issue: #298
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-24 22:41:14 +03:00
parent 3fb2edc904
commit 2cad797d2c
9 changed files with 266 additions and 244 deletions
File diff suppressed because one or more lines are too long
+112 -112
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -149,8 +149,8 @@ vertex; an interior thickness breakpoint stays on its physical boundary rather
than keeping a proportional fraction of the new edge. Unrelated exact records
remain byte-equivalent. The candidate then proves that every new exact record
is lattice-safe and continuously covered by room-wall/partition carriers. An
unchanged historical violation may remain readable, but Resize cannot add or
replace it with a different violation.
unchanged historical endpoint may remain readable even when its record changes
around it, but Resize cannot add or replace it with a different violation.
When the two owners of a shared moving seam split one physically continuous
side-wall record at their meeting point, the mapped atoms are joined back only
+4 -3
View File
@@ -71,9 +71,10 @@ projection for side walls. A rigidly translated moving edge carries every
breakpoint by the same vector; a side edge that only changes length moves its
paired topology endpoint and leaves interior thickness boundaries fixed. A
continuous carrier-coverage and lattice proof runs before preview/commit. It
compares exact historical debt by record identity, so an old off-grid record is
not silently migrated, while a new or changed violation rejects the whole
candidate. This is distinct from the retained generic scale/rotation helper
compares exact historical debt by record identity and endpoint identity, so an
old off-grid endpoint is not silently migrated even when its record's other end
moves, while a new or changed violation rejects the whole candidate. This is
distinct from the retained generic scale/rotation helper
used only by isolated historical pure tests.
## 2. Growth (centreline ±½)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 290 KiB

+12 -12
View File
@@ -2,7 +2,7 @@
"version": 1,
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"sourceFingerprint": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceFingerprint": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -14,7 +14,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "5ba555c1f46416ce17e40ef2b6ecd9eb6e1d0ce6b8d467acd0682af95bd36307"
},
"view-touch": {
@@ -25,7 +25,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "deaae14893e66337b42e45ccfe87cf16f571cfc10f98da4250a6a6589a092a43"
},
"space-create": {
@@ -36,7 +36,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "c33a7279165a4cec6fa6fadb6fd08cd967e082a17fe101ef442d27d36ae59b6b"
},
"room-contour-close": {
@@ -47,7 +47,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "29b57b4e38cc332849bf7198f709b9af4f6f4a3044fb042308d181ffe2ae1eda"
},
"plan-context-tray": {
@@ -58,7 +58,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "b0398943d8a7c8215159d2d54b895c28feb1be8e303e65ca106ea5185c34644e"
},
"device-editor": {
@@ -69,7 +69,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "c3704489b36fd4177e761124fac032d8d09d6a94e3110f9a77d40ec110c1590b"
},
"device-display-preview": {
@@ -80,7 +80,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "7241478b3fae2a5b1895f241bc50a65f1aaacce907d311cc87796a251c389a8b"
},
"background-editor": {
@@ -91,8 +91,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"imageSha256": "4f40aef13801fda2749f9b42e1db56993756e4517d2a06265d40a26743913b78"
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "8241b09b0d821247a86eaadd6043a37dde29623c870d8a142bcd381c638a306d"
},
"room-card": {
"file": "08-room-card.png",
@@ -102,7 +102,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
},
"device-info": {
@@ -113,7 +113,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "c3a3a4e0cf22bec8f48da581241ac553747a0c3cfe5db34e5c8be9bad4240561",
"sourceSha256": "662c04e08745a510fd710d98780c3960471546776bf6dfd66c1529bd892a0826",
"imageSha256": "8e65ff2191ad1f1172848418c8381967ecff2cc831653a8f9c3ae11352a0e84b"
}
}
+1 -1
View File
@@ -8600,7 +8600,7 @@ class HouseplanCard extends LitElement {
else changedWalls.push(wall);
}
if (wallRecordCarrierViolations(
changedWalls, wallCarriers, this._wallKeyPitch, NORM_W,
changedWalls, wallCarriers, this._wallKeyPitch, NORM_W, s.walls || [],
).length) return { ok: false, reason: 'wall-metadata' };
if (!this._rszSpaceCandidateRenderable(this._space, sp)) {
return { ok: false, reason: 'physical-geometry' };
+16 -2
View File
@@ -777,6 +777,7 @@ export function wallRecordCarrierViolations(
carriers: [number[], number[]][],
pitch: number,
coordScale = 1,
latticeDebt: WallEntry[] | null | undefined = [],
): string[] {
const scale = coordScale > 0 ? coordScale : 1;
const latticePitch = Math.abs(pitch);
@@ -787,6 +788,16 @@ export function wallRecordCarrierViolations(
const steps = normalised / latticePitch;
return Math.abs(steps - Math.round(steps)) < LATTICE_NOISE_STEPS;
};
// A Resize may need to rewrite a record whose other endpoint was authored
// off-grid historically. That coordinate is not newly produced by Resize:
// allow it only when the exact same physical endpoint already existed in the
// immutable source snapshot. Carrier coverage is still proved below.
const oldEndpoints = (latticeDebt || []).flatMap((wall) => {
const span = entrySpan(wall, scale);
return span ? span.map((point) => [...point]) : [];
});
const pointIsLatticeSafe = (point: number[]): boolean => point.every((value, axis) =>
onLattice(value) || oldEndpoints.some((old) => Math.abs(value - old[axis]) <= eps));
const violations: string[] = [];
const signature = (wall: WallEntry): string => JSON.stringify([
@@ -797,7 +808,7 @@ export function wallRecordCarrierViolations(
if (!span) continue;
const [a, b] = span;
if (![a[0], a[1], b[0], b[1]].every(Number.isFinite)
|| ![a[0], a[1], b[0], b[1]].every(onLattice)) {
|| !pointIsLatticeSafe(a) || !pointIsLatticeSafe(b)) {
violations.push(signature(wall));
continue;
}
@@ -838,8 +849,11 @@ export function wallRecordsHaveCarrierCoverage(
carriers: [number[], number[]][],
pitch: number,
coordScale = 1,
latticeDebt: WallEntry[] | null | undefined = [],
): boolean {
return wallRecordCarrierViolations(walls, carriers, pitch, coordScale).length === 0;
return wallRecordCarrierViolations(
walls, carriers, pitch, coordScale, latticeDebt,
).length === 0;
}
/** Upsert or remove a wall entry by endpoints. */
+7
View File
@@ -676,6 +676,13 @@ test('issue 298 carrier proof covers collinear chains and rejects gaps or off-gr
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 253 key collisions never erase different exact or legacy records', () => {