fix: sync live wall thickness preview

Issue: #234
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-08-22 16:49:29 +03:00
parent f82141cd23
commit 7105e645d9
11 changed files with 67 additions and 36 deletions
File diff suppressed because one or more lines are too long
+6 -5
View File
@@ -7,7 +7,8 @@
* no recorded thickness — the shape a mid-edit toolbar field used to produce —
* and then reads three things that used to disagree: the committed partitions,
* what the Thickness tool reports for the same stretch, and what a resumed
* legacy draft carries.
* legacy draft carries. A missing live tail uses the current field; only an
* internal historical gap inherits the preceding segment.
*/
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
@@ -39,8 +40,8 @@ const res = await page.evaluate(async () => {
[100 + 12 * g, 100 + 6 * g],
];
// Two recorded segments at 30 and 25 cm, then a third point whose thickness
// is missing: exactly what a field left mid-edit produced before #234.
// Two recorded segments at 30 and 25 cm, then a live third segment whose
// record does not exist yet. The current field is 30 cm.
c._drawWallField = '30';
await upd();
out.fieldAcceptedThirty = c._drawWallCm === 30;
@@ -61,8 +62,8 @@ const res = await page.evaluate(async () => {
out.storedThreeSegments = stored.length === 3;
out.storedFirstThirty = stored[0] === 30;
out.storedSecondTwentyFive = stored[1] === 25;
// The gap inherits the previous segment, never the global default.
out.gapInheritedPrevious = stored[2] === 25;
// A live missing tail uses the current field, not the previous segment.
out.liveTailUsesCurrentField = stored[2] === 30;
out.nothingFellBackToFifteen = !stored.includes(15);
// The Thickness tool reads the same value it stored: the mismatch between
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -59,8 +59,10 @@
edited between segments — used to be previewed at the drawn value and saved at
15 cm, and the plan only revealed it later when the Thickness tool reported a
number nobody had chosen. Preview, both writers and the Thickness highlight now
read one resolver, and a missing record inherits the previous segment of the
same chain ([#234](https://github.com/Matysh/houseplan-card/issues/234)).
read one resolver: a historical missing record inherits the previous segment,
while the live rubber-band uses the current field value and therefore no longer
changes thickness on click
([#234](https://github.com/Matysh/houseplan-card/issues/234)).
- Wall hatching no longer changes density with the space's grid scale. The same
15 cm wall used to look different on plans with different `cell_cm` — from a
+3 -2
View File
@@ -69,8 +69,9 @@
отрезками, — показывался с нарисованной толщиной, а сохранялся с 15 см, и
расхождение обнаруживалось позже: инструмент «Толщина» подсвечивал значение,
которого никто не выбирал. Теперь превью, оба пути записи и подсветка читают
один резолвер, а пропуск наследует толщину предыдущего отрезка той же цепочки
([#234](https://github.com/Matysh/houseplan-card/issues/234)).
один резолвер: старый пропуск наследует толщину предыдущего отрезка, а live
rubber-band использует текущее значение поля и больше не меняет толщину при
клике ([#234](https://github.com/Matysh/houseplan-card/issues/234)).
- Плотность штриховки стен больше не зависит от масштаба пространства. Раньше
одна и та же стена 15 см выглядела на планах с разным `cell_cm` по-разному —
Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 342 KiB

+12 -12
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"fixture": "synthetic-only",
"sourceFingerprint": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceFingerprint": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"captureScriptSha256": "34f2219790d46efd8250e7a1bd829cb8fc0b0547e1260635fefa52407551b41b",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -13,7 +13,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "2885f96e348b15ab7c696e56e99bddcd9bb2ee94218a883e5a2155f45f5042aa"
},
"view-touch": {
@@ -24,7 +24,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "f62d8af3617c00a5e99511bd765980d2e27badf25047a1be34046b64195abe6c"
},
"space-create": {
@@ -35,7 +35,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "c33a7279165a4cec6fa6fadb6fd08cd967e082a17fe101ef442d27d36ae59b6b"
},
"room-contour-close": {
@@ -46,8 +46,8 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"imageSha256": "4d63670c7bcca33da21a6cf17275786bed12e8422a50d02d4dabe59645f2cd82"
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "3d22e1a2a96823e443833e40ea8b93747099b0fae1e9e19315d8789ae4604997"
},
"plan-context-tray": {
"file": "05-plan-context-tray.png",
@@ -57,7 +57,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "a6c526fede11bc3503fd2384bd4a3f6afa008f481c5c47578f06cae6c81c6f9b"
},
"device-editor": {
@@ -68,7 +68,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "9585b59add4d35b5a6f028ce5b720ef1b77a3f8b19436d192cd1a0e6fc637264"
},
"device-display-preview": {
@@ -79,7 +79,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "cfc317da4628d079a116ff71311fbf06b1eb3b181928a5ea7ba888ab936a5e8b"
},
"background-editor": {
@@ -90,7 +90,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "d7cfe70551d9260169df8efd832e32ed7df99c7f60b55d1fd4a8322bd4c47175"
},
"room-card": {
@@ -101,7 +101,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
},
"device-info": {
@@ -112,7 +112,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "967bdb37e447ec29a88d82707f5ff957e576be72fbe801445c6cbdec970e43eb",
"sourceSha256": "2d8295e95a0721f11075dd0388ab4960c79b480ae199a381d3bbd1086e5e5b34",
"imageSha256": "a06cbf83f09e2f67b3566d7c0b10e973db060c3d20786f26f74ada6a21937c3e"
}
}
+16 -2
View File
@@ -50,8 +50,22 @@ export const MUTANTS = [
+ 'that silently replaced 30 cm with 15 cm in the stored plan (#234)',
patches: [{
file: 'src/wall-face-graph.ts',
find: ' const cm = own ?? previous ?? fallbackTail;',
replace: ' const cm = own ?? fallbackTail;',
find: ' const inherited = previous ?? active ?? fallback;',
replace: ' const inherited = active ?? fallback;',
}],
},
{
id: 'chain-thickness-live-tail-inherits-previous',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="live rubber-band" '
+ 'test/wall-face-graph.test.mjs',
because: 'the last missing thickness belongs to the live rubber-band, so a field change '
+ 'between clicks must preview the value that the click will commit instead of inheriting '
+ 'the preceding segment (#234)',
patches: [{
file: 'src/wall-face-graph.ts',
find: ' const liveTail = active ?? previous ?? fallback;',
replace: ' const liveTail = previous ?? active ?? fallback;',
}],
},
{
+10 -5
View File
@@ -65,9 +65,11 @@ export function normalizeUnifiedWallTool(value: unknown): unknown {
* the owner discovered it much later by hovering a wall. Two formulas for one
* meaning always drift; there is exactly one here now.
*
* A missing record inherits the previous segment of the same chain, then the
* toolbar field, then the default (owner's decision 2026-08-21): that is what
* the person saw on screen while drawing, and a global default is not.
* A missing record inside a committed chain inherits the previous segment,
* then the toolbar field, then the default (owner's decision 2026-08-21).
* The final missing record is different: it is the live rubber-band, so the
* toolbar field must win before the previous segment. Otherwise changing the
* field between clicks previews the old thickness and commits the new one.
*
* Strictly positive is the validity boundary. The previous `wallChainSegments`
* accepted a recorded zero, which cannot be drawn through the UI (1..100 cm,
@@ -89,12 +91,15 @@ export function chainSegmentCms(
// ровно тем дублированием, которое задача и убирает. Невалидный default —
// дефект вызывающего, поэтому он приводится к минимальной допустимой
// толщине (1 см, docs/WALL-THICKNESS.md), а не к выдуманному значению.
const fallbackTail = valid(activeCm) ?? valid(defaultCm) ?? 1;
const active = valid(activeCm);
const fallback = valid(defaultCm) ?? 1;
const out: number[] = [];
let previous: number | null = null;
for (let i = 0; i < count; i++) {
const own = valid(recorded?.[i]);
const cm = own ?? previous ?? fallbackTail;
const inherited = previous ?? active ?? fallback;
const liveTail = active ?? previous ?? fallback;
const cm = own ?? (i === count - 1 ? liveTail : inherited);
out.push(cm);
previous = cm;
}
+10 -2
View File
@@ -55,17 +55,25 @@ test('chainSegmentCms fills a gap from the previous segment, then the field (#23
// потеряна. Раньше запись давала 15, превью — 30; теперь ответ один.
assert.deepEqual(chainSegmentCms(3, [30, 30], 30, 15), [30, 30, 30]);
// Дырка в середине наследует предыдущий, а не следующий и не дефолт.
assert.deepEqual(chainSegmentCms(4, [30, undefined, 20], 12, 15), [30, 30, 20, 20]);
assert.deepEqual(chainSegmentCms(3, [30, undefined, 20], 12, 15), [30, 30, 20]);
// Нет ни одной записи — текущее поле.
assert.deepEqual(chainSegmentCms(2, [], 22, 15), [22, 22]);
// Нет и поля — дефолт вызывающего.
assert.deepEqual(chainSegmentCms(2, [], null, 15), [15, 15]);
});
test('chainSegmentCms gives the live rubber-band the current field value (#234)', () => {
// The last missing record is not historical damage: it is the segment under
// the cursor. A field change between clicks must be visible before commit.
assert.deepEqual(chainSegmentCms(2, [12], 24, 15), [12, 24]);
// With no active field, a missing tail remains a historical gap and inherits.
assert.deepEqual(chainSegmentCms(3, [30, undefined], null, 15), [30, 30, 30]);
});
test('chainSegmentCms treats only strictly positive records as valid (#234)', () => {
// Прежняя wallChainSegments считала записанный 0 валидным; через UI ноль
// недостижим (1..100 см), но в старом черновике лежать может.
assert.deepEqual(chainSegmentCms(3, [0, 30, 0], 25, 15), [25, 30, 30]);
assert.deepEqual(chainSegmentCms(3, [0, 30, 0], 25, 15), [25, 30, 25]);
assert.deepEqual(chainSegmentCms(4, [NaN, -5, null, 'x'], 18, 15), [18, 18, 18, 18]);
assert.deepEqual(chainSegmentCms(2, [Infinity, 40], 18, 15), [18, 40]);
});