mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
feat: добавить лестницы между этажами (#663)
Прямые и винтовые лестницы получили отдельную модель, инструменты редактора, безопасную межэтажную навигацию, вычитание из чистой площади и плоское отображение в 2.5D. Issue: #663 User-Visible: yes
This commit is contained in:
@@ -361,8 +361,14 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
|
||||
* нельзя положить внутрь охраняемого ею чанка. Геометрия и сам рендер остаются
|
||||
* ленивыми. Факт оставляет 953 Б сверху и 1 047 Б до нижней границы; общий
|
||||
* бюджет не меняется.
|
||||
*
|
||||
* 2026-09-26, #663: 296 000 → 299 700 (замер 298 722). Read-only геометрия
|
||||
* двух типов лестниц, межэтажный link hit-target и вычитание их footprint из
|
||||
* чистой площади нужны непосредственно в View. Placement, transform и
|
||||
* properties вынесены в lazy editor; общий бюджет 301 066 Б не меняется.
|
||||
* Центр оставляет 978 Б сверху и 1 022 Б до нижней границы полосы.
|
||||
*/
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 296_000;
|
||||
export const INITIAL_VIEW_GZIP_CEILING = 299_700;
|
||||
export const INITIAL_VIEW_CEILING_BAND = 2_000;
|
||||
|
||||
/**
|
||||
@@ -606,8 +612,14 @@ export const LAZY_FURNITURE_ART_GZIP_CEILING = 17_900;
|
||||
* контроллер хранит сессионные позиции, ограничивает их геометрией комнаты
|
||||
* и арбитрирует pointer/pinch/cancel. Центр оставляет 1 012 Б сверху и
|
||||
* 988 Б до нижней границы полосы.
|
||||
* - #663: 232 900 → 236 400 (замер 235 443). В ленивый редактор вошёл
|
||||
* отдельный runtime двух типов лестниц: placement, wall/stair magnet,
|
||||
* continuous move/resize/rotate, properties dialog и undo/redo. Read-only
|
||||
* символ и межэтажный переход отделены в eager runtime; первый View не
|
||||
* загружает эти editor-only ветки. Центр оставляет 957 Б сверху и 1 043 Б
|
||||
* до нижней границы полосы.
|
||||
*/
|
||||
export const LAZY_EDITOR_GZIP_CEILING = 232_900;
|
||||
export const LAZY_EDITOR_GZIP_CEILING = 236_400;
|
||||
export const LAZY_GRAPH_CEILING_BAND = 2_000;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1379,6 +1379,76 @@
|
||||
"required": false,
|
||||
"type": "float"
|
||||
},
|
||||
"config.spaces[].stairs": {
|
||||
"required": false
|
||||
},
|
||||
"config.spaces[].stairs[]": {
|
||||
"allowExtra": true
|
||||
},
|
||||
"config.spaces[].stairs[].angle": {
|
||||
"max": 360.0,
|
||||
"min": -360.0,
|
||||
"opaque": "<function _finite>",
|
||||
"required": true
|
||||
},
|
||||
"config.spaces[].stairs[].direction": {
|
||||
"enum": [
|
||||
"backward",
|
||||
"clockwise",
|
||||
"counterclockwise",
|
||||
"forward"
|
||||
],
|
||||
"required": true
|
||||
},
|
||||
"config.spaces[].stairs[].id": {
|
||||
"maxLength": 64,
|
||||
"minLength": 1,
|
||||
"required": true,
|
||||
"type": "str"
|
||||
},
|
||||
"config.spaces[].stairs[].kind": {
|
||||
"enum": [
|
||||
"spiral",
|
||||
"straight"
|
||||
],
|
||||
"required": true
|
||||
},
|
||||
"config.spaces[].stairs[].length": {
|
||||
"max": 5000.0,
|
||||
"min": 1e-07,
|
||||
"opaque": "<function _finite>",
|
||||
"required": false
|
||||
},
|
||||
"config.spaces[].stairs[].radius": {
|
||||
"max": 5000.0,
|
||||
"min": 1e-07,
|
||||
"opaque": "<function _finite>",
|
||||
"required": false
|
||||
},
|
||||
"config.spaces[].stairs[].target_space_id": {
|
||||
"nullable": true,
|
||||
"opaque": "Match('^[a-z0-9_-]{1,64}$', msg=None)",
|
||||
"required": false,
|
||||
"type": "str"
|
||||
},
|
||||
"config.spaces[].stairs[].width": {
|
||||
"max": 5000.0,
|
||||
"min": 1e-07,
|
||||
"opaque": "<function _finite>",
|
||||
"required": false
|
||||
},
|
||||
"config.spaces[].stairs[].x": {
|
||||
"max": 5000.0,
|
||||
"min": -5000.0,
|
||||
"opaque": "<function _finite>",
|
||||
"required": true
|
||||
},
|
||||
"config.spaces[].stairs[].y": {
|
||||
"max": 5000.0,
|
||||
"min": -5000.0,
|
||||
"opaque": "<function _finite>",
|
||||
"required": true
|
||||
},
|
||||
"config.spaces[].title": {
|
||||
"required": true,
|
||||
"type": "str"
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"hostRefs": 4880,
|
||||
"portPrivates": 94,
|
||||
"harnessPrivates": 100,
|
||||
"bundleBytes": 2540346
|
||||
"bundleBytes": 2567812
|
||||
}
|
||||
|
||||
@@ -72,6 +72,91 @@ function relocateEditorPatch(patch, cardSource, editorSource) {
|
||||
// `find` обязан встречаться в файле ровно один раз: патч, который ложится «куда
|
||||
// попало», проверяет не то, что объявлен проверять. Это контролирует --check.
|
||||
const MUTANT_DEFINITIONS = [
|
||||
{
|
||||
id: 'stairs-continuous-transform-snaps-to-lattice',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/coordinate-canonicalization.test.mjs',
|
||||
because: '#663 AC2/AC9/AC11: a wall-face magnet is continuous and save/load or Optimize '
|
||||
+ 'must not pull the stair back to a grid node.',
|
||||
patches: [{
|
||||
file: 'src/coordinate-canonicalization.ts',
|
||||
find: " scalarFields(stair, ['x', 'y', 'length', 'width', 'radius', 'angle']);\n",
|
||||
replace: " latticeFields(stair, ['x', 'y', 'length', 'width', 'radius', 'angle']); // mutant\n",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-area-does-not-subtract-footprints',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/stairs.test.mjs',
|
||||
because: '#663 AC4: stair footprints reduce the clean room area once while leaving the '
|
||||
+ 'visible floor polygon intact.',
|
||||
patches: [{
|
||||
file: 'src/stairs.ts',
|
||||
find: ' return difference(source, ...footprints);\n',
|
||||
replace: ' return source; // mutant: footprints no longer affect clean area\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-view-pan-opens-target-floor',
|
||||
guard: 'node demo/smoke_stairs.mjs',
|
||||
because: '#663 AC6: a pan or pinch beginning on a stair must not be promoted to the '
|
||||
+ 'compatibility click that navigates to another floor.',
|
||||
patches: [{
|
||||
file: 'src/stairs-view.ts',
|
||||
find: ' if (!active || this.owner._suppressClick || this.suppressClick\n',
|
||||
replace: ' if (!active || this.suppressClick\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-fixed-floor-still-navigates',
|
||||
guard: 'node demo/smoke_stairs.mjs',
|
||||
because: '#663 AC8: fixed-floor cards show stairs but never promise or perform navigation.',
|
||||
patches: [{
|
||||
file: 'src/stairs-editor-model.ts',
|
||||
find: " if (fixedFloor) return 'fixed';\n",
|
||||
replace: ' if (false && fixedFloor) return \'fixed\'; // mutant: fixed-floor guard removed\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-link-auto-creates-target-object',
|
||||
guard: 'node demo/smoke_stairs.mjs',
|
||||
because: '#663 AC5: assigning a target is a one-way link and must not invent a mirrored '
|
||||
+ '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'
|
||||
+ ' 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'
|
||||
+ ' const mirror = this.owner._model.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-tread-count-depends-on-render-scale',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/stairs.test.mjs',
|
||||
because: '#663 AC3: 30 cm is a physical interval; viewport scale may change pixels but '
|
||||
+ 'must not change the number or placement of treads.',
|
||||
patches: [{
|
||||
file: 'src/stairs.ts',
|
||||
find: ' const treadN = cmToNorm(STAIR_TREAD_CM, cellCm);\n',
|
||||
replace: ' const treadN = cmToNorm(STAIR_TREAD_CM, cellCm) * NORM_W / scale; // mutant\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-backend-allows-251-items',
|
||||
guard: 'node scripts/backend-test-guard.mjs '
|
||||
+ 'issue_663_stair_schema_is_discriminated_bounded_and_forward_compatible '
|
||||
+ 'tests_backend/test_validation.py',
|
||||
because: '#663 AC13: persisted stair collections are bounded at 250 before render and '
|
||||
+ 'support/diagnostic paths can receive them.',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/validation.py',
|
||||
find: ' vol.Optional("stairs"): vol.All([STAIR_SCHEMA], vol.Length(max=MAX_STAIRS)),\n',
|
||||
replace: ' vol.Optional("stairs"): vol.All([STAIR_SCHEMA], vol.Length(max=MAX_STAIRS + 1)), # mutant\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'acl-readonly-group-becomes-writer',
|
||||
guard: 'node scripts/backend-test-guard.mjs '
|
||||
@@ -6648,6 +6733,7 @@ const MUTANT_DEFINITIONS = [
|
||||
file: 'src/space-render.ts',
|
||||
find: ' ${passageGlowTunnels}\n'
|
||||
+ ' <g class="decorlayer" pointer-events="none">${decorImages}</g>\n'
|
||||
+ ' <g class="hp-stairs-layer" pointer-events="none">${stairShapes}</g>\n'
|
||||
+ ' ${glowPools}\n ${wallUnion',
|
||||
replace: ' ${passageGlowTunnels}\n'
|
||||
+ ' ${!space.bg && !disp.showNames ? svg`<g class="room-svg-labels" pointer-events="none">${space.rooms.map((room) => {\n'
|
||||
@@ -6656,6 +6742,7 @@ const MUTANT_DEFINITIONS = [
|
||||
+ ' data-area=${room.area || nothing} x=${center[0]} y=${center[1]}>${room.name}</text>`;\n'
|
||||
+ ' })}</g>` : nothing}\n'
|
||||
+ ' <g class="decorlayer" pointer-events="none">${decorImages}</g>\n'
|
||||
+ ' <g class="hp-stairs-layer" pointer-events="none">${stairShapes}</g>\n'
|
||||
+ ' ${glowPools}\n'
|
||||
+ ' ${wallUnion',
|
||||
}],
|
||||
|
||||
Reference in New Issue
Block a user