@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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { GROUND_SUPPORT_ID } from '../hooks/spatial-grid/support-host-id';
|
|
2
|
+
import { deriveLegacyLevelHeight } from '../services/level-height';
|
|
3
|
+
import { getCeilingClampBound } from '../services/storey';
|
|
4
|
+
import { computeWallSlabSupport } from '../systems/slab/slab-support';
|
|
5
|
+
import { DEFAULT_WALL_HEIGHT } from '../systems/wall/wall-footprint';
|
|
6
|
+
function getFiniteNumber(value, fallback) {
|
|
7
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
8
|
+
}
|
|
9
|
+
function getStringArray(value) {
|
|
10
|
+
return Array.isArray(value)
|
|
11
|
+
? value.filter((entry) => typeof entry === 'string')
|
|
12
|
+
: [];
|
|
13
|
+
}
|
|
14
|
+
// Walls whose top lands within this of the storey plane become plane-bound;
|
|
15
|
+
// ceilings whose stored height lands within this of their clamp bound become
|
|
16
|
+
// follows-mode. The strict comparison preserves intentional 0.20-short walls.
|
|
17
|
+
const PLANE_BOUND_EPSILON = 0.2;
|
|
18
|
+
// A ground-pinned wall counts as buried only when the elected slab's occupied
|
|
19
|
+
// interval strictly straddles the pinned base. The tolerance absorbs float
|
|
20
|
+
// drift in stamped offsets without capturing a slab that merely touches the
|
|
21
|
+
// base from above.
|
|
22
|
+
const BURIED_PIN_EPSILON = 1e-3;
|
|
23
|
+
/**
|
|
24
|
+
* Applies the vertical-model load migration to serialized scene nodes.
|
|
25
|
+
*
|
|
26
|
+
* This must remain pure, idempotent, and server-safe: the editor loader and
|
|
27
|
+
* hosted scene authority both call it so they compare and persist the same
|
|
28
|
+
* canonical fields during collaboration.
|
|
29
|
+
*/
|
|
30
|
+
export function migrateVerticalSceneNodes(sourceNodes) {
|
|
31
|
+
const nodes = { ...sourceNodes };
|
|
32
|
+
let changed = false;
|
|
33
|
+
const replaceNode = (id, node) => {
|
|
34
|
+
nodes[id] = node;
|
|
35
|
+
changed = true;
|
|
36
|
+
};
|
|
37
|
+
// A level without `height` marks a scene saved before the vertical model
|
|
38
|
+
// landed. Compute the gate before mutating anything so stair and ceiling
|
|
39
|
+
// intent on already-migrated scenes is never reclassified.
|
|
40
|
+
const isLegacyScene = Object.values(nodes).some((node) => node?.type === 'level' && !('height' in node));
|
|
41
|
+
// Ordinals are semantic and compact independently within each building.
|
|
42
|
+
const buildingNodes = Object.values(nodes).filter((node) => node?.type === 'building');
|
|
43
|
+
const levelsByBuilding = new Map();
|
|
44
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
45
|
+
if (node?.type !== 'level')
|
|
46
|
+
continue;
|
|
47
|
+
const buildingId = buildingNodes.find((building) => building.id === node.parentId)?.id ??
|
|
48
|
+
buildingNodes.find((building) => getStringArray(building.children).includes(id))?.id ??
|
|
49
|
+
null;
|
|
50
|
+
const bucket = levelsByBuilding.get(buildingId) ?? [];
|
|
51
|
+
bucket.push({ id, ordinal: getFiniteNumber(node.level, 0) });
|
|
52
|
+
levelsByBuilding.set(buildingId, bucket);
|
|
53
|
+
}
|
|
54
|
+
for (const bucket of levelsByBuilding.values()) {
|
|
55
|
+
const sorted = [...bucket].sort((a, b) => a.ordinal - b.ordinal);
|
|
56
|
+
const negativeCount = sorted.filter((entry) => entry.ordinal < 0).length;
|
|
57
|
+
sorted.forEach((entry, index) => {
|
|
58
|
+
const nextOrdinal = index - negativeCount;
|
|
59
|
+
const current = nodes[entry.id];
|
|
60
|
+
if (current.level !== nextOrdinal) {
|
|
61
|
+
replaceNode(entry.id, { ...current, level: nextOrdinal });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Materialize exact legacy storey planes before classifying wall tops.
|
|
66
|
+
const legacyLevelIds = Object.entries(nodes)
|
|
67
|
+
.filter(([, node]) => node?.type === 'level' && !('height' in node))
|
|
68
|
+
.map(([id]) => id);
|
|
69
|
+
const derivedHeights = new Map();
|
|
70
|
+
for (const levelId of legacyLevelIds) {
|
|
71
|
+
derivedHeights.set(levelId, deriveLegacyLevelHeight(levelId, nodes));
|
|
72
|
+
}
|
|
73
|
+
for (const levelId of legacyLevelIds) {
|
|
74
|
+
const plane = derivedHeights.get(levelId);
|
|
75
|
+
const level = nodes[levelId];
|
|
76
|
+
replaceNode(levelId, { ...level, height: plane });
|
|
77
|
+
const children = getStringArray(level.children)
|
|
78
|
+
.map((childId) => nodes[childId])
|
|
79
|
+
.filter((child) => child !== undefined);
|
|
80
|
+
const slabs = children.filter((child) => child.type === 'slab');
|
|
81
|
+
const walls = children.filter((child) => child.type === 'wall');
|
|
82
|
+
for (const wall of walls) {
|
|
83
|
+
const electedBase = computeWallSlabSupport({
|
|
84
|
+
start: wall.start,
|
|
85
|
+
end: wall.end,
|
|
86
|
+
curveOffset: wall.curveOffset,
|
|
87
|
+
thickness: wall.thickness,
|
|
88
|
+
}, slabs, walls).elevation;
|
|
89
|
+
const effectiveHeight = wall.height ?? DEFAULT_WALL_HEIGHT;
|
|
90
|
+
const top = Math.max(0, electedBase) + effectiveHeight;
|
|
91
|
+
if (Math.abs(plane - top) < PLANE_BOUND_EPSILON) {
|
|
92
|
+
if ('height' in wall) {
|
|
93
|
+
const { height: _height, ...planeBound } = wall;
|
|
94
|
+
replaceNode(wall.id, planeBound);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (wall.height !== effectiveHeight) {
|
|
98
|
+
replaceNode(wall.id, { ...wall, height: effectiveHeight });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (isLegacyScene) {
|
|
103
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
104
|
+
if (node?.type !== 'stair' || node.totalRise !== 2.5)
|
|
105
|
+
continue;
|
|
106
|
+
const { totalRise: _totalRise, ...derivedRise } = node;
|
|
107
|
+
replaceNode(id, derivedRise);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Preserve the exact occupied interval of legacy slabs.
|
|
111
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
112
|
+
if (node?.type !== 'slab' || 'thickness' in node)
|
|
113
|
+
continue;
|
|
114
|
+
const elevation = getFiniteNumber(node.elevation, 0.05);
|
|
115
|
+
replaceNode(id, elevation < 0
|
|
116
|
+
? { ...node, thickness: 0.05, recessed: true }
|
|
117
|
+
: { ...node, thickness: elevation });
|
|
118
|
+
}
|
|
119
|
+
if (isLegacyScene) {
|
|
120
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
121
|
+
if (node?.type !== 'ceiling' || !('height' in node))
|
|
122
|
+
continue;
|
|
123
|
+
const dropHeight = () => {
|
|
124
|
+
const { height: _height, ...follows } = node;
|
|
125
|
+
replaceNode(id, follows);
|
|
126
|
+
};
|
|
127
|
+
if (node.autoFromWalls === true) {
|
|
128
|
+
dropHeight();
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (typeof node.parentId !== 'string')
|
|
132
|
+
continue;
|
|
133
|
+
const bound = getCeilingClampBound(node.parentId, nodes, Array.isArray(node.polygon) ? node.polygon : []);
|
|
134
|
+
const stored = getFiniteNumber(node.height, Number.NaN);
|
|
135
|
+
if (Number.isFinite(bound) && Math.abs(stored - bound) < PLANE_BOUND_EPSILON) {
|
|
136
|
+
dropHeight();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Terrain-sculpt-era drafting stamped `supportSlabId: 'ground'` onto walls
|
|
141
|
+
// drawn in 3D on flat scenes. The pin short-circuits slab election, so a
|
|
142
|
+
// wall whose feet sit inside a floor slab keeps its base at the level floor
|
|
143
|
+
// — buried in the slab, z-fighting its side faces — while the panel shows
|
|
144
|
+
// the base as automatic. Heal the pin only in that buried state: a deck
|
|
145
|
+
// hovering above an intentionally grounded wall must keep its pin (dropping
|
|
146
|
+
// it would lift the wall onto the deck). Scenes with sculpted terrain are
|
|
147
|
+
// skipped wholesale — a ground host is load-bearing there, and the live
|
|
148
|
+
// terrain field isn't visible to this pure pass.
|
|
149
|
+
const hasSculptedTerrain = Object.values(nodes).some((node) => node?.type === 'site' && node.terrain != null);
|
|
150
|
+
if (!hasSculptedTerrain) {
|
|
151
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
152
|
+
if (node?.type !== 'wall' || node.supportSlabId !== GROUND_SUPPORT_ID)
|
|
153
|
+
continue;
|
|
154
|
+
const levelId = typeof node.parentId === 'string' ? node.parentId : null;
|
|
155
|
+
if (!levelId || nodes[levelId]?.type !== 'level')
|
|
156
|
+
continue;
|
|
157
|
+
const siblings = Object.values(nodes).filter((sibling) => sibling != null && sibling.parentId === levelId);
|
|
158
|
+
const support = computeWallSlabSupport({
|
|
159
|
+
start: node.start,
|
|
160
|
+
end: node.end,
|
|
161
|
+
curveOffset: node.curveOffset,
|
|
162
|
+
thickness: node.thickness,
|
|
163
|
+
}, siblings.filter((sibling) => sibling.type === 'slab'), siblings.filter((sibling) => sibling.type === 'wall'));
|
|
164
|
+
const elected = support.electedSlabId ? nodes[support.electedSlabId] : null;
|
|
165
|
+
if (!elected)
|
|
166
|
+
continue;
|
|
167
|
+
const pinnedBase = getFiniteNumber(node.supportOffset, 0);
|
|
168
|
+
const electedTop = getFiniteNumber(elected.elevation, 0.05);
|
|
169
|
+
const electedBottom = electedTop - getFiniteNumber(elected.thickness, 0.05);
|
|
170
|
+
const buried = electedBottom <= pinnedBase + BURIED_PIN_EPSILON &&
|
|
171
|
+
electedTop > pinnedBase + BURIED_PIN_EPSILON;
|
|
172
|
+
if (!buried)
|
|
173
|
+
continue;
|
|
174
|
+
const { supportSlabId: _host, supportOffset: _offset, ...healed } = node;
|
|
175
|
+
replaceNode(id, healed);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return changed ? { changed, nodes } : { changed, nodes: sourceNodes };
|
|
179
|
+
}
|