@pascal-app/viewer 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/dist/components/renderers/node-renderer.d.ts +1 -1
  2. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.js +38 -22
  4. package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
  5. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
  6. package/dist/components/renderers/parametric-node-renderer.js +34 -0
  7. package/dist/components/renderers/scene-renderer.d.ts +1 -1
  8. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  9. package/dist/components/viewer/ground-occluder.d.ts +1 -1
  10. package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
  11. package/dist/components/viewer/ground-occluder.js +12 -24
  12. package/dist/components/viewer/index.d.ts +37 -1
  13. package/dist/components/viewer/index.d.ts.map +1 -1
  14. package/dist/components/viewer/index.js +186 -63
  15. package/dist/components/viewer/lights.d.ts +1 -1
  16. package/dist/components/viewer/lights.d.ts.map +1 -1
  17. package/dist/components/viewer/lights.js +170 -43
  18. package/dist/components/viewer/perf-monitor.d.ts +1 -1
  19. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  20. package/dist/components/viewer/perf-monitor.js +99 -11
  21. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  22. package/dist/components/viewer/post-processing.js +231 -54
  23. package/dist/components/viewer/registered-systems.d.ts +12 -0
  24. package/dist/components/viewer/registered-systems.d.ts.map +1 -0
  25. package/dist/components/viewer/registered-systems.js +47 -0
  26. package/dist/components/viewer/scene-bvh.d.ts +17 -0
  27. package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
  28. package/dist/components/viewer/scene-bvh.js +94 -0
  29. package/dist/components/viewer/selection-manager.d.ts +1 -1
  30. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  31. package/dist/components/viewer/selection-manager.js +20 -5
  32. package/dist/components/viewer/viewer-camera.d.ts +1 -1
  33. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  34. package/dist/components/viewer/walkthrough-controls.d.ts +1 -1
  35. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  36. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  37. package/dist/hooks/use-node-events.d.ts +5 -73
  38. package/dist/hooks/use-node-events.d.ts.map +1 -1
  39. package/dist/hooks/use-node-events.js +19 -7
  40. package/dist/index.d.ts +36 -4
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +67 -3
  43. package/dist/lib/csg-utils.d.ts +16 -0
  44. package/dist/lib/csg-utils.d.ts.map +1 -0
  45. package/dist/lib/csg-utils.js +33 -0
  46. package/dist/lib/drawable-geometry.d.ts +21 -0
  47. package/dist/lib/drawable-geometry.d.ts.map +1 -0
  48. package/dist/lib/drawable-geometry.js +22 -0
  49. package/dist/lib/edge-style.d.ts +3 -0
  50. package/dist/lib/edge-style.d.ts.map +1 -0
  51. package/dist/lib/edge-style.js +11 -0
  52. package/dist/lib/gpu-perf.d.ts +4 -0
  53. package/dist/lib/gpu-perf.d.ts.map +1 -0
  54. package/dist/lib/gpu-perf.js +23 -0
  55. package/dist/lib/ink-edges.d.ts +9 -0
  56. package/dist/lib/ink-edges.d.ts.map +1 -0
  57. package/dist/lib/ink-edges.js +43 -0
  58. package/dist/lib/isolation.d.ts +39 -0
  59. package/dist/lib/isolation.d.ts.map +1 -0
  60. package/dist/lib/isolation.js +103 -0
  61. package/dist/lib/layers.d.ts +18 -0
  62. package/dist/lib/layers.d.ts.map +1 -1
  63. package/dist/lib/layers.js +18 -0
  64. package/dist/lib/materials.d.ts +25 -15
  65. package/dist/lib/materials.d.ts.map +1 -1
  66. package/dist/lib/materials.js +277 -62
  67. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  68. package/dist/lib/merged-outline-node.js +7 -0
  69. package/dist/lib/polygon-union.d.ts +4 -0
  70. package/dist/lib/polygon-union.d.ts.map +1 -0
  71. package/dist/lib/polygon-union.js +289 -0
  72. package/dist/lib/polygon-union.test.d.ts +2 -0
  73. package/dist/lib/polygon-union.test.d.ts.map +1 -0
  74. package/dist/lib/polygon-union.test.js +105 -0
  75. package/dist/lib/scene-themes.d.ts +29 -0
  76. package/dist/lib/scene-themes.d.ts.map +1 -0
  77. package/dist/lib/scene-themes.js +193 -0
  78. package/dist/store/use-viewer.d.ts +42 -4
  79. package/dist/store/use-viewer.d.ts.map +1 -1
  80. package/dist/store/use-viewer.js +26 -3
  81. package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
  82. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  83. package/dist/systems/ceiling/ceiling-system.js +62 -12
  84. package/dist/systems/door/door-system.d.ts.map +1 -1
  85. package/dist/systems/door/door-system.js +373 -22
  86. package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
  87. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
  88. package/dist/systems/elevator/elevator-interaction-system.js +62 -0
  89. package/dist/systems/fence/fence-system.d.ts +3 -0
  90. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  91. package/dist/systems/fence/fence-system.js +9 -5
  92. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
  93. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
  94. package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
  95. package/dist/systems/geometry/geometry-system.d.ts +34 -0
  96. package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
  97. package/dist/systems/geometry/geometry-system.js +279 -0
  98. package/dist/systems/interactive/interactive-system.d.ts +1 -1
  99. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  100. package/dist/systems/item/item-system.d.ts +7 -0
  101. package/dist/systems/item/item-system.d.ts.map +1 -1
  102. package/dist/systems/item/item-system.js +8 -11
  103. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  104. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  105. package/dist/systems/item-light/item-light-system.js +18 -5
  106. package/dist/systems/roof/roof-materials.d.ts +3 -2
  107. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  108. package/dist/systems/roof/roof-materials.js +38 -14
  109. package/dist/systems/roof/roof-system.d.ts +27 -0
  110. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  111. package/dist/systems/roof/roof-system.js +396 -59
  112. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  113. package/dist/systems/slab/slab-system.js +167 -49
  114. package/dist/systems/slab/slab-system.test.d.ts +2 -0
  115. package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
  116. package/dist/systems/slab/slab-system.test.js +62 -0
  117. package/dist/systems/stair/stair-materials.d.ts +4 -3
  118. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  119. package/dist/systems/stair/stair-materials.js +30 -15
  120. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  121. package/dist/systems/stair/stair-system.js +50 -61
  122. package/dist/systems/surface-hole-geometry.d.ts +3 -0
  123. package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
  124. package/dist/systems/surface-hole-geometry.js +4 -0
  125. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  126. package/dist/systems/wall/wall-cutout.js +18 -2
  127. package/dist/systems/wall/wall-materials.d.ts +4 -3
  128. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  129. package/dist/systems/wall/wall-materials.js +52 -28
  130. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  131. package/dist/systems/wall/wall-system.js +171 -26
  132. package/dist/systems/window/window-system.d.ts.map +1 -1
  133. package/dist/systems/window/window-system.js +71 -7
  134. package/package.json +3 -4
  135. package/dist/components/renderers/building/building-renderer.d.ts +0 -5
  136. package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
  137. package/dist/components/renderers/building/building-renderer.js +0 -11
  138. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
  139. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
  140. package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
  141. package/dist/components/renderers/column/column-renderer.d.ts +0 -5
  142. package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
  143. package/dist/components/renderers/column/column-renderer.js +0 -597
  144. package/dist/components/renderers/door/door-renderer.d.ts +0 -5
  145. package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
  146. package/dist/components/renderers/door/door-renderer.js +0 -16
  147. package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
  148. package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
  149. package/dist/components/renderers/fence/fence-renderer.js +0 -15
  150. package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
  151. package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
  152. package/dist/components/renderers/guide/guide-renderer.js +0 -38
  153. package/dist/components/renderers/item/item-renderer.d.ts +0 -5
  154. package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
  155. package/dist/components/renderers/item/item-renderer.js +0 -172
  156. package/dist/components/renderers/level/level-renderer.d.ts +0 -5
  157. package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
  158. package/dist/components/renderers/level/level-renderer.js +0 -11
  159. package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
  160. package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
  161. package/dist/components/renderers/roof/roof-materials.js +0 -16
  162. package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
  163. package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
  164. package/dist/components/renderers/roof/roof-renderer.js +0 -32
  165. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
  166. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
  167. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
  168. package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
  169. package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
  170. package/dist/components/renderers/scan/scan-renderer.js +0 -54
  171. package/dist/components/renderers/site/site-renderer.d.ts +0 -5
  172. package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
  173. package/dist/components/renderers/site/site-renderer.js +0 -106
  174. package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
  175. package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
  176. package/dist/components/renderers/slab/slab-renderer.js +0 -58
  177. package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
  178. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
  179. package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
  180. package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
  181. package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
  182. package/dist/components/renderers/stair/stair-renderer.js +0 -603
  183. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
  184. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
  185. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
  186. package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
  187. package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
  188. package/dist/components/renderers/wall/wall-renderer.js +0 -16
  189. package/dist/components/renderers/window/window-renderer.d.ts +0 -5
  190. package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
  191. package/dist/components/renderers/window/window-renderer.js +0 -21
  192. package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
  193. package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
  194. package/dist/components/renderers/zone/zone-renderer.js +0 -181
@@ -1,4 +1,4 @@
1
- import { sceneRegistry, useScene, } from '@pascal-app/core';
1
+ import { getEffectiveNode, getSegmentSlopeFrame, hasSegmentMaterialOverride, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
4
  import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
@@ -31,8 +31,22 @@ const _quaternion = new THREE.Quaternion();
31
31
  const _scale = new THREE.Vector3(1, 1, 1);
32
32
  const _yAxis = new THREE.Vector3(0, 1, 0);
33
33
  const _uvFaceNormal = new THREE.Vector3();
34
+ const _uvWorldDown = new THREE.Vector3(0, -1, 0);
35
+ const _uvDownSlope = new THREE.Vector3();
36
+ const _uvAcrossSlope = new THREE.Vector3();
37
+ const _tmpVec3A = new THREE.Vector3();
38
+ const _tmpVec3B = new THREE.Vector3();
39
+ const _surfaceRay = new THREE.Ray();
40
+ const _surfaceOrigin = new THREE.Vector3();
41
+ const _surfaceDir = new THREE.Vector3(0, -1, 0);
42
+ const _surfaceHits = [];
43
+ const _surfaceV0 = new THREE.Vector3();
44
+ const _surfaceV1 = new THREE.Vector3();
45
+ const _surfaceV2 = new THREE.Vector3();
46
+ const _surfaceFaceNormal = new THREE.Vector3();
34
47
  // Pending merged-roof updates carried across frames (for throttling)
35
48
  const pendingRoofUpdates = new Set();
49
+ const warnedMergedRoofNaNIds = new Set();
36
50
  const MAX_ROOFS_PER_FRAME = 1;
37
51
  const MAX_SEGMENTS_PER_FRAME = 3;
38
52
  // ============================================================================
@@ -42,10 +56,16 @@ export const RoofSystem = () => {
42
56
  const dirtyNodes = useScene((state) => state.dirtyNodes);
43
57
  const clearDirty = useScene((state) => state.clearDirty);
44
58
  const rootNodeIds = useScene((state) => state.rootNodeIds);
59
+ // Subscribe so an override-only update (no scene write) still re-runs
60
+ // the component, letting the useFrame loop pick up the latest dirtyNodes
61
+ // set from the render pass that received the override-publishing
62
+ // `markDirty` call. Mirrors WallSystem / DoorSystem.
63
+ useLiveNodeOverrides((s) => s.overrides);
45
64
  useFrame(() => {
46
65
  // Clear stale pending updates when the scene is unloaded
47
66
  if (rootNodeIds.length === 0) {
48
67
  pendingRoofUpdates.clear();
68
+ warnedMergedRoofNaNIds.clear();
49
69
  return;
50
70
  }
51
71
  if (dirtyNodes.size === 0 && pendingRoofUpdates.size === 0)
@@ -57,14 +77,37 @@ export const RoofSystem = () => {
57
77
  const node = nodes[id];
58
78
  if (!node)
59
79
  return;
80
+ // Roof accessories (chimney, skylight, solar-panel, dormer,
81
+ // ridge-vent, box-vent — anything declaring
82
+ // `capabilities.roofAccessory` on its NodeDefinition) cascade
83
+ // their dirty mark to the host segment's parent roof so the
84
+ // merged shell re-CSGs with the new cut. Without this, moving /
85
+ // resizing an accessory leaves the merged roof showing the
86
+ // previous cut shape (stale CSG) once the user exits segment
87
+ // edit mode. Registry-driven so the viewer stays kind-agnostic.
88
+ const def = nodeRegistry.get(node.type);
89
+ if (def?.capabilities?.roofAccessory) {
90
+ const segId = node.roofSegmentId;
91
+ const seg = segId ? nodes[segId] : undefined;
92
+ if (seg?.parentId) {
93
+ pendingRoofUpdates.add(seg.parentId);
94
+ }
95
+ clearDirty(id);
96
+ return;
97
+ }
60
98
  if (node.type === 'roof-segment') {
61
99
  const mesh = sceneRegistry.nodes.get(id);
100
+ // Merge any live override (width / depth / wallHeight / pitch /
101
+ // rotation) so the mesh rebuild reflects the in-flight handle drag
102
+ // without zustand churn. When no override is set this returns the
103
+ // scene node unchanged. Same pattern as DoorSystem / WallSystem.
104
+ const effectiveSegment = getEffectiveNode(node);
62
105
  if (mesh) {
63
106
  // Only compute expensive individual CSG when the segment is actually rendered
64
107
  // (its parent group is visible = the roof is selected for editing)
65
108
  const isVisible = mesh.parent?.visible !== false;
66
109
  if (isVisible && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
67
- updateRoofSegmentGeometry(node, mesh);
110
+ updateRoofSegmentGeometry(effectiveSegment, mesh);
68
111
  segmentsProcessed++;
69
112
  }
70
113
  else if (isVisible) {
@@ -78,12 +121,16 @@ export const RoofSystem = () => {
78
121
  if (mesh.geometry.type === 'BoxGeometry') {
79
122
  mesh.geometry.dispose();
80
123
  const placeholder = new THREE.BufferGeometry();
81
- placeholder.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
124
+ // Three zero-vertices (one degenerate, invisible triangle), not an
125
+ // empty attribute: an empty position (count 0) leaves WebGPU vertex
126
+ // buffer slot 0 unbound if the mesh is ever drawn, and computeBoundsTree
127
+ // needs a real position buffer to index.
128
+ placeholder.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(9), 3));
82
129
  computeGeometryBoundsTree(placeholder);
83
130
  mesh.geometry = placeholder;
84
131
  }
85
- mesh.position.set(node.position[0], node.position[1], node.position[2]);
86
- mesh.rotation.y = node.rotation;
132
+ mesh.position.set(effectiveSegment.position[0], effectiveSegment.position[1], effectiveSegment.position[2]);
133
+ mesh.rotation.y = effectiveSegment.rotation;
87
134
  }
88
135
  clearDirty(id);
89
136
  }
@@ -91,8 +138,8 @@ export const RoofSystem = () => {
91
138
  clearDirty(id);
92
139
  }
93
140
  // Queue the parent roof for a merged geometry update
94
- if (node.parentId) {
95
- pendingRoofUpdates.add(node.parentId);
141
+ if (effectiveSegment.parentId) {
142
+ pendingRoofUpdates.add(effectiveSegment.parentId);
96
143
  }
97
144
  }
98
145
  else if (node.type === 'roof') {
@@ -111,13 +158,15 @@ export const RoofSystem = () => {
111
158
  continue;
112
159
  }
113
160
  const group = sceneRegistry.nodes.get(id);
114
- if (group) {
115
- const mergedMesh = group.getObjectByName('merged-roof');
116
- if (mergedMesh?.visible !== false) {
117
- // Only rebuild when visible — RoofEditSystem re-triggers via markDirty on edit mode exit
118
- updateMergedRoofGeometry(node, group, nodes);
119
- roofsProcessed++;
120
- }
161
+ if (!group)
162
+ continue;
163
+ const mergedMesh = group.getObjectByName('merged-roof');
164
+ if (!mergedMesh)
165
+ continue;
166
+ if (mergedMesh.visible !== false) {
167
+ // Only rebuild when visible — RoofEditSystem re-triggers via markDirty on edit mode exit
168
+ updateMergedRoofGeometry(node, group, nodes);
169
+ roofsProcessed++;
121
170
  }
122
171
  pendingRoofUpdates.delete(id);
123
172
  }
@@ -139,9 +188,21 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
139
188
  const mergedMesh = group.getObjectByName('merged-roof');
140
189
  if (!mergedMesh)
141
190
  return;
191
+ // Segments that carry their own material / preset (catch-all or any of
192
+ // the role-specific fields) are rendered as their own per-segment mesh
193
+ // in `RoofRenderer` so the painted material is preserved. Exclude them
194
+ // from the merged shell — otherwise the merged mesh would draw on top
195
+ // with the roof's default material.
196
+ //
197
+ // Merge each child through `getEffectiveNode` so an in-flight handle
198
+ // drag (live override on width / depth / wallHeight / pitch / rotation)
199
+ // is reflected in the merged shell during the drag, not only on commit.
142
200
  const children = (roofNode.children ?? [])
143
- .map((id) => nodes[id])
144
- .filter(Boolean);
201
+ .map((id) => {
202
+ const scn = nodes[id];
203
+ return scn ? getEffectiveNode(scn) : undefined;
204
+ })
205
+ .filter((n) => n !== undefined && !hasSegmentMaterialOverride(n));
145
206
  if (children.length === 0) {
146
207
  mergedMesh.geometry.dispose();
147
208
  // Keep a valid position attribute so Drei's BVH can index safely.
@@ -156,6 +217,73 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
156
217
  const brushes = getRoofSegmentBrushes(child);
157
218
  if (!brushes)
158
219
  continue;
220
+ // Per-child cuts in SEGMENT-LOCAL space: subtract every accessory
221
+ // that contributes a cut (declares
222
+ // `capabilities.roofAccessory.buildCut`) from shin / deck / wall
223
+ // before we accumulate. Mirrors roof-system v1 — the cut is built
224
+ // in segment-local, then carved out before the segment transform
225
+ // stacks on. Registry-driven so the viewer never names a kind.
226
+ let workingShin = brushes.shinSlab;
227
+ let workingDeck = brushes.deckSlab;
228
+ let workingWall = brushes.wallBrush;
229
+ for (const childElemId of child.children ?? []) {
230
+ const childElem = nodes[childElemId];
231
+ if (!childElem)
232
+ continue;
233
+ const meta = typeof childElem.metadata === 'object' && childElem.metadata !== null
234
+ ? childElem.metadata
235
+ : undefined;
236
+ if (meta?.isTransient)
237
+ continue;
238
+ const childDef = nodeRegistry.get(childElem.type);
239
+ const buildCut = childDef?.capabilities?.roofAccessory?.buildCut;
240
+ if (!buildCut)
241
+ continue;
242
+ const cutGeo = buildCut(childElem, child);
243
+ if (!cutGeo)
244
+ continue;
245
+ // Wrap the kind-emitted geometry in a Brush. Kinds return raw
246
+ // shapes; the viewer welds (mandatory after rotations leave
247
+ // duplicated verts), attaches a single material group, and
248
+ // builds the bounds tree — keeping kind code free of
249
+ // three-bvh-csg / three-mesh-bvh imports.
250
+ const welded = mergeVertices(cutGeo, 1e-4);
251
+ cutGeo.dispose();
252
+ const idxCount = welded.getIndex()?.count ?? 0;
253
+ if (idxCount === 0) {
254
+ welded.dispose();
255
+ continue;
256
+ }
257
+ welded.clearGroups();
258
+ welded.addGroup(0, idxCount, 0);
259
+ welded.computeVertexNormals();
260
+ computeGeometryBoundsTree(welded);
261
+ const cut = new Brush(welded, dummyMats[0]);
262
+ cut.updateMatrixWorld();
263
+ try {
264
+ const nextShin = csgEvaluator.evaluate(workingShin, cut, SUBTRACTION);
265
+ workingShin.geometry.dispose();
266
+ prepareBrushForCSG(nextShin);
267
+ workingShin = nextShin;
268
+ const nextDeck = csgEvaluator.evaluate(workingDeck, cut, SUBTRACTION);
269
+ workingDeck.geometry.dispose();
270
+ prepareBrushForCSG(nextDeck);
271
+ workingDeck = nextDeck;
272
+ const nextWall = csgEvaluator.evaluate(workingWall, cut, SUBTRACTION);
273
+ workingWall.geometry.dispose();
274
+ prepareBrushForCSG(nextWall);
275
+ workingWall = nextWall;
276
+ }
277
+ catch (e) {
278
+ console.error(`[${childElem.type}] cut CSG failed:`, e);
279
+ }
280
+ finally {
281
+ cut.geometry.dispose();
282
+ }
283
+ }
284
+ brushes.shinSlab = workingShin;
285
+ brushes.deckSlab = workingDeck;
286
+ brushes.wallBrush = workingWall;
159
287
  _matrix.compose(_position.set(child.position[0], child.position[1], child.position[2]), _quaternion.setFromAxisAngle(_yAxis, child.rotation), _scale);
160
288
  const applyTransform = (brush) => {
161
289
  csgGeometry(brush).applyMatrix4(_matrix);
@@ -214,6 +342,22 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
214
342
  const shinDeck = csgEvaluator.evaluate(finalShinTrimmed, finalDeckTrimmed, ADDITION);
215
343
  const combined = csgEvaluator.evaluate(shinDeck, finalWallTrimmed, ADDITION);
216
344
  const resultGeo = csgGeometry(combined);
345
+ if (geometryHasNaNPositions(resultGeo)) {
346
+ if (!warnedMergedRoofNaNIds.has(roofNode.id)) {
347
+ console.warn('[RoofSystem] Skipping merged roof geometry with NaN positions', roofNode.id);
348
+ warnedMergedRoofNaNIds.add(roofNode.id);
349
+ }
350
+ resultGeo.dispose();
351
+ finalShinTrimmed.geometry.dispose();
352
+ finalDeckTrimmed.geometry.dispose();
353
+ finalWallTrimmed.geometry.dispose();
354
+ shinDeck.geometry.dispose();
355
+ totalShinSlab.geometry.dispose();
356
+ totalDeckSlab.geometry.dispose();
357
+ totalWall.geometry.dispose();
358
+ totalInner.geometry.dispose();
359
+ return;
360
+ }
217
361
  const resultMaterials = csgMaterials(combined);
218
362
  const matToIndex = new Map([
219
363
  [dummyMats[0], 0],
@@ -242,19 +386,54 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
242
386
  totalInner.geometry.dispose();
243
387
  }
244
388
  }
245
- const dummyMats = [
389
+ function geometryHasNaNPositions(geometry) {
390
+ const position = geometry.getAttribute('position');
391
+ if (!position)
392
+ return false;
393
+ for (let i = 0; i < position.array.length; i++) {
394
+ if (Number.isNaN(position.array[i]))
395
+ return true;
396
+ }
397
+ return false;
398
+ }
399
+ /**
400
+ * Four dummy materials used as identity placeholders during CSG. Shared
401
+ * across every input brush so three-bvh-csg can preserve reference
402
+ * equality on the result and `mapRoofGroupMaterialIndex` can map result
403
+ * groups back to slots 0..3. Exposed so kinds that compose additional
404
+ * CSG ops on top of `getRoofSegmentBrushes` (e.g. dormer) use the same
405
+ * identity refs.
406
+ */
407
+ export const roofCsgDummyMats = [
246
408
  new THREE.MeshBasicMaterial(),
247
409
  new THREE.MeshBasicMaterial(),
248
410
  new THREE.MeshBasicMaterial(),
249
411
  new THREE.MeshBasicMaterial(),
250
412
  ];
251
- const ROOF_MATERIAL_SLOT_COUNT = 4;
252
- function mapRoofGroupMaterialIndex(groupMaterialIndex, csgMaterials, matToIndex) {
413
+ // Internal alias kept so the surrounding file's many call sites don't churn.
414
+ const dummyMats = roofCsgDummyMats;
415
+ export const ROOF_MATERIAL_SLOT_COUNT = 4;
416
+ export function mapRoofGroupMaterialIndex(groupMaterialIndex, csgMaterials, matToIndex) {
253
417
  if (groupMaterialIndex === undefined)
254
418
  return 0;
419
+ // Primary path — reference-equality lookup. Fast and exact when
420
+ // three-bvh-csg preserves the original `dummyMats` references on
421
+ // the result brush.
255
422
  const sourceMaterial = csgMaterials[groupMaterialIndex];
256
423
  const mappedIndex = sourceMaterial ? matToIndex.get(sourceMaterial) : undefined;
257
- return mappedIndex ?? 0;
424
+ if (mappedIndex !== undefined)
425
+ return mappedIndex;
426
+ // Robust fallback — every input brush was constructed with the same
427
+ // 4-slot `dummyMats` array, so after N union/subtraction passes the
428
+ // result's material array is `[dummyMats[0..3], dummyMats[0..3], ...]`
429
+ // and the group's materialIndex is `slot + (brushOffset * 4)`. The
430
+ // slot we care about is therefore `materialIndex % 4`. Without this
431
+ // fallback, any CSG pass that returns a fresh `Material` object (or
432
+ // clones the dummyMats refs) makes every group collapse to slot 0
433
+ // (Wall) — which is the "shape is there but the wrong colour"
434
+ // symptom roofs show after deselect / refresh.
435
+ return (((groupMaterialIndex % ROOF_MATERIAL_SLOT_COUNT) + ROOF_MATERIAL_SLOT_COUNT) %
436
+ ROOF_MATERIAL_SLOT_COUNT);
258
437
  }
259
438
  function normalizeRoofMaterialIndex(materialIndex) {
260
439
  if (materialIndex === undefined || !Number.isFinite(materialIndex))
@@ -272,38 +451,20 @@ const RAKE_FACE_ALIGNMENT_EPSILON = 0.35;
272
451
  * Ports the prototype's CSG approach using three-bvh-csg.
273
452
  */
274
453
  export function getRoofSegmentBrushes(node) {
275
- const { roofType, width, depth, wallHeight, roofHeight, wallThickness, deckThickness, overhang, shingleThickness, } = node;
276
- const activeRh = roofType === 'flat' ? 0 : roofHeight;
277
- let run = Math.min(width, depth) / 2;
278
- let rise = activeRh;
279
- if (roofType === 'shed') {
280
- run = depth;
281
- }
282
- if (roofType === 'gable') {
283
- run = depth / 2;
284
- }
285
- if (roofType === 'gambrel') {
286
- run = depth / 4;
287
- rise = activeRh * 0.6;
288
- }
289
- if (roofType === 'mansard') {
290
- run = Math.min(width, depth) * 0.15;
291
- rise = activeRh * 0.7;
292
- }
293
- if (roofType === 'dutch') {
294
- run = Math.min(width, depth) * 0.25;
295
- rise = activeRh * 0.5;
296
- }
297
- const tanTheta = run > 0 ? rise / run : 0;
298
- const cosTheta = Math.cos(Math.atan2(rise, run)) || 1;
299
- const sinTheta = Math.sin(Math.atan2(rise, run)) || 0;
454
+ const { roofType, width, depth, wallHeight, wallThickness, deckThickness, overhang, shingleThickness, } = node;
455
+ const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node);
456
+ const shapeRatios = {
457
+ gambrelLowerWidthRatio: node.gambrelLowerWidthRatio,
458
+ mansardSteepWidthRatio: node.mansardSteepWidthRatio,
459
+ dutchHipWidthRatio: node.dutchHipWidthRatio,
460
+ };
300
461
  const verticalRt = activeRh > 0 ? deckThickness / cosTheta : deckThickness;
301
462
  const baseI = Math.min(width, depth) * 0.25;
302
463
  const getVol = (wExt, vOffset, baseY, matIndex, isVoid) => {
303
464
  const wV = Math.max(0.01, width + 2 * wExt);
304
465
  const dV = Math.max(0.01, depth + 2 * wExt);
305
466
  const autoDrop = wExt * tanTheta;
306
- const whV = wallHeight - autoDrop + vOffset;
467
+ const whV = Math.max(0.01, wallHeight - autoDrop + vOffset);
307
468
  let rhV = activeRh;
308
469
  if (activeRh > 0) {
309
470
  rhV = activeRh + autoDrop;
@@ -315,7 +476,7 @@ export function getRoofSegmentBrushes(node) {
315
476
  if (isVoid) {
316
477
  structuralI += deckThickness;
317
478
  }
318
- const faces = getModuleFaces(roofType, wV, dV, whV, rhV, safeBaseY, { dutchI: structuralI }, width, depth, tanTheta);
479
+ const faces = getModuleFaces(roofType, wV, dV, whV, rhV, safeBaseY, { dutchI: structuralI }, width, depth, tanTheta, shapeRatios);
319
480
  return createGeometryFromFaces(faces, matIndex);
320
481
  };
321
482
  const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false);
@@ -389,8 +550,8 @@ export function getRoofSegmentBrushes(node) {
389
550
  };
390
551
  const insetsBot = getInsets(shinBotWh, botBaseY, true, shinBotW, shinBotD);
391
552
  const insetsTop = getInsets(shinTopWh, topBaseY, false, shinTopW, shinTopD);
392
- const botFaces = getModuleFaces(roofType, shinBotW, shinBotD, shinBotWh, shinBotRh, botBaseY, insetsBot, width, depth, tanTheta);
393
- const topFaces = getModuleFaces(roofType, shinTopW, shinTopD, shinTopWh, shinTopRh, topBaseY, insetsTop, width, depth, tanTheta);
553
+ const botFaces = getModuleFaces(roofType, shinBotW, shinBotD, shinBotWh, shinBotRh, botBaseY, insetsBot, width, depth, tanTheta, shapeRatios);
554
+ const topFaces = getModuleFaces(roofType, shinTopW, shinTopD, shinTopWh, shinTopRh, topBaseY, insetsTop, width, depth, tanTheta, shapeRatios);
394
555
  const shinBotGeo = createGeometryFromFaces(botFaces, 1);
395
556
  const shinTopGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : 1);
396
557
  if (transZ !== 0) {
@@ -509,7 +670,7 @@ export function generateRoofSegmentGeometry(node) {
509
670
  ensureUv2Attribute(resultGeo);
510
671
  return resultGeo;
511
672
  }
512
- function remapRoofShellFaces(geometry, node) {
673
+ export function remapRoofShellFaces(geometry, node) {
513
674
  const position = geometry.getAttribute('position');
514
675
  const index = geometry.getIndex();
515
676
  if (!(position && index) || index.count === 0 || geometry.groups.length === 0)
@@ -600,8 +761,12 @@ function getRakeAxis(node) {
600
761
  /**
601
762
  * Generates faces for a roof module volume.
602
763
  * Supports: hip, gable, shed, gambrel, dutch, mansard, flat.
764
+ *
765
+ * `shapeRatios` controls the kink positions on multi-slope roofs. The
766
+ * height ratios are already baked into `tanTheta` (via the slope frame)
767
+ * so they don't need to be threaded again.
603
768
  */
604
- function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanTheta) {
769
+ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanTheta, shapeRatios) {
605
770
  const v = (x, y, z) => new THREE.Vector3(x, y, z);
606
771
  const { iF = 0, iB = 0, iL = 0, iR = 0 } = insets;
607
772
  const b1 = v(-w / 2 + iL, baseY, d / 2 - iF);
@@ -646,7 +811,7 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
646
811
  faces.push([e1, e2, t2, t1], [e2, e3, t2], [e3, e4, t1, t2], [e4, e1, t1]);
647
812
  }
648
813
  else if (type === 'gambrel') {
649
- const mz = (baseD / 2) * 0.5;
814
+ const mz = (baseD / 2) * shapeRatios.gambrelLowerWidthRatio;
650
815
  const dist = d / 2 - mz;
651
816
  const mh = wh + dist * (tanTheta || 0);
652
817
  const m1 = v(-w / 2, mh, mz);
@@ -658,7 +823,7 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
658
823
  faces.push([e4, e1, m1, r1, m4], [e2, e3, m3, r2, m2], [e1, e2, m2, m1], [m1, m2, r2, r1], [e3, e4, m4, m3], [m3, m4, r1, r2]);
659
824
  }
660
825
  else if (type === 'mansard') {
661
- const i = Math.min(baseW, baseD) * 0.15;
826
+ const i = Math.min(baseW, baseD) * shapeRatios.mansardSteepWidthRatio;
662
827
  const mh = wh + i * (tanTheta || 0);
663
828
  const m1 = v(-w / 2 + i, mh, d / 2 - i);
664
829
  const m2 = v(w / 2 - i, mh, d / 2 - i);
@@ -685,7 +850,9 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
685
850
  }
686
851
  }
687
852
  else if (type === 'dutch') {
688
- const i = insets.dutchI !== undefined ? insets.dutchI : Math.min(baseW, baseD) * 0.25;
853
+ const i = insets.dutchI !== undefined
854
+ ? insets.dutchI
855
+ : Math.min(baseW, baseD) * shapeRatios.dutchHipWidthRatio;
689
856
  const mh = wh + i * (tanTheta || 0);
690
857
  if (w >= d) {
691
858
  const m1 = v(-w / 2 + i, mh, d / 2 - i);
@@ -728,6 +895,25 @@ function createGeometryFromFaces(faces, matRule = null) {
728
895
  const vA = new THREE.Vector3().subVectors(p1, p0);
729
896
  const vB = new THREE.Vector3().subVectors(p2, p0);
730
897
  const normal = new THREE.Vector3().crossVectors(vA, vB).normalize();
898
+ let slopeAlignedDown = null;
899
+ let slopeAlignedAcross = null;
900
+ let slopeAlignedVOrigin = 0;
901
+ if (normal.y > SHINGLE_SURFACE_EPSILON) {
902
+ _uvDownSlope.copy(_uvWorldDown).projectOnPlane(normal);
903
+ if (_uvDownSlope.lengthSq() > 1e-8) {
904
+ _uvDownSlope.normalize();
905
+ _uvAcrossSlope.crossVectors(_uvDownSlope, normal).normalize();
906
+ let highestPoint = face[0];
907
+ for (const candidate of face) {
908
+ if (candidate.y > highestPoint.y) {
909
+ highestPoint = candidate;
910
+ }
911
+ }
912
+ slopeAlignedDown = _uvDownSlope.clone();
913
+ slopeAlignedAcross = _uvAcrossSlope.clone();
914
+ slopeAlignedVOrigin = highestPoint.dot(slopeAlignedDown);
915
+ }
916
+ }
731
917
  let assignedMatIndex = 0;
732
918
  if (typeof matRule === 'function') {
733
919
  assignedMatIndex = matRule(normal);
@@ -750,9 +936,16 @@ function createGeometryFromFaces(faces, matRule = null) {
750
936
  normals.push(normal.x, normal.y, normal.z);
751
937
  normals.push(normal.x, normal.y, normal.z);
752
938
  normals.push(normal.x, normal.y, normal.z);
753
- pushRoofUv(uvs, p0, normal);
754
- pushRoofUv(uvs, fi, normal);
755
- pushRoofUv(uvs, fi1, normal);
939
+ if (slopeAlignedDown && slopeAlignedAcross) {
940
+ uvs.push(p0.dot(slopeAlignedAcross), slopeAlignedVOrigin - p0.dot(slopeAlignedDown));
941
+ uvs.push(fi.dot(slopeAlignedAcross), slopeAlignedVOrigin - fi.dot(slopeAlignedDown));
942
+ uvs.push(fi1.dot(slopeAlignedAcross), slopeAlignedVOrigin - fi1.dot(slopeAlignedDown));
943
+ }
944
+ else {
945
+ pushRoofUv(uvs, p0, normal);
946
+ pushRoofUv(uvs, fi, normal);
947
+ pushRoofUv(uvs, fi1, normal);
948
+ }
756
949
  indices.push(vertexCount, vertexCount + 1, vertexCount + 2);
757
950
  faceVertexCount += 3;
758
951
  vertexCount += 3;
@@ -786,11 +979,20 @@ function pushRoofUv(uvs, point, normal) {
786
979
  uvs.push(point.x, point.z);
787
980
  return;
788
981
  }
982
+ if (_uvFaceNormal.y > SHINGLE_SURFACE_EPSILON) {
983
+ _uvDownSlope.copy(_uvWorldDown).projectOnPlane(_uvFaceNormal);
984
+ if (_uvDownSlope.lengthSq() > 1e-8) {
985
+ _uvDownSlope.normalize();
986
+ _uvAcrossSlope.crossVectors(_uvDownSlope, _uvFaceNormal).normalize();
987
+ uvs.push(point.dot(_uvAcrossSlope), point.dot(_uvDownSlope));
988
+ return;
989
+ }
990
+ }
789
991
  if (absX >= absZ) {
790
- uvs.push(point.z, point.y);
992
+ uvs.push(_uvFaceNormal.x >= 0 ? point.z : -point.z, -point.y);
791
993
  return;
792
994
  }
793
- uvs.push(point.x, point.y);
995
+ uvs.push(_uvFaceNormal.z >= 0 ? point.x : -point.x, -point.y);
794
996
  }
795
997
  function ensureUv2Attribute(geometry) {
796
998
  const uv = geometry.getAttribute('uv');
@@ -798,3 +1000,138 @@ function ensureUv2Attribute(geometry) {
798
1000
  return;
799
1001
  geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
800
1002
  }
1003
+ /**
1004
+ * Returns the outer roof surface frame (point + normal) at a given segment-local XZ.
1005
+ * This is used for skylight placement + cut direction so cutouts remain perpendicular
1006
+ * to the true roof surface even on multi-slope roofs (gambrel/mansard/dutch).
1007
+ */
1008
+ export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
1009
+ const { roofType, width, depth, wallHeight, wallThickness, deckThickness, overhang, shingleThickness, } = segment;
1010
+ const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(segment);
1011
+ if (roofType === 'flat' || activeRh === 0) {
1012
+ return {
1013
+ point: new THREE.Vector3(lx, wallHeight + deckThickness + shingleThickness, lz),
1014
+ normal: new THREE.Vector3(0, 1, 0),
1015
+ };
1016
+ }
1017
+ const verticalRt = deckThickness / cosTheta;
1018
+ const horizontalOverhang = overhang * cosTheta;
1019
+ const deckExt = wallThickness / 2 + horizontalOverhang;
1020
+ const stSin = shingleThickness * sinTheta;
1021
+ const stCos = shingleThickness * cosTheta;
1022
+ const shinBotW = Math.max(0.01, width + 2 * deckExt);
1023
+ const shinBotD = Math.max(0.01, depth + 2 * deckExt);
1024
+ const deckDrop = deckExt * tanTheta;
1025
+ const shinBotWh = wallHeight - deckDrop + verticalRt;
1026
+ let shinBotRh = activeRh;
1027
+ if (activeRh > 0) {
1028
+ shinBotRh = activeRh + deckDrop;
1029
+ if (roofType === 'shed')
1030
+ shinBotRh = activeRh + 2 * deckDrop;
1031
+ }
1032
+ let shinTopW = shinBotW;
1033
+ let shinTopD = shinBotD;
1034
+ let transZ = 0;
1035
+ if (['hip', 'mansard', 'dutch'].includes(roofType)) {
1036
+ shinTopW += 2 * stSin;
1037
+ shinTopD += 2 * stSin;
1038
+ }
1039
+ else {
1040
+ shinTopW += 2 * stSin;
1041
+ shinTopD += 2 * stSin;
1042
+ transZ = stSin;
1043
+ }
1044
+ const shinTopWh = shinBotWh + stCos;
1045
+ const shinTopRh = shinBotRh + stCos;
1046
+ const topBaseY = 0;
1047
+ const baseI = Math.min(width, depth) * 0.25;
1048
+ const getInsets = (_wh, _baseY, isVoid, _wV, _dV) => {
1049
+ const inset = Math.max(0.01, baseI);
1050
+ let iF = 0;
1051
+ let iB = 0;
1052
+ let iL = 0;
1053
+ let iR = 0;
1054
+ if (roofType === 'hip') {
1055
+ iF = inset;
1056
+ iB = inset;
1057
+ iL = inset;
1058
+ iR = inset;
1059
+ }
1060
+ else if (roofType === 'gable' || roofType === 'gambrel') {
1061
+ iL = inset;
1062
+ iR = inset;
1063
+ }
1064
+ else if (roofType === 'mansard' || roofType === 'dutch') {
1065
+ iF = inset;
1066
+ iB = inset;
1067
+ iL = inset;
1068
+ iR = inset;
1069
+ }
1070
+ else if (roofType === 'shed') {
1071
+ iF = inset;
1072
+ }
1073
+ let structuralI = baseI;
1074
+ if (isVoid) {
1075
+ structuralI += shingleThickness;
1076
+ }
1077
+ return { iF, iB, iL, iR, dutchI: structuralI };
1078
+ };
1079
+ const insetsTop = getInsets(shinTopWh, topBaseY, false, shinTopW, shinTopD);
1080
+ const shapeRatios = {
1081
+ gambrelLowerWidthRatio: segment.gambrelLowerWidthRatio,
1082
+ mansardSteepWidthRatio: segment.mansardSteepWidthRatio,
1083
+ dutchHipWidthRatio: segment.dutchHipWidthRatio,
1084
+ };
1085
+ const topFaces = getModuleFaces(roofType, shinTopW, shinTopD, shinTopWh, shinTopRh, topBaseY, insetsTop, width, depth, tanTheta, shapeRatios);
1086
+ const topGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : 1);
1087
+ if (transZ !== 0)
1088
+ topGeo.translate(0, 0, transZ);
1089
+ topGeo.computeBoundingBox();
1090
+ const topY = wallHeight + activeRh + deckThickness + shingleThickness + 10;
1091
+ _surfaceOrigin.set(lx, topY, lz);
1092
+ _surfaceRay.set(_surfaceOrigin, _surfaceDir);
1093
+ _surfaceHits.length = 0;
1094
+ const pos = topGeo.getAttribute('position');
1095
+ const index = topGeo.getIndex();
1096
+ if (!pos || !index) {
1097
+ topGeo.dispose();
1098
+ return {
1099
+ point: new THREE.Vector3(lx, wallHeight, lz),
1100
+ normal: new THREE.Vector3(0, 1, 0),
1101
+ };
1102
+ }
1103
+ let bestT = Number.POSITIVE_INFINITY;
1104
+ let bestPoint = null;
1105
+ let bestNormal = null;
1106
+ for (let i = 0; i < index.count; i += 3) {
1107
+ const a = index.getX(i);
1108
+ const b = index.getX(i + 1);
1109
+ const c = index.getX(i + 2);
1110
+ _surfaceV0.fromBufferAttribute(pos, a);
1111
+ _surfaceV1.fromBufferAttribute(pos, b);
1112
+ _surfaceV2.fromBufferAttribute(pos, c);
1113
+ const hit = _surfaceRay.intersectTriangle(_surfaceV0, _surfaceV1, _surfaceV2, false, _tmpVec3A);
1114
+ if (!hit)
1115
+ continue;
1116
+ const t = hit.distanceTo(_surfaceOrigin);
1117
+ if (t < bestT) {
1118
+ bestT = t;
1119
+ bestPoint = hit.clone();
1120
+ _surfaceFaceNormal
1121
+ .subVectors(_surfaceV1, _surfaceV0)
1122
+ .cross(_tmpVec3B.subVectors(_surfaceV2, _surfaceV0))
1123
+ .normalize();
1124
+ bestNormal = _surfaceFaceNormal.clone();
1125
+ }
1126
+ }
1127
+ topGeo.dispose();
1128
+ if (!bestPoint || !bestNormal) {
1129
+ return {
1130
+ point: new THREE.Vector3(lx, wallHeight, lz),
1131
+ normal: new THREE.Vector3(0, 1, 0),
1132
+ };
1133
+ }
1134
+ if (bestNormal.y < 0)
1135
+ bestNormal.multiplyScalar(-1);
1136
+ return { point: bestPoint, normal: bestNormal };
1137
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,QAAQ,EAGd,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,eAAO,MAAM,UAAU,YAwBtB,CAAA;AAkBD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAG7E"}
1
+ {"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,QAAQ,EAGd,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAe9B,eAAO,MAAM,UAAU,YAkCtB,CAAA;AAkBD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAG7E"}