@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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
cascadeDirty,
|
|
5
|
+
type HandleDragModifiers,
|
|
6
|
+
type LinearResizeHandle,
|
|
7
|
+
type RadialResizeHandle,
|
|
8
|
+
type SceneApi,
|
|
9
|
+
useLiveNodeOverrides,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { replacePreviewOverrideIds } from './preview-overrides'
|
|
13
|
+
|
|
14
|
+
export function linearResizeFactor<N>(
|
|
15
|
+
descriptor: LinearResizeHandle<N> | RadialResizeHandle<N>,
|
|
16
|
+
): number {
|
|
17
|
+
if (descriptor.kind === 'radial-resize') return 1
|
|
18
|
+
return descriptor.anchor === 'center' ? 2 : descriptor.anchor === 'min' ? 1 : -1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createLinearResizeDragBinding({
|
|
22
|
+
descriptor,
|
|
23
|
+
initialNode,
|
|
24
|
+
nodeId,
|
|
25
|
+
sceneApi,
|
|
26
|
+
initialModifiers,
|
|
27
|
+
}: {
|
|
28
|
+
descriptor: LinearResizeHandle<AnyNode>
|
|
29
|
+
initialNode: AnyNode
|
|
30
|
+
nodeId: AnyNodeId
|
|
31
|
+
sceneApi: SceneApi
|
|
32
|
+
initialModifiers: HandleDragModifiers
|
|
33
|
+
}) {
|
|
34
|
+
const overrideId = descriptor.overrideTarget?.(initialNode, sceneApi) ?? nodeId
|
|
35
|
+
let lastModifiers = initialModifiers
|
|
36
|
+
let previewOverrideIds = new Set<AnyNodeId>()
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
overrideId,
|
|
40
|
+
commit: descriptor.commit
|
|
41
|
+
? (patch: Partial<AnyNode>) =>
|
|
42
|
+
descriptor.commit?.(initialNode, patch, sceneApi, lastModifiers)
|
|
43
|
+
: undefined,
|
|
44
|
+
apply(next: number, modifiers: HandleDragModifiers): Partial<AnyNode> {
|
|
45
|
+
lastModifiers = modifiers
|
|
46
|
+
const patch = descriptor.apply(initialNode, next, sceneApi, modifiers) as Partial<AnyNode>
|
|
47
|
+
const previewEntries = descriptor.previewOverrides?.(initialNode, next, sceneApi, modifiers)
|
|
48
|
+
if (!previewEntries) return patch
|
|
49
|
+
|
|
50
|
+
previewOverrideIds = replacePreviewOverrideIds(
|
|
51
|
+
previewOverrideIds,
|
|
52
|
+
previewEntries,
|
|
53
|
+
(previewId) => {
|
|
54
|
+
useLiveNodeOverrides.getState().clear(previewId)
|
|
55
|
+
useScene.getState().markDirty(previewId)
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
useLiveNodeOverrides
|
|
59
|
+
.getState()
|
|
60
|
+
.setMany(
|
|
61
|
+
previewEntries.map(([id, previewPatch]) => [id, previewPatch as Record<string, unknown>]),
|
|
62
|
+
)
|
|
63
|
+
for (const [previewId] of previewEntries) {
|
|
64
|
+
for (const id of cascadeDirty(previewId, { scene: sceneApi }))
|
|
65
|
+
useScene.getState().markDirty(id)
|
|
66
|
+
}
|
|
67
|
+
return patch
|
|
68
|
+
},
|
|
69
|
+
clearPreview(): void {
|
|
70
|
+
for (const previewId of previewOverrideIds) {
|
|
71
|
+
useLiveNodeOverrides.getState().clear(previewId)
|
|
72
|
+
for (const id of cascadeDirty(previewId, { scene: sceneApi }))
|
|
73
|
+
useScene.getState().markDirty(id)
|
|
74
|
+
}
|
|
75
|
+
previewOverrideIds = new Set()
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -49,4 +49,50 @@ describe('resolveResizeSnapValue', () => {
|
|
|
49
49
|
).toBe(0.56)
|
|
50
50
|
expect(magneticSnap).not.toHaveBeenCalled()
|
|
51
51
|
})
|
|
52
|
+
|
|
53
|
+
it('applies a structural connection snap independently of the active mode', () => {
|
|
54
|
+
const connectionSnap = mock(() => 0.6)
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
resolveResizeSnapValue({
|
|
58
|
+
rawValue: 0.59,
|
|
59
|
+
gridSnapEnabled: false,
|
|
60
|
+
gridSnapActive: false,
|
|
61
|
+
gridSnapStep: 0.1,
|
|
62
|
+
magneticSnapActive: false,
|
|
63
|
+
connectionSnap,
|
|
64
|
+
}),
|
|
65
|
+
).toBe(0.6)
|
|
66
|
+
expect(connectionSnap).toHaveBeenCalledWith(0.59)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('bypasses a structural connection snap while force-moving', () => {
|
|
70
|
+
const connectionSnap = mock(() => 0.6)
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
resolveResizeSnapValue({
|
|
74
|
+
rawValue: 0.59,
|
|
75
|
+
gridSnapEnabled: false,
|
|
76
|
+
gridSnapActive: false,
|
|
77
|
+
gridSnapStep: 0.1,
|
|
78
|
+
magneticSnapActive: false,
|
|
79
|
+
connectionSnapActive: false,
|
|
80
|
+
connectionSnap,
|
|
81
|
+
}),
|
|
82
|
+
).toBe(0.59)
|
|
83
|
+
expect(connectionSnap).not.toHaveBeenCalled()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('keeps the last valid value when pointer projection is non-finite', () => {
|
|
87
|
+
expect(
|
|
88
|
+
resolveResizeSnapValue({
|
|
89
|
+
rawValue: Number.NaN,
|
|
90
|
+
fallbackValue: 12.3,
|
|
91
|
+
gridSnapEnabled: false,
|
|
92
|
+
gridSnapActive: false,
|
|
93
|
+
gridSnapStep: 0.5,
|
|
94
|
+
magneticSnapActive: false,
|
|
95
|
+
}),
|
|
96
|
+
).toBe(12.3)
|
|
97
|
+
})
|
|
52
98
|
})
|
|
@@ -2,22 +2,31 @@ import { snapScalar } from '@pascal-app/core'
|
|
|
2
2
|
|
|
3
3
|
export function resolveResizeSnapValue({
|
|
4
4
|
rawValue,
|
|
5
|
+
fallbackValue = rawValue,
|
|
5
6
|
gridSnapEnabled,
|
|
6
7
|
gridSnapActive,
|
|
7
8
|
gridSnapStep,
|
|
8
9
|
magneticSnapActive,
|
|
9
10
|
magneticSnap,
|
|
11
|
+
connectionSnapActive = true,
|
|
12
|
+
connectionSnap,
|
|
10
13
|
}: {
|
|
11
14
|
rawValue: number
|
|
15
|
+
fallbackValue?: number
|
|
12
16
|
gridSnapEnabled: boolean
|
|
13
17
|
gridSnapActive: boolean
|
|
14
18
|
gridSnapStep: number
|
|
15
19
|
magneticSnapActive: boolean
|
|
16
20
|
magneticSnap?: (value: number) => number
|
|
21
|
+
connectionSnapActive?: boolean
|
|
22
|
+
connectionSnap?: (value: number) => number
|
|
17
23
|
}): number {
|
|
24
|
+
if (!Number.isFinite(rawValue)) return fallbackValue
|
|
18
25
|
const gridValue =
|
|
19
26
|
gridSnapEnabled && gridSnapActive && gridSnapStep > 0
|
|
20
27
|
? snapScalar(rawValue, gridSnapStep)
|
|
21
28
|
: rawValue
|
|
22
|
-
|
|
29
|
+
const modeValue = magneticSnapActive && magneticSnap ? magneticSnap(gridValue) : gridValue
|
|
30
|
+
const resolved = connectionSnapActive && connectionSnap ? connectionSnap(modeValue) : modeValue
|
|
31
|
+
return Number.isFinite(resolved) ? resolved : fallbackValue
|
|
23
32
|
}
|
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type Cursor,
|
|
7
|
+
cascadeDirty,
|
|
7
8
|
createSceneApi,
|
|
9
|
+
type HandleDragModifiers,
|
|
8
10
|
runAsSingleSceneHistoryStep,
|
|
9
11
|
useLiveNodeOverrides,
|
|
10
12
|
useScene,
|
|
@@ -46,6 +48,7 @@ export type HandleDragStartContext = {
|
|
|
46
48
|
|
|
47
49
|
export type HandleDragMoveContext = {
|
|
48
50
|
event: PointerEvent
|
|
51
|
+
modifiers: HandleDragModifiers
|
|
49
52
|
getPointerRay: GetPointerRay
|
|
50
53
|
intersectPlane: IntersectPlane
|
|
51
54
|
}
|
|
@@ -177,6 +180,7 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
177
180
|
|
|
178
181
|
let lastPatch: Partial<AnyNode> | null = null
|
|
179
182
|
let historyPaused = true
|
|
183
|
+
let altKey = event.nativeEvent.altKey
|
|
180
184
|
|
|
181
185
|
const resumeHistory = () => {
|
|
182
186
|
if (!historyPaused) return
|
|
@@ -185,12 +189,18 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
const onMove = (moveEvent: PointerEvent) => {
|
|
188
|
-
const patch = session.move({
|
|
192
|
+
const patch = session.move({
|
|
193
|
+
event: moveEvent,
|
|
194
|
+
modifiers: { altKey },
|
|
195
|
+
getPointerRay,
|
|
196
|
+
intersectPlane,
|
|
197
|
+
})
|
|
189
198
|
if (!patch) return
|
|
190
199
|
lastPatch = patch
|
|
191
200
|
useLiveNodeOverrides.getState().set(overrideId, patch as Record<string, unknown>)
|
|
192
201
|
if (markDirty) {
|
|
193
|
-
|
|
202
|
+
for (const id of cascadeDirty(overrideId, { scene: createSceneApi(useScene) }))
|
|
203
|
+
useScene.getState().markDirty(id)
|
|
194
204
|
}
|
|
195
205
|
}
|
|
196
206
|
|
|
@@ -199,6 +209,7 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
199
209
|
window.removeEventListener('pointerup', onUp)
|
|
200
210
|
window.removeEventListener('pointercancel', onCancel)
|
|
201
211
|
window.removeEventListener('keydown', onKeyDown, true)
|
|
212
|
+
window.removeEventListener('keyup', onKeyUp, true)
|
|
202
213
|
if (document.body.style.cursor === cursor) {
|
|
203
214
|
document.body.style.cursor = ''
|
|
204
215
|
}
|
|
@@ -213,7 +224,8 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
213
224
|
const clearOverride = () => {
|
|
214
225
|
useLiveNodeOverrides.getState().clear(overrideId)
|
|
215
226
|
if (markDirty) {
|
|
216
|
-
|
|
227
|
+
for (const id of cascadeDirty(overrideId, { scene: createSceneApi(useScene) }))
|
|
228
|
+
useScene.getState().markDirty(id)
|
|
217
229
|
}
|
|
218
230
|
}
|
|
219
231
|
|
|
@@ -240,17 +252,25 @@ export function useHandleDrag(args: UseHandleDragArgs) {
|
|
|
240
252
|
// Escape / ⌘Z abort the drag — capture phase so they win over the global
|
|
241
253
|
// use-keyboard arms (⌘Z must never history-jump under a live pointer).
|
|
242
254
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
255
|
+
if (e.key === 'Alt') {
|
|
256
|
+
altKey = true
|
|
257
|
+
return
|
|
258
|
+
}
|
|
243
259
|
if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
|
|
244
260
|
e.preventDefault()
|
|
245
261
|
e.stopPropagation()
|
|
246
262
|
swallowNextClick()
|
|
247
263
|
onCancel()
|
|
248
264
|
}
|
|
265
|
+
const onKeyUp = (e: KeyboardEvent) => {
|
|
266
|
+
if (e.key === 'Alt') altKey = false
|
|
267
|
+
}
|
|
249
268
|
|
|
250
269
|
dragCleanupRef.current = onCancel
|
|
251
270
|
window.addEventListener('pointermove', onMove)
|
|
252
271
|
window.addEventListener('pointerup', onUp)
|
|
253
272
|
window.addEventListener('pointercancel', onCancel)
|
|
254
273
|
window.addEventListener('keydown', onKeyDown, true)
|
|
274
|
+
window.addEventListener('keyup', onKeyUp, true)
|
|
255
275
|
}
|
|
256
276
|
}
|
|
@@ -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,8 @@ import {
|
|
|
31
49
|
writePersistedSelection,
|
|
32
50
|
} from '../../lib/scene'
|
|
33
51
|
import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
|
|
52
|
+
import { useUnitFocusRules } from '../../lib/units'
|
|
53
|
+
import { type CameraHintAction, useCameraHintFocus } from '../../store/use-camera-hint-focus'
|
|
34
54
|
import useEditor from '../../store/use-editor'
|
|
35
55
|
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
36
56
|
import useSessionGroups from '../../store/use-session-groups'
|
|
@@ -52,7 +72,7 @@ import { PanelManager } from '../ui/panels/panel-manager'
|
|
|
52
72
|
import { ErrorBoundary } from '../ui/primitives/error-boundary'
|
|
53
73
|
import { useSidebarStore } from '../ui/primitives/sidebar'
|
|
54
74
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
|
|
55
|
-
import { SceneLoader } from '../ui/scene-loader'
|
|
75
|
+
import { SceneLoader, SceneLoadFailed } from '../ui/scene-loader'
|
|
56
76
|
import { AppSidebar } from '../ui/sidebar/app-sidebar'
|
|
57
77
|
import type { ExtraPanel } from '../ui/sidebar/icon-rail'
|
|
58
78
|
import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/settings-panel'
|
|
@@ -61,6 +81,7 @@ import type { SidebarTab } from '../ui/sidebar/tab-bar'
|
|
|
61
81
|
import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
|
|
62
82
|
import { ViewerStage } from '../viewer/viewer-stage'
|
|
63
83
|
import type { ViewerStageMode } from '../viewer/viewer-stage-modes'
|
|
84
|
+
import { CaptureCameraRig } from './capture-camera-rig'
|
|
64
85
|
import { CustomCameraControls } from './custom-camera-controls'
|
|
65
86
|
import { DeleteConfirmationDialog } from './delete-confirmation-dialog'
|
|
66
87
|
import { EditorLayoutV2 } from './editor-layout-v2'
|
|
@@ -98,7 +119,12 @@ const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
|
|
98
119
|
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
|
99
120
|
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
|
100
121
|
const SCENE_READY_FALLBACK_MS = 8000
|
|
122
|
+
const PRESENTATION_PROJECT_NOT_RESTORED = Symbol('presentation-project-not-restored')
|
|
123
|
+
const useClientLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
|
|
101
124
|
type PaintCursorBadgeState = 'empty' | 'ready' | 'blocked'
|
|
125
|
+
const recordEditorRender: ProfilerOnRenderCallback = (_id, _phase, actualDuration) => {
|
|
126
|
+
if (PERF_OVERLAY_ENABLED) recordPerfSample('react-render', actualDuration)
|
|
127
|
+
}
|
|
102
128
|
const EDITOR_HOVER_STYLES: HoverStyles = {
|
|
103
129
|
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
|
|
104
130
|
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
|
|
@@ -178,6 +204,12 @@ export interface EditorProps {
|
|
|
178
204
|
// Persistence — defaults to localStorage when omitted
|
|
179
205
|
onLoad?: () => Promise<SceneGraph | null>
|
|
180
206
|
onSave?: (scene: SceneGraph, options?: { keepalive?: boolean }) => Promise<void>
|
|
207
|
+
/**
|
|
208
|
+
* Cmd/Ctrl+S. Return true when the host handled the save (the community
|
|
209
|
+
* version checkpoint); anything else falls through to flushing the autosave,
|
|
210
|
+
* so the chord still saves when the host's control isn't mounted.
|
|
211
|
+
*/
|
|
212
|
+
onSaveShortcut?: () => boolean | undefined
|
|
181
213
|
onDirty?: () => void
|
|
182
214
|
onSaveStatusChange?: (status: SaveStatus) => void
|
|
183
215
|
|
|
@@ -380,7 +412,7 @@ type ShortcutKey = {
|
|
|
380
412
|
}
|
|
381
413
|
|
|
382
414
|
type CameraControlHint = {
|
|
383
|
-
action:
|
|
415
|
+
action: CameraHintAction
|
|
384
416
|
keys: ShortcutKey[]
|
|
385
417
|
alternativeKeys?: ShortcutKey[]
|
|
386
418
|
}
|
|
@@ -514,7 +546,15 @@ function ViewerCanvasControlsHint({
|
|
|
514
546
|
isPreviewMode: boolean
|
|
515
547
|
onDismiss: () => void
|
|
516
548
|
}) {
|
|
517
|
-
const
|
|
549
|
+
const all = isPreviewMode ? PREVIEW_CAMERA_CONTROL_HINTS : EDITOR_CAMERA_CONTROL_HINTS
|
|
550
|
+
// A host teaching one gesture at a time narrows this to the one it is asking
|
|
551
|
+
// for, and to nothing once it is done. Null — the default — is all of them.
|
|
552
|
+
const focus = useCameraHintFocus((state) => state.actions)
|
|
553
|
+
const hints = focus === null ? all : all.filter((hint) => focus.includes(hint.action))
|
|
554
|
+
|
|
555
|
+
if (hints.length === 0) {
|
|
556
|
+
return null
|
|
557
|
+
}
|
|
518
558
|
|
|
519
559
|
return (
|
|
520
560
|
<div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
|
|
@@ -522,7 +562,10 @@ function ViewerCanvasControlsHint({
|
|
|
522
562
|
aria-label="Camera controls hint"
|
|
523
563
|
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
564
|
>
|
|
525
|
-
<div
|
|
565
|
+
<div
|
|
566
|
+
className="grid min-w-0 flex-1 items-start divide-x divide-border/18"
|
|
567
|
+
style={{ gridTemplateColumns: `repeat(${hints.length}, minmax(0, 1fr))` }}
|
|
568
|
+
>
|
|
526
569
|
{hints.map((hint) => (
|
|
527
570
|
<CameraControlHintItem hint={hint} key={hint.action} />
|
|
528
571
|
))}
|
|
@@ -743,6 +786,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
743
786
|
isStudioMode,
|
|
744
787
|
onThumbnailCapture,
|
|
745
788
|
viewerSceneSlot,
|
|
789
|
+
presentationsReady,
|
|
746
790
|
}: {
|
|
747
791
|
isVersionPreviewMode: boolean
|
|
748
792
|
isLoading: boolean
|
|
@@ -750,6 +794,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
750
794
|
isStudioMode: boolean
|
|
751
795
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
752
796
|
viewerSceneSlot?: ReactNode
|
|
797
|
+
presentationsReady: boolean
|
|
753
798
|
}) {
|
|
754
799
|
// Studio mode is a clean render/snapshot surface — no selection or editing
|
|
755
800
|
// affordances. It mirrors version-preview's chrome gating on the canvas.
|
|
@@ -784,10 +829,12 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
784
829
|
{!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
|
|
785
830
|
{!(isLoading || noEditing) && <ToolManager />}
|
|
786
831
|
{isFirstPersonMode && <FirstPersonControls />}
|
|
832
|
+
{isCaptureMode && <CaptureCameraRig />}
|
|
787
833
|
<CustomCameraControls />
|
|
788
834
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
789
835
|
{!isFirstPersonMode && <SiteEdgeLabels />}
|
|
790
836
|
<InteractiveSystem />
|
|
837
|
+
{presentationsReady ? <ViewerPresentations /> : null}
|
|
791
838
|
{!noEditing && viewerSceneSlot}
|
|
792
839
|
</>
|
|
793
840
|
)
|
|
@@ -971,6 +1018,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
971
1018
|
sceneReadyKey,
|
|
972
1019
|
onSceneReadyChange,
|
|
973
1020
|
onThumbnailCapture,
|
|
1021
|
+
presentationsReady,
|
|
974
1022
|
viewerSceneSlot,
|
|
975
1023
|
floorplanSceneSlot,
|
|
976
1024
|
disablePostFx = false,
|
|
@@ -984,6 +1032,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
984
1032
|
sceneReadyKey: number
|
|
985
1033
|
onSceneReadyChange: (ready: boolean) => void
|
|
986
1034
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
1035
|
+
presentationsReady: boolean
|
|
987
1036
|
viewerSceneSlot?: ReactNode
|
|
988
1037
|
floorplanSceneSlot?: ReactNode
|
|
989
1038
|
disablePostFx?: boolean
|
|
@@ -992,6 +1041,11 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
992
1041
|
const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
|
|
993
1042
|
const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio)
|
|
994
1043
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
1044
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
1045
|
+
useUnitFocusRules()
|
|
1046
|
+
const presetIsolation = useEditor((s) =>
|
|
1047
|
+
s.captureMode.mode === 'preset' ? s.captureMode.isolated : null,
|
|
1048
|
+
)
|
|
995
1049
|
|
|
996
1050
|
const [isCameraControlsHintVisible, setIsCameraControlsHintVisible] = useState<boolean | null>(
|
|
997
1051
|
null,
|
|
@@ -1102,11 +1156,18 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1102
1156
|
defaultRender={EDITOR_DEFAULT_RENDER}
|
|
1103
1157
|
disablePostFx={disablePostFx}
|
|
1104
1158
|
hoverStyles={EDITOR_HOVER_STYLES}
|
|
1159
|
+
isolate={presetIsolation}
|
|
1160
|
+
// Preset captures isolate one subtree and keep the exterior transparent.
|
|
1161
|
+
// Other modes retain the viewer's configured background policy.
|
|
1162
|
+
transparent={presetIsolation === null ? undefined : true}
|
|
1105
1163
|
onSceneReadyChange={onSceneReadyChange}
|
|
1106
1164
|
renderContext="editor"
|
|
1107
1165
|
renderPaused={!show3d && !showLoader}
|
|
1108
1166
|
sceneReadyKey={sceneReadyKey}
|
|
1109
|
-
|
|
1167
|
+
// Walk/drone framing during snapshot capture is camera-only: the
|
|
1168
|
+
// viewer's default selection manager would hover-highlight whatever
|
|
1169
|
+
// the cursor crosses, which orbit capture never does.
|
|
1170
|
+
selectionManager={isFirstPersonMode && !isCaptureMode ? 'default' : 'custom'}
|
|
1110
1171
|
>
|
|
1111
1172
|
<ViewerSceneContent
|
|
1112
1173
|
isFirstPersonMode={isFirstPersonMode}
|
|
@@ -1114,6 +1175,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1114
1175
|
isStudioMode={isStudioMode}
|
|
1115
1176
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
1116
1177
|
onThumbnailCapture={onThumbnailCapture}
|
|
1178
|
+
presentationsReady={presentationsReady}
|
|
1117
1179
|
viewerSceneSlot={viewerSceneSlot}
|
|
1118
1180
|
/>
|
|
1119
1181
|
</Viewer>
|
|
@@ -1178,7 +1240,7 @@ function PreviewStage({
|
|
|
1178
1240
|
)
|
|
1179
1241
|
}
|
|
1180
1242
|
|
|
1181
|
-
|
|
1243
|
+
function EditorContent({
|
|
1182
1244
|
layoutVersion = 'v1',
|
|
1183
1245
|
appMenuButton,
|
|
1184
1246
|
sidebarTop,
|
|
@@ -1194,6 +1256,7 @@ export default function Editor({
|
|
|
1194
1256
|
projectId,
|
|
1195
1257
|
onLoad,
|
|
1196
1258
|
onSave,
|
|
1259
|
+
onSaveShortcut,
|
|
1197
1260
|
onDirty,
|
|
1198
1261
|
onSaveStatusChange,
|
|
1199
1262
|
previewScene,
|
|
@@ -1211,18 +1274,51 @@ export default function Editor({
|
|
|
1211
1274
|
}: EditorProps) {
|
|
1212
1275
|
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
1213
1276
|
const isStudioMode = useEditor((s) => s.workspaceMode === 'studio')
|
|
1277
|
+
const presentationProjectId = projectId ?? null
|
|
1278
|
+
const presentationPersistenceRef = useRef<LocalProjectPresentationPersistence | null>(null)
|
|
1279
|
+
const [restoredPresentationProjectId, setRestoredPresentationProjectId] = useState<
|
|
1280
|
+
string | null | typeof PRESENTATION_PROJECT_NOT_RESTORED
|
|
1281
|
+
>(PRESENTATION_PROJECT_NOT_RESTORED)
|
|
1282
|
+
const presentationsReady = restoredPresentationProjectId === presentationProjectId
|
|
1283
|
+
|
|
1284
|
+
useClientLayoutEffect(() => {
|
|
1285
|
+
const persistence = createLocalProjectPresentationPersistence()
|
|
1286
|
+
presentationPersistenceRef.current = persistence
|
|
1287
|
+
return () => {
|
|
1288
|
+
presentationPersistenceRef.current = null
|
|
1289
|
+
persistence.dispose()
|
|
1290
|
+
}
|
|
1291
|
+
}, [])
|
|
1292
|
+
|
|
1293
|
+
useClientLayoutEffect(() => {
|
|
1294
|
+
const persistence = presentationPersistenceRef.current
|
|
1295
|
+
if (!persistence) return
|
|
1296
|
+
persistence.switchProject(presentationProjectId)
|
|
1297
|
+
setRestoredPresentationProjectId(presentationProjectId)
|
|
1298
|
+
}, [presentationProjectId])
|
|
1214
1299
|
|
|
1215
1300
|
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
|
|
1216
1301
|
|
|
1217
|
-
const { isLoadingSceneRef } = useAutoSave({
|
|
1302
|
+
const { isLoadingSceneRef, saveNow } = useAutoSave({
|
|
1218
1303
|
onSave,
|
|
1219
1304
|
onDirty,
|
|
1220
1305
|
onSaveStatusChange,
|
|
1221
1306
|
isVersionPreviewMode,
|
|
1222
1307
|
})
|
|
1223
1308
|
|
|
1309
|
+
const handleSaveShortcut = useCallback(() => {
|
|
1310
|
+
if (onSaveShortcut?.() === true) return
|
|
1311
|
+
saveNow()
|
|
1312
|
+
}, [onSaveShortcut, saveNow])
|
|
1313
|
+
useSaveShortcut(handleSaveShortcut)
|
|
1314
|
+
|
|
1224
1315
|
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
|
1225
1316
|
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
|
|
1317
|
+
// A failed `onLoad` is shown as an error with a retry, never as an empty
|
|
1318
|
+
// scene: an editor that renders the default scaffold after a failed load
|
|
1319
|
+
// autosaves that scaffold over the real project.
|
|
1320
|
+
const [sceneLoadError, setSceneLoadError] = useState<unknown>(null)
|
|
1321
|
+
const [sceneLoadAttempt, setSceneLoadAttempt] = useState(0)
|
|
1226
1322
|
const [sceneReadyKey, setSceneReadyKey] = useState(0)
|
|
1227
1323
|
const [isViewerSceneReady, setIsViewerSceneReady] = useState(false)
|
|
1228
1324
|
const [previewStageMode, setPreviewStageMode] = useState<ViewerStageMode>('3d')
|
|
@@ -1258,12 +1354,13 @@ export default function Editor({
|
|
|
1258
1354
|
}
|
|
1259
1355
|
}, [projectId])
|
|
1260
1356
|
|
|
1261
|
-
// Load
|
|
1357
|
+
// Load on mount, project switches, and explicit retry attempts.
|
|
1262
1358
|
useEffect(() => {
|
|
1263
1359
|
let cancelled = false
|
|
1264
1360
|
|
|
1265
|
-
async function load() {
|
|
1361
|
+
async function load(attempt: number) {
|
|
1266
1362
|
isLoadingSceneRef.current = true
|
|
1363
|
+
setSceneLoadError(null)
|
|
1267
1364
|
setHasLoadedInitialScene(false)
|
|
1268
1365
|
setIsViewerSceneReady(false)
|
|
1269
1366
|
setIsSceneLoading(true)
|
|
@@ -1272,36 +1369,45 @@ export default function Editor({
|
|
|
1272
1369
|
// Session groups are not scene-graph state — clear on every load/switch.
|
|
1273
1370
|
useSessionGroups.getState().clearGroups()
|
|
1274
1371
|
|
|
1372
|
+
let failed = false
|
|
1275
1373
|
try {
|
|
1276
1374
|
const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
|
|
1277
|
-
if (!cancelled) {
|
|
1375
|
+
if (!cancelled && attempt === sceneLoadAttempt) {
|
|
1278
1376
|
applySceneGraphToEditor(sceneGraph)
|
|
1279
1377
|
setIsViewerSceneReady(false)
|
|
1280
1378
|
setSceneReadyKey((key) => key + 1)
|
|
1281
1379
|
}
|
|
1282
|
-
} catch {
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
// Leave the store unloaded and the autosave loop in its loading
|
|
1382
|
+
// state: nothing may be written until a load actually succeeds.
|
|
1383
|
+
failed = true
|
|
1283
1384
|
if (!cancelled) {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
setSceneReadyKey((key) => key + 1)
|
|
1385
|
+
console.error('[editor] scene load failed', error)
|
|
1386
|
+
setSceneLoadError(error ?? new Error('Scene load failed'))
|
|
1287
1387
|
}
|
|
1288
1388
|
} finally {
|
|
1289
1389
|
if (!cancelled) {
|
|
1290
1390
|
setIsSceneLoading(false)
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1391
|
+
if (!failed) {
|
|
1392
|
+
setHasLoadedInitialScene(true)
|
|
1393
|
+
requestAnimationFrame(() => {
|
|
1394
|
+
isLoadingSceneRef.current = false
|
|
1395
|
+
})
|
|
1396
|
+
}
|
|
1295
1397
|
}
|
|
1296
1398
|
}
|
|
1297
1399
|
}
|
|
1298
1400
|
|
|
1299
|
-
load()
|
|
1401
|
+
load(sceneLoadAttempt)
|
|
1300
1402
|
|
|
1301
1403
|
return () => {
|
|
1302
1404
|
cancelled = true
|
|
1303
1405
|
}
|
|
1304
|
-
}, [onLoad, isLoadingSceneRef])
|
|
1406
|
+
}, [onLoad, isLoadingSceneRef, sceneLoadAttempt])
|
|
1407
|
+
|
|
1408
|
+
const retrySceneLoad = useCallback(() => {
|
|
1409
|
+
setSceneLoadAttempt((attempt) => attempt + 1)
|
|
1410
|
+
}, [])
|
|
1305
1411
|
|
|
1306
1412
|
// Apply preview scene when version preview mode changes
|
|
1307
1413
|
useEffect(() => {
|
|
@@ -1417,6 +1523,7 @@ export default function Editor({
|
|
|
1417
1523
|
<CustomCameraControls />
|
|
1418
1524
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
1419
1525
|
<InteractiveSystem />
|
|
1526
|
+
{presentationsReady ? <ViewerPresentations /> : null}
|
|
1420
1527
|
</Viewer>
|
|
1421
1528
|
)
|
|
1422
1529
|
|
|
@@ -1430,6 +1537,7 @@ export default function Editor({
|
|
|
1430
1537
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
1431
1538
|
onSceneReadyChange={handleSceneReadyChange}
|
|
1432
1539
|
onThumbnailCapture={onThumbnailCapture}
|
|
1540
|
+
presentationsReady={presentationsReady}
|
|
1433
1541
|
sceneReadyKey={sceneReadyKey}
|
|
1434
1542
|
showLoader={showLoader}
|
|
1435
1543
|
viewerSceneSlot={viewerSceneSlot}
|
|
@@ -1490,7 +1598,11 @@ export default function Editor({
|
|
|
1490
1598
|
<FloorplanModeCoordinator />
|
|
1491
1599
|
{visibleLoader && (
|
|
1492
1600
|
<div className="fixed inset-0 z-60">
|
|
1493
|
-
|
|
1601
|
+
{sceneLoadError ? (
|
|
1602
|
+
<SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
|
|
1603
|
+
) : (
|
|
1604
|
+
<SceneLoader className="bg-background" />
|
|
1605
|
+
)}
|
|
1494
1606
|
</div>
|
|
1495
1607
|
)}
|
|
1496
1608
|
|
|
@@ -1527,7 +1639,10 @@ export default function Editor({
|
|
|
1527
1639
|
<HelperManager />
|
|
1528
1640
|
</div>
|
|
1529
1641
|
)}
|
|
1530
|
-
{
|
|
1642
|
+
{/* Capture mode drives walk / drone from its own overlay, which
|
|
1643
|
+
owns the framing chrome — the walkthrough HUD would both
|
|
1644
|
+
clutter the frame and offer a second, conflicting exit. */}
|
|
1645
|
+
{isFirstPersonMode && !isCaptureMode && (
|
|
1531
1646
|
<FirstPersonOverlay
|
|
1532
1647
|
onExit={() => useEditor.getState().setFirstPersonMode(false)}
|
|
1533
1648
|
/>
|
|
@@ -1563,7 +1678,11 @@ export default function Editor({
|
|
|
1563
1678
|
<FloorplanModeCoordinator />
|
|
1564
1679
|
{visibleLoader && (
|
|
1565
1680
|
<div className="fixed inset-0 z-60">
|
|
1566
|
-
|
|
1681
|
+
{sceneLoadError ? (
|
|
1682
|
+
<SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
|
|
1683
|
+
) : (
|
|
1684
|
+
<SceneLoader className="bg-background" />
|
|
1685
|
+
)}
|
|
1567
1686
|
</div>
|
|
1568
1687
|
)}
|
|
1569
1688
|
|
|
@@ -1613,3 +1732,11 @@ export default function Editor({
|
|
|
1613
1732
|
</div>
|
|
1614
1733
|
)
|
|
1615
1734
|
}
|
|
1735
|
+
|
|
1736
|
+
export default function Editor(props: EditorProps) {
|
|
1737
|
+
return (
|
|
1738
|
+
<Profiler id="editor" onRender={recordEditorRender}>
|
|
1739
|
+
<EditorContent {...props} />
|
|
1740
|
+
</Profiler>
|
|
1741
|
+
)
|
|
1742
|
+
}
|