@pascal-app/core 1.0.0-beta.4 → 1.0.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 (263) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/dist/capture/index.d.ts +3 -0
  4. package/dist/capture/index.d.ts.map +1 -0
  5. package/dist/capture/index.js +2 -0
  6. package/dist/capture/schema.d.ts +417 -0
  7. package/dist/capture/schema.d.ts.map +1 -0
  8. package/dist/capture/schema.js +348 -0
  9. package/dist/capture/source.d.ts +60 -0
  10. package/dist/capture/source.d.ts.map +1 -0
  11. package/dist/capture/source.js +254 -0
  12. package/dist/events/bus.d.ts +55 -7
  13. package/dist/events/bus.d.ts.map +1 -1
  14. package/dist/events/hidden-wall-pointer-hold.d.ts +28 -0
  15. package/dist/events/hidden-wall-pointer-hold.d.ts.map +1 -0
  16. package/dist/events/hidden-wall-pointer-hold.js +37 -0
  17. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +12 -0
  18. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  19. package/dist/hooks/spatial-grid/spatial-grid-manager.js +39 -1
  20. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +2785 -1
  21. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  22. package/dist/hooks/spatial-grid/spatial-grid-sync.js +132 -19
  23. package/dist/hooks/spatial-grid/support-host-patch.d.ts +45 -1
  24. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -1
  25. package/dist/hooks/spatial-grid/support-host-patch.js +171 -6
  26. package/dist/index.d.ts +15 -6
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +17 -5
  29. package/dist/lib/asset-storage.d.ts.map +1 -1
  30. package/dist/lib/asset-storage.js +4 -1
  31. package/dist/lib/polygon-union.d.ts +4 -0
  32. package/dist/lib/polygon-union.d.ts.map +1 -0
  33. package/dist/lib/polygon-union.js +306 -0
  34. package/dist/lib/roof-overlap.d.ts +32 -0
  35. package/dist/lib/roof-overlap.d.ts.map +1 -0
  36. package/dist/lib/roof-overlap.js +72 -0
  37. package/dist/lib/room-topology-index.d.ts +50 -0
  38. package/dist/lib/room-topology-index.d.ts.map +1 -0
  39. package/dist/lib/room-topology-index.js +237 -0
  40. package/dist/lib/slab-polygon.d.ts +35 -2
  41. package/dist/lib/slab-polygon.d.ts.map +1 -1
  42. package/dist/lib/slab-polygon.js +133 -27
  43. package/dist/lib/space-detection.d.ts +40 -3
  44. package/dist/lib/space-detection.d.ts.map +1 -1
  45. package/dist/lib/space-detection.js +890 -246
  46. package/dist/registry/handles.d.ts +24 -3
  47. package/dist/registry/handles.d.ts.map +1 -1
  48. package/dist/registry/index.d.ts +3 -3
  49. package/dist/registry/index.d.ts.map +1 -1
  50. package/dist/registry/index.js +1 -1
  51. package/dist/registry/registry.d.ts +29 -6
  52. package/dist/registry/registry.d.ts.map +1 -1
  53. package/dist/registry/registry.js +111 -4
  54. package/dist/registry/scene-api.d.ts +16 -0
  55. package/dist/registry/scene-api.d.ts.map +1 -1
  56. package/dist/registry/scene-api.js +49 -1
  57. package/dist/registry/types.d.ts +261 -13
  58. package/dist/registry/types.d.ts.map +1 -1
  59. package/dist/registry/use-registry-version.d.ts +11 -0
  60. package/dist/registry/use-registry-version.d.ts.map +1 -0
  61. package/dist/registry/use-registry-version.js +15 -0
  62. package/dist/schema/__bench__/node-parsers.bench.d.ts +24 -0
  63. package/dist/schema/__bench__/node-parsers.bench.d.ts.map +1 -0
  64. package/dist/schema/__bench__/node-parsers.bench.js +314 -0
  65. package/dist/schema/__fixtures__/node-fixtures.d.ts +28 -0
  66. package/dist/schema/__fixtures__/node-fixtures.d.ts.map +1 -0
  67. package/dist/schema/__fixtures__/node-fixtures.js +125 -0
  68. package/dist/schema/base.d.ts +1 -1
  69. package/dist/schema/base.d.ts.map +1 -1
  70. package/dist/schema/base.js +8 -1
  71. package/dist/schema/compiled-node-parsers.d.ts +40 -0
  72. package/dist/schema/compiled-node-parsers.d.ts.map +1 -0
  73. package/dist/schema/compiled-node-parsers.js +95 -0
  74. package/dist/schema/index.d.ts +20 -15
  75. package/dist/schema/index.d.ts.map +1 -1
  76. package/dist/schema/index.js +19 -12
  77. package/dist/schema/material.d.ts +2 -1
  78. package/dist/schema/material.d.ts.map +1 -1
  79. package/dist/schema/material.js +1 -0
  80. package/dist/schema/nodes/automatic-downspout.d.ts +17 -0
  81. package/dist/schema/nodes/automatic-downspout.d.ts.map +1 -0
  82. package/dist/schema/nodes/automatic-downspout.js +237 -0
  83. package/dist/schema/nodes/block.d.ts +107 -0
  84. package/dist/schema/nodes/block.d.ts.map +1 -0
  85. package/dist/schema/nodes/block.js +207 -0
  86. package/dist/schema/nodes/box-vent.d.ts +8 -2
  87. package/dist/schema/nodes/box-vent.d.ts.map +1 -1
  88. package/dist/schema/nodes/box-vent.js +2 -0
  89. package/dist/schema/nodes/building.d.ts +1 -1
  90. package/dist/schema/nodes/cabinet.d.ts +25 -3
  91. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  92. package/dist/schema/nodes/cabinet.js +26 -5
  93. package/dist/schema/nodes/ceiling.d.ts +1 -1
  94. package/dist/schema/nodes/chimney.d.ts +2 -2
  95. package/dist/schema/nodes/column.d.ts +6 -6
  96. package/dist/schema/nodes/construction-dimension.d.ts +4 -4
  97. package/dist/schema/nodes/cupola.d.ts +9 -1
  98. package/dist/schema/nodes/cupola.d.ts.map +1 -1
  99. package/dist/schema/nodes/cupola.js +2 -0
  100. package/dist/schema/nodes/door.d.ts +1 -1
  101. package/dist/schema/nodes/dormer.d.ts +44 -1
  102. package/dist/schema/nodes/dormer.d.ts.map +1 -1
  103. package/dist/schema/nodes/dormer.js +131 -7
  104. package/dist/schema/nodes/downspout.d.ts +11 -1
  105. package/dist/schema/nodes/downspout.d.ts.map +1 -1
  106. package/dist/schema/nodes/downspout.js +24 -0
  107. package/dist/schema/nodes/duct-fitting.d.ts +18 -1
  108. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -1
  109. package/dist/schema/nodes/duct-fitting.js +25 -4
  110. package/dist/schema/nodes/duct-segment.d.ts +23 -1
  111. package/dist/schema/nodes/duct-segment.d.ts.map +1 -1
  112. package/dist/schema/nodes/duct-segment.js +20 -0
  113. package/dist/schema/nodes/duct-terminal.d.ts +1 -1
  114. package/dist/schema/nodes/elevator.d.ts +1 -1
  115. package/dist/schema/nodes/eyebrow-vent.d.ts +8 -2
  116. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -1
  117. package/dist/schema/nodes/eyebrow-vent.js +2 -0
  118. package/dist/schema/nodes/fence.d.ts +1 -1
  119. package/dist/schema/nodes/guide.d.ts +1 -1
  120. package/dist/schema/nodes/gutter.d.ts +36 -2
  121. package/dist/schema/nodes/gutter.d.ts.map +1 -1
  122. package/dist/schema/nodes/gutter.js +308 -1
  123. package/dist/schema/nodes/hanger-overrides.d.ts +7 -0
  124. package/dist/schema/nodes/hanger-overrides.d.ts.map +1 -0
  125. package/dist/schema/nodes/hanger-overrides.js +6 -0
  126. package/dist/schema/nodes/hvac-equipment.d.ts +1 -1
  127. package/dist/schema/nodes/item.d.ts +4 -3
  128. package/dist/schema/nodes/item.d.ts.map +1 -1
  129. package/dist/schema/nodes/item.js +5 -0
  130. package/dist/schema/nodes/lean-to-extension.d.ts +212 -0
  131. package/dist/schema/nodes/lean-to-extension.d.ts.map +1 -0
  132. package/dist/schema/nodes/lean-to-extension.js +114 -0
  133. package/dist/schema/nodes/level.d.ts +5 -2
  134. package/dist/schema/nodes/level.d.ts.map +1 -1
  135. package/dist/schema/nodes/level.js +9 -0
  136. package/dist/schema/nodes/lineset.d.ts +1 -1
  137. package/dist/schema/nodes/liquid-line.d.ts +1 -1
  138. package/dist/schema/nodes/measurement.d.ts +1 -1
  139. package/dist/schema/nodes/pipe-fitting.d.ts +9 -1
  140. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -1
  141. package/dist/schema/nodes/pipe-fitting.js +6 -2
  142. package/dist/schema/nodes/pipe-segment.d.ts +23 -1
  143. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -1
  144. package/dist/schema/nodes/pipe-segment.js +20 -0
  145. package/dist/schema/nodes/pipe-trap.d.ts +1 -1
  146. package/dist/schema/nodes/ridge-vent.d.ts +1 -1
  147. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  148. package/dist/schema/nodes/ridge-vent.js +4 -1
  149. package/dist/schema/nodes/roof-segment-shape.d.ts +4 -0
  150. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -1
  151. package/dist/schema/nodes/roof-segment-shape.js +64 -1
  152. package/dist/schema/nodes/roof-segment-walls.d.ts +3 -1
  153. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -1
  154. package/dist/schema/nodes/roof-segment-walls.js +1 -1
  155. package/dist/schema/nodes/roof-segment.d.ts +41 -1
  156. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  157. package/dist/schema/nodes/roof-segment.js +79 -2
  158. package/dist/schema/nodes/roof.d.ts +22 -1
  159. package/dist/schema/nodes/roof.d.ts.map +1 -1
  160. package/dist/schema/nodes/roof.js +14 -0
  161. package/dist/schema/nodes/scan.d.ts +19 -2
  162. package/dist/schema/nodes/scan.d.ts.map +1 -1
  163. package/dist/schema/nodes/scan.js +11 -1
  164. package/dist/schema/nodes/shelf.d.ts +2 -2
  165. package/dist/schema/nodes/site.d.ts +1 -1
  166. package/dist/schema/nodes/skylight.d.ts +1 -1
  167. package/dist/schema/nodes/slab.d.ts +1 -1
  168. package/dist/schema/nodes/solar-panel.d.ts +1 -1
  169. package/dist/schema/nodes/spawn.d.ts +1 -1
  170. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  171. package/dist/schema/nodes/stair.d.ts +1 -1
  172. package/dist/schema/nodes/structural-grid.d.ts +1 -1
  173. package/dist/schema/nodes/turbine-vent.d.ts +7 -1
  174. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -1
  175. package/dist/schema/nodes/turbine-vent.js +2 -0
  176. package/dist/schema/nodes/wall.d.ts +4 -4
  177. package/dist/schema/nodes/wall.d.ts.map +1 -1
  178. package/dist/schema/nodes/wall.js +7 -1
  179. package/dist/schema/nodes/window.d.ts +10 -3
  180. package/dist/schema/nodes/window.d.ts.map +1 -1
  181. package/dist/schema/nodes/window.js +4 -0
  182. package/dist/schema/nodes/zone.d.ts +2 -2
  183. package/dist/schema/nodes/zone.js +1 -1
  184. package/dist/schema/types.d.ts +485 -109
  185. package/dist/schema/types.d.ts.map +1 -1
  186. package/dist/schema/types.js +27 -1
  187. package/dist/services/hosting.d.ts +2 -2
  188. package/dist/services/hosting.d.ts.map +1 -1
  189. package/dist/services/hosting.js +4 -4
  190. package/dist/services/index.d.ts +2 -2
  191. package/dist/services/index.d.ts.map +1 -1
  192. package/dist/services/index.js +2 -2
  193. package/dist/services/storey.d.ts +3 -10
  194. package/dist/services/storey.d.ts.map +1 -1
  195. package/dist/services/storey.js +45 -15
  196. package/dist/services/system-graph.d.ts +11 -0
  197. package/dist/services/system-graph.d.ts.map +1 -1
  198. package/dist/services/system-graph.js +35 -5
  199. package/dist/store/actions/node-actions.d.ts +15 -4
  200. package/dist/store/actions/node-actions.d.ts.map +1 -1
  201. package/dist/store/actions/node-actions.js +458 -19
  202. package/dist/store/history-control.d.ts +5 -0
  203. package/dist/store/history-control.d.ts.map +1 -1
  204. package/dist/store/history-control.js +68 -6
  205. package/dist/store/history-invalidation.d.ts +3 -0
  206. package/dist/store/history-invalidation.d.ts.map +1 -0
  207. package/dist/store/history-invalidation.js +72 -0
  208. package/dist/store/scene-hydration.d.ts +5 -0
  209. package/dist/store/scene-hydration.d.ts.map +1 -0
  210. package/dist/store/scene-hydration.js +57 -0
  211. package/dist/store/use-scene.d.ts +3 -0
  212. package/dist/store/use-scene.d.ts.map +1 -1
  213. package/dist/store/use-scene.js +407 -227
  214. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  215. package/dist/systems/elevator/elevator-service.js +16 -11
  216. package/dist/systems/roof/roof-elevation-system.d.ts +3 -0
  217. package/dist/systems/roof/roof-elevation-system.d.ts.map +1 -0
  218. package/dist/systems/roof/roof-elevation-system.js +94 -0
  219. package/dist/systems/roof/roof-elevation.d.ts +4 -0
  220. package/dist/systems/roof/roof-elevation.d.ts.map +1 -0
  221. package/dist/systems/roof/roof-elevation.js +95 -0
  222. package/dist/systems/roof/roof-footprint.d.ts +17 -0
  223. package/dist/systems/roof/roof-footprint.d.ts.map +1 -0
  224. package/dist/systems/roof/roof-footprint.js +83 -0
  225. package/dist/systems/slab/slab-support.d.ts +0 -31
  226. package/dist/systems/slab/slab-support.d.ts.map +1 -1
  227. package/dist/systems/slab/slab-support.js +26 -4
  228. package/dist/systems/stair/stair-flight.d.ts +17 -0
  229. package/dist/systems/stair/stair-flight.d.ts.map +1 -0
  230. package/dist/systems/stair/stair-flight.js +27 -0
  231. package/dist/systems/stair/stair-opening-preview.d.ts +1556 -1328
  232. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  233. package/dist/systems/stair/stair-opening-system.d.ts +1 -0
  234. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  235. package/dist/systems/stair/stair-opening-system.js +93 -96
  236. package/dist/systems/stair/stair-rise.js +21 -20
  237. package/dist/systems/wall/wall-curve.d.ts +1 -0
  238. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  239. package/dist/systems/wall/wall-curve.js +73 -0
  240. package/dist/systems/wall/wall-footprint.d.ts +1 -0
  241. package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
  242. package/dist/systems/wall/wall-footprint.js +1 -0
  243. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  244. package/dist/systems/wall/wall-mitering.js +88 -9
  245. package/dist/systems/wall/wall-topology.d.ts +53 -0
  246. package/dist/systems/wall/wall-topology.d.ts.map +1 -0
  247. package/dist/systems/wall/wall-topology.js +414 -0
  248. package/dist/utils/clone-scene-graph.d.ts +2 -0
  249. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  250. package/dist/utils/clone-scene-graph.js +39 -2
  251. package/dist/utils/scene-migrations.d.ts +1 -0
  252. package/dist/utils/scene-migrations.d.ts.map +1 -1
  253. package/dist/utils/scene-migrations.js +1 -0
  254. package/dist/utils/vertical-scene-migration.d.ts +13 -0
  255. package/dist/utils/vertical-scene-migration.d.ts.map +1 -0
  256. package/dist/utils/vertical-scene-migration.js +179 -0
  257. package/dist/validation/validate-build-json.d.ts +3 -0
  258. package/dist/validation/validate-build-json.d.ts.map +1 -1
  259. package/dist/validation/validate-build-json.js +49 -2
  260. package/package.json +10 -3
  261. package/dist/systems/elevator/elevator-runtime-system.d.ts +0 -2
  262. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +0 -1
  263. package/dist/systems/elevator/elevator-runtime-system.js +0 -8
@@ -0,0 +1,414 @@
1
+ import { GROUND_SUPPORT_ID } from '../../hooks/spatial-grid/support-host-id';
2
+ import { terrainSupportLift } from '../../lib/terrain-support';
3
+ import { getScaledDimensions, WallNode as WallSchema, } from '../../schema';
4
+ import { getWallArcData, getWallCurveFrameAt, getWallCurveLength, isCurvedWall } from './wall-curve';
5
+ const WALL_MIN_LENGTH = 0.01;
6
+ const WALL_SPLIT_ENDPOINT_EPSILON = 0.02;
7
+ const WALL_INTERSECTION_EPSILON = 1e-6;
8
+ function distanceSquared(a, b) {
9
+ const dx = a[0] - b[0];
10
+ const dz = a[1] - b[1];
11
+ return dx * dx + dz * dz;
12
+ }
13
+ function isSegmentLongEnough(start, end) {
14
+ return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH;
15
+ }
16
+ function wallSegmentsCoverSegment(start, end, walls) {
17
+ const dx = end[0] - start[0];
18
+ const dz = end[1] - start[1];
19
+ const lengthSquared = dx * dx + dz * dz;
20
+ if (lengthSquared <= WALL_INTERSECTION_EPSILON * WALL_INTERSECTION_EPSILON)
21
+ return false;
22
+ const length = Math.sqrt(lengthSquared);
23
+ const intervals = [];
24
+ for (const wall of walls) {
25
+ if (Math.abs(wall.curveOffset ?? 0) > WALL_INTERSECTION_EPSILON)
26
+ continue;
27
+ const startDistance = Math.abs((wall.start[0] - start[0]) * dz - (wall.start[1] - start[1]) * dx) / length;
28
+ const endDistance = Math.abs((wall.end[0] - start[0]) * dz - (wall.end[1] - start[1]) * dx) / length;
29
+ if (startDistance > WALL_INTERSECTION_EPSILON || endDistance > WALL_INTERSECTION_EPSILON) {
30
+ continue;
31
+ }
32
+ const wallStartT = ((wall.start[0] - start[0]) * dx + (wall.start[1] - start[1]) * dz) / lengthSquared;
33
+ const wallEndT = ((wall.end[0] - start[0]) * dx + (wall.end[1] - start[1]) * dz) / lengthSquared;
34
+ const intervalStart = Math.max(0, Math.min(wallStartT, wallEndT));
35
+ const intervalEnd = Math.min(1, Math.max(wallStartT, wallEndT));
36
+ if (intervalEnd >= intervalStart)
37
+ intervals.push([intervalStart, intervalEnd]);
38
+ }
39
+ intervals.sort((left, right) => left[0] - right[0]);
40
+ const parameterTolerance = WALL_INTERSECTION_EPSILON / length;
41
+ let coveredUntil = 0;
42
+ for (const [intervalStart, intervalEnd] of intervals) {
43
+ if (intervalStart > coveredUntil + parameterTolerance)
44
+ return false;
45
+ coveredUntil = Math.max(coveredUntil, intervalEnd);
46
+ if (coveredUntil >= 1 - parameterTolerance)
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+ function projectPointOntoWallCenterline(point, wall) {
52
+ if (isCurvedWall(wall)) {
53
+ const arc = getWallArcData(wall);
54
+ if (!arc)
55
+ return null;
56
+ const pointAngle = Math.atan2(point[1] - arc.center.y, point[0] - arc.center.x);
57
+ let directedAngle = (pointAngle - arc.startAngle) * arc.direction;
58
+ while (directedAngle < 0)
59
+ directedAngle += Math.PI * 2;
60
+ const wallT = directedAngle / Math.abs(arc.delta);
61
+ if (wallT <= 0 || wallT >= 1)
62
+ return null;
63
+ return { point: wallPointAt(wall, wallT), wallT };
64
+ }
65
+ const dx = wall.end[0] - wall.start[0];
66
+ const dz = wall.end[1] - wall.start[1];
67
+ const lengthSquared = dx * dx + dz * dz;
68
+ if (lengthSquared < 1e-9)
69
+ return null;
70
+ const wallT = ((point[0] - wall.start[0]) * dx + (point[1] - wall.start[1]) * dz) / lengthSquared;
71
+ if (wallT <= 0 || wallT >= 1)
72
+ return null;
73
+ return {
74
+ point: [wall.start[0] + dx * wallT, wall.start[1] + dz * wallT],
75
+ wallT,
76
+ };
77
+ }
78
+ function nearestWallProjection(point, walls, radius, ignoreWallIds = new Set()) {
79
+ let best = null;
80
+ let bestDistance = Number.POSITIVE_INFINITY;
81
+ for (const wall of walls) {
82
+ if (ignoreWallIds.has(wall.id))
83
+ continue;
84
+ const projection = projectPointOntoWallCenterline(point, wall);
85
+ if (!projection)
86
+ continue;
87
+ const candidateDistance = distanceSquared(point, projection.point);
88
+ if (candidateDistance > radius * radius || candidateDistance >= bestDistance)
89
+ continue;
90
+ const corner = [wall.start, wall.end].find((candidate) => distanceSquared(projection.point, candidate) <=
91
+ WALL_SPLIT_ENDPOINT_EPSILON * WALL_SPLIT_ENDPOINT_EPSILON);
92
+ best = corner
93
+ ? { wall: null, point: [corner[0], corner[1]], wallT: projection.wallT }
94
+ : { wall, ...projection };
95
+ bestDistance = candidateDistance;
96
+ }
97
+ return best;
98
+ }
99
+ export function planWallSplitAtPoint(nodes, args) {
100
+ if (!args.levelId)
101
+ return { ok: false, reason: 'no-host' };
102
+ const walls = Object.values(nodes).filter((node) => node.type === 'wall' && node.parentId === args.levelId);
103
+ const projection = nearestWallProjection(args.point, walls, args.radius, new Set(args.ignoreWallIds ?? []));
104
+ if (!projection)
105
+ return { ok: false, reason: 'no-host' };
106
+ if (!projection.wall) {
107
+ return {
108
+ ok: true,
109
+ plan: { point: projection.point, changes: { create: [], update: [], delete: [] } },
110
+ };
111
+ }
112
+ const split = splitWall(projection.wall, [projection.wallT], nodes);
113
+ if (!split) {
114
+ return {
115
+ ok: true,
116
+ plan: { point: projection.point, changes: { create: [], update: [], delete: [] } },
117
+ };
118
+ }
119
+ return {
120
+ ok: true,
121
+ plan: {
122
+ point: projection.point,
123
+ changes: {
124
+ create: split.create.map((node) => ({ node, parentId: args.levelId ?? undefined })),
125
+ update: split.update,
126
+ delete: [projection.wall.id],
127
+ },
128
+ },
129
+ };
130
+ }
131
+ function straightSegmentIntersection(start, end, wall) {
132
+ const rx = end[0] - start[0];
133
+ const rz = end[1] - start[1];
134
+ const sx = wall.end[0] - wall.start[0];
135
+ const sz = wall.end[1] - wall.start[1];
136
+ const denominator = rx * sz - rz * sx;
137
+ if (Math.abs(denominator) < 1e-9)
138
+ return null;
139
+ const offsetX = wall.start[0] - start[0];
140
+ const offsetZ = wall.start[1] - start[1];
141
+ const draftT = (offsetX * sz - offsetZ * sx) / denominator;
142
+ const wallT = (offsetX * rz - offsetZ * rx) / denominator;
143
+ if (draftT <= 0 || draftT >= 1 || wallT < 0 || wallT > 1)
144
+ return null;
145
+ return {
146
+ wallId: wall.id,
147
+ point: [start[0] + draftT * rx, start[1] + draftT * rz],
148
+ draftT,
149
+ wallT,
150
+ };
151
+ }
152
+ function curvedSegmentIntersections(start, end, wall) {
153
+ const arc = getWallArcData(wall);
154
+ if (!arc)
155
+ return [];
156
+ const dx = end[0] - start[0];
157
+ const dz = end[1] - start[1];
158
+ const offsetX = start[0] - arc.center.x;
159
+ const offsetZ = start[1] - arc.center.y;
160
+ const a = dx * dx + dz * dz;
161
+ if (a < 1e-12)
162
+ return [];
163
+ const b = 2 * (offsetX * dx + offsetZ * dz);
164
+ const c = offsetX * offsetX + offsetZ * offsetZ - arc.radius * arc.radius;
165
+ const discriminant = b * b - 4 * a * c;
166
+ if (discriminant < -1e-9)
167
+ return [];
168
+ const root = Math.sqrt(Math.max(0, discriminant));
169
+ const results = [];
170
+ for (const rawDraftT of [(-b - root) / (2 * a), (-b + root) / (2 * a)]) {
171
+ if (rawDraftT < -1e-9 || rawDraftT > 1 + 1e-9)
172
+ continue;
173
+ const point = [start[0] + rawDraftT * dx, start[1] + rawDraftT * dz];
174
+ const angle = Math.atan2(point[1] - arc.center.y, point[0] - arc.center.x);
175
+ let directedAngle = (angle - arc.startAngle) * arc.direction;
176
+ while (directedAngle < 0)
177
+ directedAngle += Math.PI * 2;
178
+ const rawWallT = directedAngle / Math.abs(arc.delta);
179
+ if (rawWallT < -1e-9 || rawWallT > 1 + 1e-9)
180
+ continue;
181
+ if (results.some((candidate) => distanceSquared(candidate.point, point) < 1e-12))
182
+ continue;
183
+ results.push({
184
+ wallId: wall.id,
185
+ point,
186
+ draftT: Math.max(0, Math.min(1, rawDraftT)),
187
+ wallT: Math.max(0, Math.min(1, rawWallT)),
188
+ });
189
+ }
190
+ return results;
191
+ }
192
+ function joinCrossingAtNearbyWallEndpoint(crossing, walls) {
193
+ const wall = walls.find((candidate) => candidate.id === crossing.wallId);
194
+ if (!wall)
195
+ return crossing;
196
+ const endpointIndex = [wall.start, wall.end].findIndex((endpoint) => distanceSquared(crossing.point, endpoint) <=
197
+ WALL_SPLIT_ENDPOINT_EPSILON * WALL_SPLIT_ENDPOINT_EPSILON);
198
+ if (endpointIndex < 0)
199
+ return crossing;
200
+ const endpoint = endpointIndex === 0 ? wall.start : wall.end;
201
+ return { ...crossing, point: [endpoint[0], endpoint[1]], wallT: endpointIndex };
202
+ }
203
+ function wallLength(wall) {
204
+ return isCurvedWall(wall)
205
+ ? getWallCurveLength(wall)
206
+ : Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]);
207
+ }
208
+ function wallPointAt(wall, wallT) {
209
+ if (wallT <= WALL_INTERSECTION_EPSILON)
210
+ return wall.start;
211
+ if (wallT >= 1 - WALL_INTERSECTION_EPSILON)
212
+ return wall.end;
213
+ const frame = getWallCurveFrameAt(wall, wallT);
214
+ return [frame.point.x, frame.point.y];
215
+ }
216
+ function segmentCurveOffset(wall, startT, endT) {
217
+ const arc = getWallArcData(wall);
218
+ if (!arc)
219
+ return wall.curveOffset;
220
+ const angle = Math.abs(arc.delta) * (endT - startT);
221
+ return arc.direction * arc.radius * (1 - Math.cos(angle / 2));
222
+ }
223
+ function attachmentSpan(node) {
224
+ if (node.type === 'door') {
225
+ const door = node;
226
+ return {
227
+ min: door.position[0] - door.width / 2,
228
+ max: door.position[0] + door.width / 2,
229
+ center: door.position[0],
230
+ };
231
+ }
232
+ if (node.type === 'window') {
233
+ const window = node;
234
+ return {
235
+ min: window.position[0] - window.width / 2,
236
+ max: window.position[0] + window.width / 2,
237
+ center: window.position[0],
238
+ };
239
+ }
240
+ if (node.type === 'item') {
241
+ const item = node;
242
+ if (item.asset.attachTo !== 'wall' && item.asset.attachTo !== 'wall-side')
243
+ return null;
244
+ const [width] = getScaledDimensions(item);
245
+ return {
246
+ min: item.position[0] - width / 2,
247
+ max: item.position[0] + width / 2,
248
+ center: item.position[0],
249
+ };
250
+ }
251
+ return null;
252
+ }
253
+ function wallAttachments(wall, nodes) {
254
+ const ids = new Set((wall.children ?? []));
255
+ for (const node of Object.values(nodes)) {
256
+ if (node.parentId === wall.id ||
257
+ ('wallId' in node && typeof node.wallId === 'string' && node.wallId === wall.id)) {
258
+ ids.add(node.id);
259
+ }
260
+ }
261
+ return [...ids].flatMap((id) => {
262
+ const node = nodes[id];
263
+ return node ? [node] : [];
264
+ });
265
+ }
266
+ function remapAttachment(node, wall, nextLocalX) {
267
+ if (!(node.type === 'door' || node.type === 'window' || node.type === 'item'))
268
+ return null;
269
+ const nextLength = wallLength(wall);
270
+ const clampedX = Math.max(0, Math.min(nextLength, nextLocalX));
271
+ return {
272
+ parentId: wall.id,
273
+ wallId: wall.id,
274
+ position: [clampedX, node.position[1], node.position[2]],
275
+ ...(node.type === 'item' ? { wallT: nextLength > 1e-6 ? clampedX / nextLength : 0 } : {}),
276
+ };
277
+ }
278
+ function splitWall(wall, splitParameters, nodes) {
279
+ const parameters = [
280
+ 0,
281
+ ...splitParameters
282
+ .filter((wallT) => wallT > WALL_INTERSECTION_EPSILON && wallT < 1 - WALL_INTERSECTION_EPSILON)
283
+ .sort((left, right) => left - right),
284
+ 1,
285
+ ];
286
+ const { id: _id, parentId: _parentId, children: _children, ...properties } = wall;
287
+ const parsedSegments = parameters.slice(0, -1).map((startT, index) => {
288
+ const endT = parameters[index + 1];
289
+ return WallSchema.parse({
290
+ ...properties,
291
+ start: wallPointAt(wall, startT),
292
+ end: wallPointAt(wall, endT),
293
+ curveOffset: segmentCurveOffset(wall, startT, endT),
294
+ children: [],
295
+ });
296
+ });
297
+ const originalElevation = wall.supportSlabId === GROUND_SUPPORT_ID && wall.parentId
298
+ ? (terrainSupportLift(nodes, wall.parentId, wall.start[0], wall.start[1]) ?? 0) +
299
+ (wall.supportOffset ?? 0)
300
+ : null;
301
+ const segments = parsedSegments.map((segment) => {
302
+ if (originalElevation === null || !wall.parentId)
303
+ return segment;
304
+ const terrainElevation = terrainSupportLift(nodes, wall.parentId, segment.start[0], segment.start[1]) ?? 0;
305
+ const supportOffset = originalElevation - terrainElevation;
306
+ return {
307
+ ...segment,
308
+ supportOffset: Math.abs(supportOffset) > 1e-6 ? supportOffset : undefined,
309
+ };
310
+ });
311
+ const totalLength = wallLength(wall);
312
+ const segmentChildren = segments.map(() => []);
313
+ const updates = [];
314
+ for (const attachment of wallAttachments(wall, nodes)) {
315
+ const span = attachmentSpan(attachment);
316
+ if (!span)
317
+ return null;
318
+ const segmentIndex = parameters.slice(0, -1).findIndex((startT, index) => {
319
+ const endT = parameters[index + 1];
320
+ return span.min >= totalLength * startT - 1e-4 && span.max <= totalLength * endT + 1e-4;
321
+ });
322
+ if (segmentIndex < 0)
323
+ return null;
324
+ const segment = segments[segmentIndex];
325
+ const update = remapAttachment(attachment, segment, span.center - totalLength * parameters[segmentIndex]);
326
+ if (!update)
327
+ return null;
328
+ segmentChildren[segmentIndex].push(attachment.id);
329
+ updates.push({ id: attachment.id, data: update });
330
+ }
331
+ return {
332
+ create: segments.map((segment, index) => WallSchema.parse({ ...segment, children: segmentChildren[index] })),
333
+ update: updates,
334
+ };
335
+ }
336
+ export function planWallInsertion(nodes, args) {
337
+ const walls = Object.values(nodes).filter((node) => node.type === 'wall' && node.parentId === args.levelId);
338
+ const endProjection = nearestWallProjection(args.end, walls, args.joinRadius);
339
+ const startProjection = nearestWallProjection(args.start, walls, args.joinRadius);
340
+ const resolvedStart = startProjection?.point ?? args.start;
341
+ const resolvedEnd = endProjection?.point ?? args.end;
342
+ if (wallSegmentsCoverSegment(resolvedStart, resolvedEnd, walls)) {
343
+ return { ok: false, reason: 'covered-existing-wall' };
344
+ }
345
+ const crossings = walls
346
+ .flatMap((wall) => isCurvedWall(wall)
347
+ ? curvedSegmentIntersections(resolvedStart, resolvedEnd, wall)
348
+ : [straightSegmentIntersection(resolvedStart, resolvedEnd, wall)].filter((crossing) => crossing !== null))
349
+ .map((crossing) => joinCrossingAtNearbyWallEndpoint(crossing, walls))
350
+ .filter(({ draftT }) => draftT > WALL_INTERSECTION_EPSILON && draftT < 1 - WALL_INTERSECTION_EPSILON)
351
+ .sort((left, right) => left.draftT - right.draftT);
352
+ const splitPoints = crossings.reduce((points, crossing) => {
353
+ if (!points.some((point) => distanceSquared(point, crossing.point) <= 1e-12)) {
354
+ points.push(crossing.point);
355
+ }
356
+ return points;
357
+ }, []);
358
+ const vertices = [resolvedStart, ...splitPoints, resolvedEnd];
359
+ if (vertices.some((start, index) => index < vertices.length - 1 && !isSegmentLongEnough(start, vertices[index + 1]))) {
360
+ return { ok: false, reason: 'segment-too-short' };
361
+ }
362
+ const wallProperties = { ...(args.wallDefaults ?? {}) };
363
+ delete wallProperties.id;
364
+ delete wallProperties.parentId;
365
+ delete wallProperties.children;
366
+ const existingWallCount = Object.values(nodes).filter((node) => node.type === 'wall').length;
367
+ const insertedWalls = vertices.slice(0, -1).map((start, index) => WallSchema.parse({
368
+ ...wallProperties,
369
+ name: `Wall ${existingWallCount + index + 1}`,
370
+ start,
371
+ end: vertices[index + 1],
372
+ }));
373
+ const splitWalls = new Map();
374
+ const addSplitParameter = (wallId, wallT) => {
375
+ const parameters = splitWalls.get(wallId) ?? [];
376
+ if (!parameters.some((candidate) => Math.abs(candidate - wallT) <= WALL_INTERSECTION_EPSILON)) {
377
+ parameters.push(wallT);
378
+ }
379
+ splitWalls.set(wallId, parameters);
380
+ };
381
+ for (const projection of [startProjection, endProjection]) {
382
+ if (projection?.wall) {
383
+ addSplitParameter(projection.wall.id, projection.wallT);
384
+ }
385
+ }
386
+ for (const crossing of crossings) {
387
+ if (crossing.wallT <= WALL_INTERSECTION_EPSILON ||
388
+ crossing.wallT >= 1 - WALL_INTERSECTION_EPSILON) {
389
+ continue;
390
+ }
391
+ addSplitParameter(crossing.wallId, crossing.wallT);
392
+ }
393
+ const splitPlans = [...splitWalls].flatMap(([wallId, parameters]) => {
394
+ const wall = walls.find((candidate) => candidate.id === wallId);
395
+ const split = wall ? splitWall(wall, parameters, nodes) : null;
396
+ return split ? [[wallId, split]] : [];
397
+ });
398
+ const replacementWalls = splitPlans.flatMap(([, split]) => split.create);
399
+ const plan = {
400
+ changes: {
401
+ create: [...replacementWalls, ...insertedWalls].map((node) => ({
402
+ node,
403
+ parentId: args.levelId,
404
+ })),
405
+ update: splitPlans.flatMap(([, split]) => split.update),
406
+ delete: splitPlans.map(([wallId]) => wallId),
407
+ },
408
+ insertedWalls,
409
+ terminalWallId: insertedWalls.at(-1).id,
410
+ resolvedStart,
411
+ resolvedEnd,
412
+ };
413
+ return { ok: true, plan };
414
+ }
@@ -1,9 +1,11 @@
1
1
  import type { AnyNode, AnyNodeId } from '../schema';
2
2
  import type { Collection, CollectionId } from '../schema/collections';
3
+ import type { SceneMaterial, SceneMaterialId } from '../schema/scene-material';
3
4
  export type SceneGraph = {
4
5
  nodes: Record<AnyNodeId, AnyNode>;
5
6
  rootNodeIds: AnyNodeId[];
6
7
  collections?: Record<CollectionId, Collection>;
8
+ materials?: Record<SceneMaterialId, SceneMaterial>;
7
9
  installedPlugins?: string[];
8
10
  };
9
11
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"clone-scene-graph.d.ts","sourceRoot":"","sources":["../../src/utils/clone-scene-graph.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAC9C,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAUD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAuIlE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,OAAO,EAAE,SAAS,GACjB;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAsG/E;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,CAAA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,qBAA0B,GAClC,UAAU,CAqEZ"}
1
+ {"version":3,"file":"clone-scene-graph.d.ts","sourceRoot":"","sources":["../../src/utils/clone-scene-graph.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAE9E,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,CAAA;IAClD,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAUD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CA8JlE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,OAAO,EAAE,SAAS,GACjB;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAqH/E;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,CAAA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,qBAA0B,GAClC,UAAU,CA0EZ"}
@@ -18,7 +18,7 @@ function extractIdPrefix(id) {
18
18
  * - Multi-scene in-memory scenarios
19
19
  */
20
20
  export function cloneSceneGraph(sceneGraph) {
21
- const { nodes, rootNodeIds, collections, installedPlugins } = sceneGraph;
21
+ const { nodes, rootNodeIds, collections, materials, installedPlugins } = sceneGraph;
22
22
  // Build ID mapping: old ID -> new ID
23
23
  const idMap = new Map();
24
24
  // Pass 1: Generate new IDs for all nodes
@@ -64,6 +64,18 @@ export function cloneSceneGraph(sceneGraph) {
64
64
  ;
65
65
  clonedNode.roofSegmentId = idMap.get(clonedNode.roofSegmentId);
66
66
  }
67
+ if ('hostRoofId' in clonedNode && typeof clonedNode.hostRoofId === 'string') {
68
+ ;
69
+ clonedNode.hostRoofId = idMap.get(clonedNode.hostRoofId);
70
+ }
71
+ if ('hostRoofSegmentId' in clonedNode && typeof clonedNode.hostRoofSegmentId === 'string') {
72
+ ;
73
+ clonedNode.hostRoofSegmentId = idMap.get(clonedNode.hostRoofSegmentId);
74
+ }
75
+ if (clonedNode.type === 'roof' && clonedNode.support?.kind === 'roof') {
76
+ clonedNode.support.roofSegmentId = (idMap.get(clonedNode.support.roofSegmentId) ??
77
+ clonedNode.support.roofSegmentId);
78
+ }
67
79
  // Remap supportSlabId (persisted slab-support hosts). The 'ground'
68
80
  // sentinel is not a node id — keep it as-is.
69
81
  if ('supportSlabId' in clonedNode &&
@@ -127,6 +139,12 @@ export function cloneSceneGraph(sceneGraph) {
127
139
  nodes: clonedNodes,
128
140
  rootNodeIds: clonedRootNodeIds,
129
141
  ...(clonedCollections && { collections: clonedCollections }),
142
+ // Material ids are deliberately *not* remapped. Nodes point at these
143
+ // through `slots` values shaped `scene:mat_…` — opaque strings that the
144
+ // node remapping above copies verbatim, since `idMap` only covers node
145
+ // ids. Minting fresh material ids here would orphan every one of those
146
+ // refs and the clone would render with default materials.
147
+ ...(materials && { materials: structuredClone(materials) }),
130
148
  ...(installedPlugins && { installedPlugins: [...installedPlugins] }),
131
149
  };
132
150
  }
@@ -217,6 +235,20 @@ export function cloneLevelSubtree(nodes, levelId) {
217
235
  cloned.roofSegmentId =
218
236
  idMap.get(cloned.roofSegmentId) ?? cloned.roofSegmentId;
219
237
  }
238
+ if ('hostRoofId' in cloned && typeof cloned.hostRoofId === 'string') {
239
+ ;
240
+ cloned.hostRoofId =
241
+ idMap.get(cloned.hostRoofId) ?? cloned.hostRoofId;
242
+ }
243
+ if ('hostRoofSegmentId' in cloned && typeof cloned.hostRoofSegmentId === 'string') {
244
+ ;
245
+ cloned.hostRoofSegmentId =
246
+ idMap.get(cloned.hostRoofSegmentId) ?? cloned.hostRoofSegmentId;
247
+ }
248
+ if (cloned.type === 'roof' && cloned.support?.kind === 'roof') {
249
+ cloned.support.roofSegmentId = (idMap.get(cloned.support.roofSegmentId) ??
250
+ cloned.support.roofSegmentId);
251
+ }
220
252
  // Remap supportSlabId when the host slab is inside the cloned subtree;
221
253
  // preserve it otherwise (like wallId, the reference may point outside).
222
254
  if ('supportSlabId' in cloned && typeof cloned.supportSlabId === 'string') {
@@ -247,7 +279,7 @@ export function forkSceneGraph(sceneGraph, options = {}) {
247
279
  if (options.preserveScans) {
248
280
  return cloneSceneGraph(sceneGraph);
249
281
  }
250
- const { nodes, rootNodeIds, collections, installedPlugins } = sceneGraph;
282
+ const { nodes, rootNodeIds, collections, materials, installedPlugins } = sceneGraph;
251
283
  // First, identify scan and guide node IDs to exclude (user-uploaded imagery)
252
284
  const excludedNodeIds = new Set();
253
285
  for (const [nodeId, node] of Object.entries(nodes)) {
@@ -299,6 +331,11 @@ export function forkSceneGraph(sceneGraph, options = {}) {
299
331
  nodes: filteredNodes,
300
332
  rootNodeIds: filteredRootNodeIds,
301
333
  ...(filteredCollections && { collections: filteredCollections }),
334
+ // Kept whole rather than filtered to the surviving nodes: a palette entry
335
+ // is authored content in its own right, and dropping the scan node that
336
+ // happened to be its only user would silently delete a material the fork's
337
+ // owner can still pick from the palette.
338
+ ...(materials && { materials }),
302
339
  ...(installedPlugins && { installedPlugins }),
303
340
  });
304
341
  }
@@ -1,3 +1,4 @@
1
1
  export { type HealSceneResult, healSceneNodes } from './heal-scene-graph';
2
2
  export { type RetiredSceneNodeMigration, removeRetiredDrawingSheetNodes, } from './retired-scene-nodes';
3
+ export { migrateVerticalSceneNodes, type VerticalSceneMigration, } from './vertical-scene-migration';
3
4
  //# sourceMappingURL=scene-migrations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-migrations.d.ts","sourceRoot":"","sources":["../../src/utils/scene-migrations.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,EACL,KAAK,yBAAyB,EAC9B,8BAA8B,GAC/B,MAAM,uBAAuB,CAAA"}
1
+ {"version":3,"file":"scene-migrations.d.ts","sourceRoot":"","sources":["../../src/utils/scene-migrations.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,EACL,KAAK,yBAAyB,EAC9B,8BAA8B,GAC/B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,yBAAyB,EACzB,KAAK,sBAAsB,GAC5B,MAAM,4BAA4B,CAAA"}
@@ -4,3 +4,4 @@
4
4
  // API routes.
5
5
  export { healSceneNodes } from './heal-scene-graph';
6
6
  export { removeRetiredDrawingSheetNodes, } from './retired-scene-nodes';
7
+ export { migrateVerticalSceneNodes, } from './vertical-scene-migration';
@@ -0,0 +1,13 @@
1
+ export type VerticalSceneMigration = {
2
+ changed: boolean;
3
+ nodes: Record<string, unknown>;
4
+ };
5
+ /**
6
+ * Applies the vertical-model load migration to serialized scene nodes.
7
+ *
8
+ * This must remain pure, idempotent, and server-safe: the editor loader and
9
+ * hosted scene authority both call it so they compare and persist the same
10
+ * canonical fields during collaboration.
11
+ */
12
+ export declare function migrateVerticalSceneNodes(sourceNodes: Record<string, unknown>): VerticalSceneMigration;
13
+ //# sourceMappingURL=vertical-scene-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vertical-scene-migration.d.ts","sourceRoot":"","sources":["../../src/utils/vertical-scene-migration.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B,CAAA;AAuBD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,sBAAsB,CA+KxB"}