@pascal-app/core 1.0.0-beta.4 → 1.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- 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/space-detection.d.ts +29 -3
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +858 -242
- package/dist/registry/index.d.ts +3 -2
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +2 -1
- package/dist/registry/registry.d.ts +17 -1
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +74 -0
- package/dist/registry/types.d.ts +35 -0
- 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/index.d.ts +1 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -0
- package/dist/schema/nodes/level.d.ts +3 -0
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +9 -0
- package/dist/schema/types.d.ts +1 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -1
- 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/store/actions/node-actions.d.ts +8 -4
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +31 -4
- 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/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +75 -163
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-service.js +16 -11
- 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-opening-preview.d.ts +1 -0
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-rise.d.ts.map +1 -1
- package/dist/systems/stair/stair-rise.js +4 -2
- 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 +13 -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/package.json +1 -1
package/dist/store/use-scene.js
CHANGED
|
@@ -7,7 +7,7 @@ import { BuildingNode } from '../schema';
|
|
|
7
7
|
import { generateCollectionId } from '../schema/collections';
|
|
8
8
|
import { DoorNode as DoorNodeSchema } from '../schema/nodes/door';
|
|
9
9
|
import { ElevatorNode as ElevatorNodeSchema } from '../schema/nodes/elevator';
|
|
10
|
-
import { LevelNode } from '../schema/nodes/level';
|
|
10
|
+
import { LevelNode, normalizeLevelBaseElevation } from '../schema/nodes/level';
|
|
11
11
|
import { getPitchFromActiveRoofHeight, } from '../schema/nodes/roof-segment';
|
|
12
12
|
import { segmentPointToRoofWallFace } from '../schema/nodes/roof-segment-walls';
|
|
13
13
|
import { ShelfNode as ShelfNodeSchema } from '../schema/nodes/shelf';
|
|
@@ -18,12 +18,9 @@ import { getEffectiveWallSurfaceMaterial } from '../schema/nodes/wall';
|
|
|
18
18
|
import { WindowNode as WindowNodeSchema } from '../schema/nodes/window';
|
|
19
19
|
import { generateSceneMaterialId, SceneMaterial, } from '../schema/scene-material';
|
|
20
20
|
import { AnyNode as AnyNodeSchema } from '../schema/types';
|
|
21
|
-
import { deriveLegacyLevelHeight } from '../services/level-height';
|
|
22
|
-
import { getCeilingClampBound } from '../services/storey';
|
|
23
|
-
import { computeWallSlabSupport } from '../systems/slab/slab-support';
|
|
24
|
-
import { DEFAULT_WALL_HEIGHT } from '../systems/wall/wall-footprint';
|
|
25
21
|
import { healSceneNodes } from '../utils/heal-scene-graph';
|
|
26
22
|
import { removeRetiredDrawingSheetNodes } from '../utils/retired-scene-nodes';
|
|
23
|
+
import { migrateVerticalSceneNodes } from '../utils/vertical-scene-migration';
|
|
27
24
|
import * as nodeActions from './actions/node-actions';
|
|
28
25
|
import { areSceneSnapshotsEqual, getSceneHistoryPauseDepth, notifySceneCommit, pauseSceneHistory, resetSceneHistoryPauseDepth, resumeSceneHistory, } from './history-control';
|
|
29
26
|
import useLiveNodeOverrides from './use-live-node-overrides';
|
|
@@ -485,14 +482,6 @@ function migrateWallAssembly(node) {
|
|
|
485
482
|
const { assemblyLayers: _assemblyLayers, ...wall } = node;
|
|
486
483
|
return assemblyThickness > 0 ? { ...wall, thickness: assemblyThickness } : wall;
|
|
487
484
|
}
|
|
488
|
-
// Walls whose top lands within this of the storey plane become plane-bound;
|
|
489
|
-
// ceilings whose stored height lands within this of their clamp bound become
|
|
490
|
-
// follows-mode (step 3f) — same census-backed threshold for both.
|
|
491
|
-
// From a prod census: the 0.15-short "hole pattern" (default 2.5 walls next to
|
|
492
|
-
// a taller wall) must snap to the plane, while intentional 0.20-short walls
|
|
493
|
-
// (2.5 under a 2.7 plane, 2.3 under a 2.5 plane) must keep their explicit
|
|
494
|
-
// height — hence 0.20 with a strictly-less-than comparison.
|
|
495
|
-
const PLANE_BOUND_EPSILON = 0.2;
|
|
496
485
|
function migrateNodes(nodes) {
|
|
497
486
|
// Repair pre-existing corruption (null children, zero-length walls) before
|
|
498
487
|
// any per-type migration runs, so already-saved scenes load cleanly.
|
|
@@ -749,6 +738,7 @@ function migrateNodes(nodes) {
|
|
|
749
738
|
const levelNumber = getFiniteNumber(node.level, 0);
|
|
750
739
|
patchedNodes[id] = {
|
|
751
740
|
...node,
|
|
741
|
+
baseElevation: normalizeLevelBaseElevation(node.baseElevation),
|
|
752
742
|
level: levelNumber,
|
|
753
743
|
children: validChildren,
|
|
754
744
|
};
|
|
@@ -771,155 +761,8 @@ function migrateNodes(nodes) {
|
|
|
771
761
|
patchedNodes[id] = normalized;
|
|
772
762
|
}
|
|
773
763
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
// landed. Computed before this pass mutates anything: the stair-rise
|
|
777
|
-
// cleanup below must never run on already-migrated scenes.
|
|
778
|
-
const isLegacyScene = Object.values(patchedNodes).some((node) => node?.type === 'level' && !('height' in node));
|
|
779
|
-
// 3a. Ordinal renumber — always runs, per building (idempotent
|
|
780
|
-
// self-healing; MCP's create-level historically wrote its elevation PARAM
|
|
781
|
-
// into the ordinal, so fractional/duplicate ordinals exist in the wild).
|
|
782
|
-
const buildingNodes = Object.values(patchedNodes).filter((node) => node?.type === 'building');
|
|
783
|
-
const levelsByBuilding = new Map();
|
|
784
|
-
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
785
|
-
if (node?.type !== 'level')
|
|
786
|
-
continue;
|
|
787
|
-
// Mirrors the building resolution in services/storey.ts: an explicit
|
|
788
|
-
// parentId pointing at a building wins, membership in a building's
|
|
789
|
-
// children array is the legacy fallback, and unresolvable levels share
|
|
790
|
-
// one orphan bucket.
|
|
791
|
-
const buildingId = buildingNodes.find((building) => building.id === node.parentId)?.id ??
|
|
792
|
-
buildingNodes.find((building) => getStringArray(building.children).includes(id))?.id ??
|
|
793
|
-
null;
|
|
794
|
-
const bucket = levelsByBuilding.get(buildingId) ?? [];
|
|
795
|
-
bucket.push({ id, ordinal: getFiniteNumber(node.level, 0) });
|
|
796
|
-
levelsByBuilding.set(buildingId, bucket);
|
|
797
|
-
}
|
|
798
|
-
for (const bucket of levelsByBuilding.values()) {
|
|
799
|
-
// Anchored at zero on purpose: ordinals are semantic — `level < 0`
|
|
800
|
-
// renders "Basement N" and `level === 0` is the ground-floor default —
|
|
801
|
-
// so negatives compact upward toward −1 and non-negatives compact down
|
|
802
|
-
// to 0. A blind 0..n renumber would rename basements.
|
|
803
|
-
const sorted = [...bucket].sort((a, b) => a.ordinal - b.ordinal);
|
|
804
|
-
const negativeCount = sorted.filter((entry) => entry.ordinal < 0).length;
|
|
805
|
-
sorted.forEach((entry, index) => {
|
|
806
|
-
const nextOrdinal = index - negativeCount;
|
|
807
|
-
const current = patchedNodes[entry.id];
|
|
808
|
-
if (current.level !== nextOrdinal) {
|
|
809
|
-
patchedNodes[entry.id] = { ...current, level: nextOrdinal };
|
|
810
|
-
}
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
// 3b. Stored storey heights: materialize the legacy stacked height verbatim
|
|
814
|
-
// (never rounded or snapped — snapping would move existing buildings).
|
|
815
|
-
// All planes derive before any wall height below mutates.
|
|
816
|
-
const legacyLevelIds = Object.entries(patchedNodes)
|
|
817
|
-
.filter(([, node]) => node?.type === 'level' && !('height' in node))
|
|
818
|
-
.map(([id]) => id);
|
|
819
|
-
const derivedHeights = new Map();
|
|
820
|
-
for (const levelId of legacyLevelIds) {
|
|
821
|
-
derivedHeights.set(levelId, deriveLegacyLevelHeight(levelId, patchedNodes));
|
|
822
|
-
}
|
|
823
|
-
for (const levelId of legacyLevelIds) {
|
|
824
|
-
const plane = derivedHeights.get(levelId);
|
|
825
|
-
const level = patchedNodes[levelId];
|
|
826
|
-
patchedNodes[levelId] = { ...level, height: plane };
|
|
827
|
-
// 3c. Wall-top classification against the just-written plane, using the
|
|
828
|
-
// same slab-support election as deriveLegacyLevelHeight (call shape
|
|
829
|
-
// mirrored from services/level-height.ts). Walls whose top meets the
|
|
830
|
-
// plane drop their explicit height and follow the level from now on;
|
|
831
|
-
// walls ending short (or tall) keep an explicit height — materializing
|
|
832
|
-
// the 2.5 default onto absent-height walls that end short of the plane.
|
|
833
|
-
const children = getStringArray(level.children)
|
|
834
|
-
.map((childId) => patchedNodes[childId])
|
|
835
|
-
.filter((child) => child !== undefined);
|
|
836
|
-
const slabs = children.filter((child) => child.type === 'slab');
|
|
837
|
-
const walls = children.filter((child) => child.type === 'wall');
|
|
838
|
-
for (const wall of walls) {
|
|
839
|
-
const electedBase = computeWallSlabSupport({
|
|
840
|
-
start: wall.start,
|
|
841
|
-
end: wall.end,
|
|
842
|
-
curveOffset: wall.curveOffset,
|
|
843
|
-
thickness: wall.thickness,
|
|
844
|
-
}, slabs, walls).elevation;
|
|
845
|
-
const effectiveHeight = wall.height ?? DEFAULT_WALL_HEIGHT;
|
|
846
|
-
const top = Math.max(0, electedBase) + effectiveHeight;
|
|
847
|
-
if (Math.abs(plane - top) < PLANE_BOUND_EPSILON) {
|
|
848
|
-
if ('height' in wall) {
|
|
849
|
-
const { height: _height, ...planeBound } = wall;
|
|
850
|
-
patchedNodes[wall.id] = planeBound;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
else {
|
|
854
|
-
patchedNodes[wall.id] = { ...wall, height: effectiveHeight };
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
// 3d. Stair rise: on legacy scenes a totalRise of exactly 2.5 is the old
|
|
859
|
-
// schema default, not a user choice — drop it so the rise derives from the
|
|
860
|
-
// storey height. Gated on isLegacyScene because on a post-migration scene
|
|
861
|
-
// a stored 2.5 IS a deliberately typed value and must survive reloads.
|
|
862
|
-
if (isLegacyScene) {
|
|
863
|
-
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
864
|
-
if (node?.type !== 'stair')
|
|
865
|
-
continue;
|
|
866
|
-
if (node.totalRise !== 2.5)
|
|
867
|
-
continue;
|
|
868
|
-
const { totalRise: _totalRise, ...derivedRise } = node;
|
|
869
|
-
patchedNodes[id] = derivedRise;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
// 3e. Slab placement/thickness split. `elevation` stays the walking surface;
|
|
873
|
-
// the new `thickness` grows downward so the solid occupies
|
|
874
|
-
// [elevation − thickness, elevation]. Legacy solids extruded [0, elevation],
|
|
875
|
-
// so thickness = elevation EXACTLY (including degenerate 0 — MIN_SLAB_THICKNESS
|
|
876
|
-
// applies to edits only, never here) keeps the occupied interval identical.
|
|
877
|
-
// Legacy pools (elevation < 0) become explicit `recessed` intent with
|
|
878
|
-
// elevation unchanged. Gated per slab on a missing `thickness` — the
|
|
879
|
-
// migration output is cast, so schema defaults never materialize on load.
|
|
880
|
-
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
881
|
-
if (node?.type !== 'slab' || 'thickness' in node)
|
|
882
|
-
continue;
|
|
883
|
-
const elevation = getFiniteNumber(node.elevation, 0.05);
|
|
884
|
-
patchedNodes[id] =
|
|
885
|
-
elevation < 0
|
|
886
|
-
? { ...node, thickness: 0.05, recessed: true }
|
|
887
|
-
: { ...node, thickness: elevation };
|
|
888
|
-
}
|
|
889
|
-
// 3f. Ceiling follows-mode classification (the ceiling mirror of 3c; runs
|
|
890
|
-
// after 3b/3e so the clamp bound sees stored level heights and split slab
|
|
891
|
-
// thicknesses). A stored ceiling height within PLANE_BOUND_EPSILON of its
|
|
892
|
-
// clamp bound (min(storey plane, covering-slab underside) − margin, via
|
|
893
|
-
// getCeilingClampBound) is the legacy default tracking the level top, not
|
|
894
|
-
// a choice — drop it so the ceiling follows the level from now on.
|
|
895
|
-
// autoFromWalls ceilings always convert: their height was derived by the
|
|
896
|
-
// space-detection sync, never user intent. Gated on isLegacyScene, which
|
|
897
|
-
// is exact — nothing shipped between the level-height migration and this
|
|
898
|
-
// one — and makes the step idempotent. Known accepted edge: a
|
|
899
|
-
// post-migration user typing a custom height exactly equal to the bound
|
|
900
|
-
// keeps it (the gate prevents re-classification on later loads).
|
|
901
|
-
if (isLegacyScene) {
|
|
902
|
-
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
903
|
-
if (node?.type !== 'ceiling' || !('height' in node))
|
|
904
|
-
continue;
|
|
905
|
-
const dropHeight = () => {
|
|
906
|
-
const { height: _height, ...follows } = node;
|
|
907
|
-
patchedNodes[id] = follows;
|
|
908
|
-
};
|
|
909
|
-
if (node.autoFromWalls === true) {
|
|
910
|
-
dropHeight();
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
if (typeof node.parentId !== 'string')
|
|
914
|
-
continue;
|
|
915
|
-
const bound = getCeilingClampBound(node.parentId, patchedNodes, Array.isArray(node.polygon) ? node.polygon : []);
|
|
916
|
-
const stored = getFiniteNumber(node.height, Number.NaN);
|
|
917
|
-
if (Number.isFinite(bound) && Math.abs(stored - bound) < PLANE_BOUND_EPSILON) {
|
|
918
|
-
dropHeight();
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
return { nodes: patchedNodes, mintedMaterials };
|
|
764
|
+
const vertical = migrateVerticalSceneNodes(patchedNodes);
|
|
765
|
+
return { nodes: vertical.nodes, mintedMaterials };
|
|
923
766
|
}
|
|
924
767
|
function getNodeChildIds(node) {
|
|
925
768
|
if (!('children' in node && Array.isArray(node.children))) {
|
|
@@ -971,7 +814,76 @@ function collectReachableNodeIds(nodes, rootNodeIds) {
|
|
|
971
814
|
}
|
|
972
815
|
function sceneHistorySnapshotFromState(state) {
|
|
973
816
|
const { nodes, rootNodeIds, collections, materials, installedPlugins } = state;
|
|
974
|
-
|
|
817
|
+
// Fresh placement nodes are renderable drafts, not document history. Excluding their
|
|
818
|
+
// entire subtree here protects both local undo and external commit subscribers.
|
|
819
|
+
const transientNodeIds = new Set();
|
|
820
|
+
for (const node of Object.values(nodes)) {
|
|
821
|
+
const metadata = node.metadata;
|
|
822
|
+
if (metadata &&
|
|
823
|
+
typeof metadata === 'object' &&
|
|
824
|
+
!Array.isArray(metadata) &&
|
|
825
|
+
metadata.isNew === true) {
|
|
826
|
+
transientNodeIds.add(node.id);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (transientNodeIds.size === 0) {
|
|
830
|
+
return { nodes, rootNodeIds, collections, materials, installedPlugins };
|
|
831
|
+
}
|
|
832
|
+
const childIdsByParentId = new Map();
|
|
833
|
+
const addChild = (parentId, childId) => {
|
|
834
|
+
const childIds = childIdsByParentId.get(parentId) ?? new Set();
|
|
835
|
+
childIds.add(childId);
|
|
836
|
+
childIdsByParentId.set(parentId, childIds);
|
|
837
|
+
};
|
|
838
|
+
for (const node of Object.values(nodes)) {
|
|
839
|
+
if (node.parentId)
|
|
840
|
+
addChild(node.parentId, node.id);
|
|
841
|
+
for (const childId of getNodeChildIds(node))
|
|
842
|
+
addChild(node.id, childId);
|
|
843
|
+
}
|
|
844
|
+
const pendingIds = [...transientNodeIds];
|
|
845
|
+
while (pendingIds.length > 0) {
|
|
846
|
+
const parentId = pendingIds.pop();
|
|
847
|
+
if (!parentId)
|
|
848
|
+
continue;
|
|
849
|
+
for (const childId of childIdsByParentId.get(parentId) ?? []) {
|
|
850
|
+
if (transientNodeIds.has(childId))
|
|
851
|
+
continue;
|
|
852
|
+
transientNodeIds.add(childId);
|
|
853
|
+
pendingIds.push(childId);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const historyNodes = {};
|
|
857
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
858
|
+
if (transientNodeIds.has(id))
|
|
859
|
+
continue;
|
|
860
|
+
if (!('children' in node && Array.isArray(node.children))) {
|
|
861
|
+
historyNodes[id] = node;
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const children = node.children.filter((childId) => !transientNodeIds.has(childId));
|
|
865
|
+
historyNodes[id] =
|
|
866
|
+
children.length === node.children.length ? node : { ...node, children };
|
|
867
|
+
}
|
|
868
|
+
const historyCollections = {};
|
|
869
|
+
for (const [id, collection] of Object.entries(collections)) {
|
|
870
|
+
const nodeIds = collection.nodeIds.filter((nodeId) => !transientNodeIds.has(nodeId));
|
|
871
|
+
if (collection.controlNodeId && transientNodeIds.has(collection.controlNodeId)) {
|
|
872
|
+
const { controlNodeId: _controlNodeId, ...rest } = collection;
|
|
873
|
+
historyCollections[id] = { ...rest, nodeIds };
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
historyCollections[id] =
|
|
877
|
+
nodeIds.length === collection.nodeIds.length ? collection : { ...collection, nodeIds };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
nodes: historyNodes,
|
|
882
|
+
rootNodeIds: rootNodeIds.filter((id) => !transientNodeIds.has(id)),
|
|
883
|
+
collections: historyCollections,
|
|
884
|
+
materials,
|
|
885
|
+
installedPlugins,
|
|
886
|
+
};
|
|
975
887
|
}
|
|
976
888
|
const useScene = create()(temporal((set, get) => ({
|
|
977
889
|
// 1. Flat dictionary of all nodes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elevator-service.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAG/E,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AA2BD,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,SAAS,EAAE,CAEb;AAED,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,EAAE,CAEV;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,SAAS,EAAE,CAsBb;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B;IACD,OAAO,EAAE,kBAAkB,EAAE,CAAA;IAC7B,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAA;IACvC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACpB,
|
|
1
|
+
{"version":3,"file":"elevator-service.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAG/E,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AA2BD,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,SAAS,EAAE,CAEb;AAED,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,EAAE,CAEV;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,SAAS,EAAE,CAsBb;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B;IACD,OAAO,EAAE,kBAAkB,EAAE,CAAA;IAC7B,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAA;IACvC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACpB,CA6CA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getLevelElevations } from '../../services/storey';
|
|
2
2
|
function getBuildingLevels(elevator, nodes) {
|
|
3
3
|
const building = elevator.parentId && nodes[elevator.parentId]?.type === 'building'
|
|
4
4
|
? nodes[elevator.parentId]
|
|
@@ -51,17 +51,12 @@ export function resolveElevatorServiceLevels(elevator, nodes) {
|
|
|
51
51
|
}
|
|
52
52
|
export function resolveElevatorLevels(elevator, nodes) {
|
|
53
53
|
const allLevels = resolveElevatorBuildingLevels(elevator, nodes);
|
|
54
|
-
const
|
|
55
|
-
let cumulativeY = 0;
|
|
56
|
-
for (const level of allLevels) {
|
|
57
|
-
baseYByLevelId.set(level.id, cumulativeY);
|
|
58
|
-
cumulativeY += getStoredLevelHeight(level);
|
|
59
|
-
}
|
|
54
|
+
const levelElevations = getLevelElevations(nodes);
|
|
60
55
|
const serviceLevels = resolveElevatorServiceLevels(elevator, nodes);
|
|
61
56
|
const entries = serviceLevels.map((level) => ({
|
|
62
57
|
id: level.id,
|
|
63
58
|
label: String(level.level),
|
|
64
|
-
baseY:
|
|
59
|
+
baseY: levelElevations.get(level.id)?.baseY ?? 0,
|
|
65
60
|
}));
|
|
66
61
|
const defaultEntry = entries.find((entry) => entry.id === elevator.defaultLevelId) ??
|
|
67
62
|
entries.find((entry) => entry.id === elevator.fromLevelId) ??
|
|
@@ -69,15 +64,25 @@ export function resolveElevatorLevels(elevator, nodes) {
|
|
|
69
64
|
null;
|
|
70
65
|
const firstServedLevel = serviceLevels[0] ?? null;
|
|
71
66
|
const lastServedLevel = serviceLevels[serviceLevels.length - 1] ?? null;
|
|
72
|
-
const shaftBaseY = firstServedLevel ? (
|
|
67
|
+
const shaftBaseY = firstServedLevel ? (levelElevations.get(firstServedLevel.id)?.baseY ?? 0) : 0;
|
|
73
68
|
const lastServedIndex = lastServedLevel
|
|
74
69
|
? allLevels.findIndex((level) => level.id === lastServedLevel.id)
|
|
75
70
|
: -1;
|
|
76
71
|
const nextLevel = lastServedIndex >= 0 ? allLevels[lastServedIndex + 1] : null;
|
|
72
|
+
// Highest ceiling in the stack, not the topmost level's: a negative
|
|
73
|
+
// baseElevation on the top level can put its ceiling below the level
|
|
74
|
+
// beneath it, and a shaft top under a served level would clip the cab.
|
|
75
|
+
let stackTopY = 0;
|
|
76
|
+
for (const level of allLevels) {
|
|
77
|
+
const elevation = levelElevations.get(level.id);
|
|
78
|
+
if (!elevation)
|
|
79
|
+
continue;
|
|
80
|
+
stackTopY = Math.max(stackTopY, elevation.baseY + elevation.height);
|
|
81
|
+
}
|
|
77
82
|
const shaftTopY = nextLevel
|
|
78
|
-
? (
|
|
83
|
+
? (levelElevations.get(nextLevel.id)?.baseY ?? stackTopY)
|
|
79
84
|
: lastServedLevel
|
|
80
|
-
?
|
|
85
|
+
? stackTopY
|
|
81
86
|
: elevator.cabHeight + 0.3;
|
|
82
87
|
return {
|
|
83
88
|
entries,
|
|
@@ -114,37 +114,6 @@ export type WallSlabSupportSegment = {
|
|
|
114
114
|
end: number;
|
|
115
115
|
elevation: number;
|
|
116
116
|
};
|
|
117
|
-
/**
|
|
118
|
-
* `preferredSlabId` is a persisted support host (`wall.supportSlabId`):
|
|
119
|
-
* while that slab is still in the candidate set (still overlaps the wall
|
|
120
|
-
* band with enough covered length), the elected `elevation` is pinned to
|
|
121
|
-
* it instead of the majority/best-coverage election. `baseSegments` /
|
|
122
|
-
* `baseElevation` (fill-down) still derive from ALL supporting slabs
|
|
123
|
-
* unchanged. A preferred slab that no longer qualifies is silently
|
|
124
|
-
* ignored — deliberately never cleared here, so the host resumes if the
|
|
125
|
-
* slab's polygon returns (only slab deletion strips the stored field).
|
|
126
|
-
*
|
|
127
|
-
* `maxElevation` is the pointer-decided support cap (level-local Y, same
|
|
128
|
-
* semantics as the item election): when set, elevation groups whose
|
|
129
|
-
* walking surface sits above `maxElevation + SUPPORT_ELEVATION_EPSILON`
|
|
130
|
-
* are excluded from the majority/best election — a deck hanging above the
|
|
131
|
-
* surface the cursor ray actually hit never captures the elected base.
|
|
132
|
-
* `baseSegments` / `baseElevation` stay uncapped (geometry fill-down), and
|
|
133
|
-
* an explicit `preferredSlabId` still wins over the cap.
|
|
134
|
-
*
|
|
135
|
-
* `levelBase` is what "no slab supports this here" evaluates to — the
|
|
136
|
-
* sculpted ground under the wall (`levelBaseElevationAt`), or 0 for a level
|
|
137
|
-
* with no terrain under it. A caller-resolved scalar rather than a terrain
|
|
138
|
-
* lookup in here, so this stays pure and the sample stays at the wall's own
|
|
139
|
-
* XZ. It substitutes for every place this function used to write a literal
|
|
140
|
-
* `0`, and nowhere else: a slab that supports the wall's faces still wins
|
|
141
|
-
* outright, so a slab pad on a hillside keeps its wall at the pad's
|
|
142
|
-
* elevation instead of being overruled by the ground around it. The one
|
|
143
|
-
* clamp — center-only support, where the old code already clamped a
|
|
144
|
-
* recessed slab up to `0` so a wall over a pool didn't sink — generalizes to
|
|
145
|
-
* "never below the ground", which is the same rule with the ground no longer
|
|
146
|
-
* assumed flat.
|
|
147
|
-
*/
|
|
148
117
|
export declare function computeWallSlabSupport(wallLike: WallOverlapInput, slabs: readonly SlabNode[], levelWalls: WallNode[], preferredSlabId?: string | null, maxElevation?: number | null, levelBase?: number): WallSlabSupport;
|
|
149
118
|
export declare function computeWallSlabElevation(wallLike: WallOverlapInput, slabs: readonly SlabNode[], levelWalls: WallNode[]): number;
|
|
150
119
|
//# sourceMappingURL=slab-support.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slab-support.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-support.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAKtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,0BAA0B,CACxC,iBAAiB,EAAE,MAAM,EACzB,IAAI,EAAE,QAAQ,EACd,UAAU,EAAE,QAAQ,EAAE,EACtB,UAAU,EAAE,SAAS,QAAQ,EAAE,EAC/B,YAAY,EAAE,MAAM,GACnB,kBAAkB,CA6BpB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,QAAQ,EACd,UAAU,EAAE,QAAQ,EAAE,EACtB,UAAU,EAAE,SAAS,QAAQ,EAAE,EAC/B,YAAY,EAAE,MAAM,GACnB,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,OAAO,CAchG;AA0BD,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC/B,OAAO,CAQT;AAsID,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvB,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAgED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,gBAAgB,EAChD,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACxD,UAAU,CAAC,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACnC,OAAO,CAqBT;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAChC,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAC7C,OAAO,CAoBT;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAA;AAa7C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAA;IACjB,mFAAmF;IACnF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,mFAAmF;IACnF,aAAa,EAAE,MAAM,CAAA;IACrB,4FAA4F;IAC5F,YAAY,EAAE,sBAAsB,EAAE,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;
|
|
1
|
+
{"version":3,"file":"slab-support.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-support.ts"],"names":[],"mappings":"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"}
|
|
@@ -396,6 +396,31 @@ const WALL_SLAB_ELEVATION_POOL_EPSILON = 1e-4;
|
|
|
396
396
|
* "never below the ground", which is the same rule with the ground no longer
|
|
397
397
|
* assumed flat.
|
|
398
398
|
*/
|
|
399
|
+
// A rendered slab polygon depends only on the slab set and the level's walls,
|
|
400
|
+
// never on the wall being tested — but a per-frame pass asks for support once
|
|
401
|
+
// per wall, so the identical polygons were rebuilt for every wall on the level
|
|
402
|
+
// (and each rebuild scans all of `levelWalls`). Keyed on array identity: the
|
|
403
|
+
// caller derives those arrays once and only rebuilds them when the scene or a
|
|
404
|
+
// live preview changes, so a hit means the inputs are the same objects.
|
|
405
|
+
let polygonMemoSlabs = null;
|
|
406
|
+
let polygonMemoWalls = null;
|
|
407
|
+
let polygonMemo = new Map();
|
|
408
|
+
function renderedSlabPolygon(slab, slabs, levelWalls) {
|
|
409
|
+
if (polygonMemoSlabs !== slabs || polygonMemoWalls !== levelWalls) {
|
|
410
|
+
polygonMemoSlabs = slabs;
|
|
411
|
+
polygonMemoWalls = levelWalls;
|
|
412
|
+
polygonMemo = new Map();
|
|
413
|
+
}
|
|
414
|
+
const cached = polygonMemo.get(slab.id);
|
|
415
|
+
if (cached)
|
|
416
|
+
return cached;
|
|
417
|
+
const polygon = getRenderableSlabPolygon(slab, {
|
|
418
|
+
walls: levelWalls,
|
|
419
|
+
siblingSlabs: slabs.filter((other) => other.id !== slab.id),
|
|
420
|
+
});
|
|
421
|
+
polygonMemo.set(slab.id, polygon);
|
|
422
|
+
return polygon;
|
|
423
|
+
}
|
|
399
424
|
export function computeWallSlabSupport(wallLike, slabs, levelWalls, preferredSlabId, maxElevation, levelBase = 0) {
|
|
400
425
|
const { start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS } = wallLike;
|
|
401
426
|
const halfThickness = Math.max(thickness / 2, 0);
|
|
@@ -417,10 +442,7 @@ export function computeWallSlabSupport(wallLike, slabs, levelWalls, preferredSla
|
|
|
417
442
|
for (const slab of slabs) {
|
|
418
443
|
if (slab.polygon.length < 3)
|
|
419
444
|
continue;
|
|
420
|
-
const renderedPolygon =
|
|
421
|
-
walls: levelWalls,
|
|
422
|
-
siblingSlabs: slabs.filter((other) => other.id !== slab.id),
|
|
423
|
-
});
|
|
445
|
+
const renderedPolygon = renderedSlabPolygon(slab, slabs, levelWalls);
|
|
424
446
|
let supported = 0;
|
|
425
447
|
const perPolyline = polylines.map((line) => {
|
|
426
448
|
let intervals = polylineInsideIntervals(line, renderedPolygon);
|
|
@@ -1338,6 +1338,7 @@ export declare function getNodesWithLiveStairOpeningInputs(nodes: Record<string,
|
|
|
1338
1338
|
type: "level";
|
|
1339
1339
|
children: (`item_${string}` | `ceiling_${string}` | `column_${string}` | `wall_${string}` | `fence_${string}` | `construction-dimension_${string}` | `structural-grid_${string}` | `zone_${string}` | `slab_${string}` | `roof_${string}` | `stair_${string}` | `scan_${string}` | `guide_${string}` | `measurement_${string}` | `spawn_${string}` | `shelf_${string}` | `duct-segment_${string}` | `duct-fitting_${string}` | `duct-terminal_${string}` | `hvac-equipment_${string}` | `lineset_${string}` | `liquid-line_${string}` | `pipe-segment_${string}` | `pipe-fitting_${string}` | `pipe-trap_${string}`)[];
|
|
1340
1340
|
level: number;
|
|
1341
|
+
baseElevation: number;
|
|
1341
1342
|
name?: string | undefined;
|
|
1342
1343
|
camera?: {
|
|
1343
1344
|
position: [number, number, number];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stair-opening-preview.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC7E,OAA6B,EAAE,KAAK,iBAAiB,EAAE,MAAM,qCAAqC,CAAA;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAGpE,KAAK,oBAAoB,GAAG;IAC1B,EAAE,EAAE,SAAS,CAAA;IACb,IAAI,EAAE,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC,CAAA;CACtC,CAAA;AAoBD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,WAYvC;AAED,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC
|
|
1
|
+
{"version":3,"file":"stair-opening-preview.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC7E,OAA6B,EAAE,KAAK,iBAAiB,EAAE,MAAM,qCAAqC,CAAA;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAA;AAGpE,KAAK,oBAAoB,GAAG;IAC1B,EAAE,EAAE,SAAS,CAAA;IACb,IAAI,EAAE,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC,CAAA;CACtC,CAAA;AAoBD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,WAYvC;AAED,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,EAClD,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACrD,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwBvC;AAED,wBAAgB,qCAAqC;;mBAUlC,oBAAoB,EAAE;;EAiCxC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stair-rise.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-rise.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAoB,MAAM,cAAc,CAAA;AAInF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"stair-rise.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-rise.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAoB,MAAM,cAAc,CAAA;AAInF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAiC9F;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,KAAK,CAAC;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAgClD"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getFloorStackedPosition } from '../../hooks/spatial-grid/floor-placed-elevation';
|
|
2
2
|
import { DEFAULT_LEVEL_HEIGHT } from '../../services/level-height';
|
|
3
|
-
import {
|
|
3
|
+
import { getLevelFloorToFloorHeight } from '../../services/storey';
|
|
4
4
|
export function resolveStairTotalRise(stair, nodes) {
|
|
5
5
|
if (stair.totalRise !== undefined)
|
|
6
6
|
return stair.totalRise;
|
|
@@ -27,7 +27,9 @@ export function resolveStairTotalRise(stair, nodes) {
|
|
|
27
27
|
return (deck.elevation ?? 0.05) - baseElevation;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
return level?.type === 'level'
|
|
30
|
+
return level?.type === 'level'
|
|
31
|
+
? getLevelFloorToFloorHeight(level.id, nodes)
|
|
32
|
+
: DEFAULT_LEVEL_HEIGHT;
|
|
31
33
|
}
|
|
32
34
|
const RISE_SYNC_EPSILON = 1e-4;
|
|
33
35
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wall-mitering.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-mitering.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAO5C,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;CAClB;AASD,KAAK,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAGzE,KAAK,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAmBlD,iBAAS,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,SAAY,GAAG,MAAM,CAG7D;AA8CD,UAAU,QAAQ;IAChB,YAAY,EAAE,OAAO,CAAA;IACrB,cAAc,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,aAAa,CAAA;KAAE,CAAC,CAAA;CACpF;
|
|
1
|
+
{"version":3,"file":"wall-mitering.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-mitering.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAO5C,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;CAClB;AASD,KAAK,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAGzE,KAAK,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAmBlD,iBAAS,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,SAAY,GAAG,MAAM,CAG7D;AA8CD,UAAU,QAAQ;IAChB,YAAY,EAAE,OAAO,CAAA;IACrB,cAAc,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,aAAa,CAAA;KAAE,CAAC,CAAA;CACpF;AAyUD,MAAM,WAAW,aAAa;IAE5B,YAAY,EAAE,YAAY,CAAA;IAE1B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;CACjC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,aAAa,CAWrE;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,QAAQ,EACd,SAAS,EAAE,aAAa,GACvB,uBAAuB,GAAG,IAAI,CA0BhC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CA8C/F;AAGD,OAAO,EAAE,UAAU,EAAE,CAAA"}
|
|
@@ -52,6 +52,49 @@ function pointOnWallSegment(point, wall, tolerance = TOLERANCE) {
|
|
|
52
52
|
const dist = Math.sqrt((point.x - projX) ** 2 + (point.y - projY) ** 2);
|
|
53
53
|
return dist < tolerance;
|
|
54
54
|
}
|
|
55
|
+
// --- Uniform grid used to prefilter T-junction candidates --------------------
|
|
56
|
+
// 2 m cells: small enough that a dense imported floor spreads across many
|
|
57
|
+
// buckets, large enough that an ordinary room wall touches only a few.
|
|
58
|
+
const JUNCTION_GRID_CELL = 2.0;
|
|
59
|
+
// A wall whose AABB would touch more than this many cells (a very long diagonal)
|
|
60
|
+
// is kept in a fallback list checked against every junction. Such walls are rare,
|
|
61
|
+
// and a model made only of them is a model with very few walls — where the naive
|
|
62
|
+
// scan was never the problem.
|
|
63
|
+
const JUNCTION_GRID_MAX_CELLS_PER_WALL = 64;
|
|
64
|
+
function cellKey(x, y) {
|
|
65
|
+
return `${Math.floor(x / JUNCTION_GRID_CELL)},${Math.floor(y / JUNCTION_GRID_CELL)}`;
|
|
66
|
+
}
|
|
67
|
+
function buildJunctionGrid(walls) {
|
|
68
|
+
const grid = new Map();
|
|
69
|
+
const oversized = [];
|
|
70
|
+
for (const wall of walls) {
|
|
71
|
+
// Pad by TOLERANCE so a point sitting exactly on the AABB edge still lands
|
|
72
|
+
// in a covered cell.
|
|
73
|
+
const minX = Math.min(wall.start[0], wall.end[0]) - TOLERANCE;
|
|
74
|
+
const maxX = Math.max(wall.start[0], wall.end[0]) + TOLERANCE;
|
|
75
|
+
const minY = Math.min(wall.start[1], wall.end[1]) - TOLERANCE;
|
|
76
|
+
const maxY = Math.max(wall.start[1], wall.end[1]) + TOLERANCE;
|
|
77
|
+
const cx0 = Math.floor(minX / JUNCTION_GRID_CELL);
|
|
78
|
+
const cx1 = Math.floor(maxX / JUNCTION_GRID_CELL);
|
|
79
|
+
const cy0 = Math.floor(minY / JUNCTION_GRID_CELL);
|
|
80
|
+
const cy1 = Math.floor(maxY / JUNCTION_GRID_CELL);
|
|
81
|
+
if ((cx1 - cx0 + 1) * (cy1 - cy0 + 1) > JUNCTION_GRID_MAX_CELLS_PER_WALL) {
|
|
82
|
+
oversized.push(wall);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (let cx = cx0; cx <= cx1; cx++) {
|
|
86
|
+
for (let cy = cy0; cy <= cy1; cy++) {
|
|
87
|
+
const key = `${cx},${cy}`;
|
|
88
|
+
const bucket = grid.get(key);
|
|
89
|
+
if (bucket)
|
|
90
|
+
bucket.push(wall);
|
|
91
|
+
else
|
|
92
|
+
grid.set(key, [wall]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { grid, oversized };
|
|
97
|
+
}
|
|
55
98
|
function findJunctions(walls) {
|
|
56
99
|
const junctions = new Map();
|
|
57
100
|
// First pass: group walls by their endpoints
|
|
@@ -69,17 +112,47 @@ function findJunctions(walls) {
|
|
|
69
112
|
}
|
|
70
113
|
junctions.get(keyEnd)?.connectedWalls.push({ wall, endType: 'end' });
|
|
71
114
|
}
|
|
72
|
-
// Second pass: detect T-junctions (walls passing through junction points)
|
|
115
|
+
// Second pass: detect T-junctions (walls passing through junction points).
|
|
116
|
+
//
|
|
117
|
+
// The naive form of this pass is `for each junction: for each wall` — O(J×N).
|
|
118
|
+
// On a real imported floor (1081 walls, 2047 endpoint keys) that is ~2.2M
|
|
119
|
+
// pointOnWallSegment calls and measured 584 ms per findJunctions() call, which
|
|
120
|
+
// WallSystem then repeats every frame while progressively rebuilding.
|
|
121
|
+
//
|
|
122
|
+
// A T-junction can only exist where the junction point lies ON the wall
|
|
123
|
+
// segment, so it must lie inside the wall's AABB. Bucketing walls by the grid
|
|
124
|
+
// cells their AABB covers therefore loses nothing: the cell containing the
|
|
125
|
+
// point is always one of the cells the wall was indexed into. With the input
|
|
126
|
+
// ordering restored below, the result matches the naive pass exactly; measured
|
|
127
|
+
// 11 ms on the same geometry.
|
|
128
|
+
const { grid, oversized } = buildJunctionGrid(walls);
|
|
129
|
+
const wallOrder = new Map(walls.map((wall, index) => [wall.id, index]));
|
|
73
130
|
for (const [_key, junction] of junctions.entries()) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
131
|
+
const p = junction.meetingPoint;
|
|
132
|
+
const cellCandidates = grid.get(cellKey(p.x, p.y));
|
|
133
|
+
const passthrough = [];
|
|
134
|
+
for (const bucket of [cellCandidates, oversized]) {
|
|
135
|
+
if (!bucket || bucket.length === 0)
|
|
77
136
|
continue;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
junction.connectedWalls.
|
|
137
|
+
for (const wall of bucket) {
|
|
138
|
+
// Skip if wall already in this junction
|
|
139
|
+
if (junction.connectedWalls.some((cw) => cw.wall.id === wall.id))
|
|
140
|
+
continue;
|
|
141
|
+
// Check if junction point lies on this wall's segment (not at endpoints)
|
|
142
|
+
if (pointOnWallSegment(junction.meetingPoint, wall)) {
|
|
143
|
+
passthrough.push(wall);
|
|
144
|
+
}
|
|
81
145
|
}
|
|
82
146
|
}
|
|
147
|
+
// Append in input order, not bucket order. Two collinear walls overlapping a
|
|
148
|
+
// junction tie on angle in `calculateJunctionIntersections`, so its stable
|
|
149
|
+
// sort leaves them in the order they were appended here — and an oversized
|
|
150
|
+
// wall would otherwise land after a shorter collinear neighbour it precedes
|
|
151
|
+
// in `walls`, picking the other wall's thickness for the miter.
|
|
152
|
+
passthrough.sort((a, b) => (wallOrder.get(a.id) ?? 0) - (wallOrder.get(b.id) ?? 0));
|
|
153
|
+
for (const wall of passthrough) {
|
|
154
|
+
junction.connectedWalls.push({ wall, endType: 'passthrough' });
|
|
155
|
+
}
|
|
83
156
|
}
|
|
84
157
|
// Filter to only junctions with 2+ walls
|
|
85
158
|
const actualJunctions = new Map();
|
|
@@ -186,8 +259,14 @@ function calculateJunctionIntersections(junction, getThickness) {
|
|
|
186
259
|
});
|
|
187
260
|
}
|
|
188
261
|
}
|
|
189
|
-
// Sort by outgoing angle
|
|
190
|
-
|
|
262
|
+
// Sort by outgoing angle, then by wall ID so equal-angle walls produce the
|
|
263
|
+
// same pairing regardless of scene iteration order.
|
|
264
|
+
processedWalls.sort((a, b) => {
|
|
265
|
+
const angleOrder = a.angle - b.angle;
|
|
266
|
+
if (angleOrder !== 0)
|
|
267
|
+
return angleOrder;
|
|
268
|
+
return a.wallId < b.wallId ? -1 : a.wallId > b.wallId ? 1 : 0;
|
|
269
|
+
});
|
|
191
270
|
const wallIntersections = new Map();
|
|
192
271
|
const n = processedWalls.length;
|
|
193
272
|
if (n < 2)
|