@pascal-app/core 0.3.3 → 0.4.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/dist/events/bus.d.ts +4 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +3 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +11 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -8
- package/dist/materials.d.ts +10 -0
- package/dist/materials.d.ts.map +1 -0
- package/dist/materials.js +22 -0
- package/dist/schema/index.d.ts +3 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +3 -1
- package/dist/schema/material.d.ts +2 -2
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/door.d.ts +1 -1
- package/dist/schema/nodes/item.d.ts +2 -2
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +2 -0
- package/dist/schema/nodes/roof-segment.d.ts +1 -1
- package/dist/schema/nodes/roof.d.ts +1 -1
- package/dist/schema/nodes/site.d.ts +1 -1
- package/dist/schema/nodes/slab.d.ts +1 -1
- package/dist/schema/nodes/stair-segment.d.ts +81 -0
- package/dist/schema/nodes/stair-segment.d.ts.map +1 -0
- package/dist/schema/nodes/stair-segment.js +42 -0
- package/dist/schema/nodes/stair.d.ts +56 -0
- package/dist/schema/nodes/stair.d.ts.map +1 -0
- package/dist/schema/nodes/stair.js +22 -0
- package/dist/schema/nodes/wall.d.ts +1 -1
- package/dist/schema/nodes/window.d.ts +1 -1
- package/dist/schema/types.d.ts +128 -10
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +25 -29
- package/dist/store/use-live-transforms.d.ts +14 -0
- package/dist/store/use-live-transforms.d.ts.map +1 -0
- package/dist/store/use-live-transforms.js +20 -0
- package/dist/store/use-scene.d.ts +2 -5
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +25 -15
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +1 -17
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +18 -0
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +71 -26
- package/dist/systems/stair/stair-system.d.ts +2 -0
- package/dist/systems/stair/stair-system.d.ts.map +1 -0
- package/dist/systems/stair/stair-system.js +354 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +2 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +8 -24
- package/dist/utils/clone-scene-graph.d.ts +25 -1
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +160 -5
- package/package.json +6 -1
|
@@ -11,8 +11,8 @@ function extractIdPrefix(id) {
|
|
|
11
11
|
* parent-child relationships and other internal references.
|
|
12
12
|
*
|
|
13
13
|
* This is useful for:
|
|
14
|
+
* - Duplicating a project (host app creates a new project record, then loads the cloned scene)
|
|
14
15
|
* - Copying nodes between different projects
|
|
15
|
-
* - Duplicating a subset of a scene within the same project
|
|
16
16
|
* - Multi-scene in-memory scenarios
|
|
17
17
|
*/
|
|
18
18
|
export function cloneSceneGraph(sceneGraph) {
|
|
@@ -28,17 +28,28 @@ export function cloneSceneGraph(sceneGraph) {
|
|
|
28
28
|
const clonedNodes = {};
|
|
29
29
|
for (const [oldId, node] of Object.entries(nodes)) {
|
|
30
30
|
const newId = idMap.get(oldId);
|
|
31
|
-
// structuredClone to avoid shared references between original and clone
|
|
32
31
|
const clonedNode = structuredClone({ ...node, id: newId });
|
|
33
32
|
// Remap parentId
|
|
34
33
|
if (clonedNode.parentId && typeof clonedNode.parentId === 'string') {
|
|
35
34
|
clonedNode.parentId = (idMap.get(clonedNode.parentId) ?? null);
|
|
36
35
|
}
|
|
37
|
-
// Remap children array (
|
|
36
|
+
// Remap children array (buildings, levels, walls, items, etc.)
|
|
37
|
+
// Children can be either string IDs or embedded node objects (with an `id` property).
|
|
38
|
+
// Normalize both forms to remapped string IDs.
|
|
38
39
|
if ('children' in clonedNode && Array.isArray(clonedNode.children)) {
|
|
39
40
|
;
|
|
40
41
|
clonedNode.children = clonedNode.children
|
|
41
|
-
.map((
|
|
42
|
+
.map((child) => {
|
|
43
|
+
if (typeof child === 'string')
|
|
44
|
+
return idMap.get(child);
|
|
45
|
+
if (child &&
|
|
46
|
+
typeof child === 'object' &&
|
|
47
|
+
'id' in child &&
|
|
48
|
+
typeof child.id === 'string') {
|
|
49
|
+
return idMap.get(child.id);
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
})
|
|
42
53
|
.filter((id) => id !== undefined);
|
|
43
54
|
}
|
|
44
55
|
// Remap wallId (items/doors/windows attached to walls)
|
|
@@ -57,7 +68,6 @@ export function cloneSceneGraph(sceneGraph) {
|
|
|
57
68
|
if (collections) {
|
|
58
69
|
clonedCollections = {};
|
|
59
70
|
const collectionIdMap = new Map();
|
|
60
|
-
// Generate new collection IDs
|
|
61
71
|
for (const collectionId of Object.keys(collections)) {
|
|
62
72
|
collectionIdMap.set(collectionId, generateId('collection'));
|
|
63
73
|
}
|
|
@@ -94,3 +104,148 @@ export function cloneSceneGraph(sceneGraph) {
|
|
|
94
104
|
...(clonedCollections && { collections: clonedCollections }),
|
|
95
105
|
};
|
|
96
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Deep clones a level node and all its descendants with fresh IDs.
|
|
109
|
+
* All internal references (parentId, children, wallId) are remapped to the new IDs.
|
|
110
|
+
* The cloned level node's parentId is preserved (building ID) — not remapped.
|
|
111
|
+
*
|
|
112
|
+
* Unlike `cloneSceneGraph` (which operates on serialized data), this function works
|
|
113
|
+
* on live runtime nodes that may have non-serializable properties (Three.js objects,
|
|
114
|
+
* etc.). It uses JSON roundtrip to safely strip them.
|
|
115
|
+
*
|
|
116
|
+
* @returns clonedNodes - flat array of all cloned nodes (level + descendants)
|
|
117
|
+
* @returns newLevelId - the ID of the cloned level node
|
|
118
|
+
* @returns idMap - old ID → new ID mapping
|
|
119
|
+
*/
|
|
120
|
+
export function cloneLevelSubtree(nodes, levelId) {
|
|
121
|
+
const levelNode = nodes[levelId];
|
|
122
|
+
if (!levelNode || levelNode.type !== 'level') {
|
|
123
|
+
throw new Error(`Node "${levelId}" is not a level`);
|
|
124
|
+
}
|
|
125
|
+
// Recursively collect the level node + all descendants via children arrays
|
|
126
|
+
const subtreeIds = new Set();
|
|
127
|
+
const collect = (id) => {
|
|
128
|
+
if (subtreeIds.has(id))
|
|
129
|
+
return;
|
|
130
|
+
const node = nodes[id];
|
|
131
|
+
if (!node)
|
|
132
|
+
return;
|
|
133
|
+
subtreeIds.add(id);
|
|
134
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
135
|
+
for (const childId of node.children) {
|
|
136
|
+
collect(childId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
collect(levelId);
|
|
141
|
+
// Build ID mapping: old → new
|
|
142
|
+
const idMap = new Map();
|
|
143
|
+
for (const oldId of subtreeIds) {
|
|
144
|
+
const prefix = extractIdPrefix(oldId);
|
|
145
|
+
idMap.set(oldId, generateId(prefix));
|
|
146
|
+
}
|
|
147
|
+
const newLevelId = idMap.get(levelId);
|
|
148
|
+
// Clone each node with remapped references.
|
|
149
|
+
// Use JSON roundtrip instead of structuredClone because live runtime nodes may
|
|
150
|
+
// carry non-serializable properties (Three.js Object3D refs, functions, etc.)
|
|
151
|
+
// that structuredClone would throw on.
|
|
152
|
+
const clonedNodes = [];
|
|
153
|
+
for (const oldId of subtreeIds) {
|
|
154
|
+
const node = nodes[oldId];
|
|
155
|
+
if (!node)
|
|
156
|
+
continue;
|
|
157
|
+
const newId = idMap.get(oldId);
|
|
158
|
+
// JSON roundtrip: safely strips functions, Object3D, circular refs, etc.
|
|
159
|
+
const cloned = JSON.parse(JSON.stringify(node));
|
|
160
|
+
cloned.id = newId;
|
|
161
|
+
// Remap parentId — but only for descendants, not the level node itself
|
|
162
|
+
// (the level's parentId points to the building, which is outside the subtree)
|
|
163
|
+
if (oldId !== levelId && cloned.parentId && typeof cloned.parentId === 'string') {
|
|
164
|
+
cloned.parentId = (idMap.get(cloned.parentId) ?? cloned.parentId);
|
|
165
|
+
}
|
|
166
|
+
// Remap children array
|
|
167
|
+
if ('children' in cloned && Array.isArray(cloned.children)) {
|
|
168
|
+
;
|
|
169
|
+
cloned.children = cloned.children
|
|
170
|
+
.map((child) => {
|
|
171
|
+
if (typeof child === 'string')
|
|
172
|
+
return idMap.get(child) ?? child;
|
|
173
|
+
if (child &&
|
|
174
|
+
typeof child === 'object' &&
|
|
175
|
+
'id' in child &&
|
|
176
|
+
typeof child.id === 'string') {
|
|
177
|
+
return idMap.get(child.id) ?? child.id;
|
|
178
|
+
}
|
|
179
|
+
return child;
|
|
180
|
+
})
|
|
181
|
+
.filter((id) => typeof id === 'string');
|
|
182
|
+
}
|
|
183
|
+
// Remap wallId (doors/windows attached to walls)
|
|
184
|
+
if ('wallId' in cloned && typeof cloned.wallId === 'string') {
|
|
185
|
+
;
|
|
186
|
+
cloned.wallId = idMap.get(cloned.wallId) ?? cloned.wallId;
|
|
187
|
+
}
|
|
188
|
+
clonedNodes.push(cloned);
|
|
189
|
+
}
|
|
190
|
+
return { clonedNodes, newLevelId, idMap };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Forks a scene graph for use as a new project: clones with new IDs and strips
|
|
194
|
+
* scan and guide nodes (and their references) since those contain user-uploaded
|
|
195
|
+
* imagery that shouldn't carry over to a forked project.
|
|
196
|
+
*/
|
|
197
|
+
export function forkSceneGraph(sceneGraph) {
|
|
198
|
+
const { nodes, rootNodeIds, collections } = sceneGraph;
|
|
199
|
+
// First, identify scan and guide node IDs to exclude (user-uploaded imagery)
|
|
200
|
+
const excludedNodeIds = new Set();
|
|
201
|
+
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
202
|
+
if (node.type === 'scan' || node.type === 'guide') {
|
|
203
|
+
excludedNodeIds.add(nodeId);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Build a filtered scene graph without scan nodes
|
|
207
|
+
const filteredNodes = {};
|
|
208
|
+
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
209
|
+
if (excludedNodeIds.has(nodeId))
|
|
210
|
+
continue;
|
|
211
|
+
const clonedNode = structuredClone(node);
|
|
212
|
+
// Remove scan children from any parent that references them.
|
|
213
|
+
// Children can be string IDs or embedded node objects.
|
|
214
|
+
if ('children' in clonedNode && Array.isArray(clonedNode.children)) {
|
|
215
|
+
;
|
|
216
|
+
clonedNode.children = clonedNode.children.filter((child) => {
|
|
217
|
+
const childId = typeof child === 'string'
|
|
218
|
+
? child
|
|
219
|
+
: child && typeof child === 'object' && 'id' in child
|
|
220
|
+
? child.id
|
|
221
|
+
: null;
|
|
222
|
+
return childId ? !excludedNodeIds.has(childId) : true;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
filteredNodes[nodeId] = clonedNode;
|
|
226
|
+
}
|
|
227
|
+
const filteredRootNodeIds = rootNodeIds.filter((id) => !excludedNodeIds.has(id));
|
|
228
|
+
// Filter collections to remove references to scan nodes
|
|
229
|
+
let filteredCollections;
|
|
230
|
+
if (collections) {
|
|
231
|
+
filteredCollections = {};
|
|
232
|
+
for (const [collectionId, collection] of Object.entries(collections)) {
|
|
233
|
+
const filteredNodeIds = collection.nodeIds.filter((id) => !excludedNodeIds.has(id));
|
|
234
|
+
if (filteredNodeIds.length > 0) {
|
|
235
|
+
filteredCollections[collectionId] = {
|
|
236
|
+
...collection,
|
|
237
|
+
nodeIds: filteredNodeIds,
|
|
238
|
+
controlNodeId: collection.controlNodeId && excludedNodeIds.has(collection.controlNodeId)
|
|
239
|
+
? undefined
|
|
240
|
+
: collection.controlNodeId,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Now clone the filtered graph with new IDs
|
|
246
|
+
return cloneSceneGraph({
|
|
247
|
+
nodes: filteredNodes,
|
|
248
|
+
rootNodeIds: filteredRootNodeIds,
|
|
249
|
+
...(filteredCollections && { collections: filteredCollections }),
|
|
250
|
+
});
|
|
251
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Core library for Pascal 3D building editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./clone-scene-graph": {
|
|
15
|
+
"types": "./dist/utils/clone-scene-graph.d.ts",
|
|
16
|
+
"import": "./dist/utils/clone-scene-graph.js",
|
|
17
|
+
"default": "./dist/utils/clone-scene-graph.js"
|
|
13
18
|
}
|
|
14
19
|
},
|
|
15
20
|
"files": [
|