@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
|
@@ -14,15 +14,33 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
type HoverStyles,
|
|
16
16
|
InteractiveSystem,
|
|
17
|
+
PERF_OVERLAY_ENABLED,
|
|
18
|
+
recordPerfSample,
|
|
17
19
|
SceneEnvironment,
|
|
18
20
|
useViewer,
|
|
19
21
|
Viewer,
|
|
22
|
+
ViewerPresentations,
|
|
20
23
|
} from '@pascal-app/viewer'
|
|
21
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
memo,
|
|
26
|
+
Profiler,
|
|
27
|
+
type ProfilerOnRenderCallback,
|
|
28
|
+
type ReactNode,
|
|
29
|
+
useCallback,
|
|
30
|
+
useEffect,
|
|
31
|
+
useLayoutEffect,
|
|
32
|
+
useRef,
|
|
33
|
+
useState,
|
|
34
|
+
} from 'react'
|
|
22
35
|
import { ViewerOverlay } from '../../components/viewer-overlay'
|
|
23
36
|
import { ViewerZoneSystem } from '../../components/viewer-zone-system'
|
|
24
37
|
import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
|
|
25
38
|
import { useKeyboard } from '../../hooks/use-keyboard'
|
|
39
|
+
import { useSaveShortcut } from '../../hooks/use-save-shortcut'
|
|
40
|
+
import {
|
|
41
|
+
createLocalProjectPresentationPersistence,
|
|
42
|
+
type LocalProjectPresentationPersistence,
|
|
43
|
+
} from '../../lib/local-project-presentation-persistence'
|
|
26
44
|
import { type ActivePaintMaterial, hasActivePaintMaterial } from '../../lib/material-paint'
|
|
27
45
|
import {
|
|
28
46
|
applySceneGraphToEditor,
|
|
@@ -31,6 +49,7 @@ import {
|
|
|
31
49
|
writePersistedSelection,
|
|
32
50
|
} from '../../lib/scene'
|
|
33
51
|
import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
|
|
52
|
+
import { type CameraHintAction, useCameraHintFocus } from '../../store/use-camera-hint-focus'
|
|
34
53
|
import useEditor from '../../store/use-editor'
|
|
35
54
|
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
36
55
|
import useSessionGroups from '../../store/use-session-groups'
|
|
@@ -52,7 +71,7 @@ import { PanelManager } from '../ui/panels/panel-manager'
|
|
|
52
71
|
import { ErrorBoundary } from '../ui/primitives/error-boundary'
|
|
53
72
|
import { useSidebarStore } from '../ui/primitives/sidebar'
|
|
54
73
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
|
|
55
|
-
import { SceneLoader } from '../ui/scene-loader'
|
|
74
|
+
import { SceneLoader, SceneLoadFailed } from '../ui/scene-loader'
|
|
56
75
|
import { AppSidebar } from '../ui/sidebar/app-sidebar'
|
|
57
76
|
import type { ExtraPanel } from '../ui/sidebar/icon-rail'
|
|
58
77
|
import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/settings-panel'
|
|
@@ -61,6 +80,7 @@ import type { SidebarTab } from '../ui/sidebar/tab-bar'
|
|
|
61
80
|
import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
|
|
62
81
|
import { ViewerStage } from '../viewer/viewer-stage'
|
|
63
82
|
import type { ViewerStageMode } from '../viewer/viewer-stage-modes'
|
|
83
|
+
import { CaptureCameraRig } from './capture-camera-rig'
|
|
64
84
|
import { CustomCameraControls } from './custom-camera-controls'
|
|
65
85
|
import { DeleteConfirmationDialog } from './delete-confirmation-dialog'
|
|
66
86
|
import { EditorLayoutV2 } from './editor-layout-v2'
|
|
@@ -98,7 +118,12 @@ const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
|
|
98
118
|
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
|
99
119
|
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
|
100
120
|
const SCENE_READY_FALLBACK_MS = 8000
|
|
121
|
+
const PRESENTATION_PROJECT_NOT_RESTORED = Symbol('presentation-project-not-restored')
|
|
122
|
+
const useClientLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
|
|
101
123
|
type PaintCursorBadgeState = 'empty' | 'ready' | 'blocked'
|
|
124
|
+
const recordEditorRender: ProfilerOnRenderCallback = (_id, _phase, actualDuration) => {
|
|
125
|
+
if (PERF_OVERLAY_ENABLED) recordPerfSample('react-render', actualDuration)
|
|
126
|
+
}
|
|
102
127
|
const EDITOR_HOVER_STYLES: HoverStyles = {
|
|
103
128
|
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
|
|
104
129
|
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
|
|
@@ -178,6 +203,12 @@ export interface EditorProps {
|
|
|
178
203
|
// Persistence — defaults to localStorage when omitted
|
|
179
204
|
onLoad?: () => Promise<SceneGraph | null>
|
|
180
205
|
onSave?: (scene: SceneGraph, options?: { keepalive?: boolean }) => Promise<void>
|
|
206
|
+
/**
|
|
207
|
+
* Cmd/Ctrl+S. Return true when the host handled the save (the community
|
|
208
|
+
* version checkpoint); anything else falls through to flushing the autosave,
|
|
209
|
+
* so the chord still saves when the host's control isn't mounted.
|
|
210
|
+
*/
|
|
211
|
+
onSaveShortcut?: () => boolean | undefined
|
|
181
212
|
onDirty?: () => void
|
|
182
213
|
onSaveStatusChange?: (status: SaveStatus) => void
|
|
183
214
|
|
|
@@ -380,7 +411,7 @@ type ShortcutKey = {
|
|
|
380
411
|
}
|
|
381
412
|
|
|
382
413
|
type CameraControlHint = {
|
|
383
|
-
action:
|
|
414
|
+
action: CameraHintAction
|
|
384
415
|
keys: ShortcutKey[]
|
|
385
416
|
alternativeKeys?: ShortcutKey[]
|
|
386
417
|
}
|
|
@@ -514,7 +545,15 @@ function ViewerCanvasControlsHint({
|
|
|
514
545
|
isPreviewMode: boolean
|
|
515
546
|
onDismiss: () => void
|
|
516
547
|
}) {
|
|
517
|
-
const
|
|
548
|
+
const all = isPreviewMode ? PREVIEW_CAMERA_CONTROL_HINTS : EDITOR_CAMERA_CONTROL_HINTS
|
|
549
|
+
// A host teaching one gesture at a time narrows this to the one it is asking
|
|
550
|
+
// for, and to nothing once it is done. Null — the default — is all of them.
|
|
551
|
+
const focus = useCameraHintFocus((state) => state.actions)
|
|
552
|
+
const hints = focus === null ? all : all.filter((hint) => focus.includes(hint.action))
|
|
553
|
+
|
|
554
|
+
if (hints.length === 0) {
|
|
555
|
+
return null
|
|
556
|
+
}
|
|
518
557
|
|
|
519
558
|
return (
|
|
520
559
|
<div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
|
|
@@ -522,7 +561,10 @@ function ViewerCanvasControlsHint({
|
|
|
522
561
|
aria-label="Camera controls hint"
|
|
523
562
|
className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-elevation-4 backdrop-blur-xl"
|
|
524
563
|
>
|
|
525
|
-
<div
|
|
564
|
+
<div
|
|
565
|
+
className="grid min-w-0 flex-1 items-start divide-x divide-border/18"
|
|
566
|
+
style={{ gridTemplateColumns: `repeat(${hints.length}, minmax(0, 1fr))` }}
|
|
567
|
+
>
|
|
526
568
|
{hints.map((hint) => (
|
|
527
569
|
<CameraControlHintItem hint={hint} key={hint.action} />
|
|
528
570
|
))}
|
|
@@ -743,6 +785,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
743
785
|
isStudioMode,
|
|
744
786
|
onThumbnailCapture,
|
|
745
787
|
viewerSceneSlot,
|
|
788
|
+
presentationsReady,
|
|
746
789
|
}: {
|
|
747
790
|
isVersionPreviewMode: boolean
|
|
748
791
|
isLoading: boolean
|
|
@@ -750,6 +793,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
750
793
|
isStudioMode: boolean
|
|
751
794
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
752
795
|
viewerSceneSlot?: ReactNode
|
|
796
|
+
presentationsReady: boolean
|
|
753
797
|
}) {
|
|
754
798
|
// Studio mode is a clean render/snapshot surface — no selection or editing
|
|
755
799
|
// affordances. It mirrors version-preview's chrome gating on the canvas.
|
|
@@ -784,10 +828,12 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
784
828
|
{!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
|
|
785
829
|
{!(isLoading || noEditing) && <ToolManager />}
|
|
786
830
|
{isFirstPersonMode && <FirstPersonControls />}
|
|
831
|
+
{isCaptureMode && <CaptureCameraRig />}
|
|
787
832
|
<CustomCameraControls />
|
|
788
833
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
789
834
|
{!isFirstPersonMode && <SiteEdgeLabels />}
|
|
790
835
|
<InteractiveSystem />
|
|
836
|
+
{presentationsReady ? <ViewerPresentations /> : null}
|
|
791
837
|
{!noEditing && viewerSceneSlot}
|
|
792
838
|
</>
|
|
793
839
|
)
|
|
@@ -971,6 +1017,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
971
1017
|
sceneReadyKey,
|
|
972
1018
|
onSceneReadyChange,
|
|
973
1019
|
onThumbnailCapture,
|
|
1020
|
+
presentationsReady,
|
|
974
1021
|
viewerSceneSlot,
|
|
975
1022
|
floorplanSceneSlot,
|
|
976
1023
|
disablePostFx = false,
|
|
@@ -984,6 +1031,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
984
1031
|
sceneReadyKey: number
|
|
985
1032
|
onSceneReadyChange: (ready: boolean) => void
|
|
986
1033
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
1034
|
+
presentationsReady: boolean
|
|
987
1035
|
viewerSceneSlot?: ReactNode
|
|
988
1036
|
floorplanSceneSlot?: ReactNode
|
|
989
1037
|
disablePostFx?: boolean
|
|
@@ -992,6 +1040,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
992
1040
|
const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
|
|
993
1041
|
const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio)
|
|
994
1042
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
1043
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
995
1044
|
|
|
996
1045
|
const [isCameraControlsHintVisible, setIsCameraControlsHintVisible] = useState<boolean | null>(
|
|
997
1046
|
null,
|
|
@@ -1106,7 +1155,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1106
1155
|
renderContext="editor"
|
|
1107
1156
|
renderPaused={!show3d && !showLoader}
|
|
1108
1157
|
sceneReadyKey={sceneReadyKey}
|
|
1109
|
-
|
|
1158
|
+
// Walk/drone framing during snapshot capture is camera-only: the
|
|
1159
|
+
// viewer's default selection manager would hover-highlight whatever
|
|
1160
|
+
// the cursor crosses, which orbit capture never does.
|
|
1161
|
+
selectionManager={isFirstPersonMode && !isCaptureMode ? 'default' : 'custom'}
|
|
1110
1162
|
>
|
|
1111
1163
|
<ViewerSceneContent
|
|
1112
1164
|
isFirstPersonMode={isFirstPersonMode}
|
|
@@ -1114,6 +1166,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1114
1166
|
isStudioMode={isStudioMode}
|
|
1115
1167
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
1116
1168
|
onThumbnailCapture={onThumbnailCapture}
|
|
1169
|
+
presentationsReady={presentationsReady}
|
|
1117
1170
|
viewerSceneSlot={viewerSceneSlot}
|
|
1118
1171
|
/>
|
|
1119
1172
|
</Viewer>
|
|
@@ -1178,7 +1231,7 @@ function PreviewStage({
|
|
|
1178
1231
|
)
|
|
1179
1232
|
}
|
|
1180
1233
|
|
|
1181
|
-
|
|
1234
|
+
function EditorContent({
|
|
1182
1235
|
layoutVersion = 'v1',
|
|
1183
1236
|
appMenuButton,
|
|
1184
1237
|
sidebarTop,
|
|
@@ -1194,6 +1247,7 @@ export default function Editor({
|
|
|
1194
1247
|
projectId,
|
|
1195
1248
|
onLoad,
|
|
1196
1249
|
onSave,
|
|
1250
|
+
onSaveShortcut,
|
|
1197
1251
|
onDirty,
|
|
1198
1252
|
onSaveStatusChange,
|
|
1199
1253
|
previewScene,
|
|
@@ -1211,18 +1265,51 @@ export default function Editor({
|
|
|
1211
1265
|
}: EditorProps) {
|
|
1212
1266
|
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
1213
1267
|
const isStudioMode = useEditor((s) => s.workspaceMode === 'studio')
|
|
1268
|
+
const presentationProjectId = projectId ?? null
|
|
1269
|
+
const presentationPersistenceRef = useRef<LocalProjectPresentationPersistence | null>(null)
|
|
1270
|
+
const [restoredPresentationProjectId, setRestoredPresentationProjectId] = useState<
|
|
1271
|
+
string | null | typeof PRESENTATION_PROJECT_NOT_RESTORED
|
|
1272
|
+
>(PRESENTATION_PROJECT_NOT_RESTORED)
|
|
1273
|
+
const presentationsReady = restoredPresentationProjectId === presentationProjectId
|
|
1274
|
+
|
|
1275
|
+
useClientLayoutEffect(() => {
|
|
1276
|
+
const persistence = createLocalProjectPresentationPersistence()
|
|
1277
|
+
presentationPersistenceRef.current = persistence
|
|
1278
|
+
return () => {
|
|
1279
|
+
presentationPersistenceRef.current = null
|
|
1280
|
+
persistence.dispose()
|
|
1281
|
+
}
|
|
1282
|
+
}, [])
|
|
1283
|
+
|
|
1284
|
+
useClientLayoutEffect(() => {
|
|
1285
|
+
const persistence = presentationPersistenceRef.current
|
|
1286
|
+
if (!persistence) return
|
|
1287
|
+
persistence.switchProject(presentationProjectId)
|
|
1288
|
+
setRestoredPresentationProjectId(presentationProjectId)
|
|
1289
|
+
}, [presentationProjectId])
|
|
1214
1290
|
|
|
1215
1291
|
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
|
|
1216
1292
|
|
|
1217
|
-
const { isLoadingSceneRef } = useAutoSave({
|
|
1293
|
+
const { isLoadingSceneRef, saveNow } = useAutoSave({
|
|
1218
1294
|
onSave,
|
|
1219
1295
|
onDirty,
|
|
1220
1296
|
onSaveStatusChange,
|
|
1221
1297
|
isVersionPreviewMode,
|
|
1222
1298
|
})
|
|
1223
1299
|
|
|
1300
|
+
const handleSaveShortcut = useCallback(() => {
|
|
1301
|
+
if (onSaveShortcut?.() === true) return
|
|
1302
|
+
saveNow()
|
|
1303
|
+
}, [onSaveShortcut, saveNow])
|
|
1304
|
+
useSaveShortcut(handleSaveShortcut)
|
|
1305
|
+
|
|
1224
1306
|
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
|
1225
1307
|
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
|
|
1308
|
+
// A failed `onLoad` is shown as an error with a retry, never as an empty
|
|
1309
|
+
// scene: an editor that renders the default scaffold after a failed load
|
|
1310
|
+
// autosaves that scaffold over the real project.
|
|
1311
|
+
const [sceneLoadError, setSceneLoadError] = useState<unknown>(null)
|
|
1312
|
+
const [sceneLoadAttempt, setSceneLoadAttempt] = useState(0)
|
|
1226
1313
|
const [sceneReadyKey, setSceneReadyKey] = useState(0)
|
|
1227
1314
|
const [isViewerSceneReady, setIsViewerSceneReady] = useState(false)
|
|
1228
1315
|
const [previewStageMode, setPreviewStageMode] = useState<ViewerStageMode>('3d')
|
|
@@ -1258,12 +1345,14 @@ export default function Editor({
|
|
|
1258
1345
|
}
|
|
1259
1346
|
}, [projectId])
|
|
1260
1347
|
|
|
1261
|
-
// Load
|
|
1348
|
+
// Load on mount, project switches, and explicit retry attempts.
|
|
1262
1349
|
useEffect(() => {
|
|
1350
|
+
void sceneLoadAttempt
|
|
1263
1351
|
let cancelled = false
|
|
1264
1352
|
|
|
1265
1353
|
async function load() {
|
|
1266
1354
|
isLoadingSceneRef.current = true
|
|
1355
|
+
setSceneLoadError(null)
|
|
1267
1356
|
setHasLoadedInitialScene(false)
|
|
1268
1357
|
setIsViewerSceneReady(false)
|
|
1269
1358
|
setIsSceneLoading(true)
|
|
@@ -1272,6 +1361,7 @@ export default function Editor({
|
|
|
1272
1361
|
// Session groups are not scene-graph state — clear on every load/switch.
|
|
1273
1362
|
useSessionGroups.getState().clearGroups()
|
|
1274
1363
|
|
|
1364
|
+
let failed = false
|
|
1275
1365
|
try {
|
|
1276
1366
|
const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
|
|
1277
1367
|
if (!cancelled) {
|
|
@@ -1279,19 +1369,23 @@ export default function Editor({
|
|
|
1279
1369
|
setIsViewerSceneReady(false)
|
|
1280
1370
|
setSceneReadyKey((key) => key + 1)
|
|
1281
1371
|
}
|
|
1282
|
-
} catch {
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
// Leave the store unloaded and the autosave loop in its loading
|
|
1374
|
+
// state: nothing may be written until a load actually succeeds.
|
|
1375
|
+
failed = true
|
|
1283
1376
|
if (!cancelled) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
setSceneReadyKey((key) => key + 1)
|
|
1377
|
+
console.error('[editor] scene load failed', error)
|
|
1378
|
+
setSceneLoadError(error ?? new Error('Scene load failed'))
|
|
1287
1379
|
}
|
|
1288
1380
|
} finally {
|
|
1289
1381
|
if (!cancelled) {
|
|
1290
1382
|
setIsSceneLoading(false)
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1383
|
+
if (!failed) {
|
|
1384
|
+
setHasLoadedInitialScene(true)
|
|
1385
|
+
requestAnimationFrame(() => {
|
|
1386
|
+
isLoadingSceneRef.current = false
|
|
1387
|
+
})
|
|
1388
|
+
}
|
|
1295
1389
|
}
|
|
1296
1390
|
}
|
|
1297
1391
|
}
|
|
@@ -1301,7 +1395,11 @@ export default function Editor({
|
|
|
1301
1395
|
return () => {
|
|
1302
1396
|
cancelled = true
|
|
1303
1397
|
}
|
|
1304
|
-
}, [onLoad, isLoadingSceneRef])
|
|
1398
|
+
}, [onLoad, isLoadingSceneRef, sceneLoadAttempt])
|
|
1399
|
+
|
|
1400
|
+
const retrySceneLoad = useCallback(() => {
|
|
1401
|
+
setSceneLoadAttempt((attempt) => attempt + 1)
|
|
1402
|
+
}, [])
|
|
1305
1403
|
|
|
1306
1404
|
// Apply preview scene when version preview mode changes
|
|
1307
1405
|
useEffect(() => {
|
|
@@ -1417,6 +1515,7 @@ export default function Editor({
|
|
|
1417
1515
|
<CustomCameraControls />
|
|
1418
1516
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
1419
1517
|
<InteractiveSystem />
|
|
1518
|
+
{presentationsReady ? <ViewerPresentations /> : null}
|
|
1420
1519
|
</Viewer>
|
|
1421
1520
|
)
|
|
1422
1521
|
|
|
@@ -1430,6 +1529,7 @@ export default function Editor({
|
|
|
1430
1529
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
1431
1530
|
onSceneReadyChange={handleSceneReadyChange}
|
|
1432
1531
|
onThumbnailCapture={onThumbnailCapture}
|
|
1532
|
+
presentationsReady={presentationsReady}
|
|
1433
1533
|
sceneReadyKey={sceneReadyKey}
|
|
1434
1534
|
showLoader={showLoader}
|
|
1435
1535
|
viewerSceneSlot={viewerSceneSlot}
|
|
@@ -1490,7 +1590,11 @@ export default function Editor({
|
|
|
1490
1590
|
<FloorplanModeCoordinator />
|
|
1491
1591
|
{visibleLoader && (
|
|
1492
1592
|
<div className="fixed inset-0 z-60">
|
|
1493
|
-
|
|
1593
|
+
{sceneLoadError ? (
|
|
1594
|
+
<SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
|
|
1595
|
+
) : (
|
|
1596
|
+
<SceneLoader className="bg-background" />
|
|
1597
|
+
)}
|
|
1494
1598
|
</div>
|
|
1495
1599
|
)}
|
|
1496
1600
|
|
|
@@ -1527,7 +1631,10 @@ export default function Editor({
|
|
|
1527
1631
|
<HelperManager />
|
|
1528
1632
|
</div>
|
|
1529
1633
|
)}
|
|
1530
|
-
{
|
|
1634
|
+
{/* Capture mode drives walk / drone from its own overlay, which
|
|
1635
|
+
owns the framing chrome — the walkthrough HUD would both
|
|
1636
|
+
clutter the frame and offer a second, conflicting exit. */}
|
|
1637
|
+
{isFirstPersonMode && !isCaptureMode && (
|
|
1531
1638
|
<FirstPersonOverlay
|
|
1532
1639
|
onExit={() => useEditor.getState().setFirstPersonMode(false)}
|
|
1533
1640
|
/>
|
|
@@ -1563,7 +1670,11 @@ export default function Editor({
|
|
|
1563
1670
|
<FloorplanModeCoordinator />
|
|
1564
1671
|
{visibleLoader && (
|
|
1565
1672
|
<div className="fixed inset-0 z-60">
|
|
1566
|
-
|
|
1673
|
+
{sceneLoadError ? (
|
|
1674
|
+
<SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
|
|
1675
|
+
) : (
|
|
1676
|
+
<SceneLoader className="bg-background" />
|
|
1677
|
+
)}
|
|
1567
1678
|
</div>
|
|
1568
1679
|
)}
|
|
1569
1680
|
|
|
@@ -1613,3 +1724,11 @@ export default function Editor({
|
|
|
1613
1724
|
</div>
|
|
1614
1725
|
)
|
|
1615
1726
|
}
|
|
1727
|
+
|
|
1728
|
+
export default function Editor(props: EditorProps) {
|
|
1729
|
+
return (
|
|
1730
|
+
<Profiler id="editor" onRender={recordEditorRender}>
|
|
1731
|
+
<EditorContent {...props} />
|
|
1732
|
+
</Profiler>
|
|
1733
|
+
)
|
|
1734
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
|
-
import { Copy, Group, Move, Search, Spline, Trash2, Ungroup } from 'lucide-react'
|
|
4
|
+
import { Copy, Group, Move, PencilRuler, Search, Spline, Trash2, Ungroup } from 'lucide-react'
|
|
5
5
|
import type { MouseEventHandler, PointerEventHandler } from 'react'
|
|
6
6
|
|
|
7
7
|
type NodeActionMenuProps = {
|
|
@@ -10,6 +10,7 @@ type NodeActionMenuProps = {
|
|
|
10
10
|
onDelete?: MouseEventHandler<HTMLButtonElement>
|
|
11
11
|
onDuplicate?: MouseEventHandler<HTMLButtonElement>
|
|
12
12
|
onMove?: MouseEventHandler<HTMLButtonElement>
|
|
13
|
+
onEditMesh?: MouseEventHandler<HTMLButtonElement>
|
|
13
14
|
onCurve?: MouseEventHandler<HTMLButtonElement>
|
|
14
15
|
/** Session group (Ctrl/Cmd+G) — multi-selection floating pill. */
|
|
15
16
|
onGroup?: MouseEventHandler<HTMLButtonElement>
|
|
@@ -27,6 +28,7 @@ export function NodeActionMenu({
|
|
|
27
28
|
onDelete,
|
|
28
29
|
onDuplicate,
|
|
29
30
|
onMove,
|
|
31
|
+
onEditMesh,
|
|
30
32
|
onCurve,
|
|
31
33
|
onGroup,
|
|
32
34
|
onUngroup,
|
|
@@ -65,6 +67,17 @@ export function NodeActionMenu({
|
|
|
65
67
|
<Move className="h-4 w-4" />
|
|
66
68
|
</button>
|
|
67
69
|
)}
|
|
70
|
+
{onEditMesh && (
|
|
71
|
+
<button
|
|
72
|
+
aria-label="Edit mesh"
|
|
73
|
+
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
74
|
+
onClick={onEditMesh}
|
|
75
|
+
title="Edit mesh"
|
|
76
|
+
type="button"
|
|
77
|
+
>
|
|
78
|
+
<PencilRuler className="h-4 w-4" />
|
|
79
|
+
</button>
|
|
80
|
+
)}
|
|
68
81
|
{onGroup && (
|
|
69
82
|
<button
|
|
70
83
|
aria-label="Group selection"
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
HandleArrow,
|
|
63
63
|
NO_RAYCAST,
|
|
64
64
|
} from './handles/handle-arrow'
|
|
65
|
-
import {
|
|
65
|
+
import { createLinearResizeDragBinding } from './handles/linear-resize-drag'
|
|
66
66
|
import { resolveResizeSnapValue } from './handles/resize-snap'
|
|
67
67
|
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
68
68
|
|
|
@@ -235,16 +235,15 @@ export function NodeArrowHandles() {
|
|
|
235
235
|
typeof def.handles === 'function'
|
|
236
236
|
? def.handles(node as never, descriptorSceneApi)
|
|
237
237
|
: (def.handles as HandleDescriptor[])
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
238
|
+
return all.filter((descriptor) => {
|
|
239
|
+
if (descriptor.kind === 'translate') return false
|
|
240
|
+
const visible =
|
|
241
|
+
'visible' in descriptor
|
|
242
|
+
? descriptor.visible?.(node as never, descriptorSceneApi)
|
|
243
|
+
: undefined
|
|
244
|
+
if ('shape' in descriptor && descriptor.shape === 'move-cross') return visible === true
|
|
245
|
+
return visible !== false
|
|
246
|
+
})
|
|
248
247
|
}, [node, def, descriptorSceneApi])
|
|
249
248
|
|
|
250
249
|
const shouldRender =
|
|
@@ -293,18 +292,25 @@ function NodeArrowHandlesForNode({
|
|
|
293
292
|
descriptors: HandleDescriptor[]
|
|
294
293
|
}) {
|
|
295
294
|
const parentId = node.parentId ?? null
|
|
296
|
-
const grandparentId = useScene((state) => {
|
|
297
|
-
if (!parentId) return null
|
|
298
|
-
const parent = state.nodes[parentId as AnyNodeId]
|
|
299
|
-
return parent?.parentId ?? null
|
|
300
|
-
})
|
|
301
295
|
|
|
302
296
|
const portalMode: HandlePortal = descriptors.some((d) => d.portal === 'grandparent')
|
|
303
297
|
? 'grandparent'
|
|
304
298
|
: 'parent'
|
|
305
299
|
|
|
300
|
+
const portalTargetResolver = descriptors.find(
|
|
301
|
+
(descriptor) => descriptor.portalTarget !== undefined,
|
|
302
|
+
)?.portalTarget
|
|
303
|
+
const descriptorSceneApi = useMemo(() => createSceneApi(useScene), [])
|
|
304
|
+
|
|
306
305
|
// Portal target: the mesh we createPortal into.
|
|
307
|
-
const portalTargetId =
|
|
306
|
+
const portalTargetId = useScene((state) => {
|
|
307
|
+
if (portalTargetResolver) {
|
|
308
|
+
return portalTargetResolver(node as never, descriptorSceneApi) ?? null
|
|
309
|
+
}
|
|
310
|
+
const parentId = node.parentId ?? null
|
|
311
|
+
if (!parentId || portalMode === 'parent') return parentId
|
|
312
|
+
return state.nodes[parentId as AnyNodeId]?.parentId ?? null
|
|
313
|
+
})
|
|
308
314
|
// Outer wrapper mirrors this mesh's local pose. For 'parent' mode the
|
|
309
315
|
// outer IS the node (so handles + drag math both live in node-local).
|
|
310
316
|
// For 'grandparent' the outer rides the parent and an inner group adds
|
|
@@ -710,10 +716,17 @@ function LinearArrow({
|
|
|
710
716
|
getPointerRay(event.nativeEvent.clientX, event.nativeEvent.clientY, _resizeRay),
|
|
711
717
|
) / localToWorldScale
|
|
712
718
|
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
?
|
|
716
|
-
|
|
719
|
+
const linearBinding =
|
|
720
|
+
descriptor.kind === 'linear-resize'
|
|
721
|
+
? createLinearResizeDragBinding({
|
|
722
|
+
descriptor,
|
|
723
|
+
initialNode,
|
|
724
|
+
nodeId,
|
|
725
|
+
sceneApi,
|
|
726
|
+
initialModifiers: { altKey: event.nativeEvent.altKey },
|
|
727
|
+
})
|
|
728
|
+
: null
|
|
729
|
+
const overrideId = linearBinding?.overrideId ?? nodeId
|
|
717
730
|
const initialValue = descriptor.currentValue(initialNode)
|
|
718
731
|
const minBound = resolveBound(descriptor.min, Number.NEGATIVE_INFINITY, initialNode, sceneApi)
|
|
719
732
|
const maxBound = resolveBound(descriptor.max, Number.POSITIVE_INFINITY, initialNode, sceneApi)
|
|
@@ -730,14 +743,9 @@ function LinearArrow({
|
|
|
730
743
|
// when the (snapped + clamped) value actually changes, so the cue
|
|
731
744
|
// tracks real size steps instead of every sub-pixel pointer jitter.
|
|
732
745
|
let lastTickValue = initialValue
|
|
733
|
-
let previewOverrideIds = new Set<AnyNodeId>()
|
|
734
|
-
|
|
735
746
|
return {
|
|
736
747
|
overrideId,
|
|
737
|
-
commit:
|
|
738
|
-
descriptor.kind === 'linear-resize' && descriptor.commit
|
|
739
|
-
? (patch) => descriptor.commit?.(initialNode, patch, sceneApi)
|
|
740
|
-
: undefined,
|
|
748
|
+
commit: linearBinding?.commit,
|
|
741
749
|
onBegin: () => {
|
|
742
750
|
// Always claim the handle-drag scope so the HUD knows a resize is the
|
|
743
751
|
// active interaction (keeps the idle select hints off-screen). The
|
|
@@ -755,12 +763,9 @@ function LinearArrow({
|
|
|
755
763
|
descriptor.onDragEnd?.(initialNode as never, sceneApi)
|
|
756
764
|
}
|
|
757
765
|
if (onDrag) useOpeningGuides.getState().clear()
|
|
758
|
-
|
|
759
|
-
useLiveNodeOverrides.getState().clear(previewId)
|
|
760
|
-
useScene.getState().markDirty(previewId)
|
|
761
|
-
}
|
|
766
|
+
linearBinding?.clearPreview()
|
|
762
767
|
},
|
|
763
|
-
move: ({ event: moveEvent, getPointerRay: getMovePointerRay }) => {
|
|
768
|
+
move: ({ event: moveEvent, modifiers, getPointerRay: getMovePointerRay }) => {
|
|
764
769
|
const currentPointer =
|
|
765
770
|
closestAxisParameterToRay(
|
|
766
771
|
_resizeOriginW,
|
|
@@ -772,43 +777,27 @@ function LinearArrow({
|
|
|
772
777
|
const linearDescriptor = descriptor.kind === 'linear-resize' ? descriptor : null
|
|
773
778
|
const snappedNext = resolveResizeSnapValue({
|
|
774
779
|
rawValue: rawNext,
|
|
780
|
+
fallbackValue: lastTickValue,
|
|
775
781
|
gridSnapEnabled: linearDescriptor?.gridSnap === true,
|
|
776
|
-
gridSnapActive: isGridSnapActive(),
|
|
782
|
+
gridSnapActive: isGridSnapActive() && !modifiers.altKey,
|
|
777
783
|
gridSnapStep: useEditor.getState().gridSnapStep,
|
|
778
|
-
magneticSnapActive: isMagneticSnapActive(),
|
|
784
|
+
magneticSnapActive: isMagneticSnapActive() && !modifiers.altKey,
|
|
779
785
|
magneticSnap: linearDescriptor?.magneticSnap
|
|
780
786
|
? (value) => linearDescriptor.magneticSnap?.(initialNode, value, sceneApi) ?? value
|
|
781
787
|
: undefined,
|
|
788
|
+
connectionSnapActive: !modifiers.altKey,
|
|
789
|
+
connectionSnap: linearDescriptor?.connectionSnap
|
|
790
|
+
? (value) => linearDescriptor.connectionSnap?.(initialNode, value, sceneApi) ?? value
|
|
791
|
+
: undefined,
|
|
782
792
|
})
|
|
783
793
|
const next = Math.min(maxBound, Math.max(minBound, snappedNext))
|
|
784
794
|
if (next !== lastTickValue) {
|
|
785
795
|
lastTickValue = next
|
|
786
796
|
sfxEmitter.emit('sfx:resize')
|
|
787
797
|
}
|
|
788
|
-
const patch =
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const nextPreviewOverrideIds = replacePreviewOverrideIds(
|
|
792
|
-
previewOverrideIds,
|
|
793
|
-
previewEntries,
|
|
794
|
-
(previewId) => {
|
|
795
|
-
useLiveNodeOverrides.getState().clear(previewId)
|
|
796
|
-
useScene.getState().markDirty(previewId)
|
|
797
|
-
},
|
|
798
|
-
)
|
|
799
|
-
useLiveNodeOverrides
|
|
800
|
-
.getState()
|
|
801
|
-
.setMany(
|
|
802
|
-
previewEntries.map(([id, previewPatch]) => [
|
|
803
|
-
id,
|
|
804
|
-
previewPatch as Record<string, unknown>,
|
|
805
|
-
]),
|
|
806
|
-
)
|
|
807
|
-
for (const [previewId] of previewEntries) {
|
|
808
|
-
useScene.getState().markDirty(previewId)
|
|
809
|
-
}
|
|
810
|
-
previewOverrideIds = nextPreviewOverrideIds
|
|
811
|
-
}
|
|
798
|
+
const patch = linearBinding
|
|
799
|
+
? linearBinding.apply(next, modifiers)
|
|
800
|
+
: (descriptor.apply(initialNode as never, next, sceneApi) as Partial<AnyNode>)
|
|
812
801
|
// Let the kind publish live guides for the edge being resized.
|
|
813
802
|
onDrag?.({ ...(initialNode as object), ...patch } as AnyNode, sceneApi)
|
|
814
803
|
return patch
|