@pascal-app/core 0.9.2 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/dist/events/bus.d.ts +21 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +116 -66
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +316 -441
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +193 -12
- package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
- package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/support-host-id.js +2 -0
- package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
- package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
- package/dist/index.d.ts +23 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -10
- package/dist/lib/measurement-geometry.d.ts +4 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -1
- package/dist/lib/measurement-geometry.js +27 -10
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +33 -1
- package/dist/lib/space-detection.d.ts +15 -4
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +174 -88
- package/dist/lib/terrain-brush.d.ts +163 -0
- package/dist/lib/terrain-brush.d.ts.map +1 -0
- package/dist/lib/terrain-brush.js +359 -0
- package/dist/lib/terrain-codec.d.ts +51 -0
- package/dist/lib/terrain-codec.d.ts.map +1 -0
- package/dist/lib/terrain-codec.js +167 -0
- package/dist/lib/terrain-field.d.ts +150 -0
- package/dist/lib/terrain-field.d.ts.map +1 -0
- package/dist/lib/terrain-field.js +248 -0
- package/dist/lib/terrain-raycast.d.ts +44 -0
- package/dist/lib/terrain-raycast.d.ts.map +1 -0
- package/dist/lib/terrain-raycast.js +188 -0
- package/dist/lib/terrain-source.d.ts +60 -0
- package/dist/lib/terrain-source.d.ts.map +1 -0
- package/dist/lib/terrain-source.js +90 -0
- package/dist/lib/terrain-support.d.ts +60 -0
- package/dist/lib/terrain-support.d.ts.map +1 -0
- package/dist/lib/terrain-support.js +150 -0
- package/dist/lib/zone-quantities.d.ts +1 -1
- package/dist/lib/zone-quantities.d.ts.map +1 -1
- package/dist/lib/zone-quantities.js +311 -66
- package/dist/material-library.d.ts +9 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +56 -4
- package/dist/registry/handles.d.ts +16 -1
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +5 -2
- package/dist/registry/types.d.ts +64 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +6 -3
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +6 -3
- package/dist/schema/nodes/cabinet.d.ts +2 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -1
- package/dist/schema/nodes/cabinet.js +4 -2
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +7 -1
- package/dist/schema/nodes/column.d.ts +1 -0
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +2 -0
- package/dist/schema/nodes/construction-dimension.d.ts +195 -0
- package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
- package/dist/schema/nodes/construction-dimension.js +151 -0
- package/dist/schema/nodes/door.d.ts +29 -0
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +20 -0
- package/dist/schema/nodes/duct-terminal.d.ts +1 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -1
- package/dist/schema/nodes/duct-terminal.js +2 -0
- package/dist/schema/nodes/fence.d.ts +2 -0
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +8 -0
- package/dist/schema/nodes/hvac-equipment.d.ts +1 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -1
- package/dist/schema/nodes/hvac-equipment.js +2 -0
- package/dist/schema/nodes/item.d.ts +1 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +13 -0
- package/dist/schema/nodes/level.d.ts +2 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +12 -33
- package/dist/schema/nodes/shelf.d.ts +1 -0
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +2 -0
- package/dist/schema/nodes/site.d.ts +9 -0
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/site.js +8 -6
- package/dist/schema/nodes/slab.d.ts +5 -0
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +14 -2
- package/dist/schema/nodes/spawn.d.ts +1 -0
- package/dist/schema/nodes/spawn.d.ts.map +1 -1
- package/dist/schema/nodes/spawn.js +2 -0
- package/dist/schema/nodes/stair.d.ts +3 -1
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +8 -1
- package/dist/schema/nodes/structural-grid.d.ts +27 -0
- package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
- package/dist/schema/nodes/structural-grid.js +17 -0
- package/dist/schema/nodes/wall.d.ts +5 -2
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +13 -4
- package/dist/schema/nodes/window.d.ts +29 -0
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +18 -0
- package/dist/schema/nodes/zone.d.ts +20 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +15 -0
- package/dist/schema/terrain.d.ts +28 -0
- package/dist/schema/terrain.d.ts.map +1 -0
- package/dist/schema/terrain.js +30 -0
- package/dist/schema/types.d.ts +211 -3
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -1
- package/dist/services/level-height.d.ts +8 -9
- package/dist/services/level-height.d.ts.map +1 -1
- package/dist/services/level-height.js +48 -16
- package/dist/services/storey.d.ts +103 -0
- package/dist/services/storey.d.ts.map +1 -0
- package/dist/services/storey.js +296 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +47 -3
- package/dist/store/history-control.d.ts +20 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +110 -13
- package/dist/store/use-live-terrain.d.ts +65 -0
- package/dist/store/use-live-terrain.d.ts.map +1 -0
- package/dist/store/use-live-terrain.js +31 -0
- package/dist/store/use-live-transforms.d.ts +8 -0
- package/dist/store/use-live-transforms.d.ts.map +1 -1
- package/dist/store/use-scene.d.ts +31 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +638 -8
- package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-opening-system.js +27 -21
- package/dist/systems/elevator/elevator-service.d.ts +0 -2
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-service.js +2 -24
- package/dist/systems/slab/slab-placement.d.ts +11 -0
- package/dist/systems/slab/slab-placement.d.ts.map +1 -0
- package/dist/systems/slab/slab-placement.js +14 -0
- package/dist/systems/slab/slab-support.d.ts +137 -0
- package/dist/systems/slab/slab-support.d.ts.map +1 -0
- package/dist/systems/slab/slab-support.js +557 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +1060 -935
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +18 -15
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.js +32 -4
- package/dist/systems/stair/stair-rise.d.ts +22 -0
- package/dist/systems/stair/stair-rise.d.ts.map +1 -0
- package/dist/systems/stair/stair-rise.js +79 -0
- package/dist/systems/wall/wall-curve.d.ts +10 -0
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +1 -1
- package/dist/systems/wall/wall-mitering.js +6 -6
- package/dist/systems/wall/wall-top.d.ts +40 -0
- package/dist/systems/wall/wall-top.d.ts.map +1 -0
- package/dist/systems/wall/wall-top.js +46 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +34 -3
- package/dist/utils/heal-scene-graph.d.ts +1 -1
- package/dist/utils/heal-scene-graph.d.ts.map +1 -1
- package/dist/utils/heal-scene-graph.js +25 -9
- package/dist/validation/validate-build-json.d.ts +2 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +59 -3
- package/package.json +2 -2
- package/dist/hooks/scene-registry/scene-registry.test.d.ts +0 -2
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +0 -1
- package/dist/hooks/scene-registry/scene-registry.test.js +0 -26
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -366
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +0 -2
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +0 -1
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +0 -363
- package/dist/lib/measurement-geometry.test.d.ts +0 -2
- package/dist/lib/measurement-geometry.test.d.ts.map +0 -1
- package/dist/lib/measurement-geometry.test.js +0 -102
- package/dist/lib/slab-polygon.test.d.ts +0 -2
- package/dist/lib/slab-polygon.test.d.ts.map +0 -1
- package/dist/lib/slab-polygon.test.js +0 -635
- package/dist/lib/space-detection-pause.test.d.ts +0 -2
- package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
- package/dist/lib/space-detection-pause.test.js +0 -42
- package/dist/lib/space-detection.test.d.ts +0 -2
- package/dist/lib/space-detection.test.d.ts.map +0 -1
- package/dist/lib/space-detection.test.js +0 -376
- package/dist/lib/zone-quantities.test.d.ts +0 -2
- package/dist/lib/zone-quantities.test.d.ts.map +0 -1
- package/dist/lib/zone-quantities.test.js +0 -82
- package/dist/registry/registry.test.d.ts +0 -2
- package/dist/registry/registry.test.d.ts.map +0 -1
- package/dist/registry/registry.test.js +0 -196
- package/dist/registry/relations-resolver.test.d.ts +0 -2
- package/dist/registry/relations-resolver.test.d.ts.map +0 -1
- package/dist/registry/relations-resolver.test.js +0 -183
- package/dist/registry/scene-api.test.d.ts +0 -2
- package/dist/registry/scene-api.test.d.ts.map +0 -1
- package/dist/registry/scene-api.test.js +0 -183
- package/dist/registry/subtree.test.d.ts +0 -2
- package/dist/registry/subtree.test.d.ts.map +0 -1
- package/dist/registry/subtree.test.js +0 -163
- package/dist/schema/asset-url.test.d.ts +0 -2
- package/dist/schema/asset-url.test.d.ts.map +0 -1
- package/dist/schema/asset-url.test.js +0 -134
- package/dist/schema/material.test.d.ts +0 -2
- package/dist/schema/material.test.d.ts.map +0 -1
- package/dist/schema/material.test.js +0 -43
- package/dist/schema/nodes/measurement.test.d.ts +0 -2
- package/dist/schema/nodes/measurement.test.d.ts.map +0 -1
- package/dist/schema/nodes/measurement.test.js +0 -139
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +0 -2
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +0 -1
- package/dist/schema/nodes/ridge-vent-defaults.test.js +0 -205
- package/dist/schema/nodes/roof-segment-shape.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-shape.test.js +0 -175
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-surface.test.js +0 -48
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-trim.test.js +0 -107
- package/dist/schema/nodes/roof-segment-walls.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-walls.test.js +0 -92
- package/dist/schema/nodes/wall.test.d.ts +0 -2
- package/dist/schema/nodes/wall.test.d.ts.map +0 -1
- package/dist/schema/nodes/wall.test.js +0 -208
- package/dist/services/alignment-anchors.test.d.ts +0 -2
- package/dist/services/alignment-anchors.test.d.ts.map +0 -1
- package/dist/services/alignment-anchors.test.js +0 -383
- package/dist/services/alignment.test.d.ts +0 -2
- package/dist/services/alignment.test.d.ts.map +0 -1
- package/dist/services/alignment.test.js +0 -92
- package/dist/services/drag-session.test.d.ts +0 -2
- package/dist/services/drag-session.test.d.ts.map +0 -1
- package/dist/services/drag-session.test.js +0 -219
- package/dist/services/hosting.test.d.ts +0 -2
- package/dist/services/hosting.test.d.ts.map +0 -1
- package/dist/services/hosting.test.js +0 -230
- package/dist/services/movement.test.d.ts +0 -2
- package/dist/services/movement.test.d.ts.map +0 -1
- package/dist/services/movement.test.js +0 -133
- package/dist/services/opening-guides.test.d.ts +0 -2
- package/dist/services/opening-guides.test.d.ts.map +0 -1
- package/dist/services/opening-guides.test.js +0 -208
- package/dist/services/port-connectivity.test.d.ts +0 -2
- package/dist/services/port-connectivity.test.d.ts.map +0 -1
- package/dist/services/port-connectivity.test.js +0 -297
- package/dist/services/riser-diagram.test.d.ts +0 -2
- package/dist/services/riser-diagram.test.d.ts.map +0 -1
- package/dist/services/riser-diagram.test.js +0 -66
- package/dist/services/single-undo-dance.test.d.ts +0 -2
- package/dist/services/single-undo-dance.test.d.ts.map +0 -1
- package/dist/services/single-undo-dance.test.js +0 -210
- package/dist/services/snap.test.d.ts +0 -2
- package/dist/services/snap.test.d.ts.map +0 -1
- package/dist/services/snap.test.js +0 -137
- package/dist/services/system-graph.test.d.ts +0 -2
- package/dist/services/system-graph.test.d.ts.map +0 -1
- package/dist/services/system-graph.test.js +0 -138
- package/dist/services/validate-dwv.test.d.ts +0 -2
- package/dist/services/validate-dwv.test.d.ts.map +0 -1
- package/dist/services/validate-dwv.test.js +0 -109
- package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
- package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
- package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
- package/dist/store/actions/reparent.test.d.ts +0 -2
- package/dist/store/actions/reparent.test.d.ts.map +0 -1
- package/dist/store/actions/reparent.test.js +0 -212
- package/dist/store/actions/ridge-vent-update.test.d.ts +0 -2
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +0 -1
- package/dist/store/actions/ridge-vent-update.test.js +0 -264
- package/dist/store/use-scene-dirty-tracking.test.d.ts +0 -2
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +0 -1
- package/dist/store/use-scene-dirty-tracking.test.js +0 -68
- package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
- package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-elevator-migration.test.js +0 -109
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts +0 -2
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +0 -1
- package/dist/store/use-scene-measurement-roundtrip.test.js +0 -97
- package/dist/store/use-scene-plugins.test.d.ts +0 -2
- package/dist/store/use-scene-plugins.test.d.ts.map +0 -1
- package/dist/store/use-scene-plugins.test.js +0 -51
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +0 -2
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-wall-slot-migration.test.js +0 -210
- package/dist/store/use-scene-window-migration.test.d.ts +0 -2
- package/dist/store/use-scene-window-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-window-migration.test.js +0 -85
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
- package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
- package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-runtime.test.js +0 -64
- package/dist/systems/fence/fence-spline.test.d.ts +0 -2
- package/dist/systems/fence/fence-spline.test.d.ts.map +0 -1
- package/dist/systems/fence/fence-spline.test.js +0 -136
- package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
- package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
- package/dist/systems/stair/stair-opening-preview.test.js +0 -80
- package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
- package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
- package/dist/systems/stair/stair-opening-sync.test.js +0 -485
- package/dist/systems/wall/wall-mitering.test.d.ts +0 -2
- package/dist/systems/wall/wall-mitering.test.d.ts.map +0 -1
- package/dist/systems/wall/wall-mitering.test.js +0 -63
- package/dist/utils/clone-scene-graph.test.d.ts +0 -2
- package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
- package/dist/utils/clone-scene-graph.test.js +0 -62
- package/dist/utils/heal-scene-graph.test.d.ts +0 -2
- package/dist/utils/heal-scene-graph.test.d.ts.map +0 -1
- package/dist/utils/heal-scene-graph.test.js +0 -88
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
import { getRenderableSlabPolygon } from '../../lib/slab-polygon';
|
|
2
|
+
import { terrainSupportLift } from '../../lib/terrain-support';
|
|
2
3
|
import { nodeRegistry } from '../../registry';
|
|
3
4
|
import { getScaledDimensions, isLowProfileItemSurface } from '../../schema';
|
|
5
|
+
import { getWallPlaneTop } from '../../services/storey';
|
|
6
|
+
import useLiveNodeOverrides, { getEffectiveNode } from '../../store/use-live-node-overrides';
|
|
7
|
+
import useLiveTransforms from '../../store/use-live-transforms';
|
|
4
8
|
import useScene from '../../store/use-scene';
|
|
5
|
-
import {
|
|
9
|
+
import { computeWallSlabSupport, pointInPolygon, SUPPORT_ELEVATION_EPSILON, } from '../../systems/slab/slab-support';
|
|
6
10
|
import { DEFAULT_WALL_THICKNESS } from '../../systems/wall/wall-footprint';
|
|
11
|
+
import { resolveWallEffectiveHeight } from '../../systems/wall/wall-top';
|
|
7
12
|
import { getFloorPlacedFootprints } from './floor-placed-elevation';
|
|
8
13
|
import { SpatialGrid } from './spatial-grid';
|
|
14
|
+
import { GROUND_SUPPORT_ID } from './support-host-id';
|
|
9
15
|
import { WallSpatialGrid } from './wall-spatial-grid';
|
|
16
|
+
export { computeWallSlabElevation, computeWallSlabSupport, pointInPolygon, SUPPORT_ELEVATION_EPSILON, wallOverlapsPolygon, } from '../../systems/slab/slab-support';
|
|
10
17
|
// ============================================================================
|
|
11
18
|
// GEOMETRY HELPERS
|
|
12
19
|
// ============================================================================
|
|
13
|
-
/**
|
|
14
|
-
* Point-in-polygon test using ray casting algorithm.
|
|
15
|
-
*/
|
|
16
|
-
export function pointInPolygon(px, pz, polygon) {
|
|
17
|
-
let inside = false;
|
|
18
|
-
const n = polygon.length;
|
|
19
|
-
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
20
|
-
const xi = polygon[i][0], zi = polygon[i][1];
|
|
21
|
-
const xj = polygon[j][0], zj = polygon[j][1];
|
|
22
|
-
if (zi > pz !== zj > pz && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
|
|
23
|
-
inside = !inside;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return inside;
|
|
27
|
-
}
|
|
28
20
|
/**
|
|
29
21
|
* Compute the 4 XZ footprint corners of an item given its position, dimensions, and Y rotation.
|
|
30
22
|
*/
|
|
@@ -203,400 +195,6 @@ export function itemOverlapsPolygon(position, dimensions, rotation, polygon, ins
|
|
|
203
195
|
}
|
|
204
196
|
return false;
|
|
205
197
|
}
|
|
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
|
-
}
|
|
333
|
-
/**
|
|
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.
|
|
340
|
-
*/
|
|
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);
|
|
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;
|
|
365
|
-
/**
|
|
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.
|
|
424
|
-
*
|
|
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.
|
|
428
|
-
*/
|
|
429
|
-
export function wallOverlapsPolygon(startOrWall, endOrPolygon, polygonArg) {
|
|
430
|
-
// Two call shapes:
|
|
431
|
-
// wallOverlapsPolygon(wallLike, polygon) — preferred; curve-aware
|
|
432
|
-
// wallOverlapsPolygon(start, end, polygon) — legacy chord-only
|
|
433
|
-
let start;
|
|
434
|
-
let end;
|
|
435
|
-
let polygon;
|
|
436
|
-
let curveOffset = 0;
|
|
437
|
-
let thickness = DEFAULT_WALL_THICKNESS;
|
|
438
|
-
if (Array.isArray(startOrWall)) {
|
|
439
|
-
start = startOrWall;
|
|
440
|
-
end = endOrPolygon;
|
|
441
|
-
polygon = polygonArg;
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
start = startOrWall.start;
|
|
445
|
-
end = startOrWall.end;
|
|
446
|
-
curveOffset = startOrWall.curveOffset ?? 0;
|
|
447
|
-
thickness = startOrWall.thickness ?? DEFAULT_WALL_THICKNESS;
|
|
448
|
-
polygon = endOrPolygon;
|
|
449
|
-
}
|
|
450
|
-
const halfThickness = Math.max(thickness / 2, 0);
|
|
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));
|
|
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]);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
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);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
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 });
|
|
590
|
-
}
|
|
591
|
-
}
|
|
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;
|
|
599
|
-
}
|
|
600
198
|
export class SpatialGridManager {
|
|
601
199
|
floorGrids = new Map(); // levelId -> grid
|
|
602
200
|
wallGrids = new Map(); // levelId -> wall grid
|
|
@@ -631,7 +229,14 @@ export class SpatialGridManager {
|
|
|
631
229
|
}
|
|
632
230
|
getWallHeight(wallId) {
|
|
633
231
|
const wall = this.walls.get(wallId);
|
|
634
|
-
|
|
232
|
+
if (!wall)
|
|
233
|
+
return 0;
|
|
234
|
+
if (wall.height != null)
|
|
235
|
+
return wall.height;
|
|
236
|
+
const nodes = useScene.getState().nodes;
|
|
237
|
+
const levelId = resolveNodeLevelId(wall, nodes);
|
|
238
|
+
const support = this.getSlabSupportForWall(levelId, wall.start, wall.end, wall.curveOffset ?? 0, wall.thickness, wall.supportSlabId ?? null, undefined, wall.supportOffset);
|
|
239
|
+
return resolveWallEffectiveHeight(wall, getWallPlaneTop(wall, levelId, nodes), support.elevation);
|
|
635
240
|
}
|
|
636
241
|
getCeilingGrid(ceilingId) {
|
|
637
242
|
if (!this.ceilingGrids.has(ceilingId)) {
|
|
@@ -645,10 +250,122 @@ export class SpatialGridManager {
|
|
|
645
250
|
}
|
|
646
251
|
return this.slabsByLevel.get(levelId);
|
|
647
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Per-slab RENDERED polygon cache (`getRenderableSlabPolygon`). Item
|
|
255
|
+
* support queries run per frame and the projection scans the level's
|
|
256
|
+
* walls + sibling slabs, so the result is cached per slab id and
|
|
257
|
+
* dropped for the whole level whenever a slab or wall on that level
|
|
258
|
+
* flows through the manager's create/update/delete handlers.
|
|
259
|
+
*/
|
|
260
|
+
renderedSlabPolygons = new Map();
|
|
261
|
+
invalidateRenderedSlabPolygons(levelId) {
|
|
262
|
+
const slabMap = this.slabsByLevel.get(levelId);
|
|
263
|
+
if (!slabMap)
|
|
264
|
+
return;
|
|
265
|
+
for (const slabId of slabMap.keys())
|
|
266
|
+
this.renderedSlabPolygons.delete(slabId);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* True while a slab or wall on `levelId` has a live preview: group drags
|
|
270
|
+
* publish translated slab polygons and wall endpoints to
|
|
271
|
+
* `useLiveNodeOverrides`, and the slab move tool / room-preset stamp
|
|
272
|
+
* publish a translation DELTA to `useLiveTransforms` — either way the
|
|
273
|
+
* scene store commits only on release, so the committed cache and index
|
|
274
|
+
* would elect support against pre-drag footprints (items and walls
|
|
275
|
+
* visibly drop to ground mid-preview). Support queries then read
|
|
276
|
+
* live-effective records and skip the rendered-polygon cache.
|
|
277
|
+
*/
|
|
278
|
+
levelHasLivePreview(levelId) {
|
|
279
|
+
const nodes = useScene.getState().nodes;
|
|
280
|
+
const structuralOnLevel = (id) => {
|
|
281
|
+
const node = nodes[id];
|
|
282
|
+
if (!node || (node.type !== 'slab' && node.type !== 'wall'))
|
|
283
|
+
return false;
|
|
284
|
+
return resolveNodeLevelId(node, nodes) === levelId;
|
|
285
|
+
};
|
|
286
|
+
const overrides = useLiveNodeOverrides.getState().overrides;
|
|
287
|
+
for (const id of overrides.keys()) {
|
|
288
|
+
if (structuralOnLevel(id))
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
const transforms = useLiveTransforms.getState().transforms;
|
|
292
|
+
for (const id of transforms.keys()) {
|
|
293
|
+
if (structuralOnLevel(id))
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The live-effective slab record: field overrides merged, then the
|
|
300
|
+
* `useLiveTransforms` DELTA (slab publishers — move tool, room-preset
|
|
301
|
+
* stamp — store a translation, not an absolute position) applied to the
|
|
302
|
+
* polygon, holes, and elevation. Mapping happens exactly ONCE at each
|
|
303
|
+
* public query's loop entry: `slabSupportsFootprint` /
|
|
304
|
+
* `getRenderedSlabPolygon` take the already-effective record and must
|
|
305
|
+
* never re-map, or the delta would apply twice.
|
|
306
|
+
*/
|
|
307
|
+
effectiveSlabRecord(slab) {
|
|
308
|
+
let effective = getEffectiveNode(slab);
|
|
309
|
+
const live = useLiveTransforms.getState().get(slab.id);
|
|
310
|
+
if (live) {
|
|
311
|
+
const [dx, dy, dz] = live.position;
|
|
312
|
+
if (dx !== 0 || dy !== 0 || dz !== 0) {
|
|
313
|
+
effective = {
|
|
314
|
+
...effective,
|
|
315
|
+
polygon: effective.polygon.map(([x, z]) => [x + dx, z + dz]),
|
|
316
|
+
holes: (effective.holes || []).map((hole) => hole.map(([x, z]) => [x + dx, z + dz])),
|
|
317
|
+
elevation: (effective.elevation ?? 0.05) + dy,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return effective;
|
|
322
|
+
}
|
|
323
|
+
getRenderedSlabPolygon(levelId, slab) {
|
|
324
|
+
const live = this.levelHasLivePreview(levelId);
|
|
325
|
+
if (!live) {
|
|
326
|
+
const cached = this.renderedSlabPolygons.get(slab.id);
|
|
327
|
+
if (cached)
|
|
328
|
+
return cached;
|
|
329
|
+
}
|
|
330
|
+
const siblingSlabs = [];
|
|
331
|
+
for (const other of this.getSlabMap(levelId).values()) {
|
|
332
|
+
if (other.id !== slab.id)
|
|
333
|
+
siblingSlabs.push(live ? this.effectiveSlabRecord(other) : other);
|
|
334
|
+
}
|
|
335
|
+
const walls = this.getLevelWallNodes(levelId);
|
|
336
|
+
const polygon = getRenderableSlabPolygon(slab, {
|
|
337
|
+
walls: live ? walls.map((wall) => getEffectiveNode(wall)) : walls,
|
|
338
|
+
siblingSlabs,
|
|
339
|
+
});
|
|
340
|
+
if (!live)
|
|
341
|
+
this.renderedSlabPolygons.set(slab.id, polygon);
|
|
342
|
+
return polygon;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Support test shared by election, candidate listing, and persisted-host
|
|
346
|
+
* validation: the footprint overlaps the slab's RENDERED polygon (what
|
|
347
|
+
* users see — matching the wall election in `computeWallSlabSupport`),
|
|
348
|
+
* with the center-point hole veto kept against the stored holes (holes
|
|
349
|
+
* are data, never render-offset).
|
|
350
|
+
*/
|
|
351
|
+
slabSupportsFootprint(levelId, slab, position, dimensions, rotation) {
|
|
352
|
+
if (slab.polygon.length < 3)
|
|
353
|
+
return false;
|
|
354
|
+
const rendered = this.getRenderedSlabPolygon(levelId, slab);
|
|
355
|
+
if (!itemOverlapsPolygon(position, dimensions, rotation, rendered, 0.01))
|
|
356
|
+
return false;
|
|
357
|
+
const [cx, , cz] = position;
|
|
358
|
+
for (const hole of slab.holes || []) {
|
|
359
|
+
if (hole.length >= 3 && pointInPolygon(cx, cz, hole))
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
648
364
|
// Called when nodes change
|
|
649
365
|
handleNodeCreated(node, levelId) {
|
|
650
366
|
if (node.type === 'slab') {
|
|
651
367
|
this.getSlabMap(levelId).set(node.id, node);
|
|
368
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
652
369
|
}
|
|
653
370
|
else if (node.type === 'ceiling') {
|
|
654
371
|
this.ceilings.set(node.id, node);
|
|
@@ -656,6 +373,8 @@ export class SpatialGridManager {
|
|
|
656
373
|
else if (node.type === 'wall') {
|
|
657
374
|
const wall = node;
|
|
658
375
|
this.walls.set(wall.id, wall);
|
|
376
|
+
// Rendered slab polygons adopt wall bands — a new wall can extend them.
|
|
377
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
659
378
|
}
|
|
660
379
|
else if (node.type === 'item') {
|
|
661
380
|
const item = node;
|
|
@@ -700,6 +419,7 @@ export class SpatialGridManager {
|
|
|
700
419
|
handleNodeUpdated(node, levelId) {
|
|
701
420
|
if (node.type === 'slab') {
|
|
702
421
|
this.getSlabMap(levelId).set(node.id, node);
|
|
422
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
703
423
|
}
|
|
704
424
|
else if (node.type === 'ceiling') {
|
|
705
425
|
this.ceilings.set(node.id, node);
|
|
@@ -707,6 +427,7 @@ export class SpatialGridManager {
|
|
|
707
427
|
else if (node.type === 'wall') {
|
|
708
428
|
const wall = node;
|
|
709
429
|
this.walls.set(wall.id, wall);
|
|
430
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
710
431
|
}
|
|
711
432
|
else if (node.type === 'item') {
|
|
712
433
|
const item = node;
|
|
@@ -756,6 +477,9 @@ export class SpatialGridManager {
|
|
|
756
477
|
}
|
|
757
478
|
handleNodeDeleted(nodeId, nodeType, levelId) {
|
|
758
479
|
if (nodeType === 'slab') {
|
|
480
|
+
// Invalidate before removal so the deleted slab's own cache entry
|
|
481
|
+
// (still keyed in the level map here) is dropped with its siblings'.
|
|
482
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
759
483
|
this.getSlabMap(levelId).delete(nodeId);
|
|
760
484
|
}
|
|
761
485
|
else if (nodeType === 'ceiling') {
|
|
@@ -764,6 +488,7 @@ export class SpatialGridManager {
|
|
|
764
488
|
}
|
|
765
489
|
else if (nodeType === 'wall') {
|
|
766
490
|
this.walls.delete(nodeId);
|
|
491
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
767
492
|
// Remove all items attached to this wall from the spatial grid
|
|
768
493
|
const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId);
|
|
769
494
|
return removedItemIds; // Caller can use this to delete the items from scene
|
|
@@ -901,7 +626,8 @@ export class SpatialGridManager {
|
|
|
901
626
|
if (!slabMap)
|
|
902
627
|
return 0;
|
|
903
628
|
let maxElevation = 0;
|
|
904
|
-
for (const
|
|
629
|
+
for (const stored of slabMap.values()) {
|
|
630
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
905
631
|
if (slab.polygon.length >= 3 && pointInPolygon(x, z, slab.polygon)) {
|
|
906
632
|
// Check if point is in any hole
|
|
907
633
|
let inHole = false;
|
|
@@ -924,37 +650,142 @@ export class SpatialGridManager {
|
|
|
924
650
|
}
|
|
925
651
|
/**
|
|
926
652
|
* Get the slab elevation for an item using its full footprint (bounding box).
|
|
927
|
-
*
|
|
928
|
-
*
|
|
653
|
+
* Thin wrapper over {@link getSlabSupportForItem} for callers (and tests)
|
|
654
|
+
* that only need the number.
|
|
929
655
|
*/
|
|
930
|
-
getSlabElevationForItem(levelId, position, dimensions, rotation) {
|
|
656
|
+
getSlabElevationForItem(levelId, position, dimensions, rotation, maxElevation) {
|
|
657
|
+
return this.getSlabSupportForItem(levelId, position, dimensions, rotation, maxElevation)
|
|
658
|
+
.elevation;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Elect the supporting slab for a footprint: the highest-elevation slab
|
|
662
|
+
* whose RENDERED polygon the footprint overlaps (center-point hole veto
|
|
663
|
+
* applies). Returns `{ elevation: 0, slabId: null }` when nothing
|
|
664
|
+
* overlaps.
|
|
665
|
+
*
|
|
666
|
+
* `maxElevation` is the pointer-decided cap: when set, only slabs whose
|
|
667
|
+
* walking surface sits at or below `maxElevation +
|
|
668
|
+
* SUPPORT_ELEVATION_EPSILON` may win — a deck hanging above the surface
|
|
669
|
+
* the cursor ray actually hit never captures the election.
|
|
670
|
+
*/
|
|
671
|
+
getSlabSupportForItem(levelId, position, dimensions, rotation, maxElevation) {
|
|
931
672
|
const slabMap = this.slabsByLevel.get(levelId);
|
|
932
673
|
if (!slabMap)
|
|
933
|
-
return 0;
|
|
934
|
-
let
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
674
|
+
return { elevation: 0, slabId: null };
|
|
675
|
+
let winningElevation = Number.NEGATIVE_INFINITY;
|
|
676
|
+
let winnerId = null;
|
|
677
|
+
for (const stored of slabMap.values()) {
|
|
678
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
679
|
+
const elevation = slab.elevation ?? 0.05;
|
|
680
|
+
if (maxElevation != null && elevation > maxElevation + SUPPORT_ELEVATION_EPSILON)
|
|
681
|
+
continue;
|
|
682
|
+
if (!this.slabSupportsFootprint(levelId, slab, position, dimensions, rotation))
|
|
683
|
+
continue;
|
|
684
|
+
if (elevation > winningElevation) {
|
|
685
|
+
winningElevation = elevation;
|
|
686
|
+
winnerId = slab.id;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return winnerId === null
|
|
690
|
+
? { elevation: 0, slabId: null }
|
|
691
|
+
: { elevation: winningElevation, slabId: winnerId };
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* The walking surface the pointer actually points at: the nearest slab
|
|
695
|
+
* plane the ray crosses INSIDE that slab's rendered polygon (hole veto
|
|
696
|
+
* applies), or the level base (`elevation: 0, slabId: null`) when it
|
|
697
|
+
* crosses none. Ray origin/direction are level-local. Deliberately a
|
|
698
|
+
* point test, not a footprint test — it answers "which surface is under
|
|
699
|
+
* the cursor", which then caps the footprint election so a deck hanging
|
|
700
|
+
* above the aimed-at floor never lifts the placement. `point` is the
|
|
701
|
+
* ray's crossing of that surface's plane — the stable plan point
|
|
702
|
+
* callers should elect/preview at (see {@link PointedSupportSurface}).
|
|
703
|
+
*/
|
|
704
|
+
getPointedSupportSurface(levelId, rayOrigin, rayDirection) {
|
|
705
|
+
const slabMap = this.slabsByLevel.get(levelId);
|
|
706
|
+
const [ox, oy, oz] = rayOrigin;
|
|
707
|
+
const [dx, dy, dz] = rayDirection;
|
|
708
|
+
if (Math.abs(dy) < 1e-9)
|
|
709
|
+
return { elevation: 0, slabId: null, point: null };
|
|
710
|
+
let best = null;
|
|
711
|
+
if (slabMap) {
|
|
712
|
+
for (const stored of slabMap.values()) {
|
|
713
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
714
|
+
if (slab.polygon.length < 3)
|
|
715
|
+
continue;
|
|
716
|
+
const elevation = slab.elevation ?? 0.05;
|
|
717
|
+
const t = (elevation - oy) / dy;
|
|
718
|
+
if (t <= 0)
|
|
719
|
+
continue;
|
|
720
|
+
if (best && t >= best.t)
|
|
721
|
+
continue;
|
|
722
|
+
const x = ox + dx * t;
|
|
723
|
+
const z = oz + dz * t;
|
|
724
|
+
const rendered = this.getRenderedSlabPolygon(levelId, slab);
|
|
725
|
+
if (rendered.length < 3 || !pointInPolygon(x, z, rendered))
|
|
726
|
+
continue;
|
|
940
727
|
let inHole = false;
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
for (const hole of holes) {
|
|
944
|
-
if (hole.length >= 3 && pointInPolygon(cx, cz, hole)) {
|
|
728
|
+
for (const hole of slab.holes || []) {
|
|
729
|
+
if (hole.length >= 3 && pointInPolygon(x, z, hole)) {
|
|
945
730
|
inHole = true;
|
|
946
731
|
break;
|
|
947
732
|
}
|
|
948
733
|
}
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
maxElevation = elevation;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
734
|
+
if (inHole)
|
|
735
|
+
continue;
|
|
736
|
+
best = { t, elevation, slabId: slab.id };
|
|
955
737
|
}
|
|
956
738
|
}
|
|
957
|
-
|
|
739
|
+
if (best) {
|
|
740
|
+
return {
|
|
741
|
+
elevation: best.elevation,
|
|
742
|
+
slabId: best.slabId,
|
|
743
|
+
point: [ox + dx * best.t, oz + dz * best.t],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const tBase = -oy / dy;
|
|
747
|
+
return {
|
|
748
|
+
elevation: 0,
|
|
749
|
+
slabId: null,
|
|
750
|
+
point: tBase > 0 ? [ox + dx * tBase, oz + dz * tBase] : null,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* All slabs supporting a footprint, one entry per overlapping slab
|
|
755
|
+
* (highest elevation first; slab id breaks ties deterministically).
|
|
756
|
+
* Commit-side ambiguity check: persist a `supportSlabId` only when the
|
|
757
|
+
* candidates carry ≥ 2 distinct elevations.
|
|
758
|
+
*/
|
|
759
|
+
getSupportCandidatesForFootprint(levelId, position, dimensions, rotation) {
|
|
760
|
+
const slabMap = this.slabsByLevel.get(levelId);
|
|
761
|
+
if (!slabMap)
|
|
762
|
+
return [];
|
|
763
|
+
const candidates = [];
|
|
764
|
+
for (const stored of slabMap.values()) {
|
|
765
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
766
|
+
if (!this.slabSupportsFootprint(levelId, slab, position, dimensions, rotation))
|
|
767
|
+
continue;
|
|
768
|
+
candidates.push({ slabId: slab.id, elevation: slab.elevation ?? 0.05 });
|
|
769
|
+
}
|
|
770
|
+
candidates.sort((a, b) => b.elevation - a.elevation || (a.slabId < b.slabId ? -1 : a.slabId > b.slabId ? 1 : 0));
|
|
771
|
+
return candidates;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Elevation of a persisted support host for a footprint, or null when
|
|
775
|
+
* the slab no longer exists on the level or no longer overlaps the
|
|
776
|
+
* footprint (same overlap test as election). Deliberately read-only: a
|
|
777
|
+
* host reshaped away is NOT cleared — callers fall back to election and
|
|
778
|
+
* the stale reference resumes hosting if the slab's polygon returns.
|
|
779
|
+
* Slab deletion is the only writer (`deleteNodesAction` strips it).
|
|
780
|
+
*/
|
|
781
|
+
getHostSlabElevationForFootprint(levelId, slabId, position, dimensions, rotation) {
|
|
782
|
+
const stored = this.slabsByLevel.get(levelId)?.get(slabId);
|
|
783
|
+
if (!stored)
|
|
784
|
+
return null;
|
|
785
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
786
|
+
if (!this.slabSupportsFootprint(levelId, slab, position, dimensions, rotation))
|
|
787
|
+
return null;
|
|
788
|
+
return slab.elevation ?? 0.05;
|
|
958
789
|
}
|
|
959
790
|
/**
|
|
960
791
|
* Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
|
|
@@ -963,19 +794,43 @@ export class SpatialGridManager {
|
|
|
963
794
|
* Accepts an optional `curveOffset` so curved walls evaluate overlap
|
|
964
795
|
* against their actual centerline samples, not just the chord.
|
|
965
796
|
*/
|
|
966
|
-
getSlabElevationForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS) {
|
|
967
|
-
return this.getSlabSupportForWall(levelId, start, end, curveOffset, thickness)
|
|
968
|
-
|
|
969
|
-
|
|
797
|
+
getSlabElevationForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS, preferredSlabId) {
|
|
798
|
+
return this.getSlabSupportForWall(levelId, start, end, curveOffset, thickness, preferredSlabId)
|
|
799
|
+
.elevation;
|
|
800
|
+
}
|
|
801
|
+
getSlabSupportForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS, preferredSlabId, maxElevation, supportOffset = 0) {
|
|
802
|
+
if (preferredSlabId === GROUND_SUPPORT_ID) {
|
|
803
|
+
const nodes = useScene.getState().nodes;
|
|
804
|
+
const elevation = (terrainSupportLift(nodes, levelId, start[0], start[1]) ?? 0) + supportOffset;
|
|
805
|
+
return {
|
|
806
|
+
elevation,
|
|
807
|
+
electedSlabId: null,
|
|
808
|
+
baseElevation: elevation,
|
|
809
|
+
baseSegments: [{ start: 0, end: 1, elevation }],
|
|
810
|
+
};
|
|
811
|
+
}
|
|
970
812
|
const slabMap = this.slabsByLevel.get(levelId);
|
|
971
813
|
if (!slabMap) {
|
|
814
|
+
const elevation = supportOffset;
|
|
972
815
|
return {
|
|
973
|
-
elevation
|
|
974
|
-
|
|
975
|
-
|
|
816
|
+
elevation,
|
|
817
|
+
electedSlabId: null,
|
|
818
|
+
baseElevation: elevation,
|
|
819
|
+
baseSegments: [{ start: 0, end: 1, elevation }],
|
|
976
820
|
};
|
|
977
821
|
}
|
|
978
|
-
|
|
822
|
+
const support = computeWallSlabSupport({ start, end, curveOffset, thickness }, [...slabMap.values()].map((slab) => this.effectiveSlabRecord(slab)), this.getLevelWallNodes(levelId).map((wall) => getEffectiveNode(wall)), preferredSlabId, maxElevation);
|
|
823
|
+
if (supportOffset === 0)
|
|
824
|
+
return support;
|
|
825
|
+
return {
|
|
826
|
+
...support,
|
|
827
|
+
elevation: support.elevation + supportOffset,
|
|
828
|
+
baseElevation: support.baseElevation + supportOffset,
|
|
829
|
+
baseSegments: support.baseSegments.map((segment) => ({
|
|
830
|
+
...segment,
|
|
831
|
+
elevation: segment.elevation + supportOffset,
|
|
832
|
+
})),
|
|
833
|
+
};
|
|
979
834
|
}
|
|
980
835
|
/**
|
|
981
836
|
* Walls on a level, resolved fresh from the scene store (the manager's
|
|
@@ -1070,6 +925,7 @@ export class SpatialGridManager {
|
|
|
1070
925
|
return { valid: conflicts.length === 0, conflictIds: conflicts };
|
|
1071
926
|
}
|
|
1072
927
|
clearLevel(levelId) {
|
|
928
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
1073
929
|
this.floorGrids.delete(levelId);
|
|
1074
930
|
this.wallGrids.delete(levelId);
|
|
1075
931
|
this.slabsByLevel.delete(levelId);
|
|
@@ -1082,7 +938,26 @@ export class SpatialGridManager {
|
|
|
1082
938
|
this.ceilingGrids.clear();
|
|
1083
939
|
this.ceilings.clear();
|
|
1084
940
|
this.itemCeilingMap.clear();
|
|
941
|
+
this.renderedSlabPolygons.clear();
|
|
1085
942
|
}
|
|
1086
943
|
}
|
|
1087
944
|
// Singleton instance
|
|
1088
945
|
export const spatialGridManager = new SpatialGridManager();
|
|
946
|
+
/** Level-local Y where the rendered wall mesh begins. */
|
|
947
|
+
export function getWallBaseElevationForNodes(wall, nodes) {
|
|
948
|
+
const levelId = resolveNodeLevelId(wall, nodes);
|
|
949
|
+
return spatialGridManager.getSlabSupportForWall(levelId, wall.start, wall.end, wall.curveOffset ?? 0, wall.thickness, wall.supportSlabId ?? null, undefined, wall.supportOffset).elevation;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Effective (extruded) height of a wall resolved from a nodes record:
|
|
953
|
+
* {@link resolveWallEffectiveHeight} over the covering-clamped plane top
|
|
954
|
+
* (`getWallPlaneTop`) and the singleton manager's slab election — so the
|
|
955
|
+
* value always agrees with the rendered wall. One shared resolver for the
|
|
956
|
+
* editor overlays (measurement label, action menu, side handles) that used
|
|
957
|
+
* to copy this derivation locally.
|
|
958
|
+
*/
|
|
959
|
+
export function getWallEffectiveHeightForNodes(wall, nodes) {
|
|
960
|
+
const levelId = resolveNodeLevelId(wall, nodes);
|
|
961
|
+
const baseElevation = getWallBaseElevationForNodes(wall, nodes);
|
|
962
|
+
return resolveWallEffectiveHeight(wall, getWallPlaneTop(wall, levelId, nodes), baseElevation);
|
|
963
|
+
}
|