@pascal-app/editor 1.0.0-beta.5 → 1.0.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.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +63 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +44 -21
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +103 -10
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +5 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +455 -0
- package/src/lib/portable-export.ts +1042 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +38 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { Manifold as ManifoldSolid, ManifoldToplevel } from 'manifold-3d'
|
|
2
|
+
import type { PrintShellCompileDiagnostic } from './print-shell-compiler-baseline'
|
|
3
|
+
import type {
|
|
4
|
+
ManifoldCompileOutput,
|
|
5
|
+
ManifoldMeshData,
|
|
6
|
+
ManifoldRuntimeOptions,
|
|
7
|
+
} from './print-shell-compiler-protocol'
|
|
8
|
+
|
|
9
|
+
let modulePromise: Promise<ManifoldToplevel> | null = null
|
|
10
|
+
const MANIFOLD_OUTPUT_WELD_EPSILON_METERS = 2e-5
|
|
11
|
+
const COLLINEAR_SEAM_CROSS_LENGTH_SQ = 1e-20
|
|
12
|
+
|
|
13
|
+
type Triangle = [number, number, number]
|
|
14
|
+
|
|
15
|
+
type ManifoldFactory = (config?: {
|
|
16
|
+
locateFile?: (path: string) => string
|
|
17
|
+
}) => Promise<ManifoldToplevel>
|
|
18
|
+
|
|
19
|
+
// manifold-3d's emscripten glue awaits import('node:module') behind a Node
|
|
20
|
+
// check. The branch never executes in a browser, but webpack refuses to
|
|
21
|
+
// *build* a graph that can reach it, so a static specifier here poisons every
|
|
22
|
+
// external bundler that compiles this package's source (#715). The factory is
|
|
23
|
+
// therefore loaded through an import() no bundler can trace: the bare
|
|
24
|
+
// specifier resolves wherever node-style resolution exists at runtime (bun
|
|
25
|
+
// tests, dev servers, bundlers that inline it anyway), and the version-pinned
|
|
26
|
+
// CDN copy covers bundled browser builds that left the specifier unresolved —
|
|
27
|
+
// emscripten then locates manifold.wasm relative to the glue's own URL. Hosts
|
|
28
|
+
// that can't reach the CDN (offline, CSP) pass their own URLs through
|
|
29
|
+
// configureManifoldRuntime.
|
|
30
|
+
const MANIFOLD_VERSION = '3.5.1'
|
|
31
|
+
const FALLBACK_MODULE_URL = `https://cdn.jsdelivr.net/npm/manifold-3d@${MANIFOLD_VERSION}/manifold.js`
|
|
32
|
+
|
|
33
|
+
function importUntraced(specifier: string): Promise<{ default: ManifoldFactory }> {
|
|
34
|
+
return import(/* webpackIgnore: true */ /* @vite-ignore */ specifier)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function loadManifoldFactory(moduleUrl?: string): Promise<ManifoldFactory> {
|
|
38
|
+
const specifiers = moduleUrl ? [moduleUrl] : ['manifold-3d', FALLBACK_MODULE_URL]
|
|
39
|
+
let lastError: unknown
|
|
40
|
+
for (const specifier of specifiers) {
|
|
41
|
+
try {
|
|
42
|
+
return (await importUntraced(specifier)).default
|
|
43
|
+
} catch (error) {
|
|
44
|
+
lastError = error
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw lastError instanceof Error ? lastError : new Error('Failed to load manifold-3d.')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function getManifoldModule(runtime?: ManifoldRuntimeOptions): Promise<ManifoldToplevel> {
|
|
51
|
+
modulePromise ??= loadManifoldFactory(runtime?.moduleUrl)
|
|
52
|
+
.then((factory) => {
|
|
53
|
+
const wasmUrl = runtime?.wasmUrl
|
|
54
|
+
return factory(wasmUrl ? { locateFile: () => wasmUrl } : undefined)
|
|
55
|
+
})
|
|
56
|
+
.then((module) => {
|
|
57
|
+
module.setup()
|
|
58
|
+
return module
|
|
59
|
+
})
|
|
60
|
+
try {
|
|
61
|
+
return await modulePromise
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// A transient load failure (offline, blocked CDN) must not poison every
|
|
64
|
+
// later compile attempt with the cached rejection.
|
|
65
|
+
modulePromise = null
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function manifoldMesh(
|
|
71
|
+
module: ManifoldToplevel,
|
|
72
|
+
mesh: ManifoldMeshData,
|
|
73
|
+
): InstanceType<ManifoldToplevel['Mesh']> {
|
|
74
|
+
return new module.Mesh({
|
|
75
|
+
numProp: 3,
|
|
76
|
+
vertProperties: mesh.positions,
|
|
77
|
+
triVerts: mesh.indices,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function distanceSquared(positions: Float32Array, left: number, right: number): number {
|
|
82
|
+
const dx = positions[left * 3]! - positions[right * 3]!
|
|
83
|
+
const dy = positions[left * 3 + 1]! - positions[right * 3 + 1]!
|
|
84
|
+
const dz = positions[left * 3 + 2]! - positions[right * 3 + 2]!
|
|
85
|
+
return dx * dx + dy * dy + dz * dz
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function triangleCrossLengthSquared(positions: Float32Array, [a, b, c]: Triangle): number {
|
|
89
|
+
const abX = positions[b * 3]! - positions[a * 3]!
|
|
90
|
+
const abY = positions[b * 3 + 1]! - positions[a * 3 + 1]!
|
|
91
|
+
const abZ = positions[b * 3 + 2]! - positions[a * 3 + 2]!
|
|
92
|
+
const acX = positions[c * 3]! - positions[a * 3]!
|
|
93
|
+
const acY = positions[c * 3 + 1]! - positions[a * 3 + 1]!
|
|
94
|
+
const acZ = positions[c * 3 + 2]! - positions[a * 3 + 2]!
|
|
95
|
+
const crossX = abY * acZ - abZ * acY
|
|
96
|
+
const crossY = abZ * acX - abX * acZ
|
|
97
|
+
const crossZ = abX * acY - abY * acX
|
|
98
|
+
return crossX * crossX + crossY * crossY + crossZ * crossZ
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function collinearSeam(
|
|
102
|
+
positions: Float32Array,
|
|
103
|
+
triangle: Triangle,
|
|
104
|
+
): { start: number; middle: number; end: number } | null {
|
|
105
|
+
if (triangleCrossLengthSquared(positions, triangle) > COLLINEAR_SEAM_CROSS_LENGTH_SQ) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const [a, b, c] = triangle
|
|
110
|
+
const edges = [
|
|
111
|
+
{ start: a, middle: c, end: b, lengthSquared: distanceSquared(positions, a, b) },
|
|
112
|
+
{ start: b, middle: a, end: c, lengthSquared: distanceSquared(positions, b, c) },
|
|
113
|
+
{ start: c, middle: b, end: a, lengthSquared: distanceSquared(positions, c, a) },
|
|
114
|
+
].sort((left, right) => right.lengthSquared - left.lengthSquared)
|
|
115
|
+
const longest = edges[0]!
|
|
116
|
+
if (longest.lengthSquared === 0) return null
|
|
117
|
+
|
|
118
|
+
const startOffset = longest.start * 3
|
|
119
|
+
const middleOffset = longest.middle * 3
|
|
120
|
+
const endOffset = longest.end * 3
|
|
121
|
+
const edgeX = positions[endOffset]! - positions[startOffset]!
|
|
122
|
+
const edgeY = positions[endOffset + 1]! - positions[startOffset + 1]!
|
|
123
|
+
const edgeZ = positions[endOffset + 2]! - positions[startOffset + 2]!
|
|
124
|
+
const middleX = positions[middleOffset]! - positions[startOffset]!
|
|
125
|
+
const middleY = positions[middleOffset + 1]! - positions[startOffset + 1]!
|
|
126
|
+
const middleZ = positions[middleOffset + 2]! - positions[startOffset + 2]!
|
|
127
|
+
const projection = middleX * edgeX + middleY * edgeY + middleZ * edgeZ
|
|
128
|
+
if (projection <= 0 || projection >= longest.lengthSquared) return null
|
|
129
|
+
|
|
130
|
+
return longest
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function stitchCollinearSeams(positions: Float32Array, input: Triangle[]): Triangle[] {
|
|
134
|
+
const triangles: Array<Triangle | null> = [...input]
|
|
135
|
+
|
|
136
|
+
// Manifold can encode a T-junction as one zero-area triangle: one surface owns the long
|
|
137
|
+
// edge while the other owns its two segments. Split the neighboring face at the middle
|
|
138
|
+
// vertex before removing the collapsed face so indexed edge incidence remains closed.
|
|
139
|
+
for (let triangleIndex = 0; triangleIndex < triangles.length; triangleIndex += 1) {
|
|
140
|
+
const triangle = triangles[triangleIndex]
|
|
141
|
+
if (!triangle) continue
|
|
142
|
+
const seam = collinearSeam(positions, triangle)
|
|
143
|
+
if (!seam) continue
|
|
144
|
+
|
|
145
|
+
const matches: Array<{ index: number; edgeOffset: number }> = []
|
|
146
|
+
for (let candidateIndex = 0; candidateIndex < triangles.length; candidateIndex += 1) {
|
|
147
|
+
if (candidateIndex === triangleIndex) continue
|
|
148
|
+
const candidate = triangles[candidateIndex]
|
|
149
|
+
if (
|
|
150
|
+
!candidate ||
|
|
151
|
+
triangleCrossLengthSquared(positions, candidate) <= COLLINEAR_SEAM_CROSS_LENGTH_SQ
|
|
152
|
+
) {
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
for (let edgeOffset = 0; edgeOffset < 3; edgeOffset += 1) {
|
|
156
|
+
const from = candidate[edgeOffset]!
|
|
157
|
+
const to = candidate[(edgeOffset + 1) % 3]!
|
|
158
|
+
if ((from === seam.start && to === seam.end) || (from === seam.end && to === seam.start)) {
|
|
159
|
+
matches.push({ index: candidateIndex, edgeOffset })
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (matches.length !== 1) continue
|
|
164
|
+
|
|
165
|
+
const match = matches[0]!
|
|
166
|
+
const neighbor = triangles[match.index]!
|
|
167
|
+
const from = neighbor[match.edgeOffset]!
|
|
168
|
+
const to = neighbor[(match.edgeOffset + 1) % 3]!
|
|
169
|
+
const opposite = neighbor[(match.edgeOffset + 2) % 3]!
|
|
170
|
+
triangles[match.index] = [from, seam.middle, opposite]
|
|
171
|
+
triangles.push([seam.middle, to, opposite])
|
|
172
|
+
triangles[triangleIndex] = null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return triangles.filter((triangle): triangle is Triangle => triangle !== null)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function manifoldOutput(solid: ManifoldSolid): { positions: Float32Array; indices: Uint32Array } {
|
|
179
|
+
const mesh = solid.getMesh()
|
|
180
|
+
const positions = new Float32Array(mesh.numVert * 3)
|
|
181
|
+
for (let index = 0; index < mesh.numVert; index += 1) {
|
|
182
|
+
const sourceOffset = index * mesh.numProp
|
|
183
|
+
positions[index * 3] = mesh.vertProperties[sourceOffset]!
|
|
184
|
+
positions[index * 3 + 1] = mesh.vertProperties[sourceOffset + 1]!
|
|
185
|
+
positions[index * 3 + 2] = mesh.vertProperties[sourceOffset + 2]!
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const parents = new Uint32Array(mesh.numVert)
|
|
189
|
+
for (let index = 0; index < parents.length; index += 1) parents[index] = index
|
|
190
|
+
const find = (index: number): number => {
|
|
191
|
+
let root = index
|
|
192
|
+
while (parents[root] !== root) root = parents[root]!
|
|
193
|
+
while (parents[index] !== index) {
|
|
194
|
+
const next = parents[index]!
|
|
195
|
+
parents[index] = root
|
|
196
|
+
index = next
|
|
197
|
+
}
|
|
198
|
+
return root
|
|
199
|
+
}
|
|
200
|
+
for (let index = 0; index < mesh.mergeFromVert.length; index += 1) {
|
|
201
|
+
parents[find(mesh.mergeFromVert[index]!)] = find(mesh.mergeToVert[index]!)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Float32 boolean output can leave seam vertices just over 10 microns apart. Dropping the
|
|
205
|
+
// resulting sliver triangle by area opens the shell; weld the vertices first so adjacent
|
|
206
|
+
// faces inherit one indexed edge, then remove only triangles collapsed by that topology.
|
|
207
|
+
const cellSize = MANIFOLD_OUTPUT_WELD_EPSILON_METERS
|
|
208
|
+
const cellRoots = new Map<string, number[]>()
|
|
209
|
+
const cellCoordinate = (value: number) => Math.floor(value / cellSize)
|
|
210
|
+
const cellKey = (x: number, y: number, z: number) => `${x},${y},${z}`
|
|
211
|
+
const weldDistanceSquared = cellSize * cellSize
|
|
212
|
+
for (let index = 0; index < mesh.numVert; index += 1) {
|
|
213
|
+
const root = find(index)
|
|
214
|
+
if (root !== index) continue
|
|
215
|
+
const cellX = cellCoordinate(positions[root * 3]!)
|
|
216
|
+
const cellY = cellCoordinate(positions[root * 3 + 1]!)
|
|
217
|
+
const cellZ = cellCoordinate(positions[root * 3 + 2]!)
|
|
218
|
+
let weldedTo: number | null = null
|
|
219
|
+
for (let xOffset = -1; xOffset <= 1 && weldedTo === null; xOffset += 1) {
|
|
220
|
+
for (let yOffset = -1; yOffset <= 1 && weldedTo === null; yOffset += 1) {
|
|
221
|
+
for (let zOffset = -1; zOffset <= 1 && weldedTo === null; zOffset += 1) {
|
|
222
|
+
const candidates = cellRoots.get(
|
|
223
|
+
cellKey(cellX + xOffset, cellY + yOffset, cellZ + zOffset),
|
|
224
|
+
)
|
|
225
|
+
for (const candidate of candidates ?? []) {
|
|
226
|
+
if (distanceSquared(positions, root, candidate) <= weldDistanceSquared) {
|
|
227
|
+
weldedTo = candidate
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (weldedTo === null) {
|
|
235
|
+
const key = cellKey(cellX, cellY, cellZ)
|
|
236
|
+
const roots = cellRoots.get(key) ?? []
|
|
237
|
+
roots.push(root)
|
|
238
|
+
cellRoots.set(key, roots)
|
|
239
|
+
} else {
|
|
240
|
+
parents[root] = find(weldedTo)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const triangles: Triangle[] = []
|
|
245
|
+
for (let index = 0; index + 2 < mesh.triVerts.length; index += 3) {
|
|
246
|
+
const a = find(mesh.triVerts[index]!)
|
|
247
|
+
const b = find(mesh.triVerts[index + 1]!)
|
|
248
|
+
const c = find(mesh.triVerts[index + 2]!)
|
|
249
|
+
if (a === b || b === c || c === a) continue
|
|
250
|
+
triangles.push([a, b, c])
|
|
251
|
+
}
|
|
252
|
+
const indices = stitchCollinearSeams(positions, triangles).flat()
|
|
253
|
+
|
|
254
|
+
return { positions, indices: new Uint32Array(indices) }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function elapsed(startedAt: number): number {
|
|
258
|
+
return Math.max(0, performance.now() - startedAt)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function compileManifoldMeshData(
|
|
262
|
+
meshes: ManifoldMeshData[],
|
|
263
|
+
runtime?: ManifoldRuntimeOptions,
|
|
264
|
+
): Promise<ManifoldCompileOutput> {
|
|
265
|
+
const startedAt = performance.now()
|
|
266
|
+
const sourceNodeIds = Array.from(new Set(meshes.map((mesh) => mesh.nodeId))).sort()
|
|
267
|
+
const diagnostics: PrintShellCompileDiagnostic[] = []
|
|
268
|
+
const solids: ManifoldSolid[] = []
|
|
269
|
+
let union: ManifoldSolid | null = null
|
|
270
|
+
let result: ManifoldSolid | null = null
|
|
271
|
+
|
|
272
|
+
if (meshes.length === 0) {
|
|
273
|
+
return {
|
|
274
|
+
status: 'blocked',
|
|
275
|
+
positions: null,
|
|
276
|
+
indices: null,
|
|
277
|
+
diagnostics: [
|
|
278
|
+
{
|
|
279
|
+
severity: 'error',
|
|
280
|
+
code: 'no_shell_meshes',
|
|
281
|
+
message: 'No structural meshes are available for Manifold compilation.',
|
|
282
|
+
nodeIds: [],
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
durationMs: elapsed(startedAt),
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const module = await getManifoldModule(runtime)
|
|
291
|
+
for (const mesh of meshes) {
|
|
292
|
+
try {
|
|
293
|
+
solids.push(new module.Manifold(manifoldMesh(module, mesh)))
|
|
294
|
+
} catch (error) {
|
|
295
|
+
diagnostics.push({
|
|
296
|
+
severity: 'error',
|
|
297
|
+
code: 'manifold_input_failed',
|
|
298
|
+
message: `Node ${mesh.nodeId}: ${
|
|
299
|
+
error instanceof Error ? error.message : 'Manifold rejected the shell input.'
|
|
300
|
+
}`,
|
|
301
|
+
nodeIds: [mesh.nodeId],
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (diagnostics.length > 0) {
|
|
306
|
+
return {
|
|
307
|
+
status: 'blocked',
|
|
308
|
+
positions: null,
|
|
309
|
+
indices: null,
|
|
310
|
+
diagnostics,
|
|
311
|
+
durationMs: elapsed(startedAt),
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
union = module.Manifold.union(solids)
|
|
316
|
+
result = union.asOriginal()
|
|
317
|
+
const status = result.status()
|
|
318
|
+
if (status !== 'NoError') {
|
|
319
|
+
return {
|
|
320
|
+
status: 'blocked',
|
|
321
|
+
positions: null,
|
|
322
|
+
indices: null,
|
|
323
|
+
diagnostics: [
|
|
324
|
+
{
|
|
325
|
+
severity: 'error',
|
|
326
|
+
code: 'manifold_union_failed',
|
|
327
|
+
message: `Manifold union failed with ${status}.`,
|
|
328
|
+
nodeIds: sourceNodeIds,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
durationMs: elapsed(startedAt),
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const output = manifoldOutput(result)
|
|
336
|
+
if (output.indices.length === 0) {
|
|
337
|
+
return {
|
|
338
|
+
status: 'blocked',
|
|
339
|
+
positions: null,
|
|
340
|
+
indices: null,
|
|
341
|
+
diagnostics: [
|
|
342
|
+
{
|
|
343
|
+
severity: 'error',
|
|
344
|
+
code: 'manifold_union_failed',
|
|
345
|
+
message: 'Manifold produced no printable triangles.',
|
|
346
|
+
nodeIds: sourceNodeIds,
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
durationMs: elapsed(startedAt),
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
status: 'compiled',
|
|
354
|
+
positions: output.positions,
|
|
355
|
+
indices: output.indices,
|
|
356
|
+
diagnostics: [],
|
|
357
|
+
durationMs: elapsed(startedAt),
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
return {
|
|
361
|
+
status: 'blocked',
|
|
362
|
+
positions: null,
|
|
363
|
+
indices: null,
|
|
364
|
+
diagnostics: [
|
|
365
|
+
{
|
|
366
|
+
severity: 'error',
|
|
367
|
+
code: 'manifold_worker_failed',
|
|
368
|
+
message: error instanceof Error ? error.message : 'Manifold compilation failed.',
|
|
369
|
+
nodeIds: sourceNodeIds,
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
durationMs: elapsed(startedAt),
|
|
373
|
+
}
|
|
374
|
+
} finally {
|
|
375
|
+
for (const solid of solids) solid.delete()
|
|
376
|
+
union?.delete()
|
|
377
|
+
result?.delete()
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
ManifoldRuntimeOptions,
|
|
20
|
+
ManifoldWorkerRequest,
|
|
21
|
+
ManifoldWorkerResponse,
|
|
22
|
+
} from './print-shell-compiler-protocol'
|
|
23
|
+
|
|
24
|
+
const WORKER_TIMEOUT_MS = 60_000
|
|
25
|
+
|
|
26
|
+
let manifoldRuntime: ManifoldRuntimeOptions | undefined
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Overrides where the print-export worker loads the manifold-3d module and
|
|
30
|
+
* wasm from. See the loader in print-shell-compiler-manifold-core.ts for the
|
|
31
|
+
* default resolution order; hosts with restrictive networks should call this
|
|
32
|
+
* with self-hosted asset URLs before the first print export.
|
|
33
|
+
*/
|
|
34
|
+
export function configureManifoldRuntime(options: ManifoldRuntimeOptions | undefined): void {
|
|
35
|
+
manifoldRuntime = options
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ManifoldCompileRunner = (meshes: ManifoldMeshData[]) => Promise<ManifoldCompileOutput>
|
|
39
|
+
|
|
40
|
+
export type SemanticManifoldCompileOptions = SemanticPrintCompileOptions & {
|
|
41
|
+
runner?: ManifoldCompileRunner
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type PendingRequest = {
|
|
45
|
+
resolve: (output: ManifoldCompileOutput) => void
|
|
46
|
+
reject: (error: Error) => void
|
|
47
|
+
timeout: ReturnType<typeof setTimeout>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let worker: Worker | null = null
|
|
51
|
+
let nextRequestId = 1
|
|
52
|
+
const pendingRequests = new Map<number, PendingRequest>()
|
|
53
|
+
|
|
54
|
+
function resetWorker(error: Error) {
|
|
55
|
+
worker?.terminate()
|
|
56
|
+
worker = null
|
|
57
|
+
for (const pending of pendingRequests.values()) {
|
|
58
|
+
clearTimeout(pending.timeout)
|
|
59
|
+
pending.reject(error)
|
|
60
|
+
}
|
|
61
|
+
pendingRequests.clear()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getWorker(): Worker {
|
|
65
|
+
if (worker) return worker
|
|
66
|
+
if (typeof Worker === 'undefined') {
|
|
67
|
+
throw new Error('Web Workers are unavailable in this environment.')
|
|
68
|
+
}
|
|
69
|
+
worker = new Worker(new URL('./print-shell-compiler-manifold.worker.ts', import.meta.url), {
|
|
70
|
+
type: 'module',
|
|
71
|
+
})
|
|
72
|
+
worker.addEventListener('message', (event: MessageEvent<ManifoldWorkerResponse>) => {
|
|
73
|
+
const pending = pendingRequests.get(event.data.id)
|
|
74
|
+
if (!pending) return
|
|
75
|
+
pendingRequests.delete(event.data.id)
|
|
76
|
+
clearTimeout(pending.timeout)
|
|
77
|
+
pending.resolve(event.data)
|
|
78
|
+
})
|
|
79
|
+
worker.addEventListener('error', (event) => {
|
|
80
|
+
resetWorker(new Error(event.message || 'The Manifold worker failed.'))
|
|
81
|
+
})
|
|
82
|
+
worker.addEventListener('messageerror', () => {
|
|
83
|
+
resetWorker(new Error('The Manifold worker returned an unreadable response.'))
|
|
84
|
+
})
|
|
85
|
+
return worker
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const runManifoldWorker: ManifoldCompileRunner = (meshes) => {
|
|
89
|
+
const activeWorker = getWorker()
|
|
90
|
+
const id = nextRequestId
|
|
91
|
+
nextRequestId += 1
|
|
92
|
+
const request: ManifoldWorkerRequest = { id, meshes, runtime: manifoldRuntime }
|
|
93
|
+
const transfer = meshes.flatMap((mesh) => [
|
|
94
|
+
mesh.positions.buffer as ArrayBuffer,
|
|
95
|
+
mesh.indices.buffer as ArrayBuffer,
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
resetWorker(new Error(`The Manifold worker exceeded ${WORKER_TIMEOUT_MS / 1000} seconds.`))
|
|
101
|
+
}, WORKER_TIMEOUT_MS)
|
|
102
|
+
pendingRequests.set(id, { resolve, reject, timeout })
|
|
103
|
+
try {
|
|
104
|
+
activeWorker.postMessage(request, transfer)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
resetWorker(
|
|
107
|
+
error instanceof Error ? error : new Error('Failed to start the Manifold worker.'),
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function blockedResult(
|
|
114
|
+
inputMeshCount: number,
|
|
115
|
+
sourceNodeIds: Iterable<string>,
|
|
116
|
+
diagnostics: PrintShellCompileDiagnostic[],
|
|
117
|
+
): PrintShellCompileResult {
|
|
118
|
+
return {
|
|
119
|
+
backend: 'manifold-3d',
|
|
120
|
+
status: 'blocked',
|
|
121
|
+
scene: null,
|
|
122
|
+
inputMeshCount,
|
|
123
|
+
sourceNodeIds: Array.from(sourceNodeIds).sort(),
|
|
124
|
+
diagnostics,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function compileSemanticPrintShellWithManifold(
|
|
129
|
+
source: THREE.Object3D,
|
|
130
|
+
nodes: Record<string, AnyNode>,
|
|
131
|
+
options: SemanticManifoldCompileOptions = {},
|
|
132
|
+
): Promise<PrintShellCompileResult> {
|
|
133
|
+
const { runner = runManifoldWorker, ...semanticOptions } = options
|
|
134
|
+
const prepared = prepareSemanticPrintShellSource(source, nodes, {
|
|
135
|
+
...semanticOptions,
|
|
136
|
+
wallSolids: semanticOptions.wallSolids ?? true,
|
|
137
|
+
})
|
|
138
|
+
if (prepared.status === 'blocked') {
|
|
139
|
+
return blockedResult(prepared.inputMeshCount, prepared.sourceNodeIds, prepared.diagnostics)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const input = collectPrintShellInput(prepared.scene)
|
|
143
|
+
prepared.dispose()
|
|
144
|
+
if (input.diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
|
145
|
+
for (const geometry of input.geometries) geometry.dispose()
|
|
146
|
+
return blockedResult(input.inputMeshCount, input.sourceNodeIds, input.diagnostics)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const meshes = input.geometries.map((geometry, index) =>
|
|
150
|
+
geometryToManifoldMeshData(geometry, input.geometryNodeIds[index]!),
|
|
151
|
+
)
|
|
152
|
+
for (const geometry of input.geometries) geometry.dispose()
|
|
153
|
+
|
|
154
|
+
let output: ManifoldCompileOutput
|
|
155
|
+
try {
|
|
156
|
+
output = await runner(meshes)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return blockedResult(input.inputMeshCount, input.sourceNodeIds, [
|
|
159
|
+
...input.diagnostics,
|
|
160
|
+
{
|
|
161
|
+
severity: 'error',
|
|
162
|
+
code: 'manifold_worker_failed',
|
|
163
|
+
message: error instanceof Error ? error.message : 'The Manifold worker failed.',
|
|
164
|
+
nodeIds: Array.from(input.sourceNodeIds).sort(),
|
|
165
|
+
},
|
|
166
|
+
])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const diagnostics = [...input.diagnostics, ...output.diagnostics]
|
|
170
|
+
if (output.status === 'blocked') {
|
|
171
|
+
return blockedResult(input.inputMeshCount, input.sourceNodeIds, diagnostics)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const geometry = geometryFromManifoldMeshData(output.positions, output.indices)
|
|
175
|
+
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial())
|
|
176
|
+
mesh.name = 'print-shell-manifold'
|
|
177
|
+
mesh.userData = {
|
|
178
|
+
printCompiler: 'manifold-3d',
|
|
179
|
+
sourceNodeIds: Array.from(input.sourceNodeIds).sort(),
|
|
180
|
+
}
|
|
181
|
+
const scene = new THREE.Group()
|
|
182
|
+
scene.name = 'compiled-print-shell'
|
|
183
|
+
scene.add(mesh)
|
|
184
|
+
diagnostics.push({
|
|
185
|
+
severity: 'info',
|
|
186
|
+
code: runner === runManifoldWorker ? 'manifold_worker_compiler' : 'manifold_compiler_candidate',
|
|
187
|
+
message: `Compiled with Manifold in ${output.durationMs.toFixed(1)} ms${
|
|
188
|
+
runner === runManifoldWorker ? ' off the main thread' : ' through the in-process test runner'
|
|
189
|
+
}.`,
|
|
190
|
+
nodeIds: Array.from(input.sourceNodeIds).sort(),
|
|
191
|
+
})
|
|
192
|
+
return {
|
|
193
|
+
backend: 'manifold-3d',
|
|
194
|
+
status: 'compiled',
|
|
195
|
+
scene,
|
|
196
|
+
inputMeshCount: input.inputMeshCount,
|
|
197
|
+
sourceNodeIds: Array.from(input.sourceNodeIds).sort(),
|
|
198
|
+
diagnostics,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -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, event.data.runtime)
|
|
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,38 @@
|
|
|
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 ManifoldRuntimeOptions = {
|
|
26
|
+
/** URL of the manifold-3d emscripten glue module. Defaults to the pinned CDN copy. */
|
|
27
|
+
moduleUrl?: string
|
|
28
|
+
/** URL of manifold.wasm. Defaults to resolving relative to the glue module. */
|
|
29
|
+
wasmUrl?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ManifoldWorkerRequest = {
|
|
33
|
+
id: number
|
|
34
|
+
meshes: ManifoldMeshData[]
|
|
35
|
+
runtime?: ManifoldRuntimeOptions
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ManifoldWorkerResponse = ManifoldCompileOutput & { id: number }
|