@pascal-app/core 0.9.2 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +319 -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 +204 -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 +232 -91
- 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 +85 -0
- package/dist/lib/terrain-support.d.ts.map +1 -0
- package/dist/lib/terrain-support.js +218 -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 +83 -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 +150 -0
- package/dist/systems/slab/slab-support.d.ts.map +1 -0
- package/dist/systems/slab/slab-support.js +575 -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,9 +1,14 @@
|
|
|
1
|
+
import { GROUND_SUPPORT_ID } from '../hooks/spatial-grid/support-host-id';
|
|
1
2
|
import { CeilingNode, SlabNode, ZoneNode, } from '../schema';
|
|
3
|
+
import { DEFAULT_LEVEL_HEIGHT } from '../services/level-height';
|
|
4
|
+
import { CEILING_CLAMP_MARGIN, findLevelAboveId, getCeilingClampBound, getLevelElevations, getStoredLevelHeight, } from '../services/storey';
|
|
2
5
|
import { getSceneHistoryPauseDepth, pauseSceneHistory, resumeSceneHistory, } from '../store/history-control';
|
|
6
|
+
import { computeWallSlabSupport } from '../systems/slab/slab-support';
|
|
3
7
|
import { getClampedWallCurveOffset, getWallCurveFrameAt, isCurvedWall, } from '../systems/wall/wall-curve';
|
|
8
|
+
import { resolveWallTop } from '../systems/wall/wall-top';
|
|
4
9
|
import { simplifyClosedPolygon } from './polygon-geometry';
|
|
10
|
+
import { levelBaseElevationAt } from './terrain-support';
|
|
5
11
|
const DEFAULT_AUTO_SLAB_ELEVATION = 0.05;
|
|
6
|
-
const DEFAULT_AUTO_CEILING_HEIGHT = 2.5;
|
|
7
12
|
const CEILING_HEIGHT_EPSILON = 1e-6;
|
|
8
13
|
const ROOM_CURVE_TOLERANCE = 0.04;
|
|
9
14
|
const MAX_CURVE_SUBDIVISION_DEPTH = 6;
|
|
@@ -19,6 +24,9 @@ const WALL_JUNCTION_TOLERANCE = 0.08;
|
|
|
19
24
|
// deleted) and the node is demoted to manual so user data survives.
|
|
20
25
|
const ORPHAN_MERGE_COVERAGE_THRESHOLD = 0.6;
|
|
21
26
|
const COVERAGE_SAMPLE_STEPS = 12;
|
|
27
|
+
// Rewrite deadband for an existing auto surface's elevation/height: below this
|
|
28
|
+
// the derived plane is the same plane and writing it would churn history.
|
|
29
|
+
const ROOM_VERTICAL_PLANE_EPSILON = 1e-3;
|
|
22
30
|
function pointFromTuple(point) {
|
|
23
31
|
return { x: point[0], y: point[1] };
|
|
24
32
|
}
|
|
@@ -194,51 +202,84 @@ function wallBoundsRoom(wall, roomPolygon) {
|
|
|
194
202
|
const matchingPoints = candidates.filter((point) => pointDistanceToPolygonBoundary(point, roomPolygon) <= WALL_ROOM_BOUNDARY_TOLERANCE);
|
|
195
203
|
return matchingPoints.length >= 2;
|
|
196
204
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return true;
|
|
205
|
+
/**
|
|
206
|
+
* The clamp bound for a ceiling polygon under this planning context —
|
|
207
|
+
* the context's cross-level resolver when provided, else the plane-only
|
|
208
|
+
* `storeyHeight - CEILING_CLAMP_MARGIN` degradation.
|
|
209
|
+
*/
|
|
210
|
+
function resolveCeilingClampBound(polygon, context) {
|
|
211
|
+
if (context.ceilingClampBound)
|
|
212
|
+
return context.ceilingClampBound(polygon);
|
|
213
|
+
return (context.storeyHeight ?? DEFAULT_LEVEL_HEIGHT) - CEILING_CLAMP_MARGIN;
|
|
209
214
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
/**
|
|
216
|
+
* The base a boundary wall actually stands on, in level-local metres.
|
|
217
|
+
*
|
|
218
|
+
* Resolved the same way the wall renderer resolves it — the level base under
|
|
219
|
+
* the wall's own start point (sculpted ground or the flat plane), the slab
|
|
220
|
+
* election on top of that, plus the wall's stored `supportOffset`. Reading the
|
|
221
|
+
* ground for `GROUND_SUPPORT_ID` walls only is what left stamped room presets
|
|
222
|
+
* flat: `resolveWallSupportSlabPatch` writes no host at all for a wall on bare
|
|
223
|
+
* terrain, so the sentinel is a hint about pointer intent, never a precondition
|
|
224
|
+
* for standing on the ground.
|
|
225
|
+
*/
|
|
226
|
+
function boundaryWallBase(wall, walls, supportSlabs, nodes, levelId) {
|
|
227
|
+
const levelBase = levelBaseElevationAt(nodes, levelId, wall.start[0], wall.start[1]);
|
|
228
|
+
const offset = wall.supportOffset ?? 0;
|
|
229
|
+
if (wall.supportSlabId === GROUND_SUPPORT_ID)
|
|
230
|
+
return levelBase + offset;
|
|
231
|
+
return (computeWallSlabSupport(wall, supportSlabs, walls, wall.supportSlabId ?? null, null, levelBase)
|
|
232
|
+
.elevation + offset);
|
|
217
233
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
/**
|
|
235
|
+
* The plane an auto floor/ceiling takes when its enclosing walls disagree.
|
|
236
|
+
*
|
|
237
|
+
* `floor` takes the HIGHEST wall base and `ceiling` the LOWEST wall top —
|
|
238
|
+
* the only pair that cannot open a hole: a floor at the lowest base would
|
|
239
|
+
* leave daylight under every wall standing higher, and a ceiling at the
|
|
240
|
+
* highest top would poke out through the shortest wall. Both surfaces stay
|
|
241
|
+
* flat (a slab is one scalar elevation by schema; see `vertical-model.md`),
|
|
242
|
+
* so a room on a slope is a level room cut into the hillside — the walls on
|
|
243
|
+
* the low side extend down to meet it, which is what their `baseSegments`
|
|
244
|
+
* fill-down already does.
|
|
245
|
+
*
|
|
246
|
+
* Non-finite inputs are the one abstain: a broken graph should keep the
|
|
247
|
+
* existing placement rather than move a surface to NaN.
|
|
248
|
+
*/
|
|
249
|
+
function roomFloorPlane(wallBases) {
|
|
250
|
+
if (wallBases.length === 0 || wallBases.some((value) => !Number.isFinite(value)))
|
|
251
|
+
return undefined;
|
|
252
|
+
return Math.max(...wallBases);
|
|
226
253
|
}
|
|
227
|
-
function
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
254
|
+
function roomCeilingPlane(wallTops) {
|
|
255
|
+
if (wallTops.length === 0 || wallTops.some((value) => !Number.isFinite(value)))
|
|
256
|
+
return undefined;
|
|
257
|
+
return Math.min(...wallTops);
|
|
258
|
+
}
|
|
259
|
+
function autoRoomVerticalPlacements(spaces, walls, supportSlabs, nodes, storeyHeight) {
|
|
260
|
+
const wallsById = new Map(walls.map((wall) => [wall.id, wall]));
|
|
261
|
+
const placements = new Map();
|
|
262
|
+
for (const space of spaces) {
|
|
263
|
+
const boundaryWalls = space.wallIds.flatMap((id) => {
|
|
264
|
+
const wall = wallsById.get(id);
|
|
265
|
+
return wall ? [wall] : [];
|
|
266
|
+
});
|
|
267
|
+
if (boundaryWalls.length !== space.wallIds.length)
|
|
231
268
|
continue;
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
269
|
+
const wallBases = boundaryWalls.map((wall) => boundaryWallBase(wall, walls, supportSlabs, nodes, space.levelId));
|
|
270
|
+
const base = roomFloorPlane(wallBases);
|
|
271
|
+
if (base === undefined)
|
|
272
|
+
continue;
|
|
273
|
+
const wallTops = boundaryWalls.map((wall, index) => resolveWallTop(wall, storeyHeight, wallBases[index] ?? base));
|
|
274
|
+
const top = roomCeilingPlane(wallTops);
|
|
275
|
+
if (top === undefined)
|
|
276
|
+
continue;
|
|
277
|
+
placements.set(polygonSignature(space.polygon.map(pointFromTuple)), {
|
|
278
|
+
slabElevation: base + DEFAULT_AUTO_SLAB_ELEVATION,
|
|
279
|
+
ceilingHeight: top - CEILING_CLAMP_MARGIN,
|
|
280
|
+
});
|
|
236
281
|
}
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
function resolveAutoCeilingHeight(roomPolygon, context = {}) {
|
|
240
|
-
return (resolveRoomSlabElevation(roomPolygon, context.slabs) +
|
|
241
|
-
resolveRoomWallHeight(roomPolygon, context.walls));
|
|
282
|
+
return placements;
|
|
242
283
|
}
|
|
243
284
|
function getWallDirection(wall) {
|
|
244
285
|
const dx = wall.end[0] - wall.start[0];
|
|
@@ -604,7 +645,7 @@ function sameTuplePolygon(current, next) {
|
|
|
604
645
|
return (current.length === next.length &&
|
|
605
646
|
current.every((point, index) => point[0] === next[index]?.[0] && point[1] === next[index]?.[1]));
|
|
606
647
|
}
|
|
607
|
-
function wallGeometrySignature(wall) {
|
|
648
|
+
function wallGeometrySignature(wall, nodes, levelId) {
|
|
608
649
|
return [
|
|
609
650
|
wall.id,
|
|
610
651
|
wall.start[0].toFixed(4),
|
|
@@ -612,12 +653,42 @@ function wallGeometrySignature(wall) {
|
|
|
612
653
|
wall.end[0].toFixed(4),
|
|
613
654
|
wall.end[1].toFixed(4),
|
|
614
655
|
(wall.thickness ?? 0.2).toFixed(4),
|
|
615
|
-
(
|
|
656
|
+
// Plane-bound (no stored height) is a distinct state, not a default
|
|
657
|
+
// value: it resolves to the storey plane, so it must not alias an
|
|
658
|
+
// explicit height of the same magnitude in the trigger signature.
|
|
659
|
+
wall.height == null ? 'plane' : wall.height.toFixed(4),
|
|
660
|
+
wall.supportSlabId ?? 'elected',
|
|
661
|
+
(wall.supportOffset ?? 0).toFixed(4),
|
|
616
662
|
getClampedWallCurveOffset(wall).toFixed(4),
|
|
663
|
+
// The ground under this wall, sampled at the SAME point
|
|
664
|
+
// `boundaryWallBase` samples it. Sculpting changes only `site.terrain`,
|
|
665
|
+
// so without a terrain term here every signature stays byte-identical
|
|
666
|
+
// and the sync early-exits — a room's floor and ceiling could never
|
|
667
|
+
// follow ground that moved beneath its walls.
|
|
668
|
+
//
|
|
669
|
+
// The sample, not the field, and not the resolved base: hashing the
|
|
670
|
+
// heightfield would re-trigger every level for a stroke on the far side
|
|
671
|
+
// of the lot, and resolving the full slab election would fold slab
|
|
672
|
+
// POLYGONS into the signature, which is exactly the delete/recreate
|
|
673
|
+
// feedback the comment below is about. Sampling where the placement
|
|
674
|
+
// samples means the two cannot disagree in either direction — no missed
|
|
675
|
+
// re-run, no spurious one.
|
|
676
|
+
//
|
|
677
|
+
// Granularity is per stroke, not per dab: live dabs publish to
|
|
678
|
+
// `useLiveTerrain` and never touch the scene store, so this runs once on
|
|
679
|
+
// release — inside the stroke's own `runAsSingleSceneHistoryStep`, which
|
|
680
|
+
// is what puts the moved floor and the terrain that moved it in the same
|
|
681
|
+
// undo step. Mid-drag the ground-hosted walls follow the brush while the
|
|
682
|
+
// floor waits for release; re-deriving per dab would mean a scene write
|
|
683
|
+
// per dab and a floor that jitters under the cursor.
|
|
684
|
+
levelBaseElevationAt(nodes, levelId, wall.start[0], wall.start[1]).toFixed(4),
|
|
617
685
|
].join('|');
|
|
618
686
|
}
|
|
619
|
-
function levelWallSnapshot(walls) {
|
|
620
|
-
return walls
|
|
687
|
+
function levelWallSnapshot(walls, nodes, levelId) {
|
|
688
|
+
return walls
|
|
689
|
+
.map((wall) => wallGeometrySignature(wall, nodes, levelId))
|
|
690
|
+
.sort()
|
|
691
|
+
.join('||');
|
|
621
692
|
}
|
|
622
693
|
function zoneGeometrySignature(zone) {
|
|
623
694
|
return [
|
|
@@ -627,13 +698,24 @@ function zoneGeometrySignature(zone) {
|
|
|
627
698
|
zone.polygon.map(([x, z]) => `${x.toFixed(4)},${z.toFixed(4)}`).join(';'),
|
|
628
699
|
].join('|');
|
|
629
700
|
}
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
// traced room footprint can adopt its enclosing walls
|
|
633
|
-
// next remodel.
|
|
701
|
+
// Slab/ceiling POLYGONS stay out of the trigger signature: including
|
|
702
|
+
// generated footprints caused delete/recreate feedback. Zones are included
|
|
703
|
+
// only so a newly traced room footprint can adopt its enclosing walls
|
|
704
|
+
// without waiting for the next remodel. Slab ELEVATIONS and the level's
|
|
705
|
+
// stored storey height ARE included — both feed the explicit-ceiling
|
|
706
|
+
// re-clamp bound (the storey plane), and neither is rewritten by
|
|
707
|
+
// the sync, so regeneration triggers when they change without feedback.
|
|
708
|
+
// Stage 3-B adds the LEVEL-ABOVE's covering-slab undersides (elevation −
|
|
709
|
+
// thickness, recessed pools excluded): a deck created, lowered, or
|
|
710
|
+
// thickened above must re-run the sync below so ceilings re-clamp under
|
|
711
|
+
// it. Same polygon exclusion applies — the level-above's own auto sync
|
|
712
|
+
// rewrites its slab footprints, and hashing them here would re-trigger
|
|
713
|
+
// this level on every remodel above.
|
|
634
714
|
function levelStructureSnapshots(nodes) {
|
|
635
715
|
const wallsByLevel = new Map();
|
|
636
716
|
const zonesByLevel = new Map();
|
|
717
|
+
const slabElevationsByLevel = new Map();
|
|
718
|
+
const coveringUndersidesByLevel = new Map();
|
|
637
719
|
for (const node of Object.values(nodes)) {
|
|
638
720
|
if (!(node && typeof node === 'object' && 'parentId' in node && node.parentId))
|
|
639
721
|
continue;
|
|
@@ -648,13 +730,33 @@ function levelStructureSnapshots(nodes) {
|
|
|
648
730
|
zones.push(ZoneNode.parse(node));
|
|
649
731
|
zonesByLevel.set(levelId, zones);
|
|
650
732
|
}
|
|
733
|
+
else if (node.type === 'slab') {
|
|
734
|
+
const elevations = slabElevationsByLevel.get(levelId) ?? [];
|
|
735
|
+
elevations.push(`${node.id}:${(node.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION).toFixed(4)}`);
|
|
736
|
+
slabElevationsByLevel.set(levelId, elevations);
|
|
737
|
+
if (node.recessed !== true) {
|
|
738
|
+
const undersides = coveringUndersidesByLevel.get(levelId) ?? [];
|
|
739
|
+
const elevation = node.elevation ?? 0.05;
|
|
740
|
+
const thickness = node.thickness ?? 0.05;
|
|
741
|
+
undersides.push(`${node.id}:${(elevation - thickness).toFixed(4)}`);
|
|
742
|
+
coveringUndersidesByLevel.set(levelId, undersides);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
651
745
|
}
|
|
746
|
+
const levelElevations = getLevelElevations(nodes);
|
|
652
747
|
const snapshots = new Map();
|
|
653
748
|
const levelIds = new Set([...wallsByLevel.keys(), ...zonesByLevel.keys()]);
|
|
654
749
|
for (const levelId of levelIds) {
|
|
655
750
|
const walls = wallsByLevel.get(levelId) ?? [];
|
|
656
751
|
const zones = zonesByLevel.get(levelId) ?? [];
|
|
657
|
-
|
|
752
|
+
const level = nodes[levelId];
|
|
753
|
+
const storeyKey = level?.type === 'level' && typeof level.height === 'number' ? level.height.toFixed(4) : '';
|
|
754
|
+
const slabKey = (slabElevationsByLevel.get(levelId) ?? []).sort().join(';');
|
|
755
|
+
const aboveId = findLevelAboveId(levelId, levelElevations);
|
|
756
|
+
const aboveSlabKey = aboveId
|
|
757
|
+
? (coveringUndersidesByLevel.get(aboveId) ?? []).sort().join(';')
|
|
758
|
+
: '';
|
|
759
|
+
snapshots.set(levelId, `${storeyKey}#${levelWallSnapshot(walls, nodes, levelId)}##${zones.map(zoneGeometrySignature).sort().join('||')}##${slabKey}##${aboveSlabKey}`);
|
|
658
760
|
}
|
|
659
761
|
return snapshots;
|
|
660
762
|
}
|
|
@@ -712,7 +814,7 @@ export function resolveAutoZonePolygon(zone, resolve) {
|
|
|
712
814
|
const room = extractRooms(walls).find((candidate) => sameStringSet([...new Set(candidate.boundaryFaces.map((boundary) => boundary.wallId))], zone.boundaryWallIds));
|
|
713
815
|
return room ? room.polygon.map(pointToTuple) : zone.polygon;
|
|
714
816
|
}
|
|
715
|
-
export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
817
|
+
export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {}) {
|
|
716
818
|
const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls);
|
|
717
819
|
const manualSignatures = new Set(manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))));
|
|
718
820
|
const manualPolygons = manualSlabs.map((slab) => slab.polygon.map(pointFromTuple));
|
|
@@ -758,7 +860,11 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
758
860
|
return;
|
|
759
861
|
matchedDetectedIdx.add(index);
|
|
760
862
|
matchedSlabIds.add(existing.slab.id);
|
|
761
|
-
|
|
863
|
+
const polygon = room.poly.map(pointToTuple);
|
|
864
|
+
updatesById.set(existing.slab.id, {
|
|
865
|
+
polygon,
|
|
866
|
+
elevation: context.elevationForRoom?.(polygon),
|
|
867
|
+
});
|
|
762
868
|
});
|
|
763
869
|
const remainingDetected = detected
|
|
764
870
|
.map((room, index) => ({ room, index }))
|
|
@@ -787,7 +893,11 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
787
893
|
continue;
|
|
788
894
|
matchedDetectedIdx.add(index);
|
|
789
895
|
matchedSlabIds.add(bestMatch.entry.slab.id);
|
|
790
|
-
|
|
896
|
+
const polygon = room.poly.map(pointToTuple);
|
|
897
|
+
updatesById.set(bestMatch.entry.slab.id, {
|
|
898
|
+
polygon,
|
|
899
|
+
elevation: context.elevationForRoom?.(polygon),
|
|
900
|
+
});
|
|
791
901
|
}
|
|
792
902
|
const detectedRoomPolygons = detectedAll.map((room) => room.poly);
|
|
793
903
|
const slabsToDelete = [];
|
|
@@ -809,10 +919,17 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
809
919
|
...existingAuto
|
|
810
920
|
.filter((slab) => updatesById.has(slab.id))
|
|
811
921
|
.flatMap((slab) => {
|
|
812
|
-
const
|
|
813
|
-
if (!
|
|
922
|
+
const update = updatesById.get(slab.id);
|
|
923
|
+
if (!update)
|
|
814
924
|
return [];
|
|
815
|
-
|
|
925
|
+
const data = {};
|
|
926
|
+
if (!sameTuplePolygon(slab.polygon, update.polygon))
|
|
927
|
+
data.polygon = update.polygon;
|
|
928
|
+
if (update.elevation !== undefined &&
|
|
929
|
+
Math.abs(slab.elevation - update.elevation) > ROOM_VERTICAL_PLANE_EPSILON) {
|
|
930
|
+
data.elevation = update.elevation;
|
|
931
|
+
}
|
|
932
|
+
return Object.keys(data).length > 0 ? [{ id: slab.id, data }] : [];
|
|
816
933
|
}),
|
|
817
934
|
...slabDemotions,
|
|
818
935
|
];
|
|
@@ -826,11 +943,15 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
826
943
|
continue;
|
|
827
944
|
const name = nextAutoRoomName(plannedSlabsForNaming, 'Slab');
|
|
828
945
|
plannedSlabsForNaming.push({ name });
|
|
946
|
+
const polygon = room.poly.map(pointToTuple);
|
|
947
|
+
const elevation = context.elevationForRoom?.(polygon);
|
|
829
948
|
slabsToCreate.push(SlabNode.parse({
|
|
830
949
|
name,
|
|
831
|
-
polygon
|
|
950
|
+
polygon,
|
|
832
951
|
holes: [],
|
|
833
|
-
elevation:
|
|
952
|
+
elevation: elevation !== undefined && Number.isFinite(elevation)
|
|
953
|
+
? elevation
|
|
954
|
+
: DEFAULT_AUTO_SLAB_ELEVATION,
|
|
834
955
|
autoFromWalls: true,
|
|
835
956
|
}));
|
|
836
957
|
}
|
|
@@ -840,8 +961,8 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
840
961
|
delete: slabsToDelete,
|
|
841
962
|
};
|
|
842
963
|
}
|
|
843
|
-
function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore) {
|
|
844
|
-
const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs);
|
|
964
|
+
function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore, context = {}) {
|
|
965
|
+
const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs, context);
|
|
845
966
|
if (plan.delete.length > 0) {
|
|
846
967
|
sceneStore.getState().deleteNodes(plan.delete);
|
|
847
968
|
}
|
|
@@ -853,22 +974,6 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore)
|
|
|
853
974
|
}
|
|
854
975
|
return plan;
|
|
855
976
|
}
|
|
856
|
-
export function projectAutoSlabsForPlan(existingSlabs, plan) {
|
|
857
|
-
const slabsById = new Map(existingSlabs.map((slab) => [slab.id, slab]));
|
|
858
|
-
for (const id of plan.delete) {
|
|
859
|
-
slabsById.delete(id);
|
|
860
|
-
}
|
|
861
|
-
for (const update of plan.update) {
|
|
862
|
-
const slab = slabsById.get(update.id);
|
|
863
|
-
if (!slab)
|
|
864
|
-
continue;
|
|
865
|
-
slabsById.set(update.id, SlabNode.parse({ ...slab, ...update.data }));
|
|
866
|
-
}
|
|
867
|
-
for (const slab of plan.create) {
|
|
868
|
-
slabsById.set(slab.id, slab);
|
|
869
|
-
}
|
|
870
|
-
return [...slabsById.values()];
|
|
871
|
-
}
|
|
872
977
|
export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context = {}) {
|
|
873
978
|
const manualCeilings = existingCeilings.filter((ceiling) => !ceiling.autoFromWalls);
|
|
874
979
|
const manualSignatures = new Set(manualCeilings.map((ceiling) => polygonSignature(ceiling.polygon.map(pointFromTuple))));
|
|
@@ -887,7 +992,6 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
887
992
|
centroid: polygonCentroid(room.poly),
|
|
888
993
|
area: Math.abs(polygonArea(room.poly)),
|
|
889
994
|
bbox: bboxOf(room.poly),
|
|
890
|
-
ceilingHeight: resolveAutoCeilingHeight(room.poly, context),
|
|
891
995
|
}));
|
|
892
996
|
const detected = detectedAll.filter(({ sig, poly }) => !manualSignatures.has(sig) && !matchesManualFootprint(poly, manualPolygons));
|
|
893
997
|
const existingAuto = existingCeilings.filter((ceiling) => ceiling.autoFromWalls);
|
|
@@ -916,9 +1020,10 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
916
1020
|
return;
|
|
917
1021
|
matchedDetectedIdx.add(index);
|
|
918
1022
|
matchedCeilingIds.add(existing.ceiling.id);
|
|
1023
|
+
const polygon = room.poly.map(pointToTuple);
|
|
919
1024
|
updatesById.set(existing.ceiling.id, {
|
|
920
|
-
polygon
|
|
921
|
-
height:
|
|
1025
|
+
polygon,
|
|
1026
|
+
height: context.heightForRoom?.(polygon),
|
|
922
1027
|
});
|
|
923
1028
|
});
|
|
924
1029
|
const remainingDetected = detected
|
|
@@ -948,9 +1053,10 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
948
1053
|
continue;
|
|
949
1054
|
matchedDetectedIdx.add(index);
|
|
950
1055
|
matchedCeilingIds.add(bestMatch.entry.ceiling.id);
|
|
1056
|
+
const polygon = room.poly.map(pointToTuple);
|
|
951
1057
|
updatesById.set(bestMatch.entry.ceiling.id, {
|
|
952
|
-
polygon
|
|
953
|
-
height:
|
|
1058
|
+
polygon,
|
|
1059
|
+
height: context.heightForRoom?.(polygon),
|
|
954
1060
|
});
|
|
955
1061
|
}
|
|
956
1062
|
const detectedRoomPolygons = detectedAll.map((room) => room.poly);
|
|
@@ -967,6 +1073,24 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
967
1073
|
ceilingDemotions.push({ id: ceiling.id, data: { autoFromWalls: false } });
|
|
968
1074
|
}
|
|
969
1075
|
}
|
|
1076
|
+
// Stage 3-B reactive re-clamp (clamp-never-ask): a covering slab
|
|
1077
|
+
// created, moved, or thickened on the level above can leave an EXISTING
|
|
1078
|
+
// manual explicit-height ceiling poking into its solid. Clamp explicit
|
|
1079
|
+
// heights down to the bound; never raise them — a user-lowered ceiling
|
|
1080
|
+
// is intent, only an over-bound one is a conflict. Follows-mode
|
|
1081
|
+
// ceilings (absent height) derive under the bound by construction and
|
|
1082
|
+
// are skipped, so the clamp can never convert one to an explicit
|
|
1083
|
+
// height.
|
|
1084
|
+
const manualClamps = manualCeilings.flatMap((ceiling) => {
|
|
1085
|
+
if (ceiling.height == null)
|
|
1086
|
+
return [];
|
|
1087
|
+
const bound = resolveCeilingClampBound(ceiling.polygon, context);
|
|
1088
|
+
if (!Number.isFinite(bound))
|
|
1089
|
+
return [];
|
|
1090
|
+
return ceiling.height > bound + CEILING_HEIGHT_EPSILON
|
|
1091
|
+
? [{ id: ceiling.id, data: { height: bound } }]
|
|
1092
|
+
: [];
|
|
1093
|
+
});
|
|
970
1094
|
const ceilingsToUpdate = [
|
|
971
1095
|
...existingAuto
|
|
972
1096
|
.filter((ceiling) => updatesById.has(ceiling.id))
|
|
@@ -975,16 +1099,17 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
975
1099
|
if (!update)
|
|
976
1100
|
return [];
|
|
977
1101
|
const data = {};
|
|
978
|
-
if (!sameTuplePolygon(ceiling.polygon, update.polygon))
|
|
1102
|
+
if (!sameTuplePolygon(ceiling.polygon, update.polygon))
|
|
979
1103
|
data.polygon = update.polygon;
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1104
|
+
if (update.height !== undefined &&
|
|
1105
|
+
(ceiling.height === undefined ||
|
|
1106
|
+
Math.abs(ceiling.height - update.height) > ROOM_VERTICAL_PLANE_EPSILON)) {
|
|
983
1107
|
data.height = update.height;
|
|
984
1108
|
}
|
|
985
|
-
return Object.keys(data).length
|
|
1109
|
+
return Object.keys(data).length > 0 ? [{ id: ceiling.id, data }] : [];
|
|
986
1110
|
}),
|
|
987
1111
|
...ceilingDemotions,
|
|
1112
|
+
...manualClamps,
|
|
988
1113
|
];
|
|
989
1114
|
const plannedCeilingsForNaming = [...existingCeilings];
|
|
990
1115
|
const ceilingsToCreate = [];
|
|
@@ -996,11 +1121,13 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
996
1121
|
continue;
|
|
997
1122
|
const name = nextAutoRoomName(plannedCeilingsForNaming, 'Ceiling');
|
|
998
1123
|
plannedCeilingsForNaming.push({ name });
|
|
1124
|
+
const polygon = room.poly.map(pointToTuple);
|
|
1125
|
+
const height = context.heightForRoom?.(polygon);
|
|
999
1126
|
ceilingsToCreate.push(CeilingNode.parse({
|
|
1000
1127
|
name,
|
|
1001
|
-
polygon
|
|
1128
|
+
polygon,
|
|
1002
1129
|
holes: [],
|
|
1003
|
-
height:
|
|
1130
|
+
...(height !== undefined && Number.isFinite(height) ? { height } : {}),
|
|
1004
1131
|
autoFromWalls: true,
|
|
1005
1132
|
}));
|
|
1006
1133
|
}
|
|
@@ -1066,10 +1193,24 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
|
1066
1193
|
},
|
|
1067
1194
|
})));
|
|
1068
1195
|
}
|
|
1196
|
+
const levelNode = nodes[levelId];
|
|
1197
|
+
const storeyHeight = levelNode?.type === 'level'
|
|
1198
|
+
? getStoredLevelHeight(levelNode)
|
|
1199
|
+
: DEFAULT_LEVEL_HEIGHT;
|
|
1069
1200
|
const parsedSlabs = slabs.map((slab) => SlabNode.parse(slab));
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1201
|
+
const verticalPlacements = autoRoomVerticalPlacements(spaces, walls,
|
|
1202
|
+
// A derived floor cannot be evidence for its own next elevation: that
|
|
1203
|
+
// would lift unpinned walls, then lift the floor again on every pass.
|
|
1204
|
+
parsedSlabs.filter((slab) => !slab.autoFromWalls), nodes, storeyHeight);
|
|
1205
|
+
const placementFor = (polygon) => verticalPlacements.get(polygonSignature(polygon.map(pointFromTuple)));
|
|
1206
|
+
syncAutoSlabsForLevel(levelId, roomPolygons, parsedSlabs, sceneStore, {
|
|
1207
|
+
elevationForRoom: (polygon) => placementFor(polygon)?.slabElevation,
|
|
1208
|
+
});
|
|
1209
|
+
syncAutoCeilingsForLevel(levelId, roomPolygons, ceilings.map((ceiling) => CeilingNode.parse(ceiling)), sceneStore, {
|
|
1210
|
+
storeyHeight,
|
|
1211
|
+
ceilingClampBound: (polygon) => getCeilingClampBound(levelId, nodes, polygon),
|
|
1212
|
+
heightForRoom: (polygon) => placementFor(polygon)?.ceilingHeight,
|
|
1213
|
+
});
|
|
1073
1214
|
const zonePlan = planAutoZonesForLevel(spaces, zones.map((zone) => ZoneNode.parse(zone)));
|
|
1074
1215
|
if (zonePlan.update.length > 0)
|
|
1075
1216
|
updateNodes(zonePlan.update);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sculpt brush: pointer motion → `HeightPatch`.
|
|
3
|
+
*
|
|
4
|
+
* The obvious implementation — `h += strength * falloff` every tick — is wrong,
|
|
5
|
+
* and it is wrong in a way that is well documented by the people who ship it.
|
|
6
|
+
* Dwelling in one spot accumulates without bound, overlapping dabs double-apply,
|
|
7
|
+
* and a fast drag deposits less material than a slow one. That is exactly the
|
|
8
|
+
* "hash edges and jagged" result expert Sims builders complain about and route
|
|
9
|
+
* around. This module implements the model the mature tools converge on instead:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Freeze a snapshot at pointer-down.** Every dab is evaluated against the
|
|
12
|
+
* snapshot, never against the running result.
|
|
13
|
+
* 2. **Saturating coverage mask.** A dab raises coverage to `max(mask, falloff)`
|
|
14
|
+
* rather than adding to it, and the height is always
|
|
15
|
+
* `snapshot + delta * mask`. Re-crossing your own stroke therefore cannot
|
|
16
|
+
* compound — the second pass recomputes the same value from the same
|
|
17
|
+
* snapshot.
|
|
18
|
+
* 3. **Arc-length dab spacing.** `advanceStroke` walks from the previous dab to
|
|
19
|
+
* the new pointer position in fixed metre steps, so stroke strength is a
|
|
20
|
+
* function of distance travelled, not of mouse speed or frame rate.
|
|
21
|
+
* 4. **Cosine-bell falloff.** Of the standard sculpt kernels this is one of only
|
|
22
|
+
* two that are C¹ at *both* centre and rim, which is what prevents a visible
|
|
23
|
+
* ridge at the brush boundary.
|
|
24
|
+
*
|
|
25
|
+
* The consequence users feel: one stroke is one bounded, repeatable edit. Hold
|
|
26
|
+
* still and nothing runs away; press again to go further.
|
|
27
|
+
*
|
|
28
|
+
* The `TerrainStroke` returned by `beginStroke` is a mutable accumulator — the
|
|
29
|
+
* one mutable thing in this file, and deliberately so. It is owned by exactly
|
|
30
|
+
* one in-flight gesture and dies with it, which is cheaper and clearer than
|
|
31
|
+
* rebuilding a coverage map per pointer-move.
|
|
32
|
+
*/
|
|
33
|
+
import { type HeightPatch, type TerrainField } from './terrain-field';
|
|
34
|
+
export type BrushShape = 'round' | 'square';
|
|
35
|
+
/**
|
|
36
|
+
* `raise`/`lower` are one verb with an inverted sign at the call site, matching
|
|
37
|
+
* every surveyed tool's modifier-inverts convention. `flatten` is absolute:
|
|
38
|
+
* it converges on a target height, which is the operation that makes two
|
|
39
|
+
* disjoint building pads provably equal — the thing freehand push/pull cannot do.
|
|
40
|
+
*/
|
|
41
|
+
export type TerrainVerb = 'raise' | 'lower' | 'flatten' | 'smooth';
|
|
42
|
+
export type BrushSettings = {
|
|
43
|
+
/** Brush radius in metres. */
|
|
44
|
+
radius: number;
|
|
45
|
+
/** 0–1. Scales how far one full stroke pass gets toward its goal. */
|
|
46
|
+
strength: number;
|
|
47
|
+
/**
|
|
48
|
+
* 0–1 softness. 0 is a hard-edged stamp; 1 puts the cosine bell across the
|
|
49
|
+
* whole radius. The inner `1 - falloff` fraction stays at full coverage.
|
|
50
|
+
*/
|
|
51
|
+
falloff: number;
|
|
52
|
+
shape: BrushShape;
|
|
53
|
+
};
|
|
54
|
+
export declare const DEFAULT_BRUSH_SETTINGS: BrushSettings;
|
|
55
|
+
/**
|
|
56
|
+
* Metres a single raise/lower stroke moves the ground at full strength and full
|
|
57
|
+
* coverage. Bounding it per stroke is what makes the verb aimable: the user
|
|
58
|
+
* knows a pass is worth at most this much, and repeats for more.
|
|
59
|
+
*/
|
|
60
|
+
export declare const RAISE_METRES_PER_STROKE = 1;
|
|
61
|
+
/**
|
|
62
|
+
* Smallest brush radius that still bites, as a multiple of the field's spacing.
|
|
63
|
+
*
|
|
64
|
+
* A dab only moves samples that fall *inside* its radius, so a brush narrower
|
|
65
|
+
* than the gap between samples can land entirely between four of them and change
|
|
66
|
+
* nothing: `advanceStroke` returns `null`, and the user drags with no mark, no
|
|
67
|
+
* cursor change, and no error. 1.5 rather than the ~0.71 where a round brush
|
|
68
|
+
* first catches a corner, because a brush that only bites when it happens to
|
|
69
|
+
* straddle a sample is worse than one that is honestly too small — at 1.5 the
|
|
70
|
+
* worst-case coverage is 0.97, so the dab lands wherever it is aimed.
|
|
71
|
+
*
|
|
72
|
+
* A *multiple* of spacing rather than a constant because spacing is not fixed:
|
|
73
|
+
* `fieldExtentForSite` stretches it once a lot exceeds what 257² samples cover
|
|
74
|
+
* at the default, so on a 300 m lot the old flat 0.5 m floor sat below half the
|
|
75
|
+
* sample gap and the brush silently stopped working at the bottom of its range.
|
|
76
|
+
*/
|
|
77
|
+
export declare const MIN_BRUSH_RADIUS_IN_SPACINGS = 1.5;
|
|
78
|
+
/**
|
|
79
|
+
* Clamp a radius to what `field` can actually resolve.
|
|
80
|
+
*
|
|
81
|
+
* Lives here rather than in the editor's UI layer because it is a fact about the
|
|
82
|
+
* footprint math above, not a preference: every producer of a radius (slider,
|
|
83
|
+
* `[`/`]`, a future import preset) has to respect it or it produces a brush that
|
|
84
|
+
* does nothing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function minBrushRadius(field: Pick<TerrainField, 'spacing'>): number;
|
|
87
|
+
export type TerrainStroke = {
|
|
88
|
+
readonly verb: TerrainVerb;
|
|
89
|
+
readonly settings: BrushSettings;
|
|
90
|
+
/** The field at pointer-down. Never mutated; every dab reads from it. */
|
|
91
|
+
readonly snapshot: TerrainField;
|
|
92
|
+
/** Absolute target height in metres. Only meaningful for `flatten`. */
|
|
93
|
+
readonly target: number;
|
|
94
|
+
/** Sample index → saturating coverage in 0–1. */
|
|
95
|
+
readonly mask: Map<number, number>;
|
|
96
|
+
/** Lazily computed diffused snapshot, for `smooth`. */
|
|
97
|
+
smoothed: Int16Array | null;
|
|
98
|
+
/** Last dab centre, for arc-length spacing. Null until the first dab. */
|
|
99
|
+
lastX: number | null;
|
|
100
|
+
lastZ: number | null;
|
|
101
|
+
};
|
|
102
|
+
export declare function beginStroke(options: {
|
|
103
|
+
field: TerrainField;
|
|
104
|
+
verb: TerrainVerb;
|
|
105
|
+
settings?: Partial<BrushSettings>;
|
|
106
|
+
/** Absolute height for `flatten`, in metres. Ignored by the other verbs. */
|
|
107
|
+
target?: number;
|
|
108
|
+
}): TerrainStroke;
|
|
109
|
+
/**
|
|
110
|
+
* Sample the height a `flatten` stroke should aim at, from the field itself.
|
|
111
|
+
*
|
|
112
|
+
* Sampling before the stroke starts (right-click / first click, per the surveyed
|
|
113
|
+
* tools) is what makes "flatten this pad to match that corner" a two-step
|
|
114
|
+
* gesture instead of a numeric-entry chore.
|
|
115
|
+
*/
|
|
116
|
+
export declare function sampleTarget(field: TerrainField, x: number, z: number): number;
|
|
117
|
+
/**
|
|
118
|
+
* Forget where the last dab landed, without ending the stroke.
|
|
119
|
+
*
|
|
120
|
+
* Needed because dab spacing is a true *arc length*: `advanceStroke` interpolates
|
|
121
|
+
* from the previous centre, so any gap in the pointer stream is bridged with a
|
|
122
|
+
* line of dabs when it resumes. That is exactly right for a dropped frame, and
|
|
123
|
+
* exactly wrong when the caller deliberately skipped a stretch — a camera zoom
|
|
124
|
+
* mid-stroke moves the ground under a held brush, and bridging back to a centre
|
|
125
|
+
* from before the zoom paints a stripe across everything in between.
|
|
126
|
+
*
|
|
127
|
+
* A stroke resumed after this deposits its next dab at the pointer, like the
|
|
128
|
+
* first one. The mask is untouched, so the stroke stays saturating across the
|
|
129
|
+
* break and the whole gesture is still one undo step.
|
|
130
|
+
*/
|
|
131
|
+
export declare function detachStrokeAnchor(stroke: TerrainStroke): void;
|
|
132
|
+
/**
|
|
133
|
+
* Extend the stroke to the pointer's new position and return the patch to apply.
|
|
134
|
+
*
|
|
135
|
+
* Returns `null` when the motion is shorter than the dab spacing (nothing
|
|
136
|
+
* changed yet) or when the footprint misses the field entirely. The patch is
|
|
137
|
+
* absolute heights over the union of the dabs placed by *this* call, recomputed
|
|
138
|
+
* from the snapshot and the saturated mask — which is why applying patches in
|
|
139
|
+
* order converges rather than compounding.
|
|
140
|
+
*/
|
|
141
|
+
export declare function advanceStroke(stroke: TerrainStroke, x: number, z: number): HeightPatch | null;
|
|
142
|
+
/**
|
|
143
|
+
* Coverage in 0–1 for a sample at offset `(dx, dz)` from a dab centre.
|
|
144
|
+
*
|
|
145
|
+
* Round uses Euclidean distance, square uses Chebyshev — the same radius means
|
|
146
|
+
* the square brush's inscribed circle matches the round one, so switching shape
|
|
147
|
+
* mid-session does not change the brush's apparent size.
|
|
148
|
+
*/
|
|
149
|
+
export declare function weightAt(settings: BrushSettings, dx: number, dz: number): number;
|
|
150
|
+
/**
|
|
151
|
+
* The world-XZ height under a brush, for the readout the sculpt HUD shows.
|
|
152
|
+
* Thin, but it keeps the tool from importing `heightAt` just to render a number.
|
|
153
|
+
*/
|
|
154
|
+
export declare function brushHeightAt(field: TerrainField, x: number, z: number): number;
|
|
155
|
+
/** Peak coverage the stroke reached, for tests and for the HUD's "pass" readout. */
|
|
156
|
+
export declare function maxCoverage(stroke: TerrainStroke): number;
|
|
157
|
+
/**
|
|
158
|
+
* Highest sample height in metres under a world-XZ rect — the seed a `flatten`
|
|
159
|
+
* pad uses when the user asks for "level with the high corner" rather than
|
|
160
|
+
* typing a number.
|
|
161
|
+
*/
|
|
162
|
+
export declare function highestOver(field: TerrainField, minX: number, minZ: number, maxX: number, maxZ: number): number;
|
|
163
|
+
//# sourceMappingURL=terrain-brush.d.ts.map
|