@pascal-app/core 0.8.0 → 0.9.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/dist/events/bus.d.ts +39 -13
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +6 -22
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +33 -26
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +273 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +13 -2
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +95 -9
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +91 -18
- package/dist/index.d.ts +22 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/lib/polygon-relations.d.ts +10 -0
- package/dist/lib/polygon-relations.d.ts.map +1 -0
- package/dist/lib/polygon-relations.js +73 -0
- package/dist/lib/space-detection-pause.test.d.ts +2 -0
- package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
- package/dist/lib/space-detection-pause.test.js +42 -0
- package/dist/lib/space-detection.d.ts +30 -1
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +215 -36
- package/dist/lib/space-detection.test.d.ts +2 -0
- package/dist/lib/space-detection.test.d.ts.map +1 -0
- package/dist/lib/space-detection.test.js +77 -0
- package/dist/material-library.d.ts +1 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +1940 -143
- package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
- package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
- package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
- package/dist/registry/handles.d.ts +321 -0
- package/dist/registry/handles.d.ts.map +1 -0
- package/dist/registry/handles.js +24 -0
- package/dist/registry/index.d.ts +7 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +4 -0
- package/dist/registry/registry.d.ts +93 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +201 -0
- package/dist/registry/registry.test.d.ts +2 -0
- package/dist/registry/registry.test.d.ts.map +1 -0
- package/dist/registry/registry.test.js +183 -0
- package/dist/registry/relations-resolver.d.ts +42 -0
- package/dist/registry/relations-resolver.d.ts.map +1 -0
- package/dist/registry/relations-resolver.js +77 -0
- package/dist/registry/relations-resolver.test.d.ts +2 -0
- package/dist/registry/relations-resolver.test.d.ts.map +1 -0
- package/dist/registry/relations-resolver.test.js +183 -0
- package/dist/registry/scene-api.d.ts +44 -0
- package/dist/registry/scene-api.d.ts.map +1 -0
- package/dist/registry/scene-api.js +111 -0
- package/dist/registry/scene-api.test.d.ts +2 -0
- package/dist/registry/scene-api.test.d.ts.map +1 -0
- package/dist/registry/scene-api.test.js +183 -0
- package/dist/registry/subtree.d.ts +67 -0
- package/dist/registry/subtree.d.ts.map +1 -0
- package/dist/registry/subtree.js +119 -0
- package/dist/registry/subtree.test.d.ts +2 -0
- package/dist/registry/subtree.test.d.ts.map +1 -0
- package/dist/registry/subtree.test.js +133 -0
- package/dist/registry/types.d.ts +1420 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +1 -0
- package/dist/schema/index.d.ts +17 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +17 -3
- package/dist/schema/material.d.ts +10 -0
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +10 -0
- package/dist/schema/nodes/box-vent.d.ts +74 -0
- package/dist/schema/nodes/box-vent.d.ts.map +1 -0
- package/dist/schema/nodes/box-vent.js +63 -0
- package/dist/schema/nodes/building.d.ts +1 -1
- package/dist/schema/nodes/building.d.ts.map +1 -1
- package/dist/schema/nodes/building.js +3 -2
- package/dist/schema/nodes/ceiling.d.ts +2 -0
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +1 -1
- package/dist/schema/nodes/chimney.d.ts +158 -0
- package/dist/schema/nodes/chimney.d.ts.map +1 -0
- package/dist/schema/nodes/chimney.js +67 -0
- package/dist/schema/nodes/column.d.ts +597 -5
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +586 -6
- package/dist/schema/nodes/cupola.d.ts +66 -0
- package/dist/schema/nodes/cupola.d.ts.map +1 -0
- package/dist/schema/nodes/cupola.js +28 -0
- package/dist/schema/nodes/dormer.d.ts +228 -0
- package/dist/schema/nodes/dormer.d.ts.map +1 -0
- package/dist/schema/nodes/dormer.js +119 -0
- package/dist/schema/nodes/downspout.d.ts +75 -0
- package/dist/schema/nodes/downspout.d.ts.map +1 -0
- package/dist/schema/nodes/downspout.js +63 -0
- package/dist/schema/nodes/elevator.d.ts +105 -0
- package/dist/schema/nodes/elevator.d.ts.map +1 -0
- package/dist/schema/nodes/elevator.js +54 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
- package/dist/schema/nodes/eyebrow-vent.js +40 -0
- package/dist/schema/nodes/fence.d.ts +1 -0
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +2 -0
- package/dist/schema/nodes/gutter.d.ts +84 -0
- package/dist/schema/nodes/gutter.d.ts.map +1 -0
- package/dist/schema/nodes/gutter.js +76 -0
- package/dist/schema/nodes/item.d.ts +4 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +9 -0
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +2 -0
- package/dist/schema/nodes/ridge-vent.d.ts +67 -0
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
- package/dist/schema/nodes/ridge-vent.js +30 -0
- package/dist/schema/nodes/roof-segment.d.ts +195 -1
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +254 -3
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +4 -16
- package/dist/schema/nodes/shelf.d.ts +105 -0
- package/dist/schema/nodes/shelf.d.ts.map +1 -0
- package/dist/schema/nodes/shelf.js +84 -0
- package/dist/schema/nodes/site.d.ts +1 -122
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/site.js +2 -6
- package/dist/schema/nodes/skylight.d.ts +279 -0
- package/dist/schema/nodes/skylight.d.ts.map +1 -0
- package/dist/schema/nodes/skylight.js +152 -0
- package/dist/schema/nodes/slab.d.ts +2 -0
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +1 -1
- package/dist/schema/nodes/solar-panel.d.ts +117 -0
- package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
- package/dist/schema/nodes/solar-panel.js +47 -0
- package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
- package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
- package/dist/schema/nodes/surface-hole-metadata.js +3 -1
- package/dist/schema/nodes/turbine-vent.d.ts +68 -0
- package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
- package/dist/schema/nodes/turbine-vent.js +41 -0
- package/dist/schema/types.d.ts +1325 -125
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +26 -0
- package/dist/services/alignment-anchors.d.ts +100 -0
- package/dist/services/alignment-anchors.d.ts.map +1 -0
- package/dist/services/alignment-anchors.js +283 -0
- package/dist/services/alignment-anchors.test.d.ts +2 -0
- package/dist/services/alignment-anchors.test.d.ts.map +1 -0
- package/dist/services/alignment-anchors.test.js +383 -0
- package/dist/services/alignment.d.ts +126 -0
- package/dist/services/alignment.d.ts.map +1 -0
- package/dist/services/alignment.js +169 -0
- package/dist/services/alignment.test.d.ts +2 -0
- package/dist/services/alignment.test.d.ts.map +1 -0
- package/dist/services/alignment.test.js +92 -0
- package/dist/services/drag-session.d.ts +47 -0
- package/dist/services/drag-session.d.ts.map +1 -0
- package/dist/services/drag-session.js +100 -0
- package/dist/services/drag-session.test.d.ts +2 -0
- package/dist/services/drag-session.test.d.ts.map +1 -0
- package/dist/services/drag-session.test.js +219 -0
- package/dist/services/hosting.d.ts +80 -0
- package/dist/services/hosting.d.ts.map +1 -0
- package/dist/services/hosting.js +122 -0
- package/dist/services/hosting.test.d.ts +2 -0
- package/dist/services/hosting.test.d.ts.map +1 -0
- package/dist/services/hosting.test.js +199 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +6 -0
- package/dist/services/movement.d.ts +48 -0
- package/dist/services/movement.d.ts.map +1 -0
- package/dist/services/movement.js +65 -0
- package/dist/services/movement.test.d.ts +2 -0
- package/dist/services/movement.test.d.ts.map +1 -0
- package/dist/services/movement.test.js +133 -0
- package/dist/services/single-undo-dance.test.d.ts +2 -0
- package/dist/services/single-undo-dance.test.d.ts.map +1 -0
- package/dist/services/single-undo-dance.test.js +210 -0
- package/dist/services/snap.d.ts +72 -0
- package/dist/services/snap.d.ts.map +1 -0
- package/dist/services/snap.js +120 -0
- package/dist/services/snap.test.d.ts +2 -0
- package/dist/services/snap.test.d.ts.map +1 -0
- package/dist/services/snap.test.js +95 -0
- package/dist/solar-panel-presets.d.ts +17 -0
- package/dist/solar-panel-presets.d.ts.map +1 -0
- package/dist/solar-panel-presets.js +43 -0
- package/dist/store/actions/node-actions.d.ts +14 -2
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +492 -15
- package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
- package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
- package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
- package/dist/store/actions/reparent.test.d.ts +2 -0
- package/dist/store/actions/reparent.test.d.ts.map +1 -0
- package/dist/store/actions/reparent.test.js +212 -0
- package/dist/store/use-interactive.d.ts +39 -0
- package/dist/store/use-interactive.d.ts.map +1 -1
- package/dist/store/use-interactive.js +75 -0
- package/dist/store/use-live-node-overrides.d.ts +22 -0
- package/dist/store/use-live-node-overrides.d.ts.map +1 -0
- package/dist/store/use-live-node-overrides.js +59 -0
- package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
- package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-elevator-migration.test.js +109 -0
- package/dist/store/use-scene.d.ts +11 -0
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +196 -6
- package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
- package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-dispatch.js +57 -0
- package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
- package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-geometry.js +50 -0
- package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
- package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-opening-sync.js +218 -0
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-opening-system.js +48 -0
- package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-runtime-system.js +8 -0
- package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
- package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-runtime.js +225 -0
- package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
- package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-runtime.test.js +64 -0
- package/dist/systems/elevator/elevator-service.d.ts +19 -0
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-service.js +111 -0
- package/dist/systems/stair/stair-footprint.d.ts +44 -0
- package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
- package/dist/systems/stair/stair-footprint.js +168 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +1777 -0
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-preview.js +93 -0
- package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
- package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-preview.test.js +80 -0
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +172 -147
- package/dist/systems/stair/stair-opening-sync.test.js +385 -2
- package/dist/systems/stair/stair-opening-system.d.ts +2 -0
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-system.js +104 -0
- package/dist/systems/wall/wall-move.d.ts +48 -0
- package/dist/systems/wall/wall-move.d.ts.map +1 -0
- package/dist/systems/wall/wall-move.js +193 -0
- package/dist/utils/clone-scene-graph.d.ts +6 -4
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +6 -4
- package/dist/utils/clone-scene-graph.test.d.ts +2 -0
- package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
- package/dist/utils/clone-scene-graph.test.js +59 -0
- package/dist/validation/validate-build-json.d.ts +43 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -0
- package/dist/validation/validate-build-json.js +227 -0
- package/package.json +9 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const AXIS_EPSILON = 1e-6;
|
|
2
|
+
export function getPerpendicularWallMoveAxis(start, end) {
|
|
3
|
+
const dx = end[0] - start[0];
|
|
4
|
+
const dz = end[1] - start[1];
|
|
5
|
+
const length = Math.hypot(dx, dz);
|
|
6
|
+
if (length < AXIS_EPSILON)
|
|
7
|
+
return null;
|
|
8
|
+
// Perpendicular (normal) direction for moving the wall "sideways".
|
|
9
|
+
// This matches the arrow handles shown in the editor.
|
|
10
|
+
return [-dz / length, dx / length];
|
|
11
|
+
}
|
|
12
|
+
export function constrainWallMoveDeltaToAxis(deltaX, deltaZ, axis) {
|
|
13
|
+
if (axis) {
|
|
14
|
+
const projected = deltaX * axis[0] + deltaZ * axis[1];
|
|
15
|
+
return [axis[0] * projected, axis[1] * projected];
|
|
16
|
+
}
|
|
17
|
+
return [deltaX, deltaZ];
|
|
18
|
+
}
|
|
19
|
+
function pointsEqual(a, b) {
|
|
20
|
+
return Math.abs(a[0] - b[0]) <= AXIS_EPSILON && Math.abs(a[1] - b[1]) <= AXIS_EPSILON;
|
|
21
|
+
}
|
|
22
|
+
function wallTouchesPoint(wall, point) {
|
|
23
|
+
return pointsEqual(wall.start, point) || pointsEqual(wall.end, point);
|
|
24
|
+
}
|
|
25
|
+
function otherWallEndpoint(wall, point) {
|
|
26
|
+
return pointsEqual(wall.start, point) ? wall.end : wall.start;
|
|
27
|
+
}
|
|
28
|
+
function wallLengthFromPoint(wall, point) {
|
|
29
|
+
const freeEndpoint = otherWallEndpoint(wall, point);
|
|
30
|
+
return Math.hypot(freeEndpoint[0] - point[0], freeEndpoint[1] - point[1]);
|
|
31
|
+
}
|
|
32
|
+
function getMoveWallRelation(wall, sharedPoint, nextPoint) {
|
|
33
|
+
const moveX = nextPoint[0] - sharedPoint[0];
|
|
34
|
+
const moveZ = nextPoint[1] - sharedPoint[1];
|
|
35
|
+
const moveLength = Math.hypot(moveX, moveZ);
|
|
36
|
+
if (moveLength < AXIS_EPSILON)
|
|
37
|
+
return 'stationary';
|
|
38
|
+
const freeEndpoint = otherWallEndpoint(wall, sharedPoint);
|
|
39
|
+
const wallX = freeEndpoint[0] - sharedPoint[0];
|
|
40
|
+
const wallZ = freeEndpoint[1] - sharedPoint[1];
|
|
41
|
+
const wallLength = Math.hypot(wallX, wallZ);
|
|
42
|
+
if (wallLength < AXIS_EPSILON)
|
|
43
|
+
return 'stationary';
|
|
44
|
+
const normalizedCross = Math.abs(moveX * wallZ - moveZ * wallX) / (moveLength * wallLength);
|
|
45
|
+
if (normalizedCross > 1e-4)
|
|
46
|
+
return 'off-axis';
|
|
47
|
+
const normalizedDot = (moveX * wallX + moveZ * wallZ) / (moveLength * wallLength);
|
|
48
|
+
return normalizedDot >= 0 ? 'same-direction' : 'opposite-direction';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Apply a junction plan to a list of linked walls, producing the per-wall
|
|
52
|
+
* endpoint updates. Mirrors the 3D `MoveWallTool`'s drag-preview behavior
|
|
53
|
+
* so the 2D move handler can drive the same scene topology.
|
|
54
|
+
*
|
|
55
|
+
* `linkedWallTargetPlans` take precedence over `linkedWallsToMove` — when
|
|
56
|
+
* the planner emits a target plan for a same-direction-consumed wall the
|
|
57
|
+
* matchPoint/targetPoint pair encodes the pivot, not the original ↔ next
|
|
58
|
+
* endpoint mapping.
|
|
59
|
+
*/
|
|
60
|
+
export function getLinkedWallUpdates(linkedWalls, originalStart, originalEnd, nextStart, nextEnd) {
|
|
61
|
+
return linkedWalls.map(({ wall, matchPoint, targetPoint }) => {
|
|
62
|
+
if (matchPoint && targetPoint) {
|
|
63
|
+
return {
|
|
64
|
+
id: wall.id,
|
|
65
|
+
start: pointsEqual(wall.start, matchPoint) ? targetPoint : wall.start,
|
|
66
|
+
end: pointsEqual(wall.end, matchPoint) ? targetPoint : wall.end,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const targetStart = targetPoint ?? nextStart;
|
|
70
|
+
const targetEnd = targetPoint ?? nextEnd;
|
|
71
|
+
return {
|
|
72
|
+
id: wall.id,
|
|
73
|
+
start: pointsEqual(wall.start, originalStart)
|
|
74
|
+
? targetStart
|
|
75
|
+
: pointsEqual(wall.start, originalEnd)
|
|
76
|
+
? targetEnd
|
|
77
|
+
: wall.start,
|
|
78
|
+
end: pointsEqual(wall.end, originalStart)
|
|
79
|
+
? targetStart
|
|
80
|
+
: pointsEqual(wall.end, originalEnd)
|
|
81
|
+
? targetEnd
|
|
82
|
+
: wall.end,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function getPlannedLinkedWallUpdates(plan, originalStart, originalEnd, nextStart, nextEnd) {
|
|
87
|
+
const movePlans = new Map();
|
|
88
|
+
for (const wall of plan.linkedWallsToMove) {
|
|
89
|
+
movePlans.set(wall.id, { wall });
|
|
90
|
+
}
|
|
91
|
+
for (const targetPlan of plan.linkedWallTargetPlans) {
|
|
92
|
+
movePlans.set(targetPlan.wall.id, {
|
|
93
|
+
wall: targetPlan.wall,
|
|
94
|
+
matchPoint: targetPlan.originalPoint,
|
|
95
|
+
targetPoint: targetPlan.targetPoint,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return getLinkedWallUpdates(Array.from(movePlans.values()), originalStart, originalEnd, nextStart, nextEnd);
|
|
99
|
+
}
|
|
100
|
+
export function planWallMoveJunctions(linkedWalls, originalStart, originalEnd, nextStart, nextEnd) {
|
|
101
|
+
const linkedWallsToMove = new Map();
|
|
102
|
+
const linkedWallTargetPlans = new Map();
|
|
103
|
+
const bridgePlans = new Map();
|
|
104
|
+
const wallsToDelete = new Map();
|
|
105
|
+
const addStandardEndpointPlan = (endpoint, point, nextPoint, relatedWalls, keySuffix = '', useTargetPlans = false) => {
|
|
106
|
+
const hasSideBranch = relatedWalls.some((entry) => entry.relation === 'off-axis');
|
|
107
|
+
const hasOppositeBridge = relatedWalls.some((entry) => entry.relation === 'opposite-direction' && hasSideBranch);
|
|
108
|
+
for (const { wall, relation } of relatedWalls) {
|
|
109
|
+
if (relation === 'stationary' ||
|
|
110
|
+
relation === 'same-direction' ||
|
|
111
|
+
(relation === 'opposite-direction' && !hasSideBranch)) {
|
|
112
|
+
if (useTargetPlans) {
|
|
113
|
+
linkedWallTargetPlans.set(wall.id, {
|
|
114
|
+
wall,
|
|
115
|
+
originalPoint: point,
|
|
116
|
+
targetPoint: nextPoint,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
linkedWallsToMove.set(wall.id, wall);
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (relation === 'off-axis' && hasOppositeBridge) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
bridgePlans.set(`${wall.id}:${endpoint}${keySuffix}`, {
|
|
128
|
+
wall,
|
|
129
|
+
originalPoint: point,
|
|
130
|
+
movedEndpoint: endpoint,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const addEndpointPlan = (endpoint, point, nextPoint) => {
|
|
135
|
+
const moveLength = Math.hypot(nextPoint[0] - point[0], nextPoint[1] - point[1]);
|
|
136
|
+
const linkedAtEndpoint = linkedWalls
|
|
137
|
+
.filter((wall) => wallTouchesPoint(wall, point))
|
|
138
|
+
.map((wall) => ({
|
|
139
|
+
wall,
|
|
140
|
+
relation: getMoveWallRelation(wall, point, nextPoint),
|
|
141
|
+
}));
|
|
142
|
+
const consumedSameDirectionWall = linkedAtEndpoint
|
|
143
|
+
.filter((entry) => entry.relation === 'same-direction')
|
|
144
|
+
.map((entry) => ({
|
|
145
|
+
...entry,
|
|
146
|
+
distance: wallLengthFromPoint(entry.wall, point),
|
|
147
|
+
}))
|
|
148
|
+
.filter((entry) => moveLength + AXIS_EPSILON >= entry.distance)
|
|
149
|
+
.sort((a, b) => a.distance - b.distance)[0];
|
|
150
|
+
if (consumedSameDirectionWall) {
|
|
151
|
+
const pivotPoint = [
|
|
152
|
+
...otherWallEndpoint(consumedSameDirectionWall.wall, point),
|
|
153
|
+
];
|
|
154
|
+
const bridgeSource = linkedAtEndpoint.find((entry) => entry.relation === 'opposite-direction');
|
|
155
|
+
wallsToDelete.set(consumedSameDirectionWall.wall.id, consumedSameDirectionWall.wall);
|
|
156
|
+
linkedWallTargetPlans.set(consumedSameDirectionWall.wall.id, {
|
|
157
|
+
wall: consumedSameDirectionWall.wall,
|
|
158
|
+
originalPoint: point,
|
|
159
|
+
targetPoint: pivotPoint,
|
|
160
|
+
});
|
|
161
|
+
if (bridgeSource) {
|
|
162
|
+
linkedWallTargetPlans.set(bridgeSource.wall.id, {
|
|
163
|
+
wall: bridgeSource.wall,
|
|
164
|
+
originalPoint: point,
|
|
165
|
+
targetPoint: pivotPoint,
|
|
166
|
+
});
|
|
167
|
+
bridgePlans.set(`${bridgeSource.wall.id}:${endpoint}:through`, {
|
|
168
|
+
wall: bridgeSource.wall,
|
|
169
|
+
originalPoint: pivotPoint,
|
|
170
|
+
movedEndpoint: endpoint,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const linkedAtPivot = linkedWalls
|
|
175
|
+
.filter((wall) => wall.id !== consumedSameDirectionWall.wall.id && wallTouchesPoint(wall, pivotPoint))
|
|
176
|
+
.map((wall) => ({
|
|
177
|
+
wall,
|
|
178
|
+
relation: getMoveWallRelation(wall, pivotPoint, nextPoint),
|
|
179
|
+
}));
|
|
180
|
+
addStandardEndpointPlan(endpoint, pivotPoint, nextPoint, linkedAtPivot, ':through-pivot', true);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
addStandardEndpointPlan(endpoint, point, nextPoint, linkedAtEndpoint);
|
|
184
|
+
};
|
|
185
|
+
addEndpointPlan('start', originalStart, nextStart);
|
|
186
|
+
addEndpointPlan('end', originalEnd, nextEnd);
|
|
187
|
+
return {
|
|
188
|
+
linkedWallsToMove: Array.from(linkedWallsToMove.values()),
|
|
189
|
+
linkedWallTargetPlans: Array.from(linkedWallTargetPlans.values()),
|
|
190
|
+
bridgePlans: Array.from(bridgePlans.values()),
|
|
191
|
+
wallsToDelete: Array.from(wallsToDelete.values()),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -33,10 +33,12 @@ export declare function cloneLevelSubtree(nodes: Record<AnyNodeId, AnyNode>, lev
|
|
|
33
33
|
newLevelId: AnyNodeId;
|
|
34
34
|
idMap: Map<string, string>;
|
|
35
35
|
};
|
|
36
|
+
export type ForkSceneGraphOptions = {
|
|
37
|
+
preserveScans?: boolean;
|
|
38
|
+
};
|
|
36
39
|
/**
|
|
37
|
-
* Forks a scene graph for use as a new project: clones with new IDs and
|
|
38
|
-
* scan and guide nodes
|
|
39
|
-
* imagery that shouldn't carry over to a forked project.
|
|
40
|
+
* Forks a scene graph for use as a new project: clones with new IDs and, by
|
|
41
|
+
* default, strips scan and guide nodes since they contain user-uploaded imagery.
|
|
40
42
|
*/
|
|
41
|
-
export declare function forkSceneGraph(sceneGraph: SceneGraph): SceneGraph;
|
|
43
|
+
export declare function forkSceneGraph(sceneGraph: SceneGraph, options?: ForkSceneGraphOptions): SceneGraph;
|
|
42
44
|
//# sourceMappingURL=clone-scene-graph.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clone-scene-graph.d.ts","sourceRoot":"","sources":["../../src/utils/clone-scene-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;CAC/C,CAAA;AAUD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAuGlE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,OAAO,EAAE,SAAS,GACjB;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CA8E/E;AAED
|
|
1
|
+
{"version":3,"file":"clone-scene-graph.d.ts","sourceRoot":"","sources":["../../src/utils/clone-scene-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;CAC/C,CAAA;AAUD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAuGlE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,OAAO,EAAE,SAAS,GACjB;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CA8E/E;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,CAAA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,qBAA0B,GAClC,UAAU,CAoEZ"}
|
|
@@ -190,11 +190,13 @@ export function cloneLevelSubtree(nodes, levelId) {
|
|
|
190
190
|
return { clonedNodes, newLevelId, idMap };
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
|
-
* Forks a scene graph for use as a new project: clones with new IDs and
|
|
194
|
-
* scan and guide nodes
|
|
195
|
-
* imagery that shouldn't carry over to a forked project.
|
|
193
|
+
* Forks a scene graph for use as a new project: clones with new IDs and, by
|
|
194
|
+
* default, strips scan and guide nodes since they contain user-uploaded imagery.
|
|
196
195
|
*/
|
|
197
|
-
export function forkSceneGraph(sceneGraph) {
|
|
196
|
+
export function forkSceneGraph(sceneGraph, options = {}) {
|
|
197
|
+
if (options.preserveScans) {
|
|
198
|
+
return cloneSceneGraph(sceneGraph);
|
|
199
|
+
}
|
|
198
200
|
const { nodes, rootNodeIds, collections } = sceneGraph;
|
|
199
201
|
// First, identify scan and guide node IDs to exclude (user-uploaded imagery)
|
|
200
202
|
const excludedNodeIds = new Set();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone-scene-graph.test.d.ts","sourceRoot":"","sources":["../../src/utils/clone-scene-graph.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { forkSceneGraph } from './clone-scene-graph';
|
|
3
|
+
function makeNode(id, type, extra = {}) {
|
|
4
|
+
return {
|
|
5
|
+
object: 'node',
|
|
6
|
+
id,
|
|
7
|
+
type,
|
|
8
|
+
parentId: null,
|
|
9
|
+
visible: true,
|
|
10
|
+
metadata: {},
|
|
11
|
+
...extra,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function makeSceneGraph() {
|
|
15
|
+
const site = makeNode('site_1', 'site', { children: ['level_1'] });
|
|
16
|
+
const level = makeNode('level_1', 'level', {
|
|
17
|
+
parentId: 'site_1',
|
|
18
|
+
children: ['wall_1', 'scan_1', 'guide_1'],
|
|
19
|
+
});
|
|
20
|
+
const wall = makeNode('wall_1', 'wall', { parentId: 'level_1' });
|
|
21
|
+
const scan = makeNode('scan_1', 'scan', { parentId: 'level_1', url: 'scan.glb' });
|
|
22
|
+
const guide = makeNode('guide_1', 'guide', { parentId: 'level_1', url: 'guide.png' });
|
|
23
|
+
return {
|
|
24
|
+
nodes: {
|
|
25
|
+
['site_1']: site,
|
|
26
|
+
['level_1']: level,
|
|
27
|
+
['wall_1']: wall,
|
|
28
|
+
['scan_1']: scan,
|
|
29
|
+
['guide_1']: guide,
|
|
30
|
+
},
|
|
31
|
+
rootNodeIds: ['site_1'],
|
|
32
|
+
collections: {
|
|
33
|
+
['collection_1']: {
|
|
34
|
+
id: 'collection_1',
|
|
35
|
+
name: 'References',
|
|
36
|
+
nodeIds: ['scan_1', 'guide_1'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
describe('forkSceneGraph', () => {
|
|
42
|
+
test('strips scan and guide nodes by default', () => {
|
|
43
|
+
const forked = forkSceneGraph(makeSceneGraph());
|
|
44
|
+
const nodes = Object.values(forked.nodes);
|
|
45
|
+
expect(nodes.some((node) => node.type === 'scan')).toBe(false);
|
|
46
|
+
expect(nodes.some((node) => node.type === 'guide')).toBe(false);
|
|
47
|
+
expect(nodes.some((node) => node.type === 'wall')).toBe(true);
|
|
48
|
+
expect(forked.collections).toEqual({});
|
|
49
|
+
});
|
|
50
|
+
test('preserves scan and guide nodes when requested', () => {
|
|
51
|
+
const forked = forkSceneGraph(makeSceneGraph(), { preserveScans: true });
|
|
52
|
+
const nodes = Object.values(forked.nodes);
|
|
53
|
+
expect(nodes.some((node) => node.type === 'scan')).toBe(true);
|
|
54
|
+
expect(nodes.some((node) => node.type === 'guide')).toBe(true);
|
|
55
|
+
expect(nodes.map((node) => node.id)).not.toContain('scan_1');
|
|
56
|
+
expect(nodes.map((node) => node.id)).not.toContain('guide_1');
|
|
57
|
+
expect(Object.values(forked.collections ?? {}).flatMap((collection) => collection.nodeIds)).toHaveLength(2);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type AnyNodeType } from '../schema/types';
|
|
2
|
+
export type ValidationSeverity = 'error' | 'warning';
|
|
3
|
+
export type ValidationIssue = {
|
|
4
|
+
severity: ValidationSeverity;
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
nodeId?: string;
|
|
8
|
+
};
|
|
9
|
+
export type BuildStats = {
|
|
10
|
+
total: number;
|
|
11
|
+
byType: Partial<Record<AnyNodeType, number>>;
|
|
12
|
+
unknownTypes: Record<string, number>;
|
|
13
|
+
floorAreaM2: number;
|
|
14
|
+
};
|
|
15
|
+
export type ParsedBuildJson = {
|
|
16
|
+
nodes: Record<string, unknown>;
|
|
17
|
+
rootNodeIds: string[];
|
|
18
|
+
};
|
|
19
|
+
export type SchemaIssue = {
|
|
20
|
+
nodeId: string;
|
|
21
|
+
nodeType: string;
|
|
22
|
+
path: string;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
export type ValidateBuildJsonResult = {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
parsed: ParsedBuildJson | null;
|
|
28
|
+
stats: BuildStats;
|
|
29
|
+
errors: ValidationIssue[];
|
|
30
|
+
warnings: ValidationIssue[];
|
|
31
|
+
schemaIssues: SchemaIssue[];
|
|
32
|
+
schemaIssueCount: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Pre-flight validator for `{ nodes, rootNodeIds }` build JSON loaded via
|
|
36
|
+
* Load Build (drag-drop, IFC converter output, hand-edited files).
|
|
37
|
+
*
|
|
38
|
+
* Reports issues without mutating; the scene store still owns migration
|
|
39
|
+
* and orphan cleanup at import time. Hard errors mean the file is
|
|
40
|
+
* structurally unusable and import should be blocked.
|
|
41
|
+
*/
|
|
42
|
+
export declare function validateBuildJson(input: unknown): ValidateBuildJsonResult;
|
|
43
|
+
//# sourceMappingURL=validate-build-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-build-json.d.ts","sourceRoot":"","sources":["../../src/validation/validate-build-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE3D,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,CAAA;AAEpD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5C,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,KAAK,EAAE,UAAU,CAAA;IACjB,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,YAAY,EAAE,WAAW,EAAE,CAAA;IAC3B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AA8BD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CAuNzE"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { AnyNode } from '../schema/types';
|
|
2
|
+
const KNOWN_TYPES = new Set(AnyNode.options.map((o) => o.shape.type.parse(undefined)));
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function polygonAreaM2(points) {
|
|
7
|
+
if (points.length < 3)
|
|
8
|
+
return 0;
|
|
9
|
+
let area = 0;
|
|
10
|
+
for (let i = 0; i < points.length; i++) {
|
|
11
|
+
const a = points[i];
|
|
12
|
+
const b = points[(i + 1) % points.length];
|
|
13
|
+
if (!a || !b)
|
|
14
|
+
return 0;
|
|
15
|
+
area += a[0] * b[1] - b[0] * a[1];
|
|
16
|
+
}
|
|
17
|
+
return Math.abs(area) / 2;
|
|
18
|
+
}
|
|
19
|
+
function isPointArray(value) {
|
|
20
|
+
if (!Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
return value.every((p) => Array.isArray(p) && p.length === 2 && typeof p[0] === 'number' && typeof p[1] === 'number');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Pre-flight validator for `{ nodes, rootNodeIds }` build JSON loaded via
|
|
26
|
+
* Load Build (drag-drop, IFC converter output, hand-edited files).
|
|
27
|
+
*
|
|
28
|
+
* Reports issues without mutating; the scene store still owns migration
|
|
29
|
+
* and orphan cleanup at import time. Hard errors mean the file is
|
|
30
|
+
* structurally unusable and import should be blocked.
|
|
31
|
+
*/
|
|
32
|
+
export function validateBuildJson(input) {
|
|
33
|
+
const errors = [];
|
|
34
|
+
const warnings = [];
|
|
35
|
+
const schemaIssues = [];
|
|
36
|
+
const stats = { total: 0, byType: {}, unknownTypes: {}, floorAreaM2: 0 };
|
|
37
|
+
if (!isPlainObject(input)) {
|
|
38
|
+
errors.push({
|
|
39
|
+
severity: 'error',
|
|
40
|
+
code: 'not_an_object',
|
|
41
|
+
message: 'File is not a JSON object.',
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
parsed: null,
|
|
46
|
+
stats,
|
|
47
|
+
errors,
|
|
48
|
+
warnings,
|
|
49
|
+
schemaIssues,
|
|
50
|
+
schemaIssueCount: 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const nodesRaw = input.nodes;
|
|
54
|
+
const rootNodeIdsRaw = input.rootNodeIds;
|
|
55
|
+
if (!isPlainObject(nodesRaw)) {
|
|
56
|
+
errors.push({
|
|
57
|
+
severity: 'error',
|
|
58
|
+
code: 'missing_nodes',
|
|
59
|
+
message: 'Missing or invalid "nodes" — expected an object of id → node.',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(rootNodeIdsRaw) || !rootNodeIdsRaw.every((id) => typeof id === 'string')) {
|
|
63
|
+
errors.push({
|
|
64
|
+
severity: 'error',
|
|
65
|
+
code: 'missing_root_node_ids',
|
|
66
|
+
message: 'Missing or invalid "rootNodeIds" — expected an array of node IDs.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (errors.length > 0) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
parsed: null,
|
|
73
|
+
stats,
|
|
74
|
+
errors,
|
|
75
|
+
warnings,
|
|
76
|
+
schemaIssues,
|
|
77
|
+
schemaIssueCount: 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const nodes = nodesRaw;
|
|
81
|
+
const rootNodeIds = rootNodeIdsRaw;
|
|
82
|
+
if (rootNodeIds.length === 0) {
|
|
83
|
+
errors.push({
|
|
84
|
+
severity: 'error',
|
|
85
|
+
code: 'empty_root_node_ids',
|
|
86
|
+
message: '"rootNodeIds" is empty — no entry point into the scene.',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
let validRootCount = 0;
|
|
90
|
+
let mismatchedKeyCount = 0;
|
|
91
|
+
let schemaFailureCount = 0;
|
|
92
|
+
for (const [key, value] of Object.entries(nodes)) {
|
|
93
|
+
if (!isPlainObject(value)) {
|
|
94
|
+
warnings.push({
|
|
95
|
+
severity: 'warning',
|
|
96
|
+
code: 'node_not_object',
|
|
97
|
+
message: `Node "${key}" is not an object.`,
|
|
98
|
+
nodeId: key,
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
stats.total += 1;
|
|
103
|
+
const id = typeof value.id === 'string' ? value.id : null;
|
|
104
|
+
const type = typeof value.type === 'string' ? value.type : null;
|
|
105
|
+
const parentId = typeof value.parentId === 'string' ? value.parentId : null;
|
|
106
|
+
if (id && id !== key) {
|
|
107
|
+
mismatchedKeyCount += 1;
|
|
108
|
+
}
|
|
109
|
+
if (!type) {
|
|
110
|
+
warnings.push({
|
|
111
|
+
severity: 'warning',
|
|
112
|
+
code: 'missing_type',
|
|
113
|
+
message: `Node "${key}" has no "type" field.`,
|
|
114
|
+
nodeId: key,
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (KNOWN_TYPES.has(type)) {
|
|
119
|
+
const t = type;
|
|
120
|
+
stats.byType[t] = (stats.byType[t] ?? 0) + 1;
|
|
121
|
+
const parseResult = AnyNode.safeParse(value);
|
|
122
|
+
if (!parseResult.success) {
|
|
123
|
+
schemaFailureCount += 1;
|
|
124
|
+
const issue = parseResult.error.issues[0];
|
|
125
|
+
schemaIssues.push({
|
|
126
|
+
nodeId: key,
|
|
127
|
+
nodeType: type,
|
|
128
|
+
path: issue ? issue.path.join('.') : '',
|
|
129
|
+
message: issue ? issue.message : 'schema mismatch',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (type === 'slab') {
|
|
133
|
+
const polygon = value.polygon;
|
|
134
|
+
if (isPointArray(polygon)) {
|
|
135
|
+
let area = polygonAreaM2(polygon);
|
|
136
|
+
const holes = value.holes;
|
|
137
|
+
if (Array.isArray(holes)) {
|
|
138
|
+
for (const hole of holes) {
|
|
139
|
+
if (isPointArray(hole))
|
|
140
|
+
area -= polygonAreaM2(hole);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
stats.floorAreaM2 += Math.max(0, area);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
stats.unknownTypes[type] = (stats.unknownTypes[type] ?? 0) + 1;
|
|
149
|
+
}
|
|
150
|
+
if (parentId && !(parentId in nodes)) {
|
|
151
|
+
warnings.push({
|
|
152
|
+
severity: 'warning',
|
|
153
|
+
code: 'orphan_parent',
|
|
154
|
+
message: `Node "${key}" has parentId "${parentId}" which is not in the file (will be dropped on import).`,
|
|
155
|
+
nodeId: key,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (mismatchedKeyCount > 0) {
|
|
160
|
+
warnings.push({
|
|
161
|
+
severity: 'warning',
|
|
162
|
+
code: 'key_id_mismatch',
|
|
163
|
+
message: `${mismatchedKeyCount} node${mismatchedKeyCount === 1 ? '' : 's'} have a key that does not match their "id" field.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const unknownTypeNames = Object.keys(stats.unknownTypes);
|
|
167
|
+
if (unknownTypeNames.length > 0) {
|
|
168
|
+
const totalUnknown = unknownTypeNames.reduce((n, t) => n + stats.unknownTypes[t], 0);
|
|
169
|
+
warnings.push({
|
|
170
|
+
severity: 'warning',
|
|
171
|
+
code: 'unknown_types',
|
|
172
|
+
message: `${totalUnknown} node${totalUnknown === 1 ? '' : 's'} use unknown type${unknownTypeNames.length === 1 ? '' : 's'}: ${unknownTypeNames.join(', ')}.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (schemaFailureCount > 0) {
|
|
176
|
+
errors.push({
|
|
177
|
+
severity: 'error',
|
|
178
|
+
code: 'schema_failure',
|
|
179
|
+
message: `${schemaFailureCount} node${schemaFailureCount === 1 ? '' : 's'} did not match the expected schema. See details below — these would cause the editor to crash on load.`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
for (const id of rootNodeIds) {
|
|
183
|
+
if (id in nodes) {
|
|
184
|
+
validRootCount += 1;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
warnings.push({
|
|
188
|
+
severity: 'warning',
|
|
189
|
+
code: 'orphan_root',
|
|
190
|
+
message: `Root node "${id}" is not in the file (will be ignored on import).`,
|
|
191
|
+
nodeId: id,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (rootNodeIds.length > 0 && validRootCount === 0) {
|
|
196
|
+
errors.push({
|
|
197
|
+
severity: 'error',
|
|
198
|
+
code: 'no_valid_roots',
|
|
199
|
+
message: 'None of the rootNodeIds point to a node in the file.',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const hasBuildingOrSite = (stats.byType.building ?? 0) > 0 || (stats.byType.site ?? 0) > 0;
|
|
203
|
+
if (!hasBuildingOrSite) {
|
|
204
|
+
warnings.push({
|
|
205
|
+
severity: 'warning',
|
|
206
|
+
code: 'no_building',
|
|
207
|
+
message: 'No site or building node found.',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if ((stats.byType.level ?? 0) === 0) {
|
|
211
|
+
warnings.push({
|
|
212
|
+
severity: 'warning',
|
|
213
|
+
code: 'no_levels',
|
|
214
|
+
message: 'No level nodes found.',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const ok = errors.length === 0;
|
|
218
|
+
return {
|
|
219
|
+
ok,
|
|
220
|
+
parsed: ok ? { nodes, rootNodeIds } : null,
|
|
221
|
+
stats,
|
|
222
|
+
errors,
|
|
223
|
+
warnings,
|
|
224
|
+
schemaIssues,
|
|
225
|
+
schemaIssueCount: schemaFailureCount,
|
|
226
|
+
};
|
|
227
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Core library for Pascal 3D building editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
"import": "./dist/utils/clone-scene-graph.js",
|
|
17
17
|
"default": "./dist/utils/clone-scene-graph.js"
|
|
18
18
|
},
|
|
19
|
+
"./registry": {
|
|
20
|
+
"types": "./dist/registry/index.d.ts",
|
|
21
|
+
"import": "./dist/registry/index.js",
|
|
22
|
+
"default": "./dist/registry/index.js"
|
|
23
|
+
},
|
|
19
24
|
"./schema": {
|
|
20
25
|
"types": "./dist/schema/index.d.ts",
|
|
21
26
|
"import": "./dist/schema/index.js",
|
|
@@ -54,6 +59,8 @@
|
|
|
54
59
|
"scripts": {
|
|
55
60
|
"build": "tsc --build",
|
|
56
61
|
"dev": "tsc --build --watch",
|
|
62
|
+
"test": "bun test",
|
|
63
|
+
"bench:registry": "bun run src/registry/__bench__/relations-resolver.bench.ts",
|
|
57
64
|
"prepublishOnly": "npm run build"
|
|
58
65
|
},
|
|
59
66
|
"peerDependencies": {
|
|
@@ -76,7 +83,7 @@
|
|
|
76
83
|
"@types/bun": "^1.3.0",
|
|
77
84
|
"@types/react": "^19.2.2",
|
|
78
85
|
"@types/three": "^0.184.0",
|
|
79
|
-
"typescript": "6.0.
|
|
86
|
+
"typescript": "6.0.3"
|
|
80
87
|
},
|
|
81
88
|
"keywords": [
|
|
82
89
|
"3d",
|