@pascal-app/core 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/events/bus.d.ts +30 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
- package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
- package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +27 -1
- package/dist/hooks/scene-registry/scene-registry.test.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
- package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +93 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +69 -12
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +476 -195
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +19 -1
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- package/dist/lib/level-name.d.ts +4 -0
- package/dist/lib/level-name.d.ts.map +1 -0
- package/dist/lib/level-name.js +10 -0
- package/dist/lib/measurement-geometry.d.ts +19 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.js +232 -0
- package/dist/lib/measurement-geometry.test.d.ts +2 -0
- package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.test.js +102 -0
- package/dist/lib/polygon-geometry.d.ts +0 -1
- package/dist/lib/polygon-geometry.d.ts.map +1 -1
- package/dist/lib/polygon-geometry.js +0 -18
- package/dist/lib/selection-proxy.d.ts +4 -0
- package/dist/lib/selection-proxy.d.ts.map +1 -0
- package/dist/lib/selection-proxy.js +12 -0
- package/dist/lib/slab-polygon.d.ts +31 -2
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +525 -31
- package/dist/lib/slab-polygon.test.d.ts +2 -0
- package/dist/lib/slab-polygon.test.d.ts.map +1 -0
- package/dist/lib/slab-polygon.test.js +635 -0
- package/dist/lib/slots.d.ts +14 -0
- package/dist/lib/slots.d.ts.map +1 -0
- package/dist/lib/slots.js +26 -0
- package/dist/lib/space-detection.d.ts +25 -2
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +414 -118
- package/dist/lib/space-detection.test.js +301 -2
- package/dist/lib/wall-distance.d.ts +60 -0
- package/dist/lib/wall-distance.d.ts.map +1 -0
- package/dist/lib/wall-distance.js +85 -0
- package/dist/lib/zone-quantities.d.ts +21 -0
- package/dist/lib/zone-quantities.d.ts.map +1 -0
- package/dist/lib/zone-quantities.js +183 -0
- package/dist/lib/zone-quantities.test.d.ts +2 -0
- package/dist/lib/zone-quantities.test.d.ts.map +1 -0
- package/dist/lib/zone-quantities.test.js +82 -0
- package/dist/material-library.d.ts +20 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +2181 -441
- package/dist/registry/handles.d.ts +75 -2
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -3
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/registry.d.ts +46 -3
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +82 -1
- package/dist/registry/registry.test.js +14 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +4 -0
- package/dist/registry/subtree.test.js +30 -0
- package/dist/registry/types.d.ts +717 -38
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +21 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +17 -3
- package/dist/schema/material.d.ts +3 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +7 -5
- package/dist/schema/material.test.d.ts +2 -0
- package/dist/schema/material.test.d.ts.map +1 -0
- package/dist/schema/material.test.js +43 -0
- package/dist/schema/nodes/box-vent.d.ts +2 -2
- package/dist/schema/nodes/cabinet.d.ts +534 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -0
- package/dist/schema/nodes/cabinet.js +137 -0
- package/dist/schema/nodes/ceiling.d.ts +3 -2
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +4 -0
- package/dist/schema/nodes/chimney.d.ts +4 -4
- package/dist/schema/nodes/column.d.ts +3 -2
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +3 -0
- package/dist/schema/nodes/cupola.d.ts +2 -2
- package/dist/schema/nodes/door.d.ts +13 -5
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +12 -0
- package/dist/schema/nodes/dormer.d.ts +8 -8
- package/dist/schema/nodes/dormer.js +1 -1
- package/dist/schema/nodes/downspout.d.ts +2 -2
- package/dist/schema/nodes/duct-fitting.d.ts +91 -0
- package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/duct-fitting.js +92 -0
- package/dist/schema/nodes/duct-segment.d.ts +61 -0
- package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
- package/dist/schema/nodes/duct-segment.js +73 -0
- package/dist/schema/nodes/duct-terminal.d.ts +57 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
- package/dist/schema/nodes/duct-terminal.js +51 -0
- package/dist/schema/nodes/elevator.d.ts +3 -2
- package/dist/schema/nodes/elevator.d.ts.map +1 -1
- package/dist/schema/nodes/elevator.js +3 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
- package/dist/schema/nodes/fence.d.ts +18 -2
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +25 -2
- package/dist/schema/nodes/gutter.d.ts +2 -2
- package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
- package/dist/schema/nodes/hvac-equipment.js +55 -0
- package/dist/schema/nodes/item.d.ts +8 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +11 -0
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +2 -0
- package/dist/schema/nodes/lineset.d.ts +42 -0
- package/dist/schema/nodes/lineset.d.ts.map +1 -0
- package/dist/schema/nodes/lineset.js +38 -0
- package/dist/schema/nodes/liquid-line.d.ts +34 -0
- package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
- package/dist/schema/nodes/liquid-line.js +24 -0
- package/dist/schema/nodes/measurement.d.ts +306 -0
- package/dist/schema/nodes/measurement.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.js +87 -0
- package/dist/schema/nodes/measurement.test.d.ts +2 -0
- package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.test.js +139 -0
- package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
- package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-fitting.js +45 -0
- package/dist/schema/nodes/pipe-segment.d.ts +48 -0
- package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-segment.js +36 -0
- package/dist/schema/nodes/pipe-trap.d.ts +43 -0
- package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-trap.js +36 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +2 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
- package/dist/schema/nodes/ridge-vent.d.ts +18 -3
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
- package/dist/schema/nodes/ridge-vent.js +221 -3
- package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
- package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.js +318 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-trim.test.js +107 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.js +330 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
- package/dist/schema/nodes/roof-segment.d.ts +84 -8
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +290 -5
- package/dist/schema/nodes/roof.d.ts +8 -8
- package/dist/schema/nodes/shelf.d.ts +3 -2
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +6 -0
- package/dist/schema/nodes/skylight.d.ts +10 -10
- package/dist/schema/nodes/slab.d.ts +3 -2
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +4 -0
- package/dist/schema/nodes/solar-panel.d.ts +4 -4
- package/dist/schema/nodes/stair-segment.d.ts +4 -4
- package/dist/schema/nodes/stair.d.ts +13 -12
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +3 -0
- package/dist/schema/nodes/turbine-vent.d.ts +2 -2
- package/dist/schema/nodes/wall.d.ts +245 -6
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +257 -0
- package/dist/schema/nodes/wall.test.d.ts +2 -0
- package/dist/schema/nodes/wall.test.d.ts.map +1 -0
- package/dist/schema/nodes/wall.test.js +208 -0
- package/dist/schema/nodes/window.d.ts +11 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +12 -0
- package/dist/schema/nodes/zone.d.ts +2 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +6 -0
- package/dist/schema/scene-material.d.ts +41 -0
- package/dist/schema/scene-material.d.ts.map +1 -0
- package/dist/schema/scene-material.js +9 -0
- package/dist/schema/types.d.ts +1089 -94
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +23 -0
- package/dist/services/alignment-anchors.d.ts.map +1 -1
- package/dist/services/alignment-anchors.js +30 -1
- package/dist/services/alignment.d.ts +5 -0
- package/dist/services/alignment.d.ts.map +1 -1
- package/dist/services/alignment.js +2 -0
- package/dist/services/hosting.d.ts +8 -0
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +13 -4
- package/dist/services/hosting.test.js +32 -1
- package/dist/services/index.d.ts +8 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +8 -2
- package/dist/services/level-height.d.ts +28 -0
- package/dist/services/level-height.d.ts.map +1 -0
- package/dist/services/level-height.js +62 -0
- package/dist/services/opening-guides.d.ts +141 -0
- package/dist/services/opening-guides.d.ts.map +1 -0
- package/dist/services/opening-guides.js +267 -0
- package/dist/services/opening-guides.test.d.ts +2 -0
- package/dist/services/opening-guides.test.d.ts.map +1 -0
- package/dist/services/opening-guides.test.js +208 -0
- package/dist/services/port-connectivity.d.ts +115 -0
- package/dist/services/port-connectivity.d.ts.map +1 -0
- package/dist/services/port-connectivity.js +300 -0
- package/dist/services/port-connectivity.test.d.ts +2 -0
- package/dist/services/port-connectivity.test.d.ts.map +1 -0
- package/dist/services/port-connectivity.test.js +297 -0
- package/dist/services/riser-diagram.d.ts +40 -0
- package/dist/services/riser-diagram.d.ts.map +1 -0
- package/dist/services/riser-diagram.js +110 -0
- package/dist/services/riser-diagram.test.d.ts +2 -0
- package/dist/services/riser-diagram.test.d.ts.map +1 -0
- package/dist/services/riser-diagram.test.js +66 -0
- package/dist/services/snap.d.ts +9 -1
- package/dist/services/snap.d.ts.map +1 -1
- package/dist/services/snap.js +21 -1
- package/dist/services/snap.test.js +43 -1
- package/dist/services/system-graph.d.ts +30 -0
- package/dist/services/system-graph.d.ts.map +1 -0
- package/dist/services/system-graph.js +164 -0
- package/dist/services/system-graph.test.d.ts +2 -0
- package/dist/services/system-graph.test.d.ts.map +1 -0
- package/dist/services/system-graph.test.js +138 -0
- package/dist/services/validate-dwv.d.ts +32 -0
- package/dist/services/validate-dwv.d.ts.map +1 -0
- package/dist/services/validate-dwv.js +118 -0
- package/dist/services/validate-dwv.test.d.ts +2 -0
- package/dist/services/validate-dwv.test.d.ts.map +1 -0
- package/dist/services/validate-dwv.test.js +109 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +94 -1
- package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
- package/dist/store/actions/ridge-vent-update.test.js +264 -0
- package/dist/store/history-control.d.ts +11 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +33 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
- package/dist/store/use-scene-dirty-tracking.test.js +68 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
- package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
- package/dist/store/use-scene-plugins.test.d.ts +2 -0
- package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
- package/dist/store/use-scene-plugins.test.js +51 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
- package/dist/store/use-scene-window-migration.test.d.ts +2 -0
- package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-window-migration.test.js +85 -0
- package/dist/store/use-scene.d.ts +17 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +358 -51
- package/dist/systems/elevator/elevator-service.js +1 -1
- package/dist/systems/fence/fence-centerline.d.ts +12 -0
- package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
- package/dist/systems/fence/fence-centerline.js +37 -0
- package/dist/systems/fence/fence-spline.d.ts +39 -0
- package/dist/systems/fence/fence-spline.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.js +196 -0
- package/dist/systems/fence/fence-spline.test.d.ts +2 -0
- package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.test.js +136 -0
- package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
- package/dist/systems/stair/stair-footprint.js +21 -11
- package/dist/systems/stair/stair-opening-preview.d.ts +1139 -469
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +1 -18
- package/dist/systems/stair/stair-opening-sync.test.js +39 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +38 -2
- package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
- package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-mitering.test.js +63 -0
- package/dist/utils/clone-scene-graph.d.ts +1 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +23 -3
- package/dist/utils/clone-scene-graph.test.js +3 -0
- package/dist/utils/heal-scene-graph.d.ts +18 -0
- package/dist/utils/heal-scene-graph.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.js +90 -0
- package/dist/utils/heal-scene-graph.test.d.ts +2 -0
- package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.test.js +88 -0
- package/dist/validation/validate-build-json.d.ts +1 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +31 -2
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stair-opening-preview.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC7E,OAA6B,EAAE,KAAK,iBAAiB,EAAE,MAAM,qCAAqC,CAAA;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAGpE,KAAK,oBAAoB,GAAG;IAC1B,EAAE,EAAE,SAAS,CAAA;IACb,IAAI,EAAE,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC,CAAA;CACtC,CAAA;AAoBD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,WAYvC;AAED,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC
|
|
1
|
+
{"version":3,"file":"stair-opening-preview.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC7E,OAA6B,EAAE,KAAK,iBAAiB,EAAE,MAAM,qCAAqC,CAAA;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAGpE,KAAK,oBAAoB,GAAG;IAC1B,EAAE,EAAE,SAAS,CAAA;IACb,IAAI,EAAE,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC,CAAA;CACtC,CAAA;AAoBD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,WAYvC;AAED,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwBvC;AAED,wBAAgB,qCAAqC;;mBAUlC,oBAAoB,EAAE;;EAiCxC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stair-opening-sync.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-sync.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,WAAW,EACX,QAAQ,EAIT,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"stair-opening-sync.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-sync.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,WAAW,EACX,QAAQ,EAIT,MAAM,cAAc,CAAA;AA0qBrB,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;QAQvC,SAAS;UAAQ,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IAqG5E"}
|
|
@@ -329,19 +329,6 @@ function getSpiralOpeningPolygon(stair, offset = 0) {
|
|
|
329
329
|
return toWorldPlanPoint(stair, Math.cos(angle) * radius, Math.sin(angle) * radius);
|
|
330
330
|
});
|
|
331
331
|
}
|
|
332
|
-
function getSpiralLandingPolygon(stair, offset = 0) {
|
|
333
|
-
const width = Math.max(stair.width ?? 1, 0.4);
|
|
334
|
-
const outerRadius = Math.max(0.05, (stair.innerRadius ?? 0.9) + width);
|
|
335
|
-
const depth = Math.max(stair.topLandingDepth ?? 0.9, 0.1);
|
|
336
|
-
const halfWidth = width / 2;
|
|
337
|
-
const localPoints = [
|
|
338
|
-
[outerRadius - offset, -halfWidth - offset],
|
|
339
|
-
[outerRadius + depth + offset, -halfWidth - offset],
|
|
340
|
-
[outerRadius + depth + offset, halfWidth + offset],
|
|
341
|
-
[outerRadius - offset, halfWidth + offset],
|
|
342
|
-
];
|
|
343
|
-
return localPoints.map(([x, z]) => toWorldPlanPoint(stair, x, z));
|
|
344
|
-
}
|
|
345
332
|
function getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation, openingOffsetOverride) {
|
|
346
333
|
const layouts = getStraightStairLayouts(stair, nodes);
|
|
347
334
|
if (layouts.length === 0)
|
|
@@ -413,11 +400,7 @@ function getStairOpeningPolygons(stair, nodes, targetElevation, openingOffsetOve
|
|
|
413
400
|
}
|
|
414
401
|
if (stair.stairType === 'spiral') {
|
|
415
402
|
const offset = Math.max(openingOffset - STAIR_SLAB_OPENING_TIGHTENING, 0);
|
|
416
|
-
|
|
417
|
-
if (stair.topLandingMode === 'integrated') {
|
|
418
|
-
polygons.push(getSpiralLandingPolygon(stair, offset));
|
|
419
|
-
}
|
|
420
|
-
return polygons;
|
|
403
|
+
return [getSpiralOpeningPolygon(stair, offset)];
|
|
421
404
|
}
|
|
422
405
|
if (typeof targetElevation === 'number') {
|
|
423
406
|
return getStraightOpeningPolygonsForSurface(stair, nodes, targetElevation, openingOffset);
|
|
@@ -443,4 +443,43 @@ describe('syncAutoStairOpenings', () => {
|
|
|
443
443
|
expect(landingUpdate?.data.holes).toEqual([manualOpening]);
|
|
444
444
|
expect(landingUpdate?.data.holeMetadata).toEqual([{ source: 'manual' }]);
|
|
445
445
|
});
|
|
446
|
+
test('does not add a separate rectangular hole for an integrated spiral top landing', () => {
|
|
447
|
+
const building = BuildingNode.parse({ name: 'Building' });
|
|
448
|
+
const ground = LevelNode.parse({ name: 'Ground', level: 0, parentId: building.id });
|
|
449
|
+
const upper = LevelNode.parse({ name: 'Upper', level: 1, parentId: building.id });
|
|
450
|
+
const landingSlab = SlabNode.parse({
|
|
451
|
+
name: 'Landing Slab',
|
|
452
|
+
parentId: upper.id,
|
|
453
|
+
polygon: [
|
|
454
|
+
[-4, -4],
|
|
455
|
+
[4, -4],
|
|
456
|
+
[4, 4],
|
|
457
|
+
[-4, 4],
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
const stair = StairNode.parse({
|
|
461
|
+
id: 'stair_spiral_landing',
|
|
462
|
+
name: 'Spiral Landing Stair',
|
|
463
|
+
parentId: ground.id,
|
|
464
|
+
position: [0, 0, 0],
|
|
465
|
+
rotation: Math.PI / 2,
|
|
466
|
+
stairType: 'spiral',
|
|
467
|
+
fromLevelId: ground.id,
|
|
468
|
+
toLevelId: upper.id,
|
|
469
|
+
slabOpeningMode: 'destination',
|
|
470
|
+
innerRadius: 0.35,
|
|
471
|
+
width: 1.2,
|
|
472
|
+
sweepAngle: Math.PI * 1.6,
|
|
473
|
+
topLandingMode: 'integrated',
|
|
474
|
+
topLandingDepth: 1.1,
|
|
475
|
+
});
|
|
476
|
+
const nodes = Object.fromEntries([building, ground, upper, landingSlab, stair].map((node) => [node.id, node]));
|
|
477
|
+
const updates = syncAutoStairOpenings(nodes);
|
|
478
|
+
const landingUpdate = updates.find((update) => update.id === landingSlab.id);
|
|
479
|
+
const holes = landingUpdate?.data.holes ?? [];
|
|
480
|
+
const rectangularHoles = holes.filter((hole) => hole.length === 4);
|
|
481
|
+
expect(holes).toHaveLength(1);
|
|
482
|
+
expect(rectangularHoles).toHaveLength(0);
|
|
483
|
+
expect(landingUpdate?.data.holeMetadata).toEqual([{ source: 'stair', stairId: stair.id }]);
|
|
484
|
+
});
|
|
446
485
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wall-mitering.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-mitering.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAO5C,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;CAClB;AASD,KAAK,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAGzE,KAAK,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"wall-mitering.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-mitering.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAO5C,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;CAClB;AASD,KAAK,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAGzE,KAAK,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAmBlD,iBAAS,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,SAAY,GAAG,MAAM,CAG7D;AA8CD,UAAU,QAAQ;IAChB,YAAY,EAAE,OAAO,CAAA;IACrB,cAAc,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,aAAa,CAAA;KAAE,CAAC,CAAA;CACpF;AAkPD,MAAM,WAAW,aAAa;IAE5B,YAAY,EAAE,YAAY,CAAA;IAE1B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;CACjC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,aAAa,CAWrE;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,QAAQ,EACd,SAAS,EAAE,aAAa,GACvB,uBAAuB,GAAG,IAAI,CA0BhC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CA8C/F;AAGD,OAAO,EAAE,UAAU,EAAE,CAAA"}
|
|
@@ -3,6 +3,16 @@ import { getWallCurveFrameAt, isCurvedWall } from './wall-curve';
|
|
|
3
3
|
// UTILITY FUNCTIONS
|
|
4
4
|
// ============================================================================
|
|
5
5
|
const TOLERANCE = 0.001;
|
|
6
|
+
// Miter joints are line-line intersections, so the joint point sits a distance
|
|
7
|
+
// ≈ halfThickness / sin(θ) from the junction, where θ is the angle between the
|
|
8
|
+
// two walls. As θ → 0 (two walls nearly collinear — e.g. a room-preset preview
|
|
9
|
+
// dragged on top of an existing wall, or a freshly-drawn wall almost parallel
|
|
10
|
+
// to its neighbour) that distance runs away to infinity and the wall renders as
|
|
11
|
+
// an infinite spike. Cap the joint at this multiple of the wall half-thickness;
|
|
12
|
+
// beyond it we fall back to a square (butt) joint, exactly like the existing
|
|
13
|
+
// parallel-walls guard. 10× preserves every realistic corner (a 0.1 m wall keeps
|
|
14
|
+
// mitering down to ~11°) while bounding the pathological near-collinear case.
|
|
15
|
+
const MITER_LIMIT = 10;
|
|
6
16
|
function pointToKey(p, tolerance = TOLERANCE) {
|
|
7
17
|
const snap = 1 / tolerance;
|
|
8
18
|
return `${Math.round(p.x * snap)},${Math.round(p.y * snap)}`;
|
|
@@ -144,7 +154,14 @@ function calculateJunctionIntersections(junction, getThickness) {
|
|
|
144
154
|
const edgeA = createLineFromPointAndVector(pA, v);
|
|
145
155
|
const edgeB = createLineFromPointAndVector(pB, v);
|
|
146
156
|
const angle = Math.atan2(v.y, v.x);
|
|
147
|
-
processedWalls.push({
|
|
157
|
+
processedWalls.push({
|
|
158
|
+
wallId: wall.id,
|
|
159
|
+
angle,
|
|
160
|
+
edgeA,
|
|
161
|
+
edgeB,
|
|
162
|
+
isPassthrough: true,
|
|
163
|
+
halfThickness: halfT,
|
|
164
|
+
});
|
|
148
165
|
}
|
|
149
166
|
}
|
|
150
167
|
else {
|
|
@@ -159,7 +176,14 @@ function calculateJunctionIntersections(junction, getThickness) {
|
|
|
159
176
|
const edgeA = createLineFromPointAndVector(pA, v);
|
|
160
177
|
const edgeB = createLineFromPointAndVector(pB, v);
|
|
161
178
|
const angle = Math.atan2(v.y, v.x);
|
|
162
|
-
processedWalls.push({
|
|
179
|
+
processedWalls.push({
|
|
180
|
+
wallId: wall.id,
|
|
181
|
+
angle,
|
|
182
|
+
edgeA,
|
|
183
|
+
edgeB,
|
|
184
|
+
isPassthrough: false,
|
|
185
|
+
halfThickness: halfT,
|
|
186
|
+
});
|
|
163
187
|
}
|
|
164
188
|
}
|
|
165
189
|
// Sort by outgoing angle
|
|
@@ -182,6 +206,18 @@ function calculateJunctionIntersections(junction, getThickness) {
|
|
|
182
206
|
x: (wall1.edgeA.b * wall2.edgeB.c - wall2.edgeB.b * wall1.edgeA.c) / det,
|
|
183
207
|
y: (wall2.edgeB.a * wall1.edgeA.c - wall1.edgeA.a * wall2.edgeB.c) / det,
|
|
184
208
|
};
|
|
209
|
+
// Miter limit: `det` only catches walls that are *exactly* parallel. Two
|
|
210
|
+
// walls meeting at a shallow angle have a small-but-nonzero `det`, so `p`
|
|
211
|
+
// lands far from the junction (∝ 1/sin θ) and the wall renders as an
|
|
212
|
+
// infinite spike. Reject any joint farther than MITER_LIMIT half-thicknesses
|
|
213
|
+
// from the meeting point — those walls fall back to a square joint.
|
|
214
|
+
const maxMiter = MITER_LIMIT * Math.max(wall1.halfThickness, wall2.halfThickness);
|
|
215
|
+
const dx = p.x - meetingPoint.x;
|
|
216
|
+
const dy = p.y - meetingPoint.y;
|
|
217
|
+
if (!(Number.isFinite(p.x) && Number.isFinite(p.y)) ||
|
|
218
|
+
dx * dx + dy * dy > maxMiter * maxMiter) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
185
221
|
// Only assign intersection to non-passthrough walls
|
|
186
222
|
// Passthrough walls don't receive junction data (their geometry doesn't change)
|
|
187
223
|
if (!wall1.isPassthrough) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wall-mitering.test.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-mitering.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { calculateLevelMiters, getWallMiterBoundaryPoints } from './wall-mitering';
|
|
3
|
+
function wall(id, start, end) {
|
|
4
|
+
return {
|
|
5
|
+
id,
|
|
6
|
+
type: 'wall',
|
|
7
|
+
object: 'node',
|
|
8
|
+
visible: true,
|
|
9
|
+
parentId: 'level_test',
|
|
10
|
+
children: [],
|
|
11
|
+
start,
|
|
12
|
+
end,
|
|
13
|
+
thickness: 0.1,
|
|
14
|
+
height: 2.5,
|
|
15
|
+
frontSide: 'interior',
|
|
16
|
+
backSide: 'exterior',
|
|
17
|
+
metadata: {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function maxBoundaryCoord(walls) {
|
|
21
|
+
const miter = calculateLevelMiters(walls);
|
|
22
|
+
let max = 0;
|
|
23
|
+
for (const w of walls) {
|
|
24
|
+
const bp = getWallMiterBoundaryPoints(w, miter);
|
|
25
|
+
expect(bp).not.toBeNull();
|
|
26
|
+
if (!bp)
|
|
27
|
+
continue;
|
|
28
|
+
for (const p of [bp.startLeft, bp.startRight, bp.endLeft, bp.endRight]) {
|
|
29
|
+
expect(Number.isFinite(p.x)).toBe(true);
|
|
30
|
+
expect(Number.isFinite(p.y)).toBe(true);
|
|
31
|
+
max = Math.max(max, Math.abs(p.x), Math.abs(p.y));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return max;
|
|
35
|
+
}
|
|
36
|
+
describe('wall mitering miter limit', () => {
|
|
37
|
+
// Two 3 m walls sharing the origin, meeting at decreasing angles. Without a
|
|
38
|
+
// miter limit the joint point runs to infinity as the angle → 0 (∝ 1/sin θ),
|
|
39
|
+
// which is the "infinite wall" seen when a room-preset preview lands on top of
|
|
40
|
+
// an existing wall. The boundary must stay bounded near the wall length.
|
|
41
|
+
test.each([90, 30, 10, 5, 1, 0.1, 0.01])('stays bounded at a %s° junction', (deg) => {
|
|
42
|
+
const rad = (deg * Math.PI) / 180;
|
|
43
|
+
const walls = [
|
|
44
|
+
wall('A', [0, 0], [3, 0]),
|
|
45
|
+
wall('B', [0, 0], [3 * Math.cos(rad), 3 * Math.sin(rad)]),
|
|
46
|
+
];
|
|
47
|
+
// 3 m walls + a few cm of joint: anything past ~4 m is a runaway spike.
|
|
48
|
+
expect(maxBoundaryCoord(walls)).toBeLessThan(4);
|
|
49
|
+
});
|
|
50
|
+
test('still miters a normal 90° corner', () => {
|
|
51
|
+
const walls = [wall('A', [0, 0], [3, 0]), wall('B', [0, 0], [0, 3])];
|
|
52
|
+
const miter = calculateLevelMiters(walls);
|
|
53
|
+
const bpA = getWallMiterBoundaryPoints(walls[0], miter);
|
|
54
|
+
expect(bpA).not.toBeNull();
|
|
55
|
+
if (!bpA)
|
|
56
|
+
throw new Error('expected miter boundary points');
|
|
57
|
+
// The shared corner is pulled to the mitred intersection, offset from the
|
|
58
|
+
// raw butt position (halfThickness 0.05) by the diagonal of the joint.
|
|
59
|
+
const startSideX = Math.min(bpA.startLeft.x, bpA.startRight.x);
|
|
60
|
+
expect(startSideX).toBeLessThan(-0.001);
|
|
61
|
+
expect(startSideX).toBeGreaterThan(-0.5);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clone-scene-graph.d.ts","sourceRoot":"","sources":["../../src/utils/clone-scene-graph.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"clone-scene-graph.d.ts","sourceRoot":"","sources":["../../src/utils/clone-scene-graph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAC9C,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAUD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAmHlE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,OAAO,EAAE,SAAS,GACjB;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAwF/E;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,CAAA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,qBAA0B,GAClC,UAAU,CAqEZ"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { remapMeasurementReferences } from '../lib/measurement-geometry';
|
|
1
2
|
import { generateId } from '../schema/base';
|
|
2
3
|
/**
|
|
3
4
|
* Extracts the type prefix from a node ID (e.g., "wall_abc123" -> "wall")
|
|
@@ -16,7 +17,7 @@ function extractIdPrefix(id) {
|
|
|
16
17
|
* - Multi-scene in-memory scenarios
|
|
17
18
|
*/
|
|
18
19
|
export function cloneSceneGraph(sceneGraph) {
|
|
19
|
-
const { nodes, rootNodeIds, collections } = sceneGraph;
|
|
20
|
+
const { nodes, rootNodeIds, collections, installedPlugins } = sceneGraph;
|
|
20
21
|
// Build ID mapping: old ID -> new ID
|
|
21
22
|
const idMap = new Map();
|
|
22
23
|
// Pass 1: Generate new IDs for all nodes
|
|
@@ -57,6 +58,14 @@ export function cloneSceneGraph(sceneGraph) {
|
|
|
57
58
|
;
|
|
58
59
|
clonedNode.wallId = idMap.get(clonedNode.wallId);
|
|
59
60
|
}
|
|
61
|
+
// Remap roofSegmentId (doors/windows/items hosted on roof wall faces)
|
|
62
|
+
if ('roofSegmentId' in clonedNode && typeof clonedNode.roofSegmentId === 'string') {
|
|
63
|
+
;
|
|
64
|
+
clonedNode.roofSegmentId = idMap.get(clonedNode.roofSegmentId);
|
|
65
|
+
}
|
|
66
|
+
if (clonedNode.type === 'measurement') {
|
|
67
|
+
clonedNode.measurement = remapMeasurementReferences(clonedNode.measurement, idMap);
|
|
68
|
+
}
|
|
60
69
|
clonedNodes[newId] = clonedNode;
|
|
61
70
|
}
|
|
62
71
|
// Remap root node IDs
|
|
@@ -102,6 +111,7 @@ export function cloneSceneGraph(sceneGraph) {
|
|
|
102
111
|
nodes: clonedNodes,
|
|
103
112
|
rootNodeIds: clonedRootNodeIds,
|
|
104
113
|
...(clonedCollections && { collections: clonedCollections }),
|
|
114
|
+
...(installedPlugins && { installedPlugins: [...installedPlugins] }),
|
|
105
115
|
};
|
|
106
116
|
}
|
|
107
117
|
/**
|
|
@@ -119,7 +129,7 @@ export function cloneSceneGraph(sceneGraph) {
|
|
|
119
129
|
*/
|
|
120
130
|
export function cloneLevelSubtree(nodes, levelId) {
|
|
121
131
|
const levelNode = nodes[levelId];
|
|
122
|
-
if (
|
|
132
|
+
if (levelNode?.type !== 'level') {
|
|
123
133
|
throw new Error(`Node "${levelId}" is not a level`);
|
|
124
134
|
}
|
|
125
135
|
// Recursively collect the level node + all descendants via children arrays
|
|
@@ -185,6 +195,15 @@ export function cloneLevelSubtree(nodes, levelId) {
|
|
|
185
195
|
;
|
|
186
196
|
cloned.wallId = idMap.get(cloned.wallId) ?? cloned.wallId;
|
|
187
197
|
}
|
|
198
|
+
// Remap roofSegmentId (doors/windows/items hosted on roof wall faces)
|
|
199
|
+
if ('roofSegmentId' in cloned && typeof cloned.roofSegmentId === 'string') {
|
|
200
|
+
;
|
|
201
|
+
cloned.roofSegmentId =
|
|
202
|
+
idMap.get(cloned.roofSegmentId) ?? cloned.roofSegmentId;
|
|
203
|
+
}
|
|
204
|
+
if (cloned.type === 'measurement') {
|
|
205
|
+
cloned.measurement = remapMeasurementReferences(cloned.measurement, idMap);
|
|
206
|
+
}
|
|
188
207
|
clonedNodes.push(cloned);
|
|
189
208
|
}
|
|
190
209
|
return { clonedNodes, newLevelId, idMap };
|
|
@@ -197,7 +216,7 @@ export function forkSceneGraph(sceneGraph, options = {}) {
|
|
|
197
216
|
if (options.preserveScans) {
|
|
198
217
|
return cloneSceneGraph(sceneGraph);
|
|
199
218
|
}
|
|
200
|
-
const { nodes, rootNodeIds, collections } = sceneGraph;
|
|
219
|
+
const { nodes, rootNodeIds, collections, installedPlugins } = sceneGraph;
|
|
201
220
|
// First, identify scan and guide node IDs to exclude (user-uploaded imagery)
|
|
202
221
|
const excludedNodeIds = new Set();
|
|
203
222
|
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
@@ -249,5 +268,6 @@ export function forkSceneGraph(sceneGraph, options = {}) {
|
|
|
249
268
|
nodes: filteredNodes,
|
|
250
269
|
rootNodeIds: filteredRootNodeIds,
|
|
251
270
|
...(filteredCollections && { collections: filteredCollections }),
|
|
271
|
+
...(installedPlugins && { installedPlugins }),
|
|
252
272
|
});
|
|
253
273
|
}
|
|
@@ -36,6 +36,7 @@ function makeSceneGraph() {
|
|
|
36
36
|
nodeIds: ['scan_1', 'guide_1'],
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
|
+
installedPlugins: ['pascal:trees'],
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
describe('forkSceneGraph', () => {
|
|
@@ -46,6 +47,7 @@ describe('forkSceneGraph', () => {
|
|
|
46
47
|
expect(nodes.some((node) => node.type === 'guide')).toBe(false);
|
|
47
48
|
expect(nodes.some((node) => node.type === 'wall')).toBe(true);
|
|
48
49
|
expect(forked.collections).toEqual({});
|
|
50
|
+
expect(forked.installedPlugins).toEqual(['pascal:trees']);
|
|
49
51
|
});
|
|
50
52
|
test('preserves scan and guide nodes when requested', () => {
|
|
51
53
|
const forked = forkSceneGraph(makeSceneGraph(), { preserveScans: true });
|
|
@@ -55,5 +57,6 @@ describe('forkSceneGraph', () => {
|
|
|
55
57
|
expect(nodes.map((node) => node.id)).not.toContain('scan_1');
|
|
56
58
|
expect(nodes.map((node) => node.id)).not.toContain('guide_1');
|
|
57
59
|
expect(Object.values(forked.collections ?? {}).flatMap((collection) => collection.nodeIds)).toHaveLength(2);
|
|
60
|
+
expect(forked.installedPlugins).toEqual(['pascal:trees']);
|
|
58
61
|
});
|
|
59
62
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface HealSceneResult {
|
|
2
|
+
nodes: Record<string, unknown>;
|
|
3
|
+
/** Ids of zero-length walls that were dropped. */
|
|
4
|
+
droppedWallIds: string[];
|
|
5
|
+
/** Count of non-string (e.g. null) entries removed from `children` arrays. */
|
|
6
|
+
strippedChildRefs: number;
|
|
7
|
+
/**
|
|
8
|
+
* Count of child references removed because the child's `parentId` points at
|
|
9
|
+
* a different node (stale reparent leftovers), plus same-array duplicates.
|
|
10
|
+
*/
|
|
11
|
+
strippedStaleChildRefs: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns a healed copy of a `nodes` map. Pure — does not mutate `input`.
|
|
15
|
+
* Nodes that need no repair are passed through by reference.
|
|
16
|
+
*/
|
|
17
|
+
export declare function healSceneNodes(input: Record<string, unknown>): HealSceneResult;
|
|
18
|
+
//# sourceMappingURL=heal-scene-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heal-scene-graph.d.ts","sourceRoot":"","sources":["../../src/utils/heal-scene-graph.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,kDAAkD;IAClD,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,8EAA8E;IAC9E,iBAAiB,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,sBAAsB,EAAE,MAAM,CAAA;CAC/B;AAgBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,CA0D9E"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Repairs scene-graph corruption that pre-dates the source fixes, so existing
|
|
2
|
+
// saved scenes still load. Known kinds of damage:
|
|
3
|
+
//
|
|
4
|
+
// 1. A `children` array containing a non-string entry. The capture wall-merge
|
|
5
|
+
// re-attached a wall-hosted item without minting an id, so `undefined` was
|
|
6
|
+
// pushed into the wall's children — which serializes to `[null]`. The wall
|
|
7
|
+
// schema rejects `null` children, so the whole scene fails to load.
|
|
8
|
+
// 2. A zero-length wall (start === end). It renders nothing, but lingers as a
|
|
9
|
+
// junk node and is a foot-gun for snapping/mitering.
|
|
10
|
+
// 3. A child referenced by a parent it no longer belongs to: the child's
|
|
11
|
+
// `parentId` points at node B while node A's `children` still lists it
|
|
12
|
+
// (stale leftover from a reparent that didn't clean the old parent). The
|
|
13
|
+
// duplicate reference renders the child twice (duplicate React keys in the
|
|
14
|
+
// 2D plan, doubled hosted geometry in 3D). Same-array duplicates are
|
|
15
|
+
// collapsed too.
|
|
16
|
+
//
|
|
17
|
+
// All are also prevented at the source now; this is the load-time safety net
|
|
18
|
+
// for already-saved scenes.
|
|
19
|
+
const ZERO_LENGTH_EPS = 1e-6;
|
|
20
|
+
function isWallLike(node) {
|
|
21
|
+
if (!node || typeof node !== 'object')
|
|
22
|
+
return false;
|
|
23
|
+
const n = node;
|
|
24
|
+
return (n.type === 'wall' &&
|
|
25
|
+
Array.isArray(n.start) &&
|
|
26
|
+
Array.isArray(n.end) &&
|
|
27
|
+
typeof n.start[0] === 'number' &&
|
|
28
|
+
typeof n.start[1] === 'number' &&
|
|
29
|
+
typeof n.end[0] === 'number' &&
|
|
30
|
+
typeof n.end[1] === 'number');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns a healed copy of a `nodes` map. Pure — does not mutate `input`.
|
|
34
|
+
* Nodes that need no repair are passed through by reference.
|
|
35
|
+
*/
|
|
36
|
+
export function healSceneNodes(input) {
|
|
37
|
+
const droppedWallIds = [];
|
|
38
|
+
// Pass 1: drop childless zero-length walls. (Only childless ones — a wall
|
|
39
|
+
// carrying a door/window must keep its hosts, degenerate or not.)
|
|
40
|
+
const kept = {};
|
|
41
|
+
for (const [id, node] of Object.entries(input)) {
|
|
42
|
+
if (isWallLike(node)) {
|
|
43
|
+
const children = node.children;
|
|
44
|
+
const childless = !Array.isArray(children) || children.length === 0;
|
|
45
|
+
const dx = node.end[0] - node.start[0];
|
|
46
|
+
const dz = node.end[1] - node.start[1];
|
|
47
|
+
if (childless && Math.hypot(dx, dz) <= ZERO_LENGTH_EPS) {
|
|
48
|
+
droppedWallIds.push(id);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
kept[id] = node;
|
|
53
|
+
}
|
|
54
|
+
const dropped = new Set(droppedWallIds);
|
|
55
|
+
let strippedChildRefs = 0;
|
|
56
|
+
let strippedStaleChildRefs = 0;
|
|
57
|
+
// Pass 2: clean `children` arrays — drop non-string entries (the `[null]`
|
|
58
|
+
// bug), references to walls we just removed, same-array duplicates, and
|
|
59
|
+
// stale references whose child's `parentId` names a different parent.
|
|
60
|
+
const nodes = {};
|
|
61
|
+
for (const [id, node] of Object.entries(kept)) {
|
|
62
|
+
const children = node?.children;
|
|
63
|
+
if (Array.isArray(children)) {
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const cleaned = children.filter((c) => {
|
|
66
|
+
if (typeof c !== 'string' || dropped.has(c)) {
|
|
67
|
+
strippedChildRefs++;
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (seen.has(c)) {
|
|
71
|
+
strippedStaleChildRefs++;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
seen.add(c);
|
|
75
|
+
const child = kept[c];
|
|
76
|
+
if (child && typeof child.parentId === 'string' && child.parentId !== id) {
|
|
77
|
+
strippedStaleChildRefs++;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
if (cleaned.length !== children.length) {
|
|
83
|
+
nodes[id] = { ...node, children: cleaned };
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
nodes[id] = node;
|
|
88
|
+
}
|
|
89
|
+
return { nodes, droppedWallIds, strippedChildRefs, strippedStaleChildRefs };
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heal-scene-graph.test.d.ts","sourceRoot":"","sources":["../../src/utils/heal-scene-graph.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { healSceneNodes } from './heal-scene-graph';
|
|
3
|
+
describe('healSceneNodes', () => {
|
|
4
|
+
test('strips non-string (null) children entries', () => {
|
|
5
|
+
const { nodes, strippedChildRefs } = healSceneNodes({
|
|
6
|
+
wall_a: {
|
|
7
|
+
id: 'wall_a',
|
|
8
|
+
type: 'wall',
|
|
9
|
+
start: [0, 0],
|
|
10
|
+
end: [1, 0],
|
|
11
|
+
children: [null, 'item_x'],
|
|
12
|
+
},
|
|
13
|
+
item_x: { id: 'item_x', type: 'item' },
|
|
14
|
+
});
|
|
15
|
+
expect(strippedChildRefs).toBe(1);
|
|
16
|
+
expect(nodes.wall_a.children).toEqual(['item_x']);
|
|
17
|
+
});
|
|
18
|
+
test('drops childless zero-length walls and removes their parent reference', () => {
|
|
19
|
+
const { nodes, droppedWallIds } = healSceneNodes({
|
|
20
|
+
level_0: { id: 'level_0', type: 'level', children: ['wall_zero', 'wall_real'] },
|
|
21
|
+
wall_zero: { id: 'wall_zero', type: 'wall', start: [5, 5], end: [5, 5], children: [] },
|
|
22
|
+
wall_real: { id: 'wall_real', type: 'wall', start: [0, 0], end: [3, 0], children: [] },
|
|
23
|
+
});
|
|
24
|
+
expect(droppedWallIds).toEqual(['wall_zero']);
|
|
25
|
+
expect('wall_zero' in nodes).toBe(false);
|
|
26
|
+
expect(nodes.level_0.children).toEqual(['wall_real']);
|
|
27
|
+
});
|
|
28
|
+
test('keeps a zero-length wall that still hosts a door/window', () => {
|
|
29
|
+
const { nodes, droppedWallIds } = healSceneNodes({
|
|
30
|
+
wall_z: { id: 'wall_z', type: 'wall', start: [1, 1], end: [1, 1], children: ['door_1'] },
|
|
31
|
+
door_1: { id: 'door_1', type: 'door' },
|
|
32
|
+
});
|
|
33
|
+
expect(droppedWallIds).toEqual([]);
|
|
34
|
+
expect('wall_z' in nodes).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
test('passes a clean scene through untouched', () => {
|
|
37
|
+
const input = {
|
|
38
|
+
wall_a: { id: 'wall_a', type: 'wall', start: [0, 0], end: [2, 0], children: ['door_1'] },
|
|
39
|
+
door_1: { id: 'door_1', type: 'door' },
|
|
40
|
+
};
|
|
41
|
+
const { nodes, droppedWallIds, strippedChildRefs } = healSceneNodes(input);
|
|
42
|
+
expect(droppedWallIds).toEqual([]);
|
|
43
|
+
expect(strippedChildRefs).toBe(0);
|
|
44
|
+
expect(nodes.wall_a).toBe(input.wall_a);
|
|
45
|
+
});
|
|
46
|
+
test('drops a stale child reference left behind by a reparent', () => {
|
|
47
|
+
// window's parentId says wall_b, but wall_a still lists it — the exact
|
|
48
|
+
// corruption that rendered a window twice (duplicate React keys in 2D).
|
|
49
|
+
const { nodes, strippedStaleChildRefs } = healSceneNodes({
|
|
50
|
+
wall_a: { id: 'wall_a', type: 'wall', start: [0, 0], end: [2, 0], children: ['window_1'] },
|
|
51
|
+
wall_b: { id: 'wall_b', type: 'wall', start: [2, 0], end: [4, 0], children: ['window_1'] },
|
|
52
|
+
window_1: { id: 'window_1', type: 'window', parentId: 'wall_b' },
|
|
53
|
+
});
|
|
54
|
+
expect(strippedStaleChildRefs).toBe(1);
|
|
55
|
+
expect(nodes.wall_a.children).toEqual([]);
|
|
56
|
+
expect(nodes.wall_b.children).toEqual(['window_1']);
|
|
57
|
+
});
|
|
58
|
+
test('collapses same-array duplicate child references', () => {
|
|
59
|
+
const { nodes, strippedStaleChildRefs } = healSceneNodes({
|
|
60
|
+
wall_a: {
|
|
61
|
+
id: 'wall_a',
|
|
62
|
+
type: 'wall',
|
|
63
|
+
start: [0, 0],
|
|
64
|
+
end: [2, 0],
|
|
65
|
+
children: ['door_1', 'door_1'],
|
|
66
|
+
},
|
|
67
|
+
door_1: { id: 'door_1', type: 'door', parentId: 'wall_a' },
|
|
68
|
+
});
|
|
69
|
+
expect(strippedStaleChildRefs).toBe(1);
|
|
70
|
+
expect(nodes.wall_a.children).toEqual(['door_1']);
|
|
71
|
+
});
|
|
72
|
+
test('keeps children whose parentId matches or is absent', () => {
|
|
73
|
+
const input = {
|
|
74
|
+
wall_a: {
|
|
75
|
+
id: 'wall_a',
|
|
76
|
+
type: 'wall',
|
|
77
|
+
start: [0, 0],
|
|
78
|
+
end: [2, 0],
|
|
79
|
+
children: ['door_1', 'item_legacy'],
|
|
80
|
+
},
|
|
81
|
+
door_1: { id: 'door_1', type: 'door', parentId: 'wall_a' },
|
|
82
|
+
item_legacy: { id: 'item_legacy', type: 'item' },
|
|
83
|
+
};
|
|
84
|
+
const { nodes, strippedStaleChildRefs } = healSceneNodes(input);
|
|
85
|
+
expect(strippedStaleChildRefs).toBe(0);
|
|
86
|
+
expect(nodes.wall_a).toBe(input.wall_a);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-build-json.d.ts","sourceRoot":"","sources":["../../src/validation/validate-build-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"validate-build-json.d.ts","sourceRoot":"","sources":["../../src/validation/validate-build-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG3D,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,CAAA;AAEpD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,KAAK,EAAE,UAAU,CAAA;IACjB,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,YAAY,EAAE,WAAW,EAAE,CAAA;IAC3B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AA8BD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CAwPzE"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AnyNode } from '../schema/types';
|
|
2
|
+
import { healSceneNodes } from '../utils/heal-scene-graph';
|
|
2
3
|
const KNOWN_TYPES = new Set(AnyNode.options.map((o) => o.shape.type.parse(undefined)));
|
|
3
4
|
function isPlainObject(value) {
|
|
4
5
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -52,6 +53,7 @@ export function validateBuildJson(input) {
|
|
|
52
53
|
}
|
|
53
54
|
const nodesRaw = input.nodes;
|
|
54
55
|
const rootNodeIdsRaw = input.rootNodeIds;
|
|
56
|
+
const installedPluginsRaw = input.installedPlugins;
|
|
55
57
|
if (!isPlainObject(nodesRaw)) {
|
|
56
58
|
errors.push({
|
|
57
59
|
severity: 'error',
|
|
@@ -77,8 +79,29 @@ export function validateBuildJson(input) {
|
|
|
77
79
|
schemaIssueCount: 0,
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
// Heal known pre-existing corruption (null children, zero-length walls) up
|
|
83
|
+
// front, so a scene saved before the source fixes still imports instead of
|
|
84
|
+
// hard-failing schema validation. `parsed` below carries the repaired nodes.
|
|
85
|
+
const { nodes, droppedWallIds, strippedChildRefs } = healSceneNodes(nodesRaw);
|
|
81
86
|
const rootNodeIds = rootNodeIdsRaw;
|
|
87
|
+
const installedPlugins = Array.isArray(installedPluginsRaw) &&
|
|
88
|
+
installedPluginsRaw.every((pluginId) => typeof pluginId === 'string')
|
|
89
|
+
? Array.from(new Set(installedPluginsRaw))
|
|
90
|
+
: undefined;
|
|
91
|
+
if (installedPluginsRaw !== undefined && installedPlugins === undefined) {
|
|
92
|
+
warnings.push({
|
|
93
|
+
severity: 'warning',
|
|
94
|
+
code: 'invalid_installed_plugins',
|
|
95
|
+
message: 'Ignored invalid "installedPlugins" — expected an array of plugin IDs.',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (strippedChildRefs > 0 || droppedWallIds.length > 0) {
|
|
99
|
+
warnings.push({
|
|
100
|
+
severity: 'warning',
|
|
101
|
+
code: 'auto_repaired',
|
|
102
|
+
message: `Repaired on import: removed ${strippedChildRefs} invalid child reference${strippedChildRefs === 1 ? '' : 's'} and ${droppedWallIds.length} zero-length wall${droppedWallIds.length === 1 ? '' : 's'}.`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
82
105
|
if (rootNodeIds.length === 0) {
|
|
83
106
|
errors.push({
|
|
84
107
|
severity: 'error',
|
|
@@ -217,7 +240,13 @@ export function validateBuildJson(input) {
|
|
|
217
240
|
const ok = errors.length === 0;
|
|
218
241
|
return {
|
|
219
242
|
ok,
|
|
220
|
-
parsed: ok
|
|
243
|
+
parsed: ok
|
|
244
|
+
? {
|
|
245
|
+
nodes,
|
|
246
|
+
rootNodeIds,
|
|
247
|
+
...(installedPlugins ? { installedPlugins } : {}),
|
|
248
|
+
}
|
|
249
|
+
: null,
|
|
221
250
|
stats,
|
|
222
251
|
errors,
|
|
223
252
|
warnings,
|