@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,330 @@
1
+ import ManifoldModule, { type Manifold as ManifoldSolid, type ManifoldToplevel } from 'manifold-3d'
2
+ import type { PrintShellCompileDiagnostic } from './print-shell-compiler-baseline'
3
+ import type { ManifoldCompileOutput, ManifoldMeshData } from './print-shell-compiler-protocol'
4
+
5
+ let modulePromise: Promise<ManifoldToplevel> | null = null
6
+ const MANIFOLD_OUTPUT_WELD_EPSILON_METERS = 2e-5
7
+ const COLLINEAR_SEAM_CROSS_LENGTH_SQ = 1e-20
8
+
9
+ type Triangle = [number, number, number]
10
+
11
+ async function getManifoldModule(wasmUrl?: string): Promise<ManifoldToplevel> {
12
+ modulePromise ??= ManifoldModule(wasmUrl ? { locateFile: () => wasmUrl } : undefined).then(
13
+ (module) => {
14
+ module.setup()
15
+ return module
16
+ },
17
+ )
18
+ return modulePromise
19
+ }
20
+
21
+ function manifoldMesh(
22
+ module: ManifoldToplevel,
23
+ mesh: ManifoldMeshData,
24
+ ): InstanceType<ManifoldToplevel['Mesh']> {
25
+ return new module.Mesh({
26
+ numProp: 3,
27
+ vertProperties: mesh.positions,
28
+ triVerts: mesh.indices,
29
+ })
30
+ }
31
+
32
+ function distanceSquared(positions: Float32Array, left: number, right: number): number {
33
+ const dx = positions[left * 3]! - positions[right * 3]!
34
+ const dy = positions[left * 3 + 1]! - positions[right * 3 + 1]!
35
+ const dz = positions[left * 3 + 2]! - positions[right * 3 + 2]!
36
+ return dx * dx + dy * dy + dz * dz
37
+ }
38
+
39
+ function triangleCrossLengthSquared(positions: Float32Array, [a, b, c]: Triangle): number {
40
+ const abX = positions[b * 3]! - positions[a * 3]!
41
+ const abY = positions[b * 3 + 1]! - positions[a * 3 + 1]!
42
+ const abZ = positions[b * 3 + 2]! - positions[a * 3 + 2]!
43
+ const acX = positions[c * 3]! - positions[a * 3]!
44
+ const acY = positions[c * 3 + 1]! - positions[a * 3 + 1]!
45
+ const acZ = positions[c * 3 + 2]! - positions[a * 3 + 2]!
46
+ const crossX = abY * acZ - abZ * acY
47
+ const crossY = abZ * acX - abX * acZ
48
+ const crossZ = abX * acY - abY * acX
49
+ return crossX * crossX + crossY * crossY + crossZ * crossZ
50
+ }
51
+
52
+ function collinearSeam(
53
+ positions: Float32Array,
54
+ triangle: Triangle,
55
+ ): { start: number; middle: number; end: number } | null {
56
+ if (triangleCrossLengthSquared(positions, triangle) > COLLINEAR_SEAM_CROSS_LENGTH_SQ) {
57
+ return null
58
+ }
59
+
60
+ const [a, b, c] = triangle
61
+ const edges = [
62
+ { start: a, middle: c, end: b, lengthSquared: distanceSquared(positions, a, b) },
63
+ { start: b, middle: a, end: c, lengthSquared: distanceSquared(positions, b, c) },
64
+ { start: c, middle: b, end: a, lengthSquared: distanceSquared(positions, c, a) },
65
+ ].sort((left, right) => right.lengthSquared - left.lengthSquared)
66
+ const longest = edges[0]!
67
+ if (longest.lengthSquared === 0) return null
68
+
69
+ const startOffset = longest.start * 3
70
+ const middleOffset = longest.middle * 3
71
+ const endOffset = longest.end * 3
72
+ const edgeX = positions[endOffset]! - positions[startOffset]!
73
+ const edgeY = positions[endOffset + 1]! - positions[startOffset + 1]!
74
+ const edgeZ = positions[endOffset + 2]! - positions[startOffset + 2]!
75
+ const middleX = positions[middleOffset]! - positions[startOffset]!
76
+ const middleY = positions[middleOffset + 1]! - positions[startOffset + 1]!
77
+ const middleZ = positions[middleOffset + 2]! - positions[startOffset + 2]!
78
+ const projection = middleX * edgeX + middleY * edgeY + middleZ * edgeZ
79
+ if (projection <= 0 || projection >= longest.lengthSquared) return null
80
+
81
+ return longest
82
+ }
83
+
84
+ function stitchCollinearSeams(positions: Float32Array, input: Triangle[]): Triangle[] {
85
+ const triangles: Array<Triangle | null> = [...input]
86
+
87
+ // Manifold can encode a T-junction as one zero-area triangle: one surface owns the long
88
+ // edge while the other owns its two segments. Split the neighboring face at the middle
89
+ // vertex before removing the collapsed face so indexed edge incidence remains closed.
90
+ for (let triangleIndex = 0; triangleIndex < triangles.length; triangleIndex += 1) {
91
+ const triangle = triangles[triangleIndex]
92
+ if (!triangle) continue
93
+ const seam = collinearSeam(positions, triangle)
94
+ if (!seam) continue
95
+
96
+ const matches: Array<{ index: number; edgeOffset: number }> = []
97
+ for (let candidateIndex = 0; candidateIndex < triangles.length; candidateIndex += 1) {
98
+ if (candidateIndex === triangleIndex) continue
99
+ const candidate = triangles[candidateIndex]
100
+ if (
101
+ !candidate ||
102
+ triangleCrossLengthSquared(positions, candidate) <= COLLINEAR_SEAM_CROSS_LENGTH_SQ
103
+ ) {
104
+ continue
105
+ }
106
+ for (let edgeOffset = 0; edgeOffset < 3; edgeOffset += 1) {
107
+ const from = candidate[edgeOffset]!
108
+ const to = candidate[(edgeOffset + 1) % 3]!
109
+ if ((from === seam.start && to === seam.end) || (from === seam.end && to === seam.start)) {
110
+ matches.push({ index: candidateIndex, edgeOffset })
111
+ }
112
+ }
113
+ }
114
+ if (matches.length !== 1) continue
115
+
116
+ const match = matches[0]!
117
+ const neighbor = triangles[match.index]!
118
+ const from = neighbor[match.edgeOffset]!
119
+ const to = neighbor[(match.edgeOffset + 1) % 3]!
120
+ const opposite = neighbor[(match.edgeOffset + 2) % 3]!
121
+ triangles[match.index] = [from, seam.middle, opposite]
122
+ triangles.push([seam.middle, to, opposite])
123
+ triangles[triangleIndex] = null
124
+ }
125
+
126
+ return triangles.filter((triangle): triangle is Triangle => triangle !== null)
127
+ }
128
+
129
+ function manifoldOutput(solid: ManifoldSolid): { positions: Float32Array; indices: Uint32Array } {
130
+ const mesh = solid.getMesh()
131
+ const positions = new Float32Array(mesh.numVert * 3)
132
+ for (let index = 0; index < mesh.numVert; index += 1) {
133
+ const sourceOffset = index * mesh.numProp
134
+ positions[index * 3] = mesh.vertProperties[sourceOffset]!
135
+ positions[index * 3 + 1] = mesh.vertProperties[sourceOffset + 1]!
136
+ positions[index * 3 + 2] = mesh.vertProperties[sourceOffset + 2]!
137
+ }
138
+
139
+ const parents = new Uint32Array(mesh.numVert)
140
+ for (let index = 0; index < parents.length; index += 1) parents[index] = index
141
+ const find = (index: number): number => {
142
+ let root = index
143
+ while (parents[root] !== root) root = parents[root]!
144
+ while (parents[index] !== index) {
145
+ const next = parents[index]!
146
+ parents[index] = root
147
+ index = next
148
+ }
149
+ return root
150
+ }
151
+ for (let index = 0; index < mesh.mergeFromVert.length; index += 1) {
152
+ parents[find(mesh.mergeFromVert[index]!)] = find(mesh.mergeToVert[index]!)
153
+ }
154
+
155
+ // Float32 boolean output can leave seam vertices just over 10 microns apart. Dropping the
156
+ // resulting sliver triangle by area opens the shell; weld the vertices first so adjacent
157
+ // faces inherit one indexed edge, then remove only triangles collapsed by that topology.
158
+ const cellSize = MANIFOLD_OUTPUT_WELD_EPSILON_METERS
159
+ const cellRoots = new Map<string, number[]>()
160
+ const cellCoordinate = (value: number) => Math.floor(value / cellSize)
161
+ const cellKey = (x: number, y: number, z: number) => `${x},${y},${z}`
162
+ const weldDistanceSquared = cellSize * cellSize
163
+ for (let index = 0; index < mesh.numVert; index += 1) {
164
+ const root = find(index)
165
+ if (root !== index) continue
166
+ const cellX = cellCoordinate(positions[root * 3]!)
167
+ const cellY = cellCoordinate(positions[root * 3 + 1]!)
168
+ const cellZ = cellCoordinate(positions[root * 3 + 2]!)
169
+ let weldedTo: number | null = null
170
+ for (let xOffset = -1; xOffset <= 1 && weldedTo === null; xOffset += 1) {
171
+ for (let yOffset = -1; yOffset <= 1 && weldedTo === null; yOffset += 1) {
172
+ for (let zOffset = -1; zOffset <= 1 && weldedTo === null; zOffset += 1) {
173
+ const candidates = cellRoots.get(
174
+ cellKey(cellX + xOffset, cellY + yOffset, cellZ + zOffset),
175
+ )
176
+ for (const candidate of candidates ?? []) {
177
+ if (distanceSquared(positions, root, candidate) <= weldDistanceSquared) {
178
+ weldedTo = candidate
179
+ break
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ if (weldedTo === null) {
186
+ const key = cellKey(cellX, cellY, cellZ)
187
+ const roots = cellRoots.get(key) ?? []
188
+ roots.push(root)
189
+ cellRoots.set(key, roots)
190
+ } else {
191
+ parents[root] = find(weldedTo)
192
+ }
193
+ }
194
+
195
+ const triangles: Triangle[] = []
196
+ for (let index = 0; index + 2 < mesh.triVerts.length; index += 3) {
197
+ const a = find(mesh.triVerts[index]!)
198
+ const b = find(mesh.triVerts[index + 1]!)
199
+ const c = find(mesh.triVerts[index + 2]!)
200
+ if (a === b || b === c || c === a) continue
201
+ triangles.push([a, b, c])
202
+ }
203
+ const indices = stitchCollinearSeams(positions, triangles).flat()
204
+
205
+ return { positions, indices: new Uint32Array(indices) }
206
+ }
207
+
208
+ function elapsed(startedAt: number): number {
209
+ return Math.max(0, performance.now() - startedAt)
210
+ }
211
+
212
+ export async function compileManifoldMeshData(
213
+ meshes: ManifoldMeshData[],
214
+ wasmUrl?: string,
215
+ ): Promise<ManifoldCompileOutput> {
216
+ const startedAt = performance.now()
217
+ const sourceNodeIds = Array.from(new Set(meshes.map((mesh) => mesh.nodeId))).sort()
218
+ const diagnostics: PrintShellCompileDiagnostic[] = []
219
+ const solids: ManifoldSolid[] = []
220
+ let union: ManifoldSolid | null = null
221
+ let result: ManifoldSolid | null = null
222
+
223
+ if (meshes.length === 0) {
224
+ return {
225
+ status: 'blocked',
226
+ positions: null,
227
+ indices: null,
228
+ diagnostics: [
229
+ {
230
+ severity: 'error',
231
+ code: 'no_shell_meshes',
232
+ message: 'No structural meshes are available for Manifold compilation.',
233
+ nodeIds: [],
234
+ },
235
+ ],
236
+ durationMs: elapsed(startedAt),
237
+ }
238
+ }
239
+
240
+ try {
241
+ const module = await getManifoldModule(wasmUrl)
242
+ for (const mesh of meshes) {
243
+ try {
244
+ solids.push(new module.Manifold(manifoldMesh(module, mesh)))
245
+ } catch (error) {
246
+ diagnostics.push({
247
+ severity: 'error',
248
+ code: 'manifold_input_failed',
249
+ message: `Node ${mesh.nodeId}: ${
250
+ error instanceof Error ? error.message : 'Manifold rejected the shell input.'
251
+ }`,
252
+ nodeIds: [mesh.nodeId],
253
+ })
254
+ }
255
+ }
256
+ if (diagnostics.length > 0) {
257
+ return {
258
+ status: 'blocked',
259
+ positions: null,
260
+ indices: null,
261
+ diagnostics,
262
+ durationMs: elapsed(startedAt),
263
+ }
264
+ }
265
+
266
+ union = module.Manifold.union(solids)
267
+ result = union.asOriginal()
268
+ const status = result.status()
269
+ if (status !== 'NoError') {
270
+ return {
271
+ status: 'blocked',
272
+ positions: null,
273
+ indices: null,
274
+ diagnostics: [
275
+ {
276
+ severity: 'error',
277
+ code: 'manifold_union_failed',
278
+ message: `Manifold union failed with ${status}.`,
279
+ nodeIds: sourceNodeIds,
280
+ },
281
+ ],
282
+ durationMs: elapsed(startedAt),
283
+ }
284
+ }
285
+
286
+ const output = manifoldOutput(result)
287
+ if (output.indices.length === 0) {
288
+ return {
289
+ status: 'blocked',
290
+ positions: null,
291
+ indices: null,
292
+ diagnostics: [
293
+ {
294
+ severity: 'error',
295
+ code: 'manifold_union_failed',
296
+ message: 'Manifold produced no printable triangles.',
297
+ nodeIds: sourceNodeIds,
298
+ },
299
+ ],
300
+ durationMs: elapsed(startedAt),
301
+ }
302
+ }
303
+ return {
304
+ status: 'compiled',
305
+ positions: output.positions,
306
+ indices: output.indices,
307
+ diagnostics: [],
308
+ durationMs: elapsed(startedAt),
309
+ }
310
+ } catch (error) {
311
+ return {
312
+ status: 'blocked',
313
+ positions: null,
314
+ indices: null,
315
+ diagnostics: [
316
+ {
317
+ severity: 'error',
318
+ code: 'manifold_worker_failed',
319
+ message: error instanceof Error ? error.message : 'Manifold compilation failed.',
320
+ nodeIds: sourceNodeIds,
321
+ },
322
+ ],
323
+ durationMs: elapsed(startedAt),
324
+ }
325
+ } finally {
326
+ for (const solid of solids) solid.delete()
327
+ union?.delete()
328
+ result?.delete()
329
+ }
330
+ }
@@ -0,0 +1,187 @@
1
+ import type { AnyNode } from '@pascal-app/core'
2
+ import * as THREE from 'three'
3
+ import {
4
+ prepareSemanticPrintShellSource,
5
+ type SemanticPrintCompileOptions,
6
+ } from './print-shell-compiler'
7
+ import {
8
+ collectPrintShellInput,
9
+ type PrintShellCompileDiagnostic,
10
+ type PrintShellCompileResult,
11
+ } from './print-shell-compiler-baseline'
12
+ import {
13
+ geometryFromManifoldMeshData,
14
+ geometryToManifoldMeshData,
15
+ } from './print-shell-compiler-mesh-data'
16
+ import type {
17
+ ManifoldCompileOutput,
18
+ ManifoldMeshData,
19
+ ManifoldWorkerRequest,
20
+ ManifoldWorkerResponse,
21
+ } from './print-shell-compiler-protocol'
22
+
23
+ const WORKER_TIMEOUT_MS = 60_000
24
+
25
+ export type ManifoldCompileRunner = (meshes: ManifoldMeshData[]) => Promise<ManifoldCompileOutput>
26
+
27
+ export type SemanticManifoldCompileOptions = SemanticPrintCompileOptions & {
28
+ runner?: ManifoldCompileRunner
29
+ }
30
+
31
+ type PendingRequest = {
32
+ resolve: (output: ManifoldCompileOutput) => void
33
+ reject: (error: Error) => void
34
+ timeout: ReturnType<typeof setTimeout>
35
+ }
36
+
37
+ let worker: Worker | null = null
38
+ let nextRequestId = 1
39
+ const pendingRequests = new Map<number, PendingRequest>()
40
+
41
+ function resetWorker(error: Error) {
42
+ worker?.terminate()
43
+ worker = null
44
+ for (const pending of pendingRequests.values()) {
45
+ clearTimeout(pending.timeout)
46
+ pending.reject(error)
47
+ }
48
+ pendingRequests.clear()
49
+ }
50
+
51
+ function getWorker(): Worker {
52
+ if (worker) return worker
53
+ if (typeof Worker === 'undefined') {
54
+ throw new Error('Web Workers are unavailable in this environment.')
55
+ }
56
+ worker = new Worker(new URL('./print-shell-compiler-manifold.worker.ts', import.meta.url), {
57
+ type: 'module',
58
+ })
59
+ worker.addEventListener('message', (event: MessageEvent<ManifoldWorkerResponse>) => {
60
+ const pending = pendingRequests.get(event.data.id)
61
+ if (!pending) return
62
+ pendingRequests.delete(event.data.id)
63
+ clearTimeout(pending.timeout)
64
+ pending.resolve(event.data)
65
+ })
66
+ worker.addEventListener('error', (event) => {
67
+ resetWorker(new Error(event.message || 'The Manifold worker failed.'))
68
+ })
69
+ worker.addEventListener('messageerror', () => {
70
+ resetWorker(new Error('The Manifold worker returned an unreadable response.'))
71
+ })
72
+ return worker
73
+ }
74
+
75
+ export const runManifoldWorker: ManifoldCompileRunner = (meshes) => {
76
+ const activeWorker = getWorker()
77
+ const id = nextRequestId
78
+ nextRequestId += 1
79
+ const request: ManifoldWorkerRequest = { id, meshes }
80
+ const transfer = meshes.flatMap((mesh) => [
81
+ mesh.positions.buffer as ArrayBuffer,
82
+ mesh.indices.buffer as ArrayBuffer,
83
+ ])
84
+
85
+ return new Promise((resolve, reject) => {
86
+ const timeout = setTimeout(() => {
87
+ resetWorker(new Error(`The Manifold worker exceeded ${WORKER_TIMEOUT_MS / 1000} seconds.`))
88
+ }, WORKER_TIMEOUT_MS)
89
+ pendingRequests.set(id, { resolve, reject, timeout })
90
+ try {
91
+ activeWorker.postMessage(request, transfer)
92
+ } catch (error) {
93
+ resetWorker(
94
+ error instanceof Error ? error : new Error('Failed to start the Manifold worker.'),
95
+ )
96
+ }
97
+ })
98
+ }
99
+
100
+ function blockedResult(
101
+ inputMeshCount: number,
102
+ sourceNodeIds: Iterable<string>,
103
+ diagnostics: PrintShellCompileDiagnostic[],
104
+ ): PrintShellCompileResult {
105
+ return {
106
+ backend: 'manifold-3d',
107
+ status: 'blocked',
108
+ scene: null,
109
+ inputMeshCount,
110
+ sourceNodeIds: Array.from(sourceNodeIds).sort(),
111
+ diagnostics,
112
+ }
113
+ }
114
+
115
+ export async function compileSemanticPrintShellWithManifold(
116
+ source: THREE.Object3D,
117
+ nodes: Record<string, AnyNode>,
118
+ options: SemanticManifoldCompileOptions = {},
119
+ ): Promise<PrintShellCompileResult> {
120
+ const { runner = runManifoldWorker, ...semanticOptions } = options
121
+ const prepared = prepareSemanticPrintShellSource(source, nodes, {
122
+ ...semanticOptions,
123
+ wallSolids: semanticOptions.wallSolids ?? true,
124
+ })
125
+ if (prepared.status === 'blocked') {
126
+ return blockedResult(prepared.inputMeshCount, prepared.sourceNodeIds, prepared.diagnostics)
127
+ }
128
+
129
+ const input = collectPrintShellInput(prepared.scene)
130
+ prepared.dispose()
131
+ if (input.diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
132
+ for (const geometry of input.geometries) geometry.dispose()
133
+ return blockedResult(input.inputMeshCount, input.sourceNodeIds, input.diagnostics)
134
+ }
135
+
136
+ const meshes = input.geometries.map((geometry, index) =>
137
+ geometryToManifoldMeshData(geometry, input.geometryNodeIds[index]!),
138
+ )
139
+ for (const geometry of input.geometries) geometry.dispose()
140
+
141
+ let output: ManifoldCompileOutput
142
+ try {
143
+ output = await runner(meshes)
144
+ } catch (error) {
145
+ return blockedResult(input.inputMeshCount, input.sourceNodeIds, [
146
+ ...input.diagnostics,
147
+ {
148
+ severity: 'error',
149
+ code: 'manifold_worker_failed',
150
+ message: error instanceof Error ? error.message : 'The Manifold worker failed.',
151
+ nodeIds: Array.from(input.sourceNodeIds).sort(),
152
+ },
153
+ ])
154
+ }
155
+
156
+ const diagnostics = [...input.diagnostics, ...output.diagnostics]
157
+ if (output.status === 'blocked') {
158
+ return blockedResult(input.inputMeshCount, input.sourceNodeIds, diagnostics)
159
+ }
160
+
161
+ const geometry = geometryFromManifoldMeshData(output.positions, output.indices)
162
+ const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial())
163
+ mesh.name = 'print-shell-manifold'
164
+ mesh.userData = {
165
+ printCompiler: 'manifold-3d',
166
+ sourceNodeIds: Array.from(input.sourceNodeIds).sort(),
167
+ }
168
+ const scene = new THREE.Group()
169
+ scene.name = 'compiled-print-shell'
170
+ scene.add(mesh)
171
+ diagnostics.push({
172
+ severity: 'info',
173
+ code: runner === runManifoldWorker ? 'manifold_worker_compiler' : 'manifold_compiler_candidate',
174
+ message: `Compiled with Manifold in ${output.durationMs.toFixed(1)} ms${
175
+ runner === runManifoldWorker ? ' off the main thread' : ' through the in-process test runner'
176
+ }.`,
177
+ nodeIds: Array.from(input.sourceNodeIds).sort(),
178
+ })
179
+ return {
180
+ backend: 'manifold-3d',
181
+ status: 'compiled',
182
+ scene,
183
+ inputMeshCount: input.inputMeshCount,
184
+ sourceNodeIds: Array.from(input.sourceNodeIds).sort(),
185
+ diagnostics,
186
+ }
187
+ }
@@ -0,0 +1,20 @@
1
+ import { compileManifoldMeshData } from './print-shell-compiler-manifold-core'
2
+ import type { ManifoldWorkerRequest, ManifoldWorkerResponse } from './print-shell-compiler-protocol'
3
+
4
+ const workerScope = self as unknown as {
5
+ addEventListener: (
6
+ type: 'message',
7
+ listener: (event: MessageEvent<ManifoldWorkerRequest>) => void,
8
+ ) => void
9
+ postMessage: (response: ManifoldWorkerResponse, transfer: Transferable[]) => void
10
+ }
11
+
12
+ workerScope.addEventListener('message', async (event) => {
13
+ const output = await compileManifoldMeshData(event.data.meshes)
14
+ const response: ManifoldWorkerResponse = { id: event.data.id, ...output }
15
+ const transfer: Transferable[] = []
16
+ if (response.status === 'compiled') {
17
+ transfer.push(response.positions.buffer as ArrayBuffer, response.indices.buffer as ArrayBuffer)
18
+ }
19
+ workerScope.postMessage(response, transfer)
20
+ })
@@ -0,0 +1,32 @@
1
+ import * as THREE from 'three'
2
+ import type { ManifoldMeshData } from './print-shell-compiler-protocol'
3
+
4
+ export function geometryToManifoldMeshData(
5
+ geometry: THREE.BufferGeometry,
6
+ nodeId: string,
7
+ ): ManifoldMeshData {
8
+ const position = geometry.getAttribute('position')
9
+ const positions = new Float32Array(position.count * 3)
10
+ for (let index = 0; index < position.count; index += 1) {
11
+ positions[index * 3] = position.getX(index)
12
+ positions[index * 3 + 1] = position.getY(index)
13
+ positions[index * 3 + 2] = position.getZ(index)
14
+ }
15
+ const geometryIndex = geometry.getIndex()
16
+ const indices = new Uint32Array(geometryIndex?.count ?? position.count)
17
+ for (let index = 0; index < indices.length; index += 1) {
18
+ indices[index] = geometryIndex?.getX(index) ?? index
19
+ }
20
+ return { nodeId, positions, indices }
21
+ }
22
+
23
+ export function geometryFromManifoldMeshData(
24
+ positions: Float32Array,
25
+ indices: Uint32Array,
26
+ ): THREE.BufferGeometry {
27
+ const geometry = new THREE.BufferGeometry()
28
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
29
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1))
30
+ geometry.computeVertexNormals()
31
+ return geometry
32
+ }
@@ -0,0 +1,30 @@
1
+ import type { PrintShellCompileDiagnostic } from './print-shell-compiler-baseline'
2
+
3
+ export type ManifoldMeshData = {
4
+ nodeId: string
5
+ positions: Float32Array
6
+ indices: Uint32Array
7
+ }
8
+
9
+ export type ManifoldCompileOutput =
10
+ | {
11
+ status: 'compiled'
12
+ positions: Float32Array
13
+ indices: Uint32Array
14
+ diagnostics: PrintShellCompileDiagnostic[]
15
+ durationMs: number
16
+ }
17
+ | {
18
+ status: 'blocked'
19
+ positions: null
20
+ indices: null
21
+ diagnostics: PrintShellCompileDiagnostic[]
22
+ durationMs: number
23
+ }
24
+
25
+ export type ManifoldWorkerRequest = {
26
+ id: number
27
+ meshes: ManifoldMeshData[]
28
+ }
29
+
30
+ export type ManifoldWorkerResponse = ManifoldCompileOutput & { id: number }