@pascal-app/core 0.8.0 → 0.9.1

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 (272) hide show
  1. package/dist/events/bus.d.ts +39 -13
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/scene-registry.d.ts +6 -22
  4. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  5. package/dist/hooks/scene-registry/scene-registry.js +33 -26
  6. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
  7. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
  8. package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
  9. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
  10. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
  11. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +273 -0
  12. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +13 -2
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.js +95 -9
  15. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.js +91 -18
  18. package/dist/index.d.ts +22 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +19 -2
  21. package/dist/lib/polygon-relations.d.ts +10 -0
  22. package/dist/lib/polygon-relations.d.ts.map +1 -0
  23. package/dist/lib/polygon-relations.js +73 -0
  24. package/dist/lib/space-detection-pause.test.d.ts +2 -0
  25. package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
  26. package/dist/lib/space-detection-pause.test.js +42 -0
  27. package/dist/lib/space-detection.d.ts +30 -1
  28. package/dist/lib/space-detection.d.ts.map +1 -1
  29. package/dist/lib/space-detection.js +215 -36
  30. package/dist/lib/space-detection.test.d.ts +2 -0
  31. package/dist/lib/space-detection.test.d.ts.map +1 -0
  32. package/dist/lib/space-detection.test.js +77 -0
  33. package/dist/material-library.d.ts +1 -1
  34. package/dist/material-library.d.ts.map +1 -1
  35. package/dist/material-library.js +1940 -143
  36. package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
  37. package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
  38. package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
  39. package/dist/registry/handles.d.ts +321 -0
  40. package/dist/registry/handles.d.ts.map +1 -0
  41. package/dist/registry/handles.js +24 -0
  42. package/dist/registry/index.d.ts +7 -0
  43. package/dist/registry/index.d.ts.map +1 -0
  44. package/dist/registry/index.js +4 -0
  45. package/dist/registry/registry.d.ts +93 -0
  46. package/dist/registry/registry.d.ts.map +1 -0
  47. package/dist/registry/registry.js +201 -0
  48. package/dist/registry/registry.test.d.ts +2 -0
  49. package/dist/registry/registry.test.d.ts.map +1 -0
  50. package/dist/registry/registry.test.js +183 -0
  51. package/dist/registry/relations-resolver.d.ts +42 -0
  52. package/dist/registry/relations-resolver.d.ts.map +1 -0
  53. package/dist/registry/relations-resolver.js +77 -0
  54. package/dist/registry/relations-resolver.test.d.ts +2 -0
  55. package/dist/registry/relations-resolver.test.d.ts.map +1 -0
  56. package/dist/registry/relations-resolver.test.js +183 -0
  57. package/dist/registry/scene-api.d.ts +44 -0
  58. package/dist/registry/scene-api.d.ts.map +1 -0
  59. package/dist/registry/scene-api.js +111 -0
  60. package/dist/registry/scene-api.test.d.ts +2 -0
  61. package/dist/registry/scene-api.test.d.ts.map +1 -0
  62. package/dist/registry/scene-api.test.js +183 -0
  63. package/dist/registry/subtree.d.ts +67 -0
  64. package/dist/registry/subtree.d.ts.map +1 -0
  65. package/dist/registry/subtree.js +119 -0
  66. package/dist/registry/subtree.test.d.ts +2 -0
  67. package/dist/registry/subtree.test.d.ts.map +1 -0
  68. package/dist/registry/subtree.test.js +133 -0
  69. package/dist/registry/types.d.ts +1420 -0
  70. package/dist/registry/types.d.ts.map +1 -0
  71. package/dist/registry/types.js +1 -0
  72. package/dist/schema/index.d.ts +17 -2
  73. package/dist/schema/index.d.ts.map +1 -1
  74. package/dist/schema/index.js +17 -3
  75. package/dist/schema/material.d.ts +10 -0
  76. package/dist/schema/material.d.ts.map +1 -1
  77. package/dist/schema/material.js +10 -0
  78. package/dist/schema/nodes/box-vent.d.ts +74 -0
  79. package/dist/schema/nodes/box-vent.d.ts.map +1 -0
  80. package/dist/schema/nodes/box-vent.js +63 -0
  81. package/dist/schema/nodes/building.d.ts +1 -1
  82. package/dist/schema/nodes/building.d.ts.map +1 -1
  83. package/dist/schema/nodes/building.js +3 -2
  84. package/dist/schema/nodes/ceiling.d.ts +2 -0
  85. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  86. package/dist/schema/nodes/ceiling.js +1 -1
  87. package/dist/schema/nodes/chimney.d.ts +158 -0
  88. package/dist/schema/nodes/chimney.d.ts.map +1 -0
  89. package/dist/schema/nodes/chimney.js +67 -0
  90. package/dist/schema/nodes/column.d.ts +597 -5
  91. package/dist/schema/nodes/column.d.ts.map +1 -1
  92. package/dist/schema/nodes/column.js +586 -6
  93. package/dist/schema/nodes/cupola.d.ts +66 -0
  94. package/dist/schema/nodes/cupola.d.ts.map +1 -0
  95. package/dist/schema/nodes/cupola.js +28 -0
  96. package/dist/schema/nodes/dormer.d.ts +228 -0
  97. package/dist/schema/nodes/dormer.d.ts.map +1 -0
  98. package/dist/schema/nodes/dormer.js +119 -0
  99. package/dist/schema/nodes/downspout.d.ts +75 -0
  100. package/dist/schema/nodes/downspout.d.ts.map +1 -0
  101. package/dist/schema/nodes/downspout.js +63 -0
  102. package/dist/schema/nodes/elevator.d.ts +105 -0
  103. package/dist/schema/nodes/elevator.d.ts.map +1 -0
  104. package/dist/schema/nodes/elevator.js +54 -0
  105. package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
  106. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
  107. package/dist/schema/nodes/eyebrow-vent.js +40 -0
  108. package/dist/schema/nodes/fence.d.ts +1 -0
  109. package/dist/schema/nodes/fence.d.ts.map +1 -1
  110. package/dist/schema/nodes/fence.js +2 -0
  111. package/dist/schema/nodes/gutter.d.ts +84 -0
  112. package/dist/schema/nodes/gutter.d.ts.map +1 -0
  113. package/dist/schema/nodes/gutter.js +76 -0
  114. package/dist/schema/nodes/item.d.ts +4 -0
  115. package/dist/schema/nodes/item.d.ts.map +1 -1
  116. package/dist/schema/nodes/item.js +9 -0
  117. package/dist/schema/nodes/level.d.ts +1 -1
  118. package/dist/schema/nodes/level.d.ts.map +1 -1
  119. package/dist/schema/nodes/level.js +2 -0
  120. package/dist/schema/nodes/ridge-vent.d.ts +67 -0
  121. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
  122. package/dist/schema/nodes/ridge-vent.js +30 -0
  123. package/dist/schema/nodes/roof-segment.d.ts +195 -1
  124. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  125. package/dist/schema/nodes/roof-segment.js +254 -3
  126. package/dist/schema/nodes/roof.d.ts.map +1 -1
  127. package/dist/schema/nodes/roof.js +4 -16
  128. package/dist/schema/nodes/shelf.d.ts +105 -0
  129. package/dist/schema/nodes/shelf.d.ts.map +1 -0
  130. package/dist/schema/nodes/shelf.js +84 -0
  131. package/dist/schema/nodes/site.d.ts +1 -122
  132. package/dist/schema/nodes/site.d.ts.map +1 -1
  133. package/dist/schema/nodes/site.js +2 -6
  134. package/dist/schema/nodes/skylight.d.ts +279 -0
  135. package/dist/schema/nodes/skylight.d.ts.map +1 -0
  136. package/dist/schema/nodes/skylight.js +152 -0
  137. package/dist/schema/nodes/slab.d.ts +2 -0
  138. package/dist/schema/nodes/slab.d.ts.map +1 -1
  139. package/dist/schema/nodes/slab.js +1 -1
  140. package/dist/schema/nodes/solar-panel.d.ts +117 -0
  141. package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
  142. package/dist/schema/nodes/solar-panel.js +47 -0
  143. package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
  144. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
  145. package/dist/schema/nodes/surface-hole-metadata.js +3 -1
  146. package/dist/schema/nodes/turbine-vent.d.ts +68 -0
  147. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
  148. package/dist/schema/nodes/turbine-vent.js +41 -0
  149. package/dist/schema/types.d.ts +1325 -125
  150. package/dist/schema/types.d.ts.map +1 -1
  151. package/dist/schema/types.js +26 -0
  152. package/dist/services/alignment-anchors.d.ts +100 -0
  153. package/dist/services/alignment-anchors.d.ts.map +1 -0
  154. package/dist/services/alignment-anchors.js +283 -0
  155. package/dist/services/alignment-anchors.test.d.ts +2 -0
  156. package/dist/services/alignment-anchors.test.d.ts.map +1 -0
  157. package/dist/services/alignment-anchors.test.js +383 -0
  158. package/dist/services/alignment.d.ts +126 -0
  159. package/dist/services/alignment.d.ts.map +1 -0
  160. package/dist/services/alignment.js +169 -0
  161. package/dist/services/alignment.test.d.ts +2 -0
  162. package/dist/services/alignment.test.d.ts.map +1 -0
  163. package/dist/services/alignment.test.js +92 -0
  164. package/dist/services/drag-session.d.ts +47 -0
  165. package/dist/services/drag-session.d.ts.map +1 -0
  166. package/dist/services/drag-session.js +100 -0
  167. package/dist/services/drag-session.test.d.ts +2 -0
  168. package/dist/services/drag-session.test.d.ts.map +1 -0
  169. package/dist/services/drag-session.test.js +219 -0
  170. package/dist/services/hosting.d.ts +80 -0
  171. package/dist/services/hosting.d.ts.map +1 -0
  172. package/dist/services/hosting.js +122 -0
  173. package/dist/services/hosting.test.d.ts +2 -0
  174. package/dist/services/hosting.test.d.ts.map +1 -0
  175. package/dist/services/hosting.test.js +199 -0
  176. package/dist/services/index.d.ts +7 -0
  177. package/dist/services/index.d.ts.map +1 -0
  178. package/dist/services/index.js +6 -0
  179. package/dist/services/movement.d.ts +48 -0
  180. package/dist/services/movement.d.ts.map +1 -0
  181. package/dist/services/movement.js +65 -0
  182. package/dist/services/movement.test.d.ts +2 -0
  183. package/dist/services/movement.test.d.ts.map +1 -0
  184. package/dist/services/movement.test.js +133 -0
  185. package/dist/services/single-undo-dance.test.d.ts +2 -0
  186. package/dist/services/single-undo-dance.test.d.ts.map +1 -0
  187. package/dist/services/single-undo-dance.test.js +210 -0
  188. package/dist/services/snap.d.ts +72 -0
  189. package/dist/services/snap.d.ts.map +1 -0
  190. package/dist/services/snap.js +120 -0
  191. package/dist/services/snap.test.d.ts +2 -0
  192. package/dist/services/snap.test.d.ts.map +1 -0
  193. package/dist/services/snap.test.js +95 -0
  194. package/dist/solar-panel-presets.d.ts +17 -0
  195. package/dist/solar-panel-presets.d.ts.map +1 -0
  196. package/dist/solar-panel-presets.js +43 -0
  197. package/dist/store/actions/node-actions.d.ts +14 -2
  198. package/dist/store/actions/node-actions.d.ts.map +1 -1
  199. package/dist/store/actions/node-actions.js +492 -15
  200. package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
  201. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
  202. package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
  203. package/dist/store/actions/reparent.test.d.ts +2 -0
  204. package/dist/store/actions/reparent.test.d.ts.map +1 -0
  205. package/dist/store/actions/reparent.test.js +212 -0
  206. package/dist/store/use-interactive.d.ts +39 -0
  207. package/dist/store/use-interactive.d.ts.map +1 -1
  208. package/dist/store/use-interactive.js +75 -0
  209. package/dist/store/use-live-node-overrides.d.ts +22 -0
  210. package/dist/store/use-live-node-overrides.d.ts.map +1 -0
  211. package/dist/store/use-live-node-overrides.js +59 -0
  212. package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
  213. package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
  214. package/dist/store/use-scene-elevator-migration.test.js +109 -0
  215. package/dist/store/use-scene.d.ts +11 -0
  216. package/dist/store/use-scene.d.ts.map +1 -1
  217. package/dist/store/use-scene.js +196 -6
  218. package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
  219. package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
  220. package/dist/systems/elevator/elevator-dispatch.js +57 -0
  221. package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
  222. package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
  223. package/dist/systems/elevator/elevator-geometry.js +50 -0
  224. package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
  225. package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
  226. package/dist/systems/elevator/elevator-opening-sync.js +218 -0
  227. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
  228. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
  229. package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
  230. package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
  231. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
  232. package/dist/systems/elevator/elevator-opening-system.js +48 -0
  233. package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
  234. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
  235. package/dist/systems/elevator/elevator-runtime-system.js +8 -0
  236. package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
  237. package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
  238. package/dist/systems/elevator/elevator-runtime.js +225 -0
  239. package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
  240. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
  241. package/dist/systems/elevator/elevator-runtime.test.js +64 -0
  242. package/dist/systems/elevator/elevator-service.d.ts +19 -0
  243. package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
  244. package/dist/systems/elevator/elevator-service.js +111 -0
  245. package/dist/systems/stair/stair-footprint.d.ts +44 -0
  246. package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
  247. package/dist/systems/stair/stair-footprint.js +168 -0
  248. package/dist/systems/stair/stair-opening-preview.d.ts +1777 -0
  249. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
  250. package/dist/systems/stair/stair-opening-preview.js +93 -0
  251. package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
  252. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
  253. package/dist/systems/stair/stair-opening-preview.test.js +80 -0
  254. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  255. package/dist/systems/stair/stair-opening-sync.js +172 -147
  256. package/dist/systems/stair/stair-opening-sync.test.js +385 -2
  257. package/dist/systems/stair/stair-opening-system.d.ts +2 -0
  258. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
  259. package/dist/systems/stair/stair-opening-system.js +104 -0
  260. package/dist/systems/wall/wall-move.d.ts +48 -0
  261. package/dist/systems/wall/wall-move.d.ts.map +1 -0
  262. package/dist/systems/wall/wall-move.js +193 -0
  263. package/dist/utils/clone-scene-graph.d.ts +6 -4
  264. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  265. package/dist/utils/clone-scene-graph.js +6 -4
  266. package/dist/utils/clone-scene-graph.test.d.ts +2 -0
  267. package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
  268. package/dist/utils/clone-scene-graph.test.js +59 -0
  269. package/dist/validation/validate-build-json.d.ts +43 -0
  270. package/dist/validation/validate-build-json.d.ts.map +1 -0
  271. package/dist/validation/validate-build-json.js +227 -0
  272. package/package.json +9 -2
@@ -0,0 +1,119 @@
1
+ import { generateId } from '../schema/base';
2
+ function extractIdPrefix(id) {
3
+ const i = id.indexOf('_');
4
+ return i === -1 ? 'node' : id.slice(0, i);
5
+ }
6
+ function getChildIds(node) {
7
+ if ('children' in node && Array.isArray(node.children)) {
8
+ return node.children;
9
+ }
10
+ return [];
11
+ }
12
+ /**
13
+ * Collect the subtree of nodes rooted at `rootId` from the live scene.
14
+ *
15
+ * - BFS walk via `node.children` arrays — order is stable and matches
16
+ * declaration order on container kinds.
17
+ * - Returns the live node references (not clones). Cheap; the caller
18
+ * chooses whether to deep-clone for persistence.
19
+ * - Returns `null` if `rootId` is missing.
20
+ */
21
+ export function collectSubtree(nodes, rootId) {
22
+ const root = nodes[rootId];
23
+ if (!root)
24
+ return null;
25
+ const descendants = [];
26
+ const seen = new Set([rootId]);
27
+ const queue = [...getChildIds(root)];
28
+ let head = 0;
29
+ while (head < queue.length) {
30
+ const id = queue[head++];
31
+ if (seen.has(id))
32
+ continue;
33
+ const node = nodes[id];
34
+ if (!node)
35
+ continue;
36
+ seen.add(id);
37
+ descendants.push(node);
38
+ for (const childId of getChildIds(node))
39
+ queue.push(childId);
40
+ }
41
+ return { root, descendants };
42
+ }
43
+ /**
44
+ * Clone a flat array of nodes with fresh IDs and rewired references,
45
+ * ready to insert via `useScene.createNodes`.
46
+ *
47
+ * Transformations applied:
48
+ * 1. Deep-clone each node via JSON round-trip (strips three.js refs,
49
+ * functions, circular links — same trick `cloneLevelSubtree` uses).
50
+ * 2. Mint a fresh id for every node, preserving the prefix
51
+ * (`wall_…`, `door_…`, etc.) so logs and lookups stay readable.
52
+ * 3. Rewrite `parentId`, `children[]` to use the fresh ids.
53
+ * 4. Stamp `position` onto the root if provided.
54
+ * 5. Set the root's `parentId` to `opts.parentId` when supplied.
55
+ *
56
+ * Intentionally generic — no awareness of host refs (`wallId`/`wallT`
57
+ * etc.). The caller is responsible for stripping or re-deriving those
58
+ * before / after calling this function. See `getHostRefFields(def)`.
59
+ */
60
+ export function cloneNodesInto(nodes, opts) {
61
+ // Phase 1 — mint fresh ids for every node, preserving the prefix.
62
+ const idMap = new Map();
63
+ for (const node of nodes) {
64
+ const prefix = extractIdPrefix(node.id);
65
+ idMap.set(node.id, generateId(prefix));
66
+ }
67
+ const rootFreshId = idMap.get(opts.rootId);
68
+ if (!rootFreshId) {
69
+ throw new Error(`cloneNodesInto: rootId "${opts.rootId}" not found in supplied nodes array`);
70
+ }
71
+ // Phase 2 — clone each node + rewire references.
72
+ const out = [];
73
+ let root = null;
74
+ for (const original of nodes) {
75
+ const cloned = JSON.parse(JSON.stringify(original));
76
+ const freshId = idMap.get(original.id);
77
+ cloned.id = freshId;
78
+ // parentId: root's parentId becomes opts.parentId (or preserved
79
+ // value if not supplied). Descendants point at the remapped parent.
80
+ if (original.id === opts.rootId) {
81
+ ;
82
+ cloned.parentId =
83
+ opts.parentId !== undefined
84
+ ? opts.parentId
85
+ : (cloned.parentId ?? null);
86
+ }
87
+ else if (cloned.parentId) {
88
+ const parentFresh = idMap.get(cloned.parentId);
89
+ cloned.parentId = parentFresh ?? null;
90
+ }
91
+ // children[]: remap any internal references, drop external ones
92
+ // (a descendant pointing at a sibling that didn't make it into
93
+ // `nodes` would dangle — `filter` drops those gracefully).
94
+ if ('children' in cloned && Array.isArray(cloned.children)) {
95
+ ;
96
+ cloned.children = cloned.children
97
+ .map((cid) => idMap.get(cid))
98
+ .filter((cid) => cid !== undefined);
99
+ }
100
+ if (original.id === opts.rootId) {
101
+ if (opts.position) {
102
+ ;
103
+ cloned.position = [
104
+ opts.position[0],
105
+ opts.position[1],
106
+ opts.position[2],
107
+ ];
108
+ }
109
+ root = cloned;
110
+ }
111
+ else {
112
+ out.push(cloned);
113
+ }
114
+ }
115
+ if (!root) {
116
+ throw new Error('cloneNodesInto: root node missing after clone');
117
+ }
118
+ return { rootId: rootFreshId, nodes: [root, ...out], idMap };
119
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=subtree.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subtree.test.d.ts","sourceRoot":"","sources":["../../src/registry/subtree.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,133 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { cloneNodesInto, collectSubtree } from './subtree';
3
+ function makeNode(id, type, extra = {}) {
4
+ return {
5
+ object: 'node',
6
+ id,
7
+ type,
8
+ parentId: null,
9
+ visible: true,
10
+ metadata: {},
11
+ ...extra,
12
+ };
13
+ }
14
+ describe('collectSubtree', () => {
15
+ test('returns null for missing root', () => {
16
+ expect(collectSubtree({}, 'missing')).toBeNull();
17
+ });
18
+ test('returns just the root for a leaf node', () => {
19
+ const root = makeNode('shelf_1', 'shelf', { width: 1 });
20
+ const sub = collectSubtree({ ['shelf_1']: root }, 'shelf_1');
21
+ expect(sub?.root).toBe(root);
22
+ expect(sub?.descendants).toEqual([]);
23
+ });
24
+ test('walks descendants in BFS / declaration order', () => {
25
+ const nodes = {
26
+ ['shelf_1']: makeNode('shelf_1', 'shelf', {
27
+ position: [0, 0, 0],
28
+ children: ['item_a', 'item_b'],
29
+ width: 1,
30
+ }),
31
+ ['item_a']: makeNode('item_a', 'item', {
32
+ parentId: 'shelf_1',
33
+ position: [0, 0, 0],
34
+ }),
35
+ ['item_b']: makeNode('item_b', 'item', {
36
+ parentId: 'shelf_1',
37
+ position: [0.3, 0, 0],
38
+ }),
39
+ };
40
+ const sub = collectSubtree(nodes, 'shelf_1');
41
+ expect(sub?.descendants.map((n) => n.id)).toEqual(['item_a', 'item_b']);
42
+ });
43
+ test('returned nodes are live references — no cloning', () => {
44
+ const item = makeNode('item_a', 'item', { parentId: 'shelf_1', position: [0, 0, 0] });
45
+ const nodes = {
46
+ ['shelf_1']: makeNode('shelf_1', 'shelf', { children: ['item_a'] }),
47
+ ['item_a']: item,
48
+ };
49
+ const sub = collectSubtree(nodes, 'shelf_1');
50
+ expect(sub?.descendants[0]).toBe(item);
51
+ });
52
+ });
53
+ describe('cloneNodesInto', () => {
54
+ test('clones a single root with fresh id and supplied position', () => {
55
+ const original = makeNode('door_orig', 'door', {
56
+ position: [1, 2, 3],
57
+ wallId: 'wall_x',
58
+ width: 0.9,
59
+ });
60
+ const { rootId, nodes } = cloneNodesInto([original], {
61
+ rootId: 'door_orig',
62
+ position: [10, 0, -4],
63
+ });
64
+ expect(nodes).toHaveLength(1);
65
+ const cloned = nodes[0];
66
+ expect(cloned.id).toBe(rootId);
67
+ expect(cloned.id).not.toBe('door_orig');
68
+ expect(cloned.id.startsWith('door_')).toBe(true);
69
+ expect(cloned.position).toEqual([10, 0, -4]);
70
+ expect(cloned.width).toBe(0.9);
71
+ // cloneNodesInto is host-ref-agnostic — wallId is preserved
72
+ // verbatim. Stripping is the caller's job (see getHostRefFields).
73
+ expect(cloned.wallId).toBe('wall_x');
74
+ });
75
+ test('preserves root position when none is supplied', () => {
76
+ const original = makeNode('shelf_orig', 'shelf', { position: [5, 0, 5] });
77
+ const { nodes } = cloneNodesInto([original], { rootId: 'shelf_orig' });
78
+ expect(nodes[0].position).toEqual([5, 0, 5]);
79
+ });
80
+ test('preserves parent/child subtree with remapped ids and relative positions', () => {
81
+ const shelf = makeNode('shelf_1', 'shelf', {
82
+ position: [5, 0, 5],
83
+ children: ['item_a', 'item_b'],
84
+ });
85
+ const itemA = makeNode('item_a', 'item', { parentId: 'shelf_1', position: [0, 0, 0] });
86
+ const itemB = makeNode('item_b', 'item', { parentId: 'shelf_1', position: [0.3, 0, 0] });
87
+ const { rootId, nodes: out } = cloneNodesInto([shelf, itemA, itemB], {
88
+ rootId: 'shelf_1',
89
+ position: [99, 0, -99],
90
+ });
91
+ expect(out).toHaveLength(3);
92
+ const root = out[0];
93
+ expect(root.id).toBe(rootId);
94
+ expect(root.id).not.toBe('shelf_1');
95
+ expect(root.position).toEqual([99, 0, -99]);
96
+ // Root's children rewritten to fresh ids; descendants' parentIds
97
+ // point at the new root id.
98
+ const ids = new Set(out.map((n) => n.id));
99
+ expect(root.children).toHaveLength(2);
100
+ for (const cid of root.children)
101
+ expect(ids.has(cid)).toBe(true);
102
+ for (let i = 1; i < out.length; i += 1) {
103
+ const desc = out[i];
104
+ expect(desc.parentId).toBe(rootId);
105
+ expect(Array.isArray(desc.position)).toBe(true);
106
+ }
107
+ });
108
+ test('parents the cloned root under opts.parentId when supplied', () => {
109
+ const orig = makeNode('shelf_1', 'shelf', { parentId: 'level_old' });
110
+ const { nodes } = cloneNodesInto([orig], {
111
+ rootId: 'shelf_1',
112
+ parentId: 'level_new',
113
+ });
114
+ expect(nodes[0].parentId).toBe('level_new');
115
+ });
116
+ test('two clones produce disjoint id sets', () => {
117
+ const orig = makeNode('shelf_1', 'shelf', {
118
+ position: [0, 0, 0],
119
+ children: ['item_a'],
120
+ });
121
+ const child = makeNode('item_a', 'item', { parentId: 'shelf_1', position: [0, 0, 0] });
122
+ const first = cloneNodesInto([orig, child], { rootId: 'shelf_1' });
123
+ const second = cloneNodesInto([orig, child], { rootId: 'shelf_1' });
124
+ const idsA = new Set(first.nodes.map((n) => n.id));
125
+ const idsB = new Set(second.nodes.map((n) => n.id));
126
+ for (const id of idsA)
127
+ expect(idsB.has(id)).toBe(false);
128
+ });
129
+ test('throws if rootId is missing from the input array', () => {
130
+ const orig = makeNode('shelf_1', 'shelf', {});
131
+ expect(() => cloneNodesInto([orig], { rootId: 'shelf_other' })).toThrow(/rootId/);
132
+ });
133
+ });