@pascal-app/editor 1.0.0-beta.5 → 1.0.1
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/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- 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 +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- 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/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -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 +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -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 +295 -239
- 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-action-menu.tsx +12 -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 +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- 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/ceiling/ceiling-system.tsx +3 -1
- 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/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -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 +304 -171
- 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 +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -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 +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- 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 +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- 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/tools/zone/zone-tool.tsx +13 -4
- 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 +63 -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/metric-control.tsx +29 -13
- 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 +44 -21
- 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 +103 -10
- 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 +266 -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 +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- 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 +502 -31
- 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 +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- 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/shelf-tree-node.tsx +3 -8
- 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 +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- 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 +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- 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 +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -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/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- 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 +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- 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 +5 -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/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- 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-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- 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/node-action-movement.ts +21 -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 +455 -0
- package/src/lib/portable-export.ts +1042 -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 +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -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 +38 -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-clipboard.ts +1 -1
- 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/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -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/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- 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-handle-group.ts +4 -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-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,21 +1,48 @@
|
|
|
1
1
|
import {
|
|
2
2
|
clearSceneHistory,
|
|
3
3
|
emitter,
|
|
4
|
+
isNodeKindEnabled,
|
|
5
|
+
nodeRegistry,
|
|
6
|
+
useRegistryVersion,
|
|
4
7
|
useScene,
|
|
8
|
+
type ParsedBuildJson,
|
|
5
9
|
validateBuildJson,
|
|
6
10
|
} from '@pascal-app/core'
|
|
7
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
+
import { useViewer, viewerPresentationRegistry } from '@pascal-app/viewer'
|
|
8
12
|
import { TreeView, VisualJson } from '@visual-json/react'
|
|
9
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Camera,
|
|
15
|
+
Check,
|
|
16
|
+
ChevronDown,
|
|
17
|
+
Copy,
|
|
18
|
+
Download,
|
|
19
|
+
Map as MapIcon,
|
|
20
|
+
Save,
|
|
21
|
+
Send,
|
|
22
|
+
Trash2,
|
|
23
|
+
Upload,
|
|
24
|
+
} from 'lucide-react'
|
|
10
25
|
import {
|
|
11
26
|
type KeyboardEvent,
|
|
12
27
|
type SyntheticEvent,
|
|
13
28
|
useCallback,
|
|
29
|
+
useEffect,
|
|
30
|
+
useId,
|
|
14
31
|
useMemo,
|
|
15
32
|
useRef,
|
|
16
33
|
useState,
|
|
34
|
+
useSyncExternalStore,
|
|
17
35
|
} from 'react'
|
|
18
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
exportFloorplanPdf,
|
|
38
|
+
type FloorplanExportScope,
|
|
39
|
+
} from '../../../../../lib/floorplan/floorplan-export'
|
|
40
|
+
import {
|
|
41
|
+
LocalAppError,
|
|
42
|
+
probeLocalApp,
|
|
43
|
+
sendGlbToLocalApp,
|
|
44
|
+
waitForLocalImport,
|
|
45
|
+
} from '../../../../../lib/send-to-app'
|
|
19
46
|
import { Button } from './../../../../../components/ui/primitives/button'
|
|
20
47
|
import {
|
|
21
48
|
Dialog,
|
|
@@ -23,12 +50,15 @@ import {
|
|
|
23
50
|
DialogTitle,
|
|
24
51
|
DialogTrigger,
|
|
25
52
|
} from './../../../../../components/ui/primitives/dialog'
|
|
53
|
+
import { Input } from './../../../../../components/ui/primitives/input'
|
|
26
54
|
import { Switch } from './../../../../../components/ui/primitives/switch'
|
|
27
55
|
import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
|
|
56
|
+
import { type SendToAppStep, useSendToApp } from './../../../../../store/use-send-to-app'
|
|
28
57
|
import useFloorplanMode from './../../../../../store/use-floorplan-mode'
|
|
29
58
|
import { AudioSettingsDialog } from './audio-settings-dialog'
|
|
30
59
|
import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog'
|
|
31
60
|
import { LoadBuildDialog, type PendingImport } from './load-build-dialog'
|
|
61
|
+
import { PrintExportButton } from './print-export-button'
|
|
32
62
|
|
|
33
63
|
type SceneNode = Record<string, unknown> & {
|
|
34
64
|
id?: unknown
|
|
@@ -53,6 +83,30 @@ type SceneGraphValue = {
|
|
|
53
83
|
detachedNodes?: SceneGraphNode[]
|
|
54
84
|
}
|
|
55
85
|
|
|
86
|
+
const MODEL_EXPORT_FORMATS = [
|
|
87
|
+
{ format: 'glb', label: 'GLB' },
|
|
88
|
+
{ format: 'usdz', label: 'USDZ' },
|
|
89
|
+
{ format: 'stl', label: 'STL' },
|
|
90
|
+
{ format: 'obj', label: 'OBJ' },
|
|
91
|
+
] as const
|
|
92
|
+
|
|
93
|
+
type ModelExportFormat = (typeof MODEL_EXPORT_FORMATS)[number]['format']
|
|
94
|
+
|
|
95
|
+
const SEND_TO_BLENDER_STEP_LABEL: Record<SendToAppStep, string> = {
|
|
96
|
+
probing: 'Looking for Blender…',
|
|
97
|
+
exporting: 'Preparing the scene…',
|
|
98
|
+
sending: 'Sending to Blender…',
|
|
99
|
+
importing: 'Blender is importing…',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const BLENDER_ADDON_URL = 'https://github.com/pascalorg/blender-addon#install'
|
|
103
|
+
|
|
104
|
+
type ExportableNodeType = {
|
|
105
|
+
type: string
|
|
106
|
+
label: string
|
|
107
|
+
supportsGeometryOnly: boolean
|
|
108
|
+
}
|
|
109
|
+
|
|
56
110
|
const isSceneNode = (value: unknown): value is SceneNode => {
|
|
57
111
|
return (
|
|
58
112
|
typeof value === 'object' &&
|
|
@@ -171,6 +225,8 @@ export interface ProjectVisibility {
|
|
|
171
225
|
|
|
172
226
|
export interface SettingsPanelProps {
|
|
173
227
|
projectId?: string
|
|
228
|
+
/** Shown as the scene name in apps the scene is sent to (Blender collection name). */
|
|
229
|
+
projectName?: string
|
|
174
230
|
projectVisibility?: ProjectVisibility
|
|
175
231
|
onVisibilityChange?: (
|
|
176
232
|
field: 'isPrivate' | 'showScansPublic' | 'showGuidesPublic',
|
|
@@ -180,22 +236,88 @@ export interface SettingsPanelProps {
|
|
|
180
236
|
|
|
181
237
|
export function SettingsPanel({
|
|
182
238
|
projectId,
|
|
239
|
+
projectName,
|
|
183
240
|
projectVisibility,
|
|
184
241
|
onVisibilityChange,
|
|
185
242
|
}: SettingsPanelProps = {}) {
|
|
186
243
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
244
|
+
const copyResetTimeoutRef = useRef<number | null>(null)
|
|
187
245
|
const nodes = useScene((state) => state.nodes)
|
|
188
246
|
const rootNodeIds = useScene((state) => state.rootNodeIds)
|
|
189
247
|
const installedPlugins = useScene((state) => state.installedPlugins)
|
|
248
|
+
const materials = useScene((state) => state.materials)
|
|
249
|
+
const collections = useScene((state) => state.collections)
|
|
190
250
|
const setScene = useScene((state) => state.setScene)
|
|
191
251
|
const clearScene = useScene((state) => state.clearScene)
|
|
192
252
|
const resetSelection = useViewer((state) => state.resetSelection)
|
|
193
|
-
const
|
|
253
|
+
const modelExport = useEditor((state) => state.modelExport)
|
|
194
254
|
const shadows = useViewer((state) => state.shadows)
|
|
195
255
|
const setPhase = useEditor((state) => state.setPhase)
|
|
196
256
|
const floorplanMode = useFloorplanMode((state) => state.mode)
|
|
257
|
+
const registryVersion = useRegistryVersion()
|
|
258
|
+
const visibleOnlySwitchId = useId()
|
|
259
|
+
const includeNodeTypeIdPrefix = useId()
|
|
260
|
+
const includePresentationIdPrefix = useId()
|
|
197
261
|
const [isGeneratingThumbnail, setIsGeneratingThumbnail] = useState(false)
|
|
262
|
+
const [exportOnlyVisible, setExportOnlyVisible] = useState(true)
|
|
263
|
+
const [excludedNodeTypes, setExcludedNodeTypes] = useState<string[]>([])
|
|
264
|
+
const [includedPresentationIds, setIncludedPresentationIds] = useState<string[]>([])
|
|
265
|
+
const [activeModelExport, setActiveModelExport] = useState<ModelExportFormat | null>(null)
|
|
266
|
+
const [modelExportError, setModelExportError] = useState<string | null>(null)
|
|
267
|
+
const [modelExportWarning, setModelExportWarning] = useState<string | null>(null)
|
|
268
|
+
const sendToBlenderStep = useSendToApp((state) => state.step)
|
|
269
|
+
const sendToBlenderMessage = useSendToApp((state) => state.message)
|
|
270
|
+
const setSendToBlenderStep = useSendToApp((state) => state.setStep)
|
|
271
|
+
const setSendToBlenderMessage = useSendToApp((state) => state.setMessage)
|
|
272
|
+
const [activeFloorplanExport, setActiveFloorplanExport] = useState<FloorplanExportScope | null>(
|
|
273
|
+
null,
|
|
274
|
+
)
|
|
275
|
+
const [floorplanExportError, setFloorplanExportError] = useState<string | null>(null)
|
|
198
276
|
const [pendingImport, setPendingImport] = useState<PendingImport | null>(null)
|
|
277
|
+
const [projectIdCopyState, setProjectIdCopyState] = useState<'idle' | 'copied' | 'error'>('idle')
|
|
278
|
+
const exportableNodeTypes = useMemo(() => {
|
|
279
|
+
void registryVersion
|
|
280
|
+
const uniqueTypes = new Set(Object.values(nodes).map((node) => node.type))
|
|
281
|
+
const options: ExportableNodeType[] = []
|
|
282
|
+
|
|
283
|
+
for (const type of uniqueTypes) {
|
|
284
|
+
const definition = nodeRegistry.get(type)
|
|
285
|
+
if (
|
|
286
|
+
!(
|
|
287
|
+
definition?.bakeGeometry ||
|
|
288
|
+
definition?.bakeGeometryAsync ||
|
|
289
|
+
definition?.bake === 'replace'
|
|
290
|
+
) ||
|
|
291
|
+
!isNodeKindEnabled(type, installedPlugins)
|
|
292
|
+
) {
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
options.push({
|
|
296
|
+
type,
|
|
297
|
+
label: definition.presentation?.label ?? type,
|
|
298
|
+
supportsGeometryOnly: !definition.bakeGeometryAsync || Boolean(definition.bakeGeometry),
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return options.sort((a, b) => {
|
|
303
|
+
const labelOrder = a.label.localeCompare(b.label)
|
|
304
|
+
return labelOrder === 0 ? a.type.localeCompare(b.type) : labelOrder
|
|
305
|
+
})
|
|
306
|
+
}, [installedPlugins, nodes, registryVersion])
|
|
307
|
+
const registeredPresentations = useSyncExternalStore(
|
|
308
|
+
viewerPresentationRegistry.subscribe,
|
|
309
|
+
viewerPresentationRegistry.getSnapshot,
|
|
310
|
+
viewerPresentationRegistry.getSnapshot,
|
|
311
|
+
)
|
|
312
|
+
const exportablePresentations = useMemo(
|
|
313
|
+
() =>
|
|
314
|
+
registeredPresentations.filter(
|
|
315
|
+
(contribution) =>
|
|
316
|
+
contribution.staticExport &&
|
|
317
|
+
(!contribution.pluginId || installedPlugins.includes(contribution.pluginId)),
|
|
318
|
+
),
|
|
319
|
+
[installedPlugins, registeredPresentations],
|
|
320
|
+
)
|
|
199
321
|
const sceneGraphValue = useMemo(
|
|
200
322
|
() => buildSceneGraphValue(nodes as Record<string, SceneNode>, rootNodeIds),
|
|
201
323
|
[nodes, rootNodeIds],
|
|
@@ -211,10 +333,22 @@ export function SettingsPanel({
|
|
|
211
333
|
}
|
|
212
334
|
}, [])
|
|
213
335
|
|
|
336
|
+
useEffect(
|
|
337
|
+
() => () => {
|
|
338
|
+
if (copyResetTimeoutRef.current !== null) {
|
|
339
|
+
window.clearTimeout(copyResetTimeoutRef.current)
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
[],
|
|
343
|
+
)
|
|
344
|
+
|
|
214
345
|
const isLocalProject = false // Props-based; only show cloud sections when projectId provided
|
|
215
346
|
|
|
216
347
|
const handleSaveBuild = () => {
|
|
217
|
-
|
|
348
|
+
// Materials ride along: nodes reference them by `scene:<id>` slot
|
|
349
|
+
// refs, so a save without the table produces a file whose custom
|
|
350
|
+
// finishes revert to defaults on the very Load Build path below.
|
|
351
|
+
const sceneData = { nodes, rootNodeIds, installedPlugins, materials, collections }
|
|
218
352
|
const json = JSON.stringify(sceneData, null, 2)
|
|
219
353
|
const blob = new Blob([json], { type: 'application/json' })
|
|
220
354
|
const url = URL.createObjectURL(blob)
|
|
@@ -270,16 +404,17 @@ export function SettingsPanel({
|
|
|
270
404
|
e.target.value = ''
|
|
271
405
|
}
|
|
272
406
|
|
|
273
|
-
const handleConfirmImport = (parsed: {
|
|
274
|
-
nodes: Record<string, unknown>
|
|
275
|
-
rootNodeIds: string[]
|
|
276
|
-
installedPlugins?: string[]
|
|
277
|
-
}) => {
|
|
407
|
+
const handleConfirmImport = (parsed: ParsedBuildJson) => {
|
|
278
408
|
const currentScene = useScene.getState()
|
|
279
409
|
setScene(
|
|
280
410
|
parsed.nodes as Parameters<typeof setScene>[0],
|
|
281
411
|
parsed.rootNodeIds as Parameters<typeof setScene>[1],
|
|
282
412
|
{
|
|
413
|
+
// Without this, every `scene:<id>` slot ref in the imported file
|
|
414
|
+
// pointed at a material that no longer existed — custom finishes
|
|
415
|
+
// silently reverted to defaults on import.
|
|
416
|
+
materials: parsed.materials,
|
|
417
|
+
collections: parsed.collections,
|
|
283
418
|
installedPlugins: parsed.installedPlugins ?? currentScene.installedPlugins,
|
|
284
419
|
hasExplicitPluginInstallState:
|
|
285
420
|
parsed.installedPlugins !== undefined || currentScene.hasExplicitPluginInstallState,
|
|
@@ -310,6 +445,25 @@ export function SettingsPanel({
|
|
|
310
445
|
setTimeout(() => setIsGeneratingThumbnail(false), 3000)
|
|
311
446
|
}
|
|
312
447
|
|
|
448
|
+
const handleCopyProjectId = async () => {
|
|
449
|
+
if (!projectId) return
|
|
450
|
+
if (copyResetTimeoutRef.current !== null) {
|
|
451
|
+
window.clearTimeout(copyResetTimeoutRef.current)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
await navigator.clipboard.writeText(projectId)
|
|
456
|
+
setProjectIdCopyState('copied')
|
|
457
|
+
} catch {
|
|
458
|
+
setProjectIdCopyState('error')
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
copyResetTimeoutRef.current = window.setTimeout(() => {
|
|
462
|
+
setProjectIdCopyState('idle')
|
|
463
|
+
copyResetTimeoutRef.current = null
|
|
464
|
+
}, 2000)
|
|
465
|
+
}
|
|
466
|
+
|
|
313
467
|
const handleVisibilityChange = async (
|
|
314
468
|
field: 'isPrivate' | 'showScansPublic' | 'showGuidesPublic',
|
|
315
469
|
value: boolean,
|
|
@@ -317,8 +471,161 @@ export function SettingsPanel({
|
|
|
317
471
|
await onVisibilityChange?.(field, value)
|
|
318
472
|
}
|
|
319
473
|
|
|
474
|
+
const handleNodeTypeInclusion = useCallback((type: string, included: boolean) => {
|
|
475
|
+
setExcludedNodeTypes((current) => {
|
|
476
|
+
const isExcluded = current.includes(type)
|
|
477
|
+
if (included) {
|
|
478
|
+
return isExcluded ? current.filter((excludedType) => excludedType !== type) : current
|
|
479
|
+
}
|
|
480
|
+
return isExcluded ? current : [...current, type]
|
|
481
|
+
})
|
|
482
|
+
}, [])
|
|
483
|
+
const handlePresentationInclusion = useCallback((id: string, included: boolean) => {
|
|
484
|
+
setIncludedPresentationIds((current) => {
|
|
485
|
+
const isIncluded = current.includes(id)
|
|
486
|
+
if (included) return isIncluded ? current : [...current, id]
|
|
487
|
+
return isIncluded ? current.filter((includedId) => includedId !== id) : current
|
|
488
|
+
})
|
|
489
|
+
}, [])
|
|
490
|
+
|
|
491
|
+
const handleModelExport = async (format: ModelExportFormat, label: string) => {
|
|
492
|
+
if (!modelExport || activeModelExport || useSendToApp.getState().step) return
|
|
493
|
+
|
|
494
|
+
setActiveModelExport(format)
|
|
495
|
+
setModelExportError(null)
|
|
496
|
+
setModelExportWarning(null)
|
|
497
|
+
try {
|
|
498
|
+
const artifact = await modelExport(format, {
|
|
499
|
+
onlyVisible: exportOnlyVisible,
|
|
500
|
+
excludedNodeTypes,
|
|
501
|
+
includedPresentationIds:
|
|
502
|
+
format === 'glb' || format === 'usdz'
|
|
503
|
+
? includedPresentationIds.filter((id) =>
|
|
504
|
+
exportablePresentations.some((contribution) => contribution.id === id),
|
|
505
|
+
)
|
|
506
|
+
: [],
|
|
507
|
+
})
|
|
508
|
+
if (!artifact) {
|
|
509
|
+
throw new Error('Model export did not produce a file')
|
|
510
|
+
}
|
|
511
|
+
if (artifact.warnings?.length) setModelExportWarning(artifact.warnings.join(' '))
|
|
512
|
+
} catch (error) {
|
|
513
|
+
setModelExportError(
|
|
514
|
+
error instanceof Error ? error.message : `Couldn’t export ${label}. Try again.`,
|
|
515
|
+
)
|
|
516
|
+
} finally {
|
|
517
|
+
setActiveModelExport(null)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const handleSendToBlender = async () => {
|
|
522
|
+
if (!modelExport || activeModelExport || useSendToApp.getState().step) return
|
|
523
|
+
|
|
524
|
+
setSendToBlenderMessage(null)
|
|
525
|
+
setSendToBlenderStep('probing')
|
|
526
|
+
try {
|
|
527
|
+
const probe = await probeLocalApp()
|
|
528
|
+
if (probe.status === 'unreachable') {
|
|
529
|
+
setSendToBlenderMessage({
|
|
530
|
+
tone: 'error',
|
|
531
|
+
text: 'Blender isn’t listening. Open Blender with the Pascal add-on installed and enabled, then try again.',
|
|
532
|
+
})
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
if (probe.status === 'refused') {
|
|
536
|
+
setSendToBlenderMessage({
|
|
537
|
+
tone: 'error',
|
|
538
|
+
text: `Blender is open but hasn’t allowed ${window.location.origin} yet. In Blender, open the Pascal tab in the 3D viewport sidebar (N) and click Allow.`,
|
|
539
|
+
})
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
setSendToBlenderStep('exporting')
|
|
544
|
+
const artifact = await modelExport('glb', {
|
|
545
|
+
onlyVisible: exportOnlyVisible,
|
|
546
|
+
excludedNodeTypes,
|
|
547
|
+
includedPresentationIds: includedPresentationIds.filter((id) =>
|
|
548
|
+
exportablePresentations.some((contribution) => contribution.id === id),
|
|
549
|
+
),
|
|
550
|
+
download: false,
|
|
551
|
+
})
|
|
552
|
+
if (!artifact) throw new Error('Model export did not produce a file')
|
|
553
|
+
|
|
554
|
+
setSendToBlenderStep('sending')
|
|
555
|
+
const queued = await sendGlbToLocalApp(probe.base, artifact.blob, {
|
|
556
|
+
name: projectName,
|
|
557
|
+
projectId,
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
setSendToBlenderStep('importing')
|
|
561
|
+
const done = await waitForLocalImport(probe.base, queued.id)
|
|
562
|
+
setSendToBlenderMessage({
|
|
563
|
+
tone: 'info',
|
|
564
|
+
text: done.summary ? `Sent to Blender. ${done.summary}.` : 'Sent to Blender.',
|
|
565
|
+
})
|
|
566
|
+
} catch (error) {
|
|
567
|
+
const text =
|
|
568
|
+
error instanceof LocalAppError || error instanceof Error
|
|
569
|
+
? error.message
|
|
570
|
+
: 'Couldn’t send the scene to Blender. Try again.'
|
|
571
|
+
setSendToBlenderMessage({ tone: 'error', text })
|
|
572
|
+
} finally {
|
|
573
|
+
setSendToBlenderStep(null)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const handleFloorplanExport = async (scope: FloorplanExportScope) => {
|
|
578
|
+
if (activeFloorplanExport) return
|
|
579
|
+
|
|
580
|
+
setActiveFloorplanExport(scope)
|
|
581
|
+
setFloorplanExportError(null)
|
|
582
|
+
try {
|
|
583
|
+
await exportFloorplanPdf(scope)
|
|
584
|
+
} catch (error) {
|
|
585
|
+
setFloorplanExportError(
|
|
586
|
+
`Couldn’t export the floor plan. ${error instanceof Error ? error.message : 'Try again.'}`,
|
|
587
|
+
)
|
|
588
|
+
} finally {
|
|
589
|
+
setActiveFloorplanExport(null)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
320
593
|
return (
|
|
321
|
-
<div className="
|
|
594
|
+
<div className="subtle-scrollbar min-h-0 flex-1 space-y-6 overflow-x-hidden overflow-y-auto overscroll-contain p-3">
|
|
595
|
+
{projectId && (
|
|
596
|
+
<div className="space-y-2">
|
|
597
|
+
<label className="font-medium text-muted-foreground text-xs uppercase">Project</label>
|
|
598
|
+
<div className="font-medium text-sm">Project ID</div>
|
|
599
|
+
<div className="flex items-center gap-2">
|
|
600
|
+
<Input
|
|
601
|
+
aria-label="Project ID"
|
|
602
|
+
className="font-mono text-xs"
|
|
603
|
+
readOnly
|
|
604
|
+
value={projectId}
|
|
605
|
+
/>
|
|
606
|
+
<Button
|
|
607
|
+
aria-label={projectIdCopyState === 'copied' ? 'Project ID copied' : 'Copy project ID'}
|
|
608
|
+
className="rounded-full"
|
|
609
|
+
onClick={() => void handleCopyProjectId()}
|
|
610
|
+
size="sm"
|
|
611
|
+
type="button"
|
|
612
|
+
variant="outline"
|
|
613
|
+
>
|
|
614
|
+
{projectIdCopyState === 'copied' ? (
|
|
615
|
+
<Check className="size-3.5" />
|
|
616
|
+
) : (
|
|
617
|
+
<Copy className="size-3.5" />
|
|
618
|
+
)}
|
|
619
|
+
{projectIdCopyState === 'copied'
|
|
620
|
+
? 'Copied'
|
|
621
|
+
: projectIdCopyState === 'error'
|
|
622
|
+
? 'Try again'
|
|
623
|
+
: 'Copy'}
|
|
624
|
+
</Button>
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
)}
|
|
628
|
+
|
|
322
629
|
{/* Visibility Section (only for cloud projects) */}
|
|
323
630
|
{projectId && !isLocalProject && (
|
|
324
631
|
<div className="space-y-3">
|
|
@@ -331,6 +638,7 @@ export function SettingsPanel({
|
|
|
331
638
|
</div>
|
|
332
639
|
</div>
|
|
333
640
|
<Switch
|
|
641
|
+
aria-label="Make project public"
|
|
334
642
|
checked={!(projectVisibility?.isPrivate ?? false)}
|
|
335
643
|
onCheckedChange={(checked) => handleVisibilityChange('isPrivate', !checked)}
|
|
336
644
|
/>
|
|
@@ -341,6 +649,7 @@ export function SettingsPanel({
|
|
|
341
649
|
<div className="text-muted-foreground text-xs">Visible to public viewers</div>
|
|
342
650
|
</div>
|
|
343
651
|
<Switch
|
|
652
|
+
aria-label="Show 3D scans to public viewers"
|
|
344
653
|
checked={projectVisibility?.showScansPublic ?? true}
|
|
345
654
|
onCheckedChange={(checked) => handleVisibilityChange('showScansPublic', checked)}
|
|
346
655
|
/>
|
|
@@ -351,6 +660,7 @@ export function SettingsPanel({
|
|
|
351
660
|
<div className="text-muted-foreground text-xs">Visible to public viewers</div>
|
|
352
661
|
</div>
|
|
353
662
|
<Switch
|
|
663
|
+
aria-label="Show floorplans to public viewers"
|
|
354
664
|
checked={projectVisibility?.showGuidesPublic ?? true}
|
|
355
665
|
onCheckedChange={(checked) => handleVisibilityChange('showGuidesPublic', checked)}
|
|
356
666
|
/>
|
|
@@ -361,6 +671,7 @@ export function SettingsPanel({
|
|
|
361
671
|
<div className="text-muted-foreground text-xs">Cast shadows from lights</div>
|
|
362
672
|
</div>
|
|
363
673
|
<Switch
|
|
674
|
+
aria-label="Enable shadows"
|
|
364
675
|
checked={shadows}
|
|
365
676
|
onCheckedChange={(checked) => useViewer.getState().setShadows(checked)}
|
|
366
677
|
/>
|
|
@@ -374,30 +685,176 @@ export function SettingsPanel({
|
|
|
374
685
|
|
|
375
686
|
<div className="space-y-2">
|
|
376
687
|
<div className="font-medium text-muted-foreground text-xs">3D model</div>
|
|
377
|
-
<
|
|
378
|
-
className="
|
|
379
|
-
|
|
380
|
-
|
|
688
|
+
<details
|
|
689
|
+
className="group"
|
|
690
|
+
onKeyDownCapture={(event) => {
|
|
691
|
+
// Keep Space available to the disclosure and switches, not canvas panning.
|
|
692
|
+
if (event.code === 'Space') event.stopPropagation()
|
|
693
|
+
}}
|
|
381
694
|
>
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
className="
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
695
|
+
<summary className="flex min-h-10 cursor-pointer list-none items-center justify-between rounded-md border px-3 text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring">
|
|
696
|
+
Export options
|
|
697
|
+
<ChevronDown aria-hidden="true" className="size-4 group-open:rotate-180" />
|
|
698
|
+
</summary>
|
|
699
|
+
<div className="space-y-3 pt-3">
|
|
700
|
+
<div className="flex items-center justify-between gap-4 rounded-md border p-3">
|
|
701
|
+
<div className="min-w-0">
|
|
702
|
+
<label className="font-medium text-sm" htmlFor={visibleOnlySwitchId}>
|
|
703
|
+
Visible nodes only
|
|
704
|
+
</label>
|
|
705
|
+
<div className="text-muted-foreground text-xs">
|
|
706
|
+
Exclude hidden furniture and other hidden scene nodes
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
<Switch
|
|
710
|
+
aria-label="Export visible nodes only"
|
|
711
|
+
checked={exportOnlyVisible}
|
|
712
|
+
id={visibleOnlySwitchId}
|
|
713
|
+
onCheckedChange={setExportOnlyVisible}
|
|
714
|
+
/>
|
|
715
|
+
</div>
|
|
716
|
+
|
|
717
|
+
<fieldset className="space-y-2 rounded-md border p-3">
|
|
718
|
+
<legend className="px-1 font-medium text-sm">Include in file</legend>
|
|
719
|
+
<p className="text-muted-foreground text-xs">
|
|
720
|
+
Choose which procedural content is baked into model files. GLB and USDZ use the
|
|
721
|
+
textured portable path; STL and OBJ remain geometry-only.
|
|
722
|
+
</p>
|
|
723
|
+
{exportableNodeTypes.length > 0 ? (
|
|
724
|
+
<div className="space-y-2 pt-1">
|
|
725
|
+
{exportableNodeTypes.map(({ type, label, supportsGeometryOnly }, index) => {
|
|
726
|
+
const switchId = `${includeNodeTypeIdPrefix}-${index}`
|
|
727
|
+
return (
|
|
728
|
+
<div className="flex items-center justify-between gap-4" key={type}>
|
|
729
|
+
<label className="min-w-0 font-medium text-sm" htmlFor={switchId}>
|
|
730
|
+
{label}
|
|
731
|
+
{!supportsGeometryOnly && (
|
|
732
|
+
<span className="text-muted-foreground text-xs"> (GLB/USDZ only)</span>
|
|
733
|
+
)}
|
|
734
|
+
</label>
|
|
735
|
+
<Switch
|
|
736
|
+
aria-label={`Include ${label} in model files`}
|
|
737
|
+
checked={!excludedNodeTypes.includes(type)}
|
|
738
|
+
id={switchId}
|
|
739
|
+
onCheckedChange={(included) => handleNodeTypeInclusion(type, included)}
|
|
740
|
+
/>
|
|
741
|
+
</div>
|
|
742
|
+
)
|
|
743
|
+
})}
|
|
744
|
+
</div>
|
|
745
|
+
) : (
|
|
746
|
+
<p className="text-muted-foreground text-xs">
|
|
747
|
+
No optional procedural content is present.
|
|
748
|
+
</p>
|
|
749
|
+
)}
|
|
750
|
+
<p className="text-muted-foreground text-xs">
|
|
751
|
+
Viewer surroundings are excluded unless selected separately below.
|
|
752
|
+
</p>
|
|
753
|
+
</fieldset>
|
|
754
|
+
{exportablePresentations.length > 0 ? (
|
|
755
|
+
<fieldset className="space-y-2 rounded-md border p-3">
|
|
756
|
+
<legend className="px-1 font-medium text-sm">Viewer surroundings</legend>
|
|
757
|
+
<p className="text-muted-foreground text-xs">
|
|
758
|
+
Optional static surroundings are included only in GLB and USDZ.
|
|
759
|
+
</p>
|
|
760
|
+
<div className="space-y-2 pt-1">
|
|
761
|
+
{exportablePresentations.map((contribution, index) => {
|
|
762
|
+
const switchId = `${includePresentationIdPrefix}-${index}`
|
|
763
|
+
const label = contribution.staticExport!.label
|
|
764
|
+
return (
|
|
765
|
+
<div
|
|
766
|
+
className="flex items-center justify-between gap-4"
|
|
767
|
+
key={contribution.id}
|
|
768
|
+
>
|
|
769
|
+
<label className="min-w-0 font-medium text-sm" htmlFor={switchId}>
|
|
770
|
+
{label}
|
|
771
|
+
</label>
|
|
772
|
+
<Switch
|
|
773
|
+
aria-label={`Include ${label} in GLB and USDZ`}
|
|
774
|
+
checked={includedPresentationIds.includes(contribution.id)}
|
|
775
|
+
id={switchId}
|
|
776
|
+
onCheckedChange={(included) =>
|
|
777
|
+
handlePresentationInclusion(contribution.id, included)
|
|
778
|
+
}
|
|
779
|
+
/>
|
|
780
|
+
</div>
|
|
781
|
+
)
|
|
782
|
+
})}
|
|
783
|
+
</div>
|
|
784
|
+
</fieldset>
|
|
785
|
+
) : null}
|
|
786
|
+
</div>
|
|
787
|
+
</details>
|
|
788
|
+
|
|
789
|
+
{MODEL_EXPORT_FORMATS.map(({ format, label }) => {
|
|
790
|
+
const isActive = activeModelExport === format
|
|
791
|
+
return (
|
|
792
|
+
<Button
|
|
793
|
+
aria-busy={isActive}
|
|
794
|
+
className="w-full justify-start gap-2"
|
|
795
|
+
disabled={activeModelExport !== null || sendToBlenderStep !== null || !modelExport}
|
|
796
|
+
key={format}
|
|
797
|
+
onClick={() => void handleModelExport(format, label)}
|
|
798
|
+
variant="outline"
|
|
799
|
+
>
|
|
800
|
+
<Download aria-hidden="true" className="size-4" />
|
|
801
|
+
{isActive ? `Exporting ${label}…` : `Export ${label}`}
|
|
802
|
+
</Button>
|
|
803
|
+
)
|
|
804
|
+
})}
|
|
805
|
+
|
|
393
806
|
<Button
|
|
807
|
+
aria-busy={sendToBlenderStep !== null}
|
|
394
808
|
className="w-full justify-start gap-2"
|
|
395
|
-
|
|
809
|
+
disabled={activeModelExport !== null || sendToBlenderStep !== null || !modelExport}
|
|
810
|
+
onClick={() => void handleSendToBlender()}
|
|
396
811
|
variant="outline"
|
|
397
812
|
>
|
|
398
|
-
<
|
|
399
|
-
|
|
813
|
+
<Send aria-hidden="true" className="size-4" />
|
|
814
|
+
{sendToBlenderStep ? SEND_TO_BLENDER_STEP_LABEL[sendToBlenderStep] : 'Send to Blender'}
|
|
400
815
|
</Button>
|
|
816
|
+
<p className="text-muted-foreground text-xs">
|
|
817
|
+
Needs the{' '}
|
|
818
|
+
<a
|
|
819
|
+
className="underline underline-offset-2"
|
|
820
|
+
href={BLENDER_ADDON_URL}
|
|
821
|
+
rel="noreferrer"
|
|
822
|
+
target="_blank"
|
|
823
|
+
>
|
|
824
|
+
Pascal add-on for Blender
|
|
825
|
+
</a>{' '}
|
|
826
|
+
running in an open Blender.
|
|
827
|
+
</p>
|
|
828
|
+
{sendToBlenderMessage ? (
|
|
829
|
+
<p
|
|
830
|
+
className={
|
|
831
|
+
sendToBlenderMessage.tone === 'error'
|
|
832
|
+
? 'text-destructive text-xs'
|
|
833
|
+
: 'text-foreground text-xs'
|
|
834
|
+
}
|
|
835
|
+
role={sendToBlenderMessage.tone === 'error' ? 'alert' : 'status'}
|
|
836
|
+
>
|
|
837
|
+
{sendToBlenderMessage.text}
|
|
838
|
+
</p>
|
|
839
|
+
) : null}
|
|
840
|
+
|
|
841
|
+
{activeModelExport ? (
|
|
842
|
+
<p className="text-muted-foreground text-xs" role="status">
|
|
843
|
+
Preparing {activeModelExport.toUpperCase()} file…
|
|
844
|
+
</p>
|
|
845
|
+
) : null}
|
|
846
|
+
{modelExportError ? (
|
|
847
|
+
<p className="text-destructive text-xs" role="alert">
|
|
848
|
+
{modelExportError}
|
|
849
|
+
</p>
|
|
850
|
+
) : null}
|
|
851
|
+
{modelExportWarning ? (
|
|
852
|
+
<p className="text-foreground text-xs" role="status">
|
|
853
|
+
Warning: {modelExportWarning}
|
|
854
|
+
</p>
|
|
855
|
+
) : null}
|
|
856
|
+
|
|
857
|
+
<PrintExportButton onlyVisible={exportOnlyVisible} />
|
|
401
858
|
</div>
|
|
402
859
|
|
|
403
860
|
<div className="space-y-2">
|
|
@@ -406,21 +863,35 @@ export function SettingsPanel({
|
|
|
406
863
|
<span>{floorplanMode === 'default' ? 'Default mode' : 'Expert mode'}</span>
|
|
407
864
|
</div>
|
|
408
865
|
<Button
|
|
866
|
+
aria-busy={activeFloorplanExport === 'full'}
|
|
409
867
|
className="w-full justify-start gap-2"
|
|
410
|
-
|
|
868
|
+
disabled={activeFloorplanExport !== null}
|
|
869
|
+
onClick={() => void handleFloorplanExport('full')}
|
|
411
870
|
variant="outline"
|
|
412
871
|
>
|
|
413
872
|
<MapIcon className="size-4" />
|
|
414
873
|
Full floor plan
|
|
415
874
|
</Button>
|
|
416
875
|
<Button
|
|
876
|
+
aria-busy={activeFloorplanExport === 'structure'}
|
|
417
877
|
className="w-full justify-start gap-2"
|
|
418
|
-
|
|
878
|
+
disabled={activeFloorplanExport !== null}
|
|
879
|
+
onClick={() => void handleFloorplanExport('structure')}
|
|
419
880
|
variant="outline"
|
|
420
881
|
>
|
|
421
882
|
<MapIcon className="size-4" />
|
|
422
883
|
Structure only
|
|
423
884
|
</Button>
|
|
885
|
+
{activeFloorplanExport ? (
|
|
886
|
+
<p className="text-muted-foreground text-xs" role="status">
|
|
887
|
+
Preparing floor-plan PDF…
|
|
888
|
+
</p>
|
|
889
|
+
) : null}
|
|
890
|
+
{floorplanExportError ? (
|
|
891
|
+
<p className="break-words text-destructive text-xs" role="alert">
|
|
892
|
+
{floorplanExportError}
|
|
893
|
+
</p>
|
|
894
|
+
) : null}
|
|
424
895
|
</div>
|
|
425
896
|
</div>
|
|
426
897
|
|