@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
|
@@ -1,21 +1,41 @@
|
|
|
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
|
+
Trash2,
|
|
22
|
+
Upload,
|
|
23
|
+
} from 'lucide-react'
|
|
10
24
|
import {
|
|
11
25
|
type KeyboardEvent,
|
|
12
26
|
type SyntheticEvent,
|
|
13
27
|
useCallback,
|
|
28
|
+
useEffect,
|
|
29
|
+
useId,
|
|
14
30
|
useMemo,
|
|
15
31
|
useRef,
|
|
16
32
|
useState,
|
|
33
|
+
useSyncExternalStore,
|
|
17
34
|
} from 'react'
|
|
18
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
exportFloorplanPdf,
|
|
37
|
+
type FloorplanExportScope,
|
|
38
|
+
} from '../../../../../lib/floorplan/floorplan-export'
|
|
19
39
|
import { Button } from './../../../../../components/ui/primitives/button'
|
|
20
40
|
import {
|
|
21
41
|
Dialog,
|
|
@@ -23,12 +43,14 @@ import {
|
|
|
23
43
|
DialogTitle,
|
|
24
44
|
DialogTrigger,
|
|
25
45
|
} from './../../../../../components/ui/primitives/dialog'
|
|
46
|
+
import { Input } from './../../../../../components/ui/primitives/input'
|
|
26
47
|
import { Switch } from './../../../../../components/ui/primitives/switch'
|
|
27
48
|
import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
|
|
28
49
|
import useFloorplanMode from './../../../../../store/use-floorplan-mode'
|
|
29
50
|
import { AudioSettingsDialog } from './audio-settings-dialog'
|
|
30
51
|
import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog'
|
|
31
52
|
import { LoadBuildDialog, type PendingImport } from './load-build-dialog'
|
|
53
|
+
import { PrintExportButton } from './print-export-button'
|
|
32
54
|
|
|
33
55
|
type SceneNode = Record<string, unknown> & {
|
|
34
56
|
id?: unknown
|
|
@@ -53,6 +75,21 @@ type SceneGraphValue = {
|
|
|
53
75
|
detachedNodes?: SceneGraphNode[]
|
|
54
76
|
}
|
|
55
77
|
|
|
78
|
+
const MODEL_EXPORT_FORMATS = [
|
|
79
|
+
{ format: 'glb', label: 'GLB' },
|
|
80
|
+
{ format: 'usdz', label: 'USDZ' },
|
|
81
|
+
{ format: 'stl', label: 'STL' },
|
|
82
|
+
{ format: 'obj', label: 'OBJ' },
|
|
83
|
+
] as const
|
|
84
|
+
|
|
85
|
+
type ModelExportFormat = (typeof MODEL_EXPORT_FORMATS)[number]['format']
|
|
86
|
+
|
|
87
|
+
type ExportableNodeType = {
|
|
88
|
+
type: string
|
|
89
|
+
label: string
|
|
90
|
+
supportsGeometryOnly: boolean
|
|
91
|
+
}
|
|
92
|
+
|
|
56
93
|
const isSceneNode = (value: unknown): value is SceneNode => {
|
|
57
94
|
return (
|
|
58
95
|
typeof value === 'object' &&
|
|
@@ -184,18 +221,78 @@ export function SettingsPanel({
|
|
|
184
221
|
onVisibilityChange,
|
|
185
222
|
}: SettingsPanelProps = {}) {
|
|
186
223
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
224
|
+
const copyResetTimeoutRef = useRef<number | null>(null)
|
|
187
225
|
const nodes = useScene((state) => state.nodes)
|
|
188
226
|
const rootNodeIds = useScene((state) => state.rootNodeIds)
|
|
189
227
|
const installedPlugins = useScene((state) => state.installedPlugins)
|
|
228
|
+
const materials = useScene((state) => state.materials)
|
|
190
229
|
const setScene = useScene((state) => state.setScene)
|
|
191
230
|
const clearScene = useScene((state) => state.clearScene)
|
|
192
231
|
const resetSelection = useViewer((state) => state.resetSelection)
|
|
193
|
-
const
|
|
232
|
+
const modelExport = useEditor((state) => state.modelExport)
|
|
194
233
|
const shadows = useViewer((state) => state.shadows)
|
|
195
234
|
const setPhase = useEditor((state) => state.setPhase)
|
|
196
235
|
const floorplanMode = useFloorplanMode((state) => state.mode)
|
|
236
|
+
const registryVersion = useRegistryVersion()
|
|
237
|
+
const visibleOnlySwitchId = useId()
|
|
238
|
+
const includeNodeTypeIdPrefix = useId()
|
|
239
|
+
const includePresentationIdPrefix = useId()
|
|
197
240
|
const [isGeneratingThumbnail, setIsGeneratingThumbnail] = useState(false)
|
|
241
|
+
const [exportOnlyVisible, setExportOnlyVisible] = useState(true)
|
|
242
|
+
const [excludedNodeTypes, setExcludedNodeTypes] = useState<string[]>([])
|
|
243
|
+
const [includedPresentationIds, setIncludedPresentationIds] = useState<string[]>([])
|
|
244
|
+
const [activeModelExport, setActiveModelExport] = useState<ModelExportFormat | null>(null)
|
|
245
|
+
const [modelExportError, setModelExportError] = useState<string | null>(null)
|
|
246
|
+
const [modelExportWarning, setModelExportWarning] = useState<string | null>(null)
|
|
247
|
+
const [activeFloorplanExport, setActiveFloorplanExport] = useState<FloorplanExportScope | null>(
|
|
248
|
+
null,
|
|
249
|
+
)
|
|
250
|
+
const [floorplanExportError, setFloorplanExportError] = useState<string | null>(null)
|
|
198
251
|
const [pendingImport, setPendingImport] = useState<PendingImport | null>(null)
|
|
252
|
+
const [projectIdCopyState, setProjectIdCopyState] = useState<'idle' | 'copied' | 'error'>('idle')
|
|
253
|
+
const exportableNodeTypes = useMemo(() => {
|
|
254
|
+
void registryVersion
|
|
255
|
+
const uniqueTypes = new Set(Object.values(nodes).map((node) => node.type))
|
|
256
|
+
const options: ExportableNodeType[] = []
|
|
257
|
+
|
|
258
|
+
for (const type of uniqueTypes) {
|
|
259
|
+
const definition = nodeRegistry.get(type)
|
|
260
|
+
if (
|
|
261
|
+
!(
|
|
262
|
+
definition?.bakeGeometry ||
|
|
263
|
+
definition?.bakeGeometryAsync ||
|
|
264
|
+
definition?.bake === 'replace'
|
|
265
|
+
) ||
|
|
266
|
+
!isNodeKindEnabled(type, installedPlugins)
|
|
267
|
+
) {
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
options.push({
|
|
271
|
+
type,
|
|
272
|
+
label: definition.presentation?.label ?? type,
|
|
273
|
+
supportsGeometryOnly: !definition.bakeGeometryAsync || Boolean(definition.bakeGeometry),
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return options.sort((a, b) => {
|
|
278
|
+
const labelOrder = a.label.localeCompare(b.label)
|
|
279
|
+
return labelOrder === 0 ? a.type.localeCompare(b.type) : labelOrder
|
|
280
|
+
})
|
|
281
|
+
}, [installedPlugins, nodes, registryVersion])
|
|
282
|
+
const registeredPresentations = useSyncExternalStore(
|
|
283
|
+
viewerPresentationRegistry.subscribe,
|
|
284
|
+
viewerPresentationRegistry.getSnapshot,
|
|
285
|
+
viewerPresentationRegistry.getSnapshot,
|
|
286
|
+
)
|
|
287
|
+
const exportablePresentations = useMemo(
|
|
288
|
+
() =>
|
|
289
|
+
registeredPresentations.filter(
|
|
290
|
+
(contribution) =>
|
|
291
|
+
contribution.staticExport &&
|
|
292
|
+
(!contribution.pluginId || installedPlugins.includes(contribution.pluginId)),
|
|
293
|
+
),
|
|
294
|
+
[installedPlugins, registeredPresentations],
|
|
295
|
+
)
|
|
199
296
|
const sceneGraphValue = useMemo(
|
|
200
297
|
() => buildSceneGraphValue(nodes as Record<string, SceneNode>, rootNodeIds),
|
|
201
298
|
[nodes, rootNodeIds],
|
|
@@ -211,10 +308,22 @@ export function SettingsPanel({
|
|
|
211
308
|
}
|
|
212
309
|
}, [])
|
|
213
310
|
|
|
311
|
+
useEffect(
|
|
312
|
+
() => () => {
|
|
313
|
+
if (copyResetTimeoutRef.current !== null) {
|
|
314
|
+
window.clearTimeout(copyResetTimeoutRef.current)
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
[],
|
|
318
|
+
)
|
|
319
|
+
|
|
214
320
|
const isLocalProject = false // Props-based; only show cloud sections when projectId provided
|
|
215
321
|
|
|
216
322
|
const handleSaveBuild = () => {
|
|
217
|
-
|
|
323
|
+
// Materials ride along: nodes reference them by `scene:<id>` slot
|
|
324
|
+
// refs, so a save without the table produces a file whose custom
|
|
325
|
+
// finishes revert to defaults on the very Load Build path below.
|
|
326
|
+
const sceneData = { nodes, rootNodeIds, installedPlugins, materials }
|
|
218
327
|
const json = JSON.stringify(sceneData, null, 2)
|
|
219
328
|
const blob = new Blob([json], { type: 'application/json' })
|
|
220
329
|
const url = URL.createObjectURL(blob)
|
|
@@ -270,16 +379,16 @@ export function SettingsPanel({
|
|
|
270
379
|
e.target.value = ''
|
|
271
380
|
}
|
|
272
381
|
|
|
273
|
-
const handleConfirmImport = (parsed: {
|
|
274
|
-
nodes: Record<string, unknown>
|
|
275
|
-
rootNodeIds: string[]
|
|
276
|
-
installedPlugins?: string[]
|
|
277
|
-
}) => {
|
|
382
|
+
const handleConfirmImport = (parsed: ParsedBuildJson) => {
|
|
278
383
|
const currentScene = useScene.getState()
|
|
279
384
|
setScene(
|
|
280
385
|
parsed.nodes as Parameters<typeof setScene>[0],
|
|
281
386
|
parsed.rootNodeIds as Parameters<typeof setScene>[1],
|
|
282
387
|
{
|
|
388
|
+
// Without this, every `scene:<id>` slot ref in the imported file
|
|
389
|
+
// pointed at a material that no longer existed — custom finishes
|
|
390
|
+
// silently reverted to defaults on import.
|
|
391
|
+
materials: parsed.materials,
|
|
283
392
|
installedPlugins: parsed.installedPlugins ?? currentScene.installedPlugins,
|
|
284
393
|
hasExplicitPluginInstallState:
|
|
285
394
|
parsed.installedPlugins !== undefined || currentScene.hasExplicitPluginInstallState,
|
|
@@ -310,6 +419,25 @@ export function SettingsPanel({
|
|
|
310
419
|
setTimeout(() => setIsGeneratingThumbnail(false), 3000)
|
|
311
420
|
}
|
|
312
421
|
|
|
422
|
+
const handleCopyProjectId = async () => {
|
|
423
|
+
if (!projectId) return
|
|
424
|
+
if (copyResetTimeoutRef.current !== null) {
|
|
425
|
+
window.clearTimeout(copyResetTimeoutRef.current)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
await navigator.clipboard.writeText(projectId)
|
|
430
|
+
setProjectIdCopyState('copied')
|
|
431
|
+
} catch {
|
|
432
|
+
setProjectIdCopyState('error')
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
copyResetTimeoutRef.current = window.setTimeout(() => {
|
|
436
|
+
setProjectIdCopyState('idle')
|
|
437
|
+
copyResetTimeoutRef.current = null
|
|
438
|
+
}, 2000)
|
|
439
|
+
}
|
|
440
|
+
|
|
313
441
|
const handleVisibilityChange = async (
|
|
314
442
|
field: 'isPrivate' | 'showScansPublic' | 'showGuidesPublic',
|
|
315
443
|
value: boolean,
|
|
@@ -317,8 +445,105 @@ export function SettingsPanel({
|
|
|
317
445
|
await onVisibilityChange?.(field, value)
|
|
318
446
|
}
|
|
319
447
|
|
|
448
|
+
const handleNodeTypeInclusion = useCallback((type: string, included: boolean) => {
|
|
449
|
+
setExcludedNodeTypes((current) => {
|
|
450
|
+
const isExcluded = current.includes(type)
|
|
451
|
+
if (included) {
|
|
452
|
+
return isExcluded ? current.filter((excludedType) => excludedType !== type) : current
|
|
453
|
+
}
|
|
454
|
+
return isExcluded ? current : [...current, type]
|
|
455
|
+
})
|
|
456
|
+
}, [])
|
|
457
|
+
const handlePresentationInclusion = useCallback((id: string, included: boolean) => {
|
|
458
|
+
setIncludedPresentationIds((current) => {
|
|
459
|
+
const isIncluded = current.includes(id)
|
|
460
|
+
if (included) return isIncluded ? current : [...current, id]
|
|
461
|
+
return isIncluded ? current.filter((includedId) => includedId !== id) : current
|
|
462
|
+
})
|
|
463
|
+
}, [])
|
|
464
|
+
|
|
465
|
+
const handleModelExport = async (format: ModelExportFormat, label: string) => {
|
|
466
|
+
if (!modelExport || activeModelExport) return
|
|
467
|
+
|
|
468
|
+
setActiveModelExport(format)
|
|
469
|
+
setModelExportError(null)
|
|
470
|
+
setModelExportWarning(null)
|
|
471
|
+
try {
|
|
472
|
+
const artifact = await modelExport(format, {
|
|
473
|
+
onlyVisible: exportOnlyVisible,
|
|
474
|
+
excludedNodeTypes,
|
|
475
|
+
includedPresentationIds:
|
|
476
|
+
format === 'glb' || format === 'usdz'
|
|
477
|
+
? includedPresentationIds.filter((id) =>
|
|
478
|
+
exportablePresentations.some((contribution) => contribution.id === id),
|
|
479
|
+
)
|
|
480
|
+
: [],
|
|
481
|
+
})
|
|
482
|
+
if (!artifact) {
|
|
483
|
+
throw new Error('Model export did not produce a file')
|
|
484
|
+
}
|
|
485
|
+
if (artifact.warnings?.length) setModelExportWarning(artifact.warnings.join(' '))
|
|
486
|
+
} catch (error) {
|
|
487
|
+
setModelExportError(
|
|
488
|
+
error instanceof Error ? error.message : `Couldn’t export ${label}. Try again.`,
|
|
489
|
+
)
|
|
490
|
+
} finally {
|
|
491
|
+
setActiveModelExport(null)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const handleFloorplanExport = async (scope: FloorplanExportScope) => {
|
|
496
|
+
if (activeFloorplanExport) return
|
|
497
|
+
|
|
498
|
+
setActiveFloorplanExport(scope)
|
|
499
|
+
setFloorplanExportError(null)
|
|
500
|
+
try {
|
|
501
|
+
await exportFloorplanPdf(scope)
|
|
502
|
+
} catch (error) {
|
|
503
|
+
setFloorplanExportError(
|
|
504
|
+
`Couldn’t export the floor plan. ${error instanceof Error ? error.message : 'Try again.'}`,
|
|
505
|
+
)
|
|
506
|
+
} finally {
|
|
507
|
+
setActiveFloorplanExport(null)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
320
511
|
return (
|
|
321
|
-
<div className="
|
|
512
|
+
<div className="subtle-scrollbar min-h-0 flex-1 space-y-6 overflow-x-hidden overflow-y-auto overscroll-contain p-3">
|
|
513
|
+
{projectId && (
|
|
514
|
+
<div className="space-y-2">
|
|
515
|
+
<label className="font-medium text-muted-foreground text-xs uppercase">Project</label>
|
|
516
|
+
<div className="font-medium text-sm">Project ID</div>
|
|
517
|
+
<div className="flex items-center gap-2">
|
|
518
|
+
<Input
|
|
519
|
+
aria-label="Project ID"
|
|
520
|
+
className="font-mono text-xs"
|
|
521
|
+
readOnly
|
|
522
|
+
value={projectId}
|
|
523
|
+
/>
|
|
524
|
+
<Button
|
|
525
|
+
aria-label={projectIdCopyState === 'copied' ? 'Project ID copied' : 'Copy project ID'}
|
|
526
|
+
className="rounded-full"
|
|
527
|
+
onClick={() => void handleCopyProjectId()}
|
|
528
|
+
size="sm"
|
|
529
|
+
type="button"
|
|
530
|
+
variant="outline"
|
|
531
|
+
>
|
|
532
|
+
{projectIdCopyState === 'copied' ? (
|
|
533
|
+
<Check className="size-3.5" />
|
|
534
|
+
) : (
|
|
535
|
+
<Copy className="size-3.5" />
|
|
536
|
+
)}
|
|
537
|
+
{projectIdCopyState === 'copied'
|
|
538
|
+
? 'Copied'
|
|
539
|
+
: projectIdCopyState === 'error'
|
|
540
|
+
? 'Try again'
|
|
541
|
+
: 'Copy'}
|
|
542
|
+
</Button>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
)}
|
|
546
|
+
|
|
322
547
|
{/* Visibility Section (only for cloud projects) */}
|
|
323
548
|
{projectId && !isLocalProject && (
|
|
324
549
|
<div className="space-y-3">
|
|
@@ -331,6 +556,7 @@ export function SettingsPanel({
|
|
|
331
556
|
</div>
|
|
332
557
|
</div>
|
|
333
558
|
<Switch
|
|
559
|
+
aria-label="Make project public"
|
|
334
560
|
checked={!(projectVisibility?.isPrivate ?? false)}
|
|
335
561
|
onCheckedChange={(checked) => handleVisibilityChange('isPrivate', !checked)}
|
|
336
562
|
/>
|
|
@@ -341,6 +567,7 @@ export function SettingsPanel({
|
|
|
341
567
|
<div className="text-muted-foreground text-xs">Visible to public viewers</div>
|
|
342
568
|
</div>
|
|
343
569
|
<Switch
|
|
570
|
+
aria-label="Show 3D scans to public viewers"
|
|
344
571
|
checked={projectVisibility?.showScansPublic ?? true}
|
|
345
572
|
onCheckedChange={(checked) => handleVisibilityChange('showScansPublic', checked)}
|
|
346
573
|
/>
|
|
@@ -351,6 +578,7 @@ export function SettingsPanel({
|
|
|
351
578
|
<div className="text-muted-foreground text-xs">Visible to public viewers</div>
|
|
352
579
|
</div>
|
|
353
580
|
<Switch
|
|
581
|
+
aria-label="Show floorplans to public viewers"
|
|
354
582
|
checked={projectVisibility?.showGuidesPublic ?? true}
|
|
355
583
|
onCheckedChange={(checked) => handleVisibilityChange('showGuidesPublic', checked)}
|
|
356
584
|
/>
|
|
@@ -361,6 +589,7 @@ export function SettingsPanel({
|
|
|
361
589
|
<div className="text-muted-foreground text-xs">Cast shadows from lights</div>
|
|
362
590
|
</div>
|
|
363
591
|
<Switch
|
|
592
|
+
aria-label="Enable shadows"
|
|
364
593
|
checked={shadows}
|
|
365
594
|
onCheckedChange={(checked) => useViewer.getState().setShadows(checked)}
|
|
366
595
|
/>
|
|
@@ -374,30 +603,141 @@ export function SettingsPanel({
|
|
|
374
603
|
|
|
375
604
|
<div className="space-y-2">
|
|
376
605
|
<div className="font-medium text-muted-foreground text-xs">3D model</div>
|
|
377
|
-
<
|
|
378
|
-
className="
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
Export GLB
|
|
384
|
-
</Button>
|
|
385
|
-
<Button
|
|
386
|
-
className="w-full justify-start gap-2"
|
|
387
|
-
onClick={() => exportScene?.('stl')}
|
|
388
|
-
variant="outline"
|
|
606
|
+
<details
|
|
607
|
+
className="group"
|
|
608
|
+
onKeyDownCapture={(event) => {
|
|
609
|
+
// Keep Space available to the disclosure and switches, not canvas panning.
|
|
610
|
+
if (event.code === 'Space') event.stopPropagation()
|
|
611
|
+
}}
|
|
389
612
|
>
|
|
390
|
-
<
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
className="
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
613
|
+
<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">
|
|
614
|
+
Export options
|
|
615
|
+
<ChevronDown aria-hidden="true" className="size-4 group-open:rotate-180" />
|
|
616
|
+
</summary>
|
|
617
|
+
<div className="space-y-3 pt-3">
|
|
618
|
+
<div className="flex items-center justify-between gap-4 rounded-md border p-3">
|
|
619
|
+
<div className="min-w-0">
|
|
620
|
+
<label className="font-medium text-sm" htmlFor={visibleOnlySwitchId}>
|
|
621
|
+
Visible nodes only
|
|
622
|
+
</label>
|
|
623
|
+
<div className="text-muted-foreground text-xs">
|
|
624
|
+
Exclude hidden furniture and other hidden scene nodes
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
<Switch
|
|
628
|
+
aria-label="Export visible nodes only"
|
|
629
|
+
checked={exportOnlyVisible}
|
|
630
|
+
id={visibleOnlySwitchId}
|
|
631
|
+
onCheckedChange={setExportOnlyVisible}
|
|
632
|
+
/>
|
|
633
|
+
</div>
|
|
634
|
+
|
|
635
|
+
<fieldset className="space-y-2 rounded-md border p-3">
|
|
636
|
+
<legend className="px-1 font-medium text-sm">Include in file</legend>
|
|
637
|
+
<p className="text-muted-foreground text-xs">
|
|
638
|
+
Choose which procedural content is baked into model files. GLB and USDZ use the
|
|
639
|
+
textured portable path; STL and OBJ remain geometry-only.
|
|
640
|
+
</p>
|
|
641
|
+
{exportableNodeTypes.length > 0 ? (
|
|
642
|
+
<div className="space-y-2 pt-1">
|
|
643
|
+
{exportableNodeTypes.map(({ type, label, supportsGeometryOnly }, index) => {
|
|
644
|
+
const switchId = `${includeNodeTypeIdPrefix}-${index}`
|
|
645
|
+
return (
|
|
646
|
+
<div className="flex items-center justify-between gap-4" key={type}>
|
|
647
|
+
<label className="min-w-0 font-medium text-sm" htmlFor={switchId}>
|
|
648
|
+
{label}
|
|
649
|
+
{!supportsGeometryOnly && (
|
|
650
|
+
<span className="text-muted-foreground text-xs"> (GLB/USDZ only)</span>
|
|
651
|
+
)}
|
|
652
|
+
</label>
|
|
653
|
+
<Switch
|
|
654
|
+
aria-label={`Include ${label} in model files`}
|
|
655
|
+
checked={!excludedNodeTypes.includes(type)}
|
|
656
|
+
id={switchId}
|
|
657
|
+
onCheckedChange={(included) => handleNodeTypeInclusion(type, included)}
|
|
658
|
+
/>
|
|
659
|
+
</div>
|
|
660
|
+
)
|
|
661
|
+
})}
|
|
662
|
+
</div>
|
|
663
|
+
) : (
|
|
664
|
+
<p className="text-muted-foreground text-xs">
|
|
665
|
+
No optional procedural content is present.
|
|
666
|
+
</p>
|
|
667
|
+
)}
|
|
668
|
+
<p className="text-muted-foreground text-xs">
|
|
669
|
+
Viewer surroundings are excluded unless selected separately below.
|
|
670
|
+
</p>
|
|
671
|
+
</fieldset>
|
|
672
|
+
{exportablePresentations.length > 0 ? (
|
|
673
|
+
<fieldset className="space-y-2 rounded-md border p-3">
|
|
674
|
+
<legend className="px-1 font-medium text-sm">Viewer surroundings</legend>
|
|
675
|
+
<p className="text-muted-foreground text-xs">
|
|
676
|
+
Optional static surroundings are included only in GLB and USDZ.
|
|
677
|
+
</p>
|
|
678
|
+
<div className="space-y-2 pt-1">
|
|
679
|
+
{exportablePresentations.map((contribution, index) => {
|
|
680
|
+
const switchId = `${includePresentationIdPrefix}-${index}`
|
|
681
|
+
const label = contribution.staticExport!.label
|
|
682
|
+
return (
|
|
683
|
+
<div
|
|
684
|
+
className="flex items-center justify-between gap-4"
|
|
685
|
+
key={contribution.id}
|
|
686
|
+
>
|
|
687
|
+
<label className="min-w-0 font-medium text-sm" htmlFor={switchId}>
|
|
688
|
+
{label}
|
|
689
|
+
</label>
|
|
690
|
+
<Switch
|
|
691
|
+
aria-label={`Include ${label} in GLB and USDZ`}
|
|
692
|
+
checked={includedPresentationIds.includes(contribution.id)}
|
|
693
|
+
id={switchId}
|
|
694
|
+
onCheckedChange={(included) =>
|
|
695
|
+
handlePresentationInclusion(contribution.id, included)
|
|
696
|
+
}
|
|
697
|
+
/>
|
|
698
|
+
</div>
|
|
699
|
+
)
|
|
700
|
+
})}
|
|
701
|
+
</div>
|
|
702
|
+
</fieldset>
|
|
703
|
+
) : null}
|
|
704
|
+
</div>
|
|
705
|
+
</details>
|
|
706
|
+
|
|
707
|
+
{MODEL_EXPORT_FORMATS.map(({ format, label }) => {
|
|
708
|
+
const isActive = activeModelExport === format
|
|
709
|
+
return (
|
|
710
|
+
<Button
|
|
711
|
+
aria-busy={isActive}
|
|
712
|
+
className="w-full justify-start gap-2"
|
|
713
|
+
disabled={activeModelExport !== null || !modelExport}
|
|
714
|
+
key={format}
|
|
715
|
+
onClick={() => void handleModelExport(format, label)}
|
|
716
|
+
variant="outline"
|
|
717
|
+
>
|
|
718
|
+
<Download aria-hidden="true" className="size-4" />
|
|
719
|
+
{isActive ? `Exporting ${label}…` : `Export ${label}`}
|
|
720
|
+
</Button>
|
|
721
|
+
)
|
|
722
|
+
})}
|
|
723
|
+
|
|
724
|
+
{activeModelExport ? (
|
|
725
|
+
<p className="text-muted-foreground text-xs" role="status">
|
|
726
|
+
Preparing {activeModelExport.toUpperCase()} file…
|
|
727
|
+
</p>
|
|
728
|
+
) : null}
|
|
729
|
+
{modelExportError ? (
|
|
730
|
+
<p className="text-destructive text-xs" role="alert">
|
|
731
|
+
{modelExportError}
|
|
732
|
+
</p>
|
|
733
|
+
) : null}
|
|
734
|
+
{modelExportWarning ? (
|
|
735
|
+
<p className="text-foreground text-xs" role="status">
|
|
736
|
+
Warning: {modelExportWarning}
|
|
737
|
+
</p>
|
|
738
|
+
) : null}
|
|
739
|
+
|
|
740
|
+
<PrintExportButton onlyVisible={exportOnlyVisible} />
|
|
401
741
|
</div>
|
|
402
742
|
|
|
403
743
|
<div className="space-y-2">
|
|
@@ -406,21 +746,35 @@ export function SettingsPanel({
|
|
|
406
746
|
<span>{floorplanMode === 'default' ? 'Default mode' : 'Expert mode'}</span>
|
|
407
747
|
</div>
|
|
408
748
|
<Button
|
|
749
|
+
aria-busy={activeFloorplanExport === 'full'}
|
|
409
750
|
className="w-full justify-start gap-2"
|
|
410
|
-
|
|
751
|
+
disabled={activeFloorplanExport !== null}
|
|
752
|
+
onClick={() => void handleFloorplanExport('full')}
|
|
411
753
|
variant="outline"
|
|
412
754
|
>
|
|
413
755
|
<MapIcon className="size-4" />
|
|
414
756
|
Full floor plan
|
|
415
757
|
</Button>
|
|
416
758
|
<Button
|
|
759
|
+
aria-busy={activeFloorplanExport === 'structure'}
|
|
417
760
|
className="w-full justify-start gap-2"
|
|
418
|
-
|
|
761
|
+
disabled={activeFloorplanExport !== null}
|
|
762
|
+
onClick={() => void handleFloorplanExport('structure')}
|
|
419
763
|
variant="outline"
|
|
420
764
|
>
|
|
421
765
|
<MapIcon className="size-4" />
|
|
422
766
|
Structure only
|
|
423
767
|
</Button>
|
|
768
|
+
{activeFloorplanExport ? (
|
|
769
|
+
<p className="text-muted-foreground text-xs" role="status">
|
|
770
|
+
Preparing floor-plan PDF…
|
|
771
|
+
</p>
|
|
772
|
+
) : null}
|
|
773
|
+
{floorplanExportError ? (
|
|
774
|
+
<p className="break-words text-destructive text-xs" role="alert">
|
|
775
|
+
{floorplanExportError}
|
|
776
|
+
</p>
|
|
777
|
+
) : null}
|
|
424
778
|
</div>
|
|
425
779
|
</div>
|
|
426
780
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Keyboard } from 'lucide-react'
|
|
2
|
-
import { useEffect, useState } from 'react'
|
|
3
2
|
import { Button } from './../../../../../components/ui/primitives/button'
|
|
4
3
|
import {
|
|
5
4
|
Dialog,
|
|
@@ -9,7 +8,10 @@ import {
|
|
|
9
8
|
DialogTitle,
|
|
10
9
|
DialogTrigger,
|
|
11
10
|
} from './../../../../../components/ui/primitives/dialog'
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ShortcutToken,
|
|
13
|
+
shortcutDisplayValue,
|
|
14
|
+
} from './../../../../../components/ui/primitives/shortcut-token'
|
|
13
15
|
|
|
14
16
|
type Shortcut = {
|
|
15
17
|
keys: string[]
|
|
@@ -22,14 +24,6 @@ type ShortcutCategory = {
|
|
|
22
24
|
shortcuts: Shortcut[]
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const KEY_DISPLAY_MAP: Record<string, string> = {
|
|
26
|
-
'Arrow Up': '↑',
|
|
27
|
-
'Arrow Down': '↓',
|
|
28
|
-
Esc: '⎋',
|
|
29
|
-
Shift: '⇧',
|
|
30
|
-
Space: '␣',
|
|
31
|
-
}
|
|
32
|
-
|
|
33
27
|
const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
34
28
|
{
|
|
35
29
|
title: 'Editor Navigation',
|
|
@@ -60,10 +54,12 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
60
54
|
{
|
|
61
55
|
keys: ['Esc'],
|
|
62
56
|
action: 'Cancel the active tool and return to Select mode',
|
|
57
|
+
note: 'Mid-draw it cancels only the chain in progress and keeps the tool armed; press it again to leave the tool.',
|
|
63
58
|
},
|
|
64
59
|
{ keys: ['Delete / Backspace'], action: 'Delete selected objects' },
|
|
65
60
|
{ keys: ['Cmd/Ctrl', 'Z'], action: 'Undo' },
|
|
66
61
|
{ keys: ['Cmd/Ctrl', 'Shift', 'Z'], action: 'Redo' },
|
|
62
|
+
{ keys: ['Cmd/Ctrl', 'S'], action: 'Save' },
|
|
67
63
|
],
|
|
68
64
|
},
|
|
69
65
|
{
|
|
@@ -147,6 +143,19 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
147
143
|
{
|
|
148
144
|
title: 'Drawing Tools',
|
|
149
145
|
shortcuts: [
|
|
146
|
+
// Shift and Ctrl each mean one thing held and another tapped, and only
|
|
147
|
+
// the hold was documented — which read as the taps not existing. Both
|
|
148
|
+
// taps are listed first because they are the ones nobody discovers.
|
|
149
|
+
{
|
|
150
|
+
keys: ['Shift'],
|
|
151
|
+
action: 'Cycle the snapping mode',
|
|
152
|
+
note: 'Tap and release without pressing anything else, while a drawing or move gesture is available.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
keys: ['Cmd/Ctrl'],
|
|
156
|
+
action: 'Cycle the grid step: 0.5 m → 0.25 m → 0.1 m → 0.05 m',
|
|
157
|
+
note: 'Tap and release on its own. Use it when the default half-metre grid is too coarse — placing a window, for instance.',
|
|
158
|
+
},
|
|
150
159
|
{
|
|
151
160
|
keys: ['Shift'],
|
|
152
161
|
action: 'Bypass guided snapping and angle constraints',
|
|
@@ -199,25 +208,13 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
199
208
|
},
|
|
200
209
|
]
|
|
201
210
|
|
|
202
|
-
function getDisplayKey(key: string, isMac: boolean): string {
|
|
203
|
-
if (key === 'Cmd/Ctrl') return isMac ? '⌘' : 'Ctrl'
|
|
204
|
-
if (key === 'Delete / Backspace') return isMac ? '⌫' : 'Backspace'
|
|
205
|
-
return KEY_DISPLAY_MAP[key] ?? key
|
|
206
|
-
}
|
|
207
|
-
|
|
208
211
|
function ShortcutKeys({ keys }: { keys: string[] }) {
|
|
209
|
-
const [isMac, setIsMac] = useState(true)
|
|
210
|
-
|
|
211
|
-
useEffect(() => {
|
|
212
|
-
setIsMac(navigator.platform.toUpperCase().indexOf('MAC') >= 0)
|
|
213
|
-
}, [])
|
|
214
|
-
|
|
215
212
|
return (
|
|
216
213
|
<div className="flex flex-wrap items-center gap-1">
|
|
217
214
|
{keys.map((key, index) => (
|
|
218
215
|
<div className="flex items-center gap-1" key={`${key}-${index}`}>
|
|
219
216
|
{index > 0 ? <span className="text-[10px] text-muted-foreground">+</span> : null}
|
|
220
|
-
<ShortcutToken displayValue={
|
|
217
|
+
<ShortcutToken displayValue={shortcutDisplayValue(key)} value={key} />
|
|
221
218
|
</div>
|
|
222
219
|
))}
|
|
223
220
|
</div>
|