@pascal-app/editor 1.0.0-beta.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +63 -8
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +23 -6
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/panel-wrapper.tsx +228 -12
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -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,8 +49,10 @@ 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'
|
|
55
|
+
import useSessionGroups from '../../store/use-session-groups'
|
|
36
56
|
import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
|
|
37
57
|
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
38
58
|
import { RoofEditSystem } from '../systems/roof/roof-edit-system'
|
|
@@ -51,13 +71,16 @@ import { PanelManager } from '../ui/panels/panel-manager'
|
|
|
51
71
|
import { ErrorBoundary } from '../ui/primitives/error-boundary'
|
|
52
72
|
import { useSidebarStore } from '../ui/primitives/sidebar'
|
|
53
73
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
|
|
54
|
-
import { SceneLoader } from '../ui/scene-loader'
|
|
74
|
+
import { SceneLoader, SceneLoadFailed } from '../ui/scene-loader'
|
|
55
75
|
import { AppSidebar } from '../ui/sidebar/app-sidebar'
|
|
56
76
|
import type { ExtraPanel } from '../ui/sidebar/icon-rail'
|
|
57
77
|
import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/settings-panel'
|
|
58
78
|
import { SitePanel, type SitePanelProps } from '../ui/sidebar/panels/site-panel'
|
|
59
79
|
import type { SidebarTab } from '../ui/sidebar/tab-bar'
|
|
60
80
|
import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
|
|
81
|
+
import { ViewerStage } from '../viewer/viewer-stage'
|
|
82
|
+
import type { ViewerStageMode } from '../viewer/viewer-stage-modes'
|
|
83
|
+
import { CaptureCameraRig } from './capture-camera-rig'
|
|
61
84
|
import { CustomCameraControls } from './custom-camera-controls'
|
|
62
85
|
import { DeleteConfirmationDialog } from './delete-confirmation-dialog'
|
|
63
86
|
import { EditorLayoutV2 } from './editor-layout-v2'
|
|
@@ -85,6 +108,8 @@ import { WallMoveSideHandles } from './wall-move-side-handles'
|
|
|
85
108
|
import { WallOpeningHighlights } from './wall-opening-highlights'
|
|
86
109
|
|
|
87
110
|
const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-dismissed:v1'
|
|
111
|
+
const PREVIEW_STAGE_SWITCHER_POSITION =
|
|
112
|
+
'top-28 right-4 left-auto translate-x-0 md:top-4 md:right-auto md:left-1/2 md:-translate-x-1/2'
|
|
88
113
|
const DELETE_CURSOR_BADGE_COLOR = '#ef4444'
|
|
89
114
|
const DELETE_CURSOR_BADGE_OFFSET_X = 14
|
|
90
115
|
const DELETE_CURSOR_BADGE_OFFSET_Y = 14
|
|
@@ -93,7 +118,12 @@ const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
|
|
93
118
|
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
|
94
119
|
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
|
95
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
|
|
96
123
|
type PaintCursorBadgeState = 'empty' | 'ready' | 'blocked'
|
|
124
|
+
const recordEditorRender: ProfilerOnRenderCallback = (_id, _phase, actualDuration) => {
|
|
125
|
+
if (PERF_OVERLAY_ENABLED) recordPerfSample('react-render', actualDuration)
|
|
126
|
+
}
|
|
97
127
|
const EDITOR_HOVER_STYLES: HoverStyles = {
|
|
98
128
|
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
|
|
99
129
|
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
|
|
@@ -173,6 +203,12 @@ export interface EditorProps {
|
|
|
173
203
|
// Persistence — defaults to localStorage when omitted
|
|
174
204
|
onLoad?: () => Promise<SceneGraph | null>
|
|
175
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
|
|
176
212
|
onDirty?: () => void
|
|
177
213
|
onSaveStatusChange?: (status: SaveStatus) => void
|
|
178
214
|
|
|
@@ -190,6 +226,13 @@ export interface EditorProps {
|
|
|
190
226
|
// Thumbnail
|
|
191
227
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
192
228
|
|
|
229
|
+
/**
|
|
230
|
+
* When true, skip the viewer post-FX pipeline (same as `?disable=postFx`).
|
|
231
|
+
* Hosts use this for a stable local "light preview" without relying only on
|
|
232
|
+
* module-load URL flags or shading toggles.
|
|
233
|
+
*/
|
|
234
|
+
disablePostFx?: boolean
|
|
235
|
+
|
|
193
236
|
// Version preview overlays (rendered by host app)
|
|
194
237
|
sidebarOverlay?: ReactNode
|
|
195
238
|
viewerBanner?: ReactNode
|
|
@@ -368,7 +411,7 @@ type ShortcutKey = {
|
|
|
368
411
|
}
|
|
369
412
|
|
|
370
413
|
type CameraControlHint = {
|
|
371
|
-
action:
|
|
414
|
+
action: CameraHintAction
|
|
372
415
|
keys: ShortcutKey[]
|
|
373
416
|
alternativeKeys?: ShortcutKey[]
|
|
374
417
|
}
|
|
@@ -502,7 +545,15 @@ function ViewerCanvasControlsHint({
|
|
|
502
545
|
isPreviewMode: boolean
|
|
503
546
|
onDismiss: () => void
|
|
504
547
|
}) {
|
|
505
|
-
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
|
+
}
|
|
506
557
|
|
|
507
558
|
return (
|
|
508
559
|
<div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
|
|
@@ -510,7 +561,10 @@ function ViewerCanvasControlsHint({
|
|
|
510
561
|
aria-label="Camera controls hint"
|
|
511
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"
|
|
512
563
|
>
|
|
513
|
-
<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
|
+
>
|
|
514
568
|
{hints.map((hint) => (
|
|
515
569
|
<CameraControlHintItem hint={hint} key={hint.action} />
|
|
516
570
|
))}
|
|
@@ -731,6 +785,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
731
785
|
isStudioMode,
|
|
732
786
|
onThumbnailCapture,
|
|
733
787
|
viewerSceneSlot,
|
|
788
|
+
presentationsReady,
|
|
734
789
|
}: {
|
|
735
790
|
isVersionPreviewMode: boolean
|
|
736
791
|
isLoading: boolean
|
|
@@ -738,6 +793,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
738
793
|
isStudioMode: boolean
|
|
739
794
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
740
795
|
viewerSceneSlot?: ReactNode
|
|
796
|
+
presentationsReady: boolean
|
|
741
797
|
}) {
|
|
742
798
|
// Studio mode is a clean render/snapshot surface — no selection or editing
|
|
743
799
|
// affordances. It mirrors version-preview's chrome gating on the canvas.
|
|
@@ -772,10 +828,12 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
772
828
|
{!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
|
|
773
829
|
{!(isLoading || noEditing) && <ToolManager />}
|
|
774
830
|
{isFirstPersonMode && <FirstPersonControls />}
|
|
831
|
+
{isCaptureMode && <CaptureCameraRig />}
|
|
775
832
|
<CustomCameraControls />
|
|
776
833
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
777
834
|
{!isFirstPersonMode && <SiteEdgeLabels />}
|
|
778
835
|
<InteractiveSystem />
|
|
836
|
+
{presentationsReady ? <ViewerPresentations /> : null}
|
|
779
837
|
{!noEditing && viewerSceneSlot}
|
|
780
838
|
</>
|
|
781
839
|
)
|
|
@@ -959,8 +1017,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
959
1017
|
sceneReadyKey,
|
|
960
1018
|
onSceneReadyChange,
|
|
961
1019
|
onThumbnailCapture,
|
|
1020
|
+
presentationsReady,
|
|
962
1021
|
viewerSceneSlot,
|
|
963
1022
|
floorplanSceneSlot,
|
|
1023
|
+
disablePostFx = false,
|
|
964
1024
|
}: {
|
|
965
1025
|
isVersionPreviewMode: boolean
|
|
966
1026
|
isLoading: boolean
|
|
@@ -971,13 +1031,16 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
971
1031
|
sceneReadyKey: number
|
|
972
1032
|
onSceneReadyChange: (ready: boolean) => void
|
|
973
1033
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
1034
|
+
presentationsReady: boolean
|
|
974
1035
|
viewerSceneSlot?: ReactNode
|
|
975
1036
|
floorplanSceneSlot?: ReactNode
|
|
1037
|
+
disablePostFx?: boolean
|
|
976
1038
|
}) {
|
|
977
1039
|
const viewMode = useEditor((s) => s.viewMode)
|
|
978
1040
|
const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
|
|
979
1041
|
const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio)
|
|
980
1042
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
1043
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
981
1044
|
|
|
982
1045
|
const [isCameraControlsHintVisible, setIsCameraControlsHintVisible] = useState<boolean | null>(
|
|
983
1046
|
null,
|
|
@@ -1086,11 +1149,16 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1086
1149
|
<SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
|
|
1087
1150
|
<Viewer
|
|
1088
1151
|
defaultRender={EDITOR_DEFAULT_RENDER}
|
|
1152
|
+
disablePostFx={disablePostFx}
|
|
1089
1153
|
hoverStyles={EDITOR_HOVER_STYLES}
|
|
1090
1154
|
onSceneReadyChange={onSceneReadyChange}
|
|
1091
1155
|
renderContext="editor"
|
|
1156
|
+
renderPaused={!show3d && !showLoader}
|
|
1092
1157
|
sceneReadyKey={sceneReadyKey}
|
|
1093
|
-
|
|
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'}
|
|
1094
1162
|
>
|
|
1095
1163
|
<ViewerSceneContent
|
|
1096
1164
|
isFirstPersonMode={isFirstPersonMode}
|
|
@@ -1098,6 +1166,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1098
1166
|
isStudioMode={isStudioMode}
|
|
1099
1167
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
1100
1168
|
onThumbnailCapture={onThumbnailCapture}
|
|
1169
|
+
presentationsReady={presentationsReady}
|
|
1101
1170
|
viewerSceneSlot={viewerSceneSlot}
|
|
1102
1171
|
/>
|
|
1103
1172
|
</Viewer>
|
|
@@ -1108,7 +1177,61 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1108
1177
|
)
|
|
1109
1178
|
})
|
|
1110
1179
|
|
|
1111
|
-
|
|
1180
|
+
function PreviewStage({
|
|
1181
|
+
isFirstPersonMode,
|
|
1182
|
+
mode,
|
|
1183
|
+
onModeChange,
|
|
1184
|
+
showLoader,
|
|
1185
|
+
viewerContent,
|
|
1186
|
+
}: {
|
|
1187
|
+
isFirstPersonMode: boolean
|
|
1188
|
+
mode: ViewerStageMode
|
|
1189
|
+
onModeChange: (mode: ViewerStageMode) => void
|
|
1190
|
+
showLoader: boolean
|
|
1191
|
+
viewerContent: ReactNode
|
|
1192
|
+
}) {
|
|
1193
|
+
const hasFloorplan = useScene((state) =>
|
|
1194
|
+
Object.values(state.nodes).some((node) => node.type === 'level'),
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
const handleModeChange = useCallback(
|
|
1198
|
+
(nextMode: ViewerStageMode) => {
|
|
1199
|
+
if (nextMode !== '3d') useEditor.getState().setFirstPersonMode(false)
|
|
1200
|
+
onModeChange(nextMode)
|
|
1201
|
+
},
|
|
1202
|
+
[onModeChange],
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
const stageMode = isFirstPersonMode || !hasFloorplan ? '3d' : mode
|
|
1206
|
+
const stageModes = hasFloorplan && !isFirstPersonMode ? undefined : (['3d'] as const)
|
|
1207
|
+
|
|
1208
|
+
return (
|
|
1209
|
+
<div className="dark relative h-full w-full overflow-hidden bg-neutral-100 text-foreground">
|
|
1210
|
+
{isFirstPersonMode ? (
|
|
1211
|
+
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
1212
|
+
) : (
|
|
1213
|
+
<ViewerOverlay
|
|
1214
|
+
hideBottomBar={stageMode !== '3d'}
|
|
1215
|
+
onBack={() => useEditor.getState().setPreviewMode(false)}
|
|
1216
|
+
/>
|
|
1217
|
+
)}
|
|
1218
|
+
|
|
1219
|
+
<ViewerStage
|
|
1220
|
+
className="absolute inset-0"
|
|
1221
|
+
mode={stageMode}
|
|
1222
|
+
modes={stageModes}
|
|
1223
|
+
onModeChange={handleModeChange}
|
|
1224
|
+
showCompass={hasFloorplan && !isFirstPersonMode}
|
|
1225
|
+
showSwitcher={hasFloorplan && !isFirstPersonMode}
|
|
1226
|
+
switcherClassName={`${PREVIEW_STAGE_SWITCHER_POSITION} ${showLoader ? 'z-[70]' : ''}`}
|
|
1227
|
+
>
|
|
1228
|
+
{viewerContent}
|
|
1229
|
+
</ViewerStage>
|
|
1230
|
+
</div>
|
|
1231
|
+
)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function EditorContent({
|
|
1112
1235
|
layoutVersion = 'v1',
|
|
1113
1236
|
appMenuButton,
|
|
1114
1237
|
sidebarTop,
|
|
@@ -1124,6 +1247,7 @@ export default function Editor({
|
|
|
1124
1247
|
projectId,
|
|
1125
1248
|
onLoad,
|
|
1126
1249
|
onSave,
|
|
1250
|
+
onSaveShortcut,
|
|
1127
1251
|
onDirty,
|
|
1128
1252
|
onSaveStatusChange,
|
|
1129
1253
|
previewScene,
|
|
@@ -1131,6 +1255,7 @@ export default function Editor({
|
|
|
1131
1255
|
isLoading = false,
|
|
1132
1256
|
onLoaderChange,
|
|
1133
1257
|
onThumbnailCapture,
|
|
1258
|
+
disablePostFx = false,
|
|
1134
1259
|
sidebarOverlay,
|
|
1135
1260
|
viewerBanner,
|
|
1136
1261
|
settingsPanelProps,
|
|
@@ -1140,20 +1265,54 @@ export default function Editor({
|
|
|
1140
1265
|
}: EditorProps) {
|
|
1141
1266
|
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
1142
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])
|
|
1143
1290
|
|
|
1144
1291
|
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
|
|
1145
1292
|
|
|
1146
|
-
const { isLoadingSceneRef } = useAutoSave({
|
|
1293
|
+
const { isLoadingSceneRef, saveNow } = useAutoSave({
|
|
1147
1294
|
onSave,
|
|
1148
1295
|
onDirty,
|
|
1149
1296
|
onSaveStatusChange,
|
|
1150
1297
|
isVersionPreviewMode,
|
|
1151
1298
|
})
|
|
1152
1299
|
|
|
1300
|
+
const handleSaveShortcut = useCallback(() => {
|
|
1301
|
+
if (onSaveShortcut?.() === true) return
|
|
1302
|
+
saveNow()
|
|
1303
|
+
}, [onSaveShortcut, saveNow])
|
|
1304
|
+
useSaveShortcut(handleSaveShortcut)
|
|
1305
|
+
|
|
1153
1306
|
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
|
1154
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)
|
|
1155
1313
|
const [sceneReadyKey, setSceneReadyKey] = useState(0)
|
|
1156
1314
|
const [isViewerSceneReady, setIsViewerSceneReady] = useState(false)
|
|
1315
|
+
const [previewStageMode, setPreviewStageMode] = useState<ViewerStageMode>('3d')
|
|
1157
1316
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
1158
1317
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
1159
1318
|
|
|
@@ -1186,18 +1345,23 @@ export default function Editor({
|
|
|
1186
1345
|
}
|
|
1187
1346
|
}, [projectId])
|
|
1188
1347
|
|
|
1189
|
-
// Load
|
|
1348
|
+
// Load on mount, project switches, and explicit retry attempts.
|
|
1190
1349
|
useEffect(() => {
|
|
1350
|
+
void sceneLoadAttempt
|
|
1191
1351
|
let cancelled = false
|
|
1192
1352
|
|
|
1193
1353
|
async function load() {
|
|
1194
1354
|
isLoadingSceneRef.current = true
|
|
1355
|
+
setSceneLoadError(null)
|
|
1195
1356
|
setHasLoadedInitialScene(false)
|
|
1196
1357
|
setIsViewerSceneReady(false)
|
|
1197
1358
|
setIsSceneLoading(true)
|
|
1198
1359
|
useScene.getState().unloadScene()
|
|
1199
1360
|
useViewer.getState().resetSelection()
|
|
1361
|
+
// Session groups are not scene-graph state — clear on every load/switch.
|
|
1362
|
+
useSessionGroups.getState().clearGroups()
|
|
1200
1363
|
|
|
1364
|
+
let failed = false
|
|
1201
1365
|
try {
|
|
1202
1366
|
const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
|
|
1203
1367
|
if (!cancelled) {
|
|
@@ -1205,19 +1369,23 @@ export default function Editor({
|
|
|
1205
1369
|
setIsViewerSceneReady(false)
|
|
1206
1370
|
setSceneReadyKey((key) => key + 1)
|
|
1207
1371
|
}
|
|
1208
|
-
} 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
|
|
1209
1376
|
if (!cancelled) {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
setSceneReadyKey((key) => key + 1)
|
|
1377
|
+
console.error('[editor] scene load failed', error)
|
|
1378
|
+
setSceneLoadError(error ?? new Error('Scene load failed'))
|
|
1213
1379
|
}
|
|
1214
1380
|
} finally {
|
|
1215
1381
|
if (!cancelled) {
|
|
1216
1382
|
setIsSceneLoading(false)
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1383
|
+
if (!failed) {
|
|
1384
|
+
setHasLoadedInitialScene(true)
|
|
1385
|
+
requestAnimationFrame(() => {
|
|
1386
|
+
isLoadingSceneRef.current = false
|
|
1387
|
+
})
|
|
1388
|
+
}
|
|
1221
1389
|
}
|
|
1222
1390
|
}
|
|
1223
1391
|
}
|
|
@@ -1227,11 +1395,18 @@ export default function Editor({
|
|
|
1227
1395
|
return () => {
|
|
1228
1396
|
cancelled = true
|
|
1229
1397
|
}
|
|
1230
|
-
}, [onLoad, isLoadingSceneRef])
|
|
1398
|
+
}, [onLoad, isLoadingSceneRef, sceneLoadAttempt])
|
|
1399
|
+
|
|
1400
|
+
const retrySceneLoad = useCallback(() => {
|
|
1401
|
+
setSceneLoadAttempt((attempt) => attempt + 1)
|
|
1402
|
+
}, [])
|
|
1231
1403
|
|
|
1232
1404
|
// Apply preview scene when version preview mode changes
|
|
1233
1405
|
useEffect(() => {
|
|
1234
1406
|
if (isVersionPreviewMode && previewScene) {
|
|
1407
|
+
// Drop session groups from the edit session so plain-click expand cannot
|
|
1408
|
+
// pull in members that are not part of the preview graph.
|
|
1409
|
+
useSessionGroups.getState().clearGroups()
|
|
1235
1410
|
applySceneGraphToEditor(previewScene)
|
|
1236
1411
|
}
|
|
1237
1412
|
}, [isVersionPreviewMode, previewScene])
|
|
@@ -1244,6 +1419,10 @@ export default function Editor({
|
|
|
1244
1419
|
return releaseReadOnly
|
|
1245
1420
|
}, [isVersionPreviewMode])
|
|
1246
1421
|
|
|
1422
|
+
useEffect(() => {
|
|
1423
|
+
if (!isPreviewMode) setPreviewStageMode('3d')
|
|
1424
|
+
}, [isPreviewMode])
|
|
1425
|
+
|
|
1247
1426
|
useEffect(() => {
|
|
1248
1427
|
document.body.classList.add('dark')
|
|
1249
1428
|
return () => {
|
|
@@ -1269,10 +1448,19 @@ export default function Editor({
|
|
|
1269
1448
|
}, [hasLoadedInitialScene, isLoading, isSceneLoading, isViewerSceneReady, sceneReadyKey])
|
|
1270
1449
|
|
|
1271
1450
|
const showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
|
|
1451
|
+
const visibleLoader =
|
|
1452
|
+
showLoader &&
|
|
1453
|
+
!(
|
|
1454
|
+
isPreviewMode &&
|
|
1455
|
+
previewStageMode === '2d' &&
|
|
1456
|
+
!isLoading &&
|
|
1457
|
+
!isSceneLoading &&
|
|
1458
|
+
hasLoadedInitialScene
|
|
1459
|
+
)
|
|
1272
1460
|
|
|
1273
1461
|
useEffect(() => {
|
|
1274
|
-
onLoaderChange?.(
|
|
1275
|
-
}, [
|
|
1462
|
+
onLoaderChange?.(visibleLoader)
|
|
1463
|
+
}, [visibleLoader, onLoaderChange])
|
|
1276
1464
|
|
|
1277
1465
|
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
|
1278
1466
|
const wasFirstPersonModeRef = useRef(isFirstPersonMode)
|
|
@@ -1313,6 +1501,7 @@ export default function Editor({
|
|
|
1313
1501
|
const previewViewerContent = (
|
|
1314
1502
|
<Viewer
|
|
1315
1503
|
defaultRender={EDITOR_DEFAULT_RENDER}
|
|
1504
|
+
disablePostFx={disablePostFx}
|
|
1316
1505
|
hoverStyles={EDITOR_HOVER_STYLES}
|
|
1317
1506
|
renderContext="editor"
|
|
1318
1507
|
selectionManager="default"
|
|
@@ -1326,11 +1515,13 @@ export default function Editor({
|
|
|
1326
1515
|
<CustomCameraControls />
|
|
1327
1516
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
1328
1517
|
<InteractiveSystem />
|
|
1518
|
+
{presentationsReady ? <ViewerPresentations /> : null}
|
|
1329
1519
|
</Viewer>
|
|
1330
1520
|
)
|
|
1331
1521
|
|
|
1332
1522
|
const viewerCanvas = (
|
|
1333
1523
|
<ViewerCanvas
|
|
1524
|
+
disablePostFx={disablePostFx}
|
|
1334
1525
|
hasLoadedInitialScene={hasLoadedInitialScene}
|
|
1335
1526
|
isFirstPersonMode={isFirstPersonMode}
|
|
1336
1527
|
isLoading={isLoading}
|
|
@@ -1338,6 +1529,7 @@ export default function Editor({
|
|
|
1338
1529
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
1339
1530
|
onSceneReadyChange={handleSceneReadyChange}
|
|
1340
1531
|
onThumbnailCapture={onThumbnailCapture}
|
|
1532
|
+
presentationsReady={presentationsReady}
|
|
1341
1533
|
sceneReadyKey={sceneReadyKey}
|
|
1342
1534
|
showLoader={showLoader}
|
|
1343
1535
|
viewerSceneSlot={viewerSceneSlot}
|
|
@@ -1374,12 +1566,13 @@ export default function Editor({
|
|
|
1374
1566
|
}
|
|
1375
1567
|
|
|
1376
1568
|
const tabBarTabs = [
|
|
1377
|
-
...(sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon, icon }) => ({
|
|
1569
|
+
...(sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon, icon, noPanel }) => ({
|
|
1378
1570
|
id,
|
|
1379
1571
|
label,
|
|
1380
1572
|
mobileDefaultSnap,
|
|
1381
1573
|
mobileIcon,
|
|
1382
1574
|
icon,
|
|
1575
|
+
noPanel,
|
|
1383
1576
|
})) ?? []),
|
|
1384
1577
|
// Host panels appear after the explicit tabs in the rail. The icon
|
|
1385
1578
|
// doubles as the mobile icon; a half-height sheet is a sensible default.
|
|
@@ -1395,23 +1588,24 @@ export default function Editor({
|
|
|
1395
1588
|
return (
|
|
1396
1589
|
<>
|
|
1397
1590
|
<FloorplanModeCoordinator />
|
|
1398
|
-
{
|
|
1591
|
+
{visibleLoader && (
|
|
1399
1592
|
<div className="fixed inset-0 z-60">
|
|
1400
|
-
|
|
1593
|
+
{sceneLoadError ? (
|
|
1594
|
+
<SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
|
|
1595
|
+
) : (
|
|
1596
|
+
<SceneLoader className="bg-background" />
|
|
1597
|
+
)}
|
|
1401
1598
|
</div>
|
|
1402
1599
|
)}
|
|
1403
1600
|
|
|
1404
1601
|
{!isLoading && isPreviewMode ? (
|
|
1405
|
-
<
|
|
1406
|
-
{isFirstPersonMode
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
{previewViewerContent}
|
|
1413
|
-
</div>
|
|
1414
|
-
</div>
|
|
1602
|
+
<PreviewStage
|
|
1603
|
+
isFirstPersonMode={isFirstPersonMode}
|
|
1604
|
+
mode={previewStageMode}
|
|
1605
|
+
onModeChange={setPreviewStageMode}
|
|
1606
|
+
showLoader={visibleLoader}
|
|
1607
|
+
viewerContent={previewViewerContent}
|
|
1608
|
+
/>
|
|
1415
1609
|
) : (
|
|
1416
1610
|
<>
|
|
1417
1611
|
<EditorLayoutV2
|
|
@@ -1437,7 +1631,10 @@ export default function Editor({
|
|
|
1437
1631
|
<HelperManager />
|
|
1438
1632
|
</div>
|
|
1439
1633
|
)}
|
|
1440
|
-
{
|
|
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 && (
|
|
1441
1638
|
<FirstPersonOverlay
|
|
1442
1639
|
onExit={() => useEditor.getState().setFirstPersonMode(false)}
|
|
1443
1640
|
/>
|
|
@@ -1471,23 +1668,24 @@ export default function Editor({
|
|
|
1471
1668
|
return (
|
|
1472
1669
|
<div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
|
|
1473
1670
|
<FloorplanModeCoordinator />
|
|
1474
|
-
{
|
|
1671
|
+
{visibleLoader && (
|
|
1475
1672
|
<div className="fixed inset-0 z-60">
|
|
1476
|
-
|
|
1673
|
+
{sceneLoadError ? (
|
|
1674
|
+
<SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
|
|
1675
|
+
) : (
|
|
1676
|
+
<SceneLoader className="bg-background" />
|
|
1677
|
+
)}
|
|
1477
1678
|
</div>
|
|
1478
1679
|
)}
|
|
1479
1680
|
|
|
1480
1681
|
{!isLoading && isPreviewMode ? (
|
|
1481
|
-
|
|
1482
|
-
{isFirstPersonMode
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
{previewViewerContent}
|
|
1489
|
-
</div>
|
|
1490
|
-
</>
|
|
1682
|
+
<PreviewStage
|
|
1683
|
+
isFirstPersonMode={isFirstPersonMode}
|
|
1684
|
+
mode={previewStageMode}
|
|
1685
|
+
onModeChange={setPreviewStageMode}
|
|
1686
|
+
showLoader={visibleLoader}
|
|
1687
|
+
viewerContent={previewViewerContent}
|
|
1688
|
+
/>
|
|
1491
1689
|
) : (
|
|
1492
1690
|
<>
|
|
1493
1691
|
{/* Sidebar */}
|
|
@@ -1526,3 +1724,11 @@ export default function Editor({
|
|
|
1526
1724
|
</div>
|
|
1527
1725
|
)
|
|
1528
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,23 +1,17 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
4
|
import { type ForwardedRef, Fragment, forwardRef } from 'react'
|
|
5
|
+
import { formatLinearMeasurement, type MetricNotation } from '../../lib/measurements'
|
|
4
6
|
|
|
5
7
|
// Canonical in-world dimension formatter — metric metres or imperial
|
|
6
8
|
// feet/inches. Shared by every measurement readout so they read the same.
|
|
7
9
|
export function formatMeasurement(
|
|
8
10
|
value: number,
|
|
9
11
|
unit: 'metric' | 'imperial',
|
|
10
|
-
metricNotation:
|
|
12
|
+
metricNotation: MetricNotation = 'meters',
|
|
11
13
|
): string {
|
|
12
|
-
|
|
13
|
-
const feet = value * 3.280_84
|
|
14
|
-
const wholeFeet = Math.floor(feet)
|
|
15
|
-
const inches = Math.round((feet - wholeFeet) * 12)
|
|
16
|
-
if (inches === 12) return `${wholeFeet + 1}'0"`
|
|
17
|
-
return `${wholeFeet}'${inches}"`
|
|
18
|
-
}
|
|
19
|
-
if (metricNotation === 'millimeters') return `${Math.round(value * 1000)}mm`
|
|
20
|
-
return `${Number.parseFloat(value.toFixed(2))}m`
|
|
14
|
+
return formatLinearMeasurement(value, unit, metricNotation)
|
|
21
15
|
}
|
|
22
16
|
|
|
23
17
|
type MeasurePart = 'height' | 'length' | 'thickness'
|
|
@@ -55,12 +49,18 @@ export function DimensionPill({
|
|
|
55
49
|
primary?: string
|
|
56
50
|
primaryRef?: ForwardedRef<HTMLSpanElement>
|
|
57
51
|
}) {
|
|
52
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
53
|
+
|
|
58
54
|
return (
|
|
59
55
|
<div className="flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur">
|
|
60
56
|
{parts.map((part, index) => {
|
|
61
57
|
const text = part.signed
|
|
62
|
-
? `${part.value < 0 ? '-' : '+'}${formatMeasurement(
|
|
63
|
-
|
|
58
|
+
? `${part.value < 0 ? '-' : '+'}${formatMeasurement(
|
|
59
|
+
Math.abs(part.value),
|
|
60
|
+
unit,
|
|
61
|
+
metricNotation,
|
|
62
|
+
)}`
|
|
63
|
+
: formatMeasurement(part.value, unit, metricNotation)
|
|
64
64
|
return (
|
|
65
65
|
<Fragment key={part.key}>
|
|
66
66
|
{index > 0 ? (
|