mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
fix: переписать слой редактирования лестниц на box-контракт декора (#676)
Лестница рисуется протяжкой, как прямоугольник или эллипс декора: длинная ось протяжки — ось подъёма, стрелка смотрит от нажатия к отпусканию, клик ставит размер по умолчанию. Выделенная лестница получает рамку декора в верхнем оверлее карточки — выше тел стен, с узлами постоянного экранного размера и курсорами по мировому направлению узла; винтовая — четыре узла на касательных. Ресайз идёт от противоположной стороны, не зеркалит и не поворачивает; магнит работает по сторонам: в пределах 6 клеток и 5° сторона ложится на видимую грань стены, при перемещении лестница доворачивается не больше чем на 5°, при ресайзе и черновике — никогда. Грани перегородок и колонн получают внешнюю сторону по обходу многоугольника. Клик, синтезированный после жеста, глушится: под «Лестницей» больше не появляется копия, под «Выбором» не пропадает выделение. Диалог свойств считает в сантиметрах лестницы (30…10000), а не в конвертере толщины стены; нетронутое поле хранит число бит в бит, «Сохранить» без изменений ничего не пишет. В Просмотре наведение на лестницу с корректной целью показывает «Переход на этаж …» — ровно при том же условии, что делает её ссылкой. Чистые функции слоя — в stairs-editor-model.ts с юнитами; смок лестниц покрывает протяжку, курсоры, рамку над стенами, клик после жеста, диалог и пять состояний подсказки; реестр мутантов получил шесть свидетелей. Issue: #676 User-Visible: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
@@ -136,14 +136,94 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'stair on the destination floor.',
|
||||
patches: [{
|
||||
file: 'src/stairs-editor.ts',
|
||||
find: ' this.write(this.stairs.map((item) => item.id === next.id ? next : item));\n'
|
||||
find: ' this.replace(next);\n'
|
||||
+ ' this.owner._recordGeometry(this.owner._t(\'history.stair_edit\'), before);\n',
|
||||
replace: ' this.write(this.stairs.map((item) => item.id === next.id ? next : item));\n'
|
||||
replace: ' this.replace(next);\n'
|
||||
+ ' const mirror = (this.owner as unknown as { _serverCfg: { spaces: Array<{ id: string; stairs?: Stair[] }> } })._serverCfg.spaces.find((item) => item.id === dialog.targetSpaceId);\n'
|
||||
+ ' if (mirror) mirror.stairs = [...(mirror.stairs || []), { ...next, id: `${next.id}-mirror` }];\n'
|
||||
+ ' this.owner._recordGeometry(this.owner._t(\'history.stair_edit\'), before);\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-handle-cursor-ignores-bearing',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/stairs-editor-model.test.mjs',
|
||||
because: '#676 AC4: the cursor over a handle must say which way it moves in world terms, '
|
||||
+ 'so a rotated stair cannot show one diagonal arrow on every node.',
|
||||
patches: [{
|
||||
file: 'src/stairs-editor-model.ts',
|
||||
find: " if (sector === 0 || sector === 4) return 'ew';\n",
|
||||
replace: " if (sector >= 0) return 'nwse'; // mutant: one cursor for every handle\n",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-dialog-clamps-to-wall-thickness',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/stairs-editor-model.test.mjs',
|
||||
because: '#676 AC5: the properties dialog converts stair sizes, not wall thickness; a '
|
||||
+ '100 cm ceiling silently shrinks every real flight on save.',
|
||||
patches: [{
|
||||
file: 'src/stairs-editor-model.ts',
|
||||
find: ' return cm >= STAIR_MIN_CM && cm <= STAIR_MAX_CM ? cm : null;\n',
|
||||
replace: ' return Math.max(1, Math.min(100, cm)); // mutant: the wall-thickness clamp is back\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-resize-mirrors-past-anchor',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/stairs-editor-model.test.mjs',
|
||||
because: '#676 AC2: a pointer dragged past the anchor side clamps at the minimum size; '
|
||||
+ 'mirroring would silently move the ascent to the other side of the anchor.',
|
||||
patches: [{
|
||||
file: 'src/stairs-editor-model.ts',
|
||||
find: ' let w = handle.sx ? Math.max(minimum, handle.sx * lx + box.w / 2) : box.w;\n',
|
||||
replace: ' let w = handle.sx ? Math.max(minimum, Math.abs(handle.sx * lx + box.w / 2)) : box.w; // mutant: mirrors\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-move-magnet-turns-any-angle',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/stairs-editor-model.test.mjs',
|
||||
because: '#676 AC3: the move magnet may align a nearly parallel side by the tolerance only; '
|
||||
+ 'turning a stair standing at 30° to the wall changes its orientation behind the user.',
|
||||
patches: [{
|
||||
file: 'src/stairs-editor-model.ts',
|
||||
find: ' const snap = snapEdgeToFaces(edge, surfaces, reach, angleTolDeg);\n'
|
||||
+ ' if (snap && (!best || Math.abs(snap.offset) < Math.abs(best.snap.offset))) best = { edge, snap };\n',
|
||||
replace: ' const snap = snapEdgeToFaces(edge, surfaces, reach, 89); // mutant: any angle snaps\n'
|
||||
+ ' if (snap && (!best || Math.abs(snap.offset) < Math.abs(best.snap.offset))) best = { edge, snap };\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-gesture-click-reaches-plan-tool',
|
||||
guard: 'node demo/smoke_stairs.mjs',
|
||||
because: '#676 AC6: the click the browser synthesizes after a handle drag must not reach '
|
||||
+ 'the plan tool: under Stairs it placed a copy, under Select it dropped the selection.',
|
||||
patches: [{
|
||||
file: 'src/stairs-editor.ts',
|
||||
find: ' this.owner._suppressClick = true;\n'
|
||||
+ ' setTimeout(() => { this.owner._suppressClick = false; }, 0);\n',
|
||||
replace: ' // mutant: the click is free to reach the stage\n',
|
||||
}, {
|
||||
file: 'src/stairs-editor.ts',
|
||||
find: ' @pointerdown=${(event: PointerEvent) => this.pointerDown(event, stair, \'resize\', { sx: handle.sx, sy: handle.sy })}\n'
|
||||
+ ' @click=${stop}></circle>\n',
|
||||
replace: ' @pointerdown=${(event: PointerEvent) => this.pointerDown(event, stair, \'resize\', { sx: handle.sx, sy: handle.sy })}></circle>\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-view-tooltip-ignores-target-state',
|
||||
guard: 'node demo/smoke_stairs.mjs',
|
||||
because: '#676 AC8: the hover tooltip has exactly the link condition; a missing, self, '
|
||||
+ 'deleted or fixed-floor target must never announce a floor to go to.',
|
||||
patches: [{
|
||||
file: 'src/stairs-view.ts',
|
||||
find: ' const tip = (event: PointerEvent): void => {\n'
|
||||
+ ' if (!active) return;\n',
|
||||
replace: ' const tip = (event: PointerEvent): void => {\n'
|
||||
+ ' // mutant: every stair announces something\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-tread-count-depends-on-render-scale',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
|
||||
Reference in New Issue
Block a user