@pascal-app/core 0.9.2 → 1.0.0-beta.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/README.md +17 -10
- package/dist/events/bus.d.ts +21 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +116 -66
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +319 -441
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +204 -12
- package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
- package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/support-host-id.js +2 -0
- package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
- package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
- package/dist/index.d.ts +23 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -10
- package/dist/lib/measurement-geometry.d.ts +4 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -1
- package/dist/lib/measurement-geometry.js +27 -10
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +33 -1
- package/dist/lib/space-detection.d.ts +15 -4
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +232 -91
- package/dist/lib/terrain-brush.d.ts +163 -0
- package/dist/lib/terrain-brush.d.ts.map +1 -0
- package/dist/lib/terrain-brush.js +359 -0
- package/dist/lib/terrain-codec.d.ts +51 -0
- package/dist/lib/terrain-codec.d.ts.map +1 -0
- package/dist/lib/terrain-codec.js +167 -0
- package/dist/lib/terrain-field.d.ts +150 -0
- package/dist/lib/terrain-field.d.ts.map +1 -0
- package/dist/lib/terrain-field.js +248 -0
- package/dist/lib/terrain-raycast.d.ts +44 -0
- package/dist/lib/terrain-raycast.d.ts.map +1 -0
- package/dist/lib/terrain-raycast.js +188 -0
- package/dist/lib/terrain-source.d.ts +60 -0
- package/dist/lib/terrain-source.d.ts.map +1 -0
- package/dist/lib/terrain-source.js +90 -0
- package/dist/lib/terrain-support.d.ts +85 -0
- package/dist/lib/terrain-support.d.ts.map +1 -0
- package/dist/lib/terrain-support.js +218 -0
- package/dist/lib/zone-quantities.d.ts +1 -1
- package/dist/lib/zone-quantities.d.ts.map +1 -1
- package/dist/lib/zone-quantities.js +311 -66
- package/dist/material-library.d.ts +9 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +56 -4
- package/dist/registry/handles.d.ts +16 -1
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +5 -2
- package/dist/registry/types.d.ts +83 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +6 -3
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +6 -3
- package/dist/schema/nodes/cabinet.d.ts +2 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -1
- package/dist/schema/nodes/cabinet.js +4 -2
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +7 -1
- package/dist/schema/nodes/column.d.ts +1 -0
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +2 -0
- package/dist/schema/nodes/construction-dimension.d.ts +195 -0
- package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
- package/dist/schema/nodes/construction-dimension.js +151 -0
- package/dist/schema/nodes/door.d.ts +29 -0
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +20 -0
- package/dist/schema/nodes/duct-terminal.d.ts +1 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -1
- package/dist/schema/nodes/duct-terminal.js +2 -0
- package/dist/schema/nodes/fence.d.ts +2 -0
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +8 -0
- package/dist/schema/nodes/hvac-equipment.d.ts +1 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -1
- package/dist/schema/nodes/hvac-equipment.js +2 -0
- package/dist/schema/nodes/item.d.ts +1 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +13 -0
- package/dist/schema/nodes/level.d.ts +2 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +12 -33
- package/dist/schema/nodes/shelf.d.ts +1 -0
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +2 -0
- package/dist/schema/nodes/site.d.ts +9 -0
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/site.js +8 -6
- package/dist/schema/nodes/slab.d.ts +5 -0
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +14 -2
- package/dist/schema/nodes/spawn.d.ts +1 -0
- package/dist/schema/nodes/spawn.d.ts.map +1 -1
- package/dist/schema/nodes/spawn.js +2 -0
- package/dist/schema/nodes/stair.d.ts +3 -1
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +8 -1
- package/dist/schema/nodes/structural-grid.d.ts +27 -0
- package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
- package/dist/schema/nodes/structural-grid.js +17 -0
- package/dist/schema/nodes/wall.d.ts +5 -2
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +13 -4
- package/dist/schema/nodes/window.d.ts +29 -0
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +18 -0
- package/dist/schema/nodes/zone.d.ts +20 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +15 -0
- package/dist/schema/terrain.d.ts +28 -0
- package/dist/schema/terrain.d.ts.map +1 -0
- package/dist/schema/terrain.js +30 -0
- package/dist/schema/types.d.ts +211 -3
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -1
- package/dist/services/level-height.d.ts +8 -9
- package/dist/services/level-height.d.ts.map +1 -1
- package/dist/services/level-height.js +48 -16
- package/dist/services/storey.d.ts +103 -0
- package/dist/services/storey.d.ts.map +1 -0
- package/dist/services/storey.js +296 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +47 -3
- package/dist/store/history-control.d.ts +20 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +110 -13
- package/dist/store/use-live-terrain.d.ts +65 -0
- package/dist/store/use-live-terrain.d.ts.map +1 -0
- package/dist/store/use-live-terrain.js +31 -0
- package/dist/store/use-live-transforms.d.ts +8 -0
- package/dist/store/use-live-transforms.d.ts.map +1 -1
- package/dist/store/use-scene.d.ts +31 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +638 -8
- package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-opening-system.js +27 -21
- package/dist/systems/elevator/elevator-service.d.ts +0 -2
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-service.js +2 -24
- package/dist/systems/slab/slab-placement.d.ts +11 -0
- package/dist/systems/slab/slab-placement.d.ts.map +1 -0
- package/dist/systems/slab/slab-placement.js +14 -0
- package/dist/systems/slab/slab-support.d.ts +150 -0
- package/dist/systems/slab/slab-support.d.ts.map +1 -0
- package/dist/systems/slab/slab-support.js +575 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +1060 -935
- 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 +18 -15
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.js +32 -4
- package/dist/systems/stair/stair-rise.d.ts +22 -0
- package/dist/systems/stair/stair-rise.d.ts.map +1 -0
- package/dist/systems/stair/stair-rise.js +79 -0
- package/dist/systems/wall/wall-curve.d.ts +10 -0
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +1 -1
- package/dist/systems/wall/wall-mitering.js +6 -6
- package/dist/systems/wall/wall-top.d.ts +40 -0
- package/dist/systems/wall/wall-top.d.ts.map +1 -0
- package/dist/systems/wall/wall-top.js +46 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +34 -3
- package/dist/utils/heal-scene-graph.d.ts +1 -1
- package/dist/utils/heal-scene-graph.d.ts.map +1 -1
- package/dist/utils/heal-scene-graph.js +25 -9
- package/dist/validation/validate-build-json.d.ts +2 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +59 -3
- package/package.json +2 -2
- package/dist/hooks/scene-registry/scene-registry.test.d.ts +0 -2
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +0 -1
- package/dist/hooks/scene-registry/scene-registry.test.js +0 -26
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -366
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +0 -2
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +0 -1
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +0 -363
- package/dist/lib/measurement-geometry.test.d.ts +0 -2
- package/dist/lib/measurement-geometry.test.d.ts.map +0 -1
- package/dist/lib/measurement-geometry.test.js +0 -102
- package/dist/lib/slab-polygon.test.d.ts +0 -2
- package/dist/lib/slab-polygon.test.d.ts.map +0 -1
- package/dist/lib/slab-polygon.test.js +0 -635
- package/dist/lib/space-detection-pause.test.d.ts +0 -2
- package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
- package/dist/lib/space-detection-pause.test.js +0 -42
- package/dist/lib/space-detection.test.d.ts +0 -2
- package/dist/lib/space-detection.test.d.ts.map +0 -1
- package/dist/lib/space-detection.test.js +0 -376
- package/dist/lib/zone-quantities.test.d.ts +0 -2
- package/dist/lib/zone-quantities.test.d.ts.map +0 -1
- package/dist/lib/zone-quantities.test.js +0 -82
- package/dist/registry/registry.test.d.ts +0 -2
- package/dist/registry/registry.test.d.ts.map +0 -1
- package/dist/registry/registry.test.js +0 -196
- package/dist/registry/relations-resolver.test.d.ts +0 -2
- package/dist/registry/relations-resolver.test.d.ts.map +0 -1
- package/dist/registry/relations-resolver.test.js +0 -183
- package/dist/registry/scene-api.test.d.ts +0 -2
- package/dist/registry/scene-api.test.d.ts.map +0 -1
- package/dist/registry/scene-api.test.js +0 -183
- package/dist/registry/subtree.test.d.ts +0 -2
- package/dist/registry/subtree.test.d.ts.map +0 -1
- package/dist/registry/subtree.test.js +0 -163
- package/dist/schema/asset-url.test.d.ts +0 -2
- package/dist/schema/asset-url.test.d.ts.map +0 -1
- package/dist/schema/asset-url.test.js +0 -134
- package/dist/schema/material.test.d.ts +0 -2
- package/dist/schema/material.test.d.ts.map +0 -1
- package/dist/schema/material.test.js +0 -43
- package/dist/schema/nodes/measurement.test.d.ts +0 -2
- package/dist/schema/nodes/measurement.test.d.ts.map +0 -1
- package/dist/schema/nodes/measurement.test.js +0 -139
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +0 -2
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +0 -1
- package/dist/schema/nodes/ridge-vent-defaults.test.js +0 -205
- package/dist/schema/nodes/roof-segment-shape.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-shape.test.js +0 -175
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-surface.test.js +0 -48
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-trim.test.js +0 -107
- package/dist/schema/nodes/roof-segment-walls.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-walls.test.js +0 -92
- package/dist/schema/nodes/wall.test.d.ts +0 -2
- package/dist/schema/nodes/wall.test.d.ts.map +0 -1
- package/dist/schema/nodes/wall.test.js +0 -208
- package/dist/services/alignment-anchors.test.d.ts +0 -2
- package/dist/services/alignment-anchors.test.d.ts.map +0 -1
- package/dist/services/alignment-anchors.test.js +0 -383
- package/dist/services/alignment.test.d.ts +0 -2
- package/dist/services/alignment.test.d.ts.map +0 -1
- package/dist/services/alignment.test.js +0 -92
- package/dist/services/drag-session.test.d.ts +0 -2
- package/dist/services/drag-session.test.d.ts.map +0 -1
- package/dist/services/drag-session.test.js +0 -219
- package/dist/services/hosting.test.d.ts +0 -2
- package/dist/services/hosting.test.d.ts.map +0 -1
- package/dist/services/hosting.test.js +0 -230
- package/dist/services/movement.test.d.ts +0 -2
- package/dist/services/movement.test.d.ts.map +0 -1
- package/dist/services/movement.test.js +0 -133
- package/dist/services/opening-guides.test.d.ts +0 -2
- package/dist/services/opening-guides.test.d.ts.map +0 -1
- package/dist/services/opening-guides.test.js +0 -208
- package/dist/services/port-connectivity.test.d.ts +0 -2
- package/dist/services/port-connectivity.test.d.ts.map +0 -1
- package/dist/services/port-connectivity.test.js +0 -297
- package/dist/services/riser-diagram.test.d.ts +0 -2
- package/dist/services/riser-diagram.test.d.ts.map +0 -1
- package/dist/services/riser-diagram.test.js +0 -66
- package/dist/services/single-undo-dance.test.d.ts +0 -2
- package/dist/services/single-undo-dance.test.d.ts.map +0 -1
- package/dist/services/single-undo-dance.test.js +0 -210
- package/dist/services/snap.test.d.ts +0 -2
- package/dist/services/snap.test.d.ts.map +0 -1
- package/dist/services/snap.test.js +0 -137
- package/dist/services/system-graph.test.d.ts +0 -2
- package/dist/services/system-graph.test.d.ts.map +0 -1
- package/dist/services/system-graph.test.js +0 -138
- package/dist/services/validate-dwv.test.d.ts +0 -2
- package/dist/services/validate-dwv.test.d.ts.map +0 -1
- package/dist/services/validate-dwv.test.js +0 -109
- package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
- package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
- package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
- package/dist/store/actions/reparent.test.d.ts +0 -2
- package/dist/store/actions/reparent.test.d.ts.map +0 -1
- package/dist/store/actions/reparent.test.js +0 -212
- package/dist/store/actions/ridge-vent-update.test.d.ts +0 -2
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +0 -1
- package/dist/store/actions/ridge-vent-update.test.js +0 -264
- package/dist/store/use-scene-dirty-tracking.test.d.ts +0 -2
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +0 -1
- package/dist/store/use-scene-dirty-tracking.test.js +0 -68
- package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
- package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-elevator-migration.test.js +0 -109
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts +0 -2
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +0 -1
- package/dist/store/use-scene-measurement-roundtrip.test.js +0 -97
- package/dist/store/use-scene-plugins.test.d.ts +0 -2
- package/dist/store/use-scene-plugins.test.d.ts.map +0 -1
- package/dist/store/use-scene-plugins.test.js +0 -51
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +0 -2
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-wall-slot-migration.test.js +0 -210
- package/dist/store/use-scene-window-migration.test.d.ts +0 -2
- package/dist/store/use-scene-window-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-window-migration.test.js +0 -85
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
- package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
- package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-runtime.test.js +0 -64
- package/dist/systems/fence/fence-spline.test.d.ts +0 -2
- package/dist/systems/fence/fence-spline.test.d.ts.map +0 -1
- package/dist/systems/fence/fence-spline.test.js +0 -136
- package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
- package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
- package/dist/systems/stair/stair-opening-preview.test.js +0 -80
- package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
- package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
- package/dist/systems/stair/stair-opening-sync.test.js +0 -485
- package/dist/systems/wall/wall-mitering.test.d.ts +0 -2
- package/dist/systems/wall/wall-mitering.test.d.ts.map +0 -1
- package/dist/systems/wall/wall-mitering.test.js +0 -63
- package/dist/utils/clone-scene-graph.test.d.ts +0 -2
- package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
- package/dist/utils/clone-scene-graph.test.js +0 -62
- package/dist/utils/heal-scene-graph.test.d.ts +0 -2
- package/dist/utils/heal-scene-graph.test.d.ts.map +0 -1
- package/dist/utils/heal-scene-graph.test.js +0 -88
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { CeilingNode, SlabNode, WallNode, ZoneNode } from '../schema';
|
|
3
|
-
import { detectSpacesForLevel, planAutoCeilingsForLevel, planAutoSlabsForLevel, planAutoZonesForLevel, resolveAutoZonePolygon, wallClosesRoom, } from './space-detection';
|
|
4
|
-
const square = [
|
|
5
|
-
[0, 0],
|
|
6
|
-
[4, 0],
|
|
7
|
-
[4, 3],
|
|
8
|
-
[0, 3],
|
|
9
|
-
];
|
|
10
|
-
function roomPolygon() {
|
|
11
|
-
return square.map(([x, y]) => ({ x, y }));
|
|
12
|
-
}
|
|
13
|
-
function squareWalls(height = 2.5) {
|
|
14
|
-
return [
|
|
15
|
-
WallNode.parse({ start: [0, 0], end: [4, 0], height }),
|
|
16
|
-
WallNode.parse({ start: [4, 0], end: [4, 3], height }),
|
|
17
|
-
WallNode.parse({ start: [4, 3], end: [0, 3], height }),
|
|
18
|
-
WallNode.parse({ start: [0, 3], end: [0, 0], height }),
|
|
19
|
-
];
|
|
20
|
-
}
|
|
21
|
-
function slab(elevation) {
|
|
22
|
-
return SlabNode.parse({
|
|
23
|
-
polygon: square,
|
|
24
|
-
elevation,
|
|
25
|
-
autoFromWalls: true,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
describe('planAutoCeilingsForLevel', () => {
|
|
29
|
-
test('creates auto ceilings at the top of the room walls', () => {
|
|
30
|
-
const created = planAutoCeilingsForLevel([roomPolygon()], [], {
|
|
31
|
-
walls: squareWalls(),
|
|
32
|
-
slabs: [slab(0.05)],
|
|
33
|
-
}).create[0];
|
|
34
|
-
expect(created?.height).toBeCloseTo(2.55);
|
|
35
|
-
});
|
|
36
|
-
test('updates existing auto ceiling height when the slab elevation changes', () => {
|
|
37
|
-
const ceiling = CeilingNode.parse({
|
|
38
|
-
polygon: square,
|
|
39
|
-
height: 2.55,
|
|
40
|
-
autoFromWalls: true,
|
|
41
|
-
});
|
|
42
|
-
const plan = planAutoCeilingsForLevel([roomPolygon()], [ceiling], {
|
|
43
|
-
walls: squareWalls(),
|
|
44
|
-
slabs: [slab(0.4)],
|
|
45
|
-
});
|
|
46
|
-
expect(plan.update).toHaveLength(1);
|
|
47
|
-
expect(plan.update[0]?.id).toBe(ceiling.id);
|
|
48
|
-
expect(plan.update[0]?.data.polygon).toBeUndefined();
|
|
49
|
-
expect(plan.update[0]?.data.height).toBeCloseTo(2.9);
|
|
50
|
-
});
|
|
51
|
-
test('updates existing auto ceiling height when wall height changes', () => {
|
|
52
|
-
const ceiling = CeilingNode.parse({
|
|
53
|
-
polygon: square,
|
|
54
|
-
height: 2.55,
|
|
55
|
-
autoFromWalls: true,
|
|
56
|
-
});
|
|
57
|
-
const plan = planAutoCeilingsForLevel([roomPolygon()], [ceiling], {
|
|
58
|
-
walls: squareWalls(3),
|
|
59
|
-
slabs: [slab(0.05)],
|
|
60
|
-
});
|
|
61
|
-
expect(plan.update).toHaveLength(1);
|
|
62
|
-
expect(plan.update[0]?.data.height).toBeCloseTo(3.05);
|
|
63
|
-
});
|
|
64
|
-
test('does not replace a manual ceiling with an auto ceiling', () => {
|
|
65
|
-
const manualCeiling = CeilingNode.parse({
|
|
66
|
-
polygon: square,
|
|
67
|
-
height: 2.5,
|
|
68
|
-
autoFromWalls: false,
|
|
69
|
-
});
|
|
70
|
-
const plan = planAutoCeilingsForLevel([roomPolygon()], [manualCeiling], {
|
|
71
|
-
walls: squareWalls(),
|
|
72
|
-
slabs: [slab(0.4)],
|
|
73
|
-
});
|
|
74
|
-
expect(plan.create).toHaveLength(0);
|
|
75
|
-
expect(plan.update).toHaveLength(0);
|
|
76
|
-
});
|
|
77
|
-
test('demotes an orphaned auto ceiling to manual with its polygon untouched', () => {
|
|
78
|
-
const ceiling = CeilingNode.parse({
|
|
79
|
-
polygon: square,
|
|
80
|
-
height: 2.55,
|
|
81
|
-
autoFromWalls: true,
|
|
82
|
-
});
|
|
83
|
-
const plan = planAutoCeilingsForLevel([], [ceiling]);
|
|
84
|
-
expect(plan.create).toHaveLength(0);
|
|
85
|
-
expect(plan.delete).toHaveLength(0);
|
|
86
|
-
expect(plan.update).toHaveLength(1);
|
|
87
|
-
expect(plan.update[0]?.id).toBe(ceiling.id);
|
|
88
|
-
// Ceilings render the stored polygon in both modes, so no polygon bake.
|
|
89
|
-
expect(plan.update[0]?.data).toEqual({ autoFromWalls: false });
|
|
90
|
-
});
|
|
91
|
-
test('deletes an unmatched auto ceiling absorbed by a room merge', () => {
|
|
92
|
-
const leftCeiling = CeilingNode.parse({
|
|
93
|
-
polygon: [
|
|
94
|
-
[0, 0],
|
|
95
|
-
[4, 0],
|
|
96
|
-
[4, 3],
|
|
97
|
-
[0, 3],
|
|
98
|
-
],
|
|
99
|
-
autoFromWalls: true,
|
|
100
|
-
});
|
|
101
|
-
const rightCeiling = CeilingNode.parse({
|
|
102
|
-
polygon: [
|
|
103
|
-
[4, 0],
|
|
104
|
-
[8, 0],
|
|
105
|
-
[8, 3],
|
|
106
|
-
[4, 3],
|
|
107
|
-
],
|
|
108
|
-
autoFromWalls: true,
|
|
109
|
-
});
|
|
110
|
-
const mergedRoom = [
|
|
111
|
-
{ x: 0, y: 0 },
|
|
112
|
-
{ x: 8, y: 0 },
|
|
113
|
-
{ x: 8, y: 3 },
|
|
114
|
-
{ x: 0, y: 3 },
|
|
115
|
-
];
|
|
116
|
-
const plan = planAutoCeilingsForLevel([mergedRoom], [leftCeiling, rightCeiling]);
|
|
117
|
-
expect(plan.create).toHaveLength(0);
|
|
118
|
-
expect(plan.delete).toHaveLength(1);
|
|
119
|
-
const survivorId = plan.update[0]?.id;
|
|
120
|
-
expect([leftCeiling.id, rightCeiling.id]).toContain(plan.delete[0]);
|
|
121
|
-
expect(plan.delete[0]).not.toBe(survivorId);
|
|
122
|
-
});
|
|
123
|
-
test('a demoted ceiling suppresses re-creating an auto ceiling when the room re-forms', () => {
|
|
124
|
-
const ceiling = CeilingNode.parse({
|
|
125
|
-
polygon: square,
|
|
126
|
-
height: 2.55,
|
|
127
|
-
autoFromWalls: true,
|
|
128
|
-
});
|
|
129
|
-
const demotion = planAutoCeilingsForLevel([], [ceiling]).update[0];
|
|
130
|
-
const demoted = CeilingNode.parse({ ...ceiling, ...demotion?.data });
|
|
131
|
-
expect(demoted.autoFromWalls).toBe(false);
|
|
132
|
-
const plan = planAutoCeilingsForLevel([roomPolygon()], [demoted], {
|
|
133
|
-
walls: squareWalls(),
|
|
134
|
-
slabs: [slab(0.05)],
|
|
135
|
-
});
|
|
136
|
-
expect(plan.create).toHaveLength(0);
|
|
137
|
-
expect(plan.update).toHaveLength(0);
|
|
138
|
-
expect(plan.delete).toHaveLength(0);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
describe('detectSpacesForLevel', () => {
|
|
142
|
-
const areaOf = (polygon) => {
|
|
143
|
-
let area = 0;
|
|
144
|
-
for (let i = 0; i < polygon.length; i += 1) {
|
|
145
|
-
const a = polygon[i];
|
|
146
|
-
const b = polygon[(i + 1) % polygon.length];
|
|
147
|
-
area += a.x * b.y - b.x * a.y;
|
|
148
|
-
}
|
|
149
|
-
return Math.abs(area / 2);
|
|
150
|
-
};
|
|
151
|
-
test('detects an isolated four-wall room', () => {
|
|
152
|
-
const walls = squareWalls();
|
|
153
|
-
const { roomPolygons, spaces } = detectSpacesForLevel('level-1', walls);
|
|
154
|
-
expect(roomPolygons).toHaveLength(1);
|
|
155
|
-
expect(new Set(spaces[0]?.wallIds)).toEqual(new Set(walls.map((wall) => wall.id)));
|
|
156
|
-
expect(spaces[0]?.boundaryFaces).toHaveLength(4);
|
|
157
|
-
expect(spaces[0]?.boundaryFaces.map((boundary) => `${boundary.wallId}:${boundary.face}`).sort()).toEqual(walls.map((wall) => `${wall.id}:front`).sort());
|
|
158
|
-
});
|
|
159
|
-
test('excludes dangling wall branches from a room boundary', () => {
|
|
160
|
-
const roomWalls = squareWalls();
|
|
161
|
-
const branch = WallNode.parse({ start: [0, 0], end: [1, 1] });
|
|
162
|
-
const { roomPolygons, spaces } = detectSpacesForLevel('level-1', [...roomWalls, branch]);
|
|
163
|
-
expect(roomPolygons).toHaveLength(1);
|
|
164
|
-
expect(roomPolygons[0]).toHaveLength(4);
|
|
165
|
-
expect(areaOf(roomPolygons[0])).toBeCloseTo(12);
|
|
166
|
-
expect(spaces[0]?.wallIds.sort()).toEqual(roomWalls.map((wall) => wall.id).sort());
|
|
167
|
-
expect(spaces[0]?.boundaryFaces).toHaveLength(4);
|
|
168
|
-
});
|
|
169
|
-
test('detects a room closed against the middle of an existing wall (T-junction)', () => {
|
|
170
|
-
// Big 6×5 room; a smaller room hangs below, its two verticals landing on the
|
|
171
|
-
// interior of the big room's bottom wall (x=1 and x=3, not endpoints). Before
|
|
172
|
-
// planarization those touch points were dangling nodes and the small room
|
|
173
|
-
// was never detected.
|
|
174
|
-
const walls = [
|
|
175
|
-
WallNode.parse({ start: [0, 0], end: [6, 0] }),
|
|
176
|
-
WallNode.parse({ start: [6, 0], end: [6, 5] }),
|
|
177
|
-
WallNode.parse({ start: [6, 5], end: [0, 5] }),
|
|
178
|
-
WallNode.parse({ start: [0, 5], end: [0, 0] }),
|
|
179
|
-
WallNode.parse({ start: [1, 0], end: [1, -2] }),
|
|
180
|
-
WallNode.parse({ start: [1, -2], end: [3, -2] }),
|
|
181
|
-
WallNode.parse({ start: [3, -2], end: [3, 0] }),
|
|
182
|
-
];
|
|
183
|
-
const { roomPolygons, spaces } = detectSpacesForLevel('level-1', walls);
|
|
184
|
-
const areas = roomPolygons.map((poly) => areaOf(poly)).sort((a, b) => a - b);
|
|
185
|
-
const smallRoom = spaces.find((space) => areaOf(space.polygon.map(([x, y]) => ({ x, y }))) < 5);
|
|
186
|
-
expect(roomPolygons).toHaveLength(2);
|
|
187
|
-
expect(areas[0]).toBeCloseTo(4, 1); // small room: 2×2
|
|
188
|
-
expect(areas[1]).toBeCloseTo(30, 1); // big room: 6×5
|
|
189
|
-
expect(new Set(smallRoom?.wallIds)).toEqual(new Set([walls[0].id, walls[4].id, walls[5].id, walls[6].id]));
|
|
190
|
-
const longWallId = walls[0].id;
|
|
191
|
-
const longWallBoundaries = spaces.flatMap((space) => space.boundaryFaces.filter((boundary) => boundary.wallId === longWallId));
|
|
192
|
-
expect(longWallBoundaries).toHaveLength(4);
|
|
193
|
-
expect(longWallBoundaries.filter((boundary) => boundary.face === 'back')).toHaveLength(1);
|
|
194
|
-
expect(longWallBoundaries.filter((boundary) => boundary.face === 'front')).toHaveLength(3);
|
|
195
|
-
expect(longWallBoundaries.map((boundary) => boundary.points)).toContainEqual([
|
|
196
|
-
[1, 0],
|
|
197
|
-
[3, 0],
|
|
198
|
-
]);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
describe('procedural zones', () => {
|
|
202
|
-
test('adopts an exact room footprint and records its enclosing walls', () => {
|
|
203
|
-
const walls = squareWalls();
|
|
204
|
-
const { spaces } = detectSpacesForLevel('level-1', walls);
|
|
205
|
-
const zone = ZoneNode.parse({ name: 'Kitchen', polygon: square });
|
|
206
|
-
const plan = planAutoZonesForLevel(spaces, [zone]);
|
|
207
|
-
expect(plan.update).toHaveLength(1);
|
|
208
|
-
expect(plan.update[0]?.data.autoFromWalls).toBe(true);
|
|
209
|
-
expect(new Set(plan.update[0]?.data.boundaryWallIds)).toEqual(new Set(walls.map((wall) => wall.id)));
|
|
210
|
-
});
|
|
211
|
-
test('derives the live polygon from effective wall endpoints', () => {
|
|
212
|
-
const walls = squareWalls();
|
|
213
|
-
const zone = ZoneNode.parse({
|
|
214
|
-
name: 'Kitchen',
|
|
215
|
-
polygon: square,
|
|
216
|
-
autoFromWalls: true,
|
|
217
|
-
boundaryWallIds: walls.map((wall) => wall.id),
|
|
218
|
-
});
|
|
219
|
-
const movedWalls = [
|
|
220
|
-
{ ...walls[0], end: [5, 0] },
|
|
221
|
-
{ ...walls[1], start: [5, 0], end: [5, 3] },
|
|
222
|
-
{ ...walls[2], start: [5, 3] },
|
|
223
|
-
walls[3],
|
|
224
|
-
];
|
|
225
|
-
const byId = new Map(movedWalls.map((wall) => [wall.id, wall]));
|
|
226
|
-
const polygon = resolveAutoZonePolygon(zone, (id) => byId.get(id));
|
|
227
|
-
const plan = planAutoZonesForLevel(detectSpacesForLevel('level-1', movedWalls).spaces, [zone]);
|
|
228
|
-
expect(polygon).toContainEqual([5, 0]);
|
|
229
|
-
expect(polygon).toContainEqual([5, 3]);
|
|
230
|
-
expect(polygon).not.toContainEqual([4, 0]);
|
|
231
|
-
expect(plan.update[0]?.data.polygon).toContainEqual([5, 0]);
|
|
232
|
-
});
|
|
233
|
-
test('leaves an unrelated site zone manual', () => {
|
|
234
|
-
const { spaces } = detectSpacesForLevel('level-1', squareWalls());
|
|
235
|
-
const zone = ZoneNode.parse({
|
|
236
|
-
name: 'Lawn',
|
|
237
|
-
polygon: [
|
|
238
|
-
[10, 10],
|
|
239
|
-
[12, 10],
|
|
240
|
-
[12, 12],
|
|
241
|
-
[10, 12],
|
|
242
|
-
],
|
|
243
|
-
});
|
|
244
|
-
expect(planAutoZonesForLevel(spaces, [zone]).update).toHaveLength(0);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
describe('wallClosesRoom', () => {
|
|
248
|
-
test('is false while a chain is still open, true once it encloses a room', () => {
|
|
249
|
-
const open = [
|
|
250
|
-
WallNode.parse({ start: [0, 0], end: [4, 0] }),
|
|
251
|
-
WallNode.parse({ start: [4, 0], end: [4, 3] }),
|
|
252
|
-
WallNode.parse({ start: [4, 3], end: [0, 3] }),
|
|
253
|
-
];
|
|
254
|
-
const closing = WallNode.parse({ start: [0, 3], end: [0, 0] });
|
|
255
|
-
expect(wallClosesRoom(open, closing)).toBe(false);
|
|
256
|
-
expect(wallClosesRoom([...open, closing], closing)).toBe(true);
|
|
257
|
-
});
|
|
258
|
-
test('fires when a bay is sealed against the middle of an existing wall', () => {
|
|
259
|
-
const bigRoom = [
|
|
260
|
-
WallNode.parse({ start: [0, 0], end: [6, 0] }),
|
|
261
|
-
WallNode.parse({ start: [6, 0], end: [6, 5] }),
|
|
262
|
-
WallNode.parse({ start: [6, 5], end: [0, 5] }),
|
|
263
|
-
WallNode.parse({ start: [0, 5], end: [0, 0] }),
|
|
264
|
-
];
|
|
265
|
-
const bayLeft = WallNode.parse({ start: [1, 0], end: [1, -2] });
|
|
266
|
-
const bayBottom = WallNode.parse({ start: [1, -2], end: [3, -2] });
|
|
267
|
-
const bayRight = WallNode.parse({ start: [3, -2], end: [3, 0] });
|
|
268
|
-
// Two sides down and across: not enclosed yet.
|
|
269
|
-
expect(wallClosesRoom([...bigRoom, bayLeft, bayBottom], bayBottom)).toBe(false);
|
|
270
|
-
// The final side lands on the interior of the big room's bottom wall.
|
|
271
|
-
expect(wallClosesRoom([...bigRoom, bayLeft, bayBottom, bayRight], bayRight)).toBe(true);
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
describe('planAutoSlabsForLevel', () => {
|
|
275
|
-
test('matches two identical rooms to their own existing auto-slabs without churn', () => {
|
|
276
|
-
// Two rooms with identical polygon signatures previously collided in a
|
|
277
|
-
// signature-keyed Map, so one detected room never matched an existing slab
|
|
278
|
-
// and churned (delete + recreate) on every pass.
|
|
279
|
-
const slabA = slab(0.05);
|
|
280
|
-
const slabB = slab(0.05);
|
|
281
|
-
const plan = planAutoSlabsForLevel([roomPolygon(), roomPolygon()], [slabA, slabB]);
|
|
282
|
-
expect(plan.create).toHaveLength(0);
|
|
283
|
-
expect(plan.delete).toHaveLength(0);
|
|
284
|
-
expect(plan.update).toHaveLength(0);
|
|
285
|
-
});
|
|
286
|
-
test('deletes an extra auto-slab when only one identical room is detected', () => {
|
|
287
|
-
const plan = planAutoSlabsForLevel([roomPolygon()], [slab(0.05), slab(0.05)]);
|
|
288
|
-
expect(plan.create).toHaveLength(0);
|
|
289
|
-
expect(plan.delete).toHaveLength(1);
|
|
290
|
-
});
|
|
291
|
-
test('demotes an orphaned auto slab to manual when its room disappears', () => {
|
|
292
|
-
const painted = SlabNode.parse({
|
|
293
|
-
polygon: square,
|
|
294
|
-
elevation: 0.4,
|
|
295
|
-
autoFromWalls: true,
|
|
296
|
-
});
|
|
297
|
-
const plan = planAutoSlabsForLevel([], [painted]);
|
|
298
|
-
expect(plan.create).toHaveLength(0);
|
|
299
|
-
expect(plan.delete).toHaveLength(0);
|
|
300
|
-
expect(plan.update).toHaveLength(1);
|
|
301
|
-
const update = plan.update[0];
|
|
302
|
-
expect(update?.id).toBe(painted.id);
|
|
303
|
-
// Demotion flips only the flag — the stored polygon stays untouched
|
|
304
|
-
// (render offsets derive from level context at geometry build time).
|
|
305
|
-
expect(update?.data).toEqual({ autoFromWalls: false });
|
|
306
|
-
});
|
|
307
|
-
test('deletes an unmatched auto slab whose area was absorbed by a room merge', () => {
|
|
308
|
-
const leftSlab = SlabNode.parse({
|
|
309
|
-
polygon: [
|
|
310
|
-
[0, 0],
|
|
311
|
-
[4, 0],
|
|
312
|
-
[4, 3],
|
|
313
|
-
[0, 3],
|
|
314
|
-
],
|
|
315
|
-
autoFromWalls: true,
|
|
316
|
-
});
|
|
317
|
-
const rightSlab = SlabNode.parse({
|
|
318
|
-
polygon: [
|
|
319
|
-
[4, 0],
|
|
320
|
-
[8, 0],
|
|
321
|
-
[8, 3],
|
|
322
|
-
[4, 3],
|
|
323
|
-
],
|
|
324
|
-
autoFromWalls: true,
|
|
325
|
-
});
|
|
326
|
-
const mergedRoom = [
|
|
327
|
-
{ x: 0, y: 0 },
|
|
328
|
-
{ x: 8, y: 0 },
|
|
329
|
-
{ x: 8, y: 3 },
|
|
330
|
-
{ x: 0, y: 3 },
|
|
331
|
-
];
|
|
332
|
-
const plan = planAutoSlabsForLevel([mergedRoom], [leftSlab, rightSlab]);
|
|
333
|
-
expect(plan.create).toHaveLength(0);
|
|
334
|
-
expect(plan.delete).toHaveLength(1);
|
|
335
|
-
expect(plan.update).toHaveLength(1);
|
|
336
|
-
const survivorId = plan.update[0]?.id;
|
|
337
|
-
expect([leftSlab.id, rightSlab.id]).toContain(plan.delete[0]);
|
|
338
|
-
expect(plan.delete[0]).not.toBe(survivorId);
|
|
339
|
-
// The survivor stays auto — updated to the merged polygon, not demoted.
|
|
340
|
-
expect(plan.update[0]?.data.autoFromWalls).toBeUndefined();
|
|
341
|
-
});
|
|
342
|
-
test('a demoted slab suppresses re-creating an auto slab when the room re-forms', () => {
|
|
343
|
-
const auto = slab(0.05);
|
|
344
|
-
const demotion = planAutoSlabsForLevel([], [auto]).update[0];
|
|
345
|
-
const demoted = SlabNode.parse({ ...auto, ...demotion?.data });
|
|
346
|
-
expect(demoted.autoFromWalls).toBe(false);
|
|
347
|
-
const plan = planAutoSlabsForLevel([roomPolygon()], [demoted]);
|
|
348
|
-
expect(plan.create).toHaveLength(0);
|
|
349
|
-
expect(plan.update).toHaveLength(0);
|
|
350
|
-
expect(plan.delete).toHaveLength(0);
|
|
351
|
-
});
|
|
352
|
-
test('manual slabs that split one room suppress a replacement full-room slab', () => {
|
|
353
|
-
const left = SlabNode.parse({
|
|
354
|
-
polygon: [
|
|
355
|
-
[0, 0],
|
|
356
|
-
[2, 0],
|
|
357
|
-
[2, 3],
|
|
358
|
-
[0, 3],
|
|
359
|
-
],
|
|
360
|
-
autoFromWalls: false,
|
|
361
|
-
});
|
|
362
|
-
const right = SlabNode.parse({
|
|
363
|
-
polygon: [
|
|
364
|
-
[2, 0],
|
|
365
|
-
[4, 0],
|
|
366
|
-
[4, 3],
|
|
367
|
-
[2, 3],
|
|
368
|
-
],
|
|
369
|
-
autoFromWalls: false,
|
|
370
|
-
});
|
|
371
|
-
const plan = planAutoSlabsForLevel([roomPolygon()], [left, right]);
|
|
372
|
-
expect(plan.create).toHaveLength(0);
|
|
373
|
-
expect(plan.update).toHaveLength(0);
|
|
374
|
-
expect(plan.delete).toHaveLength(0);
|
|
375
|
-
});
|
|
376
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"zone-quantities.test.d.ts","sourceRoot":"","sources":["../../src/lib/zone-quantities.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { CeilingNode, SlabNode, WallNode, ZoneNode } from '../schema';
|
|
3
|
-
import { deriveZoneQuantityReport } from './zone-quantities';
|
|
4
|
-
const polygon = [
|
|
5
|
-
[0, 0],
|
|
6
|
-
[4, 0],
|
|
7
|
-
[4, 3],
|
|
8
|
-
[0, 3],
|
|
9
|
-
];
|
|
10
|
-
function sceneRecord(nodes) {
|
|
11
|
-
return Object.fromEntries(nodes.map((node) => [node.id, node]));
|
|
12
|
-
}
|
|
13
|
-
function roomNodes() {
|
|
14
|
-
const zone = ZoneNode.parse({ id: 'zone_room', name: 'Studio', parentId: 'level_main', polygon });
|
|
15
|
-
const slab = SlabNode.parse({ id: 'slab_room', parentId: 'level_main', polygon });
|
|
16
|
-
const ceiling = CeilingNode.parse({ id: 'ceiling_room', parentId: 'level_main', polygon });
|
|
17
|
-
const walls = polygon.map((start, index) => WallNode.parse({
|
|
18
|
-
id: `wall_${index}`,
|
|
19
|
-
parentId: 'level_main',
|
|
20
|
-
start,
|
|
21
|
-
end: polygon[(index + 1) % polygon.length],
|
|
22
|
-
height: 2.5,
|
|
23
|
-
}));
|
|
24
|
-
return { zone, slab, ceiling, walls };
|
|
25
|
-
}
|
|
26
|
-
describe('deriveZoneQuantityReport', () => {
|
|
27
|
-
test('derives room surfaces and volume only from matching enclosure geometry', () => {
|
|
28
|
-
const { zone, slab, ceiling, walls } = roomNodes();
|
|
29
|
-
const report = deriveZoneQuantityReport(zone, sceneRecord([zone, slab, ceiling, ...walls]));
|
|
30
|
-
expect(report.classification).toBe('enclosed-room');
|
|
31
|
-
expect(report.footprintArea).toBeCloseTo(12);
|
|
32
|
-
expect(report.perimeter).toBeCloseTo(14);
|
|
33
|
-
expect(report.boundaryWallIds).toHaveLength(4);
|
|
34
|
-
expect(report.wallSurface).toEqual({
|
|
35
|
-
status: 'available',
|
|
36
|
-
value: 35,
|
|
37
|
-
note: 'Gross interior wall face before openings.',
|
|
38
|
-
});
|
|
39
|
-
expect(report.floorSurface).toEqual({
|
|
40
|
-
status: 'available',
|
|
41
|
-
value: 12,
|
|
42
|
-
note: 'Matching slab surface after openings.',
|
|
43
|
-
});
|
|
44
|
-
expect(report.volume.status).toBe('available');
|
|
45
|
-
if (report.volume.status === 'available')
|
|
46
|
-
expect(report.volume.value).toBeCloseTo(29.4);
|
|
47
|
-
});
|
|
48
|
-
test('keeps standalone zones honest about unavailable room quantities', () => {
|
|
49
|
-
const zone = ZoneNode.parse({
|
|
50
|
-
id: 'zone_site',
|
|
51
|
-
name: 'Garden',
|
|
52
|
-
parentId: 'level_main',
|
|
53
|
-
polygon,
|
|
54
|
-
});
|
|
55
|
-
const report = deriveZoneQuantityReport(zone, sceneRecord([zone]));
|
|
56
|
-
expect(report.classification).toBe('footprint');
|
|
57
|
-
expect(report.footprintArea).toBeCloseTo(12);
|
|
58
|
-
expect(report.wallSurface.status).toBe('unavailable');
|
|
59
|
-
expect(report.floorSurface.status).toBe('unavailable');
|
|
60
|
-
expect(report.volume.status).toBe('unavailable');
|
|
61
|
-
});
|
|
62
|
-
test('subtracts matching slab openings from floor surface', () => {
|
|
63
|
-
const { zone, slab } = roomNodes();
|
|
64
|
-
const slabWithHole = SlabNode.parse({
|
|
65
|
-
...slab,
|
|
66
|
-
holes: [
|
|
67
|
-
[
|
|
68
|
-
[1, 1],
|
|
69
|
-
[2, 1],
|
|
70
|
-
[2, 2],
|
|
71
|
-
[1, 2],
|
|
72
|
-
],
|
|
73
|
-
],
|
|
74
|
-
});
|
|
75
|
-
const report = deriveZoneQuantityReport(zone, sceneRecord([zone, slabWithHole]));
|
|
76
|
-
expect(report.floorSurface).toEqual({
|
|
77
|
-
status: 'available',
|
|
78
|
-
value: 11,
|
|
79
|
-
note: 'Matching slab surface after openings.',
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registry.test.d.ts","sourceRoot":"","sources":["../../src/registry/registry.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { getHostRefFields, getNodePluginId, isDrawnViaTool, isDrawnViaToolKind, isNodeKindEnabled, isPresettable, isPresettableKind, loadPlugin, nodeRegistry, registerNode, } from './registry';
|
|
4
|
-
// Re-registering a kind warns + replaces in dev (HMR) but throws in
|
|
5
|
-
// production — see `registry._register`. `bun test` runs with
|
|
6
|
-
// NODE_ENV=test (dev path), so the throw-path tests pin NODE_ENV to
|
|
7
|
-
// 'production' for the duration of the call.
|
|
8
|
-
async function inProduction(fn) {
|
|
9
|
-
const prev = process.env.NODE_ENV;
|
|
10
|
-
process.env.NODE_ENV = 'production';
|
|
11
|
-
try {
|
|
12
|
-
return await fn();
|
|
13
|
-
}
|
|
14
|
-
finally {
|
|
15
|
-
process.env.NODE_ENV = prev;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
function makeDefinition(kind, overrides = {}) {
|
|
19
|
-
return {
|
|
20
|
-
kind,
|
|
21
|
-
schemaVersion: 1,
|
|
22
|
-
schema: z.object({ type: z.literal(kind) }),
|
|
23
|
-
category: 'utility',
|
|
24
|
-
defaults: () => ({}),
|
|
25
|
-
capabilities: {},
|
|
26
|
-
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
27
|
-
...overrides,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
describe('nodeRegistry', () => {
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
nodeRegistry._reset();
|
|
33
|
-
});
|
|
34
|
-
test('starts empty', () => {
|
|
35
|
-
expect(nodeRegistry.size).toBe(0);
|
|
36
|
-
expect(nodeRegistry.has('anything')).toBe(false);
|
|
37
|
-
expect(nodeRegistry.get('anything')).toBeUndefined();
|
|
38
|
-
});
|
|
39
|
-
test('registerNode adds a definition', () => {
|
|
40
|
-
const def = makeDefinition('column');
|
|
41
|
-
registerNode(def);
|
|
42
|
-
expect(nodeRegistry.size).toBe(1);
|
|
43
|
-
expect(nodeRegistry.has('column')).toBe(true);
|
|
44
|
-
expect(nodeRegistry.get('column')).toBe(def);
|
|
45
|
-
});
|
|
46
|
-
test('registerNode throws on duplicate kind in production', async () => {
|
|
47
|
-
await inProduction(() => {
|
|
48
|
-
registerNode(makeDefinition('column'));
|
|
49
|
-
expect(() => registerNode(makeDefinition('column'))).toThrow(/duplicate node kind/);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
test('registerNode replaces on duplicate kind in dev (HMR)', () => {
|
|
53
|
-
const first = makeDefinition('column');
|
|
54
|
-
const second = makeDefinition('column');
|
|
55
|
-
registerNode(first);
|
|
56
|
-
registerNode(second);
|
|
57
|
-
expect(nodeRegistry.size).toBe(1);
|
|
58
|
-
expect(nodeRegistry.get('column')).toBe(second);
|
|
59
|
-
});
|
|
60
|
-
test('registerNode rejects empty kind', () => {
|
|
61
|
-
expect(() => registerNode(makeDefinition(''))).toThrow(/non-empty string/);
|
|
62
|
-
});
|
|
63
|
-
test('registerNode rejects invalid schemaVersion', () => {
|
|
64
|
-
expect(() => registerNode(makeDefinition('bad', { schemaVersion: 0 }))).toThrow(/schemaVersion/);
|
|
65
|
-
expect(() => registerNode(makeDefinition('bad', { schemaVersion: -1 }))).toThrow(/schemaVersion/);
|
|
66
|
-
});
|
|
67
|
-
test('entries() iterates registered definitions', () => {
|
|
68
|
-
registerNode(makeDefinition('a'));
|
|
69
|
-
registerNode(makeDefinition('b'));
|
|
70
|
-
const kinds = Array.from(nodeRegistry.entries(), ([k]) => k);
|
|
71
|
-
expect(kinds).toEqual(['a', 'b']);
|
|
72
|
-
});
|
|
73
|
-
test('schemas() returns all registered schemas', () => {
|
|
74
|
-
const a = makeDefinition('a');
|
|
75
|
-
const b = makeDefinition('b');
|
|
76
|
-
registerNode(a);
|
|
77
|
-
registerNode(b);
|
|
78
|
-
expect(nodeRegistry.schemas()).toEqual([a.schema, b.schema]);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
describe('isPresettable', () => {
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
nodeRegistry._reset();
|
|
84
|
-
});
|
|
85
|
-
test('explicit true wins', () => {
|
|
86
|
-
const def = makeDefinition('explicit-true', { capabilities: { presettable: true } });
|
|
87
|
-
expect(isPresettable(def)).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
test('explicit false wins even with parametrics', () => {
|
|
90
|
-
const def = makeDefinition('explicit-false', {
|
|
91
|
-
capabilities: { presettable: false },
|
|
92
|
-
parametrics: { groups: [] },
|
|
93
|
-
});
|
|
94
|
-
expect(isPresettable(def)).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
test('defaults to true when parametrics exists', () => {
|
|
97
|
-
const def = makeDefinition('param', { parametrics: { groups: [] } });
|
|
98
|
-
expect(isPresettable(def)).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
test('defaults to false without parametrics', () => {
|
|
101
|
-
const def = makeDefinition('no-param');
|
|
102
|
-
expect(isPresettable(def)).toBe(false);
|
|
103
|
-
});
|
|
104
|
-
test('isPresettableKind looks up the registry', () => {
|
|
105
|
-
registerNode(makeDefinition('shelfy', { parametrics: { groups: [] } }));
|
|
106
|
-
expect(isPresettableKind('shelfy')).toBe(true);
|
|
107
|
-
expect(isPresettableKind('unknown')).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
describe('getHostRefFields', () => {
|
|
111
|
-
test('returns the declared hostRefFields verbatim', () => {
|
|
112
|
-
const def = makeDefinition('door', { capabilities: { hostRefFields: ['wallId'] } });
|
|
113
|
-
expect(getHostRefFields(def)).toEqual(['wallId']);
|
|
114
|
-
});
|
|
115
|
-
test('defaults to an empty array when none declared', () => {
|
|
116
|
-
const def = makeDefinition('shelf');
|
|
117
|
-
expect(getHostRefFields(def)).toEqual([]);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
describe('isDrawnViaTool', () => {
|
|
121
|
-
beforeEach(() => {
|
|
122
|
-
nodeRegistry._reset();
|
|
123
|
-
});
|
|
124
|
-
test('true when capability set', () => {
|
|
125
|
-
const def = makeDefinition('fence', { capabilities: { drawTool: true } });
|
|
126
|
-
expect(isDrawnViaTool(def)).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
test('false when unset or not exactly true', () => {
|
|
129
|
-
expect(isDrawnViaTool(makeDefinition('column'))).toBe(false);
|
|
130
|
-
expect(isDrawnViaTool(makeDefinition('off', { capabilities: { drawTool: false } }))).toBe(false);
|
|
131
|
-
});
|
|
132
|
-
test('isDrawnViaToolKind looks up the registry', () => {
|
|
133
|
-
registerNode(makeDefinition('fence', { capabilities: { drawTool: true } }));
|
|
134
|
-
expect(isDrawnViaToolKind('fence')).toBe(true);
|
|
135
|
-
expect(isDrawnViaToolKind('unknown')).toBe(false);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
describe('loadPlugin', () => {
|
|
139
|
-
beforeEach(() => {
|
|
140
|
-
nodeRegistry._reset();
|
|
141
|
-
});
|
|
142
|
-
test('registers all nodes from a plugin', async () => {
|
|
143
|
-
const plugin = {
|
|
144
|
-
id: 'test:plugin',
|
|
145
|
-
apiVersion: 1,
|
|
146
|
-
nodes: [makeDefinition('a'), makeDefinition('b')],
|
|
147
|
-
};
|
|
148
|
-
await loadPlugin(plugin);
|
|
149
|
-
expect(nodeRegistry.size).toBe(2);
|
|
150
|
-
expect(nodeRegistry.has('a')).toBe(true);
|
|
151
|
-
expect(nodeRegistry.has('b')).toBe(true);
|
|
152
|
-
expect(getNodePluginId('a')).toBe('test:plugin');
|
|
153
|
-
expect(getNodePluginId('b')).toBe('test:plugin');
|
|
154
|
-
});
|
|
155
|
-
test('enables plugin kinds only when the project has the plugin installed', async () => {
|
|
156
|
-
await loadPlugin({ id: 'test:plugin', apiVersion: 1, nodes: [makeDefinition('plugin:node')] });
|
|
157
|
-
expect(isNodeKindEnabled('plugin:node', [])).toBe(false);
|
|
158
|
-
expect(isNodeKindEnabled('plugin:node', ['test:plugin'])).toBe(true);
|
|
159
|
-
expect(isNodeKindEnabled('plugin:node')).toBe(true);
|
|
160
|
-
expect(isNodeKindEnabled('host:node', [])).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
test('keeps built-in plugin kinds enabled independently of project installs', async () => {
|
|
163
|
-
await loadPlugin({ id: 'pascal:core', apiVersion: 1, nodes: [makeDefinition('wall')] });
|
|
164
|
-
expect(isNodeKindEnabled('wall', [])).toBe(true);
|
|
165
|
-
});
|
|
166
|
-
test('handles plugin with no nodes', async () => {
|
|
167
|
-
await loadPlugin({ id: 'empty', apiVersion: 1 });
|
|
168
|
-
expect(nodeRegistry.size).toBe(0);
|
|
169
|
-
});
|
|
170
|
-
test('handles plugin with empty nodes array', async () => {
|
|
171
|
-
await loadPlugin({ id: 'empty', apiVersion: 1, nodes: [] });
|
|
172
|
-
expect(nodeRegistry.size).toBe(0);
|
|
173
|
-
});
|
|
174
|
-
test('throws on apiVersion mismatch', async () => {
|
|
175
|
-
const plugin = {
|
|
176
|
-
id: 'old-plugin',
|
|
177
|
-
apiVersion: 99,
|
|
178
|
-
nodes: [],
|
|
179
|
-
};
|
|
180
|
-
await expect(loadPlugin(plugin)).rejects.toThrow(/apiVersion/);
|
|
181
|
-
});
|
|
182
|
-
test('propagates duplicate-kind error from a single plugin in production', async () => {
|
|
183
|
-
const plugin = {
|
|
184
|
-
id: 'broken',
|
|
185
|
-
apiVersion: 1,
|
|
186
|
-
nodes: [makeDefinition('dup'), makeDefinition('dup')],
|
|
187
|
-
};
|
|
188
|
-
await inProduction(() => expect(loadPlugin(plugin)).rejects.toThrow(/duplicate node kind/));
|
|
189
|
-
});
|
|
190
|
-
test('propagates duplicate-kind error across plugins in production', async () => {
|
|
191
|
-
await inProduction(async () => {
|
|
192
|
-
await loadPlugin({ id: 'a', apiVersion: 1, nodes: [makeDefinition('shared')] });
|
|
193
|
-
await expect(loadPlugin({ id: 'b', apiVersion: 1, nodes: [makeDefinition('shared')] })).rejects.toThrow(/duplicate node kind/);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"relations-resolver.test.d.ts","sourceRoot":"","sources":["../../src/registry/relations-resolver.test.ts"],"names":[],"mappings":""}
|