@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,179 @@
1
+ import { GROUND_SUPPORT_ID } from '../hooks/spatial-grid/support-host-id';
2
+ import { deriveLegacyLevelHeight } from '../services/level-height';
3
+ import { getCeilingClampBound } from '../services/storey';
4
+ import { computeWallSlabSupport } from '../systems/slab/slab-support';
5
+ import { DEFAULT_WALL_HEIGHT } from '../systems/wall/wall-footprint';
6
+ function getFiniteNumber(value, fallback) {
7
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
8
+ }
9
+ function getStringArray(value) {
10
+ return Array.isArray(value)
11
+ ? value.filter((entry) => typeof entry === 'string')
12
+ : [];
13
+ }
14
+ // Walls whose top lands within this of the storey plane become plane-bound;
15
+ // ceilings whose stored height lands within this of their clamp bound become
16
+ // follows-mode. The strict comparison preserves intentional 0.20-short walls.
17
+ const PLANE_BOUND_EPSILON = 0.2;
18
+ // A ground-pinned wall counts as buried only when the elected slab's occupied
19
+ // interval strictly straddles the pinned base. The tolerance absorbs float
20
+ // drift in stamped offsets without capturing a slab that merely touches the
21
+ // base from above.
22
+ const BURIED_PIN_EPSILON = 1e-3;
23
+ /**
24
+ * Applies the vertical-model load migration to serialized scene nodes.
25
+ *
26
+ * This must remain pure, idempotent, and server-safe: the editor loader and
27
+ * hosted scene authority both call it so they compare and persist the same
28
+ * canonical fields during collaboration.
29
+ */
30
+ export function migrateVerticalSceneNodes(sourceNodes) {
31
+ const nodes = { ...sourceNodes };
32
+ let changed = false;
33
+ const replaceNode = (id, node) => {
34
+ nodes[id] = node;
35
+ changed = true;
36
+ };
37
+ // A level without `height` marks a scene saved before the vertical model
38
+ // landed. Compute the gate before mutating anything so stair and ceiling
39
+ // intent on already-migrated scenes is never reclassified.
40
+ const isLegacyScene = Object.values(nodes).some((node) => node?.type === 'level' && !('height' in node));
41
+ // Ordinals are semantic and compact independently within each building.
42
+ const buildingNodes = Object.values(nodes).filter((node) => node?.type === 'building');
43
+ const levelsByBuilding = new Map();
44
+ for (const [id, node] of Object.entries(nodes)) {
45
+ if (node?.type !== 'level')
46
+ continue;
47
+ const buildingId = buildingNodes.find((building) => building.id === node.parentId)?.id ??
48
+ buildingNodes.find((building) => getStringArray(building.children).includes(id))?.id ??
49
+ null;
50
+ const bucket = levelsByBuilding.get(buildingId) ?? [];
51
+ bucket.push({ id, ordinal: getFiniteNumber(node.level, 0) });
52
+ levelsByBuilding.set(buildingId, bucket);
53
+ }
54
+ for (const bucket of levelsByBuilding.values()) {
55
+ const sorted = [...bucket].sort((a, b) => a.ordinal - b.ordinal);
56
+ const negativeCount = sorted.filter((entry) => entry.ordinal < 0).length;
57
+ sorted.forEach((entry, index) => {
58
+ const nextOrdinal = index - negativeCount;
59
+ const current = nodes[entry.id];
60
+ if (current.level !== nextOrdinal) {
61
+ replaceNode(entry.id, { ...current, level: nextOrdinal });
62
+ }
63
+ });
64
+ }
65
+ // Materialize exact legacy storey planes before classifying wall tops.
66
+ const legacyLevelIds = Object.entries(nodes)
67
+ .filter(([, node]) => node?.type === 'level' && !('height' in node))
68
+ .map(([id]) => id);
69
+ const derivedHeights = new Map();
70
+ for (const levelId of legacyLevelIds) {
71
+ derivedHeights.set(levelId, deriveLegacyLevelHeight(levelId, nodes));
72
+ }
73
+ for (const levelId of legacyLevelIds) {
74
+ const plane = derivedHeights.get(levelId);
75
+ const level = nodes[levelId];
76
+ replaceNode(levelId, { ...level, height: plane });
77
+ const children = getStringArray(level.children)
78
+ .map((childId) => nodes[childId])
79
+ .filter((child) => child !== undefined);
80
+ const slabs = children.filter((child) => child.type === 'slab');
81
+ const walls = children.filter((child) => child.type === 'wall');
82
+ for (const wall of walls) {
83
+ const electedBase = computeWallSlabSupport({
84
+ start: wall.start,
85
+ end: wall.end,
86
+ curveOffset: wall.curveOffset,
87
+ thickness: wall.thickness,
88
+ }, slabs, walls).elevation;
89
+ const effectiveHeight = wall.height ?? DEFAULT_WALL_HEIGHT;
90
+ const top = Math.max(0, electedBase) + effectiveHeight;
91
+ if (Math.abs(plane - top) < PLANE_BOUND_EPSILON) {
92
+ if ('height' in wall) {
93
+ const { height: _height, ...planeBound } = wall;
94
+ replaceNode(wall.id, planeBound);
95
+ }
96
+ }
97
+ else if (wall.height !== effectiveHeight) {
98
+ replaceNode(wall.id, { ...wall, height: effectiveHeight });
99
+ }
100
+ }
101
+ }
102
+ if (isLegacyScene) {
103
+ for (const [id, node] of Object.entries(nodes)) {
104
+ if (node?.type !== 'stair' || node.totalRise !== 2.5)
105
+ continue;
106
+ const { totalRise: _totalRise, ...derivedRise } = node;
107
+ replaceNode(id, derivedRise);
108
+ }
109
+ }
110
+ // Preserve the exact occupied interval of legacy slabs.
111
+ for (const [id, node] of Object.entries(nodes)) {
112
+ if (node?.type !== 'slab' || 'thickness' in node)
113
+ continue;
114
+ const elevation = getFiniteNumber(node.elevation, 0.05);
115
+ replaceNode(id, elevation < 0
116
+ ? { ...node, thickness: 0.05, recessed: true }
117
+ : { ...node, thickness: elevation });
118
+ }
119
+ if (isLegacyScene) {
120
+ for (const [id, node] of Object.entries(nodes)) {
121
+ if (node?.type !== 'ceiling' || !('height' in node))
122
+ continue;
123
+ const dropHeight = () => {
124
+ const { height: _height, ...follows } = node;
125
+ replaceNode(id, follows);
126
+ };
127
+ if (node.autoFromWalls === true) {
128
+ dropHeight();
129
+ continue;
130
+ }
131
+ if (typeof node.parentId !== 'string')
132
+ continue;
133
+ const bound = getCeilingClampBound(node.parentId, nodes, Array.isArray(node.polygon) ? node.polygon : []);
134
+ const stored = getFiniteNumber(node.height, Number.NaN);
135
+ if (Number.isFinite(bound) && Math.abs(stored - bound) < PLANE_BOUND_EPSILON) {
136
+ dropHeight();
137
+ }
138
+ }
139
+ }
140
+ // Terrain-sculpt-era drafting stamped `supportSlabId: 'ground'` onto walls
141
+ // drawn in 3D on flat scenes. The pin short-circuits slab election, so a
142
+ // wall whose feet sit inside a floor slab keeps its base at the level floor
143
+ // — buried in the slab, z-fighting its side faces — while the panel shows
144
+ // the base as automatic. Heal the pin only in that buried state: a deck
145
+ // hovering above an intentionally grounded wall must keep its pin (dropping
146
+ // it would lift the wall onto the deck). Scenes with sculpted terrain are
147
+ // skipped wholesale — a ground host is load-bearing there, and the live
148
+ // terrain field isn't visible to this pure pass.
149
+ const hasSculptedTerrain = Object.values(nodes).some((node) => node?.type === 'site' && node.terrain != null);
150
+ if (!hasSculptedTerrain) {
151
+ for (const [id, node] of Object.entries(nodes)) {
152
+ if (node?.type !== 'wall' || node.supportSlabId !== GROUND_SUPPORT_ID)
153
+ continue;
154
+ const levelId = typeof node.parentId === 'string' ? node.parentId : null;
155
+ if (!levelId || nodes[levelId]?.type !== 'level')
156
+ continue;
157
+ const siblings = Object.values(nodes).filter((sibling) => sibling != null && sibling.parentId === levelId);
158
+ const support = computeWallSlabSupport({
159
+ start: node.start,
160
+ end: node.end,
161
+ curveOffset: node.curveOffset,
162
+ thickness: node.thickness,
163
+ }, siblings.filter((sibling) => sibling.type === 'slab'), siblings.filter((sibling) => sibling.type === 'wall'));
164
+ const elected = support.electedSlabId ? nodes[support.electedSlabId] : null;
165
+ if (!elected)
166
+ continue;
167
+ const pinnedBase = getFiniteNumber(node.supportOffset, 0);
168
+ const electedTop = getFiniteNumber(elected.elevation, 0.05);
169
+ const electedBottom = electedTop - getFiniteNumber(elected.thickness, 0.05);
170
+ const buried = electedBottom <= pinnedBase + BURIED_PIN_EPSILON &&
171
+ electedTop > pinnedBase + BURIED_PIN_EPSILON;
172
+ if (!buried)
173
+ continue;
174
+ const { supportSlabId: _host, supportOffset: _offset, ...healed } = node;
175
+ replaceNode(id, healed);
176
+ }
177
+ }
178
+ return changed ? { changed, nodes } : { changed, nodes: sourceNodes };
179
+ }
@@ -1,3 +1,4 @@
1
+ import { SceneMaterial } from '../schema/scene-material';
1
2
  import { type AnyNodeType } from '../schema/types';
2
3
  export type ValidationSeverity = 'error' | 'warning';
3
4
  export type ValidationIssue = {
@@ -18,6 +19,8 @@ export type ParsedBuildJson = {
18
19
  nodes: Record<string, unknown>;
19
20
  rootNodeIds: string[];
20
21
  installedPlugins?: string[];
22
+ /** Scene materials referenced by node `slots` (`scene:<id>`). */
23
+ materials?: Record<string, SceneMaterial>;
21
24
  };
22
25
  export type SchemaIssue = {
23
26
  nodeId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"validate-build-json.d.ts","sourceRoot":"","sources":["../../src/validation/validate-build-json.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG3D,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,CAAA;AAEpD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5C,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,KAAK,EAAE,UAAU,CAAA;IACjB,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,YAAY,EAAE,WAAW,EAAE,CAAA;IAC3B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AA8BD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CA8SzE"}
1
+ {"version":3,"file":"validate-build-json.d.ts","sourceRoot":"","sources":["../../src/validation/validate-build-json.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,EAAW,KAAK,WAAW,EAAc,MAAM,iBAAiB,CAAA;AAGvE,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,CAAA;AAEpD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5C,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;CAC1C,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,KAAK,EAAE,UAAU,CAAA;IACjB,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,YAAY,EAAE,WAAW,EAAE,CAAA;IAC3B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AA4BD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CA4VzE"}
@@ -1,7 +1,8 @@
1
1
  import { nodeRegistry } from '../registry';
2
- import { AnyNode } from '../schema/types';
2
+ import { SceneMaterial } from '../schema/scene-material';
3
+ import { AnyNode, nodeKindOf } from '../schema/types';
3
4
  import { healSceneNodes } from '../utils/heal-scene-graph';
4
- const KNOWN_TYPES = new Set(AnyNode.options.map((o) => o.shape.type.parse(undefined)));
5
+ const KNOWN_TYPES = new Set(AnyNode.options.map(nodeKindOf));
5
6
  function isPlainObject(value) {
6
7
  return typeof value === 'object' && value !== null && !Array.isArray(value);
7
8
  }
@@ -61,6 +62,7 @@ export function validateBuildJson(input) {
61
62
  const nodesRaw = input.nodes;
62
63
  const rootNodeIdsRaw = input.rootNodeIds;
63
64
  const installedPluginsRaw = input.installedPlugins;
65
+ const materialsRaw = input.materials;
64
66
  if (!isPlainObject(nodesRaw)) {
65
67
  errors.push({
66
68
  severity: 'error',
@@ -102,6 +104,50 @@ export function validateBuildJson(input) {
102
104
  message: 'Ignored invalid "installedPlugins" — expected an array of plugin IDs.',
103
105
  });
104
106
  }
107
+ // Scene materials ride along with the graph: nodes reference them by
108
+ // `scene:<id>` slot refs, so dropping the table here silently strips
109
+ // every custom finish from the imported scene. Invalid entries are
110
+ // skipped one by one — a bad material must not take the import down.
111
+ //
112
+ // DELIBERATE: `safeParse().data` NORMALIZES — defaults are injected
113
+ // and unknown keys dropped. That is the opposite of the API boundary
114
+ // (`apiGraphSchema` preserves unknown fields on purpose), and it is
115
+ // chosen here because import feeds the live scene store, which only
116
+ // understands schema-shaped materials; a hand-edited file with a
117
+ // half-formed material should land as something the renderer can
118
+ // draw, not round-trip garbage.
119
+ let materials;
120
+ if (isPlainObject(materialsRaw)) {
121
+ const skippedIds = [];
122
+ const kept = {};
123
+ for (const [id, value] of Object.entries(materialsRaw)) {
124
+ const result = SceneMaterial.safeParse(value);
125
+ if (result.success) {
126
+ kept[id] = result.data;
127
+ }
128
+ else {
129
+ skippedIds.push(id);
130
+ }
131
+ }
132
+ if (Object.keys(kept).length > 0)
133
+ materials = kept;
134
+ if (skippedIds.length > 0) {
135
+ // Name the ids: the audience is hand-edited files, and a count
136
+ // alone leaves nothing to repair by.
137
+ warnings.push({
138
+ severity: 'warning',
139
+ code: 'invalid_materials',
140
+ message: `Ignored ${skippedIds.length} invalid scene material${skippedIds.length === 1 ? '' : 's'}: ${skippedIds.join(', ')}.`,
141
+ });
142
+ }
143
+ }
144
+ else if (materialsRaw !== undefined) {
145
+ warnings.push({
146
+ severity: 'warning',
147
+ code: 'invalid_materials',
148
+ message: 'Ignored invalid "materials" — expected an object of id → material.',
149
+ });
150
+ }
105
151
  if (strippedChildRefs > 0 || droppedWallIds.length > 0) {
106
152
  warnings.push({
107
153
  severity: 'warning',
@@ -301,6 +347,7 @@ export function validateBuildJson(input) {
301
347
  nodes,
302
348
  rootNodeIds,
303
349
  ...(installedPlugins ? { installedPlugins } : {}),
350
+ ...(materials ? { materials } : {}),
304
351
  }
305
352
  : null,
306
353
  stats,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/core",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0",
4
4
  "description": "Core library for Pascal 3D building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,6 +21,11 @@
21
21
  "import": "./dist/utils/scene-migrations.js",
22
22
  "default": "./dist/utils/scene-migrations.js"
23
23
  },
24
+ "./capture": {
25
+ "types": "./dist/capture/index.d.ts",
26
+ "import": "./dist/capture/index.js",
27
+ "default": "./dist/capture/index.js"
28
+ },
24
29
  "./registry": {
25
30
  "types": "./dist/registry/index.d.ts",
26
31
  "import": "./dist/registry/index.js",
@@ -66,20 +71,21 @@
66
71
  "dev": "tsgo --build --watch",
67
72
  "test": "bun test src",
68
73
  "bench:registry": "bun run src/registry/__bench__/relations-resolver.bench.ts",
74
+ "bench:schema": "bun run src/schema/__bench__/node-parsers.bench.ts",
69
75
  "prepublishOnly": "npm run build"
70
76
  },
71
77
  "peerDependencies": {
72
78
  "@react-three/drei": "^10",
73
79
  "@react-three/fiber": "^9",
74
80
  "react": "^18 || ^19",
75
- "three": "^0.185"
81
+ "three": "^0.186"
76
82
  },
77
83
  "dependencies": {
78
84
  "dedent": "^1.7.1",
79
85
  "idb-keyval": "^6.2.2",
80
86
  "mitt": "^3.0.1",
81
87
  "nanoid": "^5.1.6",
82
- "zod": "^4.3.5",
88
+ "zod": ">=4.5.4 <4.6",
83
89
  "zundo": "^2.3.0",
84
90
  "zustand": "^5"
85
91
  },
@@ -88,6 +94,7 @@
88
94
  "@types/bun": "^1.3.0",
89
95
  "@types/react": "^19.2.2",
90
96
  "@types/three": "^0.184.0",
97
+ "fake-indexeddb": "^6.2.5",
91
98
  "typescript": "6.0.3"
92
99
  },
93
100
  "keywords": [
@@ -1,2 +0,0 @@
1
- export declare function ElevatorRuntimeSystem(): null;
2
- //# sourceMappingURL=elevator-runtime-system.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"elevator-runtime-system.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-runtime-system.tsx"],"names":[],"mappings":"AAGA,wBAAgB,qBAAqB,SAMpC"}
@@ -1,8 +0,0 @@
1
- import { useFrame } from '@react-three/fiber';
2
- import { stepElevatorRuntimes } from './elevator-runtime';
3
- export function ElevatorRuntimeSystem() {
4
- useFrame(({ clock }, delta) => {
5
- stepElevatorRuntimes(clock.getElapsedTime() * 1000, delta);
6
- }, 2);
7
- return null;
8
- }