@pascal-app/editor 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 (283) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +15 -8
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
  6. package/src/components/editor/bake-exporter.tsx +1 -0
  7. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  8. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  9. package/src/components/editor/capture-camera-rig.tsx +59 -0
  10. package/src/components/editor/custom-camera-controls.tsx +20 -11
  11. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  12. package/src/components/editor/editor-layout-v2.tsx +17 -5
  13. package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
  14. package/src/components/editor/export-manager.tsx +175 -33
  15. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  16. package/src/components/editor/first-person-controls.tsx +182 -57
  17. package/src/components/editor/floating-action-menu.tsx +55 -55
  18. package/src/components/editor/floorplan-background-selection.test.ts +31 -4
  19. package/src/components/editor/floorplan-background-selection.ts +27 -7
  20. package/src/components/editor/floorplan-panel.tsx +81 -57
  21. package/src/components/editor/grid.tsx +12 -7
  22. package/src/components/editor/group-actions.ts +34 -50
  23. package/src/components/editor/group-floating-action-menu.tsx +30 -15
  24. package/src/components/editor/group-move-3d.ts +51 -21
  25. package/src/components/editor/group-transform-shared.test.ts +78 -0
  26. package/src/components/editor/group-transform-shared.ts +74 -7
  27. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  28. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  29. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  30. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  31. package/src/components/editor/handles/resize-snap.ts +10 -1
  32. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  33. package/src/components/editor/index.tsx +252 -46
  34. package/src/components/editor/measurement-pill.tsx +12 -12
  35. package/src/components/editor/node-action-menu.tsx +42 -1
  36. package/src/components/editor/node-arrow-handles.tsx +47 -58
  37. package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
  38. package/src/components/editor/selection-manager.tsx +132 -47
  39. package/src/components/editor/site-edge-labels.tsx +2 -1
  40. package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
  41. package/src/components/editor/snapshot-capture.test.ts +422 -0
  42. package/src/components/editor/snapshot-capture.ts +145 -0
  43. package/src/components/editor/thumbnail-generator.tsx +295 -232
  44. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  45. package/src/components/editor/wall-measurement-label.tsx +3 -2
  46. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  47. package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
  48. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  49. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  50. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  51. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  52. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  53. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  54. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
  57. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  59. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  60. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  61. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  62. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  63. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  64. package/src/components/systems/selection-affordance-services.ts +19 -0
  65. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  66. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  67. package/src/components/tools/fence/fence-drafting.ts +6 -25
  68. package/src/components/tools/item/draft-creation.test.ts +12 -0
  69. package/src/components/tools/item/draft-creation.ts +10 -0
  70. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  71. package/src/components/tools/item/face-host-commit.ts +56 -0
  72. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  73. package/src/components/tools/item/face-host-preview.ts +82 -0
  74. package/src/components/tools/item/move-tool.tsx +4 -3
  75. package/src/components/tools/item/placement-math.test.ts +11 -1
  76. package/src/components/tools/item/placement-math.ts +0 -20
  77. package/src/components/tools/item/placement-strategies.test.ts +494 -0
  78. package/src/components/tools/item/placement-strategies.ts +192 -24
  79. package/src/components/tools/item/placement-surface.test.ts +61 -0
  80. package/src/components/tools/item/placement-surface.ts +24 -0
  81. package/src/components/tools/item/placement-types.ts +6 -0
  82. package/src/components/tools/item/test-face-host.ts +121 -0
  83. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  84. package/src/components/tools/item/use-draft-node.ts +26 -4
  85. package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
  86. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  87. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  88. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  89. package/src/components/tools/registry-tool-context.tsx +30 -0
  90. package/src/components/tools/select/box-select-tool.tsx +3 -1
  91. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  92. package/src/components/tools/select/marquee-footprint.ts +29 -0
  93. package/src/components/tools/shared/placement-box.tsx +63 -8
  94. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  95. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  96. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  97. package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
  98. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  99. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  100. package/src/components/tools/stair/stair-tool.tsx +127 -74
  101. package/src/components/tools/tool-manager.tsx +25 -8
  102. package/src/components/tools/wall/wall-drafting.test.ts +527 -4
  103. package/src/components/tools/wall/wall-drafting.ts +55 -462
  104. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  105. package/src/components/ui/action-menu/index.tsx +11 -1
  106. package/src/components/ui/command-palette/editor-commands.tsx +12 -13
  107. package/src/components/ui/command-palette/index.tsx +7 -2
  108. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  109. package/src/components/ui/controls/material-picker.tsx +4 -4
  110. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  111. package/src/components/ui/controls/segmented-control.tsx +8 -1
  112. package/src/components/ui/controls/slider-control.tsx +19 -3
  113. package/src/components/ui/controls/toggle-control.tsx +18 -5
  114. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  115. package/src/components/ui/floating-level-selector.tsx +23 -6
  116. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  117. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  118. package/src/components/ui/helpers/item-helper.tsx +1 -1
  119. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  120. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  121. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  122. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  123. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  124. package/src/components/ui/panels/multi-field-value.ts +163 -0
  125. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  126. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  127. package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
  128. package/src/components/ui/panels/node-display.ts +4 -0
  129. package/src/components/ui/panels/panel-manager.tsx +77 -3
  130. package/src/components/ui/panels/panel-wrapper.tsx +228 -12
  131. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  132. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  133. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  134. package/src/components/ui/panels/reference-panel.tsx +31 -1
  135. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  136. package/src/components/ui/scene-loader.tsx +35 -0
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
  138. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  139. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
  140. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  142. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  143. package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
  144. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  145. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  146. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  147. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
  148. package/src/components/ui/sidebar/tab-bar.tsx +9 -1
  149. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  150. package/src/components/viewer/floorplan-compass-button.tsx +50 -0
  151. package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
  152. package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
  153. package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
  154. package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
  155. package/src/components/viewer/floorplan-preview.tsx +1069 -0
  156. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
  157. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  158. package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
  159. package/src/components/viewer/viewer-stage-modes.ts +49 -0
  160. package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
  161. package/src/components/viewer/viewer-stage.test.tsx +47 -0
  162. package/src/components/viewer/viewer-stage.tsx +221 -0
  163. package/src/components/viewer-overlay.tsx +13 -30
  164. package/src/components/viewer-zone-system.tsx +6 -2
  165. package/src/components/walkthrough-hud.tsx +19 -10
  166. package/src/hooks/use-auto-save.test.ts +122 -1
  167. package/src/hooks/use-auto-save.ts +119 -13
  168. package/src/hooks/use-ceiling-events.test.ts +122 -0
  169. package/src/hooks/use-drag-action.ts +17 -3
  170. package/src/hooks/use-grid-events.test.ts +240 -0
  171. package/src/hooks/use-grid-events.ts +190 -11
  172. package/src/hooks/use-keyboard.test.ts +244 -0
  173. package/src/hooks/use-keyboard.ts +164 -63
  174. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  175. package/src/hooks/use-save-shortcut.ts +33 -0
  176. package/src/index.tsx +109 -4
  177. package/src/lib/active-placement-surface.test.ts +16 -0
  178. package/src/lib/active-placement-surface.ts +14 -0
  179. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  180. package/src/lib/ceiling-surface-raycast.ts +12 -0
  181. package/src/lib/contextual-help-extension.ts +17 -0
  182. package/src/lib/contextual-help.test.ts +8 -0
  183. package/src/lib/contextual-help.ts +8 -0
  184. package/src/lib/continuation.test.ts +11 -0
  185. package/src/lib/continuation.ts +8 -1
  186. package/src/lib/direct-manipulation.test.ts +132 -0
  187. package/src/lib/direct-manipulation.ts +38 -1
  188. package/src/lib/editor-api.ts +6 -3
  189. package/src/lib/elevation-guides.test.ts +28 -1
  190. package/src/lib/elevation-guides.ts +22 -7
  191. package/src/lib/floating-menu-scale.test.ts +29 -0
  192. package/src/lib/floating-menu-scale.ts +14 -0
  193. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  194. package/src/lib/floorplan/apply-alignment.ts +10 -1
  195. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  196. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  197. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  198. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  199. package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
  200. package/src/lib/floorplan/floorplan-readonly.ts +85 -0
  201. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  202. package/src/lib/floorplan-grid-event-point.ts +13 -0
  203. package/src/lib/glb-export.test.ts +1169 -21
  204. package/src/lib/glb-export.ts +629 -101
  205. package/src/lib/grid-event-presentation.ts +16 -0
  206. package/src/lib/history.test.ts +523 -77
  207. package/src/lib/history.ts +133 -14
  208. package/src/lib/host-tree-children.test.ts +27 -0
  209. package/src/lib/host-tree-children.ts +79 -0
  210. package/src/lib/inspector-card-mode.test.ts +92 -0
  211. package/src/lib/inspector-card-mode.ts +60 -0
  212. package/src/lib/interaction/hot-set.test.ts +10 -1
  213. package/src/lib/interaction/hot-set.ts +3 -1
  214. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  215. package/src/lib/interaction/overlay-policy.ts +16 -0
  216. package/src/lib/interaction/registered-drafting.ts +36 -0
  217. package/src/lib/interaction/scope.ts +31 -0
  218. package/src/lib/level-print-export.test.ts +484 -0
  219. package/src/lib/level-print-export.ts +526 -0
  220. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  221. package/src/lib/local-project-presentation-persistence.ts +335 -0
  222. package/src/lib/material-paint.ts +2 -0
  223. package/src/lib/measurement-parser.ts +3 -0
  224. package/src/lib/measurements.test.ts +4 -0
  225. package/src/lib/model-export.ts +29 -0
  226. package/src/lib/paint-preview-owner.ts +71 -0
  227. package/src/lib/paint-scope.test.ts +58 -2
  228. package/src/lib/paint-scope.ts +8 -2
  229. package/src/lib/planar-cursor-placement.test.ts +178 -1
  230. package/src/lib/planar-cursor-placement.ts +61 -5
  231. package/src/lib/plugin-panels.test.ts +92 -1
  232. package/src/lib/plugin-panels.ts +48 -0
  233. package/src/lib/portable-export.test.ts +366 -0
  234. package/src/lib/portable-export.ts +969 -0
  235. package/src/lib/print-3mf.test.ts +86 -0
  236. package/src/lib/print-3mf.ts +161 -0
  237. package/src/lib/print-content-scope.test.ts +107 -0
  238. package/src/lib/print-content-scope.ts +90 -0
  239. package/src/lib/print-export.test.ts +294 -0
  240. package/src/lib/print-export.ts +735 -0
  241. package/src/lib/print-feature-thickness.test.ts +147 -0
  242. package/src/lib/print-feature-thickness.ts +180 -0
  243. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  244. package/src/lib/print-golden-house.test.ts +249 -0
  245. package/src/lib/print-roof-solids.test.ts +133 -0
  246. package/src/lib/print-roof-solids.ts +630 -0
  247. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  248. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  249. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  250. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  251. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  252. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  253. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  254. package/src/lib/print-shell-compiler.ts +273 -0
  255. package/src/lib/print-wall-solids.test.ts +194 -0
  256. package/src/lib/print-wall-solids.ts +328 -0
  257. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  258. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  259. package/src/lib/scene.ts +22 -8
  260. package/src/lib/selection-routing.test.ts +125 -6
  261. package/src/lib/selection-routing.ts +34 -1
  262. package/src/lib/session-groups.test.ts +88 -0
  263. package/src/lib/session-groups.ts +201 -0
  264. package/src/lib/sfx-bus.ts +4 -0
  265. package/src/lib/sfx-player.ts +8 -0
  266. package/src/lib/snapping-mode.test.ts +32 -4
  267. package/src/lib/snapping-mode.ts +11 -7
  268. package/src/lib/structured-clone-fallback.test.ts +45 -0
  269. package/src/lib/structured-clone-fallback.ts +24 -0
  270. package/src/lib/usdz-export.ts +38 -0
  271. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  272. package/src/store/live-draft-preview-stores.test.ts +3 -0
  273. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  274. package/src/store/tool-mode.test.ts +87 -0
  275. package/src/store/use-camera-hint-focus.ts +48 -0
  276. package/src/store/use-editor.tsx +299 -188
  277. package/src/store/use-floorplan-draft-preview.ts +9 -0
  278. package/src/store/use-interaction-scope.test.ts +23 -1
  279. package/src/store/use-interaction-scope.ts +63 -2
  280. package/src/store/use-placement-preview.ts +56 -3
  281. package/src/store/use-session-groups.ts +103 -0
  282. package/src/components/tools/roof/roof-tool.tsx +0 -656
  283. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -0,0 +1,419 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { type AnyNode, RoofSegmentNode, type RoofType, WallNode } from '@pascal-app/core'
3
+ import { generateRoofSegmentGeometry } from '@pascal-app/viewer'
4
+ import * as THREE from 'three'
5
+ import { prepareSceneForExport } from './glb-export'
6
+ import { filterPreparedSceneForPrintContent } from './print-content-scope'
7
+ import { exportSceneToPrintStl } from './print-export'
8
+ import { createPrintGoldenHouseFixture } from './print-golden-house.test-fixture'
9
+ import { compileSemanticPrintShell } from './print-shell-compiler'
10
+ import { compilePrintShellBaseline } from './print-shell-compiler-baseline'
11
+ import { compileManifoldMeshData } from './print-shell-compiler-manifold-core'
12
+ import { compileSemanticPrintShellWithManifold } from './print-shell-compiler-manifold-worker'
13
+
14
+ const ROOF_TYPES: RoofType[] = [
15
+ 'gable',
16
+ 'hip',
17
+ 'shed',
18
+ 'gambrel',
19
+ 'mansard',
20
+ 'flat',
21
+ 'dutch',
22
+ 'conical',
23
+ ]
24
+
25
+ function structuralBox(id: string, x: number): THREE.Group {
26
+ const group = new THREE.Group()
27
+ group.userData = { pascalId: id }
28
+ group.position.x = x
29
+ group.add(new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2)))
30
+ return group
31
+ }
32
+
33
+ function rayIntersectionCount(
34
+ root: THREE.Object3D,
35
+ x: number,
36
+ y: number,
37
+ far = Number.POSITIVE_INFINITY,
38
+ startZ = -2,
39
+ ): number {
40
+ root.updateMatrixWorld(true)
41
+ const raycaster = new THREE.Raycaster(new THREE.Vector3(x, y, startZ), new THREE.Vector3(0, 0, 1))
42
+ raycaster.far = far
43
+ const material = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide })
44
+ let count = 0
45
+
46
+ root.traverse((object) => {
47
+ const mesh = object as THREE.Mesh
48
+ if (!mesh.isMesh) return
49
+ const originalMaterial = mesh.material
50
+ mesh.material = material
51
+ count += raycaster.intersectObject(mesh, false).length
52
+ mesh.material = originalMaterial
53
+ })
54
+
55
+ material.dispose()
56
+ return count
57
+ }
58
+
59
+ function indexedNonManifoldEdgeCount(root: THREE.Object3D): number {
60
+ let count = 0
61
+ root.traverse((object) => {
62
+ const mesh = object as THREE.Mesh
63
+ const index = mesh.isMesh ? mesh.geometry.getIndex() : null
64
+ if (!index) return
65
+ const edges = new Map<string, number>()
66
+ const add = (a: number, b: number) => {
67
+ const key = a < b ? `${a}|${b}` : `${b}|${a}`
68
+ edges.set(key, (edges.get(key) ?? 0) + 1)
69
+ }
70
+ for (let offset = 0; offset + 2 < index.count; offset += 3) {
71
+ const a = index.getX(offset)
72
+ const b = index.getX(offset + 1)
73
+ const c = index.getX(offset + 2)
74
+ add(a, b)
75
+ add(b, c)
76
+ add(c, a)
77
+ }
78
+ count += Array.from(edges.values()).filter((uses) => uses > 2).length
79
+ })
80
+ return count
81
+ }
82
+
83
+ describe('print shell compiler baseline', () => {
84
+ test('unions overlapping world-space structural meshes into a closed shell', () => {
85
+ const source = new THREE.Group()
86
+ source.add(structuralBox('wall_left', -0.5), structuralBox('wall_right', 0.5))
87
+
88
+ const compiled = compilePrintShellBaseline(source)
89
+
90
+ expect(compiled.status).toBe('compiled')
91
+ expect(compiled.inputMeshCount).toBe(2)
92
+ expect(compiled.sourceNodeIds).toEqual(['wall_left', 'wall_right'])
93
+ expect(compiled.scene).not.toBeNull()
94
+
95
+ const print = exportSceneToPrintStl(compiled.scene!, { scale: 100, compiled: true })
96
+ expect(print.report.status).toBe('pass')
97
+ expect(print.report.bounds?.width).toBeCloseTo(30, 4)
98
+ expect(print.report.bounds?.depth).toBeCloseTo(20, 4)
99
+ expect(print.report.bounds?.height).toBeCloseTo(20, 4)
100
+ expect(print.report.boundaryEdgeCount).toBe(0)
101
+ expect(print.report.nonManifoldEdgeCount).toBe(0)
102
+ expect(print.report.volumeMm3).toBeCloseTo(12_000, 1)
103
+ })
104
+
105
+ test('blocks a structural mesh without Pascal provenance', () => {
106
+ const source = new THREE.Group()
107
+ source.add(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1)))
108
+
109
+ const compiled = compilePrintShellBaseline(source)
110
+
111
+ expect(compiled.status).toBe('blocked')
112
+ expect(compiled.scene).toBeNull()
113
+ expect(compiled.diagnostics).toContainEqual(
114
+ expect.objectContaining({ code: 'missing_node_provenance', severity: 'error' }),
115
+ )
116
+ })
117
+
118
+ test('compares the semantic full-house baseline union with the Manifold candidate', async () => {
119
+ const fixture = createPrintGoldenHouseFixture()
120
+
121
+ try {
122
+ const prepared = prepareSceneForExport(fixture.root, fixture.nodes, { onlyVisible: true })
123
+ const structure = filterPreparedSceneForPrintContent(
124
+ prepared.scene,
125
+ fixture.nodes,
126
+ 'structure',
127
+ )
128
+ const compiled = compileSemanticPrintShell(structure, fixture.nodes, { wallSolids: true })
129
+
130
+ expect(compiled.status).toBe('compiled')
131
+ expect(compiled.inputMeshCount).toBeGreaterThan(10)
132
+ expect(compiled.sourceNodeIds).toEqual(fixture.structuralNodeIds)
133
+ expect(compiled.scene).not.toBeNull()
134
+
135
+ const print = exportSceneToPrintStl(compiled.scene!, { scale: 100, compiled: true })
136
+ expect(print.report.status).toBe('blocked')
137
+ expect(print.report.degenerateTriangleCount).toBeGreaterThan(0)
138
+ expect(print.report.boundaryEdgeCount).toBeGreaterThan(0)
139
+ expect(print.report.volumeMm3).toBeGreaterThan(0)
140
+
141
+ const candidate = await compileSemanticPrintShellWithManifold(structure, fixture.nodes, {
142
+ runner: compileManifoldMeshData,
143
+ })
144
+ expect(candidate.diagnostics.filter((diagnostic) => diagnostic.severity === 'error')).toEqual(
145
+ [],
146
+ )
147
+ expect(candidate.backend).toBe('manifold-3d')
148
+ expect(candidate.scene).not.toBeNull()
149
+ expect(indexedNonManifoldEdgeCount(candidate.scene!)).toBe(0)
150
+ expect(rayIntersectionCount(candidate.scene!, 0, 1.05, 0.8)).toBe(0)
151
+ expect(rayIntersectionCount(candidate.scene!, 1.5, 1.05, 0.8)).toBeGreaterThanOrEqual(2)
152
+ expect(rayIntersectionCount(candidate.scene!, 0, 3.9, 1, 1)).toBe(0)
153
+ expect(rayIntersectionCount(candidate.scene!, 1.5, 3.9, 1, 1)).toBeGreaterThanOrEqual(2)
154
+
155
+ const candidatePrint = exportSceneToPrintStl(candidate.scene!, {
156
+ scale: 100,
157
+ compiled: true,
158
+ indexedTopology: true,
159
+ })
160
+ expect(
161
+ candidatePrint.report.diagnostics.filter((diagnostic) => diagnostic.severity === 'error'),
162
+ ).toEqual([])
163
+ expect(candidatePrint.report.degenerateTriangleCount).toBe(0)
164
+ expect(candidatePrint.report.boundaryEdgeCount).toBe(0)
165
+ expect(candidatePrint.report.nonManifoldEdgeCount).toBe(0)
166
+ expect(candidatePrint.report.volumeMm3).toBeGreaterThan(0)
167
+ } finally {
168
+ fixture.dispose()
169
+ }
170
+ }, 15_000)
171
+
172
+ test('blocks a Manifold worker failure without exporting display geometry', async () => {
173
+ const source = structuralBox('wall_worker-failure', 0)
174
+ const compiled = await compileSemanticPrintShellWithManifold(
175
+ source,
176
+ {},
177
+ {
178
+ runner: async () => {
179
+ throw new Error('Worker unavailable')
180
+ },
181
+ },
182
+ )
183
+
184
+ expect(compiled.status).toBe('blocked')
185
+ expect(compiled.scene).toBeNull()
186
+ expect(compiled.sourceNodeIds).toEqual(['wall_worker-failure'])
187
+ expect(compiled.diagnostics).toContainEqual(
188
+ expect.objectContaining({
189
+ code: 'manifold_worker_failed',
190
+ message: 'Worker unavailable',
191
+ severity: 'error',
192
+ }),
193
+ )
194
+ })
195
+
196
+ test('compiles a plane-bound wall independently of its flat 2D display geometry', async () => {
197
+ const levelId = 'level_print-shell-2d'
198
+ const wall = WallNode.parse({
199
+ id: 'wall_print-shell-2d',
200
+ parentId: levelId,
201
+ start: [0, 0],
202
+ end: [4, 0],
203
+ thickness: 0.2,
204
+ })
205
+ const wallRoot = new THREE.Group()
206
+ wallRoot.userData = { pascalId: wall.id }
207
+ const flatDisplay = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.2))
208
+ flatDisplay.rotation.x = -Math.PI / 2
209
+ wallRoot.add(flatDisplay)
210
+ const source = new THREE.Group()
211
+ source.add(wallRoot)
212
+ const nodes = {
213
+ [levelId]: {
214
+ object: 'node',
215
+ id: levelId,
216
+ type: 'level',
217
+ parentId: null,
218
+ children: [wall.id],
219
+ height: 2.5,
220
+ level: 0,
221
+ visible: true,
222
+ } as unknown as AnyNode,
223
+ [wall.id]: wall,
224
+ }
225
+
226
+ const compiled = await compileSemanticPrintShellWithManifold(source, nodes, {
227
+ runner: compileManifoldMeshData,
228
+ })
229
+ expect(compiled.diagnostics.filter((diagnostic) => diagnostic.severity === 'error')).toEqual([])
230
+ expect(compiled.status).toBe('compiled')
231
+ expect(compiled.scene).not.toBeNull()
232
+
233
+ const print = exportSceneToPrintStl(compiled.scene!, {
234
+ scale: 100,
235
+ compiled: true,
236
+ indexedTopology: true,
237
+ })
238
+ expect(print.report.status).toBe('pass')
239
+ expect(print.report.bounds?.height).toBeCloseTo(25, 4)
240
+ expect(print.report.boundaryEdgeCount).toBe(0)
241
+ expect(print.report.nonManifoldEdgeCount).toBe(0)
242
+ })
243
+
244
+ test('blocks unsupported semantic wall forms without falling back to display geometry', () => {
245
+ const wall = WallNode.parse({
246
+ id: 'wall_print-shell-curved',
247
+ start: [0, 0],
248
+ end: [4, 0],
249
+ height: 2.5,
250
+ thickness: 0.2,
251
+ curveOffset: 0.5,
252
+ })
253
+ const wallRoot = new THREE.Group()
254
+ wallRoot.userData = { pascalId: wall.id }
255
+ const displayMesh = new THREE.Mesh(new THREE.BoxGeometry(4, 2.5, 0.2))
256
+ displayMesh.position.set(2, 1.25, 0)
257
+ wallRoot.add(displayMesh)
258
+ const source = new THREE.Group()
259
+ source.add(wallRoot)
260
+
261
+ const compiled = compileSemanticPrintShell(source, { [wall.id]: wall }, { wallSolids: true })
262
+
263
+ expect(compiled.status).toBe('blocked')
264
+ expect(compiled.scene).toBeNull()
265
+ expect(compiled.diagnostics).toContainEqual(
266
+ expect.objectContaining({
267
+ code: 'unsupported_wall_print_curve',
268
+ severity: 'error',
269
+ nodeIds: [wall.id],
270
+ }),
271
+ )
272
+ })
273
+
274
+ test('blocks the generated display gable roof from print export', () => {
275
+ const roof = RoofSegmentNode.parse({
276
+ id: 'rseg_print-shell-fixture',
277
+ roofType: 'gable',
278
+ width: 4,
279
+ depth: 3,
280
+ wallHeight: 0.5,
281
+ pitch: 30,
282
+ wallThickness: 0.15,
283
+ deckThickness: 0.1,
284
+ overhang: 0.3,
285
+ shingleThickness: 0.05,
286
+ })
287
+ const roofRoot = new THREE.Group()
288
+ roofRoot.userData = { pascalId: roof.id }
289
+ roofRoot.add(new THREE.Mesh(generateRoofSegmentGeometry(roof)))
290
+ const source = new THREE.Group()
291
+ source.add(roofRoot)
292
+
293
+ const first = compilePrintShellBaseline(source)
294
+ const second = compilePrintShellBaseline(source)
295
+
296
+ expect(first.status).toBe('compiled')
297
+ expect(first.inputMeshCount).toBe(1)
298
+ expect(first.sourceNodeIds).toEqual([roof.id])
299
+ expect(first.scene).not.toBeNull()
300
+ expect(second.status).toBe('compiled')
301
+ expect(second.scene).not.toBeNull()
302
+
303
+ const firstPrint = exportSceneToPrintStl(first.scene!, { scale: 100 })
304
+ const secondPrint = exportSceneToPrintStl(second.scene!, { scale: 100 })
305
+ expect(firstPrint.report.status).toBe('blocked')
306
+ expect(firstPrint.report.degenerateTriangleCount).toBeGreaterThan(0)
307
+ expect(firstPrint.report.boundaryEdgeCount).toBeGreaterThan(0)
308
+ expect(firstPrint.report.nonManifoldEdgeCount).toBeGreaterThan(0)
309
+ expect(firstPrint.report.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(
310
+ expect.arrayContaining(['degenerate_triangles', 'open_boundaries', 'non_manifold_edges']),
311
+ )
312
+ expect(firstPrint.report.volumeMm3).toBeGreaterThan(0)
313
+ expect(new Uint8Array(firstPrint.buffer)).toEqual(new Uint8Array(secondPrint.buffer))
314
+ })
315
+
316
+ test('blocks unsupported semantic roof cuts without falling back to display geometry', () => {
317
+ const roof = RoofSegmentNode.parse({
318
+ id: 'rseg_print-shell-trimmed',
319
+ roofType: 'gable',
320
+ width: 4,
321
+ depth: 3,
322
+ wallHeight: 0.5,
323
+ pitch: 30,
324
+ wallThickness: 0.15,
325
+ deckThickness: 0.1,
326
+ overhang: 0.3,
327
+ shingleThickness: 0.05,
328
+ trim: { left: 0.25 },
329
+ })
330
+ const roofRoot = new THREE.Group()
331
+ roofRoot.userData = { pascalId: roof.id }
332
+ roofRoot.add(new THREE.Mesh(generateRoofSegmentGeometry(roof)))
333
+ const source = new THREE.Group()
334
+ source.add(roofRoot)
335
+
336
+ const compiled = compileSemanticPrintShell(source, { [roof.id]: roof })
337
+
338
+ expect(compiled.status).toBe('blocked')
339
+ expect(compiled.scene).toBeNull()
340
+ expect(compiled.sourceNodeIds).toEqual([roof.id])
341
+ expect(compiled.diagnostics).toContainEqual(
342
+ expect.objectContaining({
343
+ code: 'unsupported_roof_print_trim',
344
+ severity: 'error',
345
+ nodeIds: [roof.id],
346
+ }),
347
+ )
348
+ })
349
+
350
+ test('compiles canonical roof modules into deterministic manifold print shells', () => {
351
+ for (const roofType of ROOF_TYPES) {
352
+ const roof = RoofSegmentNode.parse({
353
+ id: `rseg_print-shell-${roofType}`,
354
+ roofType,
355
+ width: 4,
356
+ depth: roofType === 'conical' ? 4 : 3,
357
+ wallHeight: 0.5,
358
+ pitch: 30,
359
+ wallThickness: 0.15,
360
+ deckThickness: 0.1,
361
+ overhang: 0.3,
362
+ shingleThickness: 0.05,
363
+ })
364
+ const roofRoot = new THREE.Group()
365
+ roofRoot.userData = { pascalId: roof.id }
366
+ roofRoot.add(new THREE.Mesh(generateRoofSegmentGeometry(roof)))
367
+ const source = new THREE.Group()
368
+ source.add(roofRoot)
369
+
370
+ const compiled = compileSemanticPrintShell(source, { [roof.id]: roof })
371
+ expect(compiled.status).toBe('compiled')
372
+ expect(compiled.inputMeshCount).toBe(1)
373
+ expect(compiled.sourceNodeIds).toEqual([roof.id])
374
+ expect(compiled.scene).not.toBeNull()
375
+
376
+ const print = exportSceneToPrintStl(compiled.scene!, { scale: 100, compiled: true })
377
+ expect(print.report.status).toBe('pass')
378
+ expect(print.report.degenerateTriangleCount).toBe(0)
379
+ expect(print.report.boundaryEdgeCount).toBe(0)
380
+ expect(print.report.nonManifoldEdgeCount).toBe(0)
381
+ expect(print.report.volumeMm3).toBeGreaterThan(0)
382
+ }
383
+
384
+ const roof = RoofSegmentNode.parse({
385
+ id: 'rseg_print-shell-deterministic',
386
+ roofType: 'gable',
387
+ width: 4,
388
+ depth: 3,
389
+ wallHeight: 0.5,
390
+ pitch: 30,
391
+ wallThickness: 0.15,
392
+ deckThickness: 0.1,
393
+ overhang: 0.3,
394
+ shingleThickness: 0.05,
395
+ })
396
+ const firstSource = new THREE.Group()
397
+ const firstRoof = new THREE.Group()
398
+ firstRoof.userData = { pascalId: roof.id }
399
+ firstRoof.add(new THREE.Mesh(generateRoofSegmentGeometry(roof)))
400
+ firstSource.add(firstRoof)
401
+ const secondSource = firstSource.clone(true)
402
+ const firstCompiled = compileSemanticPrintShell(firstSource, { [roof.id]: roof })
403
+ const secondCompiled = compileSemanticPrintShell(secondSource, { [roof.id]: roof })
404
+ const firstPrint = exportSceneToPrintStl(firstCompiled.scene!, {
405
+ scale: 100,
406
+ compiled: true,
407
+ })
408
+ const secondPrint = exportSceneToPrintStl(secondCompiled.scene!, {
409
+ scale: 100,
410
+ compiled: true,
411
+ })
412
+
413
+ expect(firstPrint.report.bounds?.width).toBeCloseTo(46.6962, 3)
414
+ expect(firstPrint.report.bounds?.depth).toBeCloseTo(37.1962, 3)
415
+ expect(firstPrint.report.bounds?.height).toBeCloseTo(15.3923, 3)
416
+ expect(firstPrint.report.volumeMm3).toBeCloseTo(4_066.52, 1)
417
+ expect(new Uint8Array(firstPrint.buffer)).toEqual(new Uint8Array(secondPrint.buffer))
418
+ })
419
+ })
@@ -0,0 +1,225 @@
1
+ import { ADDITION, Brush, csgEvaluator, csgGeometry, prepareBrushForCSG } from '@pascal-app/viewer'
2
+ import * as THREE from 'three'
3
+ import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
4
+
5
+ export type PrintShellCompileDiagnostic = {
6
+ severity: 'error' | 'warning' | 'info'
7
+ code: string
8
+ message: string
9
+ nodeIds: string[]
10
+ }
11
+
12
+ export type PrintShellCompileResult = {
13
+ backend: 'pascal-three-bvh-csg' | 'manifold-3d'
14
+ status: 'compiled' | 'blocked'
15
+ scene: THREE.Object3D | null
16
+ inputMeshCount: number
17
+ sourceNodeIds: string[]
18
+ diagnostics: PrintShellCompileDiagnostic[]
19
+ }
20
+
21
+ export type PrintShellInput = {
22
+ inputMeshCount: number
23
+ sourceNodeIds: Set<string>
24
+ geometries: THREE.BufferGeometry[]
25
+ geometryNodeIds: string[]
26
+ diagnostics: PrintShellCompileDiagnostic[]
27
+ }
28
+
29
+ function nearestPascalId(object: THREE.Object3D): string | null {
30
+ let current: THREE.Object3D | null = object
31
+ while (current) {
32
+ const id = current.userData.pascalId
33
+ if (typeof id === 'string') return id
34
+ current = current.parent
35
+ }
36
+ return null
37
+ }
38
+
39
+ function hasFinitePositions(geometry: THREE.BufferGeometry): boolean {
40
+ const position = geometry.getAttribute('position')
41
+ if (!position || position.count === 0 || position.itemSize !== 3) return false
42
+ for (let index = 0; index < position.count; index += 1) {
43
+ if (
44
+ !Number.isFinite(position.getX(index)) ||
45
+ !Number.isFinite(position.getY(index)) ||
46
+ !Number.isFinite(position.getZ(index))
47
+ ) {
48
+ return false
49
+ }
50
+ }
51
+ return true
52
+ }
53
+
54
+ function worldGeometry(mesh: THREE.Mesh): THREE.BufferGeometry {
55
+ const geometry = mesh.geometry.clone()
56
+ geometry.applyMatrix4(mesh.matrixWorld)
57
+ for (const name of Object.keys(geometry.attributes)) {
58
+ if (name !== 'position') geometry.deleteAttribute(name)
59
+ }
60
+ geometry.morphAttributes = {}
61
+ geometry.clearGroups()
62
+
63
+ const indexed = mergeVertices(geometry, 1e-5)
64
+ geometry.dispose()
65
+ indexed.computeVertexNormals()
66
+ const count = indexed.getIndex()?.count ?? indexed.getAttribute('position').count
67
+ if (count > 0) indexed.addGroup(0, count, 0)
68
+ return indexed
69
+ }
70
+
71
+ export function collectPrintShellInput(source: THREE.Object3D): PrintShellInput {
72
+ source.updateMatrixWorld(true)
73
+
74
+ const diagnostics: PrintShellCompileDiagnostic[] = []
75
+ const sourceNodeIds = new Set<string>()
76
+ const geometries: THREE.BufferGeometry[] = []
77
+ const geometryNodeIds: string[] = []
78
+ let inputMeshCount = 0
79
+
80
+ source.traverse((object) => {
81
+ const mesh = object as THREE.Mesh
82
+ if (!mesh.isMesh) return
83
+
84
+ const position = mesh.geometry?.getAttribute('position')
85
+ if (!position || position.count === 0) return
86
+ inputMeshCount += 1
87
+
88
+ const nodeId = nearestPascalId(mesh)
89
+ if (!nodeId) {
90
+ diagnostics.push({
91
+ severity: 'error',
92
+ code: 'missing_node_provenance',
93
+ message: 'A structural mesh has no Pascal node identity.',
94
+ nodeIds: [],
95
+ })
96
+ return
97
+ }
98
+ sourceNodeIds.add(nodeId)
99
+
100
+ const specialized = mesh as THREE.SkinnedMesh & THREE.InstancedMesh
101
+ if (specialized.isSkinnedMesh || specialized.isInstancedMesh) {
102
+ diagnostics.push({
103
+ severity: 'error',
104
+ code: 'unsupported_dynamic_mesh',
105
+ message: `Node ${nodeId} uses skinned or instanced geometry that the baseline compiler cannot flatten safely.`,
106
+ nodeIds: [nodeId],
107
+ })
108
+ return
109
+ }
110
+ if (!hasFinitePositions(mesh.geometry)) {
111
+ diagnostics.push({
112
+ severity: 'error',
113
+ code: 'invalid_shell_input',
114
+ message: `Node ${nodeId} has empty or non-finite structural geometry.`,
115
+ nodeIds: [nodeId],
116
+ })
117
+ return
118
+ }
119
+
120
+ geometries.push(worldGeometry(mesh))
121
+ geometryNodeIds.push(nodeId)
122
+ })
123
+
124
+ if (inputMeshCount === 0) {
125
+ diagnostics.push({
126
+ severity: 'error',
127
+ code: 'no_shell_meshes',
128
+ message: 'No structural meshes are available for shell compilation.',
129
+ nodeIds: [],
130
+ })
131
+ }
132
+
133
+ return { inputMeshCount, sourceNodeIds, geometries, geometryNodeIds, diagnostics }
134
+ }
135
+
136
+ function blockedResult(
137
+ inputMeshCount: number,
138
+ sourceNodeIds: Set<string>,
139
+ diagnostics: PrintShellCompileDiagnostic[],
140
+ ): PrintShellCompileResult {
141
+ return {
142
+ backend: 'pascal-three-bvh-csg',
143
+ status: 'blocked',
144
+ scene: null,
145
+ inputMeshCount,
146
+ sourceNodeIds: Array.from(sourceNodeIds).sort(),
147
+ diagnostics,
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Synchronous baseline used only by print fixtures while backend correctness
153
+ * is evaluated. It unions world-space static meshes and preserves source node
154
+ * IDs at result level; it does not yet run in a worker or provide face-level
155
+ * provenance.
156
+ */
157
+ export function compilePrintShellBaseline(source: THREE.Object3D): PrintShellCompileResult {
158
+ const { diagnostics, geometries, inputMeshCount, sourceNodeIds } = collectPrintShellInput(source)
159
+ if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
160
+ for (const geometry of geometries) geometry.dispose()
161
+ return blockedResult(inputMeshCount, sourceNodeIds, diagnostics)
162
+ }
163
+
164
+ const material = new THREE.MeshStandardMaterial()
165
+ const ownedGeometries = new Set<THREE.BufferGeometry>(geometries)
166
+ const brushes = geometries.map((geometry) => {
167
+ const brush = new Brush(geometry, material)
168
+ prepareBrushForCSG(brush)
169
+ return brush
170
+ })
171
+
172
+ let current = brushes[0]!
173
+ try {
174
+ for (let index = 1; index < brushes.length; index += 1) {
175
+ const next = csgEvaluator.evaluate(current, brushes[index]!, ADDITION) as Brush
176
+ prepareBrushForCSG(next)
177
+ ownedGeometries.add(next.geometry)
178
+ current = next
179
+ }
180
+
181
+ const geometry = csgGeometry(current).clone()
182
+ geometry.clearGroups()
183
+ const count = geometry.getIndex()?.count ?? geometry.getAttribute('position').count
184
+ if (count > 0) geometry.addGroup(0, count, 0)
185
+ geometry.computeVertexNormals()
186
+
187
+ const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial())
188
+ mesh.name = 'print-shell-baseline'
189
+ mesh.userData = {
190
+ printCompiler: 'pascal-three-bvh-csg',
191
+ sourceNodeIds: Array.from(sourceNodeIds).sort(),
192
+ }
193
+ const scene = new THREE.Group()
194
+ scene.name = 'compiled-print-shell'
195
+ scene.add(mesh)
196
+
197
+ diagnostics.push({
198
+ severity: 'info',
199
+ code: 'baseline_compiler',
200
+ message:
201
+ 'Compiled with the synchronous Pascal Three/CSG baseline; worker scheduling and face-level provenance remain pending.',
202
+ nodeIds: Array.from(sourceNodeIds).sort(),
203
+ })
204
+
205
+ return {
206
+ backend: 'pascal-three-bvh-csg',
207
+ status: 'compiled',
208
+ scene,
209
+ inputMeshCount,
210
+ sourceNodeIds: Array.from(sourceNodeIds).sort(),
211
+ diagnostics,
212
+ }
213
+ } catch (error) {
214
+ diagnostics.push({
215
+ severity: 'error',
216
+ code: 'shell_union_failed',
217
+ message: error instanceof Error ? error.message : 'The baseline shell union failed.',
218
+ nodeIds: Array.from(sourceNodeIds).sort(),
219
+ })
220
+ return blockedResult(inputMeshCount, sourceNodeIds, diagnostics)
221
+ } finally {
222
+ for (const geometry of ownedGeometries) geometry.dispose()
223
+ material.dispose()
224
+ }
225
+ }