@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,526 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
getLevelDisplayName,
|
|
4
|
+
getLevelElevations,
|
|
5
|
+
type LevelNode,
|
|
6
|
+
} from '@pascal-app/core'
|
|
7
|
+
import { disposeObject3DResources } from '@pascal-app/viewer'
|
|
8
|
+
import { type Zippable, zipSync } from 'fflate'
|
|
9
|
+
import * as THREE from 'three'
|
|
10
|
+
import { createPrint3mf, type Print3mfPart } from './print-3mf'
|
|
11
|
+
import {
|
|
12
|
+
encodePreparedPrintSceneToStl,
|
|
13
|
+
extractPreparedPrintMesh,
|
|
14
|
+
mergePrintExportDiagnostics,
|
|
15
|
+
type PrintArtifactFormat,
|
|
16
|
+
type PrintExportBounds,
|
|
17
|
+
type PrintExportDiagnostic,
|
|
18
|
+
type PrintExportReport,
|
|
19
|
+
type PrintMeshData,
|
|
20
|
+
prepareSceneForPrint,
|
|
21
|
+
} from './print-export'
|
|
22
|
+
import {
|
|
23
|
+
applyPrintFeatureThickness,
|
|
24
|
+
applySemanticPrintFeatureThickness,
|
|
25
|
+
isPrintFeatureThicknessDiagnostic,
|
|
26
|
+
} from './print-feature-thickness'
|
|
27
|
+
import { compileSemanticPrintShell } from './print-shell-compiler'
|
|
28
|
+
import type { PrintShellCompileResult } from './print-shell-compiler-baseline'
|
|
29
|
+
|
|
30
|
+
const ZIP_MTIME = new Date(2000, 0, 1, 0, 0, 0)
|
|
31
|
+
const MILLIMETERS_PER_METER = 1000
|
|
32
|
+
const LEVEL_BASE_TOLERANCE_MM = 0.01
|
|
33
|
+
|
|
34
|
+
export type PrintBaseMode = 'none' | 'plinth'
|
|
35
|
+
|
|
36
|
+
export type PrintPlinthOptions = {
|
|
37
|
+
marginMm: number
|
|
38
|
+
thicknessMm: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type PrintLevelPartReport = {
|
|
42
|
+
kind: 'level' | 'plinth'
|
|
43
|
+
levelId: string
|
|
44
|
+
label: string
|
|
45
|
+
objectName: string
|
|
46
|
+
filename: string | null
|
|
47
|
+
sourceBaseMeters: number | null
|
|
48
|
+
report: PrintExportReport
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type PrintLevelBundleReport = {
|
|
52
|
+
kind: 'print-level-export-report'
|
|
53
|
+
version: 2
|
|
54
|
+
format: PrintArtifactFormat
|
|
55
|
+
scale: number
|
|
56
|
+
units: 'millimeter'
|
|
57
|
+
orientation: 'z-up'
|
|
58
|
+
status: 'pass' | 'warning' | 'blocked'
|
|
59
|
+
partCount: number
|
|
60
|
+
parts: PrintLevelPartReport[]
|
|
61
|
+
excludedNodeIds: string[]
|
|
62
|
+
diagnostics: PrintExportDiagnostic[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type PrintLevelPackage = {
|
|
66
|
+
data: Uint8Array<ArrayBuffer>
|
|
67
|
+
report: PrintLevelBundleReport
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type PrintLevelExportOptions = {
|
|
71
|
+
scale: number
|
|
72
|
+
format?: PrintArtifactFormat
|
|
73
|
+
plinth?: PrintPlinthOptions
|
|
74
|
+
minimumFeatureMm?: number
|
|
75
|
+
compileShells?: boolean
|
|
76
|
+
compileShell?: (
|
|
77
|
+
source: THREE.Object3D,
|
|
78
|
+
nodes: Record<string, AnyNode>,
|
|
79
|
+
) => Promise<PrintShellCompileResult>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function exportedIdentityIds(root: THREE.Object3D): Set<string> {
|
|
83
|
+
const ids = new Set<string>()
|
|
84
|
+
root.traverse((object) => {
|
|
85
|
+
const id = object.userData.pascalId
|
|
86
|
+
if (typeof id === 'string') ids.add(id)
|
|
87
|
+
})
|
|
88
|
+
return ids
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function owningLevelId(
|
|
92
|
+
id: string,
|
|
93
|
+
nodes: Record<string, AnyNode>,
|
|
94
|
+
memo: Map<string, string | null>,
|
|
95
|
+
path = new Set<string>(),
|
|
96
|
+
): string | null {
|
|
97
|
+
if (memo.has(id)) return memo.get(id) ?? null
|
|
98
|
+
const node = nodes[id]
|
|
99
|
+
if (!node || path.has(id)) return null
|
|
100
|
+
if (node.type === 'level') {
|
|
101
|
+
memo.set(id, id)
|
|
102
|
+
return id
|
|
103
|
+
}
|
|
104
|
+
if (!node.parentId) {
|
|
105
|
+
memo.set(id, null)
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
path.add(id)
|
|
110
|
+
const levelId = owningLevelId(node.parentId, nodes, memo, path)
|
|
111
|
+
path.delete(id)
|
|
112
|
+
memo.set(id, levelId)
|
|
113
|
+
return levelId
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function levelAncestors(levelId: string, nodes: Record<string, AnyNode>): Set<string> {
|
|
117
|
+
const ancestors = new Set<string>()
|
|
118
|
+
const visited = new Set<string>()
|
|
119
|
+
let parentId = nodes[levelId]?.parentId ?? null
|
|
120
|
+
while (parentId && !visited.has(parentId)) {
|
|
121
|
+
visited.add(parentId)
|
|
122
|
+
ancestors.add(parentId)
|
|
123
|
+
parentId = nodes[parentId]?.parentId ?? null
|
|
124
|
+
}
|
|
125
|
+
return ancestors
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isSpanningNode(node: AnyNode, ownerLevelId: string | null): boolean {
|
|
129
|
+
if (node.type === 'elevator') return true
|
|
130
|
+
if (node.type !== 'stair') return false
|
|
131
|
+
|
|
132
|
+
const fromLevelId = node.fromLevelId ?? ownerLevelId
|
|
133
|
+
const toLevelId = node.toLevelId
|
|
134
|
+
return Boolean(fromLevelId && toLevelId && fromLevelId !== toLevelId)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasExcludedAncestor(
|
|
138
|
+
id: string,
|
|
139
|
+
excludedIds: ReadonlySet<string>,
|
|
140
|
+
nodes: Record<string, AnyNode>,
|
|
141
|
+
): boolean {
|
|
142
|
+
const visited = new Set<string>()
|
|
143
|
+
let parentId = nodes[id]?.parentId ?? null
|
|
144
|
+
while (parentId && !visited.has(parentId)) {
|
|
145
|
+
if (excludedIds.has(parentId)) return true
|
|
146
|
+
visited.add(parentId)
|
|
147
|
+
parentId = nodes[parentId]?.parentId ?? null
|
|
148
|
+
}
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function pruneSceneToLevel(
|
|
153
|
+
source: THREE.Object3D,
|
|
154
|
+
levelId: string,
|
|
155
|
+
nodes: Record<string, AnyNode>,
|
|
156
|
+
excludedIds: ReadonlySet<string>,
|
|
157
|
+
ownerByNodeId: Map<string, string | null>,
|
|
158
|
+
): THREE.Object3D {
|
|
159
|
+
const scene = source.clone(true)
|
|
160
|
+
const ancestors = levelAncestors(levelId, nodes)
|
|
161
|
+
const removals: THREE.Object3D[] = []
|
|
162
|
+
|
|
163
|
+
scene.traverse((object) => {
|
|
164
|
+
const id = object.userData.pascalId
|
|
165
|
+
if (typeof id !== 'string') return
|
|
166
|
+
const belongsToLevel =
|
|
167
|
+
ownerByNodeId.get(id) === levelId &&
|
|
168
|
+
!excludedIds.has(id) &&
|
|
169
|
+
!hasExcludedAncestor(id, excludedIds, nodes)
|
|
170
|
+
if (!belongsToLevel && !ancestors.has(id)) removals.push(object)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
for (const object of removals) object.removeFromParent()
|
|
174
|
+
scene.name = `print-level-${levelId}`
|
|
175
|
+
return scene
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function safeFilenamePart(value: string): string {
|
|
179
|
+
return (
|
|
180
|
+
value
|
|
181
|
+
.normalize('NFKD')
|
|
182
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
183
|
+
.toLowerCase()
|
|
184
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
185
|
+
.replace(/^-+|-+$/g, '') || 'level'
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function bundleStatus(
|
|
190
|
+
diagnostics: PrintExportDiagnostic[],
|
|
191
|
+
parts: PrintLevelPartReport[],
|
|
192
|
+
): PrintLevelBundleReport['status'] {
|
|
193
|
+
if (
|
|
194
|
+
diagnostics.some((diagnostic) => diagnostic.severity === 'error') ||
|
|
195
|
+
parts.some((part) => part.report.status === 'blocked')
|
|
196
|
+
) {
|
|
197
|
+
return 'blocked'
|
|
198
|
+
}
|
|
199
|
+
if (
|
|
200
|
+
diagnostics.some((diagnostic) => diagnostic.severity === 'warning') ||
|
|
201
|
+
parts.some((part) => part.report.status === 'warning')
|
|
202
|
+
) {
|
|
203
|
+
return 'warning'
|
|
204
|
+
}
|
|
205
|
+
return 'pass'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type PreparedLevelArtifact = {
|
|
209
|
+
filename: string | null
|
|
210
|
+
objectName: string
|
|
211
|
+
bytes: Uint8Array<ArrayBuffer> | null
|
|
212
|
+
mesh: PrintMeshData | null
|
|
213
|
+
bounds: PrintExportBounds | null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function levelBaseDiagnostics(
|
|
217
|
+
level: LevelNode,
|
|
218
|
+
label: string,
|
|
219
|
+
sourceBaseMeters: number | null,
|
|
220
|
+
report: PrintExportReport,
|
|
221
|
+
): PrintExportDiagnostic[] {
|
|
222
|
+
if (sourceBaseMeters === null) {
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
severity: 'error',
|
|
226
|
+
code: 'missing_level_base',
|
|
227
|
+
message: `${label} has no finite stored level base and cannot be normalized reliably.`,
|
|
228
|
+
nodeIds: [level.id],
|
|
229
|
+
},
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const minZ = report.bounds?.min.z
|
|
234
|
+
if (minZ === undefined || Math.abs(minZ) <= LEVEL_BASE_TOLERANCE_MM) return []
|
|
235
|
+
if (minZ < 0) {
|
|
236
|
+
return [
|
|
237
|
+
{
|
|
238
|
+
severity: 'error',
|
|
239
|
+
code: 'level_geometry_below_base',
|
|
240
|
+
message: `${label} extends ${Math.abs(minZ).toFixed(3)} mm below its stored level base. Correct the level ownership or supporting slab before printing.`,
|
|
241
|
+
nodeIds: [level.id],
|
|
242
|
+
},
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
severity: 'error',
|
|
249
|
+
code: 'level_geometry_detached_from_base',
|
|
250
|
+
message: `${label} begins ${minZ.toFixed(3)} mm above its stored level base, leaving the printable part detached from the bed. Add or assign a floor solid before printing.`,
|
|
251
|
+
nodeIds: [level.id],
|
|
252
|
+
},
|
|
253
|
+
]
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function exportSceneLevelsForPrint(
|
|
257
|
+
source: THREE.Object3D,
|
|
258
|
+
nodes: Record<string, AnyNode>,
|
|
259
|
+
options: PrintLevelExportOptions,
|
|
260
|
+
): Promise<PrintLevelPackage> {
|
|
261
|
+
const format = options.format ?? 'stl'
|
|
262
|
+
const exportedIds = exportedIdentityIds(source)
|
|
263
|
+
const levelElevations = getLevelElevations(nodes)
|
|
264
|
+
const ownerByNodeId = new Map<string, string | null>()
|
|
265
|
+
for (const id of Object.keys(nodes)) owningLevelId(id, nodes, ownerByNodeId)
|
|
266
|
+
|
|
267
|
+
const levels = Object.values(nodes)
|
|
268
|
+
.filter((node): node is LevelNode => node.type === 'level' && exportedIds.has(node.id))
|
|
269
|
+
.sort(
|
|
270
|
+
(a, b) =>
|
|
271
|
+
(a.parentId ?? '').localeCompare(b.parentId ?? '') ||
|
|
272
|
+
a.level - b.level ||
|
|
273
|
+
a.id.localeCompare(b.id),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
const excludedIds = new Set<string>()
|
|
277
|
+
const diagnostics: PrintExportDiagnostic[] = []
|
|
278
|
+
for (const id of exportedIds) {
|
|
279
|
+
const node = nodes[id]
|
|
280
|
+
if (!node || !isSpanningNode(node, ownerByNodeId.get(id) ?? null)) continue
|
|
281
|
+
excludedIds.add(id)
|
|
282
|
+
diagnostics.push({
|
|
283
|
+
severity: 'error',
|
|
284
|
+
code: 'unsplit_spanning_node',
|
|
285
|
+
message: `${node.type} ${id} spans levels and was omitted. Hide it or define a deterministic split before downloading level parts.`,
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (levels.length === 0) {
|
|
290
|
+
diagnostics.push({
|
|
291
|
+
severity: 'error',
|
|
292
|
+
code: 'no_visible_levels',
|
|
293
|
+
message: 'No visible level nodes remain in the print scope.',
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const levelArtifacts: PreparedLevelArtifact[] = []
|
|
298
|
+
const levelParts: PrintLevelPartReport[] = []
|
|
299
|
+
for (const [index, level] of levels.entries()) {
|
|
300
|
+
const label = getLevelDisplayName(level)
|
|
301
|
+
const prefix = String(index + 1).padStart(2, '0')
|
|
302
|
+
const objectName = `${prefix} ${label}`
|
|
303
|
+
const filename = format === 'stl' ? `${prefix}_${safeFilenamePart(label)}.stl` : null
|
|
304
|
+
const levelScene = pruneSceneToLevel(source, level.id, nodes, excludedIds, ownerByNodeId)
|
|
305
|
+
const sourceBase = levelElevations.get(level.id)?.baseY
|
|
306
|
+
const sourceBaseMeters =
|
|
307
|
+
typeof sourceBase === 'number' && Number.isFinite(sourceBase) ? sourceBase : null
|
|
308
|
+
const compiled = options.compileShells
|
|
309
|
+
? options.compileShell
|
|
310
|
+
? await options.compileShell(levelScene, nodes)
|
|
311
|
+
: compileSemanticPrintShell(levelScene, nodes)
|
|
312
|
+
: null
|
|
313
|
+
try {
|
|
314
|
+
const printSource = compiled ? (compiled.scene ?? new THREE.Group()) : levelScene
|
|
315
|
+
const prepared = prepareSceneForPrint(printSource, {
|
|
316
|
+
scale: options.scale,
|
|
317
|
+
compiled: compiled?.status === 'compiled',
|
|
318
|
+
indexedTopology: compiled?.backend === 'manifold-3d',
|
|
319
|
+
format,
|
|
320
|
+
...(sourceBaseMeters === null ? {} : { sourceBedElevationMeters: sourceBaseMeters }),
|
|
321
|
+
})
|
|
322
|
+
let report = compiled
|
|
323
|
+
? mergePrintExportDiagnostics(
|
|
324
|
+
prepared.report,
|
|
325
|
+
compiled.diagnostics,
|
|
326
|
+
new Set(['compiler_pending']),
|
|
327
|
+
)
|
|
328
|
+
: prepared.report
|
|
329
|
+
if (compiled) {
|
|
330
|
+
report = applySemanticPrintFeatureThickness(
|
|
331
|
+
report,
|
|
332
|
+
nodes,
|
|
333
|
+
compiled.sourceNodeIds,
|
|
334
|
+
options.minimumFeatureMm,
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
const baseDiagnostics = levelBaseDiagnostics(level, label, sourceBaseMeters, report)
|
|
338
|
+
report = mergePrintExportDiagnostics(report, baseDiagnostics)
|
|
339
|
+
if (compiled) {
|
|
340
|
+
diagnostics.push(
|
|
341
|
+
...compiled.diagnostics.filter((diagnostic) => diagnostic.severity !== 'info'),
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
diagnostics.push(...report.diagnostics.filter(isPrintFeatureThicknessDiagnostic))
|
|
345
|
+
diagnostics.push(...baseDiagnostics)
|
|
346
|
+
levelArtifacts.push({
|
|
347
|
+
filename,
|
|
348
|
+
objectName,
|
|
349
|
+
bytes:
|
|
350
|
+
format === 'stl' ? new Uint8Array(encodePreparedPrintSceneToStl(prepared.scene)) : null,
|
|
351
|
+
mesh:
|
|
352
|
+
format === '3mf' && report.bounds && report.invalidTriangleCount === 0
|
|
353
|
+
? extractPreparedPrintMesh(prepared.scene)
|
|
354
|
+
: null,
|
|
355
|
+
bounds: report.bounds,
|
|
356
|
+
})
|
|
357
|
+
levelParts.push({
|
|
358
|
+
kind: 'level',
|
|
359
|
+
levelId: level.id,
|
|
360
|
+
label,
|
|
361
|
+
objectName,
|
|
362
|
+
filename,
|
|
363
|
+
sourceBaseMeters,
|
|
364
|
+
report,
|
|
365
|
+
})
|
|
366
|
+
} finally {
|
|
367
|
+
if (compiled?.scene) disposeObject3DResources(compiled.scene)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let plinthArtifact: PreparedLevelArtifact | null = null
|
|
372
|
+
let plinthPart: PrintLevelPartReport | null = null
|
|
373
|
+
if (options.plinth) {
|
|
374
|
+
const { marginMm, thicknessMm } = options.plinth
|
|
375
|
+
if (
|
|
376
|
+
!Number.isFinite(marginMm) ||
|
|
377
|
+
marginMm < 0 ||
|
|
378
|
+
!Number.isFinite(thicknessMm) ||
|
|
379
|
+
thicknessMm <= 0
|
|
380
|
+
) {
|
|
381
|
+
diagnostics.push({
|
|
382
|
+
severity: 'error',
|
|
383
|
+
code: 'invalid_plinth_dimensions',
|
|
384
|
+
message: 'Plinth margin must be non-negative and thickness must be positive.',
|
|
385
|
+
})
|
|
386
|
+
} else {
|
|
387
|
+
const buildingIds = new Set(levels.map((level) => level.parentId ?? 'unparented-building'))
|
|
388
|
+
const lowestLevel = levels[0]
|
|
389
|
+
const lowestPart = levelParts[0]
|
|
390
|
+
const bounds = lowestPart?.report.bounds
|
|
391
|
+
if (buildingIds.size > 1) {
|
|
392
|
+
diagnostics.push({
|
|
393
|
+
severity: 'error',
|
|
394
|
+
code: 'multiple_building_plinth',
|
|
395
|
+
message: 'A plinth currently requires the print scope to contain exactly one building.',
|
|
396
|
+
})
|
|
397
|
+
} else if (!lowestLevel || !lowestPart || !bounds) {
|
|
398
|
+
diagnostics.push({
|
|
399
|
+
severity: 'error',
|
|
400
|
+
code: 'plinth_missing_footprint',
|
|
401
|
+
message: 'The lowest visible level has no structural bounds for plinth generation.',
|
|
402
|
+
})
|
|
403
|
+
} else {
|
|
404
|
+
const widthMeters = ((bounds.width + marginMm * 2) * options.scale) / MILLIMETERS_PER_METER
|
|
405
|
+
const depthMeters = ((bounds.depth + marginMm * 2) * options.scale) / MILLIMETERS_PER_METER
|
|
406
|
+
const thicknessMeters = (thicknessMm * options.scale) / MILLIMETERS_PER_METER
|
|
407
|
+
const mesh = new THREE.Mesh(
|
|
408
|
+
new THREE.BoxGeometry(widthMeters, thicknessMeters, depthMeters),
|
|
409
|
+
)
|
|
410
|
+
try {
|
|
411
|
+
const prepared = prepareSceneForPrint(mesh, { ...options, format })
|
|
412
|
+
const report = applyPrintFeatureThickness(
|
|
413
|
+
prepared.report,
|
|
414
|
+
{
|
|
415
|
+
features: [{ nodeId: lowestLevel.id, thicknessMm }],
|
|
416
|
+
unmeasuredNodeIds: [],
|
|
417
|
+
},
|
|
418
|
+
options.minimumFeatureMm,
|
|
419
|
+
)
|
|
420
|
+
const filename = format === 'stl' ? '00_plinth.stl' : null
|
|
421
|
+
const objectName = '00 Plinth'
|
|
422
|
+
plinthArtifact = {
|
|
423
|
+
filename,
|
|
424
|
+
objectName,
|
|
425
|
+
bytes:
|
|
426
|
+
format === 'stl'
|
|
427
|
+
? new Uint8Array(encodePreparedPrintSceneToStl(prepared.scene))
|
|
428
|
+
: null,
|
|
429
|
+
mesh:
|
|
430
|
+
format === '3mf' &&
|
|
431
|
+
prepared.report.bounds &&
|
|
432
|
+
prepared.report.invalidTriangleCount === 0
|
|
433
|
+
? extractPreparedPrintMesh(prepared.scene)
|
|
434
|
+
: null,
|
|
435
|
+
bounds: report.bounds,
|
|
436
|
+
}
|
|
437
|
+
plinthPart = {
|
|
438
|
+
kind: 'plinth',
|
|
439
|
+
levelId: lowestLevel.id,
|
|
440
|
+
label: 'Plinth',
|
|
441
|
+
objectName,
|
|
442
|
+
filename,
|
|
443
|
+
sourceBaseMeters: null,
|
|
444
|
+
report,
|
|
445
|
+
}
|
|
446
|
+
diagnostics.push(...report.diagnostics.filter(isPrintFeatureThicknessDiagnostic))
|
|
447
|
+
diagnostics.push({
|
|
448
|
+
severity: 'info',
|
|
449
|
+
code: 'rectangular_plinth_experimental',
|
|
450
|
+
message:
|
|
451
|
+
'The plinth is a separate rectangular part derived from the lowest level bounds; footprint shaping and connectors are not implemented yet.',
|
|
452
|
+
})
|
|
453
|
+
} finally {
|
|
454
|
+
disposeObject3DResources(mesh)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
diagnostics.push({
|
|
461
|
+
severity: 'info',
|
|
462
|
+
code: 'level_parts_experimental',
|
|
463
|
+
message: options.compileShells
|
|
464
|
+
? options.compileShell
|
|
465
|
+
? 'Level parts use stored level bases and worker-backed Manifold semantic shell compilation; known wall, slab, roof, and plinth dimensions are measured, while mesh-observed thin features and self-intersections remain pending.'
|
|
466
|
+
: 'Level parts use stored level bases and the experimental synchronous semantic shell compiler; known wall, slab, roof, and plinth dimensions are measured, while worker execution, mesh-observed thin features, and self-intersections remain pending.'
|
|
467
|
+
: 'Level parts use stored level bases and semantic separation but are not boolean-unioned printable shells yet.',
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
const files: Zippable = {}
|
|
471
|
+
const parts: PrintLevelPartReport[] = []
|
|
472
|
+
const packageParts: Print3mfPart[] = []
|
|
473
|
+
if (plinthArtifact && plinthPart) {
|
|
474
|
+
if (plinthArtifact.filename && plinthArtifact.bytes) {
|
|
475
|
+
files[plinthArtifact.filename] = [plinthArtifact.bytes, { level: 0, mtime: ZIP_MTIME }]
|
|
476
|
+
}
|
|
477
|
+
if (plinthArtifact.mesh && plinthArtifact.bounds) {
|
|
478
|
+
packageParts.push({
|
|
479
|
+
name: plinthArtifact.objectName,
|
|
480
|
+
mesh: plinthArtifact.mesh,
|
|
481
|
+
bounds: plinthArtifact.bounds,
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
parts.push(plinthPart)
|
|
485
|
+
}
|
|
486
|
+
for (const [index, artifact] of levelArtifacts.entries()) {
|
|
487
|
+
if (artifact.filename && artifact.bytes) {
|
|
488
|
+
files[artifact.filename] = [artifact.bytes, { level: 0, mtime: ZIP_MTIME }]
|
|
489
|
+
}
|
|
490
|
+
if (artifact.mesh && artifact.bounds) {
|
|
491
|
+
packageParts.push({
|
|
492
|
+
name: artifact.objectName,
|
|
493
|
+
mesh: artifact.mesh,
|
|
494
|
+
bounds: artifact.bounds,
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
const part = levelParts[index]
|
|
498
|
+
if (part) parts.push(part)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
data:
|
|
503
|
+
format === '3mf'
|
|
504
|
+
? createPrint3mf(packageParts, 'Pascal level parts')
|
|
505
|
+
: zipSync(files, { level: 0 }),
|
|
506
|
+
report: {
|
|
507
|
+
kind: 'print-level-export-report',
|
|
508
|
+
version: 2,
|
|
509
|
+
format,
|
|
510
|
+
scale: options.scale,
|
|
511
|
+
units: 'millimeter',
|
|
512
|
+
orientation: 'z-up',
|
|
513
|
+
status: bundleStatus(diagnostics, parts),
|
|
514
|
+
partCount: parts.length,
|
|
515
|
+
parts,
|
|
516
|
+
excludedNodeIds: Array.from(excludedIds).sort(),
|
|
517
|
+
diagnostics,
|
|
518
|
+
},
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function isPrintLevelBundleReport(value: unknown): value is PrintLevelBundleReport {
|
|
523
|
+
if (!value || typeof value !== 'object') return false
|
|
524
|
+
const report = value as Partial<PrintLevelBundleReport>
|
|
525
|
+
return report.kind === 'print-level-export-report' && report.version === 2
|
|
526
|
+
}
|