@pascal-app/core 0.9.2 → 1.0.0-beta.1
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 +316 -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 +193 -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 +174 -88
- 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 +60 -0
- package/dist/lib/terrain-support.d.ts.map +1 -0
- package/dist/lib/terrain-support.js +150 -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 +64 -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 +137 -0
- package/dist/systems/slab/slab-support.d.ts.map +1 -0
- package/dist/systems/slab/slab-support.js +557 -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,219 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { nodeRegistry, registerNode } from '../registry/registry';
|
|
4
|
-
import { createDragSession } from './drag-session';
|
|
5
|
-
const id = (s) => s;
|
|
6
|
-
function makeSpyScene(initial = {}) {
|
|
7
|
-
const calls = {
|
|
8
|
-
pauseHistory: 0,
|
|
9
|
-
resumeHistory: 0,
|
|
10
|
-
restoreAll: 0,
|
|
11
|
-
markedDirty: [],
|
|
12
|
-
updated: [],
|
|
13
|
-
};
|
|
14
|
-
const nodes = { ...initial };
|
|
15
|
-
return {
|
|
16
|
-
get: ((nid) => nodes[nid]),
|
|
17
|
-
nodes: () => nodes,
|
|
18
|
-
update: (nid, patch) => {
|
|
19
|
-
calls.updated.push([nid, patch]);
|
|
20
|
-
const existing = nodes[nid];
|
|
21
|
-
if (existing)
|
|
22
|
-
nodes[nid] = { ...existing, ...patch };
|
|
23
|
-
},
|
|
24
|
-
upsert: (n) => {
|
|
25
|
-
nodes[n.id] = n;
|
|
26
|
-
return n.id;
|
|
27
|
-
},
|
|
28
|
-
delete: (nid) => {
|
|
29
|
-
delete nodes[nid];
|
|
30
|
-
},
|
|
31
|
-
restore: () => { },
|
|
32
|
-
restoreAll: () => {
|
|
33
|
-
calls.restoreAll += 1;
|
|
34
|
-
},
|
|
35
|
-
markDirty: (nid) => {
|
|
36
|
-
calls.markedDirty.push(nid);
|
|
37
|
-
},
|
|
38
|
-
pauseHistory: () => {
|
|
39
|
-
calls.pauseHistory += 1;
|
|
40
|
-
},
|
|
41
|
-
resumeHistory: () => {
|
|
42
|
-
calls.resumeHistory += 1;
|
|
43
|
-
},
|
|
44
|
-
getSubtree: () => null,
|
|
45
|
-
cloneNodesInto: () => null,
|
|
46
|
-
_calls: calls,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function makeDef(kind, relations) {
|
|
50
|
-
return {
|
|
51
|
-
kind,
|
|
52
|
-
schemaVersion: 1,
|
|
53
|
-
schema: z.object({ type: z.literal(kind) }),
|
|
54
|
-
category: 'utility',
|
|
55
|
-
defaults: () => ({}),
|
|
56
|
-
capabilities: {},
|
|
57
|
-
relations,
|
|
58
|
-
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
function makeAction() {
|
|
62
|
-
return {
|
|
63
|
-
begin: ({ node }) => ({ id: node?.id ?? id('default') }),
|
|
64
|
-
preview: (_ctx, point) => ({ x: point[0] }),
|
|
65
|
-
apply: (_draft, ctx) => [ctx.id],
|
|
66
|
-
cancel: () => { },
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
describe('createDragSession', () => {
|
|
70
|
-
beforeEach(() => {
|
|
71
|
-
nodeRegistry._reset();
|
|
72
|
-
});
|
|
73
|
-
test('start pauses history; commit resumes', () => {
|
|
74
|
-
const scene = makeSpyScene();
|
|
75
|
-
const session = createDragSession(makeAction(), scene);
|
|
76
|
-
session.start({ point: [0, 0] });
|
|
77
|
-
expect(scene._calls.pauseHistory).toBe(1);
|
|
78
|
-
expect(scene._calls.resumeHistory).toBe(0);
|
|
79
|
-
session.commit();
|
|
80
|
-
expect(scene._calls.resumeHistory).toBe(1);
|
|
81
|
-
});
|
|
82
|
-
test('cancel resumes history and calls restoreAll', () => {
|
|
83
|
-
const scene = makeSpyScene();
|
|
84
|
-
const session = createDragSession(makeAction(), scene);
|
|
85
|
-
session.start({ point: [0, 0] });
|
|
86
|
-
session.cancel();
|
|
87
|
-
expect(scene._calls.resumeHistory).toBe(1);
|
|
88
|
-
expect(scene._calls.restoreAll).toBe(1);
|
|
89
|
-
});
|
|
90
|
-
test('move runs preview + apply and marks the returned id dirty', () => {
|
|
91
|
-
const scene = makeSpyScene({ a: { id: id('a'), type: 'thing' } });
|
|
92
|
-
const session = createDragSession(makeAction(), scene);
|
|
93
|
-
session.start({ point: [0, 0], node: { id: id('a') } });
|
|
94
|
-
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
95
|
-
expect(session.getDraft()).toEqual({ x: 1 });
|
|
96
|
-
expect(scene._calls.markedDirty).toContain(id('a'));
|
|
97
|
-
});
|
|
98
|
-
test('snap callback is invoked when defined', () => {
|
|
99
|
-
const action = {
|
|
100
|
-
...makeAction(),
|
|
101
|
-
snap: (draft) => ({ x: Math.round(draft.x) }),
|
|
102
|
-
};
|
|
103
|
-
const scene = makeSpyScene({ a: { id: id('a'), type: 'thing' } });
|
|
104
|
-
const session = createDragSession(action, scene);
|
|
105
|
-
session.start({ point: [0, 0], node: { id: id('a') } });
|
|
106
|
-
session.move([0.7, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
107
|
-
expect(session.getDraft()).toEqual({ x: 1 });
|
|
108
|
-
});
|
|
109
|
-
test('commit returns false when action.commit returns false; calls action.cancel and restoreAll', () => {
|
|
110
|
-
const cancelSpy = mock(() => { });
|
|
111
|
-
const action = {
|
|
112
|
-
...makeAction(),
|
|
113
|
-
cancel: cancelSpy,
|
|
114
|
-
commit: () => false,
|
|
115
|
-
};
|
|
116
|
-
const scene = makeSpyScene({ a: { id: id('a'), type: 'thing' } });
|
|
117
|
-
const session = createDragSession(action, scene);
|
|
118
|
-
session.start({ point: [0, 0], node: { id: id('a') } });
|
|
119
|
-
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
120
|
-
const result = session.commit();
|
|
121
|
-
expect(result).toBe(false);
|
|
122
|
-
expect(cancelSpy).toHaveBeenCalledTimes(1);
|
|
123
|
-
expect(scene._calls.restoreAll).toBe(1);
|
|
124
|
-
});
|
|
125
|
-
test('move is a no-op when session is not active', () => {
|
|
126
|
-
const scene = makeSpyScene();
|
|
127
|
-
const session = createDragSession(makeAction(), scene);
|
|
128
|
-
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
129
|
-
expect(scene._calls.markedDirty.length).toBe(0);
|
|
130
|
-
});
|
|
131
|
-
test('repeated start is a no-op (re-entry guard)', () => {
|
|
132
|
-
const scene = makeSpyScene();
|
|
133
|
-
const session = createDragSession(makeAction(), scene);
|
|
134
|
-
session.start({ point: [0, 0] });
|
|
135
|
-
session.start({ point: [99, 99] });
|
|
136
|
-
expect(scene._calls.pauseHistory).toBe(1); // only one pause
|
|
137
|
-
});
|
|
138
|
-
test('dispose mid-drag cancels and cleans up', () => {
|
|
139
|
-
const scene = makeSpyScene();
|
|
140
|
-
const session = createDragSession(makeAction(), scene);
|
|
141
|
-
session.start({ point: [0, 0] });
|
|
142
|
-
expect(session.isActive()).toBe(true);
|
|
143
|
-
session.dispose();
|
|
144
|
-
expect(session.isActive()).toBe(false);
|
|
145
|
-
expect(scene._calls.resumeHistory).toBe(1);
|
|
146
|
-
expect(scene._calls.restoreAll).toBe(1);
|
|
147
|
-
});
|
|
148
|
-
test('dispose when inactive is a no-op', () => {
|
|
149
|
-
const scene = makeSpyScene();
|
|
150
|
-
const session = createDragSession(makeAction(), scene);
|
|
151
|
-
session.dispose();
|
|
152
|
-
expect(scene._calls.pauseHistory).toBe(0);
|
|
153
|
-
expect(scene._calls.resumeHistory).toBe(0);
|
|
154
|
-
});
|
|
155
|
-
test('onCommit callback fires on successful commit', () => {
|
|
156
|
-
const onCommit = mock(() => { });
|
|
157
|
-
const onCancel = mock(() => { });
|
|
158
|
-
const scene = makeSpyScene();
|
|
159
|
-
const session = createDragSession(makeAction(), scene, { onCommit, onCancel });
|
|
160
|
-
session.start({ point: [0, 0] });
|
|
161
|
-
session.commit();
|
|
162
|
-
expect(onCommit).toHaveBeenCalledTimes(1);
|
|
163
|
-
expect(onCancel).toHaveBeenCalledTimes(0);
|
|
164
|
-
});
|
|
165
|
-
test('onCancel callback fires on explicit cancel', () => {
|
|
166
|
-
const onCommit = mock(() => { });
|
|
167
|
-
const onCancel = mock(() => { });
|
|
168
|
-
const scene = makeSpyScene();
|
|
169
|
-
const session = createDragSession(makeAction(), scene, { onCommit, onCancel });
|
|
170
|
-
session.start({ point: [0, 0] });
|
|
171
|
-
session.cancel();
|
|
172
|
-
expect(onCommit).toHaveBeenCalledTimes(0);
|
|
173
|
-
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
174
|
-
});
|
|
175
|
-
test('dispose does NOT fire onCancel (silent cleanup)', () => {
|
|
176
|
-
const onCommit = mock(() => { });
|
|
177
|
-
const onCancel = mock(() => { });
|
|
178
|
-
const scene = makeSpyScene();
|
|
179
|
-
const session = createDragSession(makeAction(), scene, { onCommit, onCancel });
|
|
180
|
-
session.start({ point: [0, 0] });
|
|
181
|
-
session.dispose();
|
|
182
|
-
expect(onCommit).toHaveBeenCalledTimes(0);
|
|
183
|
-
expect(onCancel).toHaveBeenCalledTimes(0);
|
|
184
|
-
expect(scene._calls.resumeHistory).toBe(1);
|
|
185
|
-
expect(scene._calls.restoreAll).toBe(1);
|
|
186
|
-
});
|
|
187
|
-
test('dirty cascade fires once per id even across multiple move ticks', () => {
|
|
188
|
-
// Register a kind with no relations — cascade returns just {startId}.
|
|
189
|
-
registerNode(makeDef('thing'));
|
|
190
|
-
const scene = makeSpyScene({ a: { id: id('a'), type: 'thing' } });
|
|
191
|
-
const session = createDragSession(makeAction(), scene);
|
|
192
|
-
session.start({ point: [0, 0], node: { id: id('a') } });
|
|
193
|
-
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
194
|
-
session.move([2, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
195
|
-
session.move([3, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
196
|
-
// a is marked once, not three times
|
|
197
|
-
expect(scene._calls.markedDirty.filter((mid) => mid === id('a')).length).toBe(1);
|
|
198
|
-
});
|
|
199
|
-
test('dirty cascade follows hosts relations from the registry', () => {
|
|
200
|
-
registerNode(makeDef('wall', { hosts: ['door'] }));
|
|
201
|
-
registerNode(makeDef('door'));
|
|
202
|
-
const scene = makeSpyScene({
|
|
203
|
-
w: { id: id('w'), type: 'wall', children: [id('d')] },
|
|
204
|
-
d: { id: id('d'), type: 'door', parentId: id('w') },
|
|
205
|
-
});
|
|
206
|
-
const action = {
|
|
207
|
-
begin: () => ({ id: id('w') }),
|
|
208
|
-
preview: (_ctx, point) => ({ x: point[0] }),
|
|
209
|
-
apply: (_draft, ctx) => [ctx.id],
|
|
210
|
-
cancel: () => { },
|
|
211
|
-
};
|
|
212
|
-
const session = createDragSession(action, scene);
|
|
213
|
-
session.start({ point: [0, 0], node: { id: id('w') } });
|
|
214
|
-
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
215
|
-
// both wall and door marked dirty
|
|
216
|
-
expect(scene._calls.markedDirty).toContain(id('w'));
|
|
217
|
-
expect(scene._calls.markedDirty).toContain(id('d'));
|
|
218
|
-
});
|
|
219
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hosting.test.d.ts","sourceRoot":"","sources":["../../src/services/hosting.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { nodeRegistry, registerNode } from '../registry/registry';
|
|
4
|
-
import { canAttach, canHostOnTop, clampYToHostTop, getSurface, getTopSurfaceHeight, MAX_HOST_DEPTH, pickHost, } from './hosting';
|
|
5
|
-
const id = (s) => s;
|
|
6
|
-
function makeItem(idStr, attachTo) {
|
|
7
|
-
return {
|
|
8
|
-
id: id(idStr),
|
|
9
|
-
type: 'item',
|
|
10
|
-
parentId: null,
|
|
11
|
-
visible: true,
|
|
12
|
-
asset: attachTo ? { attachTo } : {},
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
function makeDef(kind, capabilities = {}, overrides = {}) {
|
|
16
|
-
return {
|
|
17
|
-
kind,
|
|
18
|
-
schemaVersion: 1,
|
|
19
|
-
schema: z.object({ type: z.literal(kind) }),
|
|
20
|
-
category: 'utility',
|
|
21
|
-
defaults: () => ({}),
|
|
22
|
-
capabilities,
|
|
23
|
-
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
24
|
-
...overrides,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
function makeNode(kind, idStr, parentId = null) {
|
|
28
|
-
return {
|
|
29
|
-
id: id(idStr),
|
|
30
|
-
type: kind,
|
|
31
|
-
parentId: parentId ? id(parentId) : null,
|
|
32
|
-
visible: true,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function makeFakeScene(nodes) {
|
|
36
|
-
return {
|
|
37
|
-
get: ((nid) => nodes[nid]),
|
|
38
|
-
nodes: () => nodes,
|
|
39
|
-
update: () => { },
|
|
40
|
-
upsert: () => id(''),
|
|
41
|
-
delete: () => { },
|
|
42
|
-
restore: () => { },
|
|
43
|
-
restoreAll: () => { },
|
|
44
|
-
markDirty: () => { },
|
|
45
|
-
pauseHistory: () => { },
|
|
46
|
-
resumeHistory: () => { },
|
|
47
|
-
getSubtree: () => null,
|
|
48
|
-
cloneNodesInto: () => null,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
describe('canAttach', () => {
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
nodeRegistry._reset();
|
|
54
|
-
});
|
|
55
|
-
test('rejects self-host', () => {
|
|
56
|
-
const scene = makeFakeScene({ a: makeNode('thing', 'a') });
|
|
57
|
-
const result = canAttach(id('a'), id('a'), scene);
|
|
58
|
-
expect(result.ok).toBe(false);
|
|
59
|
-
if (!result.ok)
|
|
60
|
-
expect(result.error.kind).toBe('self-host');
|
|
61
|
-
});
|
|
62
|
-
test('rejects missing host', () => {
|
|
63
|
-
const scene = makeFakeScene({ a: makeNode('thing', 'a') });
|
|
64
|
-
const result = canAttach(id('a'), id('missing'), scene);
|
|
65
|
-
expect(result.ok).toBe(false);
|
|
66
|
-
if (!result.ok)
|
|
67
|
-
expect(result.error.kind).toBe('host-missing');
|
|
68
|
-
});
|
|
69
|
-
test('detects cycle when host is a descendant of child', () => {
|
|
70
|
-
// child=a, host=b, b's parent chain leads back to a → cycle.
|
|
71
|
-
const scene = makeFakeScene({
|
|
72
|
-
a: makeNode('thing', 'a'),
|
|
73
|
-
b: makeNode('thing', 'b', 'c'),
|
|
74
|
-
c: makeNode('thing', 'c', 'a'), // c.parent = a, b.parent = c → cycle if a → b
|
|
75
|
-
});
|
|
76
|
-
const result = canAttach(id('a'), id('b'), scene);
|
|
77
|
-
expect(result.ok).toBe(false);
|
|
78
|
-
if (!result.ok)
|
|
79
|
-
expect(result.error.kind).toBe('cycle');
|
|
80
|
-
});
|
|
81
|
-
test('rejects when chain would exceed MAX_HOST_DEPTH', () => {
|
|
82
|
-
const nodes = {};
|
|
83
|
-
for (let i = 0; i <= MAX_HOST_DEPTH; i++) {
|
|
84
|
-
nodes[`n${i}`] = makeNode('thing', `n${i}`, i === 0 ? null : `n${i - 1}`);
|
|
85
|
-
}
|
|
86
|
-
// n6 is already MAX_HOST_DEPTH deep — attaching n_new beneath it would
|
|
87
|
-
// push the child to MAX_HOST_DEPTH + 1.
|
|
88
|
-
nodes.candidate = makeNode('thing', 'candidate');
|
|
89
|
-
const scene = makeFakeScene(nodes);
|
|
90
|
-
const result = canAttach(id('candidate'), id(`n${MAX_HOST_DEPTH}`), scene);
|
|
91
|
-
expect(result.ok).toBe(false);
|
|
92
|
-
if (!result.ok)
|
|
93
|
-
expect(result.error.kind).toBe('depth-exceeded');
|
|
94
|
-
});
|
|
95
|
-
test('accepts attach when chain stays within MAX_HOST_DEPTH', () => {
|
|
96
|
-
const scene = makeFakeScene({
|
|
97
|
-
root: makeNode('thing', 'root'),
|
|
98
|
-
candidate: makeNode('thing', 'candidate'),
|
|
99
|
-
});
|
|
100
|
-
expect(canAttach(id('candidate'), id('root'), scene).ok).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
test('rejects host kind not in child def.hostable.parents', () => {
|
|
103
|
-
registerNode(makeDef('shelf', { hostable: { parents: ['wall', 'slab'] } }));
|
|
104
|
-
const scene = makeFakeScene({
|
|
105
|
-
s: makeNode('shelf', 's'),
|
|
106
|
-
ceiling: makeNode('ceiling', 'ceiling'),
|
|
107
|
-
});
|
|
108
|
-
const result = canAttach(id('s'), id('ceiling'), scene);
|
|
109
|
-
expect(result.ok).toBe(false);
|
|
110
|
-
if (!result.ok)
|
|
111
|
-
expect(result.error.kind).toBe('kind-not-allowed');
|
|
112
|
-
});
|
|
113
|
-
test('accepts when host kind is in parents', () => {
|
|
114
|
-
registerNode(makeDef('shelf', { hostable: { parents: ['wall', 'slab'] } }));
|
|
115
|
-
const scene = makeFakeScene({
|
|
116
|
-
s: makeNode('shelf', 's'),
|
|
117
|
-
w: makeNode('wall', 'w'),
|
|
118
|
-
});
|
|
119
|
-
expect(canAttach(id('s'), id('w'), scene).ok).toBe(true);
|
|
120
|
-
});
|
|
121
|
-
test('no def or no hostable.parents = no kind restriction', () => {
|
|
122
|
-
// Some kinds (e.g. items via catalog) defer to runtime checks instead of
|
|
123
|
-
// declaring parents up front. canAttach should not block them.
|
|
124
|
-
const scene = makeFakeScene({
|
|
125
|
-
i: makeNode('item', 'i'),
|
|
126
|
-
w: makeNode('wall', 'w'),
|
|
127
|
-
});
|
|
128
|
-
expect(canAttach(id('i'), id('w'), scene).ok).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
test('allows missing child (placement preview before commit)', () => {
|
|
131
|
-
const scene = makeFakeScene({ w: makeNode('wall', 'w') });
|
|
132
|
-
expect(canAttach(id('future-child'), id('w'), scene).ok).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe('getSurface / getTopSurfaceHeight', () => {
|
|
136
|
-
beforeEach(() => {
|
|
137
|
-
nodeRegistry._reset();
|
|
138
|
-
});
|
|
139
|
-
test('returns null when host has no registered def', () => {
|
|
140
|
-
const node = makeNode('mystery', 'm');
|
|
141
|
-
expect(getSurface(node)).toBeNull();
|
|
142
|
-
expect(getTopSurfaceHeight(node)).toBeNull();
|
|
143
|
-
});
|
|
144
|
-
test('returns surface config when declared', () => {
|
|
145
|
-
registerNode(makeDef('table', {
|
|
146
|
-
surfaces: { top: { height: 0.74 } },
|
|
147
|
-
}));
|
|
148
|
-
const t = makeNode('table', 't');
|
|
149
|
-
expect(getSurface(t)?.top?.height).toBe(0.74);
|
|
150
|
-
expect(getTopSurfaceHeight(t)).toBe(0.74);
|
|
151
|
-
});
|
|
152
|
-
test('evaluates function-valued height with the node', () => {
|
|
153
|
-
registerNode(makeDef('shelf', {
|
|
154
|
-
surfaces: {
|
|
155
|
-
top: {
|
|
156
|
-
height: (n) => (n.id === id('high') ? 1.8 : 0.3),
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
}));
|
|
160
|
-
expect(getTopSurfaceHeight(makeNode('shelf', 'high'))).toBe(1.8);
|
|
161
|
-
expect(getTopSurfaceHeight(makeNode('shelf', 'low'))).toBe(0.3);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
describe('clampYToHostTop', () => {
|
|
165
|
-
beforeEach(() => {
|
|
166
|
-
nodeRegistry._reset();
|
|
167
|
-
});
|
|
168
|
-
test('clamps to top when host has one', () => {
|
|
169
|
-
registerNode(makeDef('table', { surfaces: { top: { height: 0.74 } } }));
|
|
170
|
-
expect(clampYToHostTop(makeNode('table', 't'), 5)).toBe(0.74);
|
|
171
|
-
});
|
|
172
|
-
test('passes through when host has no top surface', () => {
|
|
173
|
-
registerNode(makeDef('plain'));
|
|
174
|
-
expect(clampYToHostTop(makeNode('plain', 'p'), 5)).toBe(5);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
describe('pickHost', () => {
|
|
178
|
-
beforeEach(() => {
|
|
179
|
-
nodeRegistry._reset();
|
|
180
|
-
});
|
|
181
|
-
test('returns first candidate that has hostable capability', () => {
|
|
182
|
-
registerNode(makeDef('slab', { hostable: { parents: ['*'] } }));
|
|
183
|
-
registerNode(makeDef('item')); // no hostable
|
|
184
|
-
const candidates = [makeNode('item', 'i'), makeNode('slab', 's')];
|
|
185
|
-
const picked = pickHost({
|
|
186
|
-
point: [0, 0, 0],
|
|
187
|
-
candidates,
|
|
188
|
-
placedKind: 'chair',
|
|
189
|
-
});
|
|
190
|
-
expect(picked?.id).toBe(id('s'));
|
|
191
|
-
});
|
|
192
|
-
test('returns null when nothing in candidates is hostable', () => {
|
|
193
|
-
registerNode(makeDef('item'));
|
|
194
|
-
const candidates = [makeNode('item', 'i')];
|
|
195
|
-
expect(pickHost({ point: [0, 0, 0], candidates, placedKind: 'chair' })).toBeNull();
|
|
196
|
-
});
|
|
197
|
-
test('hitTest can reject hostable candidates', () => {
|
|
198
|
-
registerNode(makeDef('slab', { hostable: { parents: ['*'] } }));
|
|
199
|
-
const candidates = [makeNode('slab', 's1'), makeNode('slab', 's2')];
|
|
200
|
-
const picked = pickHost({
|
|
201
|
-
point: [0, 0, 0],
|
|
202
|
-
candidates,
|
|
203
|
-
placedKind: 'chair',
|
|
204
|
-
hitTest: (host) => host.id === id('s2'),
|
|
205
|
-
});
|
|
206
|
-
expect(picked?.id).toBe(id('s2'));
|
|
207
|
-
});
|
|
208
|
-
test('excludes ceiling-mounted hosts (ceiling fan cannot be a top surface)', () => {
|
|
209
|
-
registerNode(makeDef('item', { hostable: { parents: ['*'] } }));
|
|
210
|
-
const candidates = [makeItem('fan', 'ceiling'), makeItem('table')];
|
|
211
|
-
const picked = pickHost({ point: [0, 0, 0], candidates, placedKind: 'item' });
|
|
212
|
-
expect(picked?.id).toBe(id('table'));
|
|
213
|
-
});
|
|
214
|
-
test('keeps wall-mounted hosts (wall shelf still hosts)', () => {
|
|
215
|
-
registerNode(makeDef('item', { hostable: { parents: ['*'] } }));
|
|
216
|
-
const candidates = [makeItem('shelf', 'wall')];
|
|
217
|
-
const picked = pickHost({ point: [0, 0, 0], candidates, placedKind: 'item' });
|
|
218
|
-
expect(picked?.id).toBe(id('shelf'));
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
describe('canHostOnTop', () => {
|
|
222
|
-
test('rejects ceiling-attachTo hosts', () => {
|
|
223
|
-
expect(canHostOnTop(makeItem('fan', 'ceiling'))).toBe(false);
|
|
224
|
-
});
|
|
225
|
-
test('accepts wall / wall-side / floor (undefined) hosts', () => {
|
|
226
|
-
expect(canHostOnTop(makeItem('shelf', 'wall'))).toBe(true);
|
|
227
|
-
expect(canHostOnTop(makeItem('sconce', 'wall-side'))).toBe(true);
|
|
228
|
-
expect(canHostOnTop(makeItem('table'))).toBe(true);
|
|
229
|
-
});
|
|
230
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"movement.test.d.ts","sourceRoot":"","sources":["../../src/services/movement.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { nodeRegistry, registerNode } from '../registry/registry';
|
|
4
|
-
import { applyAxisLock, isMovable, movePlanToward, moveToward, resolveMovable } from './movement';
|
|
5
|
-
const id = (s) => s;
|
|
6
|
-
function makeDef(kind, capabilities = {}) {
|
|
7
|
-
return {
|
|
8
|
-
kind,
|
|
9
|
-
schemaVersion: 1,
|
|
10
|
-
schema: z.object({ type: z.literal(kind) }),
|
|
11
|
-
category: 'utility',
|
|
12
|
-
defaults: () => ({}),
|
|
13
|
-
capabilities,
|
|
14
|
-
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
function makeNode(kind, idStr) {
|
|
18
|
-
return {
|
|
19
|
-
id: id(idStr),
|
|
20
|
-
type: kind,
|
|
21
|
-
parentId: null,
|
|
22
|
-
visible: true,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
describe('resolveMovable', () => {
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
nodeRegistry._reset();
|
|
28
|
-
});
|
|
29
|
-
test('returns null when no def is registered', () => {
|
|
30
|
-
expect(resolveMovable(makeNode('mystery', 'm'))).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
test('returns null when def declares no movable capability', () => {
|
|
33
|
-
registerNode(makeDef('static'));
|
|
34
|
-
expect(resolveMovable(makeNode('static', 's'))).toBeNull();
|
|
35
|
-
});
|
|
36
|
-
test('returns the declared config', () => {
|
|
37
|
-
registerNode(makeDef('column', { movable: { axes: ['x', 'z'], gridSnap: true } }));
|
|
38
|
-
const config = resolveMovable(makeNode('column', 'c'));
|
|
39
|
-
expect(config?.axes).toEqual(['x', 'z']);
|
|
40
|
-
expect(config?.gridSnap).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
test('runs override callback when present', () => {
|
|
43
|
-
let overrideRan = false;
|
|
44
|
-
const config = {
|
|
45
|
-
axes: ['x'],
|
|
46
|
-
override: () => {
|
|
47
|
-
overrideRan = true;
|
|
48
|
-
return { axes: ['y'] };
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
registerNode(makeDef('weird', { movable: config }));
|
|
52
|
-
const resolved = resolveMovable(makeNode('weird', 'w'));
|
|
53
|
-
expect(overrideRan).toBe(true);
|
|
54
|
-
expect(resolved?.axes).toEqual(['y']);
|
|
55
|
-
});
|
|
56
|
-
test('override returning null falls back to base config', () => {
|
|
57
|
-
registerNode(makeDef('column', {
|
|
58
|
-
movable: { axes: ['x', 'z'], override: () => null },
|
|
59
|
-
}));
|
|
60
|
-
const resolved = resolveMovable(makeNode('column', 'c'));
|
|
61
|
-
expect(resolved?.axes).toEqual(['x', 'z']);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
describe('applyAxisLock', () => {
|
|
65
|
-
test('passes through unlocked axes only', () => {
|
|
66
|
-
expect(applyAxisLock([1, 2, 3], [10, 20, 30], ['x'])).toEqual([10, 2, 3]);
|
|
67
|
-
expect(applyAxisLock([1, 2, 3], [10, 20, 30], ['x', 'z'])).toEqual([10, 2, 30]);
|
|
68
|
-
expect(applyAxisLock([1, 2, 3], [10, 20, 30], ['x', 'y', 'z'])).toEqual([10, 20, 30]);
|
|
69
|
-
});
|
|
70
|
-
test('empty lock returns current unchanged', () => {
|
|
71
|
-
expect(applyAxisLock([1, 2, 3], [10, 20, 30], [])).toEqual([1, 2, 3]);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe('moveToward', () => {
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
nodeRegistry._reset();
|
|
77
|
-
});
|
|
78
|
-
test('returns null when node is not movable', () => {
|
|
79
|
-
registerNode(makeDef('static'));
|
|
80
|
-
expect(moveToward(makeNode('static', 's'), [0, 0, 0], [1, 1, 1])).toBeNull();
|
|
81
|
-
});
|
|
82
|
-
test('axis-locks and returns the constrained target', () => {
|
|
83
|
-
registerNode(makeDef('column', { movable: { axes: ['x', 'z'] } }));
|
|
84
|
-
expect(moveToward(makeNode('column', 'c'), [0, 0.5, 0], [1, 99, 2])).toEqual([1, 0.5, 2]);
|
|
85
|
-
});
|
|
86
|
-
test('applies grid snap when capability declares gridSnap: true', () => {
|
|
87
|
-
registerNode(makeDef('column', { movable: { axes: ['x', 'z'], gridSnap: true } }));
|
|
88
|
-
const result = moveToward(makeNode('column', 'c'), [0, 0, 0], [0.3, 0, 0.6], {
|
|
89
|
-
gridStep: 0.25,
|
|
90
|
-
});
|
|
91
|
-
expect(result).toEqual([0.25, 0, 0.5]);
|
|
92
|
-
});
|
|
93
|
-
test('grid snap can be overridden at call site', () => {
|
|
94
|
-
registerNode(makeDef('column', { movable: { axes: ['x', 'z'], gridSnap: true } }));
|
|
95
|
-
// Caller explicitly disables grid snap for this call
|
|
96
|
-
const result = moveToward(makeNode('column', 'c'), [0, 0, 0], [0.3, 0, 0.6], {
|
|
97
|
-
gridSnap: false,
|
|
98
|
-
});
|
|
99
|
-
expect(result).toEqual([0.3, 0, 0.6]);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe('movePlanToward', () => {
|
|
103
|
-
beforeEach(() => {
|
|
104
|
-
nodeRegistry._reset();
|
|
105
|
-
});
|
|
106
|
-
test('returns 2D point with X/Z constrained, Y dropped', () => {
|
|
107
|
-
registerNode(makeDef('column', { movable: { axes: ['x', 'z'], gridSnap: true } }));
|
|
108
|
-
const result = movePlanToward(makeNode('column', 'c'), 0.5, // currentY
|
|
109
|
-
[0, 0], [0.3, 0.6], { gridStep: 0.25 });
|
|
110
|
-
expect(result).toEqual([0.25, 0.5]);
|
|
111
|
-
});
|
|
112
|
-
test('returns null when node is not movable', () => {
|
|
113
|
-
registerNode(makeDef('static'));
|
|
114
|
-
expect(movePlanToward(makeNode('static', 's'), 0, [0, 0], [1, 1])).toBeNull();
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
describe('isMovable', () => {
|
|
118
|
-
beforeEach(() => {
|
|
119
|
-
nodeRegistry._reset();
|
|
120
|
-
});
|
|
121
|
-
test('true when movable.axes has entries', () => {
|
|
122
|
-
registerNode(makeDef('column', { movable: { axes: ['x'] } }));
|
|
123
|
-
expect(isMovable(makeNode('column', 'c'))).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
test('false when no movable capability', () => {
|
|
126
|
-
registerNode(makeDef('static'));
|
|
127
|
-
expect(isMovable(makeNode('static', 's'))).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
test('false when movable.axes is empty', () => {
|
|
130
|
-
registerNode(makeDef('locked', { movable: { axes: [] } }));
|
|
131
|
-
expect(isMovable(makeNode('locked', 'l'))).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"opening-guides.test.d.ts","sourceRoot":"","sources":["../../src/services/opening-guides.test.ts"],"names":[],"mappings":""}
|