@pascal-app/editor 1.0.0-beta.4 → 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 +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- 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 +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -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 +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- 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 +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- 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 +204 -114
- 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 +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- 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 +447 -180
- 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 +63 -8
- 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-brush-cursor.tsx +2 -1
- 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 +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- 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 +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/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 +23 -6
- 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 +78 -26
- 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/panel-wrapper.tsx +228 -12
- 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/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -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 +128 -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 +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- 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 +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- 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 +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -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/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -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.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -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 +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -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/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- 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 +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -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/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- 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/store/use-session-groups.ts +103 -0
- 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,9 +51,11 @@ 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'
|
|
58
|
+
import { MetricControl } from '../../../controls/metric-control'
|
|
54
59
|
import { LevelDuplicateDialog } from '../../../level-duplicate-dialog'
|
|
55
60
|
import { InlineRenameInput } from './inline-rename-input'
|
|
56
61
|
import { focusTreeNode, TreeNode } from './tree-node'
|
|
@@ -349,8 +354,25 @@ const ReferenceItem = memo(function ReferenceItem({
|
|
|
349
354
|
handleDelete: (id: string, e: React.MouseEvent) => void
|
|
350
355
|
}) {
|
|
351
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
|
+
|
|
352
373
|
const handleSelect = () => {
|
|
353
374
|
setSelectedReferenceId(refNode.id)
|
|
375
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
354
376
|
}
|
|
355
377
|
|
|
356
378
|
const handleDoubleClick = () => {
|
|
@@ -358,53 +380,98 @@ const ReferenceItem = memo(function ReferenceItem({
|
|
|
358
380
|
}
|
|
359
381
|
|
|
360
382
|
return (
|
|
361
|
-
<div
|
|
362
|
-
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"
|
|
363
|
-
onClick={handleSelect}
|
|
364
|
-
onDoubleClick={handleDoubleClick}
|
|
365
|
-
>
|
|
383
|
+
<div className="relative flex flex-col">
|
|
366
384
|
<div
|
|
367
385
|
className={cn(
|
|
368
|
-
'pointer-
|
|
369
|
-
|
|
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',
|
|
370
389
|
)}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
/>
|
|
377
404
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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)}
|
|
390
447
|
/>
|
|
391
|
-
|
|
392
|
-
<InlineRenameInput
|
|
393
|
-
defaultName={refNode.type === 'scan' ? '3D Scan' : 'Guide Image'}
|
|
394
|
-
isEditing={isEditing}
|
|
395
|
-
nodeId={refNode.id}
|
|
396
|
-
onStartEditing={() => setIsEditing(true)}
|
|
397
|
-
onStopEditing={() => setIsEditing(false)}
|
|
398
|
-
/>
|
|
399
|
-
</div>
|
|
448
|
+
</div>
|
|
400
449
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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}
|
|
408
475
|
</div>
|
|
409
476
|
)
|
|
410
477
|
})
|
|
@@ -642,7 +709,8 @@ const LevelItem = memo(function LevelItem({
|
|
|
642
709
|
? (level.parentId as BuildingNode['id'])
|
|
643
710
|
: undefined
|
|
644
711
|
|
|
645
|
-
const selectLevel = (levelId: LevelNode['id']) => {
|
|
712
|
+
const selectLevel = (levelId: LevelNode['id'], measure = true) => {
|
|
713
|
+
if (measure && selectedLevelId !== levelId) markPerfAction('level-switch', levelId)
|
|
646
714
|
setSelection(buildingId ? { buildingId, levelId } : { levelId })
|
|
647
715
|
}
|
|
648
716
|
|
|
@@ -681,7 +749,7 @@ const LevelItem = memo(function LevelItem({
|
|
|
681
749
|
)
|
|
682
750
|
}
|
|
683
751
|
createNodes(createOps)
|
|
684
|
-
selectLevel(newLevelId as LevelNode['id'])
|
|
752
|
+
selectLevel(newLevelId as LevelNode['id'], false)
|
|
685
753
|
setDuplicateDialogOpen(false)
|
|
686
754
|
}
|
|
687
755
|
|
|
@@ -873,6 +941,17 @@ const LevelItem = memo(function LevelItem({
|
|
|
873
941
|
initial={{ height: 0, opacity: 0 }}
|
|
874
942
|
transition={{ type: 'spring', bounce: 0, duration: 0.3 }}
|
|
875
943
|
>
|
|
944
|
+
<div className="relative border-border/50 border-b py-2 pr-3 pl-[60px]">
|
|
945
|
+
<div className="pointer-events-none absolute top-0 bottom-0 left-[45px] z-10 w-px bg-border/50" />
|
|
946
|
+
<MetricControl
|
|
947
|
+
label="Base elevation"
|
|
948
|
+
onChange={(value) => updateNode(level.id, { baseElevation: value })}
|
|
949
|
+
precision={2}
|
|
950
|
+
step={0.05}
|
|
951
|
+
unit="m"
|
|
952
|
+
value={Math.round((level.baseElevation ?? 0) * 100) / 100}
|
|
953
|
+
/>
|
|
954
|
+
</div>
|
|
876
955
|
<LevelReferences
|
|
877
956
|
isLastLevel={isLast}
|
|
878
957
|
levelId={level.id}
|
|
@@ -1320,7 +1399,10 @@ const ContentSection = memo(function ContentSection() {
|
|
|
1320
1399
|
if (!selectedLevelId) return []
|
|
1321
1400
|
const lvl = s.nodes[selectedLevelId] as LevelNode | undefined
|
|
1322
1401
|
if (!lvl) return []
|
|
1323
|
-
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
|
+
})
|
|
1324
1406
|
}),
|
|
1325
1407
|
)
|
|
1326
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
|
+
})
|