feat: junction limits refuse the write in every editing surface (#329)

П3 measures the WALL, not the catalogue atom: a short filler segment that
compensates a thickness step (owner's fixture, 5 cm = (30-20)/2) is a legal
continuation of a long same-thickness wall, so the rule walks the maximal
collinear run through the shared nodes before judging the length.

Resize stops at the last allowed position and names the broken rule instead
of the generic "geometry cannot be saved"; the Thickness dialog refuses
through its own toast. Both channels are pinned by demo/smoke_junction_limits
plus three mutants (angle threshold, write barrier, degenerate apex bevel).

Issue: #329
User-Visible: yes
This commit is contained in:
Codex
2026-08-27 23:31:42 +03:00
parent 7d15bd06e9
commit f514fb27fe
28 changed files with 592 additions and 185 deletions
+43 -1
View File
@@ -537,6 +537,43 @@ export const MUTANTS = [
replace: ' // mutant: pointer capture removed\n const plan = resolution.plan;',
}],
},
{
id: 'junction-limit-angle-not-enforced',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="П1" test/junction-limits.test.mjs',
because: 'the owner-approved 15° minimum between neighbouring walls is a hard limit; '
+ 'lowering it silently lets the trident apex of #329 be drawn again',
patches: [{
file: 'src/junction-limits.ts',
find: 'export const MIN_JUNCTION_ANGLE_DEG = 15;',
replace: 'export const MIN_JUNCTION_ANGLE_DEG = 0;',
}],
},
{
id: 'junction-limit-write-gate-removed',
guard: 'node demo/smoke_junction_limits.mjs',
because: 'the limits must refuse the WRITE, not merely report it — without the barrier '
+ 'an impossible junction reaches the saved document (#329 §2)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' if (introduced.length) {\n this._clearGeometryGesture();',
replace: ' if (false && introduced.length) {\n this._clearGeometryGesture();',
}],
},
{
id: 'degenerate-apex-bevelled-again',
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test --test-name-pattern="§4" test/junction-limits.test.mjs',
because: 'a sharp apex must end in ONE point on both faces; the two-point bevel is the '
+ 'flat cut the owner rejected and the bow-tie fold that carved the jags (#329 §4)',
patches: [{
file: 'src/wall-thickness.ts',
find: ' if (isDegenerateApexCorner(poly, offsets, i)) {\n'
+ ' out.push([poly[i][0], poly[i][1]]);\n'
+ ' continue;\n }',
replace: ' // mutant: degenerate apex falls back to the bevel',
}],
},
{
id: 'resize-preview-reject-silent',
guard: 'node demo/smoke_room_resize.mjs',
@@ -544,7 +581,12 @@ export const MUTANTS = [
+ 'stopped instead of restoring the original silent no-op (#293)',
patches: [{
file: 'src/houseplan-card.ts',
find: " this._showToast(this._t('resize.preview_failed'));",
find: [
' this._showToast(this._rszLimitViolation',
' ? `${this._t(\'resize.limit_stopped\')} — `',
' + this._junctionLimitLabel(this._rszLimitViolation)',
" : this._t('resize.preview_failed'));",
].join('\n'),
replace: ' // mutant: reject remains silent',
}],
},