@pascal-app/core 1.0.0-beta.4 → 1.0.0
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/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/capture/index.d.ts +3 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +2 -0
- package/dist/capture/schema.d.ts +417 -0
- package/dist/capture/schema.d.ts.map +1 -0
- package/dist/capture/schema.js +348 -0
- package/dist/capture/source.d.ts +60 -0
- package/dist/capture/source.d.ts.map +1 -0
- package/dist/capture/source.js +254 -0
- package/dist/events/bus.d.ts +55 -7
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/hidden-wall-pointer-hold.d.ts +28 -0
- package/dist/events/hidden-wall-pointer-hold.d.ts.map +1 -0
- package/dist/events/hidden-wall-pointer-hold.js +37 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +12 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +39 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +2785 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +132 -19
- package/dist/hooks/spatial-grid/support-host-patch.d.ts +45 -1
- package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/support-host-patch.js +171 -6
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -5
- package/dist/lib/asset-storage.d.ts.map +1 -1
- package/dist/lib/asset-storage.js +4 -1
- package/dist/lib/polygon-union.d.ts +4 -0
- package/dist/lib/polygon-union.d.ts.map +1 -0
- package/dist/lib/polygon-union.js +306 -0
- package/dist/lib/roof-overlap.d.ts +32 -0
- package/dist/lib/roof-overlap.d.ts.map +1 -0
- package/dist/lib/roof-overlap.js +72 -0
- package/dist/lib/room-topology-index.d.ts +50 -0
- package/dist/lib/room-topology-index.d.ts.map +1 -0
- package/dist/lib/room-topology-index.js +237 -0
- package/dist/lib/slab-polygon.d.ts +35 -2
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +133 -27
- package/dist/lib/space-detection.d.ts +40 -3
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +890 -246
- package/dist/registry/handles.d.ts +24 -3
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -3
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/registry.d.ts +29 -6
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +111 -4
- package/dist/registry/scene-api.d.ts +16 -0
- package/dist/registry/scene-api.d.ts.map +1 -1
- package/dist/registry/scene-api.js +49 -1
- package/dist/registry/types.d.ts +261 -13
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry/use-registry-version.d.ts +11 -0
- package/dist/registry/use-registry-version.d.ts.map +1 -0
- package/dist/registry/use-registry-version.js +15 -0
- package/dist/schema/__bench__/node-parsers.bench.d.ts +24 -0
- package/dist/schema/__bench__/node-parsers.bench.d.ts.map +1 -0
- package/dist/schema/__bench__/node-parsers.bench.js +314 -0
- package/dist/schema/__fixtures__/node-fixtures.d.ts +28 -0
- package/dist/schema/__fixtures__/node-fixtures.d.ts.map +1 -0
- package/dist/schema/__fixtures__/node-fixtures.js +125 -0
- package/dist/schema/base.d.ts +1 -1
- package/dist/schema/base.d.ts.map +1 -1
- package/dist/schema/base.js +8 -1
- package/dist/schema/compiled-node-parsers.d.ts +40 -0
- package/dist/schema/compiled-node-parsers.d.ts.map +1 -0
- package/dist/schema/compiled-node-parsers.js +95 -0
- package/dist/schema/index.d.ts +20 -15
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +19 -12
- package/dist/schema/material.d.ts +2 -1
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +1 -0
- package/dist/schema/nodes/automatic-downspout.d.ts +17 -0
- package/dist/schema/nodes/automatic-downspout.d.ts.map +1 -0
- package/dist/schema/nodes/automatic-downspout.js +237 -0
- package/dist/schema/nodes/block.d.ts +107 -0
- package/dist/schema/nodes/block.d.ts.map +1 -0
- package/dist/schema/nodes/block.js +207 -0
- package/dist/schema/nodes/box-vent.d.ts +8 -2
- package/dist/schema/nodes/box-vent.d.ts.map +1 -1
- package/dist/schema/nodes/box-vent.js +2 -0
- package/dist/schema/nodes/building.d.ts +1 -1
- package/dist/schema/nodes/cabinet.d.ts +25 -3
- package/dist/schema/nodes/cabinet.d.ts.map +1 -1
- package/dist/schema/nodes/cabinet.js +26 -5
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/chimney.d.ts +2 -2
- package/dist/schema/nodes/column.d.ts +6 -6
- package/dist/schema/nodes/construction-dimension.d.ts +4 -4
- package/dist/schema/nodes/cupola.d.ts +9 -1
- package/dist/schema/nodes/cupola.d.ts.map +1 -1
- package/dist/schema/nodes/cupola.js +2 -0
- package/dist/schema/nodes/door.d.ts +1 -1
- package/dist/schema/nodes/dormer.d.ts +44 -1
- package/dist/schema/nodes/dormer.d.ts.map +1 -1
- package/dist/schema/nodes/dormer.js +131 -7
- package/dist/schema/nodes/downspout.d.ts +11 -1
- package/dist/schema/nodes/downspout.d.ts.map +1 -1
- package/dist/schema/nodes/downspout.js +24 -0
- package/dist/schema/nodes/duct-fitting.d.ts +18 -1
- package/dist/schema/nodes/duct-fitting.d.ts.map +1 -1
- package/dist/schema/nodes/duct-fitting.js +25 -4
- package/dist/schema/nodes/duct-segment.d.ts +23 -1
- package/dist/schema/nodes/duct-segment.d.ts.map +1 -1
- package/dist/schema/nodes/duct-segment.js +20 -0
- package/dist/schema/nodes/duct-terminal.d.ts +1 -1
- package/dist/schema/nodes/elevator.d.ts +1 -1
- package/dist/schema/nodes/eyebrow-vent.d.ts +8 -2
- package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -1
- package/dist/schema/nodes/eyebrow-vent.js +2 -0
- package/dist/schema/nodes/fence.d.ts +1 -1
- package/dist/schema/nodes/guide.d.ts +1 -1
- package/dist/schema/nodes/gutter.d.ts +36 -2
- package/dist/schema/nodes/gutter.d.ts.map +1 -1
- package/dist/schema/nodes/gutter.js +308 -1
- package/dist/schema/nodes/hanger-overrides.d.ts +7 -0
- package/dist/schema/nodes/hanger-overrides.d.ts.map +1 -0
- package/dist/schema/nodes/hanger-overrides.js +6 -0
- package/dist/schema/nodes/hvac-equipment.d.ts +1 -1
- package/dist/schema/nodes/item.d.ts +4 -3
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +5 -0
- package/dist/schema/nodes/lean-to-extension.d.ts +212 -0
- package/dist/schema/nodes/lean-to-extension.d.ts.map +1 -0
- package/dist/schema/nodes/lean-to-extension.js +114 -0
- package/dist/schema/nodes/level.d.ts +5 -2
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +9 -0
- package/dist/schema/nodes/lineset.d.ts +1 -1
- package/dist/schema/nodes/liquid-line.d.ts +1 -1
- package/dist/schema/nodes/measurement.d.ts +1 -1
- package/dist/schema/nodes/pipe-fitting.d.ts +9 -1
- package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -1
- package/dist/schema/nodes/pipe-fitting.js +6 -2
- package/dist/schema/nodes/pipe-segment.d.ts +23 -1
- package/dist/schema/nodes/pipe-segment.d.ts.map +1 -1
- package/dist/schema/nodes/pipe-segment.js +20 -0
- package/dist/schema/nodes/pipe-trap.d.ts +1 -1
- package/dist/schema/nodes/ridge-vent.d.ts +1 -1
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
- package/dist/schema/nodes/ridge-vent.js +4 -1
- package/dist/schema/nodes/roof-segment-shape.d.ts +4 -0
- package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment-shape.js +64 -1
- package/dist/schema/nodes/roof-segment-walls.d.ts +3 -1
- package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment-walls.js +1 -1
- package/dist/schema/nodes/roof-segment.d.ts +41 -1
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +79 -2
- package/dist/schema/nodes/roof.d.ts +22 -1
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +14 -0
- package/dist/schema/nodes/scan.d.ts +19 -2
- package/dist/schema/nodes/scan.d.ts.map +1 -1
- package/dist/schema/nodes/scan.js +11 -1
- package/dist/schema/nodes/shelf.d.ts +2 -2
- package/dist/schema/nodes/site.d.ts +1 -1
- package/dist/schema/nodes/skylight.d.ts +1 -1
- package/dist/schema/nodes/slab.d.ts +1 -1
- package/dist/schema/nodes/solar-panel.d.ts +1 -1
- package/dist/schema/nodes/spawn.d.ts +1 -1
- package/dist/schema/nodes/stair-segment.d.ts +1 -1
- package/dist/schema/nodes/stair.d.ts +1 -1
- package/dist/schema/nodes/structural-grid.d.ts +1 -1
- package/dist/schema/nodes/turbine-vent.d.ts +7 -1
- package/dist/schema/nodes/turbine-vent.d.ts.map +1 -1
- package/dist/schema/nodes/turbine-vent.js +2 -0
- package/dist/schema/nodes/wall.d.ts +4 -4
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +7 -1
- package/dist/schema/nodes/window.d.ts +10 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +4 -0
- package/dist/schema/nodes/zone.d.ts +2 -2
- package/dist/schema/nodes/zone.js +1 -1
- package/dist/schema/types.d.ts +485 -109
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +27 -1
- package/dist/services/hosting.d.ts +2 -2
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +4 -4
- package/dist/services/index.d.ts +2 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -2
- package/dist/services/storey.d.ts +3 -10
- package/dist/services/storey.d.ts.map +1 -1
- package/dist/services/storey.js +45 -15
- package/dist/services/system-graph.d.ts +11 -0
- package/dist/services/system-graph.d.ts.map +1 -1
- package/dist/services/system-graph.js +35 -5
- package/dist/store/actions/node-actions.d.ts +15 -4
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +458 -19
- package/dist/store/history-control.d.ts +5 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +68 -6
- package/dist/store/history-invalidation.d.ts +3 -0
- package/dist/store/history-invalidation.d.ts.map +1 -0
- package/dist/store/history-invalidation.js +72 -0
- package/dist/store/scene-hydration.d.ts +5 -0
- package/dist/store/scene-hydration.d.ts.map +1 -0
- package/dist/store/scene-hydration.js +57 -0
- package/dist/store/use-scene.d.ts +3 -0
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +407 -227
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-service.js +16 -11
- package/dist/systems/roof/roof-elevation-system.d.ts +3 -0
- package/dist/systems/roof/roof-elevation-system.d.ts.map +1 -0
- package/dist/systems/roof/roof-elevation-system.js +94 -0
- package/dist/systems/roof/roof-elevation.d.ts +4 -0
- package/dist/systems/roof/roof-elevation.d.ts.map +1 -0
- package/dist/systems/roof/roof-elevation.js +95 -0
- package/dist/systems/roof/roof-footprint.d.ts +17 -0
- package/dist/systems/roof/roof-footprint.d.ts.map +1 -0
- package/dist/systems/roof/roof-footprint.js +83 -0
- package/dist/systems/slab/slab-support.d.ts +0 -31
- package/dist/systems/slab/slab-support.d.ts.map +1 -1
- package/dist/systems/slab/slab-support.js +26 -4
- package/dist/systems/stair/stair-flight.d.ts +17 -0
- package/dist/systems/stair/stair-flight.d.ts.map +1 -0
- package/dist/systems/stair/stair-flight.js +27 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +1556 -1328
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.d.ts +1 -0
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.js +93 -96
- package/dist/systems/stair/stair-rise.js +21 -20
- package/dist/systems/wall/wall-curve.d.ts +1 -0
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +73 -0
- package/dist/systems/wall/wall-footprint.d.ts +1 -0
- package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
- package/dist/systems/wall/wall-footprint.js +1 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +88 -9
- package/dist/systems/wall/wall-topology.d.ts +53 -0
- package/dist/systems/wall/wall-topology.d.ts.map +1 -0
- package/dist/systems/wall/wall-topology.js +414 -0
- package/dist/utils/clone-scene-graph.d.ts +2 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +39 -2
- package/dist/utils/scene-migrations.d.ts +1 -0
- package/dist/utils/scene-migrations.d.ts.map +1 -1
- package/dist/utils/scene-migrations.js +1 -0
- package/dist/utils/vertical-scene-migration.d.ts +13 -0
- package/dist/utils/vertical-scene-migration.d.ts.map +1 -0
- package/dist/utils/vertical-scene-migration.js +179 -0
- package/dist/validation/validate-build-json.d.ts +3 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +49 -2
- package/package.json +10 -3
- package/dist/systems/elevator/elevator-runtime-system.d.ts +0 -2
- package/dist/systems/elevator/elevator-runtime-system.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-runtime-system.js +0 -8
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { GROUND_SUPPORT_ID } from '../hooks/spatial-grid/support-host-id';
|
|
2
2
|
import { CeilingNode, SlabNode, ZoneNode, } from '../schema';
|
|
3
3
|
import { DEFAULT_LEVEL_HEIGHT } from '../services/level-height';
|
|
4
|
-
import { CEILING_CLAMP_MARGIN, findLevelAboveId, getCeilingClampBound, getLevelElevations, getStoredLevelHeight, } from '../services/storey';
|
|
5
|
-
import { getSceneHistoryPauseDepth, pauseSceneHistory, resumeSceneHistory, } from '../store/history-control';
|
|
4
|
+
import { CEILING_CLAMP_MARGIN, findLevelAboveId, getCeilingClampBound, getLevelBelow, getLevelElevations, getStoredLevelHeight, } from '../services/storey';
|
|
5
|
+
import { activeSceneCommitNodeIds, getSceneHistoryPauseDepth, pauseSceneHistory, resumeSceneHistory, subscribeSceneCommits, } from '../store/history-control';
|
|
6
6
|
import { computeWallSlabSupport } from '../systems/slab/slab-support';
|
|
7
7
|
import { getClampedWallCurveOffset, getWallCurveFrameAt, isCurvedWall, } from '../systems/wall/wall-curve';
|
|
8
8
|
import { resolveWallTop } from '../systems/wall/wall-top';
|
|
9
9
|
import { simplifyClosedPolygon } from './polygon-geometry';
|
|
10
|
+
import { distanceToSegment, RoomTopologyIndex, } from './room-topology-index';
|
|
10
11
|
import { levelBaseElevationAt } from './terrain-support';
|
|
11
12
|
const DEFAULT_AUTO_SLAB_ELEVATION = 0.05;
|
|
12
13
|
const CEILING_HEIGHT_EPSILON = 1e-6;
|
|
@@ -138,10 +139,10 @@ function bboxOverlapArea(a, b) {
|
|
|
138
139
|
// sampling a grid of cell centers over the subject's bbox. Cheap and robust
|
|
139
140
|
// enough for the merge-vs-demote decision; exact polygon clipping would be a
|
|
140
141
|
// heavy dependency for a 60% threshold.
|
|
141
|
-
function polygonCoverageRatio(subject, covers) {
|
|
142
|
+
function polygonCoverageRatio(subject, covers, subjectBounds) {
|
|
142
143
|
if (subject.length < 3 || covers.length === 0)
|
|
143
144
|
return 0;
|
|
144
|
-
const bbox = bboxOf(subject);
|
|
145
|
+
const bbox = subjectBounds ?? bboxOf(subject);
|
|
145
146
|
const width = bbox.maxX - bbox.minX;
|
|
146
147
|
const height = bbox.maxY - bbox.minY;
|
|
147
148
|
let inside = 0;
|
|
@@ -645,6 +646,245 @@ function sameTuplePolygon(current, next) {
|
|
|
645
646
|
return (current.length === next.length &&
|
|
646
647
|
current.every((point, index) => point[0] === next[index]?.[0] && point[1] === next[index]?.[1]));
|
|
647
648
|
}
|
|
649
|
+
function sameTuplePolygonRotation(current, next) {
|
|
650
|
+
if (sameTuplePolygon(current, next))
|
|
651
|
+
return true;
|
|
652
|
+
if (current.length !== next.length)
|
|
653
|
+
return false;
|
|
654
|
+
// Preserve the stored ring when extraction only changes its starting vertex.
|
|
655
|
+
return next.some((_, offset) => current.every((point, index) => {
|
|
656
|
+
const candidate = next[(index + offset) % next.length];
|
|
657
|
+
return point[0] === candidate[0] && point[1] === candidate[1];
|
|
658
|
+
}));
|
|
659
|
+
}
|
|
660
|
+
function sameTuplePolygons(current, next) {
|
|
661
|
+
return (current.length === next.length &&
|
|
662
|
+
current.every((polygon, index) => {
|
|
663
|
+
const nextPolygon = next[index];
|
|
664
|
+
return nextPolygon ? sameTuplePolygon(polygon, nextPolygon) : false;
|
|
665
|
+
}));
|
|
666
|
+
}
|
|
667
|
+
function crossProduct(a, b, c) {
|
|
668
|
+
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
669
|
+
}
|
|
670
|
+
function lineIntersection(start, end, clipStart, clipEnd) {
|
|
671
|
+
const segment = { x: end.x - start.x, y: end.y - start.y };
|
|
672
|
+
const clip = { x: clipEnd.x - clipStart.x, y: clipEnd.y - clipStart.y };
|
|
673
|
+
const denominator = segment.x * clip.y - segment.y * clip.x;
|
|
674
|
+
if (Math.abs(denominator) < 1e-9)
|
|
675
|
+
return end;
|
|
676
|
+
const offset = { x: clipStart.x - start.x, y: clipStart.y - start.y };
|
|
677
|
+
const t = (offset.x * clip.y - offset.y * clip.x) / denominator;
|
|
678
|
+
return { x: start.x + segment.x * t, y: start.y + segment.y * t };
|
|
679
|
+
}
|
|
680
|
+
function clipPolygonToConvex(subject, clipPolygon) {
|
|
681
|
+
if (subject.length < 3 || clipPolygon.length < 3)
|
|
682
|
+
return [];
|
|
683
|
+
const orientation = polygonArea(clipPolygon) >= 0 ? 1 : -1;
|
|
684
|
+
let output = [...subject];
|
|
685
|
+
for (let index = 0; index < clipPolygon.length; index += 1) {
|
|
686
|
+
const clipStart = clipPolygon[index];
|
|
687
|
+
const clipEnd = clipPolygon[(index + 1) % clipPolygon.length];
|
|
688
|
+
const input = output;
|
|
689
|
+
output = [];
|
|
690
|
+
if (input.length === 0)
|
|
691
|
+
break;
|
|
692
|
+
let previous = input[input.length - 1];
|
|
693
|
+
let previousInside = orientation * crossProduct(clipStart, clipEnd, previous) >= -1e-8;
|
|
694
|
+
for (const current of input) {
|
|
695
|
+
const currentInside = orientation * crossProduct(clipStart, clipEnd, current) >= -1e-8;
|
|
696
|
+
if (currentInside !== previousInside) {
|
|
697
|
+
output.push(lineIntersection(previous, current, clipStart, clipEnd));
|
|
698
|
+
}
|
|
699
|
+
if (currentInside)
|
|
700
|
+
output.push(current);
|
|
701
|
+
previous = current;
|
|
702
|
+
previousInside = currentInside;
|
|
703
|
+
}
|
|
704
|
+
output = dedupeSequentialPoints(output, 1e-7);
|
|
705
|
+
}
|
|
706
|
+
return output.length >= 3 && Math.abs(polygonArea(output)) > 1e-8 ? output : [];
|
|
707
|
+
}
|
|
708
|
+
function isConvexPolygon(polygon) {
|
|
709
|
+
let direction = 0;
|
|
710
|
+
for (let index = 0; index < polygon.length; index += 1) {
|
|
711
|
+
const cross = crossProduct(polygon[index], polygon[(index + 1) % polygon.length], polygon[(index + 2) % polygon.length]);
|
|
712
|
+
if (Math.abs(cross) < 1e-8)
|
|
713
|
+
continue;
|
|
714
|
+
const nextDirection = Math.sign(cross);
|
|
715
|
+
if (direction !== 0 && nextDirection !== direction)
|
|
716
|
+
return false;
|
|
717
|
+
direction = nextDirection;
|
|
718
|
+
}
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
function pointInTriangle(point, a, b, c) {
|
|
722
|
+
return (crossProduct(a, b, point) >= -1e-8 &&
|
|
723
|
+
crossProduct(b, c, point) >= -1e-8 &&
|
|
724
|
+
crossProduct(c, a, point) >= -1e-8);
|
|
725
|
+
}
|
|
726
|
+
function triangulatePolygon(polygon) {
|
|
727
|
+
const points = polygonArea(polygon) >= 0 ? [...polygon] : [...polygon].reverse();
|
|
728
|
+
const indices = points.map((_, index) => index);
|
|
729
|
+
const triangles = [];
|
|
730
|
+
let attempts = 0;
|
|
731
|
+
while (indices.length > 3 && attempts < points.length * points.length) {
|
|
732
|
+
let clippedEar = false;
|
|
733
|
+
for (let index = 0; index < indices.length; index += 1) {
|
|
734
|
+
const previousIndex = indices[(index - 1 + indices.length) % indices.length];
|
|
735
|
+
const currentIndex = indices[index];
|
|
736
|
+
const nextIndex = indices[(index + 1) % indices.length];
|
|
737
|
+
const previous = points[previousIndex];
|
|
738
|
+
const current = points[currentIndex];
|
|
739
|
+
const next = points[nextIndex];
|
|
740
|
+
if (crossProduct(previous, current, next) <= 1e-8)
|
|
741
|
+
continue;
|
|
742
|
+
if (indices.some((candidateIndex) => candidateIndex !== previousIndex &&
|
|
743
|
+
candidateIndex !== currentIndex &&
|
|
744
|
+
candidateIndex !== nextIndex &&
|
|
745
|
+
pointInTriangle(points[candidateIndex], previous, current, next))) {
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
triangles.push([previous, current, next]);
|
|
749
|
+
indices.splice(index, 1);
|
|
750
|
+
clippedEar = true;
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
if (!clippedEar)
|
|
754
|
+
break;
|
|
755
|
+
attempts += 1;
|
|
756
|
+
}
|
|
757
|
+
if (indices.length === 3)
|
|
758
|
+
triangles.push(indices.map((index) => points[index]));
|
|
759
|
+
return triangles;
|
|
760
|
+
}
|
|
761
|
+
function clipOpeningToRoom(opening, room) {
|
|
762
|
+
const openingIsInside = opening.every((point) => pointInPolygon(point, room) || pointDistanceToPolygonBoundary(point, room) <= 1e-7);
|
|
763
|
+
if (openingIsInside)
|
|
764
|
+
return [opening];
|
|
765
|
+
const clipRegions = isConvexPolygon(room) ? [room] : triangulatePolygon(room);
|
|
766
|
+
return clipRegions
|
|
767
|
+
.map((region) => clipPolygonToConvex(opening, region))
|
|
768
|
+
.filter((polygon) => polygon.length >= 3);
|
|
769
|
+
}
|
|
770
|
+
function partitionSurfaceOpenings(surface, roomIndices, detected) {
|
|
771
|
+
const assignments = new Map();
|
|
772
|
+
for (const roomIndex of roomIndices) {
|
|
773
|
+
assignments.set(roomIndex, { holes: [], holeMetadata: [] });
|
|
774
|
+
}
|
|
775
|
+
surface.holes.forEach((hole, holeIndex) => {
|
|
776
|
+
const holePolygon = hole.map(pointFromTuple);
|
|
777
|
+
for (const roomIndex of roomIndices) {
|
|
778
|
+
const room = detected[roomIndex];
|
|
779
|
+
const assignment = assignments.get(roomIndex);
|
|
780
|
+
if (!(room && assignment))
|
|
781
|
+
continue;
|
|
782
|
+
for (const clipped of clipOpeningToRoom(holePolygon, room.poly)) {
|
|
783
|
+
assignment.holes.push(clipped.map(pointToTuple));
|
|
784
|
+
assignment.holeMetadata.push(surface.holeMetadata[holeIndex] ?? { source: 'manual' });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
return assignments;
|
|
789
|
+
}
|
|
790
|
+
function partitionCeilingChildren(ceiling, roomIndices, detected, fallbackRoomIndex, childPosition) {
|
|
791
|
+
const assignments = new Map();
|
|
792
|
+
for (const roomIndex of roomIndices)
|
|
793
|
+
assignments.set(roomIndex, []);
|
|
794
|
+
for (const childId of ceiling.children) {
|
|
795
|
+
const tuple = childPosition?.(childId);
|
|
796
|
+
const point = tuple ? pointFromTuple(tuple) : undefined;
|
|
797
|
+
const boundaryRoomIndices = point
|
|
798
|
+
? roomIndices.filter((roomIndex) => {
|
|
799
|
+
const room = detected[roomIndex];
|
|
800
|
+
return room ? pointDistanceToPolygonBoundary(point, room.poly) <= 1e-7 : false;
|
|
801
|
+
})
|
|
802
|
+
: [];
|
|
803
|
+
const interiorRoomIndex = point && boundaryRoomIndices.length === 0
|
|
804
|
+
? roomIndices.find((roomIndex) => {
|
|
805
|
+
const room = detected[roomIndex];
|
|
806
|
+
return room ? pointInPolygon(point, room.poly) : false;
|
|
807
|
+
})
|
|
808
|
+
: undefined;
|
|
809
|
+
const roomIndex = interiorRoomIndex ??
|
|
810
|
+
(fallbackRoomIndex !== undefined && boundaryRoomIndices.includes(fallbackRoomIndex)
|
|
811
|
+
? fallbackRoomIndex
|
|
812
|
+
: boundaryRoomIndices[0]) ??
|
|
813
|
+
fallbackRoomIndex ??
|
|
814
|
+
roomIndices[0];
|
|
815
|
+
if (roomIndex !== undefined)
|
|
816
|
+
assignments.get(roomIndex)?.push(childId);
|
|
817
|
+
}
|
|
818
|
+
return assignments;
|
|
819
|
+
}
|
|
820
|
+
function sameHoleMetadata(current, next) {
|
|
821
|
+
return (current.length === next.length &&
|
|
822
|
+
current.every((metadata, index) => {
|
|
823
|
+
const candidate = next[index];
|
|
824
|
+
return (candidate?.source === metadata.source &&
|
|
825
|
+
candidate.stairId === metadata.stairId &&
|
|
826
|
+
candidate.elevatorId === metadata.elevatorId);
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
function mergedSurfaceOpenings(surfaces) {
|
|
830
|
+
const holes = [];
|
|
831
|
+
const holeMetadata = [];
|
|
832
|
+
const seen = new Set();
|
|
833
|
+
for (const surface of surfaces) {
|
|
834
|
+
surface.holes.forEach((hole, index) => {
|
|
835
|
+
const metadata = surface.holeMetadata[index] ?? { source: 'manual' };
|
|
836
|
+
const key = JSON.stringify([hole, metadata]);
|
|
837
|
+
if (seen.has(key))
|
|
838
|
+
return;
|
|
839
|
+
seen.add(key);
|
|
840
|
+
holes.push(hole);
|
|
841
|
+
holeMetadata.push(metadata);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return { holes, holeMetadata };
|
|
845
|
+
}
|
|
846
|
+
function ceilingMergeSettingsSignature(ceiling) {
|
|
847
|
+
return JSON.stringify([
|
|
848
|
+
ceiling.height ?? null,
|
|
849
|
+
ceiling.material ?? null,
|
|
850
|
+
ceiling.materialPreset ?? null,
|
|
851
|
+
ceiling.slots ?? null,
|
|
852
|
+
ceiling.visible,
|
|
853
|
+
]);
|
|
854
|
+
}
|
|
855
|
+
function slabMergeSettingsSignature(slab) {
|
|
856
|
+
return JSON.stringify([
|
|
857
|
+
slab.elevation,
|
|
858
|
+
slab.thickness,
|
|
859
|
+
slab.recessed,
|
|
860
|
+
slab.recessedRimElevation ?? null,
|
|
861
|
+
slab.fillToTerrain ?? null,
|
|
862
|
+
slab.material ?? null,
|
|
863
|
+
slab.materialPreset ?? null,
|
|
864
|
+
slab.slots ?? null,
|
|
865
|
+
slab.visible,
|
|
866
|
+
]);
|
|
867
|
+
}
|
|
868
|
+
function slabElevationForReconciledRoom(source, polygon, context) {
|
|
869
|
+
const currentDerived = context.elevationForRoom?.(polygon);
|
|
870
|
+
if (!context.previousElevationForRoom)
|
|
871
|
+
return currentDerived ?? source.elevation;
|
|
872
|
+
const previousDerived = context.previousElevationForRoom(source.polygon);
|
|
873
|
+
const sourceWasDerived = previousDerived !== undefined &&
|
|
874
|
+
Math.abs(source.elevation - previousDerived) <= ROOM_VERTICAL_PLANE_EPSILON;
|
|
875
|
+
return sourceWasDerived ? (currentDerived ?? source.elevation) : source.elevation;
|
|
876
|
+
}
|
|
877
|
+
function ceilingHeightForReconciledRoom(source, polygon, context) {
|
|
878
|
+
if (source.height === undefined)
|
|
879
|
+
return undefined;
|
|
880
|
+
const currentDerived = context.heightForRoom?.(polygon);
|
|
881
|
+
if (!context.previousHeightForRoom)
|
|
882
|
+
return currentDerived ?? source.height;
|
|
883
|
+
const previousDerived = context.previousHeightForRoom(source.polygon);
|
|
884
|
+
const sourceWasDerived = previousDerived !== undefined &&
|
|
885
|
+
Math.abs(source.height - previousDerived) <= ROOM_VERTICAL_PLANE_EPSILON;
|
|
886
|
+
return sourceWasDerived ? (currentDerived ?? source.height) : source.height;
|
|
887
|
+
}
|
|
648
888
|
function wallGeometrySignature(wall, nodes, levelId) {
|
|
649
889
|
return [
|
|
650
890
|
wall.id,
|
|
@@ -771,53 +1011,166 @@ function buildSpace(levelId, room) {
|
|
|
771
1011
|
isExterior: false,
|
|
772
1012
|
};
|
|
773
1013
|
}
|
|
774
|
-
function
|
|
775
|
-
|
|
1014
|
+
export function surfaceTouchesRooms(surface, rooms) {
|
|
1015
|
+
const { polygon, bbox } = surface;
|
|
1016
|
+
return rooms.some((room) => {
|
|
1017
|
+
if (bbox.maxX < room.bbox.minX ||
|
|
1018
|
+
room.bbox.maxX < bbox.minX ||
|
|
1019
|
+
bbox.maxY < room.bbox.minY ||
|
|
1020
|
+
room.bbox.maxY < bbox.minY) {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
return (polygonCoverageRatio(polygon, [room.polygon], bbox) > 0 ||
|
|
1024
|
+
polygonCoverageRatio(room.polygon, [polygon], room.bbox) > 0);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
function roomsAreRelated(beforeRoom, currentRoom) {
|
|
1028
|
+
const beforeIds = new Set(beforeRoom.boundaryFaces.map((boundary) => boundary.wallId));
|
|
1029
|
+
const currentIds = new Set(currentRoom.boundaryFaces.map((boundary) => boundary.wallId));
|
|
1030
|
+
const sharedWallCount = [...currentIds].filter((wallId) => beforeIds.has(wallId)).length;
|
|
1031
|
+
const smallerBoundarySize = Math.min(beforeIds.size, currentIds.size);
|
|
1032
|
+
if (sharedWallCount >= 2 && sharedWallCount >= Math.ceil(smallerBoundarySize / 2))
|
|
1033
|
+
return true;
|
|
1034
|
+
if (bboxOverlapArea(bboxOf(beforeRoom.polygon), bboxOf(currentRoom.polygon)) <= 1e-6)
|
|
776
1035
|
return false;
|
|
777
|
-
|
|
778
|
-
|
|
1036
|
+
return (polygonCoverageRatio(beforeRoom.polygon, [currentRoom.polygon]) > 0 ||
|
|
1037
|
+
polygonCoverageRatio(currentRoom.polygon, [beforeRoom.polygon]) > 0);
|
|
779
1038
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1039
|
+
function roomHasAutoSurface(room, surfaces) {
|
|
1040
|
+
return matchesManualFootprint(room.polygon, surfaces
|
|
1041
|
+
.filter((surface) => surface.autoFromWalls)
|
|
1042
|
+
.map((surface) => surface.polygon.map(pointFromTuple)));
|
|
1043
|
+
}
|
|
1044
|
+
function roomsEligibleForAutoSurface(beforeRooms, currentRooms, currentSurfaces) {
|
|
1045
|
+
return currentRooms.filter((currentRoom) => {
|
|
1046
|
+
const related = beforeRooms.flatMap((beforeRoom) => {
|
|
1047
|
+
if (!roomsAreRelated(beforeRoom, currentRoom))
|
|
1048
|
+
return [];
|
|
1049
|
+
return [
|
|
1050
|
+
{
|
|
1051
|
+
room: beforeRoom,
|
|
1052
|
+
coverage: polygonCoverageRatio(currentRoom.polygon, [beforeRoom.polygon]),
|
|
1053
|
+
},
|
|
1054
|
+
];
|
|
1055
|
+
});
|
|
1056
|
+
const maxCoverage = Math.max(0, ...related.map(({ coverage }) => coverage));
|
|
1057
|
+
const predecessors = currentRooms.length >= beforeRooms.length && maxCoverage > 0
|
|
1058
|
+
? related.filter(({ coverage }) => coverage >= maxCoverage - 1e-6).map(({ room }) => room)
|
|
1059
|
+
: related.map(({ room }) => room);
|
|
1060
|
+
if (predecessors.length === 0)
|
|
1061
|
+
return true;
|
|
1062
|
+
return predecessors.every((beforeRoom) => roomHasAutoSurface(beforeRoom, currentSurfaces));
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
function detectedRoomsByLevel(nodes) {
|
|
1066
|
+
const wallsByLevel = new Map();
|
|
1067
|
+
for (const node of Object.values(nodes)) {
|
|
1068
|
+
if (node?.type !== 'wall' || !node.parentId)
|
|
788
1069
|
continue;
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1070
|
+
const walls = wallsByLevel.get(node.parentId) ?? [];
|
|
1071
|
+
walls.push(node);
|
|
1072
|
+
wallsByLevel.set(node.parentId, walls);
|
|
1073
|
+
}
|
|
1074
|
+
return new Map([...wallsByLevel].map(([levelId, walls]) => [levelId, extractRooms(walls)]));
|
|
1075
|
+
}
|
|
1076
|
+
function levelChildren(nodes, levelId) {
|
|
1077
|
+
const level = nodes[levelId];
|
|
1078
|
+
if (level?.type !== 'level')
|
|
1079
|
+
return [];
|
|
1080
|
+
return level.children.flatMap((id) => {
|
|
1081
|
+
const node = nodes[id];
|
|
1082
|
+
return node ? [node] : [];
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
function changedWallIdsByLevel(before, current, candidateIds) {
|
|
1086
|
+
const changes = new Map();
|
|
1087
|
+
const wallIds = new Set(candidateIds);
|
|
1088
|
+
if (!candidateIds) {
|
|
1089
|
+
for (const node of Object.values(before)) {
|
|
1090
|
+
if (node?.type === 'wall')
|
|
1091
|
+
wallIds.add(node.id);
|
|
794
1092
|
}
|
|
795
|
-
|
|
796
|
-
|
|
1093
|
+
for (const node of Object.values(current)) {
|
|
1094
|
+
if (node?.type === 'wall')
|
|
1095
|
+
wallIds.add(node.id);
|
|
797
1096
|
}
|
|
798
|
-
if (Object.keys(data).length > 0)
|
|
799
|
-
update.push({ id: zone.id, data });
|
|
800
1097
|
}
|
|
801
|
-
|
|
1098
|
+
const markChanged = (levelId, wallId) => {
|
|
1099
|
+
if (!levelId)
|
|
1100
|
+
return;
|
|
1101
|
+
const ids = changes.get(levelId) ?? new Set();
|
|
1102
|
+
ids.add(wallId);
|
|
1103
|
+
changes.set(levelId, ids);
|
|
1104
|
+
};
|
|
1105
|
+
for (const wallId of wallIds) {
|
|
1106
|
+
const previous = before[wallId]?.type === 'wall' ? before[wallId] : null;
|
|
1107
|
+
const next = current[wallId]?.type === 'wall' ? current[wallId] : null;
|
|
1108
|
+
if (previous === next)
|
|
1109
|
+
continue;
|
|
1110
|
+
markChanged(previous?.parentId, wallId);
|
|
1111
|
+
markChanged(next?.parentId, wallId);
|
|
1112
|
+
}
|
|
1113
|
+
return changes;
|
|
802
1114
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1115
|
+
function descendantLevelIds(nodes, rootId) {
|
|
1116
|
+
const levelIds = new Set();
|
|
1117
|
+
const queue = [rootId];
|
|
1118
|
+
const visited = new Set();
|
|
1119
|
+
while (queue.length > 0) {
|
|
1120
|
+
const id = queue.pop();
|
|
1121
|
+
if (visited.has(id))
|
|
1122
|
+
continue;
|
|
1123
|
+
visited.add(id);
|
|
1124
|
+
const node = nodes[id];
|
|
1125
|
+
if (!node)
|
|
1126
|
+
continue;
|
|
1127
|
+
if (node.type === 'level')
|
|
1128
|
+
levelIds.add(node.id);
|
|
1129
|
+
if ('children' in node && Array.isArray(node.children))
|
|
1130
|
+
queue.push(...node.children);
|
|
1131
|
+
}
|
|
1132
|
+
return levelIds;
|
|
1133
|
+
}
|
|
1134
|
+
function fallbackLevelIdsForCandidates(before, current, candidateIds) {
|
|
1135
|
+
const levelIds = new Set();
|
|
1136
|
+
const addLevelAndLower = (levelId, nodes) => {
|
|
1137
|
+
if (!levelId)
|
|
1138
|
+
return;
|
|
1139
|
+
levelIds.add(levelId);
|
|
1140
|
+
const lower = getLevelBelow(levelId, nodes);
|
|
1141
|
+
if (lower)
|
|
1142
|
+
levelIds.add(lower.id);
|
|
1143
|
+
};
|
|
1144
|
+
for (const id of candidateIds) {
|
|
1145
|
+
for (const nodes of [before, current]) {
|
|
1146
|
+
const node = nodes[id];
|
|
1147
|
+
if (!node)
|
|
1148
|
+
continue;
|
|
1149
|
+
if (node.type === 'level' || node.type === 'building' || node.type === 'site') {
|
|
1150
|
+
for (const levelId of descendantLevelIds(nodes, node.id))
|
|
1151
|
+
levelIds.add(levelId);
|
|
1152
|
+
}
|
|
1153
|
+
else if (node.type === 'slab') {
|
|
1154
|
+
addLevelAndLower(node.parentId, nodes);
|
|
1155
|
+
}
|
|
1156
|
+
else if (node.type === 'zone') {
|
|
1157
|
+
if (node.parentId)
|
|
1158
|
+
levelIds.add(node.parentId);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return levelIds;
|
|
816
1163
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const
|
|
1164
|
+
function sameStringSet(a, b) {
|
|
1165
|
+
if (a.length !== b.length)
|
|
1166
|
+
return false;
|
|
1167
|
+
const right = new Set(b);
|
|
1168
|
+
return a.every((value) => right.has(value));
|
|
1169
|
+
}
|
|
1170
|
+
function matchAutoSurfaces(roomPolygons, existingSurfaces, mergeSettingsSignature) {
|
|
1171
|
+
const manualSurfaces = existingSurfaces.filter((surface) => !surface.autoFromWalls);
|
|
1172
|
+
const manualSignatures = new Set(manualSurfaces.map((surface) => polygonSignature(surface.polygon.map(pointFromTuple))));
|
|
1173
|
+
const manualPolygons = manualSurfaces.map((surface) => surface.polygon.map(pointFromTuple));
|
|
821
1174
|
const detectedAll = roomPolygons
|
|
822
1175
|
.map((poly) => ({
|
|
823
1176
|
poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(pointFromTuple),
|
|
@@ -834,86 +1187,185 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {})
|
|
|
834
1187
|
bbox: bboxOf(room.poly),
|
|
835
1188
|
}));
|
|
836
1189
|
const detected = detectedAll.filter(({ sig, poly }) => !manualSignatures.has(sig) && !matchesManualFootprint(poly, manualPolygons));
|
|
837
|
-
const existingAuto =
|
|
838
|
-
const
|
|
839
|
-
const poly =
|
|
1190
|
+
const existingAuto = existingSurfaces.filter((surface) => surface.autoFromWalls);
|
|
1191
|
+
const metadata = existingAuto.map((surface) => {
|
|
1192
|
+
const poly = surface.polygon.map(pointFromTuple);
|
|
840
1193
|
return {
|
|
841
|
-
|
|
1194
|
+
surface,
|
|
842
1195
|
sig: polygonSignature(poly),
|
|
843
1196
|
centroid: polygonCentroid(poly),
|
|
844
1197
|
area: Math.abs(polygonArea(poly)),
|
|
845
1198
|
bbox: bboxOf(poly),
|
|
846
1199
|
};
|
|
847
1200
|
});
|
|
848
|
-
const
|
|
849
|
-
const
|
|
850
|
-
const
|
|
1201
|
+
const conflictingSurfaceIds = new Set();
|
|
1202
|
+
const conflictingRoomIndices = new Set();
|
|
1203
|
+
const compatibleMergesByRoomIndex = new Map();
|
|
1204
|
+
detected.forEach((room, roomIndex) => {
|
|
1205
|
+
const contributors = existingAuto.filter((surface) => polygonCoverageRatio(surface.polygon.map(pointFromTuple), [room.poly]) >=
|
|
1206
|
+
ORPHAN_MERGE_COVERAGE_THRESHOLD);
|
|
1207
|
+
if (contributors.length < 2)
|
|
1208
|
+
return;
|
|
1209
|
+
if (new Set(contributors.map(mergeSettingsSignature)).size > 1) {
|
|
1210
|
+
conflictingRoomIndices.add(roomIndex);
|
|
1211
|
+
for (const surface of contributors)
|
|
1212
|
+
conflictingSurfaceIds.add(surface.id);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
compatibleMergesByRoomIndex.set(roomIndex, contributors);
|
|
1216
|
+
});
|
|
1217
|
+
const matchedSurfaceIds = new Set();
|
|
1218
|
+
const matchedDetectedIndices = new Set();
|
|
1219
|
+
const roomIndexBySurfaceId = new Map();
|
|
1220
|
+
const sourceSurfaceIdByRoomIndex = new Map();
|
|
1221
|
+
const polygonBySurfaceId = new Map();
|
|
851
1222
|
const autoBySignature = new Map();
|
|
852
|
-
for (const entry of
|
|
1223
|
+
for (const entry of metadata) {
|
|
853
1224
|
const bucket = autoBySignature.get(entry.sig) ?? [];
|
|
854
1225
|
bucket.push(entry);
|
|
855
1226
|
autoBySignature.set(entry.sig, bucket);
|
|
856
1227
|
}
|
|
857
1228
|
detected.forEach((room, index) => {
|
|
1229
|
+
if (conflictingRoomIndices.has(index)) {
|
|
1230
|
+
matchedDetectedIndices.add(index);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
858
1233
|
const existing = autoBySignature.get(room.sig)?.shift();
|
|
859
1234
|
if (!existing)
|
|
860
1235
|
return;
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
elevation: context.elevationForRoom?.(polygon),
|
|
867
|
-
});
|
|
1236
|
+
matchedDetectedIndices.add(index);
|
|
1237
|
+
matchedSurfaceIds.add(existing.surface.id);
|
|
1238
|
+
roomIndexBySurfaceId.set(existing.surface.id, index);
|
|
1239
|
+
sourceSurfaceIdByRoomIndex.set(index, existing.surface.id);
|
|
1240
|
+
polygonBySurfaceId.set(existing.surface.id, room.poly.map(pointToTuple));
|
|
868
1241
|
});
|
|
869
1242
|
const remainingDetected = detected
|
|
870
1243
|
.map((room, index) => ({ room, index }))
|
|
871
|
-
.filter(({ index }) => !
|
|
872
|
-
.sort((
|
|
873
|
-
const remainingAuto =
|
|
1244
|
+
.filter(({ index }) => !matchedDetectedIndices.has(index))
|
|
1245
|
+
.sort((left, right) => right.room.area - left.room.area);
|
|
1246
|
+
const remainingAuto = metadata.filter((entry) => !matchedSurfaceIds.has(entry.surface.id));
|
|
874
1247
|
for (const { room, index } of remainingDetected) {
|
|
875
1248
|
let bestMatch = null;
|
|
876
1249
|
for (const entry of remainingAuto) {
|
|
877
|
-
if (
|
|
1250
|
+
if (matchedSurfaceIds.has(entry.surface.id))
|
|
878
1251
|
continue;
|
|
879
|
-
const
|
|
880
|
-
const dy = room.centroid.y - entry.centroid.y;
|
|
881
|
-
const dist = Math.hypot(dx, dy);
|
|
1252
|
+
const distance = Math.hypot(room.centroid.x - entry.centroid.x, room.centroid.y - entry.centroid.y);
|
|
882
1253
|
const areaRatio = entry.area > 1e-6 ? room.area / entry.area : 999;
|
|
883
1254
|
const areaPenalty = Math.abs(Math.log(Math.max(1e-6, areaRatio)));
|
|
884
|
-
|
|
885
|
-
if (overlap <= 0.0001 && dist > 1.5)
|
|
1255
|
+
if (bboxOverlapArea(room.bbox, entry.bbox) <= 0.0001 && distance > 1.5)
|
|
886
1256
|
continue;
|
|
887
|
-
const score =
|
|
888
|
-
if (!bestMatch || score < bestMatch.score)
|
|
1257
|
+
const score = distance + areaPenalty * 0.35;
|
|
1258
|
+
if (!bestMatch || score < bestMatch.score)
|
|
889
1259
|
bestMatch = { entry, score };
|
|
890
|
-
}
|
|
891
1260
|
}
|
|
892
1261
|
if (!bestMatch)
|
|
893
1262
|
continue;
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
elevation: context.elevationForRoom?.(polygon),
|
|
900
|
-
});
|
|
1263
|
+
matchedDetectedIndices.add(index);
|
|
1264
|
+
matchedSurfaceIds.add(bestMatch.entry.surface.id);
|
|
1265
|
+
roomIndexBySurfaceId.set(bestMatch.entry.surface.id, index);
|
|
1266
|
+
sourceSurfaceIdByRoomIndex.set(index, bestMatch.entry.surface.id);
|
|
1267
|
+
polygonBySurfaceId.set(bestMatch.entry.surface.id, room.poly.map(pointToTuple));
|
|
901
1268
|
}
|
|
1269
|
+
detected.forEach((room, index) => {
|
|
1270
|
+
if (sourceSurfaceIdByRoomIndex.has(index))
|
|
1271
|
+
return;
|
|
1272
|
+
let bestSource = null;
|
|
1273
|
+
for (const entry of metadata) {
|
|
1274
|
+
const coverage = polygonCoverageRatio(room.poly, [entry.surface.polygon.map(pointFromTuple)]);
|
|
1275
|
+
if (coverage <= 0 || (bestSource && coverage <= bestSource.coverage))
|
|
1276
|
+
continue;
|
|
1277
|
+
bestSource = { id: entry.surface.id, coverage };
|
|
1278
|
+
}
|
|
1279
|
+
if (bestSource)
|
|
1280
|
+
sourceSurfaceIdByRoomIndex.set(index, bestSource.id);
|
|
1281
|
+
});
|
|
902
1282
|
const detectedRoomPolygons = detectedAll.map((room) => room.poly);
|
|
903
|
-
const
|
|
904
|
-
const
|
|
905
|
-
for (const
|
|
906
|
-
if (
|
|
1283
|
+
const deleted = [];
|
|
1284
|
+
const demote = [];
|
|
1285
|
+
for (const surface of existingAuto) {
|
|
1286
|
+
if (polygonBySurfaceId.has(surface.id))
|
|
1287
|
+
continue;
|
|
1288
|
+
if (conflictingSurfaceIds.has(surface.id)) {
|
|
1289
|
+
demote.push({ id: surface.id, data: { autoFromWalls: false } });
|
|
907
1290
|
continue;
|
|
908
|
-
const coverage = polygonCoverageRatio(slab.polygon.map(pointFromTuple), detectedRoomPolygons);
|
|
909
|
-
if (coverage >= ORPHAN_MERGE_COVERAGE_THRESHOLD) {
|
|
910
|
-
slabsToDelete.push(slab.id);
|
|
911
1291
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1292
|
+
const coverage = polygonCoverageRatio(surface.polygon.map(pointFromTuple), detectedRoomPolygons);
|
|
1293
|
+
if (coverage >= ORPHAN_MERGE_COVERAGE_THRESHOLD)
|
|
1294
|
+
deleted.push(surface.id);
|
|
1295
|
+
else
|
|
1296
|
+
demote.push({ id: surface.id, data: { autoFromWalls: false } });
|
|
1297
|
+
}
|
|
1298
|
+
return {
|
|
1299
|
+
detectedAll,
|
|
1300
|
+
detected,
|
|
1301
|
+
existingAuto,
|
|
1302
|
+
compatibleMergesByRoomIndex,
|
|
1303
|
+
matchedDetectedIndices,
|
|
1304
|
+
roomIndexBySurfaceId,
|
|
1305
|
+
sourceSurfaceIdByRoomIndex,
|
|
1306
|
+
polygonBySurfaceId,
|
|
1307
|
+
delete: deleted,
|
|
1308
|
+
demote,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
export function planAutoZonesForLevel(spaces, existingZones) {
|
|
1312
|
+
const update = [];
|
|
1313
|
+
for (const zone of existingZones) {
|
|
1314
|
+
const storedSignature = polygonSignature(zone.polygon.map(pointFromTuple));
|
|
1315
|
+
const matchingSpace = zone.autoFromWalls && zone.boundaryWallIds.length >= 3
|
|
1316
|
+
? spaces.find((space) => sameStringSet(space.wallIds, zone.boundaryWallIds))
|
|
1317
|
+
: spaces.find((space) => polygonSignature(space.polygon.map(pointFromTuple)) === storedSignature);
|
|
1318
|
+
if (!matchingSpace)
|
|
1319
|
+
continue;
|
|
1320
|
+
const data = {};
|
|
1321
|
+
if (!zone.autoFromWalls)
|
|
1322
|
+
data.autoFromWalls = true;
|
|
1323
|
+
if (!sameStringSet(zone.boundaryWallIds, matchingSpace.wallIds)) {
|
|
1324
|
+
data.boundaryWallIds = matchingSpace.wallIds;
|
|
1325
|
+
}
|
|
1326
|
+
if (!sameTuplePolygon(zone.polygon, matchingSpace.polygon)) {
|
|
1327
|
+
data.polygon = matchingSpace.polygon;
|
|
916
1328
|
}
|
|
1329
|
+
if (Object.keys(data).length > 0)
|
|
1330
|
+
update.push({ id: zone.id, data });
|
|
1331
|
+
}
|
|
1332
|
+
return { update };
|
|
1333
|
+
}
|
|
1334
|
+
export function resolveAutoZonePolygon(zone, resolve) {
|
|
1335
|
+
if (!zone.autoFromWalls || zone.boundaryWallIds.length < 3)
|
|
1336
|
+
return zone.polygon;
|
|
1337
|
+
const walls = zone.boundaryWallIds.flatMap((id) => {
|
|
1338
|
+
const node = resolve(id);
|
|
1339
|
+
return node && typeof node === 'object' && 'type' in node && node.type === 'wall'
|
|
1340
|
+
? [node]
|
|
1341
|
+
: [];
|
|
1342
|
+
});
|
|
1343
|
+
if (walls.length !== zone.boundaryWallIds.length)
|
|
1344
|
+
return zone.polygon;
|
|
1345
|
+
const room = extractRooms(walls).find((candidate) => sameStringSet([...new Set(candidate.boundaryFaces.map((boundary) => boundary.wallId))], zone.boundaryWallIds));
|
|
1346
|
+
return room ? room.polygon.map(pointToTuple) : zone.polygon;
|
|
1347
|
+
}
|
|
1348
|
+
export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {}, namingSlabs = existingSlabs) {
|
|
1349
|
+
const match = matchAutoSurfaces(roomPolygons, existingSlabs, slabMergeSettingsSignature);
|
|
1350
|
+
const { detected, existingAuto, compatibleMergesByRoomIndex: compatibleMergeSlabsByRoomIndex, matchedDetectedIndices: matchedDetectedIdx, roomIndexBySurfaceId: roomIndexBySlabId, sourceSurfaceIdByRoomIndex: sourceSlabIdByRoomIndex, delete: slabsToDelete, demote: slabDemotions, } = match;
|
|
1351
|
+
const updatesById = new Map();
|
|
1352
|
+
for (const slab of existingAuto) {
|
|
1353
|
+
const polygon = match.polygonBySurfaceId.get(slab.id);
|
|
1354
|
+
if (!polygon)
|
|
1355
|
+
continue;
|
|
1356
|
+
updatesById.set(slab.id, {
|
|
1357
|
+
polygon,
|
|
1358
|
+
elevation: slabElevationForReconciledRoom(slab, polygon, context),
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
const openingAssignmentsBySlabId = new Map();
|
|
1362
|
+
for (const slab of existingAuto) {
|
|
1363
|
+
const roomIndices = [...sourceSlabIdByRoomIndex.entries()]
|
|
1364
|
+
.filter(([, slabId]) => slabId === slab.id)
|
|
1365
|
+
.map(([roomIndex]) => roomIndex);
|
|
1366
|
+
if (roomIndices.length === 0)
|
|
1367
|
+
continue;
|
|
1368
|
+
openingAssignmentsBySlabId.set(slab.id, partitionSurfaceOpenings(slab, roomIndices, detected));
|
|
917
1369
|
}
|
|
918
1370
|
const slabsToUpdate = [
|
|
919
1371
|
...existingAuto
|
|
@@ -922,9 +1374,23 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {})
|
|
|
922
1374
|
const update = updatesById.get(slab.id);
|
|
923
1375
|
if (!update)
|
|
924
1376
|
return [];
|
|
1377
|
+
const roomIndex = roomIndexBySlabId.get(slab.id);
|
|
1378
|
+
const openings = roomIndex == null
|
|
1379
|
+
? { holes: slab.holes, holeMetadata: slab.holeMetadata }
|
|
1380
|
+
: compatibleMergeSlabsByRoomIndex.has(roomIndex)
|
|
1381
|
+
? mergedSurfaceOpenings(compatibleMergeSlabsByRoomIndex.get(roomIndex) ?? [])
|
|
1382
|
+
: (openingAssignmentsBySlabId.get(slab.id)?.get(roomIndex) ?? {
|
|
1383
|
+
holes: [],
|
|
1384
|
+
holeMetadata: [],
|
|
1385
|
+
});
|
|
925
1386
|
const data = {};
|
|
926
|
-
if (!
|
|
1387
|
+
if (!sameTuplePolygonRotation(slab.polygon, update.polygon))
|
|
927
1388
|
data.polygon = update.polygon;
|
|
1389
|
+
if (!sameTuplePolygons(slab.holes, openings.holes))
|
|
1390
|
+
data.holes = openings.holes;
|
|
1391
|
+
if (!sameHoleMetadata(slab.holeMetadata, openings.holeMetadata)) {
|
|
1392
|
+
data.holeMetadata = openings.holeMetadata;
|
|
1393
|
+
}
|
|
928
1394
|
if (update.elevation !== undefined &&
|
|
929
1395
|
Math.abs(slab.elevation - update.elevation) > ROOM_VERTICAL_PLANE_EPSILON) {
|
|
930
1396
|
data.elevation = update.elevation;
|
|
@@ -933,7 +1399,7 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {})
|
|
|
933
1399
|
}),
|
|
934
1400
|
...slabDemotions,
|
|
935
1401
|
];
|
|
936
|
-
const plannedSlabsForNaming = [...
|
|
1402
|
+
const plannedSlabsForNaming = [...namingSlabs];
|
|
937
1403
|
const slabsToCreate = [];
|
|
938
1404
|
for (let index = 0; index < detected.length; index += 1) {
|
|
939
1405
|
if (matchedDetectedIdx.has(index))
|
|
@@ -944,14 +1410,28 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {})
|
|
|
944
1410
|
const name = nextAutoRoomName(plannedSlabsForNaming, 'Slab');
|
|
945
1411
|
plannedSlabsForNaming.push({ name });
|
|
946
1412
|
const polygon = room.poly.map(pointToTuple);
|
|
947
|
-
const
|
|
1413
|
+
const sourceId = sourceSlabIdByRoomIndex.get(index);
|
|
1414
|
+
const source = sourceId ? existingAuto.find((slab) => slab.id === sourceId) : undefined;
|
|
1415
|
+
const openings = sourceId ? openingAssignmentsBySlabId.get(sourceId)?.get(index) : undefined;
|
|
1416
|
+
const elevation = source
|
|
1417
|
+
? slabElevationForReconciledRoom(source, polygon, context)
|
|
1418
|
+
: context.elevationForRoom?.(polygon);
|
|
948
1419
|
slabsToCreate.push(SlabNode.parse({
|
|
949
1420
|
name,
|
|
950
1421
|
polygon,
|
|
951
|
-
holes: [],
|
|
1422
|
+
holes: openings?.holes ?? [],
|
|
1423
|
+
holeMetadata: openings?.holeMetadata ?? [],
|
|
952
1424
|
elevation: elevation !== undefined && Number.isFinite(elevation)
|
|
953
1425
|
? elevation
|
|
954
1426
|
: DEFAULT_AUTO_SLAB_ELEVATION,
|
|
1427
|
+
thickness: source?.thickness,
|
|
1428
|
+
recessed: source?.recessed,
|
|
1429
|
+
recessedRimElevation: source?.recessedRimElevation,
|
|
1430
|
+
fillToTerrain: source?.fillToTerrain,
|
|
1431
|
+
material: source?.material,
|
|
1432
|
+
materialPreset: source?.materialPreset,
|
|
1433
|
+
slots: source?.slots,
|
|
1434
|
+
visible: source?.visible,
|
|
955
1435
|
autoFromWalls: true,
|
|
956
1436
|
}));
|
|
957
1437
|
}
|
|
@@ -961,8 +1441,8 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {})
|
|
|
961
1441
|
delete: slabsToDelete,
|
|
962
1442
|
};
|
|
963
1443
|
}
|
|
964
|
-
function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore, context = {}) {
|
|
965
|
-
const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs, context);
|
|
1444
|
+
function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore, context = {}, namingSlabs = existingSlabs) {
|
|
1445
|
+
const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs, context, namingSlabs);
|
|
966
1446
|
if (plan.delete.length > 0) {
|
|
967
1447
|
sceneStore.getState().deleteNodes(plan.delete);
|
|
968
1448
|
}
|
|
@@ -974,105 +1454,20 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore,
|
|
|
974
1454
|
}
|
|
975
1455
|
return plan;
|
|
976
1456
|
}
|
|
977
|
-
export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context = {}) {
|
|
1457
|
+
export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context = {}, namingCeilings = existingCeilings) {
|
|
978
1458
|
const manualCeilings = existingCeilings.filter((ceiling) => !ceiling.autoFromWalls);
|
|
979
|
-
const
|
|
980
|
-
const
|
|
981
|
-
const detectedAll = roomPolygons
|
|
982
|
-
.map((poly) => ({
|
|
983
|
-
poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(pointFromTuple),
|
|
984
|
-
sig: '',
|
|
985
|
-
centroid: { x: 0, y: 0 },
|
|
986
|
-
area: 0,
|
|
987
|
-
bbox: bboxOf([]),
|
|
988
|
-
}))
|
|
989
|
-
.map((room) => ({
|
|
990
|
-
...room,
|
|
991
|
-
sig: polygonSignature(room.poly),
|
|
992
|
-
centroid: polygonCentroid(room.poly),
|
|
993
|
-
area: Math.abs(polygonArea(room.poly)),
|
|
994
|
-
bbox: bboxOf(room.poly),
|
|
995
|
-
}));
|
|
996
|
-
const detected = detectedAll.filter(({ sig, poly }) => !manualSignatures.has(sig) && !matchesManualFootprint(poly, manualPolygons));
|
|
997
|
-
const existingAuto = existingCeilings.filter((ceiling) => ceiling.autoFromWalls);
|
|
998
|
-
const existingAutoMeta = existingAuto.map((ceiling) => {
|
|
999
|
-
const poly = ceiling.polygon.map(pointFromTuple);
|
|
1000
|
-
return {
|
|
1001
|
-
ceiling,
|
|
1002
|
-
sig: polygonSignature(poly),
|
|
1003
|
-
centroid: polygonCentroid(poly),
|
|
1004
|
-
area: Math.abs(polygonArea(poly)),
|
|
1005
|
-
bbox: bboxOf(poly),
|
|
1006
|
-
};
|
|
1007
|
-
});
|
|
1008
|
-
const matchedCeilingIds = new Set();
|
|
1009
|
-
const matchedDetectedIdx = new Set();
|
|
1459
|
+
const match = matchAutoSurfaces(roomPolygons, existingCeilings, ceilingMergeSettingsSignature);
|
|
1460
|
+
const { detected, existingAuto, compatibleMergesByRoomIndex: compatibleMergeCeilingsByRoomIndex, matchedDetectedIndices: matchedDetectedIdx, roomIndexBySurfaceId: roomIndexByCeilingId, sourceSurfaceIdByRoomIndex: sourceCeilingIdByRoomIndex, delete: ceilingsToDelete, demote: ceilingDemotions, } = match;
|
|
1010
1461
|
const updatesById = new Map();
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
bucket.push(entry);
|
|
1015
|
-
autoBySignature.set(entry.sig, bucket);
|
|
1016
|
-
}
|
|
1017
|
-
detected.forEach((room, index) => {
|
|
1018
|
-
const existing = autoBySignature.get(room.sig)?.shift();
|
|
1019
|
-
if (!existing)
|
|
1020
|
-
return;
|
|
1021
|
-
matchedDetectedIdx.add(index);
|
|
1022
|
-
matchedCeilingIds.add(existing.ceiling.id);
|
|
1023
|
-
const polygon = room.poly.map(pointToTuple);
|
|
1024
|
-
updatesById.set(existing.ceiling.id, {
|
|
1025
|
-
polygon,
|
|
1026
|
-
height: context.heightForRoom?.(polygon),
|
|
1027
|
-
});
|
|
1028
|
-
});
|
|
1029
|
-
const remainingDetected = detected
|
|
1030
|
-
.map((room, index) => ({ room, index }))
|
|
1031
|
-
.filter(({ index }) => !matchedDetectedIdx.has(index))
|
|
1032
|
-
.sort((a, b) => b.room.area - a.room.area);
|
|
1033
|
-
const remainingAuto = existingAutoMeta.filter((entry) => !matchedCeilingIds.has(entry.ceiling.id));
|
|
1034
|
-
for (const { room, index } of remainingDetected) {
|
|
1035
|
-
let bestMatch = null;
|
|
1036
|
-
for (const entry of remainingAuto) {
|
|
1037
|
-
if (matchedCeilingIds.has(entry.ceiling.id))
|
|
1038
|
-
continue;
|
|
1039
|
-
const dx = room.centroid.x - entry.centroid.x;
|
|
1040
|
-
const dy = room.centroid.y - entry.centroid.y;
|
|
1041
|
-
const dist = Math.hypot(dx, dy);
|
|
1042
|
-
const areaRatio = entry.area > 1e-6 ? room.area / entry.area : 999;
|
|
1043
|
-
const areaPenalty = Math.abs(Math.log(Math.max(1e-6, areaRatio)));
|
|
1044
|
-
const overlap = bboxOverlapArea(room.bbox, entry.bbox);
|
|
1045
|
-
if (overlap <= 0.0001 && dist > 1.5)
|
|
1046
|
-
continue;
|
|
1047
|
-
const score = dist + areaPenalty * 0.35;
|
|
1048
|
-
if (!bestMatch || score < bestMatch.score) {
|
|
1049
|
-
bestMatch = { entry, score };
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
if (!bestMatch)
|
|
1462
|
+
for (const ceiling of existingAuto) {
|
|
1463
|
+
const polygon = match.polygonBySurfaceId.get(ceiling.id);
|
|
1464
|
+
if (!polygon)
|
|
1053
1465
|
continue;
|
|
1054
|
-
|
|
1055
|
-
matchedCeilingIds.add(bestMatch.entry.ceiling.id);
|
|
1056
|
-
const polygon = room.poly.map(pointToTuple);
|
|
1057
|
-
updatesById.set(bestMatch.entry.ceiling.id, {
|
|
1466
|
+
updatesById.set(ceiling.id, {
|
|
1058
1467
|
polygon,
|
|
1059
|
-
height:
|
|
1468
|
+
height: ceilingHeightForReconciledRoom(ceiling, polygon, context),
|
|
1060
1469
|
});
|
|
1061
1470
|
}
|
|
1062
|
-
const detectedRoomPolygons = detectedAll.map((room) => room.poly);
|
|
1063
|
-
const ceilingsToDelete = [];
|
|
1064
|
-
const ceilingDemotions = [];
|
|
1065
|
-
for (const ceiling of existingAuto) {
|
|
1066
|
-
if (updatesById.has(ceiling.id))
|
|
1067
|
-
continue;
|
|
1068
|
-
const coverage = polygonCoverageRatio(ceiling.polygon.map(pointFromTuple), detectedRoomPolygons);
|
|
1069
|
-
if (coverage >= ORPHAN_MERGE_COVERAGE_THRESHOLD) {
|
|
1070
|
-
ceilingsToDelete.push(ceiling.id);
|
|
1071
|
-
}
|
|
1072
|
-
else {
|
|
1073
|
-
ceilingDemotions.push({ id: ceiling.id, data: { autoFromWalls: false } });
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
1471
|
// Stage 3-B reactive re-clamp (clamp-never-ask): a covering slab
|
|
1077
1472
|
// created, moved, or thickened on the level above can leave an EXISTING
|
|
1078
1473
|
// manual explicit-height ceiling poking into its solid. Clamp explicit
|
|
@@ -1091,6 +1486,18 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
1091
1486
|
? [{ id: ceiling.id, data: { height: bound } }]
|
|
1092
1487
|
: [];
|
|
1093
1488
|
});
|
|
1489
|
+
const openingAssignmentsByCeilingId = new Map();
|
|
1490
|
+
const childAssignmentsByCeilingId = new Map();
|
|
1491
|
+
for (const ceiling of existingAuto) {
|
|
1492
|
+
const roomIndices = [...sourceCeilingIdByRoomIndex.entries()]
|
|
1493
|
+
.filter(([, ceilingId]) => ceilingId === ceiling.id)
|
|
1494
|
+
.map(([roomIndex]) => roomIndex);
|
|
1495
|
+
if (roomIndices.length === 0)
|
|
1496
|
+
continue;
|
|
1497
|
+
openingAssignmentsByCeilingId.set(ceiling.id, partitionSurfaceOpenings(ceiling, roomIndices, detected));
|
|
1498
|
+
childAssignmentsByCeilingId.set(ceiling.id, partitionCeilingChildren(ceiling, roomIndices, detected, roomIndexByCeilingId.get(ceiling.id), context.childPosition));
|
|
1499
|
+
}
|
|
1500
|
+
const childReparents = [];
|
|
1094
1501
|
const ceilingsToUpdate = [
|
|
1095
1502
|
...existingAuto
|
|
1096
1503
|
.filter((ceiling) => updatesById.has(ceiling.id))
|
|
@@ -1098,9 +1505,40 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
1098
1505
|
const update = updatesById.get(ceiling.id);
|
|
1099
1506
|
if (!update)
|
|
1100
1507
|
return [];
|
|
1508
|
+
const roomIndex = roomIndexByCeilingId.get(ceiling.id);
|
|
1509
|
+
const openings = roomIndex == null
|
|
1510
|
+
? { holes: ceiling.holes, holeMetadata: ceiling.holeMetadata }
|
|
1511
|
+
: compatibleMergeCeilingsByRoomIndex.has(roomIndex)
|
|
1512
|
+
? mergedSurfaceOpenings(compatibleMergeCeilingsByRoomIndex.get(roomIndex) ?? [])
|
|
1513
|
+
: (openingAssignmentsByCeilingId.get(ceiling.id)?.get(roomIndex) ?? {
|
|
1514
|
+
holes: [],
|
|
1515
|
+
holeMetadata: [],
|
|
1516
|
+
});
|
|
1101
1517
|
const data = {};
|
|
1102
|
-
if (!
|
|
1518
|
+
if (!sameTuplePolygonRotation(ceiling.polygon, update.polygon))
|
|
1103
1519
|
data.polygon = update.polygon;
|
|
1520
|
+
if (!sameTuplePolygons(ceiling.holes, openings.holes))
|
|
1521
|
+
data.holes = openings.holes;
|
|
1522
|
+
if (!sameHoleMetadata(ceiling.holeMetadata, openings.holeMetadata)) {
|
|
1523
|
+
data.holeMetadata = openings.holeMetadata;
|
|
1524
|
+
}
|
|
1525
|
+
const mergeContributors = roomIndex == null ? undefined : compatibleMergeCeilingsByRoomIndex.get(roomIndex);
|
|
1526
|
+
const children = mergeContributors
|
|
1527
|
+
? [
|
|
1528
|
+
...new Set(mergeContributors.flatMap((contributor) => contributor.children)),
|
|
1529
|
+
]
|
|
1530
|
+
: roomIndex == null
|
|
1531
|
+
? ceiling.children
|
|
1532
|
+
: (childAssignmentsByCeilingId.get(ceiling.id)?.get(roomIndex) ?? ceiling.children);
|
|
1533
|
+
for (const contributor of mergeContributors ?? []) {
|
|
1534
|
+
if (contributor.id === ceiling.id)
|
|
1535
|
+
continue;
|
|
1536
|
+
for (const childId of contributor.children) {
|
|
1537
|
+
childReparents.push({ id: childId, parentId: ceiling.id });
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
if (!sameStringSet(ceiling.children, children))
|
|
1541
|
+
data.children = children;
|
|
1104
1542
|
if (update.height !== undefined &&
|
|
1105
1543
|
(ceiling.height === undefined ||
|
|
1106
1544
|
Math.abs(ceiling.height - update.height) > ROOM_VERTICAL_PLANE_EPSILON)) {
|
|
@@ -1111,7 +1549,7 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
1111
1549
|
...ceilingDemotions,
|
|
1112
1550
|
...manualClamps,
|
|
1113
1551
|
];
|
|
1114
|
-
const plannedCeilingsForNaming = [...
|
|
1552
|
+
const plannedCeilingsForNaming = [...namingCeilings];
|
|
1115
1553
|
const ceilingsToCreate = [];
|
|
1116
1554
|
for (let index = 0; index < detected.length; index += 1) {
|
|
1117
1555
|
if (matchedDetectedIdx.has(index))
|
|
@@ -1122,32 +1560,54 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
1122
1560
|
const name = nextAutoRoomName(plannedCeilingsForNaming, 'Ceiling');
|
|
1123
1561
|
plannedCeilingsForNaming.push({ name });
|
|
1124
1562
|
const polygon = room.poly.map(pointToTuple);
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1563
|
+
const sourceId = sourceCeilingIdByRoomIndex.get(index);
|
|
1564
|
+
const source = sourceId ? existingAuto.find((ceiling) => ceiling.id === sourceId) : undefined;
|
|
1565
|
+
const openings = sourceId ? openingAssignmentsByCeilingId.get(sourceId)?.get(index) : undefined;
|
|
1566
|
+
const children = sourceId ? childAssignmentsByCeilingId.get(sourceId)?.get(index) : undefined;
|
|
1567
|
+
const height = source
|
|
1568
|
+
? ceilingHeightForReconciledRoom(source, polygon, context)
|
|
1569
|
+
: context.heightForRoom?.(polygon);
|
|
1570
|
+
const created = CeilingNode.parse({
|
|
1127
1571
|
name,
|
|
1128
1572
|
polygon,
|
|
1129
|
-
|
|
1573
|
+
children: children ?? [],
|
|
1574
|
+
holes: openings?.holes ?? [],
|
|
1575
|
+
holeMetadata: openings?.holeMetadata ?? [],
|
|
1576
|
+
material: source?.material,
|
|
1577
|
+
materialPreset: source?.materialPreset,
|
|
1578
|
+
slots: source?.slots,
|
|
1579
|
+
visible: source?.visible,
|
|
1130
1580
|
...(height !== undefined && Number.isFinite(height) ? { height } : {}),
|
|
1131
1581
|
autoFromWalls: true,
|
|
1132
|
-
})
|
|
1582
|
+
});
|
|
1583
|
+
ceilingsToCreate.push(created);
|
|
1584
|
+
for (const childId of children ?? []) {
|
|
1585
|
+
childReparents.push({ id: childId, parentId: created.id });
|
|
1586
|
+
}
|
|
1133
1587
|
}
|
|
1134
1588
|
return {
|
|
1135
1589
|
create: ceilingsToCreate,
|
|
1136
1590
|
update: ceilingsToUpdate,
|
|
1137
1591
|
delete: ceilingsToDelete,
|
|
1592
|
+
reparent: childReparents,
|
|
1138
1593
|
};
|
|
1139
1594
|
}
|
|
1140
|
-
function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, sceneStore, context = {}) {
|
|
1141
|
-
const plan = planAutoCeilingsForLevel(roomPolygons, existingCeilings, context);
|
|
1142
|
-
if (plan.delete.length > 0) {
|
|
1143
|
-
sceneStore.getState().deleteNodes(plan.delete);
|
|
1144
|
-
}
|
|
1595
|
+
function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, sceneStore, context = {}, namingCeilings = existingCeilings) {
|
|
1596
|
+
const plan = planAutoCeilingsForLevel(roomPolygons, existingCeilings, context, namingCeilings);
|
|
1145
1597
|
if (plan.update.length > 0) {
|
|
1146
1598
|
sceneStore.getState().updateNodes(plan.update);
|
|
1147
1599
|
}
|
|
1148
1600
|
if (plan.create.length > 0) {
|
|
1149
1601
|
sceneStore.getState().createNodes(plan.create.map((node) => ({ node, parentId: levelId })));
|
|
1150
1602
|
}
|
|
1603
|
+
if (plan.reparent.length > 0) {
|
|
1604
|
+
sceneStore
|
|
1605
|
+
.getState()
|
|
1606
|
+
.updateNodes(plan.reparent.map(({ id, parentId }) => ({ id, data: { parentId } })));
|
|
1607
|
+
}
|
|
1608
|
+
if (plan.delete.length > 0) {
|
|
1609
|
+
sceneStore.getState().deleteNodes(plan.delete);
|
|
1610
|
+
}
|
|
1151
1611
|
}
|
|
1152
1612
|
function detectSpacesFromWalls(levelId, walls) {
|
|
1153
1613
|
const rooms = extractRooms(walls);
|
|
@@ -1157,6 +1617,7 @@ function detectSpacesFromWalls(levelId, walls) {
|
|
|
1157
1617
|
...resolveWallSurfaceSides(wall, roomPolygons),
|
|
1158
1618
|
}));
|
|
1159
1619
|
return {
|
|
1620
|
+
rooms,
|
|
1160
1621
|
roomPolygons,
|
|
1161
1622
|
spaces: rooms.map((room) => buildSpace(levelId, room)),
|
|
1162
1623
|
wallUpdates,
|
|
@@ -1165,7 +1626,7 @@ function detectSpacesFromWalls(levelId, walls) {
|
|
|
1165
1626
|
export function detectSpacesForLevel(levelId, walls) {
|
|
1166
1627
|
return detectSpacesFromWalls(levelId, walls);
|
|
1167
1628
|
}
|
|
1168
|
-
function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
1629
|
+
function runSpaceDetection(levelIds, sceneStore, editorStore, nodes, previousNodes, previousRoomsByLevel) {
|
|
1169
1630
|
const { updateNodes } = sceneStore.getState();
|
|
1170
1631
|
const existingSpaces = editorStore.getState().spaces;
|
|
1171
1632
|
const nextSpaces = {};
|
|
@@ -1175,11 +1636,12 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
|
1175
1636
|
}
|
|
1176
1637
|
}
|
|
1177
1638
|
for (const levelId of levelIds) {
|
|
1178
|
-
const
|
|
1179
|
-
const
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
const
|
|
1639
|
+
const children = levelChildren(nodes, levelId);
|
|
1640
|
+
const walls = children.filter((node) => node?.type === 'wall' && node.parentId === levelId);
|
|
1641
|
+
const slabs = children.filter((node) => node?.type === 'slab');
|
|
1642
|
+
const ceilings = children.filter((node) => node?.type === 'ceiling');
|
|
1643
|
+
const zones = children.filter((node) => node?.type === 'zone');
|
|
1644
|
+
const { wallUpdates, spaces, rooms } = detectSpacesFromWalls(levelId, walls);
|
|
1183
1645
|
const changedWallUpdates = wallUpdates.filter((update) => {
|
|
1184
1646
|
const wall = nodes[update.wallId];
|
|
1185
1647
|
return wall && (wall.frontSide !== update.frontSide || wall.backSide !== update.backSide);
|
|
@@ -1198,18 +1660,42 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
|
1198
1660
|
? getStoredLevelHeight(levelNode)
|
|
1199
1661
|
: DEFAULT_LEVEL_HEIGHT;
|
|
1200
1662
|
const parsedSlabs = slabs.map((slab) => SlabNode.parse(slab));
|
|
1663
|
+
const parsedCeilings = ceilings.map((ceiling) => CeilingNode.parse(ceiling));
|
|
1664
|
+
const previousRooms = previousRoomsByLevel.get(levelId) ?? [];
|
|
1665
|
+
const slabRooms = roomsEligibleForAutoSurface(previousRooms, rooms, parsedSlabs);
|
|
1666
|
+
const ceilingRooms = roomsEligibleForAutoSurface(previousRooms, rooms, parsedCeilings);
|
|
1201
1667
|
const verticalPlacements = autoRoomVerticalPlacements(spaces, walls,
|
|
1202
1668
|
// A derived floor cannot be evidence for its own next elevation: that
|
|
1203
1669
|
// would lift unpinned walls, then lift the floor again on every pass.
|
|
1204
1670
|
parsedSlabs.filter((slab) => !slab.autoFromWalls), nodes, storeyHeight);
|
|
1671
|
+
const previousChildren = levelChildren(previousNodes, levelId);
|
|
1672
|
+
const previousWalls = previousChildren.filter((node) => node?.type === 'wall' && node.parentId === levelId);
|
|
1673
|
+
const previousSlabs = previousChildren
|
|
1674
|
+
.filter((node) => node?.type === 'slab')
|
|
1675
|
+
.map((slab) => SlabNode.parse(slab));
|
|
1676
|
+
const previousLevelNode = previousNodes[levelId];
|
|
1677
|
+
const previousStoreyHeight = previousLevelNode?.type === 'level'
|
|
1678
|
+
? getStoredLevelHeight(previousLevelNode)
|
|
1679
|
+
: DEFAULT_LEVEL_HEIGHT;
|
|
1680
|
+
const previousSpaces = detectSpacesFromWalls(levelId, previousWalls).spaces;
|
|
1681
|
+
const previousVerticalPlacements = autoRoomVerticalPlacements(previousSpaces, previousWalls, previousSlabs.filter((slab) => !slab.autoFromWalls), previousNodes, previousStoreyHeight);
|
|
1205
1682
|
const placementFor = (polygon) => verticalPlacements.get(polygonSignature(polygon.map(pointFromTuple)));
|
|
1206
|
-
|
|
1683
|
+
const previousPlacementFor = (polygon) => previousVerticalPlacements.get(polygonSignature(polygon.map(pointFromTuple)));
|
|
1684
|
+
syncAutoSlabsForLevel(levelId, slabRooms.map((room) => room.polygon), parsedSlabs, sceneStore, {
|
|
1207
1685
|
elevationForRoom: (polygon) => placementFor(polygon)?.slabElevation,
|
|
1686
|
+
previousElevationForRoom: (polygon) => previousPlacementFor(polygon)?.slabElevation,
|
|
1208
1687
|
});
|
|
1209
|
-
syncAutoCeilingsForLevel(levelId,
|
|
1688
|
+
syncAutoCeilingsForLevel(levelId, ceilingRooms.map((room) => room.polygon), parsedCeilings, sceneStore, {
|
|
1210
1689
|
storeyHeight,
|
|
1211
1690
|
ceilingClampBound: (polygon) => getCeilingClampBound(levelId, nodes, polygon),
|
|
1212
1691
|
heightForRoom: (polygon) => placementFor(polygon)?.ceilingHeight,
|
|
1692
|
+
previousHeightForRoom: (polygon) => previousPlacementFor(polygon)?.ceilingHeight,
|
|
1693
|
+
childPosition: (childId) => {
|
|
1694
|
+
const child = nodes[childId];
|
|
1695
|
+
return child && Array.isArray(child.position)
|
|
1696
|
+
? [child.position[0], child.position[2]]
|
|
1697
|
+
: undefined;
|
|
1698
|
+
},
|
|
1213
1699
|
});
|
|
1214
1700
|
const zonePlan = planAutoZonesForLevel(spaces, zones.map((zone) => ZoneNode.parse(zone)));
|
|
1215
1701
|
if (zonePlan.update.length > 0)
|
|
@@ -1217,7 +1703,104 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
|
1217
1703
|
for (const space of spaces) {
|
|
1218
1704
|
nextSpaces[space.id] = space;
|
|
1219
1705
|
}
|
|
1706
|
+
previousRoomsByLevel.set(levelId, rooms);
|
|
1707
|
+
}
|
|
1708
|
+
editorStore.getState().setSpaces(nextSpaces);
|
|
1709
|
+
}
|
|
1710
|
+
function runIndexedSpaceDetection(levelId, topologyDelta, sceneStore, editorStore, nodes, previousNodes) {
|
|
1711
|
+
const { updateNodes } = sceneStore.getState();
|
|
1712
|
+
const allRoomPolygons = topologyDelta.allCurrentRooms.map((room) => room.polygon);
|
|
1713
|
+
const changedWallUpdates = topologyDelta.currentWalls
|
|
1714
|
+
.map((wall) => ({
|
|
1715
|
+
wallId: wall.id,
|
|
1716
|
+
...resolveWallSurfaceSides(wall, allRoomPolygons),
|
|
1717
|
+
}))
|
|
1718
|
+
.filter((update) => {
|
|
1719
|
+
const wall = nodes[update.wallId];
|
|
1720
|
+
return (wall?.type === 'wall' &&
|
|
1721
|
+
(wall.frontSide !== update.frontSide || wall.backSide !== update.backSide));
|
|
1722
|
+
});
|
|
1723
|
+
if (changedWallUpdates.length > 0) {
|
|
1724
|
+
updateNodes(changedWallUpdates.map((update) => ({
|
|
1725
|
+
id: update.wallId,
|
|
1726
|
+
data: { frontSide: update.frontSide, backSide: update.backSide },
|
|
1727
|
+
})));
|
|
1728
|
+
}
|
|
1729
|
+
const scopedRooms = [...topologyDelta.beforeRooms, ...topologyDelta.currentRooms].map((room) => ({
|
|
1730
|
+
polygon: room.polygon,
|
|
1731
|
+
bbox: bboxOf(room.polygon),
|
|
1732
|
+
}));
|
|
1733
|
+
if (scopedRooms.length > 0) {
|
|
1734
|
+
const unaffectedRooms = topologyDelta.allCurrentRooms
|
|
1735
|
+
.filter((room) => !topologyDelta.currentRooms.includes(room))
|
|
1736
|
+
.map((room) => ({ polygon: room.polygon, bbox: bboxOf(room.polygon) }));
|
|
1737
|
+
const currentChildren = levelChildren(nodes, levelId);
|
|
1738
|
+
const allSlabs = currentChildren
|
|
1739
|
+
.filter((node) => node.type === 'slab')
|
|
1740
|
+
.map((slab) => SlabNode.parse(slab));
|
|
1741
|
+
const allCeilings = currentChildren
|
|
1742
|
+
.filter((node) => node.type === 'ceiling')
|
|
1743
|
+
.map((ceiling) => CeilingNode.parse(ceiling));
|
|
1744
|
+
const touchesScopedRooms = (surface) => {
|
|
1745
|
+
const polygon = surface.polygon.map(pointFromTuple);
|
|
1746
|
+
const bounded = { polygon, bbox: bboxOf(polygon) };
|
|
1747
|
+
return (surfaceTouchesRooms(bounded, scopedRooms) &&
|
|
1748
|
+
(!surface.autoFromWalls || !surfaceTouchesRooms(bounded, unaffectedRooms)));
|
|
1749
|
+
};
|
|
1750
|
+
const slabs = allSlabs.filter(touchesScopedRooms);
|
|
1751
|
+
const ceilings = allCeilings.filter(touchesScopedRooms);
|
|
1752
|
+
const slabRooms = roomsEligibleForAutoSurface(topologyDelta.beforeRooms, topologyDelta.currentRooms, slabs);
|
|
1753
|
+
const ceilingRooms = roomsEligibleForAutoSurface(topologyDelta.beforeRooms, topologyDelta.currentRooms, ceilings);
|
|
1754
|
+
const levelNode = nodes[levelId];
|
|
1755
|
+
const storeyHeight = levelNode?.type === 'level'
|
|
1756
|
+
? getStoredLevelHeight(levelNode)
|
|
1757
|
+
: DEFAULT_LEVEL_HEIGHT;
|
|
1758
|
+
const currentSpaces = topologyDelta.currentRooms.map((room) => buildSpace(levelId, room));
|
|
1759
|
+
const verticalPlacements = autoRoomVerticalPlacements(currentSpaces, topologyDelta.currentWalls, allSlabs.filter((slab) => !slab.autoFromWalls), nodes, storeyHeight);
|
|
1760
|
+
const previousChildren = levelChildren(previousNodes, levelId);
|
|
1761
|
+
const previousSlabs = previousChildren
|
|
1762
|
+
.filter((node) => node.type === 'slab')
|
|
1763
|
+
.map((slab) => SlabNode.parse(slab));
|
|
1764
|
+
const previousLevelNode = previousNodes[levelId];
|
|
1765
|
+
const previousStoreyHeight = previousLevelNode?.type === 'level'
|
|
1766
|
+
? getStoredLevelHeight(previousLevelNode)
|
|
1767
|
+
: DEFAULT_LEVEL_HEIGHT;
|
|
1768
|
+
const previousSpaces = topologyDelta.beforeRooms.map((room) => buildSpace(levelId, room));
|
|
1769
|
+
const previousVerticalPlacements = autoRoomVerticalPlacements(previousSpaces, topologyDelta.previousWalls, previousSlabs.filter((slab) => !slab.autoFromWalls), previousNodes, previousStoreyHeight);
|
|
1770
|
+
const placementFor = (polygon) => verticalPlacements.get(polygonSignature(polygon.map(pointFromTuple)));
|
|
1771
|
+
const previousPlacementFor = (polygon) => previousVerticalPlacements.get(polygonSignature(polygon.map(pointFromTuple)));
|
|
1772
|
+
syncAutoSlabsForLevel(levelId, slabRooms.map((room) => room.polygon), slabs, sceneStore, {
|
|
1773
|
+
elevationForRoom: (polygon) => placementFor(polygon)?.slabElevation,
|
|
1774
|
+
previousElevationForRoom: (polygon) => previousPlacementFor(polygon)?.slabElevation,
|
|
1775
|
+
}, allSlabs);
|
|
1776
|
+
syncAutoCeilingsForLevel(levelId, ceilingRooms.map((room) => room.polygon), ceilings, sceneStore, {
|
|
1777
|
+
storeyHeight,
|
|
1778
|
+
ceilingClampBound: (polygon) => getCeilingClampBound(levelId, nodes, polygon),
|
|
1779
|
+
heightForRoom: (polygon) => placementFor(polygon)?.ceilingHeight,
|
|
1780
|
+
previousHeightForRoom: (polygon) => previousPlacementFor(polygon)?.ceilingHeight,
|
|
1781
|
+
childPosition: (childId) => {
|
|
1782
|
+
const child = nodes[childId];
|
|
1783
|
+
return child && Array.isArray(child.position)
|
|
1784
|
+
? [child.position[0], child.position[2]]
|
|
1785
|
+
: undefined;
|
|
1786
|
+
},
|
|
1787
|
+
}, allCeilings);
|
|
1788
|
+
}
|
|
1789
|
+
const spaces = topologyDelta.allCurrentRooms.map((room) => buildSpace(levelId, room));
|
|
1790
|
+
const zones = levelChildren(nodes, levelId)
|
|
1791
|
+
.filter((node) => node.type === 'zone')
|
|
1792
|
+
.map((zone) => ZoneNode.parse(zone));
|
|
1793
|
+
const zonePlan = planAutoZonesForLevel(spaces, zones);
|
|
1794
|
+
if (zonePlan.update.length > 0)
|
|
1795
|
+
updateNodes(zonePlan.update);
|
|
1796
|
+
const existingSpaces = editorStore.getState().spaces;
|
|
1797
|
+
const nextSpaces = {};
|
|
1798
|
+
for (const [spaceId, space] of Object.entries(existingSpaces)) {
|
|
1799
|
+
if (space.levelId !== levelId)
|
|
1800
|
+
nextSpaces[spaceId] = space;
|
|
1220
1801
|
}
|
|
1802
|
+
for (const space of spaces)
|
|
1803
|
+
nextSpaces[space.id] = space;
|
|
1221
1804
|
editorStore.getState().setSpaces(nextSpaces);
|
|
1222
1805
|
}
|
|
1223
1806
|
// Refcount of outstanding pause requests, matching the pauseSceneHistory
|
|
@@ -1240,66 +1823,142 @@ export function resumeSpaceDetection() {
|
|
|
1240
1823
|
export function isSpaceDetectionPaused() {
|
|
1241
1824
|
return spaceDetectionPauseDepth > 0;
|
|
1242
1825
|
}
|
|
1243
|
-
export function initSpaceDetectionSync(sceneStore, editorStore) {
|
|
1826
|
+
export function initSpaceDetectionSync(sceneStore, editorStore, options = {}) {
|
|
1244
1827
|
// Baseline from whatever is already in the store. Detection reacts to wall
|
|
1245
1828
|
// edits made IN-SESSION (create / move / delete); it must not re-litigate a
|
|
1246
1829
|
// scene that merely loaded — rerunning on hydration resurrected auto slabs
|
|
1247
1830
|
// the user had deleted in an earlier session.
|
|
1248
|
-
const
|
|
1831
|
+
const initialNodes = sceneStore.getState().nodes;
|
|
1832
|
+
const previousRoomsByLevel = new Map();
|
|
1833
|
+
const topologyIndex = new RoomTopologyIndex({
|
|
1834
|
+
detectRooms: extractRooms,
|
|
1835
|
+
sampleWall: (wall) => sampleWallPointsForRoomDetection(wall).map(pointToTuple),
|
|
1836
|
+
junctionTolerance: WALL_JUNCTION_TOLERANCE,
|
|
1837
|
+
});
|
|
1838
|
+
let previousNodes = initialNodes;
|
|
1249
1839
|
let isProcessing = false;
|
|
1840
|
+
const adoptSceneBaseline = (nodes) => {
|
|
1841
|
+
topologyIndex.rebuild(nodes);
|
|
1842
|
+
const roomsByLevel = detectedRoomsByLevel(nodes);
|
|
1843
|
+
previousRoomsByLevel.clear();
|
|
1844
|
+
const spaces = {};
|
|
1845
|
+
for (const [levelId, rooms] of roomsByLevel) {
|
|
1846
|
+
previousRoomsByLevel.set(levelId, rooms);
|
|
1847
|
+
for (const room of rooms) {
|
|
1848
|
+
const space = buildSpace(levelId, room);
|
|
1849
|
+
spaces[space.id] = space;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
editorStore.getState().setSpaces(spaces);
|
|
1853
|
+
previousNodes = nodes;
|
|
1854
|
+
};
|
|
1855
|
+
adoptSceneBaseline(initialNodes);
|
|
1856
|
+
const unsubscribeCommits = subscribeSceneCommits((commit) => {
|
|
1857
|
+
if (commit.origin === 'local')
|
|
1858
|
+
return;
|
|
1859
|
+
adoptSceneBaseline(commit.current.nodes);
|
|
1860
|
+
});
|
|
1250
1861
|
const unsubscribe = sceneStore.subscribe((state) => {
|
|
1251
1862
|
if (isProcessing)
|
|
1252
1863
|
return;
|
|
1253
1864
|
if (getSceneHistoryPauseDepth() > 0)
|
|
1254
1865
|
return;
|
|
1255
1866
|
const nodes = state.nodes;
|
|
1256
|
-
const
|
|
1867
|
+
const candidateIds = activeSceneCommitNodeIds();
|
|
1257
1868
|
// Paused: roll the snapshot forward so we don't backfill (and re-duplicate)
|
|
1258
1869
|
// every paused change once detection resumes. Whatever the AI built while
|
|
1259
1870
|
// paused becomes the new baseline; only future changes will reconcile.
|
|
1260
1871
|
if (spaceDetectionPauseDepth > 0) {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1872
|
+
adoptSceneBaseline(nodes);
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const changedWalls = changedWallIdsByLevel(previousNodes, nodes, candidateIds);
|
|
1876
|
+
if (candidateIds && changedWalls.size > 0) {
|
|
1877
|
+
const fallbackLevels = fallbackLevelIdsForCandidates(previousNodes, nodes, candidateIds);
|
|
1878
|
+
for (const levelId of changedWalls.keys())
|
|
1879
|
+
fallbackLevels.delete(levelId);
|
|
1880
|
+
isProcessing = true;
|
|
1881
|
+
pauseSceneHistory(sceneStore);
|
|
1882
|
+
try {
|
|
1883
|
+
for (const [levelId, wallIds] of changedWalls) {
|
|
1884
|
+
const topologyDelta = topologyIndex.applyWallDelta(levelId, wallIds, previousNodes, nodes);
|
|
1885
|
+
runIndexedSpaceDetection(levelId, topologyDelta, sceneStore, editorStore, nodes, previousNodes);
|
|
1886
|
+
previousRoomsByLevel.set(levelId, topologyDelta.allCurrentRooms);
|
|
1887
|
+
options.onTopologyReconcile?.({
|
|
1888
|
+
levelId,
|
|
1889
|
+
strategy: topologyDelta.strategy,
|
|
1890
|
+
examinedWallIds: topologyDelta.examinedWallIds,
|
|
1891
|
+
affectedBeforeRoomCount: topologyDelta.beforeRooms.length,
|
|
1892
|
+
affectedCurrentRoomCount: topologyDelta.currentRooms.length,
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
if (fallbackLevels.size > 0) {
|
|
1896
|
+
runSpaceDetection([...fallbackLevels], sceneStore, editorStore, sceneStore.getState().nodes, previousNodes, previousRoomsByLevel);
|
|
1897
|
+
const liveNodes = sceneStore.getState().nodes;
|
|
1898
|
+
for (const levelId of fallbackLevels)
|
|
1899
|
+
topologyIndex.rebuildLevel(levelId, liveNodes);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
finally {
|
|
1903
|
+
resumeSceneHistory(sceneStore);
|
|
1904
|
+
previousNodes = sceneStore.getState().nodes;
|
|
1905
|
+
isProcessing = false;
|
|
1264
1906
|
}
|
|
1265
1907
|
return;
|
|
1266
1908
|
}
|
|
1267
1909
|
const levelsToUpdate = new Set();
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
// `setScene` delivers a loaded scene as one atomic update, and a level's
|
|
1271
|
-
// first wall can't close a room anyway. Record it (below) and only
|
|
1272
|
-
// react to subsequent changes.
|
|
1273
|
-
const previous = previousSnapshots.get(levelId);
|
|
1274
|
-
if (previous === undefined)
|
|
1275
|
-
continue;
|
|
1276
|
-
if (previous !== (currentSnapshots.get(levelId) ?? '')) {
|
|
1910
|
+
if (candidateIds) {
|
|
1911
|
+
for (const levelId of fallbackLevelIdsForCandidates(previousNodes, nodes, candidateIds)) {
|
|
1277
1912
|
levelsToUpdate.add(levelId);
|
|
1278
1913
|
}
|
|
1279
1914
|
}
|
|
1915
|
+
else {
|
|
1916
|
+
const previousSnapshots = levelStructureSnapshots(previousNodes);
|
|
1917
|
+
const currentSnapshots = levelStructureSnapshots(nodes);
|
|
1918
|
+
for (const levelId of new Set([...previousSnapshots.keys(), ...currentSnapshots.keys()])) {
|
|
1919
|
+
// First sight of a level is a hydration baseline, not a wall edit —
|
|
1920
|
+
// `setScene` delivers a loaded scene as one atomic update, and a level's
|
|
1921
|
+
// first wall can't close a room anyway. Record it (below) and only
|
|
1922
|
+
// react to subsequent changes.
|
|
1923
|
+
const previous = previousSnapshots.get(levelId);
|
|
1924
|
+
if (previous === undefined)
|
|
1925
|
+
continue;
|
|
1926
|
+
if (previous !== (currentSnapshots.get(levelId) ?? '')) {
|
|
1927
|
+
levelsToUpdate.add(levelId);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1280
1931
|
if (levelsToUpdate.size === 0) {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1932
|
+
if (candidateIds) {
|
|
1933
|
+
previousNodes = nodes;
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const currentRoomsByLevel = detectedRoomsByLevel(nodes);
|
|
1937
|
+
previousRoomsByLevel.clear();
|
|
1938
|
+
for (const [levelId, rooms] of currentRoomsByLevel) {
|
|
1939
|
+
previousRoomsByLevel.set(levelId, rooms);
|
|
1284
1940
|
}
|
|
1941
|
+
previousNodes = nodes;
|
|
1285
1942
|
return;
|
|
1286
1943
|
}
|
|
1287
1944
|
isProcessing = true;
|
|
1288
1945
|
pauseSceneHistory(sceneStore);
|
|
1289
1946
|
try {
|
|
1290
|
-
runSpaceDetection([...levelsToUpdate], sceneStore, editorStore, nodes);
|
|
1947
|
+
runSpaceDetection([...levelsToUpdate], sceneStore, editorStore, nodes, previousNodes, previousRoomsByLevel);
|
|
1291
1948
|
}
|
|
1292
1949
|
finally {
|
|
1293
1950
|
resumeSceneHistory(sceneStore);
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
}
|
|
1951
|
+
const liveNodes = sceneStore.getState().nodes;
|
|
1952
|
+
for (const levelId of levelsToUpdate)
|
|
1953
|
+
topologyIndex.rebuildLevel(levelId, liveNodes);
|
|
1954
|
+
previousNodes = liveNodes;
|
|
1299
1955
|
isProcessing = false;
|
|
1300
1956
|
}
|
|
1301
1957
|
});
|
|
1302
|
-
return
|
|
1958
|
+
return () => {
|
|
1959
|
+
unsubscribe();
|
|
1960
|
+
unsubscribeCommits();
|
|
1961
|
+
};
|
|
1303
1962
|
}
|
|
1304
1963
|
export function wallTouchesOthers(wall, otherWalls) {
|
|
1305
1964
|
const threshold = 0.1;
|
|
@@ -1315,18 +1974,3 @@ export function wallTouchesOthers(wall, otherWalls) {
|
|
|
1315
1974
|
}
|
|
1316
1975
|
return false;
|
|
1317
1976
|
}
|
|
1318
|
-
function distanceToSegment(point, segStart, segEnd) {
|
|
1319
|
-
const [px, py] = point;
|
|
1320
|
-
const [x1, y1] = segStart;
|
|
1321
|
-
const [x2, y2] = segEnd;
|
|
1322
|
-
const dx = x2 - x1;
|
|
1323
|
-
const dy = y2 - y1;
|
|
1324
|
-
const lenSq = dx * dx + dy * dy;
|
|
1325
|
-
if (lenSq < 0.0001) {
|
|
1326
|
-
return Math.hypot(px - x1, py - y1);
|
|
1327
|
-
}
|
|
1328
|
-
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
|
|
1329
|
-
const projX = x1 + t * dx;
|
|
1330
|
-
const projY = y1 + t * dy;
|
|
1331
|
-
return Math.hypot(px - projX, py - projY);
|
|
1332
|
-
}
|