@pascal-app/core 0.3.2 → 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.
Files changed (54) hide show
  1. package/dist/events/bus.d.ts +4 -2
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/scene-registry.d.ts +2 -0
  4. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  5. package/dist/hooks/scene-registry/scene-registry.js +3 -0
  6. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +1 -1
  7. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  8. package/dist/hooks/spatial-grid/spatial-grid-sync.js +11 -3
  9. package/dist/index.d.ts +6 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +4 -8
  12. package/dist/materials.d.ts +10 -0
  13. package/dist/materials.d.ts.map +1 -0
  14. package/dist/materials.js +22 -0
  15. package/dist/schema/index.d.ts +3 -1
  16. package/dist/schema/index.d.ts.map +1 -1
  17. package/dist/schema/index.js +3 -1
  18. package/dist/schema/nodes/level.d.ts +1 -1
  19. package/dist/schema/nodes/level.d.ts.map +1 -1
  20. package/dist/schema/nodes/level.js +2 -0
  21. package/dist/schema/nodes/stair-segment.d.ts +81 -0
  22. package/dist/schema/nodes/stair-segment.d.ts.map +1 -0
  23. package/dist/schema/nodes/stair-segment.js +42 -0
  24. package/dist/schema/nodes/stair.d.ts +56 -0
  25. package/dist/schema/nodes/stair.d.ts.map +1 -0
  26. package/dist/schema/nodes/stair.js +22 -0
  27. package/dist/schema/types.d.ts +119 -1
  28. package/dist/schema/types.d.ts.map +1 -1
  29. package/dist/schema/types.js +4 -0
  30. package/dist/store/actions/node-actions.d.ts.map +1 -1
  31. package/dist/store/actions/node-actions.js +25 -29
  32. package/dist/store/use-live-transforms.d.ts +14 -0
  33. package/dist/store/use-live-transforms.d.ts.map +1 -0
  34. package/dist/store/use-live-transforms.js +20 -0
  35. package/dist/store/use-scene.d.ts +2 -5
  36. package/dist/store/use-scene.d.ts.map +1 -1
  37. package/dist/store/use-scene.js +25 -15
  38. package/dist/systems/door/door-system.d.ts.map +1 -1
  39. package/dist/systems/door/door-system.js +1 -17
  40. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  41. package/dist/systems/roof/roof-system.js +18 -0
  42. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  43. package/dist/systems/slab/slab-system.js +71 -26
  44. package/dist/systems/stair/stair-system.d.ts +2 -0
  45. package/dist/systems/stair/stair-system.d.ts.map +1 -0
  46. package/dist/systems/stair/stair-system.js +354 -0
  47. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  48. package/dist/systems/wall/wall-system.js +2 -0
  49. package/dist/systems/window/window-system.d.ts.map +1 -1
  50. package/dist/systems/window/window-system.js +8 -24
  51. package/dist/utils/clone-scene-graph.d.ts +25 -1
  52. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  53. package/dist/utils/clone-scene-graph.js +160 -5
  54. 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 (walls, levels, buildings, sites, items can have children)
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((childId) => idMap.get(childId))
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.2",
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": [