@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
package/src/store/use-editor.tsx
CHANGED
|
@@ -6,35 +6,17 @@ import {
|
|
|
6
6
|
type AnyNodeId,
|
|
7
7
|
type BrushSettings,
|
|
8
8
|
type BuildingNode,
|
|
9
|
-
type CabinetModuleNode,
|
|
10
|
-
type CabinetNode,
|
|
11
|
-
type CeilingNode,
|
|
12
9
|
type ChimneyMaterialRole,
|
|
13
|
-
type ChimneyNode,
|
|
14
|
-
type ColumnNode,
|
|
15
10
|
DEFAULT_BRUSH_SETTINGS,
|
|
16
|
-
type DoorNode,
|
|
17
|
-
type DormerNode,
|
|
18
11
|
type DormerSurfaceMaterialRole,
|
|
19
|
-
type ElevatorNode,
|
|
20
|
-
type FenceNode,
|
|
21
|
-
type ItemNode,
|
|
22
12
|
type LevelNode,
|
|
23
13
|
nodeRegistry,
|
|
24
|
-
type RoofNode,
|
|
25
|
-
type RoofSegmentNode,
|
|
26
14
|
type RoofSurfaceMaterialRole,
|
|
27
|
-
type SlabNode,
|
|
28
15
|
type Space,
|
|
29
|
-
type SpawnNode,
|
|
30
|
-
type StairNode,
|
|
31
|
-
type StairSegmentNode,
|
|
32
16
|
type StairSurfaceMaterialRole,
|
|
33
17
|
type TerrainVerb,
|
|
34
18
|
useScene,
|
|
35
|
-
type WallNode,
|
|
36
19
|
type WallSurfaceSide,
|
|
37
|
-
type WindowNode,
|
|
38
20
|
} from '@pascal-app/core'
|
|
39
21
|
import { useViewer } from '@pascal-app/viewer'
|
|
40
22
|
import { create } from 'zustand'
|
|
@@ -58,6 +40,7 @@ import {
|
|
|
58
40
|
DEFAULT_CREATABLE_MEASUREMENT_KIND,
|
|
59
41
|
normalizeCreatableMeasurementKind,
|
|
60
42
|
} from '../lib/measurement-kind'
|
|
43
|
+
import type { ModelExport } from '../lib/model-export'
|
|
61
44
|
import {
|
|
62
45
|
cyclePaintScope as cyclePaintScopeValue,
|
|
63
46
|
type PaintHoverInfo,
|
|
@@ -80,31 +63,6 @@ const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
|
|
|
80
63
|
const MIN_FLOORPLAN_PANE_RATIO = 0.15
|
|
81
64
|
const MAX_FLOORPLAN_PANE_RATIO = 0.85
|
|
82
65
|
|
|
83
|
-
function resolveMovingNodeTarget(
|
|
84
|
-
node:
|
|
85
|
-
| ItemNode
|
|
86
|
-
| WindowNode
|
|
87
|
-
| DoorNode
|
|
88
|
-
| ElevatorNode
|
|
89
|
-
| CeilingNode
|
|
90
|
-
| ChimneyNode
|
|
91
|
-
| ColumnNode
|
|
92
|
-
| DormerNode
|
|
93
|
-
| SlabNode
|
|
94
|
-
| WallNode
|
|
95
|
-
| FenceNode
|
|
96
|
-
| RoofNode
|
|
97
|
-
| RoofSegmentNode
|
|
98
|
-
| SpawnNode
|
|
99
|
-
| StairNode
|
|
100
|
-
| StairSegmentNode
|
|
101
|
-
| BuildingNode
|
|
102
|
-
| CabinetNode
|
|
103
|
-
| CabinetModuleNode,
|
|
104
|
-
) {
|
|
105
|
-
return node
|
|
106
|
-
}
|
|
107
|
-
|
|
108
66
|
export type ViewMode = '3d' | '2d' | 'split'
|
|
109
67
|
export type SplitOrientation = 'horizontal' | 'vertical'
|
|
110
68
|
export type WorkspaceMode = 'edit' | 'studio'
|
|
@@ -127,7 +85,14 @@ export type SnapshotStandardAspect = '16:9' | '9:16' | '4:3' | '3:4' | '1:1'
|
|
|
127
85
|
|
|
128
86
|
export type CaptureMode =
|
|
129
87
|
| { mode: 'idle' }
|
|
130
|
-
| {
|
|
88
|
+
| {
|
|
89
|
+
mode: 'standard'
|
|
90
|
+
crop?: SnapshotCropMode
|
|
91
|
+
standardAspect?: SnapshotStandardAspect
|
|
92
|
+
/** The host needs this exact output shape (e.g. the publish cover) —
|
|
93
|
+
* hide the crop/aspect switcher instead of merely preselecting it. */
|
|
94
|
+
lockCrop?: boolean
|
|
95
|
+
}
|
|
131
96
|
| {
|
|
132
97
|
mode: 'preset'
|
|
133
98
|
isolated: AnyNodeId[]
|
|
@@ -139,6 +104,22 @@ export type CaptureMode =
|
|
|
139
104
|
}
|
|
140
105
|
}
|
|
141
106
|
|
|
107
|
+
/**
|
|
108
|
+
* How the first-person camera moves. `walk` is the grounded street-view
|
|
109
|
+
* controller (gravity, collision, door interaction); `drone` is a free camera
|
|
110
|
+
* with no gravity or collision, offered by the snapshot capture overlay so a
|
|
111
|
+
* shot can be framed from anywhere in the scene.
|
|
112
|
+
*/
|
|
113
|
+
export type FirstPersonMovementMode = 'walk' | 'drone'
|
|
114
|
+
|
|
115
|
+
/** Degrees. Range of the capture-mode field-of-view control. */
|
|
116
|
+
export const CAPTURE_FOV_MIN = 15
|
|
117
|
+
export const CAPTURE_FOV_MAX = 110
|
|
118
|
+
|
|
119
|
+
function clampCaptureFov(fov: number): number {
|
|
120
|
+
return Math.min(Math.max(Math.round(fov), CAPTURE_FOV_MIN), CAPTURE_FOV_MAX)
|
|
121
|
+
}
|
|
122
|
+
|
|
142
123
|
export type Phase = 'site' | 'structure' | 'furnish'
|
|
143
124
|
|
|
144
125
|
/**
|
|
@@ -242,6 +223,14 @@ export type NavigationSyncPoseInput = Omit<NavigationSyncPose, 'revision'>
|
|
|
242
223
|
export type KnownTool = SiteTool | StructureTool | FurnishTool
|
|
243
224
|
export type Tool = KnownTool | (string & {})
|
|
244
225
|
|
|
226
|
+
export type ToolMode =
|
|
227
|
+
| { mode: 'select' }
|
|
228
|
+
| { mode: 'edit' }
|
|
229
|
+
| { mode: 'delete' }
|
|
230
|
+
| { mode: 'build'; tool: StructureTool }
|
|
231
|
+
| { mode: 'material-paint' }
|
|
232
|
+
| { mode: 'terrain-sculpt' }
|
|
233
|
+
|
|
245
234
|
/**
|
|
246
235
|
* Starting parameters seeded into a draw tool before it mints a node.
|
|
247
236
|
* A loose param bag — the tool's create path validates it through the
|
|
@@ -280,6 +269,9 @@ export type GuideUiState = {
|
|
|
280
269
|
type EditorState = {
|
|
281
270
|
phase: Phase
|
|
282
271
|
setPhase: (phase: Phase) => void
|
|
272
|
+
toolMode: ToolMode
|
|
273
|
+
armToolMode: (next: ToolMode) => void
|
|
274
|
+
armMaterialPaint: (material?: ActivePaintMaterial) => void
|
|
283
275
|
mode: Mode
|
|
284
276
|
setMode: (mode: Mode) => void
|
|
285
277
|
tool: Tool | null
|
|
@@ -310,29 +302,7 @@ type EditorState = {
|
|
|
310
302
|
setPlacementDragMode: (dragMode: boolean) => void
|
|
311
303
|
roofHostDragArmedId: AnyNodeId | null
|
|
312
304
|
setRoofHostDragArmedId: (nodeId: AnyNodeId | null) => void
|
|
313
|
-
setMovingNode: (
|
|
314
|
-
node:
|
|
315
|
-
| ItemNode
|
|
316
|
-
| WindowNode
|
|
317
|
-
| DoorNode
|
|
318
|
-
| ElevatorNode
|
|
319
|
-
| CeilingNode
|
|
320
|
-
| ChimneyNode
|
|
321
|
-
| ColumnNode
|
|
322
|
-
| DormerNode
|
|
323
|
-
| SlabNode
|
|
324
|
-
| WallNode
|
|
325
|
-
| FenceNode
|
|
326
|
-
| RoofNode
|
|
327
|
-
| RoofSegmentNode
|
|
328
|
-
| SpawnNode
|
|
329
|
-
| StairNode
|
|
330
|
-
| StairSegmentNode
|
|
331
|
-
| BuildingNode
|
|
332
|
-
| CabinetNode
|
|
333
|
-
| CabinetModuleNode
|
|
334
|
-
| null,
|
|
335
|
-
) => void
|
|
305
|
+
setMovingNode: (node: AnyNode | null) => void
|
|
336
306
|
/**
|
|
337
307
|
* Which view (2D floor plan or 3D viewer) most recently completed
|
|
338
308
|
* the active move — set by the committing or cancelling side just
|
|
@@ -510,6 +480,26 @@ type EditorState = {
|
|
|
510
480
|
isFirstPersonMode: boolean
|
|
511
481
|
_viewModeBeforeFirstPerson: ViewMode | null
|
|
512
482
|
setFirstPersonMode: (enabled: boolean) => void
|
|
483
|
+
// Which first-person controller runs while `isFirstPersonMode` is on. Reset to
|
|
484
|
+
// `walk` whenever first person is left, so the grounded controller stays the
|
|
485
|
+
// default entry point; only the capture overlay arms `drone`.
|
|
486
|
+
firstPersonMovementMode: FirstPersonMovementMode
|
|
487
|
+
setFirstPersonMovementMode: (mode: FirstPersonMovementMode) => void
|
|
488
|
+
// Perspective field of view (degrees) the snapshot capture overlay is driving,
|
|
489
|
+
// and the value its reset affordance returns to. Both are `null` unless the
|
|
490
|
+
// capture camera rig has armed them — i.e. unless capture mode is open on a
|
|
491
|
+
// perspective camera. The rig owns the lifecycle; the overlay only writes
|
|
492
|
+
// `captureFov` through `setCaptureFov`.
|
|
493
|
+
captureFov: number | null
|
|
494
|
+
captureFovBaseline: number | null
|
|
495
|
+
setCaptureFov: (fov: number) => void
|
|
496
|
+
armCaptureFov: (fov: number | null) => void
|
|
497
|
+
// The shutter has fired and the snapshot is being rendered/saved: walk /
|
|
498
|
+
// drone freeze look + movement so a late WASD tap or mouse twitch can't
|
|
499
|
+
// shift the frame out from under the shot. Set by the capture overlay for
|
|
500
|
+
// the whole capturing→saved window.
|
|
501
|
+
captureShutterHold: boolean
|
|
502
|
+
setCaptureShutterHold: (hold: boolean) => void
|
|
513
503
|
// Workspace mode: 'edit' is the full editing surface; 'studio' is the
|
|
514
504
|
// render/snapshot surface (clean canvas, no editing chrome or selection).
|
|
515
505
|
// Entering studio forces a 3D-only view and restores the prior view on exit.
|
|
@@ -524,11 +514,20 @@ type EditorState = {
|
|
|
524
514
|
// Read by the mobile layout so the viewer container can shrink to preview edits.
|
|
525
515
|
mobilePanelSheetHeight: number
|
|
526
516
|
setMobilePanelSheetHeight: (px: number) => void
|
|
517
|
+
modelExport: ModelExport | null
|
|
518
|
+
setModelExport: (modelExport: ModelExport | null) => void
|
|
527
519
|
}
|
|
528
520
|
|
|
529
521
|
export type PersistedEditorUiState = Pick<
|
|
530
522
|
EditorState,
|
|
531
|
-
|
|
523
|
+
| 'phase'
|
|
524
|
+
| 'toolMode'
|
|
525
|
+
| 'mode'
|
|
526
|
+
| 'tool'
|
|
527
|
+
| 'structureLayer'
|
|
528
|
+
| 'catalogCategory'
|
|
529
|
+
| 'isFloorplanOpen'
|
|
530
|
+
| 'viewMode'
|
|
532
531
|
>
|
|
533
532
|
|
|
534
533
|
type PersistedEditorLayoutState = Pick<
|
|
@@ -550,6 +549,7 @@ type PersistedEditorState = PersistedEditorUiState & PersistedEditorLayoutState
|
|
|
550
549
|
|
|
551
550
|
export const DEFAULT_PERSISTED_EDITOR_UI_STATE: PersistedEditorUiState = {
|
|
552
551
|
phase: 'site',
|
|
552
|
+
toolMode: { mode: 'select' },
|
|
553
553
|
mode: 'select',
|
|
554
554
|
tool: null,
|
|
555
555
|
structureLayer: 'elements',
|
|
@@ -576,6 +576,7 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
576
576
|
fence: CONTINUATION_PROFILES.fence.default,
|
|
577
577
|
point: CONTINUATION_PROFILES.point.default,
|
|
578
578
|
cabinet: CONTINUATION_PROFILES.cabinet.default,
|
|
579
|
+
canopy: CONTINUATION_PROFILES.canopy.default,
|
|
579
580
|
},
|
|
580
581
|
showReferenceFloor: false,
|
|
581
582
|
referenceFloorOffset: 1,
|
|
@@ -588,13 +589,67 @@ type SelectDefaultBuildingAndLevelOptions = {
|
|
|
588
589
|
forceGroundLevel?: boolean
|
|
589
590
|
}
|
|
590
591
|
|
|
592
|
+
function defaultBuildTool(phase: Phase, structureLayer: StructureLayer): StructureTool {
|
|
593
|
+
if (phase === 'site') return 'property-line'
|
|
594
|
+
if (phase === 'furnish') return 'item'
|
|
595
|
+
return structureLayer === 'zones' ? 'zone' : 'wall'
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function materializeToolMode(
|
|
599
|
+
mode: Mode,
|
|
600
|
+
tool: unknown,
|
|
601
|
+
phase: Phase,
|
|
602
|
+
structureLayer: StructureLayer,
|
|
603
|
+
): ToolMode {
|
|
604
|
+
if (mode === 'build') {
|
|
605
|
+
return {
|
|
606
|
+
mode,
|
|
607
|
+
tool:
|
|
608
|
+
typeof tool === 'string' && tool.length > 0
|
|
609
|
+
? (tool as StructureTool)
|
|
610
|
+
: defaultBuildTool(phase, structureLayer),
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return { mode } as ToolMode
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function readPersistedToolMode(state: Partial<PersistedEditorUiState> | null | undefined): {
|
|
618
|
+
mode: Mode | undefined
|
|
619
|
+
tool: unknown
|
|
620
|
+
} {
|
|
621
|
+
const candidate = state?.toolMode as Partial<ToolMode> | undefined
|
|
622
|
+
if (
|
|
623
|
+
candidate?.mode === 'select' ||
|
|
624
|
+
candidate?.mode === 'edit' ||
|
|
625
|
+
candidate?.mode === 'delete' ||
|
|
626
|
+
candidate?.mode === 'build' ||
|
|
627
|
+
candidate?.mode === 'material-paint' ||
|
|
628
|
+
candidate?.mode === 'terrain-sculpt'
|
|
629
|
+
) {
|
|
630
|
+
return {
|
|
631
|
+
mode: candidate.mode,
|
|
632
|
+
tool: candidate.mode === 'build' ? (candidate as { tool?: unknown }).tool : null,
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return { mode: state?.mode, tool: state?.tool }
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function withMaterializedToolMode(
|
|
640
|
+
state: Omit<PersistedEditorUiState, 'toolMode'>,
|
|
641
|
+
): PersistedEditorUiState {
|
|
642
|
+
return {
|
|
643
|
+
...state,
|
|
644
|
+
toolMode: materializeToolMode(state.mode, state.tool, state.phase, state.structureLayer),
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
591
648
|
function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
// `delete` and `material-paint` still have nothing to act on at site scope, so
|
|
595
|
-
// letting them survive here would restore a mode with no targets.
|
|
649
|
+
// Site has its own property-line build tool and terrain brush. The remaining
|
|
650
|
+
// modes have nothing to act on at site scope, so they restore as select.
|
|
596
651
|
if (phase === 'site') {
|
|
597
|
-
return mode === 'terrain-sculpt' ? mode : 'select'
|
|
652
|
+
return mode === 'build' || mode === 'terrain-sculpt' ? mode : 'select'
|
|
598
653
|
}
|
|
599
654
|
|
|
600
655
|
return mode === 'build' || mode === 'delete' || mode === 'material-paint' ? mode : 'select'
|
|
@@ -612,7 +667,8 @@ export function normalizePersistedEditorUiState(
|
|
|
612
667
|
state: Partial<PersistedEditorUiState> | null | undefined,
|
|
613
668
|
): PersistedEditorUiState {
|
|
614
669
|
const phase = state?.phase === 'structure' || state?.phase === 'furnish' ? state.phase : 'site'
|
|
615
|
-
|
|
670
|
+
const persistedToolMode = readPersistedToolMode(state)
|
|
671
|
+
let mode = normalizeModeForPhase(phase, persistedToolMode.mode)
|
|
616
672
|
|
|
617
673
|
// Migrate old isFloorplanOpen to viewMode
|
|
618
674
|
let viewMode: ViewMode = '3d'
|
|
@@ -631,17 +687,19 @@ export function normalizePersistedEditorUiState(
|
|
|
631
687
|
if (mode === 'terrain-sculpt' && viewMode === '2d') mode = 'select'
|
|
632
688
|
|
|
633
689
|
if (phase === 'site') {
|
|
634
|
-
return {
|
|
635
|
-
...DEFAULT_PERSISTED_EDITOR_UI_STATE,
|
|
690
|
+
return withMaterializedToolMode({
|
|
636
691
|
phase,
|
|
637
692
|
mode,
|
|
693
|
+
tool: mode === 'build' ? 'property-line' : null,
|
|
694
|
+
structureLayer: 'elements',
|
|
695
|
+
catalogCategory: null,
|
|
638
696
|
viewMode,
|
|
639
697
|
isFloorplanOpen,
|
|
640
|
-
}
|
|
698
|
+
})
|
|
641
699
|
}
|
|
642
700
|
|
|
643
701
|
if (phase === 'furnish') {
|
|
644
|
-
return {
|
|
702
|
+
return withMaterializedToolMode({
|
|
645
703
|
phase,
|
|
646
704
|
mode,
|
|
647
705
|
tool: mode === 'build' ? 'item' : null,
|
|
@@ -649,13 +707,13 @@ export function normalizePersistedEditorUiState(
|
|
|
649
707
|
catalogCategory: mode === 'build' ? (state?.catalogCategory ?? 'furniture') : null,
|
|
650
708
|
viewMode,
|
|
651
709
|
isFloorplanOpen,
|
|
652
|
-
}
|
|
710
|
+
})
|
|
653
711
|
}
|
|
654
712
|
|
|
655
713
|
const structureLayer = state?.structureLayer === 'zones' ? 'zones' : 'elements'
|
|
656
714
|
|
|
657
715
|
if (mode !== 'build') {
|
|
658
|
-
return {
|
|
716
|
+
return withMaterializedToolMode({
|
|
659
717
|
phase,
|
|
660
718
|
mode,
|
|
661
719
|
tool: null,
|
|
@@ -663,11 +721,11 @@ export function normalizePersistedEditorUiState(
|
|
|
663
721
|
catalogCategory: null,
|
|
664
722
|
viewMode,
|
|
665
723
|
isFloorplanOpen,
|
|
666
|
-
}
|
|
724
|
+
})
|
|
667
725
|
}
|
|
668
726
|
|
|
669
727
|
if (structureLayer === 'zones') {
|
|
670
|
-
return {
|
|
728
|
+
return withMaterializedToolMode({
|
|
671
729
|
phase,
|
|
672
730
|
mode,
|
|
673
731
|
tool: 'zone',
|
|
@@ -675,19 +733,25 @@ export function normalizePersistedEditorUiState(
|
|
|
675
733
|
catalogCategory: null,
|
|
676
734
|
viewMode,
|
|
677
735
|
isFloorplanOpen,
|
|
678
|
-
}
|
|
736
|
+
})
|
|
679
737
|
}
|
|
680
738
|
|
|
681
|
-
|
|
739
|
+
const tool =
|
|
740
|
+
persistedToolMode.tool &&
|
|
741
|
+
persistedToolMode.tool !== 'property-line' &&
|
|
742
|
+
persistedToolMode.tool !== 'zone'
|
|
743
|
+
? (persistedToolMode.tool as Tool)
|
|
744
|
+
: 'wall'
|
|
745
|
+
|
|
746
|
+
return withMaterializedToolMode({
|
|
682
747
|
phase,
|
|
683
748
|
mode,
|
|
684
|
-
tool
|
|
685
|
-
state?.tool && state.tool !== 'property-line' && state.tool !== 'zone' ? state.tool : 'wall',
|
|
749
|
+
tool,
|
|
686
750
|
structureLayer,
|
|
687
|
-
catalogCategory:
|
|
751
|
+
catalogCategory: tool === 'item' ? (state?.catalogCategory ?? null) : null,
|
|
688
752
|
viewMode,
|
|
689
753
|
isFloorplanOpen,
|
|
690
|
-
}
|
|
754
|
+
})
|
|
691
755
|
}
|
|
692
756
|
|
|
693
757
|
// Validate a persisted per-context mode against that context's allowed set
|
|
@@ -730,6 +794,9 @@ function normalizeContinuationByContext(
|
|
|
730
794
|
cabinet:
|
|
731
795
|
migrateContinuationMode(state?.continuationByContext?.cabinet, 'cabinet') ??
|
|
732
796
|
CONTINUATION_PROFILES.cabinet.default,
|
|
797
|
+
canopy:
|
|
798
|
+
migrateContinuationMode(state?.continuationByContext?.canopy, 'canopy') ??
|
|
799
|
+
CONTINUATION_PROFILES.canopy.default,
|
|
733
800
|
}
|
|
734
801
|
}
|
|
735
802
|
|
|
@@ -885,21 +952,16 @@ let viewModeBeforeCapture: ViewMode | null = null
|
|
|
885
952
|
*
|
|
886
953
|
* Paint and sculpt are the two modes whose scope lifetime is the *mode*, not a
|
|
887
954
|
* pointer gesture. Both must be released whenever the mode changes for any
|
|
888
|
-
* reason
|
|
889
|
-
*
|
|
890
|
-
* whole editor, so this is one function called from every mode transition rather
|
|
891
|
-
* than a rule each transition remembers.
|
|
955
|
+
* reason. A stuck `sculpting` scope would leave selection disabled across the
|
|
956
|
+
* whole editor, so the ToolMode transition owns this side effect.
|
|
892
957
|
*
|
|
893
958
|
* The eyedropper arm rides along for the same reason: it is one-shot state whose
|
|
894
959
|
* UI is unmounted the moment sculpt mode ends, so it has to be cleared on every
|
|
895
960
|
* exit path and not just the one through `setMode`.
|
|
896
961
|
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
* the dangerous class: they unmount `ToolManager` (via `noEditing`), so the
|
|
901
|
-
* sculpt tool that would otherwise release the scope on unmount is gone, and a
|
|
902
|
-
* scope leaked there is unrecoverable without a reload.
|
|
962
|
+
* View-swapping actions are the dangerous class: they unmount `ToolManager`
|
|
963
|
+
* (via `noEditing`), so the sculpt tool that would otherwise release the scope
|
|
964
|
+
* on unmount is gone, and a leaked scope is unrecoverable without a reload.
|
|
903
965
|
*/
|
|
904
966
|
function syncBrushModeScope(mode: Mode): void {
|
|
905
967
|
const scope = useInteractionScope.getState()
|
|
@@ -935,31 +997,18 @@ const useEditor = create<EditorState>()(
|
|
|
935
997
|
setPhase: (phase) => {
|
|
936
998
|
const currentPhase = get().phase
|
|
937
999
|
if (currentPhase === phase) return
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
set({ tool: 'wall', catalogCategory: null })
|
|
951
|
-
} else if (phase === 'furnish') {
|
|
952
|
-
set({ tool: 'item', catalogCategory: 'furniture' })
|
|
953
|
-
}
|
|
954
|
-
} else {
|
|
955
|
-
// Reset to select mode and clear tool/catalog when switching phases
|
|
956
|
-
set({ mode: 'select', tool: null, catalogCategory: null })
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
// Leaving the site phase must drop a held sculpt scope: this branch
|
|
960
|
-
// rewrites `mode` without going through `setMode`, so without this a
|
|
961
|
-
// stuck `sculpting` scope would disable selection in structure phase.
|
|
962
|
-
syncBrushModeScope(get().mode)
|
|
1000
|
+
const wasBuilding = get().toolMode.mode === 'build'
|
|
1001
|
+
const structureLayer = phase === 'furnish' ? 'elements' : get().structureLayer
|
|
1002
|
+
set({
|
|
1003
|
+
phase,
|
|
1004
|
+
structureLayer,
|
|
1005
|
+
catalogCategory: wasBuilding && phase === 'furnish' ? 'furniture' : null,
|
|
1006
|
+
})
|
|
1007
|
+
get().armToolMode(
|
|
1008
|
+
wasBuilding
|
|
1009
|
+
? { mode: 'build', tool: defaultBuildTool(phase, structureLayer) }
|
|
1010
|
+
: { mode: 'select' },
|
|
1011
|
+
)
|
|
963
1012
|
|
|
964
1013
|
switch (phase) {
|
|
965
1014
|
case 'site':
|
|
@@ -972,64 +1021,99 @@ const useEditor = create<EditorState>()(
|
|
|
972
1021
|
|
|
973
1022
|
case 'furnish':
|
|
974
1023
|
selectDefaultBuildingAndLevel()
|
|
975
|
-
// Furnish mode only supports elements layer, not zones
|
|
976
|
-
set({ structureLayer: 'elements' })
|
|
977
1024
|
break
|
|
978
1025
|
}
|
|
979
1026
|
},
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1027
|
+
toolMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.toolMode,
|
|
1028
|
+
armToolMode: (requested) => {
|
|
1029
|
+
const current = get()
|
|
1030
|
+
let phase = current.phase
|
|
1031
|
+
let structureLayer = current.structureLayer
|
|
1032
|
+
let viewMode = current.viewMode
|
|
1033
|
+
let isFloorplanOpen = current.isFloorplanOpen
|
|
1034
|
+
const next = materializeToolMode(
|
|
1035
|
+
requested.mode,
|
|
1036
|
+
requested.mode === 'build' ? requested.tool : null,
|
|
1037
|
+
phase,
|
|
1038
|
+
structureLayer,
|
|
1039
|
+
)
|
|
989
1040
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1041
|
+
if (next.mode === 'terrain-sculpt') {
|
|
1042
|
+
phase = 'site'
|
|
1043
|
+
structureLayer = 'elements'
|
|
1044
|
+
if (viewMode === '2d') {
|
|
1045
|
+
viewMode = 'split'
|
|
1046
|
+
isFloorplanOpen = true
|
|
1047
|
+
}
|
|
1048
|
+
} else if (next.mode === 'build' && next.tool === 'property-line') {
|
|
1049
|
+
phase = 'site'
|
|
1050
|
+
structureLayer = 'elements'
|
|
1051
|
+
} else if (next.mode === 'build' && next.tool === 'zone') {
|
|
1052
|
+
phase = 'structure'
|
|
1053
|
+
structureLayer = 'zones'
|
|
1054
|
+
} else if (next.mode === 'build' && phase === 'site') {
|
|
1055
|
+
phase = 'structure'
|
|
1056
|
+
structureLayer = 'elements'
|
|
1057
|
+
} else if (next.mode === 'material-paint' && phase === 'site') {
|
|
1058
|
+
phase = 'structure'
|
|
1059
|
+
structureLayer = 'elements'
|
|
1060
|
+
} else if (phase !== 'structure') {
|
|
1061
|
+
structureLayer = 'elements'
|
|
997
1062
|
}
|
|
998
1063
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1064
|
+
const phaseChanged = phase !== current.phase
|
|
1065
|
+
set({
|
|
1066
|
+
toolMode: next,
|
|
1067
|
+
mode: next.mode,
|
|
1068
|
+
tool: next.mode === 'build' ? next.tool : null,
|
|
1069
|
+
...(phaseChanged ? { phase } : {}),
|
|
1070
|
+
...(structureLayer !== current.structureLayer ? { structureLayer } : {}),
|
|
1071
|
+
...(viewMode !== current.viewMode ? { viewMode } : {}),
|
|
1072
|
+
...(isFloorplanOpen !== current.isFloorplanOpen ? { isFloorplanOpen } : {}),
|
|
1073
|
+
...(next.mode === 'build' && phase === 'furnish' && !current.catalogCategory
|
|
1074
|
+
? { catalogCategory: 'furniture' }
|
|
1075
|
+
: {}),
|
|
1076
|
+
})
|
|
1002
1077
|
|
|
1003
|
-
if (
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (phase === 'structure' && structureLayer === 'zones') {
|
|
1007
|
-
set({ tool: 'zone' })
|
|
1008
|
-
} else if (phase === 'structure' && structureLayer === 'elements') {
|
|
1009
|
-
set({ tool: 'wall' })
|
|
1010
|
-
} else if (phase === 'furnish') {
|
|
1011
|
-
set({ tool: 'item', catalogCategory: 'furniture' })
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
} else if (mode === 'material-paint') {
|
|
1015
|
-
get().primeMaterialPaintFromSelection()
|
|
1016
|
-
}
|
|
1017
|
-
// When leaving build mode, clear tool
|
|
1018
|
-
else if (tool) {
|
|
1019
|
-
set({ tool: null })
|
|
1078
|
+
if (phaseChanged) {
|
|
1079
|
+
if (phase === 'site') selectSiteFloorplanContext()
|
|
1080
|
+
else selectDefaultBuildingAndLevel()
|
|
1020
1081
|
}
|
|
1021
|
-
|
|
1022
|
-
if (mode === 'terrain-sculpt') {
|
|
1023
|
-
// Sculpting acts on the ground, never on a node. Clearing the
|
|
1024
|
-
// selection on entry is what stops the selected wall's gizmo from
|
|
1025
|
-
// sitting under the brush, competing for the same clicks.
|
|
1082
|
+
if (next.mode === 'material-paint') get().primeMaterialPaintFromSelection()
|
|
1083
|
+
if (next.mode === 'terrain-sculpt') {
|
|
1026
1084
|
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
1027
1085
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1086
|
+
syncBrushModeScope(next.mode)
|
|
1087
|
+
},
|
|
1088
|
+
armMaterialPaint: (material) => {
|
|
1089
|
+
get().armToolMode({ mode: 'material-paint' })
|
|
1090
|
+
if (material) get().setActivePaintMaterial(material)
|
|
1091
|
+
},
|
|
1092
|
+
mode: DEFAULT_PERSISTED_EDITOR_UI_STATE.mode,
|
|
1093
|
+
setMode: (mode) => {
|
|
1094
|
+
if (mode === 'build') {
|
|
1095
|
+
const { phase, structureLayer, toolMode } = get()
|
|
1096
|
+
get().armToolMode({
|
|
1097
|
+
mode,
|
|
1098
|
+
tool:
|
|
1099
|
+
toolMode.mode === 'build' ? toolMode.tool : defaultBuildTool(phase, structureLayer),
|
|
1100
|
+
})
|
|
1101
|
+
return
|
|
1102
|
+
}
|
|
1103
|
+
get().armToolMode({ mode } as ToolMode)
|
|
1030
1104
|
},
|
|
1031
1105
|
tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
|
|
1032
|
-
setTool: (tool) =>
|
|
1106
|
+
setTool: (tool) => {
|
|
1107
|
+
if (tool) {
|
|
1108
|
+
get().armToolMode({ mode: 'build', tool })
|
|
1109
|
+
return
|
|
1110
|
+
}
|
|
1111
|
+
if (get().toolMode.mode === 'build' || get().mode === 'build') {
|
|
1112
|
+
get().armToolMode({ mode: 'select' })
|
|
1113
|
+
return
|
|
1114
|
+
}
|
|
1115
|
+
get().armToolMode(materializeToolMode(get().mode, null, get().phase, get().structureLayer))
|
|
1116
|
+
},
|
|
1033
1117
|
toolDefaults: {},
|
|
1034
1118
|
setToolDefaults: (tool, defaults) =>
|
|
1035
1119
|
set((state) => {
|
|
@@ -1045,15 +1129,13 @@ const useEditor = create<EditorState>()(
|
|
|
1045
1129
|
setLastMeasurementKind: (kind) => set({ lastMeasurementKind: kind }),
|
|
1046
1130
|
structureLayer: DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer,
|
|
1047
1131
|
setStructureLayer: (layer) => {
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
syncBrushModeScope('select')
|
|
1056
|
-
}
|
|
1132
|
+
const wasBuilding = get().toolMode.mode === 'build'
|
|
1133
|
+
set({ structureLayer: layer })
|
|
1134
|
+
get().armToolMode(
|
|
1135
|
+
wasBuilding
|
|
1136
|
+
? { mode: 'build', tool: layer === 'zones' ? 'zone' : 'wall' }
|
|
1137
|
+
: { mode: 'select' },
|
|
1138
|
+
)
|
|
1057
1139
|
|
|
1058
1140
|
const viewer = useViewer.getState()
|
|
1059
1141
|
viewer.setSelection({
|
|
@@ -1085,7 +1167,7 @@ const useEditor = create<EditorState>()(
|
|
|
1085
1167
|
set({ placementDragMode: false })
|
|
1086
1168
|
return
|
|
1087
1169
|
}
|
|
1088
|
-
const targetNode =
|
|
1170
|
+
const targetNode = node
|
|
1089
1171
|
const isNew = Boolean((targetNode as { metadata?: { isNew?: boolean } }).metadata?.isNew)
|
|
1090
1172
|
if (isNew) {
|
|
1091
1173
|
scope.begin({
|
|
@@ -1095,6 +1177,7 @@ const useEditor = create<EditorState>()(
|
|
|
1095
1177
|
nodeType: targetNode.type,
|
|
1096
1178
|
view: '3d',
|
|
1097
1179
|
pressDrag: get().placementDragMode,
|
|
1180
|
+
driver: 'move-tool',
|
|
1098
1181
|
})
|
|
1099
1182
|
} else {
|
|
1100
1183
|
scope.begin({
|
|
@@ -1236,8 +1319,8 @@ const useEditor = create<EditorState>()(
|
|
|
1236
1319
|
isPreviewMode: false,
|
|
1237
1320
|
setPreviewMode: (preview) => {
|
|
1238
1321
|
if (preview) {
|
|
1239
|
-
set({ isPreviewMode: true,
|
|
1240
|
-
|
|
1322
|
+
set({ isPreviewMode: true, catalogCategory: null })
|
|
1323
|
+
get().armToolMode({ mode: 'select' })
|
|
1241
1324
|
// Clear zone/item selection for clean viewer drill-down hierarchy
|
|
1242
1325
|
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
1243
1326
|
} else {
|
|
@@ -1250,6 +1333,12 @@ const useEditor = create<EditorState>()(
|
|
|
1250
1333
|
const resolved: CaptureMode =
|
|
1251
1334
|
typeof next === 'boolean' ? { mode: next ? 'standard' : 'idle' } : next
|
|
1252
1335
|
const entering = resolved.mode !== 'idle'
|
|
1336
|
+
// Walk / drone framing is a capture-only camera, so leaving capture always
|
|
1337
|
+
// lands back on orbit. Run it first: it restores its own view mode, and
|
|
1338
|
+
// the capture restore below has the final say.
|
|
1339
|
+
if (!entering && get().isFirstPersonMode) {
|
|
1340
|
+
get().setFirstPersonMode(false)
|
|
1341
|
+
}
|
|
1253
1342
|
set((state) => {
|
|
1254
1343
|
if (entering) {
|
|
1255
1344
|
// Force 3D for the shot. Remember the prior mode only on the first
|
|
@@ -1381,20 +1470,33 @@ const useEditor = create<EditorState>()(
|
|
|
1381
1470
|
_viewModeBeforeFirstPerson: currentViewMode,
|
|
1382
1471
|
viewMode: '3d',
|
|
1383
1472
|
isFloorplanOpen: false,
|
|
1384
|
-
mode: 'select',
|
|
1385
|
-
tool: null,
|
|
1386
1473
|
catalogCategory: null,
|
|
1387
1474
|
})
|
|
1388
|
-
|
|
1475
|
+
get().armToolMode({ mode: 'select' })
|
|
1389
1476
|
} else {
|
|
1390
1477
|
const prevMode = get()._viewModeBeforeFirstPerson
|
|
1391
1478
|
set({
|
|
1392
1479
|
isFirstPersonMode: false,
|
|
1480
|
+
firstPersonMovementMode: 'walk',
|
|
1393
1481
|
_viewModeBeforeFirstPerson: null,
|
|
1394
1482
|
...(prevMode ? { viewMode: prevMode, isFloorplanOpen: prevMode !== '3d' } : {}),
|
|
1395
1483
|
})
|
|
1396
1484
|
}
|
|
1397
1485
|
},
|
|
1486
|
+
firstPersonMovementMode: 'walk' as FirstPersonMovementMode,
|
|
1487
|
+
setFirstPersonMovementMode: (mode) => set({ firstPersonMovementMode: mode }),
|
|
1488
|
+
captureFov: null,
|
|
1489
|
+
captureFovBaseline: null,
|
|
1490
|
+
setCaptureFov: (fov) =>
|
|
1491
|
+
set({
|
|
1492
|
+
captureFov: clampCaptureFov(fov),
|
|
1493
|
+
}),
|
|
1494
|
+
armCaptureFov: (fov) => {
|
|
1495
|
+
const captureFov = fov === null ? null : clampCaptureFov(fov)
|
|
1496
|
+
set({ captureFov, captureFovBaseline: captureFov })
|
|
1497
|
+
},
|
|
1498
|
+
captureShutterHold: false,
|
|
1499
|
+
setCaptureShutterHold: (hold) => set({ captureShutterHold: hold }),
|
|
1398
1500
|
workspaceMode: 'edit' as WorkspaceMode,
|
|
1399
1501
|
_viewModeBeforeStudio: null as ViewMode | null,
|
|
1400
1502
|
setWorkspaceMode: (mode) => {
|
|
@@ -1406,11 +1508,9 @@ const useEditor = create<EditorState>()(
|
|
|
1406
1508
|
_viewModeBeforeStudio: currentViewMode,
|
|
1407
1509
|
viewMode: '3d',
|
|
1408
1510
|
isFloorplanOpen: false,
|
|
1409
|
-
mode: 'select',
|
|
1410
|
-
tool: null,
|
|
1411
1511
|
catalogCategory: null,
|
|
1412
1512
|
})
|
|
1413
|
-
|
|
1513
|
+
get().armToolMode({ mode: 'select' })
|
|
1414
1514
|
// Clear selection so no edit affordances bleed into the clean canvas.
|
|
1415
1515
|
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
1416
1516
|
} else {
|
|
@@ -1429,6 +1529,8 @@ const useEditor = create<EditorState>()(
|
|
|
1429
1529
|
set({ floorplanPaneRatio: normalizeFloorplanPaneRatio(ratio) }),
|
|
1430
1530
|
mobilePanelSheetHeight: 0,
|
|
1431
1531
|
setMobilePanelSheetHeight: (px) => set({ mobilePanelSheetHeight: Math.max(0, px) }),
|
|
1532
|
+
modelExport: null,
|
|
1533
|
+
setModelExport: (modelExport) => set({ modelExport }),
|
|
1432
1534
|
}),
|
|
1433
1535
|
{
|
|
1434
1536
|
name: 'pascal-editor-ui-preferences',
|
|
@@ -1454,7 +1556,7 @@ const useEditor = create<EditorState>()(
|
|
|
1454
1556
|
: {}),
|
|
1455
1557
|
}
|
|
1456
1558
|
},
|
|
1457
|
-
// `
|
|
1559
|
+
// `toolMode` is persisted, but the interaction scope a brush mode holds is not
|
|
1458
1560
|
// — it lives in a separate, non-persisted store. Rehydrating into
|
|
1459
1561
|
// `terrain-sculpt` (or paint) without re-claiming the scope would restore
|
|
1460
1562
|
// the brush with selection still enabled, so every dab could grab a wall.
|
|
@@ -1463,6 +1565,7 @@ const useEditor = create<EditorState>()(
|
|
|
1463
1565
|
},
|
|
1464
1566
|
partialize: (state) => ({
|
|
1465
1567
|
phase: state.phase,
|
|
1568
|
+
toolMode: state.toolMode,
|
|
1466
1569
|
mode: state.mode,
|
|
1467
1570
|
tool: state.tool,
|
|
1468
1571
|
structureLayer: state.structureLayer,
|
|
@@ -1487,6 +1590,14 @@ const useEditor = create<EditorState>()(
|
|
|
1487
1590
|
),
|
|
1488
1591
|
)
|
|
1489
1592
|
|
|
1593
|
+
export function armToolMode(next: ToolMode): void {
|
|
1594
|
+
useEditor.getState().armToolMode(next)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
export function armMaterialPaint(material?: ActivePaintMaterial): void {
|
|
1598
|
+
useEditor.getState().armMaterialPaint(material)
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1490
1601
|
/**
|
|
1491
1602
|
* Effective magnetic-snap state: the legacy `magneticSnap` flag AND the active
|
|
1492
1603
|
* context's snapping mode. With exclusive modes, magnetic (alignment axes + wall
|