@pascal-app/core 1.0.0-beta.5 → 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 (231) 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-sync.d.ts +2785 -1
  18. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  19. package/dist/hooks/spatial-grid/spatial-grid-sync.js +132 -19
  20. package/dist/hooks/spatial-grid/support-host-patch.d.ts +45 -1
  21. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -1
  22. package/dist/hooks/spatial-grid/support-host-patch.js +171 -6
  23. package/dist/index.d.ts +14 -6
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +16 -5
  26. package/dist/lib/asset-storage.d.ts.map +1 -1
  27. package/dist/lib/asset-storage.js +4 -1
  28. package/dist/lib/polygon-union.d.ts +4 -0
  29. package/dist/lib/polygon-union.d.ts.map +1 -0
  30. package/dist/lib/polygon-union.js +306 -0
  31. package/dist/lib/roof-overlap.d.ts +32 -0
  32. package/dist/lib/roof-overlap.d.ts.map +1 -0
  33. package/dist/lib/roof-overlap.js +72 -0
  34. package/dist/lib/slab-polygon.d.ts +35 -2
  35. package/dist/lib/slab-polygon.d.ts.map +1 -1
  36. package/dist/lib/slab-polygon.js +133 -27
  37. package/dist/lib/space-detection.d.ts +11 -0
  38. package/dist/lib/space-detection.d.ts.map +1 -1
  39. package/dist/lib/space-detection.js +42 -14
  40. package/dist/registry/handles.d.ts +24 -3
  41. package/dist/registry/handles.d.ts.map +1 -1
  42. package/dist/registry/index.d.ts +3 -4
  43. package/dist/registry/index.d.ts.map +1 -1
  44. package/dist/registry/index.js +1 -2
  45. package/dist/registry/registry.d.ts +13 -6
  46. package/dist/registry/registry.d.ts.map +1 -1
  47. package/dist/registry/registry.js +37 -4
  48. package/dist/registry/scene-api.d.ts +16 -0
  49. package/dist/registry/scene-api.d.ts.map +1 -1
  50. package/dist/registry/scene-api.js +49 -1
  51. package/dist/registry/types.d.ts +226 -13
  52. package/dist/registry/types.d.ts.map +1 -1
  53. package/dist/schema/__bench__/node-parsers.bench.d.ts +24 -0
  54. package/dist/schema/__bench__/node-parsers.bench.d.ts.map +1 -0
  55. package/dist/schema/__bench__/node-parsers.bench.js +314 -0
  56. package/dist/schema/__fixtures__/node-fixtures.d.ts +28 -0
  57. package/dist/schema/__fixtures__/node-fixtures.d.ts.map +1 -0
  58. package/dist/schema/__fixtures__/node-fixtures.js +125 -0
  59. package/dist/schema/base.d.ts +1 -1
  60. package/dist/schema/base.d.ts.map +1 -1
  61. package/dist/schema/base.js +8 -1
  62. package/dist/schema/compiled-node-parsers.d.ts +40 -0
  63. package/dist/schema/compiled-node-parsers.d.ts.map +1 -0
  64. package/dist/schema/compiled-node-parsers.js +95 -0
  65. package/dist/schema/index.d.ts +19 -15
  66. package/dist/schema/index.d.ts.map +1 -1
  67. package/dist/schema/index.js +17 -12
  68. package/dist/schema/material.d.ts +2 -1
  69. package/dist/schema/material.d.ts.map +1 -1
  70. package/dist/schema/material.js +1 -0
  71. package/dist/schema/nodes/automatic-downspout.d.ts +17 -0
  72. package/dist/schema/nodes/automatic-downspout.d.ts.map +1 -0
  73. package/dist/schema/nodes/automatic-downspout.js +237 -0
  74. package/dist/schema/nodes/block.d.ts +107 -0
  75. package/dist/schema/nodes/block.d.ts.map +1 -0
  76. package/dist/schema/nodes/block.js +207 -0
  77. package/dist/schema/nodes/box-vent.d.ts +8 -2
  78. package/dist/schema/nodes/box-vent.d.ts.map +1 -1
  79. package/dist/schema/nodes/box-vent.js +2 -0
  80. package/dist/schema/nodes/building.d.ts +1 -1
  81. package/dist/schema/nodes/cabinet.d.ts +25 -3
  82. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  83. package/dist/schema/nodes/cabinet.js +26 -5
  84. package/dist/schema/nodes/ceiling.d.ts +1 -1
  85. package/dist/schema/nodes/chimney.d.ts +2 -2
  86. package/dist/schema/nodes/column.d.ts +6 -6
  87. package/dist/schema/nodes/construction-dimension.d.ts +4 -4
  88. package/dist/schema/nodes/cupola.d.ts +9 -1
  89. package/dist/schema/nodes/cupola.d.ts.map +1 -1
  90. package/dist/schema/nodes/cupola.js +2 -0
  91. package/dist/schema/nodes/door.d.ts +1 -1
  92. package/dist/schema/nodes/dormer.d.ts +44 -1
  93. package/dist/schema/nodes/dormer.d.ts.map +1 -1
  94. package/dist/schema/nodes/dormer.js +131 -7
  95. package/dist/schema/nodes/downspout.d.ts +11 -1
  96. package/dist/schema/nodes/downspout.d.ts.map +1 -1
  97. package/dist/schema/nodes/downspout.js +24 -0
  98. package/dist/schema/nodes/duct-fitting.d.ts +18 -1
  99. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -1
  100. package/dist/schema/nodes/duct-fitting.js +25 -4
  101. package/dist/schema/nodes/duct-segment.d.ts +23 -1
  102. package/dist/schema/nodes/duct-segment.d.ts.map +1 -1
  103. package/dist/schema/nodes/duct-segment.js +20 -0
  104. package/dist/schema/nodes/duct-terminal.d.ts +1 -1
  105. package/dist/schema/nodes/elevator.d.ts +1 -1
  106. package/dist/schema/nodes/eyebrow-vent.d.ts +8 -2
  107. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -1
  108. package/dist/schema/nodes/eyebrow-vent.js +2 -0
  109. package/dist/schema/nodes/fence.d.ts +1 -1
  110. package/dist/schema/nodes/guide.d.ts +1 -1
  111. package/dist/schema/nodes/gutter.d.ts +36 -2
  112. package/dist/schema/nodes/gutter.d.ts.map +1 -1
  113. package/dist/schema/nodes/gutter.js +308 -1
  114. package/dist/schema/nodes/hanger-overrides.d.ts +7 -0
  115. package/dist/schema/nodes/hanger-overrides.d.ts.map +1 -0
  116. package/dist/schema/nodes/hanger-overrides.js +6 -0
  117. package/dist/schema/nodes/hvac-equipment.d.ts +1 -1
  118. package/dist/schema/nodes/item.d.ts +4 -3
  119. package/dist/schema/nodes/item.d.ts.map +1 -1
  120. package/dist/schema/nodes/item.js +5 -0
  121. package/dist/schema/nodes/lean-to-extension.d.ts +212 -0
  122. package/dist/schema/nodes/lean-to-extension.d.ts.map +1 -0
  123. package/dist/schema/nodes/lean-to-extension.js +114 -0
  124. package/dist/schema/nodes/level.d.ts +2 -2
  125. package/dist/schema/nodes/level.d.ts.map +1 -1
  126. package/dist/schema/nodes/lineset.d.ts +1 -1
  127. package/dist/schema/nodes/liquid-line.d.ts +1 -1
  128. package/dist/schema/nodes/measurement.d.ts +1 -1
  129. package/dist/schema/nodes/pipe-fitting.d.ts +9 -1
  130. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -1
  131. package/dist/schema/nodes/pipe-fitting.js +6 -2
  132. package/dist/schema/nodes/pipe-segment.d.ts +23 -1
  133. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -1
  134. package/dist/schema/nodes/pipe-segment.js +20 -0
  135. package/dist/schema/nodes/pipe-trap.d.ts +1 -1
  136. package/dist/schema/nodes/ridge-vent.d.ts +1 -1
  137. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  138. package/dist/schema/nodes/ridge-vent.js +4 -1
  139. package/dist/schema/nodes/roof-segment-shape.d.ts +4 -0
  140. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -1
  141. package/dist/schema/nodes/roof-segment-shape.js +64 -1
  142. package/dist/schema/nodes/roof-segment-walls.d.ts +3 -1
  143. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -1
  144. package/dist/schema/nodes/roof-segment-walls.js +1 -1
  145. package/dist/schema/nodes/roof-segment.d.ts +41 -1
  146. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  147. package/dist/schema/nodes/roof-segment.js +79 -2
  148. package/dist/schema/nodes/roof.d.ts +22 -1
  149. package/dist/schema/nodes/roof.d.ts.map +1 -1
  150. package/dist/schema/nodes/roof.js +14 -0
  151. package/dist/schema/nodes/scan.d.ts +19 -2
  152. package/dist/schema/nodes/scan.d.ts.map +1 -1
  153. package/dist/schema/nodes/scan.js +11 -1
  154. package/dist/schema/nodes/shelf.d.ts +2 -2
  155. package/dist/schema/nodes/site.d.ts +1 -1
  156. package/dist/schema/nodes/skylight.d.ts +1 -1
  157. package/dist/schema/nodes/slab.d.ts +1 -1
  158. package/dist/schema/nodes/solar-panel.d.ts +1 -1
  159. package/dist/schema/nodes/spawn.d.ts +1 -1
  160. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  161. package/dist/schema/nodes/stair.d.ts +1 -1
  162. package/dist/schema/nodes/structural-grid.d.ts +1 -1
  163. package/dist/schema/nodes/turbine-vent.d.ts +7 -1
  164. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -1
  165. package/dist/schema/nodes/turbine-vent.js +2 -0
  166. package/dist/schema/nodes/wall.d.ts +4 -4
  167. package/dist/schema/nodes/wall.d.ts.map +1 -1
  168. package/dist/schema/nodes/wall.js +7 -1
  169. package/dist/schema/nodes/window.d.ts +10 -3
  170. package/dist/schema/nodes/window.d.ts.map +1 -1
  171. package/dist/schema/nodes/window.js +4 -0
  172. package/dist/schema/nodes/zone.d.ts +2 -2
  173. package/dist/schema/nodes/zone.js +1 -1
  174. package/dist/schema/types.d.ts +484 -109
  175. package/dist/schema/types.d.ts.map +1 -1
  176. package/dist/schema/types.js +27 -1
  177. package/dist/services/hosting.d.ts +2 -2
  178. package/dist/services/hosting.d.ts.map +1 -1
  179. package/dist/services/hosting.js +4 -4
  180. package/dist/services/index.d.ts +1 -1
  181. package/dist/services/index.d.ts.map +1 -1
  182. package/dist/services/index.js +1 -1
  183. package/dist/services/system-graph.d.ts +11 -0
  184. package/dist/services/system-graph.d.ts.map +1 -1
  185. package/dist/services/system-graph.js +35 -5
  186. package/dist/store/actions/node-actions.d.ts +7 -0
  187. package/dist/store/actions/node-actions.d.ts.map +1 -1
  188. package/dist/store/actions/node-actions.js +429 -17
  189. package/dist/store/history-invalidation.d.ts +3 -0
  190. package/dist/store/history-invalidation.d.ts.map +1 -0
  191. package/dist/store/history-invalidation.js +72 -0
  192. package/dist/store/scene-hydration.d.ts +5 -0
  193. package/dist/store/scene-hydration.d.ts.map +1 -0
  194. package/dist/store/scene-hydration.js +57 -0
  195. package/dist/store/use-scene.d.ts +3 -0
  196. package/dist/store/use-scene.d.ts.map +1 -1
  197. package/dist/store/use-scene.js +332 -64
  198. package/dist/systems/roof/roof-elevation-system.d.ts +3 -0
  199. package/dist/systems/roof/roof-elevation-system.d.ts.map +1 -0
  200. package/dist/systems/roof/roof-elevation-system.js +94 -0
  201. package/dist/systems/roof/roof-elevation.d.ts +4 -0
  202. package/dist/systems/roof/roof-elevation.d.ts.map +1 -0
  203. package/dist/systems/roof/roof-elevation.js +95 -0
  204. package/dist/systems/roof/roof-footprint.d.ts +17 -0
  205. package/dist/systems/roof/roof-footprint.d.ts.map +1 -0
  206. package/dist/systems/roof/roof-footprint.js +83 -0
  207. package/dist/systems/stair/stair-flight.d.ts +17 -0
  208. package/dist/systems/stair/stair-flight.d.ts.map +1 -0
  209. package/dist/systems/stair/stair-flight.js +27 -0
  210. package/dist/systems/stair/stair-opening-preview.d.ts +1555 -1328
  211. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  212. package/dist/systems/stair/stair-opening-system.d.ts +1 -0
  213. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  214. package/dist/systems/stair/stair-opening-system.js +93 -96
  215. package/dist/systems/stair/stair-rise.d.ts.map +1 -1
  216. package/dist/systems/stair/stair-rise.js +20 -21
  217. package/dist/systems/wall/wall-curve.d.ts +1 -0
  218. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  219. package/dist/systems/wall/wall-curve.js +73 -0
  220. package/dist/systems/wall/wall-footprint.d.ts +1 -0
  221. package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
  222. package/dist/systems/wall/wall-footprint.js +1 -0
  223. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  224. package/dist/utils/clone-scene-graph.js +26 -0
  225. package/dist/validation/validate-build-json.d.ts +3 -0
  226. package/dist/validation/validate-build-json.d.ts.map +1 -1
  227. package/dist/validation/validate-build-json.js +49 -2
  228. package/package.json +10 -3
  229. package/dist/systems/elevator/elevator-runtime-system.d.ts +0 -2
  230. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +0 -1
  231. package/dist/systems/elevator/elevator-runtime-system.js +0 -8
@@ -0,0 +1,237 @@
1
+ import { getWallBaseElevationForNodes } from '../../hooks/spatial-grid/spatial-grid-manager';
2
+ import { heightAt } from '../../lib/terrain-field';
3
+ import { persistedTerrainFieldOf } from '../../lib/terrain-source';
4
+ import { getLevelElevations } from '../../services/storey';
5
+ import { computeGutterEaveY } from './gutter';
6
+ const DEFAULT_MAX_RUN_PER_DOWNSPOUT_M = 10;
7
+ const OUTLET_END_INSET_M = 0.16;
8
+ const CONNECTION_TOLERANCE_M = 0.1;
9
+ const CONNECTION_TOLERANCE_SQ = CONNECTION_TOLERANCE_M * CONNECTION_TOLERANCE_M;
10
+ const FLAT_GROUND_Y = 0;
11
+ // A point on the gutter's own mesh, expressed in gutter-mesh-local
12
+ // coordinates: `alongX` is the signed distance from the gutter center along
13
+ // its length, `outwardZ` the outward offset from the eave line. For a curved
14
+ // run the flat (alongX, outwardZ) is bent onto the concentric arc descriptor
15
+ // (matches the mapping the outlet lookup + gutter geometry use); a straight
16
+ // gutter passes through unchanged.
17
+ function gutterMeshPoint(gutter, alongX, outwardZ) {
18
+ const arc = gutter.arc;
19
+ if (!arc)
20
+ return [alongX, outwardZ];
21
+ const signedRef = (Math.sign(arc.centerZ) || 1) * arc.radius;
22
+ if (Math.abs(signedRef) < 1e-9)
23
+ return [alongX, outwardZ];
24
+ const phi = (alongX - arc.centerX) / signedRef;
25
+ const radial = outwardZ - arc.centerZ;
26
+ return [arc.centerX - radial * Math.sin(phi), arc.centerZ + radial * Math.cos(phi)];
27
+ }
28
+ function gutterPointInRoofFrame(gutter, segment, offset) {
29
+ const gutterRotation = gutter.rotation ?? 0;
30
+ const [meshX, meshZ] = gutterMeshPoint(gutter, offset, 0);
31
+ const localX = gutter.position[0] + Math.cos(gutterRotation) * meshX + Math.sin(gutterRotation) * meshZ;
32
+ const localZ = gutter.position[2] - Math.sin(gutterRotation) * meshX + Math.cos(gutterRotation) * meshZ;
33
+ if (!segment)
34
+ return [localX, localZ];
35
+ const segmentRotation = segment.rotation ?? 0;
36
+ const cos = Math.cos(segmentRotation);
37
+ const sin = Math.sin(segmentRotation);
38
+ return [
39
+ (segment.position?.[0] ?? 0) + localX * cos + localZ * sin,
40
+ (segment.position?.[2] ?? 0) - localX * sin + localZ * cos,
41
+ ];
42
+ }
43
+ function rotateAndTranslate(point, position, rotation) {
44
+ const cos = Math.cos(rotation);
45
+ const sin = Math.sin(rotation);
46
+ return [
47
+ (position?.[0] ?? 0) + point[0] * cos + point[1] * sin,
48
+ (position?.[2] ?? 0) - point[0] * sin + point[1] * cos,
49
+ ];
50
+ }
51
+ function gutterFloorMidZ(gutter) {
52
+ const size = Math.max(0.04, gutter.size);
53
+ if (gutter.profile === 'half-round')
54
+ return size;
55
+ if (gutter.profile === 'box')
56
+ return size / 2;
57
+ return size * 0.4;
58
+ }
59
+ // The gutter's mount height in the host segment's local frame. Lean-to gutters
60
+ // can be raised to a shared eave line (stored as `leanToGutterEaveY` metadata by
61
+ // the lean-to assembly); the renderer mounts there rather than at the segment's
62
+ // own eave, so the outlet elevation must read the prescribed value when present.
63
+ function prescribedGutterEaveY(gutter) {
64
+ const metadata = gutter.metadata;
65
+ if (!(metadata && typeof metadata === 'object' && !Array.isArray(metadata)))
66
+ return null;
67
+ const value = metadata.leanToGutterEaveY;
68
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
69
+ }
70
+ export function resolveAutomaticDownspoutLength(nodes, segment, gutter, outletOffset) {
71
+ const roofCandidate = segment.parentId ? nodes[segment.parentId] : undefined;
72
+ const roof = roofCandidate?.type === 'roof' ? roofCandidate : undefined;
73
+ const roofParent = roof?.parentId ? nodes[roof.parentId] : undefined;
74
+ const leanTo = roofParent?.type === 'lean-to-extension' ? roofParent : undefined;
75
+ const wallCandidate = leanTo?.parentId ? nodes[leanTo.parentId] : undefined;
76
+ const wall = wallCandidate?.type === 'wall' ? wallCandidate : undefined;
77
+ const levelCandidate = wall?.parentId ? nodes[wall.parentId] : roofParent;
78
+ const level = levelCandidate?.type === 'level' ? levelCandidate : undefined;
79
+ const buildingCandidate = level?.parentId ? nodes[level.parentId] : undefined;
80
+ const building = buildingCandidate?.type === 'building' ? buildingCandidate : undefined;
81
+ const gutterRotation = gutter.rotation ?? 0;
82
+ const gutterFloorPoint = rotateAndTranslate(gutterMeshPoint(gutter, outletOffset, gutterFloorMidZ(gutter)), gutter.position, gutterRotation);
83
+ const roofPoint = rotateAndTranslate(gutterFloorPoint, segment.position, segment.rotation ?? 0);
84
+ const leanToPoint = rotateAndTranslate(roofPoint, roof?.position, roof?.rotation ?? 0);
85
+ const wallLocalPoint = leanTo
86
+ ? rotateAndTranslate(leanToPoint, leanTo.position, leanTo.rotation[1])
87
+ : leanToPoint;
88
+ const wallAngle = wall ? Math.atan2(wall.end[1] - wall.start[1], wall.end[0] - wall.start[0]) : 0;
89
+ const levelPoint = wall
90
+ ? rotateAndTranslate(wallLocalPoint, [wall.start[0], 0, wall.start[1]], -wallAngle)
91
+ : wallLocalPoint;
92
+ const buildingRotation = building?.rotation?.[1] ?? 0;
93
+ const worldPoint = rotateAndTranslate(levelPoint, building?.position, buildingRotation);
94
+ const site = Object.values(nodes).find((node) => node?.type === 'site');
95
+ const terrain = persistedTerrainFieldOf(site);
96
+ const groundY = terrain ? heightAt(terrain, worldPoint[0], worldPoint[1]) : FLAT_GROUND_Y;
97
+ const levelBaseY = level ? (getLevelElevations(nodes).get(level.id)?.baseY ?? 0) : 0;
98
+ const outletWorldY = (building?.position?.[1] ?? 0) +
99
+ levelBaseY +
100
+ (wall ? getWallBaseElevationForNodes(wall, nodes) : 0) +
101
+ (leanTo?.position[1] ?? 0) +
102
+ (roof?.position?.[1] ?? 0) +
103
+ (segment.position?.[1] ?? 0) +
104
+ (prescribedGutterEaveY(gutter) ?? computeGutterEaveY(segment)) -
105
+ Math.max(0.04, gutter.size);
106
+ return Math.max(0.1, outletWorldY - groundY);
107
+ }
108
+ function distanceSquared(a, b) {
109
+ return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
110
+ }
111
+ function find(parent, value) {
112
+ let root = value;
113
+ while (parent[root] !== root)
114
+ root = parent[root];
115
+ while (parent[value] !== value) {
116
+ const next = parent[value];
117
+ parent[value] = root;
118
+ value = next;
119
+ }
120
+ return root;
121
+ }
122
+ function union(parent, a, b) {
123
+ const rootA = find(parent, a);
124
+ const rootB = find(parent, b);
125
+ if (rootA !== rootB)
126
+ parent[rootB] = rootA;
127
+ }
128
+ function addUniquePlacement(placements, seen, gutter, offset) {
129
+ const key = `${gutter.id}:${offset.toFixed(6)}`;
130
+ if (seen.has(key))
131
+ return;
132
+ seen.add(key);
133
+ placements.push({ gutterId: gutter.id, offset });
134
+ }
135
+ export function planAutomaticDownspouts({ segments, gutters, downspouts, maxRunPerDownspout = DEFAULT_MAX_RUN_PER_DOWNSPOUT_M, }) {
136
+ if (gutters.length === 0)
137
+ return [];
138
+ const segmentById = new Map(segments.map((segment) => [segment.id, segment]));
139
+ const parent = gutters.map((_, index) => index);
140
+ const ends = [];
141
+ for (let gutterIndex = 0; gutterIndex < gutters.length; gutterIndex++) {
142
+ const gutter = gutters[gutterIndex];
143
+ const halfLength = Math.max(0, gutter.length) / 2;
144
+ const segment = gutter.roofSegmentId ? segmentById.get(gutter.roofSegmentId) : undefined;
145
+ ends.push({
146
+ gutterIndex,
147
+ offset: halfLength,
148
+ point: gutterPointInRoofFrame(gutter, segment, halfLength),
149
+ }, {
150
+ gutterIndex,
151
+ offset: -halfLength,
152
+ point: gutterPointInRoofFrame(gutter, segment, -halfLength),
153
+ });
154
+ }
155
+ const connectedEnds = new Set();
156
+ for (let i = 0; i < ends.length; i++) {
157
+ for (let j = i + 1; j < ends.length; j++) {
158
+ const a = ends[i];
159
+ const b = ends[j];
160
+ if (a.gutterIndex === b.gutterIndex)
161
+ continue;
162
+ if (distanceSquared(a.point, b.point) > CONNECTION_TOLERANCE_SQ)
163
+ continue;
164
+ connectedEnds.add(i);
165
+ connectedEnds.add(j);
166
+ union(parent, a.gutterIndex, b.gutterIndex);
167
+ }
168
+ }
169
+ const componentIndices = new Map();
170
+ for (let index = 0; index < gutters.length; index++) {
171
+ const root = find(parent, index);
172
+ const indices = componentIndices.get(root) ?? [];
173
+ indices.push(index);
174
+ componentIndices.set(root, indices);
175
+ }
176
+ const gutterIndexById = new Map(gutters.map((gutter, index) => [gutter.id, index]));
177
+ const placements = [];
178
+ const seen = new Set();
179
+ const safeMaxRun = Math.max(0.5, maxRunPerDownspout);
180
+ for (const indices of componentIndices.values()) {
181
+ const indexSet = new Set(indices);
182
+ const totalLength = indices.reduce((sum, index) => sum + Math.max(0, gutters[index]?.length ?? 0), 0);
183
+ const requiredCount = Math.max(1, Math.ceil(totalLength / safeMaxRun));
184
+ const manualCount = downspouts.filter((downspout) => {
185
+ if (!downspout.gutterId)
186
+ return false;
187
+ const gutterIndex = gutterIndexById.get(downspout.gutterId);
188
+ if (gutterIndex === undefined || !indexSet.has(gutterIndex))
189
+ return false;
190
+ const gutter = gutters[gutterIndex];
191
+ return Boolean(gutter &&
192
+ downspout.outletId &&
193
+ (gutter.outlets ?? []).some((outlet) => outlet.id === downspout.outletId));
194
+ }).length;
195
+ let remaining = Math.max(0, requiredCount - manualCount);
196
+ if (remaining === 0)
197
+ continue;
198
+ const freeEnds = ends.filter((end, endIndex) => indexSet.has(end.gutterIndex) && !connectedEnds.has(endIndex));
199
+ for (const end of freeEnds) {
200
+ if (remaining === 0)
201
+ break;
202
+ const gutter = gutters[end.gutterIndex];
203
+ const bound = Math.max(0, gutter.length / 2 - OUTLET_END_INSET_M);
204
+ addUniquePlacement(placements, seen, gutter, end.offset >= 0 ? bound : -bound);
205
+ remaining--;
206
+ }
207
+ if (remaining === 0)
208
+ continue;
209
+ const componentGutters = indices
210
+ .map((index) => gutters[index])
211
+ .sort((a, b) => b.length - a.length || a.id.localeCompare(b.id));
212
+ const interiorCandidates = [];
213
+ let round = 0;
214
+ while (interiorCandidates.length < remaining) {
215
+ let addedThisRound = 0;
216
+ for (const gutter of componentGutters) {
217
+ const interiorSlots = Math.max(1, Math.ceil(gutter.length / safeMaxRun) - 1);
218
+ if (round >= interiorSlots)
219
+ continue;
220
+ const offset = -gutter.length / 2 + (gutter.length * (round + 1)) / (interiorSlots + 1);
221
+ interiorCandidates.push({ gutterId: gutter.id, offset });
222
+ addedThisRound++;
223
+ }
224
+ if (addedThisRound === 0)
225
+ break;
226
+ round++;
227
+ }
228
+ for (const candidate of interiorCandidates) {
229
+ if (remaining === 0)
230
+ break;
231
+ const gutter = gutters[gutterIndexById.get(candidate.gutterId)];
232
+ addUniquePlacement(placements, seen, gutter, candidate.offset);
233
+ remaining--;
234
+ }
235
+ }
236
+ return placements;
237
+ }
@@ -0,0 +1,107 @@
1
+ import { z } from 'zod';
2
+ export declare const BlockVertex: z.ZodObject<{
3
+ id: z.ZodString;
4
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
5
+ }, z.core.$strip>;
6
+ export declare const BlockEdge: z.ZodObject<{
7
+ id: z.ZodString;
8
+ vertexIds: z.ZodTuple<[z.ZodString, z.ZodString], null>;
9
+ }, z.core.$strip>;
10
+ export declare const BlockFace: z.ZodObject<{
11
+ id: z.ZodString;
12
+ vertexIds: z.ZodArray<z.ZodString>;
13
+ materialSlot: z.ZodDefault<z.ZodString>;
14
+ }, z.core.$strip>;
15
+ declare const BlockTopologyShape: z.ZodObject<{
16
+ vertices: z.ZodArray<z.ZodObject<{
17
+ id: z.ZodString;
18
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
19
+ }, z.core.$strip>>;
20
+ edges: z.ZodArray<z.ZodObject<{
21
+ id: z.ZodString;
22
+ vertexIds: z.ZodTuple<[z.ZodString, z.ZodString], null>;
23
+ }, z.core.$strip>>;
24
+ faces: z.ZodArray<z.ZodObject<{
25
+ id: z.ZodString;
26
+ vertexIds: z.ZodArray<z.ZodString>;
27
+ materialSlot: z.ZodDefault<z.ZodString>;
28
+ }, z.core.$strip>>;
29
+ }, z.core.$strip>;
30
+ export type BlockVertex = z.infer<typeof BlockVertex>;
31
+ export type BlockEdge = z.infer<typeof BlockEdge>;
32
+ export type BlockFace = z.infer<typeof BlockFace>;
33
+ export type BlockTopology = z.infer<typeof BlockTopologyShape>;
34
+ export type BlockFaceFrame = {
35
+ origin: [number, number, number];
36
+ xAxis: [number, number, number];
37
+ yAxis: [number, number, number];
38
+ normal: [number, number, number];
39
+ };
40
+ export declare function getBlockFaceNormal(topology: BlockTopology, face: BlockFace): [number, number, number] | null;
41
+ export declare function getBlockFaceCentroid(topology: BlockTopology, face: BlockFace): [number, number, number] | null;
42
+ export declare function getBlockFaceFrame(topology: BlockTopology, faceId: string): BlockFaceFrame | null;
43
+ export type BlockTopologyIssue = {
44
+ path: (string | number)[];
45
+ message: string;
46
+ };
47
+ export declare function blockUndirectedEdgeKey(a: string, b: string): string;
48
+ export declare function inspectBlockTopology(topology: BlockTopology): BlockTopologyIssue[];
49
+ export declare const BlockTopology: z.ZodObject<{
50
+ vertices: z.ZodArray<z.ZodObject<{
51
+ id: z.ZodString;
52
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
53
+ }, z.core.$strip>>;
54
+ edges: z.ZodArray<z.ZodObject<{
55
+ id: z.ZodString;
56
+ vertexIds: z.ZodTuple<[z.ZodString, z.ZodString], null>;
57
+ }, z.core.$strip>>;
58
+ faces: z.ZodArray<z.ZodObject<{
59
+ id: z.ZodString;
60
+ vertexIds: z.ZodArray<z.ZodString>;
61
+ materialSlot: z.ZodDefault<z.ZodString>;
62
+ }, z.core.$strip>>;
63
+ }, z.core.$strip>;
64
+ export declare function createBoxBlockTopology(width?: number, height?: number, depth?: number): BlockTopology;
65
+ export declare const BlockNode: z.ZodObject<{
66
+ object: z.ZodDefault<z.ZodLiteral<"node">>;
67
+ name: z.ZodOptional<z.ZodString>;
68
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
69
+ visible: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
70
+ camera: z.ZodOptional<z.ZodObject<{
71
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
72
+ target: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
73
+ mode: z.ZodDefault<z.ZodEnum<{
74
+ perspective: "perspective";
75
+ orthographic: "orthographic";
76
+ }>>;
77
+ fov: z.ZodOptional<z.ZodNumber>;
78
+ zoom: z.ZodOptional<z.ZodNumber>;
79
+ }, z.core.$strip>>;
80
+ metadata: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
81
+ id: z.ZodDefault<z.ZodTemplateLiteral<`block_${string}`>>;
82
+ type: z.ZodDefault<z.ZodLiteral<"block">>;
83
+ children: z.ZodDefault<z.ZodArray<z.ZodDefault<z.ZodTemplateLiteral<`item_${string}`>>>>;
84
+ position: z.ZodDefault<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
85
+ rotation: z.ZodDefault<z.ZodNumber>;
86
+ supportSlabId: z.ZodOptional<z.ZodString>;
87
+ topology: z.ZodDefault<z.ZodObject<{
88
+ vertices: z.ZodArray<z.ZodObject<{
89
+ id: z.ZodString;
90
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
91
+ }, z.core.$strip>>;
92
+ edges: z.ZodArray<z.ZodObject<{
93
+ id: z.ZodString;
94
+ vertexIds: z.ZodTuple<[z.ZodString, z.ZodString], null>;
95
+ }, z.core.$strip>>;
96
+ faces: z.ZodArray<z.ZodObject<{
97
+ id: z.ZodString;
98
+ vertexIds: z.ZodArray<z.ZodString>;
99
+ materialSlot: z.ZodDefault<z.ZodString>;
100
+ }, z.core.$strip>>;
101
+ }, z.core.$strip>>;
102
+ slots: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
103
+ slotNames: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
104
+ }, z.core.$strip>;
105
+ export type BlockNode = z.infer<typeof BlockNode>;
106
+ export {};
107
+ //# sourceMappingURL=block.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/block.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,WAAW;;;iBAGtB,CAAA;AAEF,eAAO,MAAM,SAAS;;;iBAGpB,CAAA;AAEF,eAAO,MAAM,SAAS;;;;iBAIpB,CAAA;AAEF,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;iBAItB,CAAA;AAEF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAA;AACrD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAA;AACjD,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAA;AACjD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAE9D,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC,CAAA;AAQD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,aAAa,EACvB,IAAI,EAAE,SAAS,GACd,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAgBjC;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,aAAa,EACvB,IAAI,EAAE,SAAS,GACd,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAWjC;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAuBhG;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,UAE1D;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,aAAa,GAAG,kBAAkB,EAAE,CAgElF;AAED,eAAO,MAAM,aAAa;;;;;;;;;;;;;;iBAIxB,CAAA;AAEF,wBAAgB,sBAAsB,CAAC,KAAK,SAAI,EAAE,MAAM,SAAM,EAAE,KAAK,SAAI,GAAG,aAAa,CAqCxF;AAED,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkBpB,CAAA;AAEF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAA"}
@@ -0,0 +1,207 @@
1
+ import dedent from 'dedent';
2
+ import { z } from 'zod';
3
+ import { BaseNode, nodeType, objectId } from '../base';
4
+ import { ItemNode } from './item';
5
+ export const BlockVertex = z.object({
6
+ id: z.string().min(1),
7
+ position: z.tuple([z.number(), z.number(), z.number()]),
8
+ });
9
+ export const BlockEdge = z.object({
10
+ id: z.string().min(1),
11
+ vertexIds: z.tuple([z.string().min(1), z.string().min(1)]),
12
+ });
13
+ export const BlockFace = z.object({
14
+ id: z.string().min(1),
15
+ vertexIds: z.array(z.string().min(1)).min(3),
16
+ materialSlot: z.string().min(1).default('body'),
17
+ });
18
+ const BlockTopologyShape = z.object({
19
+ vertices: z.array(BlockVertex),
20
+ edges: z.array(BlockEdge),
21
+ faces: z.array(BlockFace),
22
+ });
23
+ function normalizeBlockVector(vector) {
24
+ const length = Math.hypot(vector[0], vector[1], vector[2]);
25
+ if (length < 1e-8)
26
+ return null;
27
+ return [vector[0] / length, vector[1] / length, vector[2] / length];
28
+ }
29
+ export function getBlockFaceNormal(topology, face) {
30
+ const vertices = new Map(topology.vertices.map((vertex) => [vertex.id, vertex.position]));
31
+ const positions = face.vertexIds
32
+ .map((id) => vertices.get(id))
33
+ .filter((value) => !!value);
34
+ if (positions.length < 3)
35
+ return null;
36
+ const normal = [0, 0, 0];
37
+ for (let index = 0; index < positions.length; index += 1) {
38
+ const current = positions[index];
39
+ const next = positions[(index + 1) % positions.length];
40
+ normal[0] += (current[1] - next[1]) * (current[2] + next[2]);
41
+ normal[1] += (current[2] - next[2]) * (current[0] + next[0]);
42
+ normal[2] += (current[0] - next[0]) * (current[1] + next[1]);
43
+ }
44
+ return normalizeBlockVector(normal);
45
+ }
46
+ export function getBlockFaceCentroid(topology, face) {
47
+ const vertices = new Map(topology.vertices.map((vertex) => [vertex.id, vertex.position]));
48
+ const positions = face.vertexIds
49
+ .map((id) => vertices.get(id))
50
+ .filter((value) => !!value);
51
+ if (positions.length !== face.vertexIds.length || positions.length === 0)
52
+ return null;
53
+ const sum = positions.reduce((total, position) => [total[0] + position[0], total[1] + position[1], total[2] + position[2]], [0, 0, 0]);
54
+ return [sum[0] / positions.length, sum[1] / positions.length, sum[2] / positions.length];
55
+ }
56
+ export function getBlockFaceFrame(topology, faceId) {
57
+ const face = topology.faces.find((candidate) => candidate.id === faceId);
58
+ if (!face)
59
+ return null;
60
+ const origin = getBlockFaceCentroid(topology, face);
61
+ const normal = getBlockFaceNormal(topology, face);
62
+ if (!(origin && normal))
63
+ return null;
64
+ const horizontal = [normal[2], 0, -normal[0]];
65
+ const xAxis = normalizeBlockVector(horizontal) ??
66
+ normalizeBlockVector([
67
+ 1 - normal[0] * normal[0],
68
+ -normal[0] * normal[1],
69
+ -normal[0] * normal[2],
70
+ ]);
71
+ if (!xAxis)
72
+ return null;
73
+ const yAxis = normalizeBlockVector([
74
+ normal[1] * xAxis[2] - normal[2] * xAxis[1],
75
+ normal[2] * xAxis[0] - normal[0] * xAxis[2],
76
+ normal[0] * xAxis[1] - normal[1] * xAxis[0],
77
+ ]);
78
+ if (!yAxis)
79
+ return null;
80
+ return { origin, xAxis, yAxis, normal };
81
+ }
82
+ export function blockUndirectedEdgeKey(a, b) {
83
+ return a < b ? `${a}\u0000${b}` : `${b}\u0000${a}`;
84
+ }
85
+ export function inspectBlockTopology(topology) {
86
+ const issues = [];
87
+ const vertexIds = new Set();
88
+ const edgeIds = new Set();
89
+ const faceIds = new Set();
90
+ const edgeKeys = new Set();
91
+ topology.vertices.forEach((vertex, index) => {
92
+ if (vertexIds.has(vertex.id)) {
93
+ issues.push({ path: ['vertices', index, 'id'], message: `Duplicate vertex id: ${vertex.id}` });
94
+ }
95
+ vertexIds.add(vertex.id);
96
+ });
97
+ topology.edges.forEach((edge, index) => {
98
+ if (edgeIds.has(edge.id)) {
99
+ issues.push({ path: ['edges', index, 'id'], message: `Duplicate edge id: ${edge.id}` });
100
+ }
101
+ edgeIds.add(edge.id);
102
+ const [a, b] = edge.vertexIds;
103
+ if (a === b) {
104
+ issues.push({ path: ['edges', index, 'vertexIds'], message: 'An edge needs two vertices' });
105
+ }
106
+ edge.vertexIds.forEach((vertexId, vertexIndex) => {
107
+ if (!vertexIds.has(vertexId)) {
108
+ issues.push({
109
+ path: ['edges', index, 'vertexIds', vertexIndex],
110
+ message: `Unknown vertex id: ${vertexId}`,
111
+ });
112
+ }
113
+ });
114
+ const key = blockUndirectedEdgeKey(a, b);
115
+ if (edgeKeys.has(key)) {
116
+ issues.push({ path: ['edges', index], message: `Duplicate edge: ${a}–${b}` });
117
+ }
118
+ edgeKeys.add(key);
119
+ });
120
+ topology.faces.forEach((face, index) => {
121
+ if (faceIds.has(face.id)) {
122
+ issues.push({ path: ['faces', index, 'id'], message: `Duplicate face id: ${face.id}` });
123
+ }
124
+ faceIds.add(face.id);
125
+ if (new Set(face.vertexIds).size < 3) {
126
+ issues.push({ path: ['faces', index, 'vertexIds'], message: 'A face needs three vertices' });
127
+ }
128
+ face.vertexIds.forEach((vertexId, vertexIndex) => {
129
+ if (!vertexIds.has(vertexId)) {
130
+ issues.push({
131
+ path: ['faces', index, 'vertexIds', vertexIndex],
132
+ message: `Unknown vertex id: ${vertexId}`,
133
+ });
134
+ }
135
+ const nextVertexId = face.vertexIds[(vertexIndex + 1) % face.vertexIds.length];
136
+ if (nextVertexId && !edgeKeys.has(blockUndirectedEdgeKey(vertexId, nextVertexId))) {
137
+ issues.push({
138
+ path: ['faces', index, 'vertexIds', vertexIndex],
139
+ message: `Missing edge for face boundary: ${vertexId}–${nextVertexId}`,
140
+ });
141
+ }
142
+ });
143
+ });
144
+ return issues;
145
+ }
146
+ export const BlockTopology = BlockTopologyShape.superRefine((topology, context) => {
147
+ for (const issue of inspectBlockTopology(topology)) {
148
+ context.addIssue({ code: 'custom', path: issue.path, message: issue.message });
149
+ }
150
+ });
151
+ export function createBoxBlockTopology(width = 2, height = 2.4, depth = 2) {
152
+ const halfWidth = width / 2;
153
+ const halfDepth = depth / 2;
154
+ return {
155
+ vertices: [
156
+ { id: 'v0', position: [-halfWidth, 0, -halfDepth] },
157
+ { id: 'v1', position: [halfWidth, 0, -halfDepth] },
158
+ { id: 'v2', position: [halfWidth, 0, halfDepth] },
159
+ { id: 'v3', position: [-halfWidth, 0, halfDepth] },
160
+ { id: 'v4', position: [-halfWidth, height, -halfDepth] },
161
+ { id: 'v5', position: [halfWidth, height, -halfDepth] },
162
+ { id: 'v6', position: [halfWidth, height, halfDepth] },
163
+ { id: 'v7', position: [-halfWidth, height, halfDepth] },
164
+ ],
165
+ edges: [
166
+ { id: 'e0', vertexIds: ['v0', 'v1'] },
167
+ { id: 'e1', vertexIds: ['v1', 'v2'] },
168
+ { id: 'e2', vertexIds: ['v2', 'v3'] },
169
+ { id: 'e3', vertexIds: ['v3', 'v0'] },
170
+ { id: 'e4', vertexIds: ['v4', 'v5'] },
171
+ { id: 'e5', vertexIds: ['v5', 'v6'] },
172
+ { id: 'e6', vertexIds: ['v6', 'v7'] },
173
+ { id: 'e7', vertexIds: ['v7', 'v4'] },
174
+ { id: 'e8', vertexIds: ['v0', 'v4'] },
175
+ { id: 'e9', vertexIds: ['v1', 'v5'] },
176
+ { id: 'e10', vertexIds: ['v2', 'v6'] },
177
+ { id: 'e11', vertexIds: ['v3', 'v7'] },
178
+ ],
179
+ faces: [
180
+ { id: 'f-bottom', vertexIds: ['v0', 'v1', 'v2', 'v3'], materialSlot: 'body' },
181
+ { id: 'f-top', vertexIds: ['v4', 'v7', 'v6', 'v5'], materialSlot: 'body' },
182
+ { id: 'f-front', vertexIds: ['v0', 'v4', 'v5', 'v1'], materialSlot: 'body' },
183
+ { id: 'f-right', vertexIds: ['v1', 'v5', 'v6', 'v2'], materialSlot: 'body' },
184
+ { id: 'f-back', vertexIds: ['v2', 'v6', 'v7', 'v3'], materialSlot: 'body' },
185
+ { id: 'f-left', vertexIds: ['v3', 'v7', 'v4', 'v0'], materialSlot: 'body' },
186
+ ],
187
+ };
188
+ }
189
+ export const BlockNode = BaseNode.extend({
190
+ id: objectId('block'),
191
+ type: nodeType('block'),
192
+ children: z.array(ItemNode.shape.id).default([]),
193
+ position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
194
+ rotation: z.number().default(0),
195
+ supportSlabId: z.string().optional(),
196
+ topology: BlockTopology.default(createBoxBlockTopology),
197
+ slots: z.record(z.string(), z.string()).default({}),
198
+ slotNames: z.record(z.string(), z.string().min(1)).default({ body: 'Body' }),
199
+ }).describe(dedent `
200
+ block node - a topology-backed editable solid.
201
+ - children: items hosted on persistent topology faces
202
+ - topology: persistent vertices, edges, and ordered face loops with stable IDs
203
+ - position/rotation: level-local placement transform
204
+ - supportSlabId: persisted placement surface that prevents later slabs from lifting the mesh
205
+ - slots: material references keyed by face materialSlot; an unbound body uses the wall-role default
206
+ - slotNames: user-facing names for stable material slots; body is the permanent default slot
207
+ `);
@@ -1,4 +1,9 @@
1
1
  import { z } from 'zod';
2
+ export declare const BoxVentMaterialRole: z.ZodEnum<{
3
+ base: "base";
4
+ top: "top";
5
+ }>;
6
+ export type BoxVentMaterialRole = z.infer<typeof BoxVentMaterialRole>;
2
7
  export declare const BoxVentNode: z.ZodObject<{
3
8
  object: z.ZodDefault<z.ZodLiteral<"node">>;
4
9
  name: z.ZodOptional<z.ZodString>;
@@ -14,7 +19,7 @@ export declare const BoxVentNode: z.ZodObject<{
14
19
  fov: z.ZodOptional<z.ZodNumber>;
15
20
  zoom: z.ZodOptional<z.ZodNumber>;
16
21
  }, z.core.$strip>>;
17
- metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
22
+ metadata: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
18
23
  id: z.ZodDefault<z.ZodTemplateLiteral<`bvent_${string}`>>;
19
24
  type: z.ZodDefault<z.ZodLiteral<"box-vent">>;
20
25
  material: z.ZodOptional<z.ZodObject<{
@@ -49,6 +54,7 @@ export declare const BoxVentNode: z.ZodObject<{
49
54
  scale: z.ZodOptional<z.ZodNumber>;
50
55
  }, z.core.$strip>>;
51
56
  }, z.core.$strip>>;
57
+ slots: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
52
58
  materialPreset: z.ZodDefault<z.ZodString>;
53
59
  roofSegmentId: z.ZodOptional<z.ZodString>;
54
60
  position: z.ZodDefault<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
@@ -68,7 +74,7 @@ export declare const BoxVentNode: z.ZodObject<{
68
74
  box: "box";
69
75
  cap: "cap";
70
76
  dome: "dome";
71
- }>>>;
77
+ }>>, unknown>;
72
78
  }, z.core.$strip>;
73
79
  export type BoxVentNode = z.infer<typeof BoxVentNode>;
74
80
  //# sourceMappingURL=box-vent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"box-vent.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/box-vent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiEvB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAA"}
1
+ {"version":3,"file":"box-vent.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/box-vent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,mBAAmB;;;EAA0B,CAAA;AAC1D,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAErE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkEvB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAA"}
@@ -2,10 +2,12 @@ import dedent from 'dedent';
2
2
  import { z } from 'zod';
3
3
  import { BaseNode, nodeType, objectId } from '../base';
4
4
  import { MaterialSchema } from '../material';
5
+ export const BoxVentMaterialRole = z.enum(['base', 'top']);
5
6
  export const BoxVentNode = BaseNode.extend({
6
7
  id: objectId('bvent'),
7
8
  type: nodeType('box-vent'),
8
9
  material: MaterialSchema.optional(),
10
+ slots: z.record(z.string(), z.string()).optional(),
9
11
  // Default to the white preset so newly-placed vents read as clean
10
12
  // painted metal — and so the paint inspector shows "White" as the
11
13
  // current selection instead of an empty "no material" state, which
@@ -14,7 +14,7 @@ export declare const BuildingNode: z.ZodObject<{
14
14
  fov: z.ZodOptional<z.ZodNumber>;
15
15
  zoom: z.ZodOptional<z.ZodNumber>;
16
16
  }, z.core.$strip>>;
17
- metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
17
+ metadata: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
18
18
  id: z.ZodDefault<z.ZodTemplateLiteral<`building_${string}`>>;
19
19
  type: z.ZodDefault<z.ZodLiteral<"building">>;
20
20
  children: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodDefault<z.ZodTemplateLiteral<`level_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`elevator_${string}`>>]>>>;