@pascal-app/editor 1.0.0-beta.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- 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 +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- 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/linear-resize-drag.ts +67 -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 +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -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 +284 -237
- 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-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 +180 -32
- 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/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/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 +135 -2
- 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 +6 -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 +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- 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 +45 -8
- 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/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 +9 -12
- 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/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- 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 +22 -5
- 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 +263 -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 +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- 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 +390 -36
- 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 +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- 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/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- 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 +122 -0
- 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 +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -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/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 +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- 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 +1 -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/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- 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-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- 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/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 +366 -0
- package/src/lib/portable-export.ts +969 -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 +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -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 +30 -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.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -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/usdz-export.ts +38 -0
- 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-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/components/tools/roof/roof-tool.tsx +0 -656
- 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
|
+
}
|