@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
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { getRenderableSlabPolygon } from '../../lib/slab-polygon';
|
|
2
|
+
import { getWallCurveFrameAt, isCurvedWall } from '../wall/wall-curve';
|
|
3
|
+
import { DEFAULT_WALL_THICKNESS } from '../wall/wall-footprint';
|
|
4
|
+
import { MIN_WALL_HEIGHT } from '../wall/wall-top';
|
|
5
|
+
/**
|
|
6
|
+
* Clamp-never-ask upper bound for a slab's elevation. A plane-bound wall
|
|
7
|
+
* (no stored `height`) keeps its top at the storey plane, so a slab that
|
|
8
|
+
* rises past `storeyHeight - MIN_WALL_HEIGHT` while electing as that
|
|
9
|
+
* wall's base would squeeze the wall body below its minimum (and at the
|
|
10
|
+
* plane, to nothing). Walls with explicit heights don't constrain — their
|
|
11
|
+
* top rides the elected base, not the plane. Negative proposals (the
|
|
12
|
+
* drag-through-zero path that commits the `recessed` intent) pass
|
|
13
|
+
* through untouched: this is a purely numeric upper bound.
|
|
14
|
+
*
|
|
15
|
+
* The election runs against `levelSlabs` with `proposedElevation`
|
|
16
|
+
* substituted into `slab`, so a slab that would only WIN the election at
|
|
17
|
+
* the proposed elevation still clamps, and a slab out-elected by a
|
|
18
|
+
* sibling doesn't. Pure.
|
|
19
|
+
*/
|
|
20
|
+
export function clampSlabElevationForWalls(proposedElevation, slab, levelWalls, levelSlabs, storeyHeight) {
|
|
21
|
+
const bound = storeyHeight - MIN_WALL_HEIGHT;
|
|
22
|
+
if (proposedElevation <= bound)
|
|
23
|
+
return { elevation: proposedElevation, clamped: false };
|
|
24
|
+
if (slab.polygon.length < 3)
|
|
25
|
+
return { elevation: proposedElevation, clamped: false };
|
|
26
|
+
const substituted = levelSlabs.some((candidate) => candidate.id === slab.id)
|
|
27
|
+
? levelSlabs.map((candidate) => candidate.id === slab.id ? { ...candidate, elevation: proposedElevation } : candidate)
|
|
28
|
+
: [...levelSlabs, { ...slab, elevation: proposedElevation }];
|
|
29
|
+
for (const wall of levelWalls) {
|
|
30
|
+
if (wall.height != null)
|
|
31
|
+
continue;
|
|
32
|
+
const wallLike = {
|
|
33
|
+
start: wall.start,
|
|
34
|
+
end: wall.end,
|
|
35
|
+
curveOffset: wall.curveOffset,
|
|
36
|
+
thickness: wall.thickness,
|
|
37
|
+
};
|
|
38
|
+
// Cheap pre-filter: a wall that never reaches the slab's footprint
|
|
39
|
+
// can't elect it, whatever the election says about sibling slabs.
|
|
40
|
+
if (!wallOverlapsPolygon(wallLike, slab.polygon))
|
|
41
|
+
continue;
|
|
42
|
+
const support = computeWallSlabSupport(wallLike, substituted, levelWalls);
|
|
43
|
+
if (Math.abs(support.elevation - proposedElevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON) {
|
|
44
|
+
return { elevation: bound, clamped: true };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { elevation: proposedElevation, clamped: false };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Static upper bound for a slab-elevation drag: probe the election with
|
|
51
|
+
* the slab raised above every sibling and the storey plane. If any
|
|
52
|
+
* plane-bound wall would elect it there, the drag may not pass
|
|
53
|
+
* `storeyHeight - MIN_WALL_HEIGHT`; otherwise it is unbounded above.
|
|
54
|
+
*/
|
|
55
|
+
export function getSlabElevationUpperBound(slab, levelWalls, levelSlabs, storeyHeight) {
|
|
56
|
+
const probe = Math.max(storeyHeight, ...levelSlabs.map((candidate) => candidate.elevation ?? 0.05)) + 1;
|
|
57
|
+
return clampSlabElevationForWalls(probe, slab, levelWalls, levelSlabs, storeyHeight).clamped
|
|
58
|
+
? storeyHeight - MIN_WALL_HEIGHT
|
|
59
|
+
: Number.POSITIVE_INFINITY;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Point-in-polygon test using ray casting algorithm.
|
|
63
|
+
*/
|
|
64
|
+
export function pointInPolygon(px, pz, polygon) {
|
|
65
|
+
let inside = false;
|
|
66
|
+
const n = polygon.length;
|
|
67
|
+
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
68
|
+
const xi = polygon[i][0], zi = polygon[i][1];
|
|
69
|
+
const xj = polygon[j][0], zj = polygon[j][1];
|
|
70
|
+
if (zi > pz !== zj > pz && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
|
|
71
|
+
inside = !inside;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return inside;
|
|
75
|
+
}
|
|
76
|
+
function pointSegmentDistance(px, pz, ax, az, bx, bz) {
|
|
77
|
+
const dx = bx - ax;
|
|
78
|
+
const dz = bz - az;
|
|
79
|
+
const lengthSquared = dx * dx + dz * dz;
|
|
80
|
+
if (lengthSquared < 1e-18)
|
|
81
|
+
return Math.hypot(px - ax, pz - az);
|
|
82
|
+
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (pz - az) * dz) / lengthSquared));
|
|
83
|
+
return Math.hypot(px - (ax + dx * t), pz - (az + dz * t));
|
|
84
|
+
}
|
|
85
|
+
// Ray-cast pointInPolygon is unreliable for points exactly on the polygon
|
|
86
|
+
// boundary: the answer flips depending on which side of the polygon the edge
|
|
87
|
+
// is on. Interval classification below therefore treats "within this distance
|
|
88
|
+
// of the boundary" as inside explicitly, so walls sitting exactly on a slab
|
|
89
|
+
// edge (the common case — auto-slab polygons derive from wall centerlines)
|
|
90
|
+
// classify identically on every side of the slab.
|
|
91
|
+
const ON_BOUNDARY_EPSILON = 1e-4;
|
|
92
|
+
export function pointOnPolygonBoundary(px, pz, polygon) {
|
|
93
|
+
const n = polygon.length;
|
|
94
|
+
for (let i = 0; i < n; i++) {
|
|
95
|
+
const [ax, az] = polygon[i];
|
|
96
|
+
const [bx, bz] = polygon[(i + 1) % n];
|
|
97
|
+
if (pointSegmentDistance(px, pz, ax, az, bx, bz) <= ON_BOUNDARY_EPSILON)
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
function mergeIntervals(intervals) {
|
|
103
|
+
if (intervals.length <= 1)
|
|
104
|
+
return intervals;
|
|
105
|
+
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
106
|
+
const merged = [[sorted[0][0], sorted[0][1]]];
|
|
107
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
108
|
+
const [intervalStart, intervalEnd] = sorted[i];
|
|
109
|
+
const last = merged[merged.length - 1];
|
|
110
|
+
if (intervalStart <= last[1] + 1e-9) {
|
|
111
|
+
last[1] = Math.max(last[1], intervalEnd);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
merged.push([intervalStart, intervalEnd]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return merged;
|
|
118
|
+
}
|
|
119
|
+
/** Total length of a merged (sorted, disjoint) interval list. */
|
|
120
|
+
function intervalsLength(intervals) {
|
|
121
|
+
let total = 0;
|
|
122
|
+
for (const [intervalStart, intervalEnd] of intervals)
|
|
123
|
+
total += intervalEnd - intervalStart;
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
/** `base` minus `cut`. Both inputs may be unsorted; the result is merged. */
|
|
127
|
+
function subtractIntervals(base, cut) {
|
|
128
|
+
if (base.length === 0 || cut.length === 0)
|
|
129
|
+
return mergeIntervals(base);
|
|
130
|
+
const cuts = mergeIntervals(cut);
|
|
131
|
+
const result = [];
|
|
132
|
+
for (const [baseStart, baseEnd] of mergeIntervals(base)) {
|
|
133
|
+
let cursor = baseStart;
|
|
134
|
+
for (const [cutStart, cutEnd] of cuts) {
|
|
135
|
+
if (cutEnd <= cursor)
|
|
136
|
+
continue;
|
|
137
|
+
if (cutStart >= baseEnd)
|
|
138
|
+
break;
|
|
139
|
+
if (cutStart > cursor)
|
|
140
|
+
result.push([cursor, cutStart]);
|
|
141
|
+
cursor = cutEnd;
|
|
142
|
+
if (cursor >= baseEnd)
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (cursor < baseEnd)
|
|
146
|
+
result.push([cursor, baseEnd]);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Sub-intervals of segment (ax,az)→(bx,bz) that lie inside the polygon (and,
|
|
152
|
+
* when `includeBoundary`, on its boundary), as [t0, t1] fractions of the
|
|
153
|
+
* segment. The segment is split at every crossing with a polygon edge and
|
|
154
|
+
* each sub-interval is classified by its midpoint, so no test point ever
|
|
155
|
+
* sits on a crossing.
|
|
156
|
+
*/
|
|
157
|
+
function segmentInsideIntervals(ax, az, bx, bz, polygon, includeBoundary) {
|
|
158
|
+
const dx = bx - ax;
|
|
159
|
+
const dz = bz - az;
|
|
160
|
+
const length = Math.hypot(dx, dz);
|
|
161
|
+
if (length < 1e-9)
|
|
162
|
+
return [];
|
|
163
|
+
const ts = [0, 1];
|
|
164
|
+
const n = polygon.length;
|
|
165
|
+
for (let i = 0; i < n; i++) {
|
|
166
|
+
const [px, pz] = polygon[i];
|
|
167
|
+
const [qx, qz] = polygon[(i + 1) % n];
|
|
168
|
+
const ex = qx - px;
|
|
169
|
+
const ez = qz - pz;
|
|
170
|
+
const denom = dx * ez - dz * ex;
|
|
171
|
+
if (Math.abs(denom) < 1e-12)
|
|
172
|
+
continue; // parallel/collinear — nothing to split at
|
|
173
|
+
const t = ((px - ax) * ez - (pz - az) * ex) / denom;
|
|
174
|
+
const s = ((px - ax) * dz - (pz - az) * dx) / denom;
|
|
175
|
+
if (t > 0 && t < 1 && s >= -1e-9 && s <= 1 + 1e-9)
|
|
176
|
+
ts.push(t);
|
|
177
|
+
}
|
|
178
|
+
ts.sort((a, b) => a - b);
|
|
179
|
+
const inside = [];
|
|
180
|
+
for (let i = 1; i < ts.length; i++) {
|
|
181
|
+
const t0 = ts[i - 1];
|
|
182
|
+
const t1 = ts[i];
|
|
183
|
+
if (t1 - t0 < 1e-9)
|
|
184
|
+
continue;
|
|
185
|
+
const tm = (t0 + t1) / 2;
|
|
186
|
+
const mx = ax + dx * tm;
|
|
187
|
+
const mz = az + dz * tm;
|
|
188
|
+
const midpointInside = pointOnPolygonBoundary(mx, mz, polygon)
|
|
189
|
+
? includeBoundary
|
|
190
|
+
: pointInPolygon(mx, mz, polygon);
|
|
191
|
+
if (midpointInside)
|
|
192
|
+
inside.push([t0, t1]);
|
|
193
|
+
}
|
|
194
|
+
return inside;
|
|
195
|
+
}
|
|
196
|
+
function polylineLength(points) {
|
|
197
|
+
let total = 0;
|
|
198
|
+
for (let i = 1; i < points.length; i++) {
|
|
199
|
+
total += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
|
|
200
|
+
}
|
|
201
|
+
return total;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Inside sub-intervals of a polyline against a polygon, in cumulative
|
|
205
|
+
* arc-length units from the polyline start (merged, disjoint). Boundary
|
|
206
|
+
* contact counts as inside for slab support (walls sit exactly on slab
|
|
207
|
+
* edges — see ON_BOUNDARY_EPSILON above); hole callers pass
|
|
208
|
+
* `includeBoundary: false` so a wall running along a stairwell hole's
|
|
209
|
+
* rim keeps the rim's support.
|
|
210
|
+
*/
|
|
211
|
+
function polylineInsideIntervals(points, polygon, includeBoundary = true) {
|
|
212
|
+
const intervals = [];
|
|
213
|
+
let offset = 0;
|
|
214
|
+
for (let i = 1; i < points.length; i++) {
|
|
215
|
+
const a = points[i - 1];
|
|
216
|
+
const b = points[i];
|
|
217
|
+
const segmentLength = Math.hypot(b.x - a.x, b.y - a.y);
|
|
218
|
+
if (segmentLength < 1e-9)
|
|
219
|
+
continue;
|
|
220
|
+
for (const [t0, t1] of segmentInsideIntervals(a.x, a.y, b.x, b.y, polygon, includeBoundary)) {
|
|
221
|
+
intervals.push([offset + t0 * segmentLength, offset + t1 * segmentLength]);
|
|
222
|
+
}
|
|
223
|
+
offset += segmentLength;
|
|
224
|
+
}
|
|
225
|
+
return mergeIntervals(intervals);
|
|
226
|
+
}
|
|
227
|
+
// Minimum length of wall that must lie on/inside a slab polygon before the
|
|
228
|
+
// wall counts as overlapping it. Point contact (a perpendicular wall butting
|
|
229
|
+
// into a room's edge) clips to ~zero length and never reaches this, so such
|
|
230
|
+
// walls don't follow the slab's elevation.
|
|
231
|
+
const WALL_SLAB_MIN_OVERLAP = 0.05;
|
|
232
|
+
/**
|
|
233
|
+
* Centerline of the wall plus its two face lines (centerline offset by
|
|
234
|
+
* ±halfThickness). The face lines catch walls whose centerline sits on or
|
|
235
|
+
* just outside the slab boundary but whose body reaches onto the slab —
|
|
236
|
+
* e.g. slab polygons drawn to the room's interior faces.
|
|
237
|
+
*/
|
|
238
|
+
function wallTestPolylines(start, end, curveOffset, halfThickness) {
|
|
239
|
+
const wallLike = { start, end, curveOffset };
|
|
240
|
+
if (curveOffset !== 0 && isCurvedWall(wallLike)) {
|
|
241
|
+
const count = 16;
|
|
242
|
+
const center = [];
|
|
243
|
+
const left = [];
|
|
244
|
+
const right = [];
|
|
245
|
+
for (let i = 0; i <= count; i++) {
|
|
246
|
+
const frame = getWallCurveFrameAt(wallLike, i / count);
|
|
247
|
+
center.push(frame.point);
|
|
248
|
+
left.push({
|
|
249
|
+
x: frame.point.x + frame.normal.x * halfThickness,
|
|
250
|
+
y: frame.point.y + frame.normal.y * halfThickness,
|
|
251
|
+
});
|
|
252
|
+
right.push({
|
|
253
|
+
x: frame.point.x - frame.normal.x * halfThickness,
|
|
254
|
+
y: frame.point.y - frame.normal.y * halfThickness,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return halfThickness > 0 ? [center, left, right] : [center];
|
|
258
|
+
}
|
|
259
|
+
const center = [
|
|
260
|
+
{ x: start[0], y: start[1] },
|
|
261
|
+
{ x: end[0], y: end[1] },
|
|
262
|
+
];
|
|
263
|
+
const dx = end[0] - start[0];
|
|
264
|
+
const dz = end[1] - start[1];
|
|
265
|
+
const len = Math.hypot(dx, dz);
|
|
266
|
+
if (len < 1e-10 || halfThickness <= 0)
|
|
267
|
+
return [center];
|
|
268
|
+
const nx = (-dz / len) * halfThickness;
|
|
269
|
+
const nz = (dx / len) * halfThickness;
|
|
270
|
+
return [
|
|
271
|
+
center,
|
|
272
|
+
[
|
|
273
|
+
{ x: start[0] + nx, y: start[1] + nz },
|
|
274
|
+
{ x: end[0] + nx, y: end[1] + nz },
|
|
275
|
+
],
|
|
276
|
+
[
|
|
277
|
+
{ x: start[0] - nx, y: start[1] - nz },
|
|
278
|
+
{ x: end[0] - nx, y: end[1] - nz },
|
|
279
|
+
],
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Test whether a wall overlaps a slab polygon along a segment of its length.
|
|
284
|
+
*
|
|
285
|
+
* The wall's centerline and both face lines are clipped against the polygon;
|
|
286
|
+
* the wall overlaps when the longest clipped inside-or-on-boundary length
|
|
287
|
+
* exceeds a threshold (5cm, halved for very short walls). Because interval
|
|
288
|
+
* midpoints classify "on the boundary" as inside explicitly (never by
|
|
289
|
+
* ray-cast tie-breaking), a wall sitting exactly on a slab edge resolves
|
|
290
|
+
* identically on every side of the slab.
|
|
291
|
+
*
|
|
292
|
+
* A wall that only touches the polygon at a point — a perpendicular wall
|
|
293
|
+
* butting into a room's edge, or a corner-to-corner touch — clips to ~zero
|
|
294
|
+
* length and does NOT overlap.
|
|
295
|
+
*/
|
|
296
|
+
export function wallOverlapsPolygon(startOrWall, endOrPolygon, polygonArg) {
|
|
297
|
+
// Two call shapes:
|
|
298
|
+
// wallOverlapsPolygon(wallLike, polygon) — preferred; curve-aware
|
|
299
|
+
// wallOverlapsPolygon(start, end, polygon) — legacy chord-only
|
|
300
|
+
let start;
|
|
301
|
+
let end;
|
|
302
|
+
let polygon;
|
|
303
|
+
let curveOffset = 0;
|
|
304
|
+
let thickness = DEFAULT_WALL_THICKNESS;
|
|
305
|
+
if (Array.isArray(startOrWall)) {
|
|
306
|
+
start = startOrWall;
|
|
307
|
+
end = endOrPolygon;
|
|
308
|
+
polygon = polygonArg;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
start = startOrWall.start;
|
|
312
|
+
end = startOrWall.end;
|
|
313
|
+
curveOffset = startOrWall.curveOffset ?? 0;
|
|
314
|
+
thickness = startOrWall.thickness ?? DEFAULT_WALL_THICKNESS;
|
|
315
|
+
polygon = endOrPolygon;
|
|
316
|
+
}
|
|
317
|
+
return wallOverlapsSlabFootprint({ start, end, curveOffset, thickness }, polygon);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* {@link wallOverlapsPolygon} with the slab's stored holes subtracted from
|
|
321
|
+
* the covered length: a wall whose band only reaches the polygon inside a
|
|
322
|
+
* hole does not overlap. Hole boundaries keep coverage (rim convention —
|
|
323
|
+
* see {@link computeWallSlabSupport}). Polygon boundary contact counts as
|
|
324
|
+
* covered, so a wall sitting exactly on a slab edge resolves identically
|
|
325
|
+
* on every side of the slab. Pure.
|
|
326
|
+
*/
|
|
327
|
+
export function wallOverlapsSlabFootprint(wallLike, polygon, holes) {
|
|
328
|
+
const { start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS } = wallLike;
|
|
329
|
+
const halfThickness = Math.max(thickness / 2, 0);
|
|
330
|
+
const polylines = wallTestPolylines(start, end, curveOffset, halfThickness);
|
|
331
|
+
const centerLength = polylineLength(polylines[0]);
|
|
332
|
+
if (centerLength < 1e-9)
|
|
333
|
+
return false;
|
|
334
|
+
let overlap = 0;
|
|
335
|
+
for (const line of polylines) {
|
|
336
|
+
let intervals = polylineInsideIntervals(line, polygon);
|
|
337
|
+
for (const hole of holes ?? []) {
|
|
338
|
+
if (intervals.length === 0)
|
|
339
|
+
break;
|
|
340
|
+
if (hole.length < 3)
|
|
341
|
+
continue;
|
|
342
|
+
intervals = subtractIntervals(intervals, polylineInsideIntervals(line, hole, false));
|
|
343
|
+
}
|
|
344
|
+
overlap = Math.max(overlap, intervalsLength(intervals));
|
|
345
|
+
}
|
|
346
|
+
const threshold = Math.max(1e-3, Math.min(WALL_SLAB_MIN_OVERLAP, centerLength * 0.5));
|
|
347
|
+
return overlap >= threshold;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Tolerance for the pointer-decided support cap: a slab still counts as
|
|
351
|
+
* "the surface you're pointing at (or below)" when its walking surface is
|
|
352
|
+
* within this many meters ABOVE the pointed elevation. Absorbs elevation
|
|
353
|
+
* noise between the ray hit and slab tops without letting a deck hanging
|
|
354
|
+
* clearly above the hit point capture the election. Defined here (rather
|
|
355
|
+
* than in the spatial-grid manager, which re-exports it) so the wall
|
|
356
|
+
* election below can honour the same cap without an import cycle.
|
|
357
|
+
*/
|
|
358
|
+
export const SUPPORT_ELEVATION_EPSILON = 0.05;
|
|
359
|
+
// A slab elevation must support at least this fraction of the wall's
|
|
360
|
+
// length before it can dictate the wall's base. Below majority, a raised
|
|
361
|
+
// slab reaching one endpoint would hoist the whole wall off the floor
|
|
362
|
+
// that actually carries it.
|
|
363
|
+
const WALL_SLAB_SUPPORT_MAJORITY = 0.5;
|
|
364
|
+
// Slabs whose elevations differ by less than this pool their support:
|
|
365
|
+
// a wall shared between two rooms' slabs is covered roughly half by
|
|
366
|
+
// each, and must still follow their common elevation.
|
|
367
|
+
const WALL_SLAB_ELEVATION_POOL_EPSILON = 1e-4;
|
|
368
|
+
/**
|
|
369
|
+
* `preferredSlabId` is a persisted support host (`wall.supportSlabId`):
|
|
370
|
+
* while that slab is still in the candidate set (still overlaps the wall
|
|
371
|
+
* band with enough covered length), the elected `elevation` is pinned to
|
|
372
|
+
* it instead of the majority/best-coverage election. `baseSegments` /
|
|
373
|
+
* `baseElevation` (fill-down) still derive from ALL supporting slabs
|
|
374
|
+
* unchanged. A preferred slab that no longer qualifies is silently
|
|
375
|
+
* ignored — deliberately never cleared here, so the host resumes if the
|
|
376
|
+
* slab's polygon returns (only slab deletion strips the stored field).
|
|
377
|
+
*
|
|
378
|
+
* `maxElevation` is the pointer-decided support cap (level-local Y, same
|
|
379
|
+
* semantics as the item election): when set, elevation groups whose
|
|
380
|
+
* walking surface sits above `maxElevation + SUPPORT_ELEVATION_EPSILON`
|
|
381
|
+
* are excluded from the majority/best election — a deck hanging above the
|
|
382
|
+
* surface the cursor ray actually hit never captures the elected base.
|
|
383
|
+
* `baseSegments` / `baseElevation` stay uncapped (geometry fill-down), and
|
|
384
|
+
* an explicit `preferredSlabId` still wins over the cap.
|
|
385
|
+
*/
|
|
386
|
+
export function computeWallSlabSupport(wallLike, slabs, levelWalls, preferredSlabId, maxElevation) {
|
|
387
|
+
const { start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS } = wallLike;
|
|
388
|
+
const halfThickness = Math.max(thickness / 2, 0);
|
|
389
|
+
const polylines = wallTestPolylines(start, end, curveOffset, halfThickness);
|
|
390
|
+
const polylineLengths = polylines.map(polylineLength);
|
|
391
|
+
const wallLength = polylineLengths[0];
|
|
392
|
+
if (wallLength < 1e-9) {
|
|
393
|
+
return { elevation: 0, electedSlabId: null, baseElevation: 0, baseSegments: [] };
|
|
394
|
+
}
|
|
395
|
+
const minSupport = Math.max(1e-3, Math.min(WALL_SLAB_MIN_OVERLAP, wallLength * 0.5));
|
|
396
|
+
const groups = [];
|
|
397
|
+
let preferredElevation = null;
|
|
398
|
+
let preferredElectedSlabId = null;
|
|
399
|
+
for (const slab of slabs) {
|
|
400
|
+
if (slab.polygon.length < 3)
|
|
401
|
+
continue;
|
|
402
|
+
const renderedPolygon = getRenderableSlabPolygon(slab, {
|
|
403
|
+
walls: levelWalls,
|
|
404
|
+
siblingSlabs: slabs.filter((other) => other.id !== slab.id),
|
|
405
|
+
});
|
|
406
|
+
let supported = 0;
|
|
407
|
+
const perPolyline = polylines.map((line) => {
|
|
408
|
+
let intervals = polylineInsideIntervals(line, renderedPolygon);
|
|
409
|
+
for (const hole of slab.holes || []) {
|
|
410
|
+
if (intervals.length === 0)
|
|
411
|
+
break;
|
|
412
|
+
if (hole.length < 3)
|
|
413
|
+
continue;
|
|
414
|
+
intervals = subtractIntervals(intervals, polylineInsideIntervals(line, hole, false));
|
|
415
|
+
}
|
|
416
|
+
supported = Math.max(supported, intervalsLength(intervals));
|
|
417
|
+
return intervals;
|
|
418
|
+
});
|
|
419
|
+
if (supported < minSupport)
|
|
420
|
+
continue;
|
|
421
|
+
const elevation = slab.elevation ?? 0.05;
|
|
422
|
+
if (preferredSlabId != null && slab.id === preferredSlabId) {
|
|
423
|
+
preferredElevation = elevation;
|
|
424
|
+
preferredElectedSlabId = slab.id;
|
|
425
|
+
}
|
|
426
|
+
let group = groups.find((candidate) => Math.abs(candidate.elevation - elevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON);
|
|
427
|
+
if (!group) {
|
|
428
|
+
group = { elevation, slabIds: [], perPolyline: polylines.map(() => []) };
|
|
429
|
+
groups.push(group);
|
|
430
|
+
}
|
|
431
|
+
group.slabIds.push(slab.id);
|
|
432
|
+
for (let i = 0; i < perPolyline.length; i++) {
|
|
433
|
+
group.perPolyline[i].push(...perPolyline[i]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const evaluatedGroups = groups.map((group) => ({
|
|
437
|
+
...group,
|
|
438
|
+
mergedPerPolyline: group.perPolyline.map(mergeIntervals),
|
|
439
|
+
}));
|
|
440
|
+
const normalizedIntervals = (group, polylineIndex) => {
|
|
441
|
+
const lineLength = polylineLengths[polylineIndex];
|
|
442
|
+
if (lineLength < 1e-9)
|
|
443
|
+
return [];
|
|
444
|
+
return group.mergedPerPolyline[polylineIndex].map(([intervalStart, intervalEnd]) => [intervalStart / lineLength, intervalEnd / lineLength]);
|
|
445
|
+
};
|
|
446
|
+
const normalizedByGroup = evaluatedGroups.map((group) => ({
|
|
447
|
+
elevation: group.elevation,
|
|
448
|
+
perPolyline: group.mergedPerPolyline.map((_, index) => normalizedIntervals(group, index)),
|
|
449
|
+
}));
|
|
450
|
+
const breakpoints = [0, 1];
|
|
451
|
+
for (const group of normalizedByGroup) {
|
|
452
|
+
for (const intervals of group.perPolyline) {
|
|
453
|
+
for (const [intervalStart, intervalEnd] of intervals) {
|
|
454
|
+
breakpoints.push(intervalStart, intervalEnd);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
breakpoints.sort((left, right) => left - right);
|
|
459
|
+
const uniqueBreakpoints = breakpoints.filter((value, index) => index === 0 || value - breakpoints[index - 1] > 1e-7);
|
|
460
|
+
const highestAt = (groupList, polylineIndex, t) => {
|
|
461
|
+
let highest = Number.NEGATIVE_INFINITY;
|
|
462
|
+
for (const group of groupList) {
|
|
463
|
+
if (group.perPolyline[polylineIndex]?.some(([intervalStart, intervalEnd]) => t >= intervalStart - 1e-7 && t <= intervalEnd + 1e-7)) {
|
|
464
|
+
highest = Math.max(highest, group.elevation);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return highest;
|
|
468
|
+
};
|
|
469
|
+
// The pointer cap filters the ELECTION's carrying profile, not the base
|
|
470
|
+
// profile: with a deck capped away, the floor that also carries the wall
|
|
471
|
+
// must still win (geometry fill-down stays uncapped).
|
|
472
|
+
const electableNormalizedGroups = maxElevation == null
|
|
473
|
+
? normalizedByGroup
|
|
474
|
+
: normalizedByGroup.filter((group) => group.elevation <= maxElevation + SUPPORT_ELEVATION_EPSILON);
|
|
475
|
+
const baseSegments = [];
|
|
476
|
+
const carryCandidates = [];
|
|
477
|
+
const accumulateCarry = (elevation, length) => {
|
|
478
|
+
let candidate = carryCandidates.find((existing) => Math.abs(existing.elevation - elevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON);
|
|
479
|
+
if (!candidate) {
|
|
480
|
+
candidate = { elevation, length: 0 };
|
|
481
|
+
carryCandidates.push(candidate);
|
|
482
|
+
}
|
|
483
|
+
candidate.length += length;
|
|
484
|
+
};
|
|
485
|
+
for (let index = 1; index < uniqueBreakpoints.length; index++) {
|
|
486
|
+
const start = uniqueBreakpoints[index - 1];
|
|
487
|
+
const end = uniqueBreakpoints[index];
|
|
488
|
+
if (end - start < 1e-7)
|
|
489
|
+
continue;
|
|
490
|
+
const midpoint = (start + end) / 2;
|
|
491
|
+
const centerElevation = highestAt(normalizedByGroup, 0, midpoint);
|
|
492
|
+
const leftElevation = polylines.length >= 3 ? highestAt(normalizedByGroup, 1, midpoint) : Number.NEGATIVE_INFINITY;
|
|
493
|
+
const rightElevation = polylines.length >= 3 ? highestAt(normalizedByGroup, 2, midpoint) : Number.NEGATIVE_INFINITY;
|
|
494
|
+
const faceElevations = [leftElevation, rightElevation].filter(Number.isFinite);
|
|
495
|
+
const segmentElevation = faceElevations.length > 0 ? Math.min(...faceElevations) : Math.max(centerElevation, 0);
|
|
496
|
+
if (electableNormalizedGroups === normalizedByGroup) {
|
|
497
|
+
if (faceElevations.length > 0 || Number.isFinite(centerElevation)) {
|
|
498
|
+
accumulateCarry(segmentElevation, end - start);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const electCenter = highestAt(electableNormalizedGroups, 0, midpoint);
|
|
503
|
+
const electLeft = polylines.length >= 3
|
|
504
|
+
? highestAt(electableNormalizedGroups, 1, midpoint)
|
|
505
|
+
: Number.NEGATIVE_INFINITY;
|
|
506
|
+
const electRight = polylines.length >= 3
|
|
507
|
+
? highestAt(electableNormalizedGroups, 2, midpoint)
|
|
508
|
+
: Number.NEGATIVE_INFINITY;
|
|
509
|
+
const electFaces = [electLeft, electRight].filter(Number.isFinite);
|
|
510
|
+
if (electFaces.length > 0 || Number.isFinite(electCenter)) {
|
|
511
|
+
accumulateCarry(electFaces.length > 0 ? Math.min(...electFaces) : Math.max(electCenter, 0), end - start);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const previous = baseSegments[baseSegments.length - 1];
|
|
515
|
+
if (previous &&
|
|
516
|
+
Math.abs(previous.elevation - segmentElevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON) {
|
|
517
|
+
previous.end = end;
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
baseSegments.push({ start, end, elevation: segmentElevation });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
let majorityElevation = Number.NEGATIVE_INFINITY;
|
|
524
|
+
let bestElevation = Number.NEGATIVE_INFINITY;
|
|
525
|
+
let bestCoverage = -1;
|
|
526
|
+
for (const candidate of carryCandidates) {
|
|
527
|
+
if (candidate.length >= WALL_SLAB_SUPPORT_MAJORITY - 1e-6) {
|
|
528
|
+
majorityElevation = Math.max(majorityElevation, candidate.elevation);
|
|
529
|
+
}
|
|
530
|
+
if (candidate.length > bestCoverage + 1e-6 ||
|
|
531
|
+
(Math.abs(candidate.length - bestCoverage) <= 1e-6 && candidate.elevation > bestElevation)) {
|
|
532
|
+
bestCoverage = candidate.length;
|
|
533
|
+
bestElevation = candidate.elevation;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const elevation = preferredElevation !== null
|
|
537
|
+
? preferredElevation
|
|
538
|
+
: majorityElevation !== Number.NEGATIVE_INFINITY
|
|
539
|
+
? majorityElevation
|
|
540
|
+
: bestElevation === Number.NEGATIVE_INFINITY
|
|
541
|
+
? 0
|
|
542
|
+
: bestElevation;
|
|
543
|
+
const electedSlabId = preferredElectedSlabId ??
|
|
544
|
+
evaluatedGroups
|
|
545
|
+
.filter((group) => maxElevation == null || group.elevation <= maxElevation + SUPPORT_ELEVATION_EPSILON)
|
|
546
|
+
.find((group) => Math.abs(group.elevation - elevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON)
|
|
547
|
+
?.slabIds.slice()
|
|
548
|
+
.sort()[0] ??
|
|
549
|
+
null;
|
|
550
|
+
if (baseSegments.length === 0)
|
|
551
|
+
baseSegments.push({ start: 0, end: 1, elevation });
|
|
552
|
+
const baseElevation = Math.min(...baseSegments.map((segment) => segment.elevation));
|
|
553
|
+
return { elevation, electedSlabId, baseElevation, baseSegments };
|
|
554
|
+
}
|
|
555
|
+
export function computeWallSlabElevation(wallLike, slabs, levelWalls) {
|
|
556
|
+
return computeWallSlabSupport(wallLike, slabs, levelWalls).elevation;
|
|
557
|
+
}
|