@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,140 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { PrintLevelBundleReport } from '../../../../../lib/level-print-export'
|
|
3
|
+
import type { ModelExport, ModelExportOptions } from '../../../../../lib/model-export'
|
|
4
|
+
import type { PrintExportReport } from '../../../../../lib/print-export'
|
|
5
|
+
import { preparePrintExport } from './print-export-button'
|
|
6
|
+
|
|
7
|
+
const report: PrintExportReport = {
|
|
8
|
+
kind: 'print-export-report',
|
|
9
|
+
version: 2,
|
|
10
|
+
format: '3mf',
|
|
11
|
+
scale: 100,
|
|
12
|
+
units: 'millimeter',
|
|
13
|
+
orientation: 'z-up',
|
|
14
|
+
status: 'pass',
|
|
15
|
+
bounds: {
|
|
16
|
+
min: { x: -25, y: -15, z: 0 },
|
|
17
|
+
max: { x: 25, y: 15, z: 20 },
|
|
18
|
+
width: 50,
|
|
19
|
+
depth: 30,
|
|
20
|
+
height: 20,
|
|
21
|
+
},
|
|
22
|
+
triangleCount: 12,
|
|
23
|
+
invalidTriangleCount: 0,
|
|
24
|
+
degenerateTriangleCount: 0,
|
|
25
|
+
boundaryEdgeCount: 0,
|
|
26
|
+
nonManifoldEdgeCount: 0,
|
|
27
|
+
connectedComponentCount: 1,
|
|
28
|
+
solidComponentCount: 1,
|
|
29
|
+
invertedWinding: false,
|
|
30
|
+
volumeMm3: 30_000,
|
|
31
|
+
diagnostics: [],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('simple 3D print export', () => {
|
|
35
|
+
test('uses one fixed safe print profile', async () => {
|
|
36
|
+
const calls: { format?: string; options?: ModelExportOptions }[] = []
|
|
37
|
+
const artifact = { blob: new Blob(['3mf']), filename: 'house.3mf', metadata: report }
|
|
38
|
+
const modelExport: ModelExport = async (format, options) => {
|
|
39
|
+
calls.push({ format, options })
|
|
40
|
+
return artifact
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const prepared = await preparePrintExport(modelExport, true)
|
|
44
|
+
|
|
45
|
+
expect(calls).toEqual([
|
|
46
|
+
{
|
|
47
|
+
format: 'print-3mf',
|
|
48
|
+
options: {
|
|
49
|
+
onlyVisible: true,
|
|
50
|
+
download: false,
|
|
51
|
+
printScale: 100,
|
|
52
|
+
printScope: 'levels',
|
|
53
|
+
printContent: 'structure',
|
|
54
|
+
printBase: 'none',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
])
|
|
58
|
+
expect(prepared).toEqual({ artifact, report })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('blocks the download when preflight finds invalid geometry', async () => {
|
|
62
|
+
const blockedReport: PrintExportReport = {
|
|
63
|
+
...report,
|
|
64
|
+
status: 'blocked',
|
|
65
|
+
diagnostics: [
|
|
66
|
+
{
|
|
67
|
+
severity: 'error',
|
|
68
|
+
code: 'open_boundary',
|
|
69
|
+
message: 'One wall has an open edge.',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}
|
|
73
|
+
const modelExport: ModelExport = async () => ({
|
|
74
|
+
blob: new Blob(['3mf']),
|
|
75
|
+
filename: 'house.3mf',
|
|
76
|
+
metadata: blockedReport,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await expect(preparePrintExport(modelExport, true)).rejects.toThrow(
|
|
80
|
+
'One wall has an open edge.',
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('shows a per-level preflight error when the bundle has no top-level error', async () => {
|
|
85
|
+
const blockedPartReport: PrintExportReport = {
|
|
86
|
+
...report,
|
|
87
|
+
status: 'blocked',
|
|
88
|
+
diagnostics: [
|
|
89
|
+
{
|
|
90
|
+
severity: 'error',
|
|
91
|
+
code: 'open_boundary',
|
|
92
|
+
message: 'The upper level has an open edge.',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
const blockedBundleReport: PrintLevelBundleReport = {
|
|
97
|
+
kind: 'print-level-export-report',
|
|
98
|
+
version: 2,
|
|
99
|
+
format: '3mf',
|
|
100
|
+
scale: 100,
|
|
101
|
+
units: 'millimeter',
|
|
102
|
+
orientation: 'z-up',
|
|
103
|
+
status: 'blocked',
|
|
104
|
+
partCount: 1,
|
|
105
|
+
parts: [
|
|
106
|
+
{
|
|
107
|
+
kind: 'level',
|
|
108
|
+
levelId: 'upper-level',
|
|
109
|
+
label: 'Upper level',
|
|
110
|
+
objectName: 'Upper level',
|
|
111
|
+
filename: null,
|
|
112
|
+
sourceBaseMeters: 3,
|
|
113
|
+
report: blockedPartReport,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
excludedNodeIds: [],
|
|
117
|
+
diagnostics: [],
|
|
118
|
+
}
|
|
119
|
+
const modelExport: ModelExport = async () => ({
|
|
120
|
+
blob: new Blob(['3mf']),
|
|
121
|
+
filename: 'house.zip',
|
|
122
|
+
metadata: blockedBundleReport,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
await expect(preparePrintExport(modelExport, true)).rejects.toThrow(
|
|
126
|
+
'The upper level has an open edge.',
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('rejects an exporter response without print metadata', async () => {
|
|
131
|
+
const modelExport: ModelExport = async () => ({
|
|
132
|
+
blob: new Blob(['3mf']),
|
|
133
|
+
filename: 'house.3mf',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
await expect(preparePrintExport(modelExport, false)).rejects.toThrow(
|
|
137
|
+
'did not return a valid file',
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AlertTriangle, Printer } from 'lucide-react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { Button } from '../../../../../components/ui/primitives/button'
|
|
4
|
+
import {
|
|
5
|
+
isPrintLevelBundleReport,
|
|
6
|
+
type PrintLevelBundleReport,
|
|
7
|
+
} from '../../../../../lib/level-print-export'
|
|
8
|
+
import type { ModelExport, ModelExportArtifact } from '../../../../../lib/model-export'
|
|
9
|
+
import {
|
|
10
|
+
isPrintExportReport,
|
|
11
|
+
type PrintExportReport,
|
|
12
|
+
} from '../../../../../lib/print-export'
|
|
13
|
+
import useEditor from '../../../../../store/use-editor'
|
|
14
|
+
|
|
15
|
+
type PreparedPrintExport = {
|
|
16
|
+
artifact: ModelExportArtifact
|
|
17
|
+
report: PrintExportReport | PrintLevelBundleReport
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function downloadArtifact(artifact: ModelExportArtifact) {
|
|
21
|
+
const url = URL.createObjectURL(artifact.blob)
|
|
22
|
+
const link = document.createElement('a')
|
|
23
|
+
link.href = url
|
|
24
|
+
link.download = artifact.filename
|
|
25
|
+
link.click()
|
|
26
|
+
URL.revokeObjectURL(url)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function firstBlockingMessage(report: PrintExportReport | PrintLevelBundleReport) {
|
|
30
|
+
const bundleDiagnostic = report.diagnostics.find((item) => item.severity === 'error')
|
|
31
|
+
if (bundleDiagnostic || !isPrintLevelBundleReport(report)) return bundleDiagnostic?.message
|
|
32
|
+
|
|
33
|
+
for (const part of report.parts) {
|
|
34
|
+
const partDiagnostic = part.report.diagnostics.find((item) => item.severity === 'error')
|
|
35
|
+
if (partDiagnostic) return partDiagnostic.message
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function preparePrintExport(
|
|
40
|
+
modelExport: ModelExport,
|
|
41
|
+
onlyVisible: boolean,
|
|
42
|
+
): Promise<PreparedPrintExport> {
|
|
43
|
+
const artifact = await modelExport('print-3mf', {
|
|
44
|
+
onlyVisible,
|
|
45
|
+
download: false,
|
|
46
|
+
printScale: 100,
|
|
47
|
+
printScope: 'levels',
|
|
48
|
+
printContent: 'structure',
|
|
49
|
+
printBase: 'none',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
!artifact ||
|
|
54
|
+
(!isPrintExportReport(artifact.metadata) && !isPrintLevelBundleReport(artifact.metadata))
|
|
55
|
+
) {
|
|
56
|
+
throw new Error('The 3D print exporter did not return a valid file.')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (artifact.metadata.status === 'blocked') {
|
|
60
|
+
throw new Error(
|
|
61
|
+
firstBlockingMessage(artifact.metadata) ??
|
|
62
|
+
'This project cannot be exported as printable parts.',
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { artifact, report: artifact.metadata }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function PrintExportButton({ onlyVisible }: { onlyVisible: boolean }) {
|
|
70
|
+
const modelExport = useEditor((state) => state.modelExport)
|
|
71
|
+
const [isExporting, setIsExporting] = useState(false)
|
|
72
|
+
const [error, setError] = useState<string | null>(null)
|
|
73
|
+
|
|
74
|
+
const handleExport = async () => {
|
|
75
|
+
if (!modelExport) return
|
|
76
|
+
|
|
77
|
+
setIsExporting(true)
|
|
78
|
+
setError(null)
|
|
79
|
+
try {
|
|
80
|
+
const prepared = await preparePrintExport(modelExport, onlyVisible)
|
|
81
|
+
downloadArtifact(prepared.artifact)
|
|
82
|
+
} catch (reason) {
|
|
83
|
+
setError(reason instanceof Error ? reason.message : '3D print export failed.')
|
|
84
|
+
} finally {
|
|
85
|
+
setIsExporting(false)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<Button
|
|
92
|
+
aria-busy={isExporting}
|
|
93
|
+
className="w-full justify-start gap-2"
|
|
94
|
+
disabled={isExporting || !modelExport}
|
|
95
|
+
onClick={handleExport}
|
|
96
|
+
variant="outline"
|
|
97
|
+
>
|
|
98
|
+
<Printer className="size-4" />
|
|
99
|
+
Export 3D print files
|
|
100
|
+
</Button>
|
|
101
|
+
{error && (
|
|
102
|
+
<div className="flex gap-2 text-destructive text-xs">
|
|
103
|
+
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
|
|
104
|
+
<span>{error}</span>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -2,10 +2,11 @@ import { type AnyNodeId, type DormerNode, useScene } from '@pascal-app/core'
|
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import Image from 'next/image'
|
|
4
4
|
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
5
6
|
import { SnapTargetIcon } from '../../../snap-target-badge'
|
|
6
7
|
import useEditor from './../../../../../store/use-editor'
|
|
7
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
8
|
-
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
9
|
+
import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
9
10
|
import { TreeNodeActions } from './tree-node-actions'
|
|
10
11
|
|
|
11
12
|
interface DormerTreeNodeProps {
|
|
@@ -26,8 +27,12 @@ export const DormerTreeNode = memo(function DormerTreeNode({
|
|
|
26
27
|
isLast,
|
|
27
28
|
}: DormerTreeNodeProps) {
|
|
28
29
|
const [isEditing, setIsEditing] = useState(false)
|
|
30
|
+
const [expanded, setExpanded] = useState(true)
|
|
29
31
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
30
32
|
const node = useScene((s) => s.nodes[nodeId] as DormerNode | undefined)
|
|
33
|
+
const children = useScene(
|
|
34
|
+
useShallow((s) => (s.nodes[nodeId] as DormerNode | undefined)?.children ?? []),
|
|
35
|
+
)
|
|
31
36
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
32
37
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
33
38
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -55,8 +60,8 @@ export const DormerTreeNode = memo(function DormerTreeNode({
|
|
|
55
60
|
<TreeNodeWrapper
|
|
56
61
|
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
57
62
|
depth={depth}
|
|
58
|
-
expanded={
|
|
59
|
-
hasChildren={
|
|
63
|
+
expanded={expanded}
|
|
64
|
+
hasChildren={children.length > 0}
|
|
60
65
|
icon={
|
|
61
66
|
<SnapTargetIcon target="roof">
|
|
62
67
|
<Image
|
|
@@ -86,7 +91,16 @@ export const DormerTreeNode = memo(function DormerTreeNode({
|
|
|
86
91
|
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
87
92
|
onMouseEnter={() => setHoveredId(nodeId)}
|
|
88
93
|
onMouseLeave={() => setHoveredId(null)}
|
|
89
|
-
onToggle={() =>
|
|
90
|
-
|
|
94
|
+
onToggle={() => setExpanded((value) => !value)}
|
|
95
|
+
>
|
|
96
|
+
{children.map((childId, index) => (
|
|
97
|
+
<TreeNode
|
|
98
|
+
depth={depth + 1}
|
|
99
|
+
isLast={index === children.length - 1}
|
|
100
|
+
key={childId}
|
|
101
|
+
nodeId={childId as AnyNodeId}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</TreeNodeWrapper>
|
|
91
105
|
)
|
|
92
106
|
})
|
|
@@ -11,11 +11,14 @@ import {
|
|
|
11
11
|
useScene,
|
|
12
12
|
type ZoneNode,
|
|
13
13
|
} from '@pascal-app/core'
|
|
14
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
14
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
15
15
|
import {
|
|
16
16
|
Camera,
|
|
17
17
|
ChevronDown,
|
|
18
|
+
ChevronRight,
|
|
18
19
|
Copy,
|
|
20
|
+
Eye,
|
|
21
|
+
EyeOff,
|
|
19
22
|
Loader2,
|
|
20
23
|
MoreHorizontal,
|
|
21
24
|
Pencil,
|
|
@@ -25,7 +28,7 @@ import {
|
|
|
25
28
|
X,
|
|
26
29
|
} from 'lucide-react'
|
|
27
30
|
import { AnimatePresence, LayoutGroup, motion } from 'motion/react'
|
|
28
|
-
import { memo, useEffect, useRef, useState } from 'react'
|
|
31
|
+
import { memo, useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
29
32
|
import { useShallow } from 'zustand/react/shallow'
|
|
30
33
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
31
34
|
import {
|
|
@@ -48,6 +51,7 @@ import {
|
|
|
48
51
|
squareMetersToAreaUnit,
|
|
49
52
|
} from './../../../../../lib/measurements'
|
|
50
53
|
import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
|
|
54
|
+
import { editorHostTreeChildrenRegistry } from './../../../../../lib/host-tree-children'
|
|
51
55
|
import { cn } from './../../../../../lib/utils'
|
|
52
56
|
import useEditor from './../../../../../store/use-editor'
|
|
53
57
|
import { useUploadStore } from '../../../../../store/use-upload'
|
|
@@ -350,8 +354,25 @@ const ReferenceItem = memo(function ReferenceItem({
|
|
|
350
354
|
handleDelete: (id: string, e: React.MouseEvent) => void
|
|
351
355
|
}) {
|
|
352
356
|
const [isEditing, setIsEditing] = useState(false)
|
|
357
|
+
const [isExpanded, setIsExpanded] = useState(true)
|
|
358
|
+
const updateNode = useScene((state) => state.updateNode)
|
|
359
|
+
const selectedReferenceId = useEditor((state) => state.selectedReferenceId)
|
|
360
|
+
const isCapture = refNode.type === 'scan'
|
|
361
|
+
const isVisible = refNode.visible !== false
|
|
362
|
+
useSyncExternalStore(
|
|
363
|
+
editorHostTreeChildrenRegistry.subscribe,
|
|
364
|
+
editorHostTreeChildrenRegistry.getSnapshot,
|
|
365
|
+
editorHostTreeChildrenRegistry.getSnapshot,
|
|
366
|
+
)
|
|
367
|
+
const hostChildren = isCapture
|
|
368
|
+
? editorHostTreeChildrenRegistry.childrenForKind(refNode.type)
|
|
369
|
+
: undefined
|
|
370
|
+
const hasHostChildren = Boolean(hostChildren?.hasChildren(refNode))
|
|
371
|
+
const HostChildren = hasHostChildren ? hostChildren?.component : undefined
|
|
372
|
+
|
|
353
373
|
const handleSelect = () => {
|
|
354
374
|
setSelectedReferenceId(refNode.id)
|
|
375
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
355
376
|
}
|
|
356
377
|
|
|
357
378
|
const handleDoubleClick = () => {
|
|
@@ -359,53 +380,98 @@ const ReferenceItem = memo(function ReferenceItem({
|
|
|
359
380
|
}
|
|
360
381
|
|
|
361
382
|
return (
|
|
362
|
-
<div
|
|
363
|
-
className="group/ref relative flex h-8 cursor-pointer select-none items-center border-border/50 border-b pr-2 text-xs transition-colors hover:bg-accent/30"
|
|
364
|
-
onClick={handleSelect}
|
|
365
|
-
onDoubleClick={handleDoubleClick}
|
|
366
|
-
>
|
|
383
|
+
<div className="relative flex flex-col">
|
|
367
384
|
<div
|
|
368
385
|
className={cn(
|
|
369
|
-
'pointer-
|
|
370
|
-
|
|
386
|
+
'group/ref relative flex h-8 cursor-pointer select-none items-center border-border/50 border-b pr-2 text-xs transition-colors hover:bg-accent/30',
|
|
387
|
+
selectedReferenceId === refNode.id && 'bg-accent/50 text-foreground',
|
|
388
|
+
!isVisible && 'opacity-50',
|
|
371
389
|
)}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
390
|
+
onClick={handleSelect}
|
|
391
|
+
onDoubleClick={handleDoubleClick}
|
|
392
|
+
>
|
|
393
|
+
<div
|
|
394
|
+
className={cn(
|
|
395
|
+
'pointer-events-none absolute z-10 w-px bg-border/50',
|
|
396
|
+
isLastRow && !(hasHostChildren && isExpanded) ? 'top-0 bottom-1/2' : 'top-0 bottom-0',
|
|
397
|
+
)}
|
|
398
|
+
style={{ left: 45 }}
|
|
399
|
+
/>
|
|
400
|
+
<div
|
|
401
|
+
className="pointer-events-none absolute top-1/2 z-10 h-px bg-border/50"
|
|
402
|
+
style={{ left: 45, width: 8 }}
|
|
403
|
+
/>
|
|
378
404
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
405
|
+
{isCapture ? (
|
|
406
|
+
<button
|
|
407
|
+
className="z-20 ml-[52px] flex h-4 w-4 shrink-0 items-center justify-center"
|
|
408
|
+
onClick={(event) => {
|
|
409
|
+
event.stopPropagation()
|
|
410
|
+
if (hasHostChildren) setIsExpanded((expanded) => !expanded)
|
|
411
|
+
}}
|
|
412
|
+
type="button"
|
|
413
|
+
>
|
|
414
|
+
{hasHostChildren ? (
|
|
415
|
+
<ChevronRight
|
|
416
|
+
className={cn('h-3 w-3 transition-transform', isExpanded && 'rotate-90')}
|
|
417
|
+
/>
|
|
418
|
+
) : null}
|
|
419
|
+
</button>
|
|
420
|
+
) : null}
|
|
421
|
+
|
|
422
|
+
<div
|
|
423
|
+
className={cn(
|
|
424
|
+
'flex h-8 min-w-0 flex-1 cursor-pointer items-center gap-2 py-0 text-muted-foreground group-hover/ref:text-foreground',
|
|
425
|
+
!isCapture && 'pl-[60px]',
|
|
426
|
+
)}
|
|
427
|
+
>
|
|
428
|
+
{isCapture ? (
|
|
429
|
+
<img
|
|
430
|
+
alt="Capture"
|
|
431
|
+
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
|
|
432
|
+
src="/icons/mesh.webp"
|
|
433
|
+
/>
|
|
434
|
+
) : (
|
|
435
|
+
<img
|
|
436
|
+
alt="Guide"
|
|
437
|
+
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
|
|
438
|
+
src="/icons/floorplan.webp"
|
|
439
|
+
/>
|
|
440
|
+
)}
|
|
441
|
+
<InlineRenameInput
|
|
442
|
+
defaultName={isCapture ? 'Capture' : 'Guide Image'}
|
|
443
|
+
isEditing={isEditing}
|
|
444
|
+
nodeId={refNode.id}
|
|
445
|
+
onStartEditing={() => setIsEditing(true)}
|
|
446
|
+
onStopEditing={() => setIsEditing(false)}
|
|
391
447
|
/>
|
|
392
|
-
|
|
393
|
-
<InlineRenameInput
|
|
394
|
-
defaultName={refNode.type === 'scan' ? '3D Scan' : 'Guide Image'}
|
|
395
|
-
isEditing={isEditing}
|
|
396
|
-
nodeId={refNode.id}
|
|
397
|
-
onStartEditing={() => setIsEditing(true)}
|
|
398
|
-
onStopEditing={() => setIsEditing(false)}
|
|
399
|
-
/>
|
|
400
|
-
</div>
|
|
448
|
+
</div>
|
|
401
449
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
450
|
+
{isCapture ? (
|
|
451
|
+
<button
|
|
452
|
+
className="z-20 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-black/5 hover:text-foreground group-hover/ref:opacity-100 dark:hover:bg-white/10"
|
|
453
|
+
onClick={(event) => {
|
|
454
|
+
event.stopPropagation()
|
|
455
|
+
updateNode(refNode.id, { visible: !isVisible })
|
|
456
|
+
}}
|
|
457
|
+
title={isVisible ? 'Hide' : 'Show'}
|
|
458
|
+
type="button"
|
|
459
|
+
>
|
|
460
|
+
{isVisible ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3" />}
|
|
461
|
+
</button>
|
|
462
|
+
) : null}
|
|
463
|
+
<button
|
|
464
|
+
className="z-20 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-black/5 hover:text-foreground group-hover/ref:opacity-100 dark:hover:bg-white/10"
|
|
465
|
+
onClick={(e) => handleDelete(refNode.id, e)}
|
|
466
|
+
title="Delete"
|
|
467
|
+
type="button"
|
|
468
|
+
>
|
|
469
|
+
<Trash2 className="h-3 w-3" />
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
472
|
+
{isExpanded && HostChildren ? (
|
|
473
|
+
<HostChildren depth={3} nodeId={refNode.id} parentVisible={isVisible} />
|
|
474
|
+
) : null}
|
|
409
475
|
</div>
|
|
410
476
|
)
|
|
411
477
|
})
|
|
@@ -643,7 +709,8 @@ const LevelItem = memo(function LevelItem({
|
|
|
643
709
|
? (level.parentId as BuildingNode['id'])
|
|
644
710
|
: undefined
|
|
645
711
|
|
|
646
|
-
const selectLevel = (levelId: LevelNode['id']) => {
|
|
712
|
+
const selectLevel = (levelId: LevelNode['id'], measure = true) => {
|
|
713
|
+
if (measure && selectedLevelId !== levelId) markPerfAction('level-switch', levelId)
|
|
647
714
|
setSelection(buildingId ? { buildingId, levelId } : { levelId })
|
|
648
715
|
}
|
|
649
716
|
|
|
@@ -682,7 +749,7 @@ const LevelItem = memo(function LevelItem({
|
|
|
682
749
|
)
|
|
683
750
|
}
|
|
684
751
|
createNodes(createOps)
|
|
685
|
-
selectLevel(newLevelId as LevelNode['id'])
|
|
752
|
+
selectLevel(newLevelId as LevelNode['id'], false)
|
|
686
753
|
setDuplicateDialogOpen(false)
|
|
687
754
|
}
|
|
688
755
|
|
|
@@ -1332,7 +1399,10 @@ const ContentSection = memo(function ContentSection() {
|
|
|
1332
1399
|
if (!selectedLevelId) return []
|
|
1333
1400
|
const lvl = s.nodes[selectedLevelId] as LevelNode | undefined
|
|
1334
1401
|
if (!lvl) return []
|
|
1335
|
-
return lvl.children.filter((childId) =>
|
|
1402
|
+
return lvl.children.filter((childId) => {
|
|
1403
|
+
const type = s.nodes[childId]?.type
|
|
1404
|
+
return type !== 'zone' && type !== 'scan'
|
|
1405
|
+
})
|
|
1336
1406
|
}),
|
|
1337
1407
|
)
|
|
1338
1408
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LevelNode, useScene } from '@pascal-app/core'
|
|
2
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
2
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { Layers } from 'lucide-react'
|
|
4
4
|
import { memo, useCallback, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
@@ -30,7 +30,10 @@ export const LevelTreeNode = memo(function LevelTreeNode({
|
|
|
30
30
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
31
31
|
const setSelection = useViewer((state) => state.setSelection)
|
|
32
32
|
|
|
33
|
-
const handleClick = useCallback(() =>
|
|
33
|
+
const handleClick = useCallback(() => {
|
|
34
|
+
if (!isSelected) markPerfAction('level-switch', nodeId)
|
|
35
|
+
setSelection({ levelId: nodeId })
|
|
36
|
+
}, [isSelected, nodeId, setSelection])
|
|
34
37
|
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
35
38
|
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
|
|
36
39
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
@@ -2,8 +2,9 @@ import { Icon as IconifyIcon } from '@iconify/react'
|
|
|
2
2
|
import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
4
|
import Image from 'next/image'
|
|
5
|
-
import { memo, useCallback, useEffect, useState } from 'react'
|
|
5
|
+
import { memo, useCallback, useEffect, useState, useSyncExternalStore } from 'react'
|
|
6
6
|
import { useShallow } from 'zustand/react/shallow'
|
|
7
|
+
import { editorHostTreeChildrenRegistry } from '../../../../../lib/host-tree-children'
|
|
7
8
|
import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
|
|
8
9
|
import { InlineRenameInput } from './inline-rename-input'
|
|
9
10
|
import {
|
|
@@ -43,9 +44,19 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
|
43
44
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
44
45
|
const setSelection = useViewer((state) => state.setSelection)
|
|
45
46
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
47
|
+
useSyncExternalStore(
|
|
48
|
+
editorHostTreeChildrenRegistry.subscribe,
|
|
49
|
+
editorHostTreeChildrenRegistry.getSnapshot,
|
|
50
|
+
editorHostTreeChildrenRegistry.getSnapshot,
|
|
51
|
+
)
|
|
46
52
|
|
|
47
53
|
const presentation = node ? nodeRegistry.get(node.type)?.presentation : undefined
|
|
48
54
|
const tree = node ? nodeRegistry.get(node.type)?.tree : undefined
|
|
55
|
+
const hostChildren = node
|
|
56
|
+
? editorHostTreeChildrenRegistry.childrenForKind(node.type)
|
|
57
|
+
: undefined
|
|
58
|
+
const hasHostChildren = Boolean(node && hostChildren?.hasChildren(node))
|
|
59
|
+
const HostChildren = hasHostChildren ? hostChildren?.component : undefined
|
|
49
60
|
const icon = presentation?.icon
|
|
50
61
|
const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.webp'
|
|
51
62
|
const iconElement =
|
|
@@ -63,7 +74,7 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
|
63
74
|
const snapTarget = resolveNodeSnapTarget(node)
|
|
64
75
|
const defaultName =
|
|
65
76
|
node ? tree?.label?.(node, useScene.getState().nodes) || node.name || presentation?.label || 'Node' : 'Node'
|
|
66
|
-
const hasChildren = children.length > 0
|
|
77
|
+
const hasChildren = children.length > 0 || hasHostChildren
|
|
67
78
|
|
|
68
79
|
useEffect(() => {
|
|
69
80
|
return useViewer.subscribe((state) => {
|
|
@@ -126,15 +137,21 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
|
126
137
|
onMouseLeave={() => setHoveredId(null)}
|
|
127
138
|
onToggle={() => setExpanded((prev) => !prev)}
|
|
128
139
|
>
|
|
129
|
-
{hasChildren &&
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
{hasChildren && (
|
|
141
|
+
<>
|
|
142
|
+
{children.map((childId, index) => (
|
|
143
|
+
<TreeNode
|
|
144
|
+
depth={depth + 1}
|
|
145
|
+
isLast={!hasHostChildren && index === children.length - 1}
|
|
146
|
+
key={childId}
|
|
147
|
+
nodeId={childId as AnyNodeId}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
{HostChildren ? (
|
|
151
|
+
<HostChildren depth={depth + 1} nodeId={nodeId} parentVisible={isVisible} />
|
|
152
|
+
) : null}
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
138
155
|
</TreeNodeWrapper>
|
|
139
156
|
)
|
|
140
157
|
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { getTreeNodeComponent } from './tree-node'
|
|
3
|
+
|
|
4
|
+
describe('site tree node routing', () => {
|
|
5
|
+
test('renders plugin node kinds through the generic tree row', () => {
|
|
6
|
+
expect(getTreeNodeComponent('lean-to-extension')).toBe(getTreeNodeComponent('plugin-kind'))
|
|
7
|
+
})
|
|
8
|
+
})
|