@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,630 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
getConicalRoofCoverage,
|
|
4
|
+
getRoofModuleFaces,
|
|
5
|
+
getRoofShapeInsets,
|
|
6
|
+
getRoofShapeRatios,
|
|
7
|
+
getSegmentSlopeFrame,
|
|
8
|
+
nodeRegistry,
|
|
9
|
+
normalizeRoofSegmentTrim,
|
|
10
|
+
type RoofSegmentNode,
|
|
11
|
+
type RoofShapeFaceVertex,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import * as THREE from 'three'
|
|
14
|
+
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
15
|
+
|
|
16
|
+
// Manufacturing geometry belongs to the editor layer, not the read-only viewer runtime.
|
|
17
|
+
const FACE_EPSILON = 1e-7
|
|
18
|
+
const SURFACE_NORMAL_EPSILON = 1e-6
|
|
19
|
+
|
|
20
|
+
type RoofFace = RoofShapeFaceVertex[]
|
|
21
|
+
|
|
22
|
+
export type PrintRoofSolidDiagnostic = {
|
|
23
|
+
severity: 'error'
|
|
24
|
+
code:
|
|
25
|
+
| 'invalid_roof_print_dimensions'
|
|
26
|
+
| 'roof_print_topology_mismatch'
|
|
27
|
+
| 'unsupported_roof_print_trim'
|
|
28
|
+
| 'unsupported_roof_print_cut'
|
|
29
|
+
message: string
|
|
30
|
+
nodeIds: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type PrintRoofSolidResult =
|
|
34
|
+
| { status: 'ready'; object: THREE.Group; diagnostics: [] }
|
|
35
|
+
| { status: 'blocked'; object: null; diagnostics: PrintRoofSolidDiagnostic[] }
|
|
36
|
+
|
|
37
|
+
type BoundaryEdge = {
|
|
38
|
+
start: RoofShapeFaceVertex
|
|
39
|
+
end: RoofShapeFaceVertex
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type RoofModuleGeometryResult =
|
|
43
|
+
| { status: 'ready'; geometry: THREE.BufferGeometry }
|
|
44
|
+
| { status: 'blocked'; message: string }
|
|
45
|
+
|
|
46
|
+
function faceNormal(face: RoofFace): THREE.Vector3 | null {
|
|
47
|
+
if (face.length < 3) return null
|
|
48
|
+
const origin = new THREE.Vector3(face[0]!.x, face[0]!.y, face[0]!.z)
|
|
49
|
+
const first = new THREE.Vector3()
|
|
50
|
+
const second = new THREE.Vector3()
|
|
51
|
+
const normal = new THREE.Vector3()
|
|
52
|
+
|
|
53
|
+
for (let firstIndex = 1; firstIndex < face.length - 1; firstIndex += 1) {
|
|
54
|
+
first.set(face[firstIndex]!.x, face[firstIndex]!.y, face[firstIndex]!.z).sub(origin)
|
|
55
|
+
for (let secondIndex = firstIndex + 1; secondIndex < face.length; secondIndex += 1) {
|
|
56
|
+
second.set(face[secondIndex]!.x, face[secondIndex]!.y, face[secondIndex]!.z).sub(origin)
|
|
57
|
+
normal.crossVectors(first, second)
|
|
58
|
+
if (normal.lengthSq() > FACE_EPSILON * FACE_EPSILON) return normal.normalize()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function pointKey(point: RoofShapeFaceVertex): string {
|
|
66
|
+
return `${Math.round(point.x / FACE_EPSILON)},${Math.round(
|
|
67
|
+
point.y / FACE_EPSILON,
|
|
68
|
+
)},${Math.round(point.z / FACE_EPSILON)}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function edgeKey(a: RoofShapeFaceVertex, b: RoofShapeFaceVertex): string {
|
|
72
|
+
const keyA = pointKey(a)
|
|
73
|
+
const keyB = pointKey(b)
|
|
74
|
+
return keyA < keyB ? `${keyA}|${keyB}` : `${keyB}|${keyA}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function projectFace(face: RoofFace, normal: THREE.Vector3): THREE.Vector2[] {
|
|
78
|
+
const absX = Math.abs(normal.x)
|
|
79
|
+
const absY = Math.abs(normal.y)
|
|
80
|
+
const absZ = Math.abs(normal.z)
|
|
81
|
+
|
|
82
|
+
if (absX >= absY && absX >= absZ) {
|
|
83
|
+
return face.map((point) => new THREE.Vector2(point.z, point.y))
|
|
84
|
+
}
|
|
85
|
+
if (absY >= absX && absY >= absZ) {
|
|
86
|
+
return face.map((point) => new THREE.Vector2(point.x, point.z))
|
|
87
|
+
}
|
|
88
|
+
return face.map((point) => new THREE.Vector2(point.x, point.y))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function geometryFromFaces(faces: RoofFace[]): THREE.BufferGeometry | null {
|
|
92
|
+
const positions: number[] = []
|
|
93
|
+
const a = new THREE.Vector3()
|
|
94
|
+
const b = new THREE.Vector3()
|
|
95
|
+
const c = new THREE.Vector3()
|
|
96
|
+
const triangleSecondEdge = new THREE.Vector3()
|
|
97
|
+
const triangleNormal = new THREE.Vector3()
|
|
98
|
+
|
|
99
|
+
for (const face of faces) {
|
|
100
|
+
const normal = faceNormal(face)
|
|
101
|
+
if (!normal) return null
|
|
102
|
+
const triangles = THREE.ShapeUtils.triangulateShape(projectFace(face, normal), [])
|
|
103
|
+
if (triangles.length === 0) return null
|
|
104
|
+
|
|
105
|
+
for (const triangle of triangles) {
|
|
106
|
+
const [indexA, indexB, indexC] = triangle
|
|
107
|
+
if (indexA === undefined || indexB === undefined || indexC === undefined) return null
|
|
108
|
+
const pointA = face[indexA]!
|
|
109
|
+
let pointB = face[indexB]!
|
|
110
|
+
let pointC = face[indexC]!
|
|
111
|
+
a.set(pointA.x, pointA.y, pointA.z)
|
|
112
|
+
b.set(pointB.x, pointB.y, pointB.z)
|
|
113
|
+
c.set(pointC.x, pointC.y, pointC.z)
|
|
114
|
+
triangleNormal.subVectors(b, a).cross(triangleSecondEdge.subVectors(c, a))
|
|
115
|
+
if (triangleNormal.dot(normal) < 0) {
|
|
116
|
+
;[pointB, pointC] = [pointC, pointB]
|
|
117
|
+
}
|
|
118
|
+
positions.push(
|
|
119
|
+
pointA.x,
|
|
120
|
+
pointA.y,
|
|
121
|
+
pointA.z,
|
|
122
|
+
pointB.x,
|
|
123
|
+
pointB.y,
|
|
124
|
+
pointB.z,
|
|
125
|
+
pointC.x,
|
|
126
|
+
pointC.y,
|
|
127
|
+
pointC.z,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const geometry = new THREE.BufferGeometry()
|
|
133
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
134
|
+
const indexed = mergeVertices(geometry, FACE_EPSILON)
|
|
135
|
+
geometry.dispose()
|
|
136
|
+
indexed.computeVertexNormals()
|
|
137
|
+
return indexed
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function selectSurfaceFaces(
|
|
141
|
+
faces: RoofFace[],
|
|
142
|
+
include: (normal: THREE.Vector3) => boolean,
|
|
143
|
+
reverse = false,
|
|
144
|
+
): RoofFace[] | null {
|
|
145
|
+
const selected: RoofFace[] = []
|
|
146
|
+
for (const face of faces) {
|
|
147
|
+
const normal = faceNormal(face)
|
|
148
|
+
if (!normal) return null
|
|
149
|
+
if (include(normal)) selected.push(reverse ? [...face].reverse() : face)
|
|
150
|
+
}
|
|
151
|
+
return selected.length > 0 ? selected : null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function boundaryEdges(faces: RoofFace[]): BoundaryEdge[] | null {
|
|
155
|
+
const edgeUses = new Map<string, BoundaryEdge[]>()
|
|
156
|
+
for (const face of faces) {
|
|
157
|
+
for (let edgeIndex = 0; edgeIndex < face.length; edgeIndex += 1) {
|
|
158
|
+
const nextIndex = (edgeIndex + 1) % face.length
|
|
159
|
+
const edge = { start: face[edgeIndex]!, end: face[nextIndex]! }
|
|
160
|
+
const key = edgeKey(edge.start, edge.end)
|
|
161
|
+
const uses = edgeUses.get(key) ?? []
|
|
162
|
+
uses.push(edge)
|
|
163
|
+
edgeUses.set(key, uses)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const boundary: BoundaryEdge[] = []
|
|
168
|
+
for (const uses of edgeUses.values()) {
|
|
169
|
+
if (uses.length === 1) boundary.push(uses[0]!)
|
|
170
|
+
else if (uses.length !== 2) return null
|
|
171
|
+
}
|
|
172
|
+
return boundary
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function pointDistanceSq(a: RoofShapeFaceVertex, b: RoofShapeFaceVertex): number {
|
|
176
|
+
const dx = a.x - b.x
|
|
177
|
+
const dy = a.y - b.y
|
|
178
|
+
const dz = a.z - b.z
|
|
179
|
+
return dx * dx + dy * dy + dz * dz
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function boundaryLoops(edges: BoundaryEdge[]): RoofFace[] | null {
|
|
183
|
+
const byStart = new Map<string, BoundaryEdge>()
|
|
184
|
+
for (const edge of edges) {
|
|
185
|
+
const key = pointKey(edge.start)
|
|
186
|
+
if (byStart.has(key)) return null
|
|
187
|
+
byStart.set(key, edge)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const unused = new Set(byStart.keys())
|
|
191
|
+
const loops: RoofFace[] = []
|
|
192
|
+
while (unused.size > 0) {
|
|
193
|
+
const firstKey = unused.values().next().value
|
|
194
|
+
if (typeof firstKey !== 'string') return null
|
|
195
|
+
const loop: RoofFace = []
|
|
196
|
+
let key = firstKey
|
|
197
|
+
|
|
198
|
+
do {
|
|
199
|
+
const edge = byStart.get(key)
|
|
200
|
+
if (!edge || !unused.delete(key)) return null
|
|
201
|
+
loop.push(edge.start)
|
|
202
|
+
key = pointKey(edge.end)
|
|
203
|
+
} while (key !== firstKey)
|
|
204
|
+
|
|
205
|
+
if (loop.length < 3) return null
|
|
206
|
+
loops.push(loop)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return loops
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function loopCentroid(loop: RoofFace): RoofShapeFaceVertex {
|
|
213
|
+
const sum = loop.reduce(
|
|
214
|
+
(total, point) => ({ x: total.x + point.x, y: total.y + point.y, z: total.z + point.z }),
|
|
215
|
+
{ x: 0, y: 0, z: 0 },
|
|
216
|
+
)
|
|
217
|
+
return { x: sum.x / loop.length, y: sum.y / loop.length, z: sum.z / loop.length }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function rotated<T>(values: T[], start: number): T[] {
|
|
221
|
+
return [...values.slice(start), ...values.slice(0, start)]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function bridgeBoundaryLoop(first: RoofFace, second: RoofFace): RoofFace[] | null {
|
|
225
|
+
const reversedSecond = [...second].reverse()
|
|
226
|
+
let best: { cost: number; second: RoofFace; moves: ('first' | 'second')[] } | undefined
|
|
227
|
+
|
|
228
|
+
for (let secondStart = 0; secondStart < reversedSecond.length; secondStart += 1) {
|
|
229
|
+
const candidate = rotated(reversedSecond, secondStart)
|
|
230
|
+
const width = candidate.length + 1
|
|
231
|
+
const costs = new Array<number>((first.length + 1) * width).fill(Number.POSITIVE_INFINITY)
|
|
232
|
+
const previous = new Array<'first' | 'second' | undefined>(costs.length)
|
|
233
|
+
const indexOf = (firstIndex: number, secondIndex: number) => firstIndex * width + secondIndex
|
|
234
|
+
costs[0] = pointDistanceSq(first[0]!, candidate[0]!)
|
|
235
|
+
|
|
236
|
+
for (let firstIndex = 0; firstIndex <= first.length; firstIndex += 1) {
|
|
237
|
+
for (let secondIndex = 0; secondIndex <= candidate.length; secondIndex += 1) {
|
|
238
|
+
const index = indexOf(firstIndex, secondIndex)
|
|
239
|
+
const cost = costs[index]!
|
|
240
|
+
if (!Number.isFinite(cost)) continue
|
|
241
|
+
const firstPoint = first[firstIndex % first.length]!
|
|
242
|
+
const secondPoint = candidate[secondIndex % candidate.length]!
|
|
243
|
+
|
|
244
|
+
if (firstIndex < first.length) {
|
|
245
|
+
const nextFirst = first[(firstIndex + 1) % first.length]!
|
|
246
|
+
const nextIndex = indexOf(firstIndex + 1, secondIndex)
|
|
247
|
+
const nextCost = cost + pointDistanceSq(nextFirst, secondPoint)
|
|
248
|
+
if (nextCost < costs[nextIndex]!) {
|
|
249
|
+
costs[nextIndex] = nextCost
|
|
250
|
+
previous[nextIndex] = 'first'
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (secondIndex < candidate.length) {
|
|
254
|
+
const nextSecond = candidate[(secondIndex + 1) % candidate.length]!
|
|
255
|
+
const nextIndex = indexOf(firstIndex, secondIndex + 1)
|
|
256
|
+
const nextCost = cost + pointDistanceSq(firstPoint, nextSecond)
|
|
257
|
+
if (nextCost < costs[nextIndex]!) {
|
|
258
|
+
costs[nextIndex] = nextCost
|
|
259
|
+
previous[nextIndex] = 'second'
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const endIndex = indexOf(first.length, candidate.length)
|
|
266
|
+
const moves: ('first' | 'second')[] = []
|
|
267
|
+
let firstIndex = first.length
|
|
268
|
+
let secondIndex = candidate.length
|
|
269
|
+
while (firstIndex > 0 || secondIndex > 0) {
|
|
270
|
+
const move = previous[indexOf(firstIndex, secondIndex)]
|
|
271
|
+
if (!move) return null
|
|
272
|
+
moves.push(move)
|
|
273
|
+
if (move === 'first') firstIndex -= 1
|
|
274
|
+
else secondIndex -= 1
|
|
275
|
+
}
|
|
276
|
+
moves.reverse()
|
|
277
|
+
|
|
278
|
+
if (!best || costs[endIndex]! < best.cost) {
|
|
279
|
+
best = { cost: costs[endIndex]!, second: candidate, moves }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!best) return null
|
|
284
|
+
const bridges: RoofFace[] = []
|
|
285
|
+
let firstIndex = 0
|
|
286
|
+
let secondIndex = 0
|
|
287
|
+
for (const move of best.moves) {
|
|
288
|
+
const firstPoint = first[firstIndex % first.length]!
|
|
289
|
+
const secondPoint = best.second[secondIndex % best.second.length]!
|
|
290
|
+
if (move === 'first') {
|
|
291
|
+
const nextFirst = first[(firstIndex + 1) % first.length]!
|
|
292
|
+
bridges.push([nextFirst, firstPoint, secondPoint])
|
|
293
|
+
firstIndex += 1
|
|
294
|
+
} else {
|
|
295
|
+
const nextSecond = best.second[(secondIndex + 1) % best.second.length]!
|
|
296
|
+
bridges.push([firstPoint, secondPoint, nextSecond])
|
|
297
|
+
secondIndex += 1
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return bridges
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function bridgeBoundaries(first: BoundaryEdge[], second: BoundaryEdge[]): RoofFace[] | null {
|
|
304
|
+
const firstLoops = boundaryLoops(first)
|
|
305
|
+
const secondLoops = boundaryLoops(second)
|
|
306
|
+
if (!firstLoops || !secondLoops || firstLoops.length !== secondLoops.length) return null
|
|
307
|
+
|
|
308
|
+
const unmatched = new Set(secondLoops.map((_, index) => index))
|
|
309
|
+
const bridges: RoofFace[] = []
|
|
310
|
+
for (const firstLoop of firstLoops) {
|
|
311
|
+
const firstCentroid = loopCentroid(firstLoop)
|
|
312
|
+
let bestIndex = -1
|
|
313
|
+
let bestScore = Number.POSITIVE_INFINITY
|
|
314
|
+
for (const candidateIndex of unmatched) {
|
|
315
|
+
const score = pointDistanceSq(firstCentroid, loopCentroid(secondLoops[candidateIndex]!))
|
|
316
|
+
if (score < bestScore) {
|
|
317
|
+
bestIndex = candidateIndex
|
|
318
|
+
bestScore = score
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (bestIndex < 0) return null
|
|
322
|
+
unmatched.delete(bestIndex)
|
|
323
|
+
const loopBridges = bridgeBoundaryLoop(firstLoop, secondLoops[bestIndex]!)
|
|
324
|
+
if (!loopBridges) return null
|
|
325
|
+
bridges.push(...loopBridges)
|
|
326
|
+
}
|
|
327
|
+
return bridges
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function splitWallBoundary(edges: BoundaryEdge[]): {
|
|
331
|
+
bottom: BoundaryEdge[]
|
|
332
|
+
top: BoundaryEdge[]
|
|
333
|
+
} | null {
|
|
334
|
+
const bottom: BoundaryEdge[] = []
|
|
335
|
+
const top: BoundaryEdge[] = []
|
|
336
|
+
for (const edge of edges) {
|
|
337
|
+
if (Math.abs(edge.start.y) <= FACE_EPSILON && Math.abs(edge.end.y) <= FACE_EPSILON) {
|
|
338
|
+
bottom.push(edge)
|
|
339
|
+
} else {
|
|
340
|
+
top.push(edge)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return bottom.length > 0 && top.length > 0 ? { bottom, top } : null
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function buildRoofModuleGeometry(
|
|
347
|
+
wallOuterFaces: RoofFace[],
|
|
348
|
+
wallInnerFaces: RoofFace[],
|
|
349
|
+
roofOuterFaces: RoofFace[],
|
|
350
|
+
roofInnerFaces: RoofFace[],
|
|
351
|
+
): RoofModuleGeometryResult {
|
|
352
|
+
const isWall = (normal: THREE.Vector3) => Math.abs(normal.y) <= SURFACE_NORMAL_EPSILON
|
|
353
|
+
const isRoof = (normal: THREE.Vector3) => normal.y > SURFACE_NORMAL_EPSILON
|
|
354
|
+
const wallOuter = selectSurfaceFaces(wallOuterFaces, isWall)
|
|
355
|
+
const wallInner = selectSurfaceFaces(wallInnerFaces, isWall, true)
|
|
356
|
+
const roofOuter = selectSurfaceFaces(roofOuterFaces, isRoof)
|
|
357
|
+
const roofInner = selectSurfaceFaces(roofInnerFaces, isRoof, true)
|
|
358
|
+
if (!wallOuter || !wallInner || !roofOuter || !roofInner) {
|
|
359
|
+
return { status: 'blocked', message: 'A canonical wall or roof surface is missing.' }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const wallOuterBoundary = boundaryEdges(wallOuter)
|
|
363
|
+
const wallInnerBoundary = boundaryEdges(wallInner)
|
|
364
|
+
const roofOuterBoundary = boundaryEdges(roofOuter)
|
|
365
|
+
const roofInnerBoundary = boundaryEdges(roofInner)
|
|
366
|
+
if (!wallOuterBoundary || !wallInnerBoundary || !roofOuterBoundary || !roofInnerBoundary) {
|
|
367
|
+
return { status: 'blocked', message: 'A canonical surface has invalid edge incidence.' }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const outerWallLoops = splitWallBoundary(wallOuterBoundary)
|
|
371
|
+
const innerWallLoops = splitWallBoundary(wallInnerBoundary)
|
|
372
|
+
if (!outerWallLoops || !innerWallLoops) {
|
|
373
|
+
return { status: 'blocked', message: 'A wall surface does not expose top and bottom loops.' }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const bottomRing = bridgeBoundaries(outerWallLoops.bottom, innerWallLoops.bottom)
|
|
377
|
+
const outerRoofJoin = bridgeBoundaries(outerWallLoops.top, roofOuterBoundary)
|
|
378
|
+
const innerRoofJoin = bridgeBoundaries(innerWallLoops.top, roofInnerBoundary)
|
|
379
|
+
if (!bottomRing) {
|
|
380
|
+
return {
|
|
381
|
+
status: 'blocked',
|
|
382
|
+
message: `Outer and inner wall bottoms have incompatible edge counts (${outerWallLoops.bottom.length} and ${innerWallLoops.bottom.length}).`,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (!outerRoofJoin) {
|
|
386
|
+
return {
|
|
387
|
+
status: 'blocked',
|
|
388
|
+
message: `Outer wall and roof boundaries have incompatible edge counts (${outerWallLoops.top.length} and ${roofOuterBoundary.length}).`,
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!innerRoofJoin) {
|
|
392
|
+
return {
|
|
393
|
+
status: 'blocked',
|
|
394
|
+
message: `Inner wall and roof boundaries have incompatible edge counts (${innerWallLoops.top.length} and ${roofInnerBoundary.length}).`,
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const geometry = geometryFromFaces([
|
|
399
|
+
...wallOuter,
|
|
400
|
+
...roofOuter,
|
|
401
|
+
...wallInner,
|
|
402
|
+
...roofInner,
|
|
403
|
+
...bottomRing,
|
|
404
|
+
...outerRoofJoin,
|
|
405
|
+
...innerRoofJoin,
|
|
406
|
+
])
|
|
407
|
+
return geometry
|
|
408
|
+
? { status: 'ready', geometry }
|
|
409
|
+
: { status: 'blocked', message: 'A canonical print face could not be triangulated.' }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function getVolumeFaces(
|
|
413
|
+
node: RoofSegmentNode,
|
|
414
|
+
options: { widthExtension: number; verticalOffset: number; isVoid: boolean },
|
|
415
|
+
): RoofFace[] {
|
|
416
|
+
const conicalCoverage = getConicalRoofCoverage(node)
|
|
417
|
+
const { activeRh, tanTheta } = getSegmentSlopeFrame(node)
|
|
418
|
+
const width = Math.max(0.01, node.width + options.widthExtension * 2)
|
|
419
|
+
const depth = Math.max(0.01, node.depth + options.widthExtension * 2)
|
|
420
|
+
const autoDrop = options.widthExtension * tanTheta
|
|
421
|
+
const wallHeight = Math.max(0.01, node.wallHeight - autoDrop + options.verticalOffset)
|
|
422
|
+
const roofHeight =
|
|
423
|
+
activeRh > 0 ? activeRh + autoDrop * (node.roofType === 'shed' ? 2 : 1) : activeRh
|
|
424
|
+
const shapeRatios = getRoofShapeRatios(node)
|
|
425
|
+
const dutchInset =
|
|
426
|
+
Math.min(node.width, node.depth) * node.dutchHipWidthRatio +
|
|
427
|
+
(options.isVoid ? node.deckThickness : 0)
|
|
428
|
+
|
|
429
|
+
return getRoofModuleFaces({
|
|
430
|
+
type: node.roofType,
|
|
431
|
+
w: width,
|
|
432
|
+
d: depth,
|
|
433
|
+
wh: wallHeight,
|
|
434
|
+
rh: roofHeight,
|
|
435
|
+
baseY: 0,
|
|
436
|
+
insets: { dutchI: dutchInset },
|
|
437
|
+
baseW: node.width,
|
|
438
|
+
baseD: node.depth,
|
|
439
|
+
tanTheta,
|
|
440
|
+
shapeRatios,
|
|
441
|
+
dutchTopRakeThickness: node.dutchTopRakeThickness,
|
|
442
|
+
conicalStartAngle: conicalCoverage.startAngle,
|
|
443
|
+
conicalSweepAngle: conicalCoverage.sweepAngle,
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function getShingleOuterFaces(node: RoofSegmentNode): RoofFace[] {
|
|
448
|
+
const conicalCoverage = getConicalRoofCoverage(node)
|
|
449
|
+
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node)
|
|
450
|
+
const shapeRatios = getRoofShapeRatios(node)
|
|
451
|
+
const horizontalOverhang = node.overhang * cosTheta
|
|
452
|
+
const deckExtension = node.wallThickness / 2 + horizontalOverhang
|
|
453
|
+
const deckVerticalThickness = activeRh > 0 ? node.deckThickness / cosTheta : node.deckThickness
|
|
454
|
+
const deckDrop = deckExtension * tanTheta
|
|
455
|
+
const shingleHorizontalThickness = node.shingleThickness * sinTheta
|
|
456
|
+
const shingleVerticalThickness = node.shingleThickness * cosTheta
|
|
457
|
+
const baseWidth = Math.max(0.01, node.width + deckExtension * 2)
|
|
458
|
+
const baseDepth = Math.max(0.01, node.depth + deckExtension * 2)
|
|
459
|
+
const baseWallHeight = node.wallHeight - deckDrop + deckVerticalThickness
|
|
460
|
+
const baseRoofHeight =
|
|
461
|
+
activeRh > 0 ? activeRh + deckDrop * (node.roofType === 'shed' ? 2 : 1) : activeRh
|
|
462
|
+
let width = baseWidth
|
|
463
|
+
let depth = baseDepth
|
|
464
|
+
let translateZ = 0
|
|
465
|
+
|
|
466
|
+
if (['hip', 'mansard', 'dutch', 'conical'].includes(node.roofType)) {
|
|
467
|
+
width += shingleHorizontalThickness * 2
|
|
468
|
+
depth += shingleHorizontalThickness * 2
|
|
469
|
+
} else if (['gable', 'gambrel'].includes(node.roofType)) {
|
|
470
|
+
depth += shingleHorizontalThickness * 2
|
|
471
|
+
} else if (node.roofType === 'shed') {
|
|
472
|
+
depth += shingleHorizontalThickness
|
|
473
|
+
translateZ = shingleHorizontalThickness / 2
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const wallHeight = baseWallHeight + shingleVerticalThickness
|
|
477
|
+
const roofHeight =
|
|
478
|
+
activeRh > 0 ? baseRoofHeight + shingleHorizontalThickness * tanTheta : baseRoofHeight
|
|
479
|
+
const insets = getRoofShapeInsets({
|
|
480
|
+
roofType: node.roofType,
|
|
481
|
+
width: node.width,
|
|
482
|
+
depth: node.depth,
|
|
483
|
+
wh: wallHeight,
|
|
484
|
+
baseY: 0,
|
|
485
|
+
isVoid: false,
|
|
486
|
+
brushW: width,
|
|
487
|
+
brushD: depth,
|
|
488
|
+
tanTheta,
|
|
489
|
+
shingleThickness: node.shingleThickness,
|
|
490
|
+
dutchHipWidthRatio: node.dutchHipWidthRatio,
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
const faces = getRoofModuleFaces({
|
|
494
|
+
type: node.roofType,
|
|
495
|
+
w: width,
|
|
496
|
+
d: depth,
|
|
497
|
+
wh: wallHeight,
|
|
498
|
+
rh: roofHeight,
|
|
499
|
+
baseY: 0,
|
|
500
|
+
insets,
|
|
501
|
+
baseW: node.width,
|
|
502
|
+
baseD: node.depth,
|
|
503
|
+
tanTheta,
|
|
504
|
+
shapeRatios,
|
|
505
|
+
dutchTopRakeThickness: node.dutchTopRakeThickness,
|
|
506
|
+
conicalStartAngle: conicalCoverage.startAngle,
|
|
507
|
+
conicalSweepAngle: conicalCoverage.sweepAngle,
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
if (translateZ === 0) return faces
|
|
511
|
+
return faces.map((face) => face.map((point) => ({ ...point, z: point.z + translateZ })))
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function blockingDiagnostics(
|
|
515
|
+
node: RoofSegmentNode,
|
|
516
|
+
nodes: Record<string, AnyNode> | undefined,
|
|
517
|
+
): PrintRoofSolidDiagnostic[] {
|
|
518
|
+
const diagnostics: PrintRoofSolidDiagnostic[] = []
|
|
519
|
+
const dimensions = [
|
|
520
|
+
node.width,
|
|
521
|
+
node.depth,
|
|
522
|
+
node.wallHeight,
|
|
523
|
+
node.wallThickness,
|
|
524
|
+
node.deckThickness,
|
|
525
|
+
node.overhang,
|
|
526
|
+
node.shingleThickness,
|
|
527
|
+
]
|
|
528
|
+
if (
|
|
529
|
+
dimensions.some((value) => !Number.isFinite(value) || value < 0) ||
|
|
530
|
+
node.width <= FACE_EPSILON ||
|
|
531
|
+
node.depth <= FACE_EPSILON ||
|
|
532
|
+
node.wallThickness <= FACE_EPSILON ||
|
|
533
|
+
node.deckThickness + node.shingleThickness <= FACE_EPSILON
|
|
534
|
+
) {
|
|
535
|
+
diagnostics.push({
|
|
536
|
+
severity: 'error',
|
|
537
|
+
code: 'invalid_roof_print_dimensions',
|
|
538
|
+
message: `Roof segment ${node.id} needs positive footprint, wall, and roof-cover thickness for print compilation.`,
|
|
539
|
+
nodeIds: [node.id],
|
|
540
|
+
})
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const trim = normalizeRoofSegmentTrim(node)
|
|
544
|
+
if (Object.values(trim).some((value) => value > FACE_EPSILON)) {
|
|
545
|
+
diagnostics.push({
|
|
546
|
+
severity: 'error',
|
|
547
|
+
code: 'unsupported_roof_print_trim',
|
|
548
|
+
message: `Roof segment ${node.id} has trim cuts that do not yet have a manifold print fixture.`,
|
|
549
|
+
nodeIds: [node.id],
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const cutNodeIds: string[] = []
|
|
554
|
+
for (const childId of node.children) {
|
|
555
|
+
const child = nodes?.[childId]
|
|
556
|
+
if (!child) {
|
|
557
|
+
cutNodeIds.push(childId)
|
|
558
|
+
continue
|
|
559
|
+
}
|
|
560
|
+
const definition = nodeRegistry.get(child.type)
|
|
561
|
+
const roofAccessory = definition?.capabilities.roofAccessory
|
|
562
|
+
if (!roofAccessory || roofAccessory.buildCut) {
|
|
563
|
+
cutNodeIds.push(child.id)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (cutNodeIds.length > 0) {
|
|
567
|
+
diagnostics.push({
|
|
568
|
+
severity: 'error',
|
|
569
|
+
code: 'unsupported_roof_print_cut',
|
|
570
|
+
message: `Roof segment ${node.id} has unresolved, unregistered, or cutting accessories that do not yet have a manifold print fixture.`,
|
|
571
|
+
nodeIds: [node.id, ...cutNodeIds].sort(),
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return diagnostics
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function buildPrintableRoofSegmentSolids(
|
|
579
|
+
node: RoofSegmentNode,
|
|
580
|
+
nodes?: Record<string, AnyNode>,
|
|
581
|
+
): PrintRoofSolidResult {
|
|
582
|
+
const diagnostics = blockingDiagnostics(node, nodes)
|
|
583
|
+
if (diagnostics.length > 0) return { status: 'blocked', object: null, diagnostics }
|
|
584
|
+
|
|
585
|
+
const { cosTheta } = getSegmentSlopeFrame(node)
|
|
586
|
+
const wallOuter = getVolumeFaces(node, {
|
|
587
|
+
widthExtension: node.wallThickness / 2,
|
|
588
|
+
verticalOffset: 0,
|
|
589
|
+
isVoid: false,
|
|
590
|
+
})
|
|
591
|
+
const wallInner = getVolumeFaces(node, {
|
|
592
|
+
widthExtension: -node.wallThickness / 2,
|
|
593
|
+
verticalOffset: 0,
|
|
594
|
+
isVoid: false,
|
|
595
|
+
})
|
|
596
|
+
const deckExtension = node.wallThickness / 2 + node.overhang * cosTheta
|
|
597
|
+
const roofInner = getVolumeFaces(node, {
|
|
598
|
+
widthExtension: deckExtension,
|
|
599
|
+
verticalOffset: 0,
|
|
600
|
+
isVoid: true,
|
|
601
|
+
})
|
|
602
|
+
const roofOuter = getShingleOuterFaces(node)
|
|
603
|
+
const geometryResult = buildRoofModuleGeometry(wallOuter, wallInner, roofOuter, roofInner)
|
|
604
|
+
if (geometryResult.status === 'blocked') {
|
|
605
|
+
return {
|
|
606
|
+
status: 'blocked',
|
|
607
|
+
object: null,
|
|
608
|
+
diagnostics: [
|
|
609
|
+
{
|
|
610
|
+
severity: 'error',
|
|
611
|
+
code: 'roof_print_topology_mismatch',
|
|
612
|
+
message: `Roof segment ${node.id} cannot form one closed print module. ${geometryResult.message}`,
|
|
613
|
+
nodeIds: [node.id],
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const root = new THREE.Group()
|
|
620
|
+
root.name = 'print-roof-segment-solids'
|
|
621
|
+
root.userData = { pascalId: node.id }
|
|
622
|
+
root.position.set(node.position[0], node.position[1], node.position[2])
|
|
623
|
+
root.rotation.y = node.rotation
|
|
624
|
+
|
|
625
|
+
const mesh = new THREE.Mesh(geometryResult.geometry)
|
|
626
|
+
mesh.name = 'print-roof-shell'
|
|
627
|
+
root.add(mesh)
|
|
628
|
+
|
|
629
|
+
return { status: 'ready', object: root, diagnostics: [] }
|
|
630
|
+
}
|