@pascal-app/core 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/events/bus.d.ts +30 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
- package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
- package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +27 -1
- package/dist/hooks/scene-registry/scene-registry.test.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
- package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +93 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +69 -12
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +476 -195
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +19 -1
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- package/dist/lib/level-name.d.ts +4 -0
- package/dist/lib/level-name.d.ts.map +1 -0
- package/dist/lib/level-name.js +10 -0
- package/dist/lib/measurement-geometry.d.ts +19 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.js +232 -0
- package/dist/lib/measurement-geometry.test.d.ts +2 -0
- package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.test.js +102 -0
- package/dist/lib/polygon-geometry.d.ts +0 -1
- package/dist/lib/polygon-geometry.d.ts.map +1 -1
- package/dist/lib/polygon-geometry.js +0 -18
- package/dist/lib/selection-proxy.d.ts +4 -0
- package/dist/lib/selection-proxy.d.ts.map +1 -0
- package/dist/lib/selection-proxy.js +12 -0
- package/dist/lib/slab-polygon.d.ts +31 -2
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +525 -31
- package/dist/lib/slab-polygon.test.d.ts +2 -0
- package/dist/lib/slab-polygon.test.d.ts.map +1 -0
- package/dist/lib/slab-polygon.test.js +635 -0
- package/dist/lib/slots.d.ts +14 -0
- package/dist/lib/slots.d.ts.map +1 -0
- package/dist/lib/slots.js +26 -0
- package/dist/lib/space-detection.d.ts +25 -2
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +414 -118
- package/dist/lib/space-detection.test.js +301 -2
- package/dist/lib/wall-distance.d.ts +60 -0
- package/dist/lib/wall-distance.d.ts.map +1 -0
- package/dist/lib/wall-distance.js +85 -0
- package/dist/lib/zone-quantities.d.ts +21 -0
- package/dist/lib/zone-quantities.d.ts.map +1 -0
- package/dist/lib/zone-quantities.js +183 -0
- package/dist/lib/zone-quantities.test.d.ts +2 -0
- package/dist/lib/zone-quantities.test.d.ts.map +1 -0
- package/dist/lib/zone-quantities.test.js +82 -0
- package/dist/material-library.d.ts +20 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +2181 -441
- package/dist/registry/handles.d.ts +75 -2
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -3
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/registry.d.ts +46 -3
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +82 -1
- package/dist/registry/registry.test.js +14 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +4 -0
- package/dist/registry/subtree.test.js +30 -0
- package/dist/registry/types.d.ts +717 -38
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +21 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +17 -3
- package/dist/schema/material.d.ts +3 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +7 -5
- package/dist/schema/material.test.d.ts +2 -0
- package/dist/schema/material.test.d.ts.map +1 -0
- package/dist/schema/material.test.js +43 -0
- package/dist/schema/nodes/box-vent.d.ts +2 -2
- package/dist/schema/nodes/cabinet.d.ts +534 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -0
- package/dist/schema/nodes/cabinet.js +137 -0
- package/dist/schema/nodes/ceiling.d.ts +3 -2
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +4 -0
- package/dist/schema/nodes/chimney.d.ts +4 -4
- package/dist/schema/nodes/column.d.ts +3 -2
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +3 -0
- package/dist/schema/nodes/cupola.d.ts +2 -2
- package/dist/schema/nodes/door.d.ts +13 -5
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +12 -0
- package/dist/schema/nodes/dormer.d.ts +8 -8
- package/dist/schema/nodes/dormer.js +1 -1
- package/dist/schema/nodes/downspout.d.ts +2 -2
- package/dist/schema/nodes/duct-fitting.d.ts +91 -0
- package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/duct-fitting.js +92 -0
- package/dist/schema/nodes/duct-segment.d.ts +61 -0
- package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
- package/dist/schema/nodes/duct-segment.js +73 -0
- package/dist/schema/nodes/duct-terminal.d.ts +57 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
- package/dist/schema/nodes/duct-terminal.js +51 -0
- package/dist/schema/nodes/elevator.d.ts +3 -2
- package/dist/schema/nodes/elevator.d.ts.map +1 -1
- package/dist/schema/nodes/elevator.js +3 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
- package/dist/schema/nodes/fence.d.ts +18 -2
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +25 -2
- package/dist/schema/nodes/gutter.d.ts +2 -2
- package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
- package/dist/schema/nodes/hvac-equipment.js +55 -0
- package/dist/schema/nodes/item.d.ts +8 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +11 -0
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +2 -0
- package/dist/schema/nodes/lineset.d.ts +42 -0
- package/dist/schema/nodes/lineset.d.ts.map +1 -0
- package/dist/schema/nodes/lineset.js +38 -0
- package/dist/schema/nodes/liquid-line.d.ts +34 -0
- package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
- package/dist/schema/nodes/liquid-line.js +24 -0
- package/dist/schema/nodes/measurement.d.ts +306 -0
- package/dist/schema/nodes/measurement.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.js +87 -0
- package/dist/schema/nodes/measurement.test.d.ts +2 -0
- package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.test.js +139 -0
- package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
- package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-fitting.js +45 -0
- package/dist/schema/nodes/pipe-segment.d.ts +48 -0
- package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-segment.js +36 -0
- package/dist/schema/nodes/pipe-trap.d.ts +43 -0
- package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-trap.js +36 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +2 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
- package/dist/schema/nodes/ridge-vent.d.ts +18 -3
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
- package/dist/schema/nodes/ridge-vent.js +221 -3
- package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
- package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.js +318 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-trim.test.js +107 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.js +330 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
- package/dist/schema/nodes/roof-segment.d.ts +84 -8
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +290 -5
- package/dist/schema/nodes/roof.d.ts +8 -8
- package/dist/schema/nodes/shelf.d.ts +3 -2
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +6 -0
- package/dist/schema/nodes/skylight.d.ts +10 -10
- package/dist/schema/nodes/slab.d.ts +3 -2
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +4 -0
- package/dist/schema/nodes/solar-panel.d.ts +4 -4
- package/dist/schema/nodes/stair-segment.d.ts +4 -4
- package/dist/schema/nodes/stair.d.ts +13 -12
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +3 -0
- package/dist/schema/nodes/turbine-vent.d.ts +2 -2
- package/dist/schema/nodes/wall.d.ts +245 -6
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +257 -0
- package/dist/schema/nodes/wall.test.d.ts +2 -0
- package/dist/schema/nodes/wall.test.d.ts.map +1 -0
- package/dist/schema/nodes/wall.test.js +208 -0
- package/dist/schema/nodes/window.d.ts +11 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +12 -0
- package/dist/schema/nodes/zone.d.ts +2 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +6 -0
- package/dist/schema/scene-material.d.ts +41 -0
- package/dist/schema/scene-material.d.ts.map +1 -0
- package/dist/schema/scene-material.js +9 -0
- package/dist/schema/types.d.ts +1089 -94
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +23 -0
- package/dist/services/alignment-anchors.d.ts.map +1 -1
- package/dist/services/alignment-anchors.js +30 -1
- package/dist/services/alignment.d.ts +5 -0
- package/dist/services/alignment.d.ts.map +1 -1
- package/dist/services/alignment.js +2 -0
- package/dist/services/hosting.d.ts +8 -0
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +13 -4
- package/dist/services/hosting.test.js +32 -1
- package/dist/services/index.d.ts +8 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +8 -2
- package/dist/services/level-height.d.ts +28 -0
- package/dist/services/level-height.d.ts.map +1 -0
- package/dist/services/level-height.js +62 -0
- package/dist/services/opening-guides.d.ts +141 -0
- package/dist/services/opening-guides.d.ts.map +1 -0
- package/dist/services/opening-guides.js +267 -0
- package/dist/services/opening-guides.test.d.ts +2 -0
- package/dist/services/opening-guides.test.d.ts.map +1 -0
- package/dist/services/opening-guides.test.js +208 -0
- package/dist/services/port-connectivity.d.ts +115 -0
- package/dist/services/port-connectivity.d.ts.map +1 -0
- package/dist/services/port-connectivity.js +300 -0
- package/dist/services/port-connectivity.test.d.ts +2 -0
- package/dist/services/port-connectivity.test.d.ts.map +1 -0
- package/dist/services/port-connectivity.test.js +297 -0
- package/dist/services/riser-diagram.d.ts +40 -0
- package/dist/services/riser-diagram.d.ts.map +1 -0
- package/dist/services/riser-diagram.js +110 -0
- package/dist/services/riser-diagram.test.d.ts +2 -0
- package/dist/services/riser-diagram.test.d.ts.map +1 -0
- package/dist/services/riser-diagram.test.js +66 -0
- package/dist/services/snap.d.ts +9 -1
- package/dist/services/snap.d.ts.map +1 -1
- package/dist/services/snap.js +21 -1
- package/dist/services/snap.test.js +43 -1
- package/dist/services/system-graph.d.ts +30 -0
- package/dist/services/system-graph.d.ts.map +1 -0
- package/dist/services/system-graph.js +164 -0
- package/dist/services/system-graph.test.d.ts +2 -0
- package/dist/services/system-graph.test.d.ts.map +1 -0
- package/dist/services/system-graph.test.js +138 -0
- package/dist/services/validate-dwv.d.ts +32 -0
- package/dist/services/validate-dwv.d.ts.map +1 -0
- package/dist/services/validate-dwv.js +118 -0
- package/dist/services/validate-dwv.test.d.ts +2 -0
- package/dist/services/validate-dwv.test.d.ts.map +1 -0
- package/dist/services/validate-dwv.test.js +109 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +94 -1
- package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
- package/dist/store/actions/ridge-vent-update.test.js +264 -0
- package/dist/store/history-control.d.ts +11 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +33 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
- package/dist/store/use-scene-dirty-tracking.test.js +68 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
- package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
- package/dist/store/use-scene-plugins.test.d.ts +2 -0
- package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
- package/dist/store/use-scene-plugins.test.js +51 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
- package/dist/store/use-scene-window-migration.test.d.ts +2 -0
- package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-window-migration.test.js +85 -0
- package/dist/store/use-scene.d.ts +17 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +358 -51
- package/dist/systems/elevator/elevator-service.js +1 -1
- package/dist/systems/fence/fence-centerline.d.ts +12 -0
- package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
- package/dist/systems/fence/fence-centerline.js +37 -0
- package/dist/systems/fence/fence-spline.d.ts +39 -0
- package/dist/systems/fence/fence-spline.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.js +196 -0
- package/dist/systems/fence/fence-spline.test.d.ts +2 -0
- package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.test.js +136 -0
- package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
- package/dist/systems/stair/stair-footprint.js +21 -11
- package/dist/systems/stair/stair-opening-preview.d.ts +1139 -469
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +1 -18
- package/dist/systems/stair/stair-opening-sync.test.js +39 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +38 -2
- package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
- package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-mitering.test.js +63 -0
- package/dist/utils/clone-scene-graph.d.ts +1 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +23 -3
- package/dist/utils/clone-scene-graph.test.js +3 -0
- package/dist/utils/heal-scene-graph.d.ts +18 -0
- package/dist/utils/heal-scene-graph.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.js +90 -0
- package/dist/utils/heal-scene-graph.test.d.ts +2 -0
- package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.test.js +88 -0
- package/dist/validation/validate-build-json.d.ts +1 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +31 -2
- package/package.json +3 -3
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { nodeRegistry } from '../registry';
|
|
2
|
+
/** Distance (meters) under which two ports count as the same joint. Joints
|
|
3
|
+
* formed by placement snapping coincide to sub-millimeter; 5 cm leaves
|
|
4
|
+
* generous slack for grid-snapped hand placement without false matches. */
|
|
5
|
+
const COINCIDENT_EPS_M = 0.05;
|
|
6
|
+
/** Below this (meters) a propagated delta is treated as zero — stops the
|
|
7
|
+
* walk from chasing sub-millimeter perpendicular residue. */
|
|
8
|
+
const DELTA_EPS_M = 1e-4;
|
|
9
|
+
const PROPAGATION_EPS_M = 1e-9;
|
|
10
|
+
function portsOf(node) {
|
|
11
|
+
return nodeRegistry.get(node.type)?.ports?.(node);
|
|
12
|
+
}
|
|
13
|
+
/** A node's distribution role from the registry (run / fitting / …). */
|
|
14
|
+
function roleOf(node) {
|
|
15
|
+
return nodeRegistry.get(node.type)?.distributionRole;
|
|
16
|
+
}
|
|
17
|
+
function distSq(a, b) {
|
|
18
|
+
const dx = a[0] - b[0];
|
|
19
|
+
const dy = a[1] - b[1];
|
|
20
|
+
const dz = a[2] - b[2];
|
|
21
|
+
return dx * dx + dy * dy + dz * dz;
|
|
22
|
+
}
|
|
23
|
+
/** Two ports mate when they coincide AND don't cross incompatible systems
|
|
24
|
+
* (a supply duct and a waste pipe that merely touch must not fuse). */
|
|
25
|
+
function portsMate(a, b, epsSq) {
|
|
26
|
+
if (distSq(a.position, b.position) > epsSq)
|
|
27
|
+
return false;
|
|
28
|
+
if (a.system && b.system && a.system !== b.system)
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Snapshot the joint graph reachable from `movedNode`'s ports, taken at the
|
|
34
|
+
* start of a move/resize. Call once before the drag; feed the result to
|
|
35
|
+
* `resolveConnectivityUpdates` on every frame.
|
|
36
|
+
*
|
|
37
|
+
* Only `run`-role partners (segments) and `fitting`-role partners are walked —
|
|
38
|
+
* terminals and equipment usually mount to a surface and shouldn't be yanked
|
|
39
|
+
* off it when an adjacent fitting nudges. Fittings that declare
|
|
40
|
+
* `portConnectivityFollow: false` are anchored fixtures (e.g. pipe-trap) and
|
|
41
|
+
* are skipped, so a connected run stretches against them instead.
|
|
42
|
+
*/
|
|
43
|
+
export function analyzePortConnectivity(movedNode, nodes) {
|
|
44
|
+
const epsSq = COINCIDENT_EPS_M * COINCIDENT_EPS_M;
|
|
45
|
+
const movedPorts = portsOf(movedNode) ?? [];
|
|
46
|
+
const startMovedPorts = {};
|
|
47
|
+
for (const p of movedPorts)
|
|
48
|
+
startMovedPorts[p.id] = p.position;
|
|
49
|
+
// Candidate partners: every run + every following fitting in the scene.
|
|
50
|
+
const candidates = [];
|
|
51
|
+
for (const other of Object.values(nodes)) {
|
|
52
|
+
if (!other || other.id === movedNode.id)
|
|
53
|
+
continue;
|
|
54
|
+
const role = roleOf(other);
|
|
55
|
+
if (role !== 'run' && role !== 'fitting')
|
|
56
|
+
continue;
|
|
57
|
+
if (role === 'fitting' && nodeRegistry.get(other.type)?.portConnectivityFollow === false) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const ports = portsOf(other);
|
|
61
|
+
if (!ports)
|
|
62
|
+
continue;
|
|
63
|
+
const startPath = role === 'run'
|
|
64
|
+
? other.path?.map((p) => [...p])
|
|
65
|
+
: undefined;
|
|
66
|
+
if (role === 'run' && (!startPath || startPath.length < 2))
|
|
67
|
+
continue;
|
|
68
|
+
const startPosition = role === 'fitting'
|
|
69
|
+
? (() => {
|
|
70
|
+
const pos = other.position;
|
|
71
|
+
return pos ? [pos[0], pos[1], pos[2]] : undefined;
|
|
72
|
+
})()
|
|
73
|
+
: undefined;
|
|
74
|
+
if (role === 'fitting' && !startPosition)
|
|
75
|
+
continue;
|
|
76
|
+
candidates.push({ id: other.id, role, ports, startPath, startPosition });
|
|
77
|
+
}
|
|
78
|
+
// Walk outward from the moved node, collecting every node reachable through
|
|
79
|
+
// coincident ports. The adjacency records each port's mates so the resolver
|
|
80
|
+
// can replay the same edges with live deltas.
|
|
81
|
+
const adjacency = {};
|
|
82
|
+
const addEdge = (nodeId, portId, mate) => {
|
|
83
|
+
const byPort = adjacency[nodeId] ?? {};
|
|
84
|
+
adjacency[nodeId] = byPort;
|
|
85
|
+
const mates = byPort[portId] ?? [];
|
|
86
|
+
byPort[portId] = mates;
|
|
87
|
+
mates.push(mate);
|
|
88
|
+
};
|
|
89
|
+
const graph = {};
|
|
90
|
+
const visited = new Set([movedNode.id]);
|
|
91
|
+
// Seed: the moved node's own ports.
|
|
92
|
+
const queue = [{ id: movedNode.id, ports: movedPorts }];
|
|
93
|
+
while (queue.length > 0) {
|
|
94
|
+
const { id, ports } = queue.shift();
|
|
95
|
+
for (const port of ports) {
|
|
96
|
+
for (const cand of candidates) {
|
|
97
|
+
if (cand.id === id)
|
|
98
|
+
continue;
|
|
99
|
+
for (const cp of cand.ports) {
|
|
100
|
+
if (!portsMate(port, cp, epsSq))
|
|
101
|
+
continue;
|
|
102
|
+
addEdge(id, port.id, { nodeId: cand.id, portId: cp.id });
|
|
103
|
+
addEdge(cand.id, cp.id, { nodeId: id, portId: port.id });
|
|
104
|
+
if (!visited.has(cand.id)) {
|
|
105
|
+
visited.add(cand.id);
|
|
106
|
+
graph[cand.id] = cand;
|
|
107
|
+
queue.push({ id: cand.id, ports: cand.ports });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const connections = Object.values(graph).map((g) => g.role === 'fitting'
|
|
114
|
+
? { kind: 'rigid-node', nodeId: g.id, startPosition: g.startPosition }
|
|
115
|
+
: { kind: 'run', nodeId: g.id, startPath: g.startPath });
|
|
116
|
+
return {
|
|
117
|
+
movedNodeId: movedNode.id,
|
|
118
|
+
startMovedPorts,
|
|
119
|
+
graph,
|
|
120
|
+
adjacency,
|
|
121
|
+
connections,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function add(a, b) {
|
|
125
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
126
|
+
}
|
|
127
|
+
function sub(a, b) {
|
|
128
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
129
|
+
}
|
|
130
|
+
function lenSq(v) {
|
|
131
|
+
return v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
|
|
132
|
+
}
|
|
133
|
+
/** Split `delta` into the component along unit `axis` and the remainder. */
|
|
134
|
+
function decompose(delta, axis) {
|
|
135
|
+
const dot = delta[0] * axis[0] + delta[1] * axis[1] + delta[2] * axis[2];
|
|
136
|
+
const parallel = [axis[0] * dot, axis[1] * dot, axis[2] * dot];
|
|
137
|
+
return { parallel, perp: sub(delta, parallel) };
|
|
138
|
+
}
|
|
139
|
+
function scale(v, scalar) {
|
|
140
|
+
return [v[0] * scalar, v[1] * scalar, v[2] * scalar];
|
|
141
|
+
}
|
|
142
|
+
function average(deltas) {
|
|
143
|
+
const sum = deltas.reduce((acc, delta) => add(acc, delta), [0, 0, 0]);
|
|
144
|
+
return scale(sum, 1 / deltas.length);
|
|
145
|
+
}
|
|
146
|
+
function nearlyEqual(a, b) {
|
|
147
|
+
return lenSq(sub(a, b)) <= DELTA_EPS_M * DELTA_EPS_M;
|
|
148
|
+
}
|
|
149
|
+
function propagationEqual(a, b) {
|
|
150
|
+
return lenSq(sub(a, b)) <= PROPAGATION_EPS_M * PROPAGATION_EPS_M;
|
|
151
|
+
}
|
|
152
|
+
function effectivePortDeltas(constraints) {
|
|
153
|
+
return Object.fromEntries(Object.entries(constraints).map(([portId, bySource]) => [
|
|
154
|
+
portId,
|
|
155
|
+
average(Object.values(bySource)),
|
|
156
|
+
]));
|
|
157
|
+
}
|
|
158
|
+
/** Unit direction of the run's segment adjacent to its `start` / `end` tip. */
|
|
159
|
+
function endpointAxis(path, portId) {
|
|
160
|
+
const n = path.length;
|
|
161
|
+
const [a, b] = portId === 'start' ? [path[1], path[0]] : [path[n - 2], path[n - 1]];
|
|
162
|
+
const dir = sub(b, a);
|
|
163
|
+
const l2 = lenSq(dir);
|
|
164
|
+
if (l2 < 1e-12)
|
|
165
|
+
return [0, 0, 0];
|
|
166
|
+
const l = Math.sqrt(l2);
|
|
167
|
+
return [dir[0] / l, dir[1] / l, dir[2] / l];
|
|
168
|
+
}
|
|
169
|
+
function runPathFromSinglePortDelta(startPath, portId, delta) {
|
|
170
|
+
const nearIdx = portId === 'start' ? 0 : startPath.length - 1;
|
|
171
|
+
const axis = endpointAxis(startPath, portId);
|
|
172
|
+
const { parallel, perp } = decompose(delta, axis);
|
|
173
|
+
const path = startPath.map((p) => add(p, perp));
|
|
174
|
+
path[nearIdx] = add(path[nearIdx], parallel);
|
|
175
|
+
return path;
|
|
176
|
+
}
|
|
177
|
+
function runEndpointDeltas(startPath, path) {
|
|
178
|
+
return {
|
|
179
|
+
start: sub(path[0], startPath[0]),
|
|
180
|
+
end: sub(path[path.length - 1], startPath[startPath.length - 1]),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function runPathFromPortDeltas(startPath, portDeltas) {
|
|
184
|
+
const startDelta = portDeltas.start;
|
|
185
|
+
const endDelta = portDeltas.end;
|
|
186
|
+
if (startDelta && endDelta) {
|
|
187
|
+
if (startPath.length === 2) {
|
|
188
|
+
return [add(startPath[0], startDelta), add(startPath[1], endDelta)];
|
|
189
|
+
}
|
|
190
|
+
if (nearlyEqual(startDelta, endDelta)) {
|
|
191
|
+
return startPath.map((p) => add(p, startDelta));
|
|
192
|
+
}
|
|
193
|
+
const startParts = decompose(startDelta, endpointAxis(startPath, 'start'));
|
|
194
|
+
const endParts = decompose(endDelta, endpointAxis(startPath, 'end'));
|
|
195
|
+
const commonPerp = average([startParts.perp, endParts.perp]);
|
|
196
|
+
const path = startPath.map((p) => add(p, commonPerp));
|
|
197
|
+
path[0] = add(path[0], startParts.parallel);
|
|
198
|
+
path[path.length - 1] = add(path[path.length - 1], endParts.parallel);
|
|
199
|
+
return path;
|
|
200
|
+
}
|
|
201
|
+
return runPathFromSinglePortDelta(startPath, startDelta ? 'start' : 'end', (startDelta ?? endDelta));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Given the moved node in its live (in-drag) transform, produce the patches
|
|
205
|
+
* that keep every connected node attached. `previewNode` is the moved node
|
|
206
|
+
* with its current drag position/rotation applied so its ports recompute.
|
|
207
|
+
*
|
|
208
|
+
* Walks the snapshotted graph, propagating each port delta outward: fittings
|
|
209
|
+
* translate rigidly, runs stretch along their axis and translate across it
|
|
210
|
+
* (never skew when driven from one end), and effective port movement carries on
|
|
211
|
+
* to neighbouring joints. Port-level output guards bound cycles while still
|
|
212
|
+
* allowing a looped/shared run to accept constraints at both endpoints.
|
|
213
|
+
*/
|
|
214
|
+
export function resolveConnectivityUpdates(connectivity, previewNode) {
|
|
215
|
+
const { graph, adjacency, startMovedPorts, movedNodeId } = connectivity;
|
|
216
|
+
if (Object.keys(graph).length === 0)
|
|
217
|
+
return [];
|
|
218
|
+
const newPorts = portsOf(previewNode) ?? [];
|
|
219
|
+
const newMovedPos = {};
|
|
220
|
+
for (const p of newPorts)
|
|
221
|
+
newMovedPos[p.id] = p.position;
|
|
222
|
+
// Each queue item drives a node's port by a delta ("this collar / endpoint
|
|
223
|
+
// must move by this much").
|
|
224
|
+
const queue = [];
|
|
225
|
+
const results = {};
|
|
226
|
+
const constrainedPorts = {};
|
|
227
|
+
const propagatedPorts = {};
|
|
228
|
+
const enqueueMates = (nodeId, portId, delta) => {
|
|
229
|
+
const byPort = propagatedPorts[nodeId] ?? {};
|
|
230
|
+
propagatedPorts[nodeId] = byPort;
|
|
231
|
+
const previous = byPort[portId];
|
|
232
|
+
if (previous && propagationEqual(previous, delta))
|
|
233
|
+
return;
|
|
234
|
+
byPort[portId] = delta;
|
|
235
|
+
for (const mate of adjacency[nodeId]?.[portId] ?? []) {
|
|
236
|
+
if (mate.nodeId === movedNodeId)
|
|
237
|
+
continue;
|
|
238
|
+
queue.push({
|
|
239
|
+
nodeId: mate.nodeId,
|
|
240
|
+
portId: mate.portId,
|
|
241
|
+
delta,
|
|
242
|
+
sourceKey: `${nodeId}:${portId}`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const acceptPortDelta = (nodeId, portId, sourceKey, delta) => {
|
|
247
|
+
const byPort = constrainedPorts[nodeId] ?? {};
|
|
248
|
+
constrainedPorts[nodeId] = byPort;
|
|
249
|
+
const bySource = byPort[portId] ?? {};
|
|
250
|
+
byPort[portId] = bySource;
|
|
251
|
+
const existing = bySource[sourceKey];
|
|
252
|
+
if (existing && propagationEqual(existing, delta)) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
bySource[sourceKey] = delta;
|
|
256
|
+
return true;
|
|
257
|
+
};
|
|
258
|
+
// Seed from the moved node's live port deltas.
|
|
259
|
+
for (const [portId, start] of Object.entries(startMovedPorts)) {
|
|
260
|
+
const now = newMovedPos[portId];
|
|
261
|
+
if (!now)
|
|
262
|
+
continue;
|
|
263
|
+
const delta = sub(now, start);
|
|
264
|
+
if (lenSq(delta) <= DELTA_EPS_M * DELTA_EPS_M)
|
|
265
|
+
continue;
|
|
266
|
+
enqueueMates(movedNodeId, portId, delta);
|
|
267
|
+
}
|
|
268
|
+
while (queue.length > 0) {
|
|
269
|
+
const { nodeId, portId, delta, sourceKey } = queue.shift();
|
|
270
|
+
const node = graph[nodeId];
|
|
271
|
+
if (!node)
|
|
272
|
+
continue;
|
|
273
|
+
if (!acceptPortDelta(nodeId, portId, sourceKey, delta))
|
|
274
|
+
continue;
|
|
275
|
+
const portDeltas = effectivePortDeltas(constrainedPorts[nodeId]);
|
|
276
|
+
if (node.role === 'fitting') {
|
|
277
|
+
const start = node.startPosition;
|
|
278
|
+
const effectiveDelta = average(Object.values(portDeltas));
|
|
279
|
+
results[nodeId] = {
|
|
280
|
+
id: nodeId,
|
|
281
|
+
data: { position: add(start, effectiveDelta) },
|
|
282
|
+
};
|
|
283
|
+
// Rigid: every collar carries the effective body translation onward.
|
|
284
|
+
for (const p of node.ports) {
|
|
285
|
+
enqueueMates(nodeId, p.id, effectiveDelta);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const startPath = node.startPath;
|
|
290
|
+
const path = runPathFromPortDeltas(startPath, portDeltas);
|
|
291
|
+
results[nodeId] = { id: nodeId, data: { path } };
|
|
292
|
+
for (const [nextPortId, nextDelta] of Object.entries(runEndpointDeltas(startPath, path))) {
|
|
293
|
+
if (lenSq(nextDelta) <= DELTA_EPS_M * DELTA_EPS_M)
|
|
294
|
+
continue;
|
|
295
|
+
enqueueMates(nodeId, nextPortId, nextDelta);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return Object.values(results);
|
|
300
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-connectivity.test.d.ts","sourceRoot":"","sources":["../../src/services/port-connectivity.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { registerNode } from '../registry';
|
|
3
|
+
import { analyzePortConnectivity, resolveConnectivityUpdates } from './port-connectivity';
|
|
4
|
+
// Stub registrations mirroring the real kinds' port + role conventions
|
|
5
|
+
// without importing the nodes package (which pulls in CSG and can't load
|
|
6
|
+
// under the test runner). A run exposes start/end at its path tips; the
|
|
7
|
+
// fitting here is a simple two-collar elbow at ±X around its position.
|
|
8
|
+
function stubDef(kind, distributionRole, ports) {
|
|
9
|
+
registerNode({
|
|
10
|
+
kind,
|
|
11
|
+
schemaVersion: 1,
|
|
12
|
+
schema: {},
|
|
13
|
+
category: 'utility',
|
|
14
|
+
distributionRole,
|
|
15
|
+
defaults: () => ({}),
|
|
16
|
+
capabilities: {},
|
|
17
|
+
ports,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
stubDef('duct-segment', 'run', (node) => {
|
|
21
|
+
const path = node.path;
|
|
22
|
+
const system = node.system;
|
|
23
|
+
return [
|
|
24
|
+
{ id: 'start', position: path[0], direction: [-1, 0, 0], diameter: 6, system },
|
|
25
|
+
{ id: 'end', position: path[path.length - 1], direction: [1, 0, 0], diameter: 6, system },
|
|
26
|
+
];
|
|
27
|
+
});
|
|
28
|
+
stubDef('duct-fitting', 'fitting', (node) => {
|
|
29
|
+
const position = node.position;
|
|
30
|
+
const system = node.system;
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
id: 'inlet',
|
|
34
|
+
position: [position[0] - 0.2, position[1], position[2]],
|
|
35
|
+
direction: [-1, 0, 0],
|
|
36
|
+
diameter: 6,
|
|
37
|
+
system,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'outlet',
|
|
41
|
+
position: [position[0] + 0.2, position[1], position[2]],
|
|
42
|
+
direction: [1, 0, 0],
|
|
43
|
+
diameter: 6,
|
|
44
|
+
system,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
});
|
|
48
|
+
stubDef('duct-tee', 'fitting', (node) => {
|
|
49
|
+
const position = node.position;
|
|
50
|
+
const system = node.system;
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
id: 'inlet',
|
|
54
|
+
position: [position[0] - 0.2, position[1], position[2]],
|
|
55
|
+
direction: [-1, 0, 0],
|
|
56
|
+
diameter: 6,
|
|
57
|
+
system,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'outlet',
|
|
61
|
+
position: [position[0] + 0.2, position[1], position[2]],
|
|
62
|
+
direction: [1, 0, 0],
|
|
63
|
+
diameter: 6,
|
|
64
|
+
system,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'branch',
|
|
68
|
+
position: [position[0], position[1], position[2] + 0.2],
|
|
69
|
+
direction: [0, 0, 1],
|
|
70
|
+
diameter: 6,
|
|
71
|
+
system,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
});
|
|
75
|
+
let nextId = 0;
|
|
76
|
+
function makeNode(type, fields) {
|
|
77
|
+
nextId += 1;
|
|
78
|
+
return { id: `${type}_${nextId}`, type, object: 'node', parentId: null, ...fields };
|
|
79
|
+
}
|
|
80
|
+
function sceneOf(...nodes) {
|
|
81
|
+
return Object.fromEntries(nodes.map((n) => [n.id, n]));
|
|
82
|
+
}
|
|
83
|
+
function expectPointClose(actual, expected) {
|
|
84
|
+
expect(actual[0]).toBeCloseTo(expected[0], 6);
|
|
85
|
+
expect(actual[1]).toBeCloseTo(expected[1], 6);
|
|
86
|
+
expect(actual[2]).toBeCloseTo(expected[2], 6);
|
|
87
|
+
}
|
|
88
|
+
describe('port connectivity — joint follow (stretch vs translate)', () => {
|
|
89
|
+
// Layout: duct A ends at the fitting's inlet (−0.2,0,0); duct B starts at the
|
|
90
|
+
// fitting's outlet (+0.2,0,0). Both runs lie on the X axis. Dragging A's
|
|
91
|
+
// mated endpoint carries the fitting and duct B; how B reacts depends on
|
|
92
|
+
// whether the drag is along its axis (stretch) or across it (translate).
|
|
93
|
+
function joint() {
|
|
94
|
+
const fitting = makeNode('duct-fitting', { position: [0, 0, 0], system: 'supply' });
|
|
95
|
+
const ductA = makeNode('duct-segment', {
|
|
96
|
+
path: [
|
|
97
|
+
[-3, 0, 0],
|
|
98
|
+
[-0.2, 0, 0],
|
|
99
|
+
],
|
|
100
|
+
system: 'supply',
|
|
101
|
+
});
|
|
102
|
+
const ductB = makeNode('duct-segment', {
|
|
103
|
+
path: [
|
|
104
|
+
[0.2, 0, 0],
|
|
105
|
+
[3, 0, 0],
|
|
106
|
+
],
|
|
107
|
+
system: 'supply',
|
|
108
|
+
});
|
|
109
|
+
return { fitting, ductA, ductB };
|
|
110
|
+
}
|
|
111
|
+
function movedA(end) {
|
|
112
|
+
const { ductA } = joint();
|
|
113
|
+
return { ...ductA, path: [[-3, 0, 0], end] };
|
|
114
|
+
}
|
|
115
|
+
test('the fitting and sibling run are picked up as carried connections', () => {
|
|
116
|
+
const { fitting, ductA, ductB } = joint();
|
|
117
|
+
const connectivity = analyzePortConnectivity(ductA, sceneOf(fitting, ductA, ductB));
|
|
118
|
+
expect(connectivity.connections.find((c) => c.kind === 'rigid-node' && c.nodeId === fitting.id)).toBeDefined();
|
|
119
|
+
expect(connectivity.connections.find((c) => c.kind === 'run' && c.nodeId === ductB.id)).toBeDefined();
|
|
120
|
+
});
|
|
121
|
+
test('perpendicular drag translates the WHOLE sibling run (no skew)', () => {
|
|
122
|
+
const { fitting, ductA, ductB } = joint();
|
|
123
|
+
const nodes = sceneOf(fitting, ductA, ductB);
|
|
124
|
+
const connectivity = analyzePortConnectivity(ductA, nodes);
|
|
125
|
+
// Move A's mated end +1 in Z — perpendicular to B's X axis.
|
|
126
|
+
const updates = resolveConnectivityUpdates(connectivity, movedA([-0.2, 0, 1]));
|
|
127
|
+
expect(updates.find((u) => u.id === fitting.id).data.position).toEqual([0, 0, 1]);
|
|
128
|
+
const bPath = updates.find((u) => u.id === ductB.id).data.path;
|
|
129
|
+
// Both ends ride +1 in Z: the run keeps its length and direction.
|
|
130
|
+
expect(bPath[0]).toEqual([0.2, 0, 1]);
|
|
131
|
+
expect(bPath[1]).toEqual([3, 0, 1]);
|
|
132
|
+
});
|
|
133
|
+
test('parallel drag stretches the sibling run (only the near end slides)', () => {
|
|
134
|
+
const { fitting, ductA, ductB } = joint();
|
|
135
|
+
const nodes = sceneOf(fitting, ductA, ductB);
|
|
136
|
+
const connectivity = analyzePortConnectivity(ductA, nodes);
|
|
137
|
+
// Move A's mated end +0.5 in X — along B's axis (the fitting slides toward B).
|
|
138
|
+
const updates = resolveConnectivityUpdates(connectivity, movedA([0.3, 0, 0]));
|
|
139
|
+
const bPath = updates.find((u) => u.id === ductB.id).data.path;
|
|
140
|
+
// Near end slid +0.5 in X; far end stayed put → the run shortened.
|
|
141
|
+
expect(bPath[0]).toEqual([0.7, 0, 0]);
|
|
142
|
+
expect(bPath[1]).toEqual([3, 0, 0]);
|
|
143
|
+
});
|
|
144
|
+
test('perpendicular slide propagates through the sibling run to its far joint', () => {
|
|
145
|
+
// Extend the chain: duct B's far end (3,0,0) meets a second elbow, and duct
|
|
146
|
+
// C hangs off that elbow. A perpendicular drag should carry the whole chain.
|
|
147
|
+
const { fitting, ductA, ductB } = joint();
|
|
148
|
+
const elbow2 = makeNode('duct-fitting', { position: [3.2, 0, 0], system: 'supply' });
|
|
149
|
+
// elbow ports are ±0.2 on X around its position → inlet at (3,0,0) meets B.
|
|
150
|
+
const ductC = makeNode('duct-segment', {
|
|
151
|
+
path: [
|
|
152
|
+
[3.4, 0, 0],
|
|
153
|
+
[6, 0, 0],
|
|
154
|
+
],
|
|
155
|
+
system: 'supply',
|
|
156
|
+
});
|
|
157
|
+
const nodes = sceneOf(fitting, ductA, ductB, elbow2, ductC);
|
|
158
|
+
const connectivity = analyzePortConnectivity(ductA, nodes);
|
|
159
|
+
const updates = resolveConnectivityUpdates(connectivity, movedA([-0.2, 0, 1]));
|
|
160
|
+
// Whole chain rode +1 in Z.
|
|
161
|
+
const bPath = updates.find((u) => u.id === ductB.id).data.path;
|
|
162
|
+
expect(bPath[1]).toEqual([3, 0, 1]);
|
|
163
|
+
expect(updates.find((u) => u.id === elbow2.id).data.position).toEqual([3.2, 0, 1]);
|
|
164
|
+
const cPath = updates.find((u) => u.id === ductC.id).data.path;
|
|
165
|
+
expect(cPath[0]).toEqual([3.4, 0, 1]);
|
|
166
|
+
expect(cPath[1]).toEqual([6, 0, 1]);
|
|
167
|
+
});
|
|
168
|
+
test('a run reached from both ends applies both endpoint deltas', () => {
|
|
169
|
+
const moved = makeNode('duct-segment', {
|
|
170
|
+
path: [
|
|
171
|
+
[0, 0, 0],
|
|
172
|
+
[3, 0, 0],
|
|
173
|
+
],
|
|
174
|
+
system: 'supply',
|
|
175
|
+
});
|
|
176
|
+
const follower = makeNode('duct-segment', {
|
|
177
|
+
path: [
|
|
178
|
+
[0, 0, 0],
|
|
179
|
+
[3, 0, 0],
|
|
180
|
+
],
|
|
181
|
+
system: 'supply',
|
|
182
|
+
});
|
|
183
|
+
const nodes = sceneOf(moved, follower);
|
|
184
|
+
const connectivity = analyzePortConnectivity(moved, nodes);
|
|
185
|
+
const preview = {
|
|
186
|
+
...moved,
|
|
187
|
+
path: [
|
|
188
|
+
[0, 0, 1],
|
|
189
|
+
[3, 0, 2],
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
const updates = resolveConnectivityUpdates(connectivity, preview);
|
|
193
|
+
const path = updates.find((u) => u.id === follower.id).data.path;
|
|
194
|
+
expect(path[0]).toEqual([0, 0, 1]);
|
|
195
|
+
expect(path[1]).toEqual([3, 0, 2]);
|
|
196
|
+
});
|
|
197
|
+
test('a polyline run reached from both ends preserves interior bend shape', () => {
|
|
198
|
+
const moved = makeNode('duct-segment', {
|
|
199
|
+
path: [
|
|
200
|
+
[0, 0, 0],
|
|
201
|
+
[3, 0, 3],
|
|
202
|
+
],
|
|
203
|
+
system: 'supply',
|
|
204
|
+
});
|
|
205
|
+
const follower = makeNode('duct-segment', {
|
|
206
|
+
path: [
|
|
207
|
+
[0, 0, 0],
|
|
208
|
+
[1, 0, 0],
|
|
209
|
+
[1, 0, 3],
|
|
210
|
+
[3, 0, 3],
|
|
211
|
+
],
|
|
212
|
+
system: 'supply',
|
|
213
|
+
});
|
|
214
|
+
const nodes = sceneOf(moved, follower);
|
|
215
|
+
const connectivity = analyzePortConnectivity(moved, nodes);
|
|
216
|
+
const preview = {
|
|
217
|
+
...moved,
|
|
218
|
+
path: [
|
|
219
|
+
[-0.5, 0, 0],
|
|
220
|
+
[3.5, 0, 3],
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
const updates = resolveConnectivityUpdates(connectivity, preview);
|
|
224
|
+
const path = updates.find((u) => u.id === follower.id).data.path;
|
|
225
|
+
expect(path).toEqual([
|
|
226
|
+
[-0.5, 0, 0],
|
|
227
|
+
[1, 0, 0],
|
|
228
|
+
[1, 0, 3],
|
|
229
|
+
[3.5, 0, 3],
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
test('a fitting reached from both collars rebroadcasts its final compatible rigid delta', () => {
|
|
233
|
+
const moved = makeNode('duct-segment', {
|
|
234
|
+
path: [
|
|
235
|
+
[-0.2, 0, 0],
|
|
236
|
+
[0.2, 0, 0],
|
|
237
|
+
],
|
|
238
|
+
system: 'supply',
|
|
239
|
+
});
|
|
240
|
+
const fitting = makeNode('duct-tee', { position: [0, 0, 0], system: 'supply' });
|
|
241
|
+
const downstream = makeNode('duct-segment', {
|
|
242
|
+
path: [
|
|
243
|
+
[0, 0, 0.2],
|
|
244
|
+
[3, 0, 0.2],
|
|
245
|
+
],
|
|
246
|
+
system: 'supply',
|
|
247
|
+
});
|
|
248
|
+
const nodes = sceneOf(moved, fitting, downstream);
|
|
249
|
+
const connectivity = analyzePortConnectivity(moved, nodes);
|
|
250
|
+
const preview = {
|
|
251
|
+
...moved,
|
|
252
|
+
path: [
|
|
253
|
+
[-0.2, 0, 1],
|
|
254
|
+
[0.2, 0, 1.00005],
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
const updates = resolveConnectivityUpdates(connectivity, preview);
|
|
258
|
+
expectPointClose(updates.find((u) => u.id === fitting.id).data.position, [0, 0, 1.000025]);
|
|
259
|
+
const path = updates.find((u) => u.id === downstream.id).data.path;
|
|
260
|
+
expectPointClose(path[0], [0, 0, 1.200025]);
|
|
261
|
+
expectPointClose(path[1], [3, 0, 1.200025]);
|
|
262
|
+
});
|
|
263
|
+
test('a fitting reached from incompatible collars merges constraints deterministically', () => {
|
|
264
|
+
const moved = makeNode('duct-segment', {
|
|
265
|
+
path: [
|
|
266
|
+
[-0.2, 0, 0],
|
|
267
|
+
[0.2, 0, 0],
|
|
268
|
+
],
|
|
269
|
+
system: 'supply',
|
|
270
|
+
});
|
|
271
|
+
const fitting = makeNode('duct-fitting', { position: [0, 0, 0], system: 'supply' });
|
|
272
|
+
const nodes = sceneOf(moved, fitting);
|
|
273
|
+
const connectivity = analyzePortConnectivity(moved, nodes);
|
|
274
|
+
const preview = {
|
|
275
|
+
...moved,
|
|
276
|
+
path: [
|
|
277
|
+
[-0.2, 0, 1],
|
|
278
|
+
[0.2, 0, -1],
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
const updates = resolveConnectivityUpdates(connectivity, preview);
|
|
282
|
+
expectPointClose(updates.find((u) => u.id === fitting.id).data.position, [0, 0, 0]);
|
|
283
|
+
});
|
|
284
|
+
test('an unrelated run not on the fitting is left alone', () => {
|
|
285
|
+
const { fitting, ductA, ductB } = joint();
|
|
286
|
+
const distant = makeNode('duct-segment', {
|
|
287
|
+
path: [
|
|
288
|
+
[10, 0, 0],
|
|
289
|
+
[13, 0, 0],
|
|
290
|
+
],
|
|
291
|
+
system: 'supply',
|
|
292
|
+
});
|
|
293
|
+
const nodes = sceneOf(fitting, ductA, ductB, distant);
|
|
294
|
+
const connectivity = analyzePortConnectivity(ductA, nodes);
|
|
295
|
+
expect(connectivity.connections.find((c) => c.nodeId === distant.id)).toBeUndefined();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AnyNode, AnyNodeId } from '../schema';
|
|
2
|
+
/** A 3D level-local point (meters) projected to 2D iso screen space.
|
|
3
|
+
* Screen Y grows DOWNWARD (SVG convention), so higher elevation → lower
|
|
4
|
+
* screen Y. */
|
|
5
|
+
export declare function projectIso(x: number, y: number, z: number): [number, number];
|
|
6
|
+
export type RiserLine = {
|
|
7
|
+
/** Projected endpoints in iso screen space. */
|
|
8
|
+
from: [number, number];
|
|
9
|
+
to: [number, number];
|
|
10
|
+
system: 'waste' | 'vent';
|
|
11
|
+
/** Nominal size in inches. */
|
|
12
|
+
diameter: number;
|
|
13
|
+
/** True for a (near-)vertical run — drawn solid/bold as a stack. */
|
|
14
|
+
vertical: boolean;
|
|
15
|
+
/** Source node, so the editor can link selection. */
|
|
16
|
+
nodeId: AnyNodeId;
|
|
17
|
+
};
|
|
18
|
+
export type RiserMarker = {
|
|
19
|
+
point: [number, number];
|
|
20
|
+
kind: 'trap' | 'vent-termination' | 'fitting';
|
|
21
|
+
label: string;
|
|
22
|
+
nodeId: AnyNodeId;
|
|
23
|
+
};
|
|
24
|
+
export type RiserDiagram = {
|
|
25
|
+
lines: RiserLine[];
|
|
26
|
+
markers: RiserMarker[];
|
|
27
|
+
/** Bounding box of all projected geometry, screen space. */
|
|
28
|
+
bounds: {
|
|
29
|
+
minX: number;
|
|
30
|
+
minY: number;
|
|
31
|
+
maxX: number;
|
|
32
|
+
maxY: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Build the riser diagram for the whole scene. Returns null when there's
|
|
37
|
+
* no DWV geometry to draw.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildRiserDiagram(nodes: Readonly<Record<AnyNodeId, AnyNode>>): RiserDiagram | null;
|
|
40
|
+
//# sourceMappingURL=riser-diagram.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"riser-diagram.d.ts","sourceRoot":"","sources":["../../src/services/riser-diagram.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAiBnD;;gBAEgB;AAChB,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAI5E;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,+CAA+C;IAC/C,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtB,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpB,MAAM,EAAE,OAAO,GAAG,MAAM,CAAA;IACxB,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAA;IACjB,qDAAqD;IACrD,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvB,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAA;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,SAAS,EAAE,CAAA;IAClB,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,4DAA4D;IAC5D,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACnE,CAAA;AAcD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,GAC1C,YAAY,GAAG,IAAI,CAuErB"}
|