@pascal-app/core 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/events/bus.d.ts +30 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
- package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
- package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +27 -1
- package/dist/hooks/scene-registry/scene-registry.test.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
- package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +93 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +69 -12
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +476 -195
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +19 -1
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- package/dist/lib/level-name.d.ts +4 -0
- package/dist/lib/level-name.d.ts.map +1 -0
- package/dist/lib/level-name.js +10 -0
- package/dist/lib/measurement-geometry.d.ts +19 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.js +232 -0
- package/dist/lib/measurement-geometry.test.d.ts +2 -0
- package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.test.js +102 -0
- package/dist/lib/polygon-geometry.d.ts +0 -1
- package/dist/lib/polygon-geometry.d.ts.map +1 -1
- package/dist/lib/polygon-geometry.js +0 -18
- package/dist/lib/selection-proxy.d.ts +4 -0
- package/dist/lib/selection-proxy.d.ts.map +1 -0
- package/dist/lib/selection-proxy.js +12 -0
- package/dist/lib/slab-polygon.d.ts +31 -2
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +525 -31
- package/dist/lib/slab-polygon.test.d.ts +2 -0
- package/dist/lib/slab-polygon.test.d.ts.map +1 -0
- package/dist/lib/slab-polygon.test.js +635 -0
- package/dist/lib/slots.d.ts +14 -0
- package/dist/lib/slots.d.ts.map +1 -0
- package/dist/lib/slots.js +26 -0
- package/dist/lib/space-detection.d.ts +25 -2
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +414 -118
- package/dist/lib/space-detection.test.js +301 -2
- package/dist/lib/wall-distance.d.ts +60 -0
- package/dist/lib/wall-distance.d.ts.map +1 -0
- package/dist/lib/wall-distance.js +85 -0
- package/dist/lib/zone-quantities.d.ts +21 -0
- package/dist/lib/zone-quantities.d.ts.map +1 -0
- package/dist/lib/zone-quantities.js +183 -0
- package/dist/lib/zone-quantities.test.d.ts +2 -0
- package/dist/lib/zone-quantities.test.d.ts.map +1 -0
- package/dist/lib/zone-quantities.test.js +82 -0
- package/dist/material-library.d.ts +20 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +2181 -441
- package/dist/registry/handles.d.ts +75 -2
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -3
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/registry.d.ts +46 -3
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +82 -1
- package/dist/registry/registry.test.js +14 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +4 -0
- package/dist/registry/subtree.test.js +30 -0
- package/dist/registry/types.d.ts +717 -38
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +21 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +17 -3
- package/dist/schema/material.d.ts +3 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +7 -5
- package/dist/schema/material.test.d.ts +2 -0
- package/dist/schema/material.test.d.ts.map +1 -0
- package/dist/schema/material.test.js +43 -0
- package/dist/schema/nodes/box-vent.d.ts +2 -2
- package/dist/schema/nodes/cabinet.d.ts +534 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -0
- package/dist/schema/nodes/cabinet.js +137 -0
- package/dist/schema/nodes/ceiling.d.ts +3 -2
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +4 -0
- package/dist/schema/nodes/chimney.d.ts +4 -4
- package/dist/schema/nodes/column.d.ts +3 -2
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +3 -0
- package/dist/schema/nodes/cupola.d.ts +2 -2
- package/dist/schema/nodes/door.d.ts +13 -5
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +12 -0
- package/dist/schema/nodes/dormer.d.ts +8 -8
- package/dist/schema/nodes/dormer.js +1 -1
- package/dist/schema/nodes/downspout.d.ts +2 -2
- package/dist/schema/nodes/duct-fitting.d.ts +91 -0
- package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/duct-fitting.js +92 -0
- package/dist/schema/nodes/duct-segment.d.ts +61 -0
- package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
- package/dist/schema/nodes/duct-segment.js +73 -0
- package/dist/schema/nodes/duct-terminal.d.ts +57 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
- package/dist/schema/nodes/duct-terminal.js +51 -0
- package/dist/schema/nodes/elevator.d.ts +3 -2
- package/dist/schema/nodes/elevator.d.ts.map +1 -1
- package/dist/schema/nodes/elevator.js +3 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
- package/dist/schema/nodes/fence.d.ts +18 -2
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +25 -2
- package/dist/schema/nodes/gutter.d.ts +2 -2
- package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
- package/dist/schema/nodes/hvac-equipment.js +55 -0
- package/dist/schema/nodes/item.d.ts +8 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +11 -0
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +2 -0
- package/dist/schema/nodes/lineset.d.ts +42 -0
- package/dist/schema/nodes/lineset.d.ts.map +1 -0
- package/dist/schema/nodes/lineset.js +38 -0
- package/dist/schema/nodes/liquid-line.d.ts +34 -0
- package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
- package/dist/schema/nodes/liquid-line.js +24 -0
- package/dist/schema/nodes/measurement.d.ts +306 -0
- package/dist/schema/nodes/measurement.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.js +87 -0
- package/dist/schema/nodes/measurement.test.d.ts +2 -0
- package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.test.js +139 -0
- package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
- package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-fitting.js +45 -0
- package/dist/schema/nodes/pipe-segment.d.ts +48 -0
- package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-segment.js +36 -0
- package/dist/schema/nodes/pipe-trap.d.ts +43 -0
- package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-trap.js +36 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +2 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
- package/dist/schema/nodes/ridge-vent.d.ts +18 -3
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
- package/dist/schema/nodes/ridge-vent.js +221 -3
- package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
- package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.js +318 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-trim.test.js +107 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.js +330 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
- package/dist/schema/nodes/roof-segment.d.ts +84 -8
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +290 -5
- package/dist/schema/nodes/roof.d.ts +8 -8
- package/dist/schema/nodes/shelf.d.ts +3 -2
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +6 -0
- package/dist/schema/nodes/skylight.d.ts +10 -10
- package/dist/schema/nodes/slab.d.ts +3 -2
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +4 -0
- package/dist/schema/nodes/solar-panel.d.ts +4 -4
- package/dist/schema/nodes/stair-segment.d.ts +4 -4
- package/dist/schema/nodes/stair.d.ts +13 -12
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +3 -0
- package/dist/schema/nodes/turbine-vent.d.ts +2 -2
- package/dist/schema/nodes/wall.d.ts +245 -6
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +257 -0
- package/dist/schema/nodes/wall.test.d.ts +2 -0
- package/dist/schema/nodes/wall.test.d.ts.map +1 -0
- package/dist/schema/nodes/wall.test.js +208 -0
- package/dist/schema/nodes/window.d.ts +11 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +12 -0
- package/dist/schema/nodes/zone.d.ts +2 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +6 -0
- package/dist/schema/scene-material.d.ts +41 -0
- package/dist/schema/scene-material.d.ts.map +1 -0
- package/dist/schema/scene-material.js +9 -0
- package/dist/schema/types.d.ts +1089 -94
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +23 -0
- package/dist/services/alignment-anchors.d.ts.map +1 -1
- package/dist/services/alignment-anchors.js +30 -1
- package/dist/services/alignment.d.ts +5 -0
- package/dist/services/alignment.d.ts.map +1 -1
- package/dist/services/alignment.js +2 -0
- package/dist/services/hosting.d.ts +8 -0
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +13 -4
- package/dist/services/hosting.test.js +32 -1
- package/dist/services/index.d.ts +8 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +8 -2
- package/dist/services/level-height.d.ts +28 -0
- package/dist/services/level-height.d.ts.map +1 -0
- package/dist/services/level-height.js +62 -0
- package/dist/services/opening-guides.d.ts +141 -0
- package/dist/services/opening-guides.d.ts.map +1 -0
- package/dist/services/opening-guides.js +267 -0
- package/dist/services/opening-guides.test.d.ts +2 -0
- package/dist/services/opening-guides.test.d.ts.map +1 -0
- package/dist/services/opening-guides.test.js +208 -0
- package/dist/services/port-connectivity.d.ts +115 -0
- package/dist/services/port-connectivity.d.ts.map +1 -0
- package/dist/services/port-connectivity.js +300 -0
- package/dist/services/port-connectivity.test.d.ts +2 -0
- package/dist/services/port-connectivity.test.d.ts.map +1 -0
- package/dist/services/port-connectivity.test.js +297 -0
- package/dist/services/riser-diagram.d.ts +40 -0
- package/dist/services/riser-diagram.d.ts.map +1 -0
- package/dist/services/riser-diagram.js +110 -0
- package/dist/services/riser-diagram.test.d.ts +2 -0
- package/dist/services/riser-diagram.test.d.ts.map +1 -0
- package/dist/services/riser-diagram.test.js +66 -0
- package/dist/services/snap.d.ts +9 -1
- package/dist/services/snap.d.ts.map +1 -1
- package/dist/services/snap.js +21 -1
- package/dist/services/snap.test.js +43 -1
- package/dist/services/system-graph.d.ts +30 -0
- package/dist/services/system-graph.d.ts.map +1 -0
- package/dist/services/system-graph.js +164 -0
- package/dist/services/system-graph.test.d.ts +2 -0
- package/dist/services/system-graph.test.d.ts.map +1 -0
- package/dist/services/system-graph.test.js +138 -0
- package/dist/services/validate-dwv.d.ts +32 -0
- package/dist/services/validate-dwv.d.ts.map +1 -0
- package/dist/services/validate-dwv.js +118 -0
- package/dist/services/validate-dwv.test.d.ts +2 -0
- package/dist/services/validate-dwv.test.d.ts.map +1 -0
- package/dist/services/validate-dwv.test.js +109 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +94 -1
- package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
- package/dist/store/actions/ridge-vent-update.test.js +264 -0
- package/dist/store/history-control.d.ts +11 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +33 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
- package/dist/store/use-scene-dirty-tracking.test.js +68 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
- package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
- package/dist/store/use-scene-plugins.test.d.ts +2 -0
- package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
- package/dist/store/use-scene-plugins.test.js +51 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
- package/dist/store/use-scene-window-migration.test.d.ts +2 -0
- package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-window-migration.test.js +85 -0
- package/dist/store/use-scene.d.ts +17 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +358 -51
- package/dist/systems/elevator/elevator-service.js +1 -1
- package/dist/systems/fence/fence-centerline.d.ts +12 -0
- package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
- package/dist/systems/fence/fence-centerline.js +37 -0
- package/dist/systems/fence/fence-spline.d.ts +39 -0
- package/dist/systems/fence/fence-spline.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.js +196 -0
- package/dist/systems/fence/fence-spline.test.d.ts +2 -0
- package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.test.js +136 -0
- package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
- package/dist/systems/stair/stair-footprint.js +21 -11
- package/dist/systems/stair/stair-opening-preview.d.ts +1139 -469
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +1 -18
- package/dist/systems/stair/stair-opening-sync.test.js +39 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +38 -2
- package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
- package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-mitering.test.js +63 -0
- package/dist/utils/clone-scene-graph.d.ts +1 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +23 -3
- package/dist/utils/clone-scene-graph.test.js +3 -0
- package/dist/utils/heal-scene-graph.d.ts +18 -0
- package/dist/utils/heal-scene-graph.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.js +90 -0
- package/dist/utils/heal-scene-graph.test.d.ts +2 -0
- package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.test.js +88 -0
- package/dist/validation/validate-build-json.d.ts +1 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +31 -2
- package/package.json +3 -3
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { getRenderableSlabPolygon } from '../../lib/slab-polygon';
|
|
2
|
+
import { nodeRegistry } from '../../registry';
|
|
1
3
|
import { getScaledDimensions, isLowProfileItemSurface } from '../../schema';
|
|
2
4
|
import useScene from '../../store/use-scene';
|
|
3
|
-
import {
|
|
5
|
+
import { getWallCurveFrameAt, isCurvedWall } from '../../systems/wall/wall-curve';
|
|
4
6
|
import { DEFAULT_WALL_THICKNESS } from '../../systems/wall/wall-footprint';
|
|
7
|
+
import { getFloorPlacedFootprints } from './floor-placed-elevation';
|
|
5
8
|
import { SpatialGrid } from './spatial-grid';
|
|
6
9
|
import { WallSpatialGrid } from './wall-spatial-grid';
|
|
7
10
|
// ============================================================================
|
|
@@ -40,6 +43,24 @@ function getItemFootprint(position, dimensions, rotation, inset = 0) {
|
|
|
40
43
|
[x + (-halfW * cos - halfD * sin), z + (-halfW * sin + halfD * cos)],
|
|
41
44
|
];
|
|
42
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Axis-aligned XZ extent of a footprint at `position`, rotated by `yRot`. The
|
|
48
|
+
* rotated width/depth is the same conservative bound the floor-placement draft
|
|
49
|
+
* uses, so a draft and an existing node are compared with identical math.
|
|
50
|
+
*/
|
|
51
|
+
function footprintBoundsXZ(position, dimensions, yRot) {
|
|
52
|
+
const [width, , depth] = dimensions;
|
|
53
|
+
const cos = Math.abs(Math.cos(yRot));
|
|
54
|
+
const sin = Math.abs(Math.sin(yRot));
|
|
55
|
+
const rotatedW = width * cos + depth * sin;
|
|
56
|
+
const rotatedD = width * sin + depth * cos;
|
|
57
|
+
return {
|
|
58
|
+
minX: position[0] - rotatedW / 2,
|
|
59
|
+
maxX: position[0] + rotatedW / 2,
|
|
60
|
+
minZ: position[2] - rotatedD / 2,
|
|
61
|
+
maxZ: position[2] + rotatedD / 2,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
43
64
|
function getItemLocalBounds(item) {
|
|
44
65
|
const [width, height, depth] = getScaledDimensions(item);
|
|
45
66
|
const minZ = item.asset.attachTo === 'wall-side' ? -depth : -depth / 2;
|
|
@@ -99,6 +120,24 @@ function resolveNodeLevelId(node, nodes) {
|
|
|
99
120
|
}
|
|
100
121
|
return 'default';
|
|
101
122
|
}
|
|
123
|
+
function expandIgnoredNodeIds(ignoreIds, nodes) {
|
|
124
|
+
const ignored = new Set(ignoreIds ?? []);
|
|
125
|
+
const queue = [...ignored];
|
|
126
|
+
while (queue.length > 0) {
|
|
127
|
+
const id = queue.pop();
|
|
128
|
+
const node = nodes[id];
|
|
129
|
+
const children = node?.children;
|
|
130
|
+
if (!Array.isArray(children))
|
|
131
|
+
continue;
|
|
132
|
+
for (const childId of children) {
|
|
133
|
+
if (typeof childId !== 'string' || ignored.has(childId))
|
|
134
|
+
continue;
|
|
135
|
+
ignored.add(childId);
|
|
136
|
+
queue.push(childId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return ignored;
|
|
140
|
+
}
|
|
102
141
|
/**
|
|
103
142
|
* Test if two line segments (a1->a2) and (b1->b2) intersect.
|
|
104
143
|
*/
|
|
@@ -164,39 +203,228 @@ export function itemOverlapsPolygon(position, dimensions, rotation, polygon, ins
|
|
|
164
203
|
}
|
|
165
204
|
return false;
|
|
166
205
|
}
|
|
206
|
+
function pointSegmentDistance(px, pz, ax, az, bx, bz) {
|
|
207
|
+
const dx = bx - ax;
|
|
208
|
+
const dz = bz - az;
|
|
209
|
+
const lengthSquared = dx * dx + dz * dz;
|
|
210
|
+
if (lengthSquared < 1e-18)
|
|
211
|
+
return Math.hypot(px - ax, pz - az);
|
|
212
|
+
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (pz - az) * dz) / lengthSquared));
|
|
213
|
+
return Math.hypot(px - (ax + dx * t), pz - (az + dz * t));
|
|
214
|
+
}
|
|
215
|
+
// Ray-cast pointInPolygon is unreliable for points exactly on the polygon
|
|
216
|
+
// boundary: the answer flips depending on which side of the polygon the edge
|
|
217
|
+
// is on. Interval classification below therefore treats "within this distance
|
|
218
|
+
// of the boundary" as inside explicitly, so walls sitting exactly on a slab
|
|
219
|
+
// edge (the common case — auto-slab polygons derive from wall centerlines)
|
|
220
|
+
// classify identically on every side of the slab.
|
|
221
|
+
const ON_BOUNDARY_EPSILON = 1e-4;
|
|
222
|
+
function pointOnPolygonBoundary(px, pz, polygon) {
|
|
223
|
+
const n = polygon.length;
|
|
224
|
+
for (let i = 0; i < n; i++) {
|
|
225
|
+
const [ax, az] = polygon[i];
|
|
226
|
+
const [bx, bz] = polygon[(i + 1) % n];
|
|
227
|
+
if (pointSegmentDistance(px, pz, ax, az, bx, bz) <= ON_BOUNDARY_EPSILON)
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function mergeIntervals(intervals) {
|
|
233
|
+
if (intervals.length <= 1)
|
|
234
|
+
return intervals;
|
|
235
|
+
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
236
|
+
const merged = [[sorted[0][0], sorted[0][1]]];
|
|
237
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
238
|
+
const [intervalStart, intervalEnd] = sorted[i];
|
|
239
|
+
const last = merged[merged.length - 1];
|
|
240
|
+
if (intervalStart <= last[1] + 1e-9) {
|
|
241
|
+
last[1] = Math.max(last[1], intervalEnd);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
merged.push([intervalStart, intervalEnd]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return merged;
|
|
248
|
+
}
|
|
249
|
+
/** Total length of a merged (sorted, disjoint) interval list. */
|
|
250
|
+
function intervalsLength(intervals) {
|
|
251
|
+
let total = 0;
|
|
252
|
+
for (const [intervalStart, intervalEnd] of intervals)
|
|
253
|
+
total += intervalEnd - intervalStart;
|
|
254
|
+
return total;
|
|
255
|
+
}
|
|
256
|
+
/** `base` minus `cut`. Both inputs may be unsorted; the result is merged. */
|
|
257
|
+
function subtractIntervals(base, cut) {
|
|
258
|
+
if (base.length === 0 || cut.length === 0)
|
|
259
|
+
return mergeIntervals(base);
|
|
260
|
+
const cuts = mergeIntervals(cut);
|
|
261
|
+
const result = [];
|
|
262
|
+
for (const [baseStart, baseEnd] of mergeIntervals(base)) {
|
|
263
|
+
let cursor = baseStart;
|
|
264
|
+
for (const [cutStart, cutEnd] of cuts) {
|
|
265
|
+
if (cutEnd <= cursor)
|
|
266
|
+
continue;
|
|
267
|
+
if (cutStart >= baseEnd)
|
|
268
|
+
break;
|
|
269
|
+
if (cutStart > cursor)
|
|
270
|
+
result.push([cursor, cutStart]);
|
|
271
|
+
cursor = cutEnd;
|
|
272
|
+
if (cursor >= baseEnd)
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
if (cursor < baseEnd)
|
|
276
|
+
result.push([cursor, baseEnd]);
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Sub-intervals of segment (ax,az)→(bx,bz) that lie inside the polygon (and,
|
|
282
|
+
* when `includeBoundary`, on its boundary), as [t0, t1] fractions of the
|
|
283
|
+
* segment. The segment is split at every crossing with a polygon edge and
|
|
284
|
+
* each sub-interval is classified by its midpoint, so no test point ever
|
|
285
|
+
* sits on a crossing.
|
|
286
|
+
*/
|
|
287
|
+
function segmentInsideIntervals(ax, az, bx, bz, polygon, includeBoundary) {
|
|
288
|
+
const dx = bx - ax;
|
|
289
|
+
const dz = bz - az;
|
|
290
|
+
const length = Math.hypot(dx, dz);
|
|
291
|
+
if (length < 1e-9)
|
|
292
|
+
return [];
|
|
293
|
+
const ts = [0, 1];
|
|
294
|
+
const n = polygon.length;
|
|
295
|
+
for (let i = 0; i < n; i++) {
|
|
296
|
+
const [px, pz] = polygon[i];
|
|
297
|
+
const [qx, qz] = polygon[(i + 1) % n];
|
|
298
|
+
const ex = qx - px;
|
|
299
|
+
const ez = qz - pz;
|
|
300
|
+
const denom = dx * ez - dz * ex;
|
|
301
|
+
if (Math.abs(denom) < 1e-12)
|
|
302
|
+
continue; // parallel/collinear — nothing to split at
|
|
303
|
+
const t = ((px - ax) * ez - (pz - az) * ex) / denom;
|
|
304
|
+
const s = ((px - ax) * dz - (pz - az) * dx) / denom;
|
|
305
|
+
if (t > 0 && t < 1 && s >= -1e-9 && s <= 1 + 1e-9)
|
|
306
|
+
ts.push(t);
|
|
307
|
+
}
|
|
308
|
+
ts.sort((a, b) => a - b);
|
|
309
|
+
const inside = [];
|
|
310
|
+
for (let i = 1; i < ts.length; i++) {
|
|
311
|
+
const t0 = ts[i - 1];
|
|
312
|
+
const t1 = ts[i];
|
|
313
|
+
if (t1 - t0 < 1e-9)
|
|
314
|
+
continue;
|
|
315
|
+
const tm = (t0 + t1) / 2;
|
|
316
|
+
const mx = ax + dx * tm;
|
|
317
|
+
const mz = az + dz * tm;
|
|
318
|
+
const midpointInside = pointOnPolygonBoundary(mx, mz, polygon)
|
|
319
|
+
? includeBoundary
|
|
320
|
+
: pointInPolygon(mx, mz, polygon);
|
|
321
|
+
if (midpointInside)
|
|
322
|
+
inside.push([t0, t1]);
|
|
323
|
+
}
|
|
324
|
+
return inside;
|
|
325
|
+
}
|
|
326
|
+
function polylineLength(points) {
|
|
327
|
+
let total = 0;
|
|
328
|
+
for (let i = 1; i < points.length; i++) {
|
|
329
|
+
total += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
|
|
330
|
+
}
|
|
331
|
+
return total;
|
|
332
|
+
}
|
|
167
333
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
334
|
+
* Inside sub-intervals of a polyline against a polygon, in cumulative
|
|
335
|
+
* arc-length units from the polyline start (merged, disjoint). Boundary
|
|
336
|
+
* contact counts as inside for slab support (walls sit exactly on slab
|
|
337
|
+
* edges — see ON_BOUNDARY_EPSILON above); hole callers pass
|
|
338
|
+
* `includeBoundary: false` so a wall running along a stairwell hole's
|
|
339
|
+
* rim keeps the rim's support.
|
|
171
340
|
*/
|
|
172
|
-
function
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const a2OnB = onSegment(bx1, bz1, bx2, bz2, ax2, az2);
|
|
188
|
-
return a1OnB && a2OnB;
|
|
341
|
+
function polylineInsideIntervals(points, polygon, includeBoundary = true) {
|
|
342
|
+
const intervals = [];
|
|
343
|
+
let offset = 0;
|
|
344
|
+
for (let i = 1; i < points.length; i++) {
|
|
345
|
+
const a = points[i - 1];
|
|
346
|
+
const b = points[i];
|
|
347
|
+
const segmentLength = Math.hypot(b.x - a.x, b.y - a.y);
|
|
348
|
+
if (segmentLength < 1e-9)
|
|
349
|
+
continue;
|
|
350
|
+
for (const [t0, t1] of segmentInsideIntervals(a.x, a.y, b.x, b.y, polygon, includeBoundary)) {
|
|
351
|
+
intervals.push([offset + t0 * segmentLength, offset + t1 * segmentLength]);
|
|
352
|
+
}
|
|
353
|
+
offset += segmentLength;
|
|
354
|
+
}
|
|
355
|
+
return mergeIntervals(intervals);
|
|
189
356
|
}
|
|
357
|
+
function polylineInsideLength(points, polygon) {
|
|
358
|
+
return intervalsLength(polylineInsideIntervals(points, polygon));
|
|
359
|
+
}
|
|
360
|
+
// Minimum length of wall that must lie on/inside a slab polygon before the
|
|
361
|
+
// wall counts as overlapping it. Point contact (a perpendicular wall butting
|
|
362
|
+
// into a room's edge) clips to ~zero length and never reaches this, so such
|
|
363
|
+
// walls don't follow the slab's elevation.
|
|
364
|
+
const WALL_SLAB_MIN_OVERLAP = 0.05;
|
|
190
365
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
|
|
196
|
-
|
|
366
|
+
* Centerline of the wall plus its two face lines (centerline offset by
|
|
367
|
+
* ±halfThickness). The face lines catch walls whose centerline sits on or
|
|
368
|
+
* just outside the slab boundary but whose body reaches onto the slab —
|
|
369
|
+
* e.g. slab polygons drawn to the room's interior faces.
|
|
370
|
+
*/
|
|
371
|
+
function wallTestPolylines(start, end, curveOffset, halfThickness) {
|
|
372
|
+
const wallLike = { start, end, curveOffset };
|
|
373
|
+
if (curveOffset !== 0 && isCurvedWall(wallLike)) {
|
|
374
|
+
const count = 16;
|
|
375
|
+
const center = [];
|
|
376
|
+
const left = [];
|
|
377
|
+
const right = [];
|
|
378
|
+
for (let i = 0; i <= count; i++) {
|
|
379
|
+
const frame = getWallCurveFrameAt(wallLike, i / count);
|
|
380
|
+
center.push(frame.point);
|
|
381
|
+
left.push({
|
|
382
|
+
x: frame.point.x + frame.normal.x * halfThickness,
|
|
383
|
+
y: frame.point.y + frame.normal.y * halfThickness,
|
|
384
|
+
});
|
|
385
|
+
right.push({
|
|
386
|
+
x: frame.point.x - frame.normal.x * halfThickness,
|
|
387
|
+
y: frame.point.y - frame.normal.y * halfThickness,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return halfThickness > 0 ? [center, left, right] : [center];
|
|
391
|
+
}
|
|
392
|
+
const center = [
|
|
393
|
+
{ x: start[0], y: start[1] },
|
|
394
|
+
{ x: end[0], y: end[1] },
|
|
395
|
+
];
|
|
396
|
+
const dx = end[0] - start[0];
|
|
397
|
+
const dz = end[1] - start[1];
|
|
398
|
+
const len = Math.hypot(dx, dz);
|
|
399
|
+
if (len < 1e-10 || halfThickness <= 0)
|
|
400
|
+
return [center];
|
|
401
|
+
const nx = (-dz / len) * halfThickness;
|
|
402
|
+
const nz = (dx / len) * halfThickness;
|
|
403
|
+
return [
|
|
404
|
+
center,
|
|
405
|
+
[
|
|
406
|
+
{ x: start[0] + nx, y: start[1] + nz },
|
|
407
|
+
{ x: end[0] + nx, y: end[1] + nz },
|
|
408
|
+
],
|
|
409
|
+
[
|
|
410
|
+
{ x: start[0] - nx, y: start[1] - nz },
|
|
411
|
+
{ x: end[0] - nx, y: end[1] - nz },
|
|
412
|
+
],
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Test whether a wall overlaps a slab polygon along a segment of its length.
|
|
417
|
+
*
|
|
418
|
+
* The wall's centerline and both face lines are clipped against the polygon;
|
|
419
|
+
* the wall overlaps when the longest clipped inside-or-on-boundary length
|
|
420
|
+
* exceeds a threshold (5cm, halved for very short walls). Because interval
|
|
421
|
+
* midpoints classify "on the boundary" as inside explicitly (never by
|
|
422
|
+
* ray-cast tie-breaking), a wall sitting exactly on a slab edge resolves
|
|
423
|
+
* identically on every side of the slab.
|
|
197
424
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
425
|
+
* A wall that only touches the polygon at a point — a perpendicular wall
|
|
426
|
+
* butting into a room's edge, or a corner-to-corner touch — clips to ~zero
|
|
427
|
+
* length and does NOT overlap.
|
|
200
428
|
*/
|
|
201
429
|
export function wallOverlapsPolygon(startOrWall, endOrPolygon, polygonArg) {
|
|
202
430
|
// Two call shapes:
|
|
@@ -220,112 +448,154 @@ export function wallOverlapsPolygon(startOrWall, endOrPolygon, polygonArg) {
|
|
|
220
448
|
polygon = endOrPolygon;
|
|
221
449
|
}
|
|
222
450
|
const halfThickness = Math.max(thickness / 2, 0);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
451
|
+
const polylines = wallTestPolylines(start, end, curveOffset, halfThickness);
|
|
452
|
+
const centerLength = polylineLength(polylines[0]);
|
|
453
|
+
if (centerLength < 1e-9)
|
|
454
|
+
return false;
|
|
455
|
+
let overlap = 0;
|
|
456
|
+
for (const line of polylines) {
|
|
457
|
+
overlap = Math.max(overlap, polylineInsideLength(line, polygon));
|
|
458
|
+
}
|
|
459
|
+
const threshold = Math.max(1e-3, Math.min(WALL_SLAB_MIN_OVERLAP, centerLength * 0.5));
|
|
460
|
+
return overlap >= threshold;
|
|
461
|
+
}
|
|
462
|
+
// A slab elevation must support at least this fraction of the wall's
|
|
463
|
+
// length before it can dictate the wall's base. Below majority, a raised
|
|
464
|
+
// slab reaching one endpoint would hoist the whole wall off the floor
|
|
465
|
+
// that actually carries it.
|
|
466
|
+
const WALL_SLAB_SUPPORT_MAJORITY = 0.5;
|
|
467
|
+
// Slabs whose elevations differ by less than this pool their support:
|
|
468
|
+
// a wall shared between two rooms' slabs is covered roughly half by
|
|
469
|
+
// each, and must still follow their common elevation.
|
|
470
|
+
const WALL_SLAB_ELEVATION_POOL_EPSILON = 1e-4;
|
|
471
|
+
export function computeWallSlabSupport(wallLike, slabs, levelWalls) {
|
|
472
|
+
const { start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS } = wallLike;
|
|
473
|
+
const halfThickness = Math.max(thickness / 2, 0);
|
|
474
|
+
const polylines = wallTestPolylines(start, end, curveOffset, halfThickness);
|
|
475
|
+
const polylineLengths = polylines.map(polylineLength);
|
|
476
|
+
const wallLength = polylineLengths[0];
|
|
477
|
+
if (wallLength < 1e-9) {
|
|
478
|
+
return { elevation: 0, baseElevation: 0, baseSegments: [] };
|
|
479
|
+
}
|
|
480
|
+
const minSupport = Math.max(1e-3, Math.min(WALL_SLAB_MIN_OVERLAP, wallLength * 0.5));
|
|
481
|
+
const groups = [];
|
|
482
|
+
for (const slab of slabs) {
|
|
483
|
+
if (slab.polygon.length < 3)
|
|
484
|
+
continue;
|
|
485
|
+
const renderedPolygon = getRenderableSlabPolygon(slab, {
|
|
486
|
+
walls: levelWalls,
|
|
487
|
+
siblingSlabs: slabs.filter((other) => other.id !== slab.id),
|
|
488
|
+
});
|
|
489
|
+
let supported = 0;
|
|
490
|
+
const perPolyline = polylines.map((line) => {
|
|
491
|
+
let intervals = polylineInsideIntervals(line, renderedPolygon);
|
|
492
|
+
for (const hole of slab.holes || []) {
|
|
493
|
+
if (intervals.length === 0)
|
|
494
|
+
break;
|
|
495
|
+
if (hole.length < 3)
|
|
496
|
+
continue;
|
|
497
|
+
intervals = subtractIntervals(intervals, polylineInsideIntervals(line, hole, false));
|
|
254
498
|
}
|
|
499
|
+
supported = Math.max(supported, intervalsLength(intervals));
|
|
500
|
+
return intervals;
|
|
501
|
+
});
|
|
502
|
+
if (supported < minSupport)
|
|
503
|
+
continue;
|
|
504
|
+
const elevation = slab.elevation ?? 0.05;
|
|
505
|
+
let group = groups.find((candidate) => Math.abs(candidate.elevation - elevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON);
|
|
506
|
+
if (!group) {
|
|
507
|
+
group = { elevation, perPolyline: polylines.map(() => []) };
|
|
508
|
+
groups.push(group);
|
|
509
|
+
}
|
|
510
|
+
for (let i = 0; i < perPolyline.length; i++) {
|
|
511
|
+
group.perPolyline[i].push(...perPolyline[i]);
|
|
255
512
|
}
|
|
256
513
|
}
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
const tnx = -uz * halfThickness;
|
|
302
|
-
const tnz = ux * halfThickness;
|
|
303
|
-
for (const t of [0, 0.25, 0.5, 0.75, 1]) {
|
|
304
|
-
const bx = start[0] + dx * t;
|
|
305
|
-
const bz = start[1] + dz * t;
|
|
306
|
-
if (pointInPolygon(bx + tnx, bz + tnz, polygon))
|
|
307
|
-
return true;
|
|
308
|
-
if (pointInPolygon(bx - tnx, bz - tnz, polygon))
|
|
309
|
-
return true;
|
|
514
|
+
const evaluatedGroups = groups.map((group) => {
|
|
515
|
+
let coverage = 0;
|
|
516
|
+
const mergedPerPolyline = group.perPolyline.map(mergeIntervals);
|
|
517
|
+
for (let i = 0; i < group.perPolyline.length; i++) {
|
|
518
|
+
const lineLength = polylineLengths[i];
|
|
519
|
+
if (lineLength < 1e-9)
|
|
520
|
+
continue;
|
|
521
|
+
coverage = Math.max(coverage, intervalsLength(mergedPerPolyline[i]) / lineLength);
|
|
522
|
+
}
|
|
523
|
+
return { ...group, coverage, mergedPerPolyline };
|
|
524
|
+
});
|
|
525
|
+
let majorityElevation = Number.NEGATIVE_INFINITY;
|
|
526
|
+
let bestElevation = Number.NEGATIVE_INFINITY;
|
|
527
|
+
let bestCoverage = -1;
|
|
528
|
+
for (const group of evaluatedGroups) {
|
|
529
|
+
if (group.coverage >= WALL_SLAB_SUPPORT_MAJORITY - 1e-6) {
|
|
530
|
+
majorityElevation = Math.max(majorityElevation, group.elevation);
|
|
531
|
+
}
|
|
532
|
+
if (group.coverage > bestCoverage + 1e-6 ||
|
|
533
|
+
(Math.abs(group.coverage - bestCoverage) <= 1e-6 && group.elevation > bestElevation)) {
|
|
534
|
+
bestCoverage = group.coverage;
|
|
535
|
+
bestElevation = group.elevation;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const elevation = majorityElevation !== Number.NEGATIVE_INFINITY
|
|
539
|
+
? majorityElevation
|
|
540
|
+
: bestElevation === Number.NEGATIVE_INFINITY
|
|
541
|
+
? 0
|
|
542
|
+
: bestElevation;
|
|
543
|
+
const normalizedIntervals = (group, polylineIndex) => {
|
|
544
|
+
const lineLength = polylineLengths[polylineIndex];
|
|
545
|
+
if (lineLength < 1e-9)
|
|
546
|
+
return [];
|
|
547
|
+
return group.mergedPerPolyline[polylineIndex].map(([intervalStart, intervalEnd]) => [intervalStart / lineLength, intervalEnd / lineLength]);
|
|
548
|
+
};
|
|
549
|
+
const normalizedByGroup = evaluatedGroups.map((group) => ({
|
|
550
|
+
elevation: group.elevation,
|
|
551
|
+
perPolyline: group.mergedPerPolyline.map((_, index) => normalizedIntervals(group, index)),
|
|
552
|
+
}));
|
|
553
|
+
const breakpoints = [0, 1];
|
|
554
|
+
for (const group of normalizedByGroup) {
|
|
555
|
+
for (const intervals of group.perPolyline) {
|
|
556
|
+
for (const [intervalStart, intervalEnd] of intervals) {
|
|
557
|
+
breakpoints.push(intervalStart, intervalEnd);
|
|
310
558
|
}
|
|
311
559
|
}
|
|
312
560
|
}
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
561
|
+
breakpoints.sort((left, right) => left - right);
|
|
562
|
+
const uniqueBreakpoints = breakpoints.filter((value, index) => index === 0 || value - breakpoints[index - 1] > 1e-7);
|
|
563
|
+
const highestAt = (polylineIndex, t) => {
|
|
564
|
+
let highest = Number.NEGATIVE_INFINITY;
|
|
565
|
+
for (const group of normalizedByGroup) {
|
|
566
|
+
if (group.perPolyline[polylineIndex]?.some(([intervalStart, intervalEnd]) => t >= intervalStart - 1e-7 && t <= intervalEnd + 1e-7)) {
|
|
567
|
+
highest = Math.max(highest, group.elevation);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return highest;
|
|
571
|
+
};
|
|
572
|
+
const baseSegments = [];
|
|
573
|
+
for (let index = 1; index < uniqueBreakpoints.length; index++) {
|
|
574
|
+
const start = uniqueBreakpoints[index - 1];
|
|
575
|
+
const end = uniqueBreakpoints[index];
|
|
576
|
+
if (end - start < 1e-7)
|
|
577
|
+
continue;
|
|
578
|
+
const midpoint = (start + end) / 2;
|
|
579
|
+
const leftElevation = polylines.length >= 3 ? highestAt(1, midpoint) : Number.NEGATIVE_INFINITY;
|
|
580
|
+
const rightElevation = polylines.length >= 3 ? highestAt(2, midpoint) : Number.NEGATIVE_INFINITY;
|
|
581
|
+
const faceElevations = [leftElevation, rightElevation].filter(Number.isFinite);
|
|
582
|
+
const segmentElevation = faceElevations.length > 0 ? Math.min(...faceElevations) : Math.max(highestAt(0, midpoint), 0);
|
|
583
|
+
const previous = baseSegments[baseSegments.length - 1];
|
|
584
|
+
if (previous &&
|
|
585
|
+
Math.abs(previous.elevation - segmentElevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON) {
|
|
586
|
+
previous.end = end;
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
baseSegments.push({ start, end, elevation: segmentElevation });
|
|
326
590
|
}
|
|
327
591
|
}
|
|
328
|
-
|
|
592
|
+
if (baseSegments.length === 0)
|
|
593
|
+
baseSegments.push({ start: 0, end: 1, elevation });
|
|
594
|
+
const baseElevation = Math.min(...baseSegments.map((segment) => segment.elevation));
|
|
595
|
+
return { elevation, baseElevation, baseSegments };
|
|
596
|
+
}
|
|
597
|
+
export function computeWallSlabElevation(wallLike, slabs, levelWalls) {
|
|
598
|
+
return computeWallSlabSupport(wallLike, slabs, levelWalls).elevation;
|
|
329
599
|
}
|
|
330
600
|
export class SpatialGridManager {
|
|
331
601
|
floorGrids = new Map(); // levelId -> grid
|
|
@@ -512,37 +782,51 @@ export class SpatialGridManager {
|
|
|
512
782
|
}
|
|
513
783
|
// Query methods
|
|
514
784
|
canPlaceOnFloor(levelId, position, dimensions, rotation, ignoreIds) {
|
|
785
|
+
return this.canPlaceOnFloorFootprints(levelId, [{ position, dimensions, rotation }], ignoreIds);
|
|
786
|
+
}
|
|
787
|
+
canPlaceOnFloorFootprints(levelId, footprints, ignoreIds) {
|
|
515
788
|
const nodes = useScene.getState().nodes;
|
|
516
|
-
const ignoreSet =
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
789
|
+
const ignoreSet = expandIgnoredNodeIds(ignoreIds, nodes);
|
|
790
|
+
const draftBounds = footprints.map((footprint) => footprintBoundsXZ(footprint.position, footprint.dimensions, footprint.rotation[1] ?? 0));
|
|
791
|
+
for (let i = 0; i < draftBounds.length; i += 1) {
|
|
792
|
+
const a = draftBounds[i];
|
|
793
|
+
for (let j = i + 1; j < draftBounds.length; j += 1) {
|
|
794
|
+
const b = draftBounds[j];
|
|
795
|
+
if (intervalsOverlap(a.minX, a.maxX, b.minX, b.maxX) &&
|
|
796
|
+
intervalsOverlap(a.minZ, a.maxZ, b.minZ, b.maxZ)) {
|
|
797
|
+
return { valid: false, conflictIds: [] };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// A floor placement conflicts with any other COLLIDING floor-resting node,
|
|
802
|
+
// not just items — every kind whose `floorPlaced.collides` is set (item /
|
|
803
|
+
// shelf / column / cabinet / stair) contributes its footprint(s) as an
|
|
804
|
+
// obstacle. Each candidate's XZ extent is read from the same declarative
|
|
805
|
+
// footprint the elevation + sync paths use, so adding a colliding kind
|
|
806
|
+
// needs no change here.
|
|
529
807
|
const conflicts = [];
|
|
530
808
|
for (const node of Object.values(nodes)) {
|
|
531
|
-
if (node.
|
|
809
|
+
if (ignoreSet.has(node.id))
|
|
532
810
|
continue;
|
|
533
|
-
const
|
|
534
|
-
if (
|
|
811
|
+
const floorPlaced = nodeRegistry.get(node.type)?.capabilities?.floorPlaced;
|
|
812
|
+
if (!floorPlaced?.collides)
|
|
535
813
|
continue;
|
|
536
|
-
if (
|
|
814
|
+
if (floorPlaced.applies && !floorPlaced.applies(node))
|
|
537
815
|
continue;
|
|
538
|
-
|
|
816
|
+
// Low-profile item surfaces (rugs, mats) are stack-on targets, not
|
|
817
|
+
// obstacles — keep the long-standing item-only exemption.
|
|
818
|
+
if (node.type === 'item' && isLowProfileItemSurface(node))
|
|
539
819
|
continue;
|
|
540
|
-
if (resolveNodeLevelId(
|
|
820
|
+
if (resolveNodeLevelId(node, nodes) !== levelId)
|
|
541
821
|
continue;
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
822
|
+
for (const footprint of getFloorPlacedFootprints(floorPlaced, node, { nodes })) {
|
|
823
|
+
const fpRotation = Array.isArray(footprint.rotation) ? (footprint.rotation[1] ?? 0) : 0;
|
|
824
|
+
const bounds = footprintBoundsXZ(footprint.position ?? node.position, footprint.dimensions, fpRotation);
|
|
825
|
+
if (draftBounds.some((draft) => intervalsOverlap(draft.minX, draft.maxX, bounds.minX, bounds.maxX) &&
|
|
826
|
+
intervalsOverlap(draft.minZ, draft.maxZ, bounds.minZ, bounds.maxZ))) {
|
|
827
|
+
conflicts.push(node.id);
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
546
830
|
}
|
|
547
831
|
}
|
|
548
832
|
return { valid: conflicts.length === 0, conflictIds: conflicts };
|
|
@@ -674,62 +958,59 @@ export class SpatialGridManager {
|
|
|
674
958
|
}
|
|
675
959
|
/**
|
|
676
960
|
* Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
|
|
677
|
-
* Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
|
|
678
961
|
* Returns the highest slab elevation found, or 0 if none.
|
|
679
962
|
*
|
|
680
963
|
* Accepts an optional `curveOffset` so curved walls evaluate overlap
|
|
681
964
|
* against their actual centerline samples, not just the chord.
|
|
682
965
|
*/
|
|
683
966
|
getSlabElevationForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS) {
|
|
967
|
+
return this.getSlabSupportForWall(levelId, start, end, curveOffset, thickness).elevation;
|
|
968
|
+
}
|
|
969
|
+
getSlabSupportForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS) {
|
|
684
970
|
const slabMap = this.slabsByLevel.get(levelId);
|
|
685
|
-
if (!slabMap)
|
|
686
|
-
return
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
971
|
+
if (!slabMap) {
|
|
972
|
+
return {
|
|
973
|
+
elevation: 0,
|
|
974
|
+
baseElevation: 0,
|
|
975
|
+
baseSegments: [{ start: 0, end: 1, elevation: 0 }],
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
return computeWallSlabSupport({ start, end, curveOffset, thickness }, [...slabMap.values()], this.getLevelWallNodes(levelId));
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Walls on a level, resolved fresh from the scene store (the manager's
|
|
982
|
+
* own wall map is only maintained on create/delete, not on updates).
|
|
983
|
+
* Cached per scene `nodes` record so per-pointer-tick callers
|
|
984
|
+
* (door/window move) don't rescan the node map.
|
|
985
|
+
*/
|
|
986
|
+
levelWallsCache = new WeakMap();
|
|
987
|
+
getLevelWallNodes(levelId) {
|
|
988
|
+
const nodes = useScene.getState().nodes;
|
|
989
|
+
let byLevel = this.levelWallsCache.get(nodes);
|
|
990
|
+
if (!byLevel) {
|
|
991
|
+
byLevel = new Map();
|
|
992
|
+
this.levelWallsCache.set(nodes, byLevel);
|
|
993
|
+
}
|
|
994
|
+
const cached = byLevel.get(levelId);
|
|
995
|
+
if (cached)
|
|
996
|
+
return cached;
|
|
997
|
+
const walls = [];
|
|
998
|
+
for (const node of Object.values(nodes)) {
|
|
999
|
+
if (node.type !== 'wall')
|
|
707
1000
|
continue;
|
|
1001
|
+
// Walk the parent chain to the owning level (guarded against cycles).
|
|
1002
|
+
let current = node;
|
|
1003
|
+
let guard = 0;
|
|
1004
|
+
while (current && current.type !== 'level' && guard < 16) {
|
|
1005
|
+
current = current.parentId ? nodes[current.parentId] : undefined;
|
|
1006
|
+
guard += 1;
|
|
708
1007
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
// midpoint falls in a staircase hole but the wall's endpoints are on solid slab.
|
|
712
|
-
let hasValidPoint = false;
|
|
713
|
-
for (const sample of holeSamplePoints) {
|
|
714
|
-
let inHole = false;
|
|
715
|
-
for (const hole of holes) {
|
|
716
|
-
if (hole.length >= 3 && pointInPolygon(sample.x, sample.y, hole)) {
|
|
717
|
-
inHole = true;
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
if (!inHole) {
|
|
722
|
-
hasValidPoint = true;
|
|
723
|
-
break;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
if (hasValidPoint) {
|
|
727
|
-
const elevation = slab.elevation ?? 0.05;
|
|
728
|
-
if (elevation > maxElevation)
|
|
729
|
-
maxElevation = elevation;
|
|
1008
|
+
if (current?.type === 'level' && current.id === levelId) {
|
|
1009
|
+
walls.push(node);
|
|
730
1010
|
}
|
|
731
1011
|
}
|
|
732
|
-
|
|
1012
|
+
byLevel.set(levelId, walls);
|
|
1013
|
+
return walls;
|
|
733
1014
|
}
|
|
734
1015
|
/**
|
|
735
1016
|
* Check if an item can be placed on a ceiling.
|