@pascal-app/editor 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5829 -12005
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +178 -149
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +433 -156
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -538
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +10 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +320 -37
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
package/src/store/use-editor.tsx
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import type { AssetInput } from '@pascal-app/core'
|
|
4
4
|
import {
|
|
5
|
+
type AnyNode,
|
|
5
6
|
type AnyNodeId,
|
|
6
7
|
type BuildingNode,
|
|
7
8
|
type CeilingNode,
|
|
9
|
+
type ChimneyMaterialRole,
|
|
10
|
+
type ChimneyNode,
|
|
8
11
|
type ColumnNode,
|
|
9
12
|
type DoorNode,
|
|
13
|
+
type DormerNode,
|
|
14
|
+
type DormerSurfaceMaterialRole,
|
|
15
|
+
type ElevatorNode,
|
|
10
16
|
type FenceNode,
|
|
11
17
|
type ItemNode,
|
|
12
18
|
type LevelNode,
|
|
@@ -27,7 +33,6 @@ import {
|
|
|
27
33
|
import { useViewer } from '@pascal-app/viewer'
|
|
28
34
|
import { create } from 'zustand'
|
|
29
35
|
import { persist } from 'zustand/middleware'
|
|
30
|
-
import { getDefaultCatalogItem } from '../components/ui/item-catalog/catalog-items'
|
|
31
36
|
import {
|
|
32
37
|
type ActivePaintMaterial,
|
|
33
38
|
type PaintableMaterialTarget,
|
|
@@ -43,6 +48,30 @@ const MAX_FLOORPLAN_PANE_RATIO = 0.85
|
|
|
43
48
|
|
|
44
49
|
export type ViewMode = '3d' | '2d' | 'split'
|
|
45
50
|
export type SplitOrientation = 'horizontal' | 'vertical'
|
|
51
|
+
export type WorkspaceMode = 'edit' | 'studio'
|
|
52
|
+
|
|
53
|
+
// Snapshot capture is invoked from two surfaces with different policies.
|
|
54
|
+
// `standard` mirrors the existing user-driven UX — pick region / viewport /
|
|
55
|
+
// area, save the blob as a project thumbnail. `preset` is the constrained
|
|
56
|
+
// variant for the unified preset capture flow (community save-as-preset
|
|
57
|
+
// modal): the overlay locks to a square crop, the renderer clears alpha
|
|
58
|
+
// (transparent background), and the rendered set is locked to `isolated`
|
|
59
|
+
// — `ThumbnailGenerator` consults `captureMode.mode === 'preset'` and
|
|
60
|
+
// applies those constraints. Keeping it a discriminated union lets us
|
|
61
|
+
// add future modes without surfacing the choice to end users.
|
|
62
|
+
export type CaptureMode =
|
|
63
|
+
| { mode: 'idle' }
|
|
64
|
+
| { mode: 'standard' }
|
|
65
|
+
| {
|
|
66
|
+
mode: 'preset'
|
|
67
|
+
isolated: AnyNodeId[]
|
|
68
|
+
framingBounds?: {
|
|
69
|
+
min: [number, number]
|
|
70
|
+
max: [number, number]
|
|
71
|
+
center: [number, number]
|
|
72
|
+
size: [number, number]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
46
75
|
|
|
47
76
|
export type Phase = 'site' | 'structure' | 'furnish'
|
|
48
77
|
|
|
@@ -58,12 +87,25 @@ export type StructureTool =
|
|
|
58
87
|
| 'ceiling'
|
|
59
88
|
| 'roof'
|
|
60
89
|
| 'column'
|
|
90
|
+
| 'elevator'
|
|
61
91
|
| 'stair'
|
|
62
92
|
| 'item'
|
|
63
93
|
| 'zone'
|
|
64
94
|
| 'spawn'
|
|
65
95
|
| 'window'
|
|
66
96
|
| 'door'
|
|
97
|
+
| 'shelf'
|
|
98
|
+
| 'box-vent'
|
|
99
|
+
| 'ridge-vent'
|
|
100
|
+
| 'turbine-vent'
|
|
101
|
+
| 'cupola'
|
|
102
|
+
| 'eyebrow-vent'
|
|
103
|
+
| 'chimney'
|
|
104
|
+
| 'solar-panel'
|
|
105
|
+
| 'skylight'
|
|
106
|
+
| 'dormer'
|
|
107
|
+
| 'gutter'
|
|
108
|
+
| 'downspout'
|
|
67
109
|
|
|
68
110
|
// Furnish mode tools (items and decoration)
|
|
69
111
|
export type FurnishTool = 'item'
|
|
@@ -86,9 +128,29 @@ export type StructureLayer = 'zones' | 'elements'
|
|
|
86
128
|
export type FloorplanSelectionTool = 'click' | 'marquee'
|
|
87
129
|
export type GridSnapStep = 0.5 | 0.25 | 0.1 | 0.05
|
|
88
130
|
|
|
131
|
+
export type NavigationSyncSource = '2d' | '3d'
|
|
132
|
+
|
|
133
|
+
export type NavigationSyncPose = {
|
|
134
|
+
source: NavigationSyncSource
|
|
135
|
+
revision: number
|
|
136
|
+
target: [number, number, number]
|
|
137
|
+
azimuth: number
|
|
138
|
+
viewWidth: number
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type NavigationSyncPoseInput = Omit<NavigationSyncPose, 'revision'>
|
|
142
|
+
|
|
89
143
|
// Combined tool type
|
|
90
144
|
export type Tool = SiteTool | StructureTool | FurnishTool
|
|
91
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Starting parameters seeded into a draw tool before it mints a node.
|
|
148
|
+
* A loose param bag — the tool's create path validates it through the
|
|
149
|
+
* kind's schema (`FenceNode.parse({ ...defaults, start, end })`), which
|
|
150
|
+
* is the real type gate, so unknown keys are simply ignored.
|
|
151
|
+
*/
|
|
152
|
+
export type ToolDefaults = Record<string, unknown>
|
|
153
|
+
|
|
92
154
|
export type MovingWallEndpoint = {
|
|
93
155
|
wall: WallNode
|
|
94
156
|
endpoint: 'start' | 'end'
|
|
@@ -103,6 +165,8 @@ export type MaterialTargetRole =
|
|
|
103
165
|
| WallSurfaceSide
|
|
104
166
|
| StairSurfaceMaterialRole
|
|
105
167
|
| RoofSurfaceMaterialRole
|
|
168
|
+
| ChimneyMaterialRole
|
|
169
|
+
| DormerSurfaceMaterialRole
|
|
106
170
|
| SingleSurfaceMaterialRole
|
|
107
171
|
|
|
108
172
|
export type SelectedMaterialTarget = {
|
|
@@ -116,6 +180,8 @@ type MaterialPaintSelectionSnapshot = {
|
|
|
116
180
|
activePaintMaterial: ActivePaintMaterial | null
|
|
117
181
|
}
|
|
118
182
|
|
|
183
|
+
export type SurfaceHoleTarget = { nodeId: string; holeIndex: number }
|
|
184
|
+
|
|
119
185
|
export type GuideUiState = {
|
|
120
186
|
locked?: boolean
|
|
121
187
|
scaleReferenceVisible?: boolean
|
|
@@ -128,6 +194,15 @@ type EditorState = {
|
|
|
128
194
|
setMode: (mode: Mode) => void
|
|
129
195
|
tool: Tool | null
|
|
130
196
|
setTool: (tool: Tool | null) => void
|
|
197
|
+
/**
|
|
198
|
+
* Per-tool starting parameters for the next node a draw tool mints.
|
|
199
|
+
* Transient (not persisted): host apps seed an entry just before
|
|
200
|
+
* activating the tool (placing a drawn preset, or a future dimension
|
|
201
|
+
* picker), the tool's create path merges it, and the tool clears its
|
|
202
|
+
* own entry on deactivation so a later manual draw isn't poisoned.
|
|
203
|
+
*/
|
|
204
|
+
toolDefaults: Partial<Record<Tool, ToolDefaults>>
|
|
205
|
+
setToolDefaults: (tool: Tool, defaults: ToolDefaults | null) => void
|
|
131
206
|
structureLayer: StructureLayer
|
|
132
207
|
setStructureLayer: (layer: StructureLayer) => void
|
|
133
208
|
catalogCategory: CatalogCategory | null
|
|
@@ -138,8 +213,11 @@ type EditorState = {
|
|
|
138
213
|
| ItemNode
|
|
139
214
|
| WindowNode
|
|
140
215
|
| DoorNode
|
|
216
|
+
| ElevatorNode
|
|
141
217
|
| CeilingNode
|
|
218
|
+
| ChimneyNode
|
|
142
219
|
| ColumnNode
|
|
220
|
+
| DormerNode
|
|
143
221
|
| SlabNode
|
|
144
222
|
| WallNode
|
|
145
223
|
| FenceNode
|
|
@@ -150,13 +228,23 @@ type EditorState = {
|
|
|
150
228
|
| StairSegmentNode
|
|
151
229
|
| BuildingNode
|
|
152
230
|
| null
|
|
231
|
+
/**
|
|
232
|
+
* True while a move was engaged by a press-drag gizmo (the on-canvas move
|
|
233
|
+
* cross) rather than a click-to-place flow. The placement coordinator reads
|
|
234
|
+
* this to commit on pointer-release instead of waiting for a click.
|
|
235
|
+
*/
|
|
236
|
+
placementDragMode: boolean
|
|
237
|
+
setPlacementDragMode: (dragMode: boolean) => void
|
|
153
238
|
setMovingNode: (
|
|
154
239
|
node:
|
|
155
240
|
| ItemNode
|
|
156
241
|
| WindowNode
|
|
157
242
|
| DoorNode
|
|
243
|
+
| ElevatorNode
|
|
158
244
|
| CeilingNode
|
|
245
|
+
| ChimneyNode
|
|
159
246
|
| ColumnNode
|
|
247
|
+
| DormerNode
|
|
160
248
|
| SlabNode
|
|
161
249
|
| WallNode
|
|
162
250
|
| FenceNode
|
|
@@ -168,10 +256,41 @@ type EditorState = {
|
|
|
168
256
|
| BuildingNode
|
|
169
257
|
| null,
|
|
170
258
|
) => void
|
|
259
|
+
/**
|
|
260
|
+
* Which view (2D floor plan or 3D viewer) most recently completed
|
|
261
|
+
* the active move — set by the committing or cancelling side just
|
|
262
|
+
* before clearing `movingNode`. Lets the *other* side's effect
|
|
263
|
+
* cleanup skip its own restore-from-snapshot when the drag was
|
|
264
|
+
* already finalised elsewhere (split view mounts both the 2D
|
|
265
|
+
* overlay and the 3D move tool for the same `movingNode`).
|
|
266
|
+
*
|
|
267
|
+
* Reset to null when the next non-null `setMovingNode` starts a
|
|
268
|
+
* fresh drag (so stale values from the previous drag don't poison
|
|
269
|
+
* cleanups). Preserved across `setMovingNode(null)` so the
|
|
270
|
+
* non-owning side's cleanup — which fires after the clear
|
|
271
|
+
* propagates — can still read who finalised. Null while a drag
|
|
272
|
+
* is in progress means "no side has claimed it yet" — both
|
|
273
|
+
* cleanups then restore to their pre-drag snapshot, which is the
|
|
274
|
+
* same baseline, so the result is idempotent.
|
|
275
|
+
*/
|
|
276
|
+
movingNodeOrigin: '2d' | '3d' | null
|
|
277
|
+
setMovingNodeOrigin: (origin: '2d' | '3d' | null) => void
|
|
171
278
|
movingWallEndpoint: MovingWallEndpoint | null
|
|
172
279
|
setMovingWallEndpoint: (value: MovingWallEndpoint | null) => void
|
|
173
280
|
movingFenceEndpoint: MovingFenceEndpoint | null
|
|
174
281
|
setMovingFenceEndpoint: (value: MovingFenceEndpoint | null) => void
|
|
282
|
+
/**
|
|
283
|
+
* Generic per-kind handle drag state. Set by a node's resize handle
|
|
284
|
+
* (height arrow, width arrow, rise / sweep / inner-radius for curved
|
|
285
|
+
* stairs, …) at drag-start and cleared on drag-end. `label`
|
|
286
|
+
* identifies which dimension the handle controls — measurement
|
|
287
|
+
* overlays read it to render the right caption; the camera controls
|
|
288
|
+
* use the truthy value to suppress one-finger pan-rotate. Replaces
|
|
289
|
+
* the previous per-kind `resizing*` fields so adding a new resize
|
|
290
|
+
* handle doesn't require a new store field.
|
|
291
|
+
*/
|
|
292
|
+
activeHandleDrag: { nodeId: AnyNodeId; label: string } | null
|
|
293
|
+
setActiveHandleDrag: (drag: { nodeId: AnyNodeId; label: string } | null) => void
|
|
175
294
|
curvingWall: WallNode | null
|
|
176
295
|
setCurvingWall: (wall: WallNode | null) => void
|
|
177
296
|
curvingFence: FenceNode | null
|
|
@@ -182,6 +301,10 @@ type EditorState = {
|
|
|
182
301
|
setActivePaintMaterial: (material: ActivePaintMaterial | null) => void
|
|
183
302
|
activePaintTarget: PaintableMaterialTarget
|
|
184
303
|
setActivePaintTarget: (target: PaintableMaterialTarget) => void
|
|
304
|
+
// When true, clicking a surface in paint mode clears it back to its
|
|
305
|
+
// default material instead of applying `activePaintMaterial`.
|
|
306
|
+
paintEraser: boolean
|
|
307
|
+
setPaintEraser: (eraser: boolean) => void
|
|
185
308
|
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
|
186
309
|
hoveredPaintTarget: PaintableMaterialTarget | null
|
|
187
310
|
setHoveredPaintTarget: (target: PaintableMaterialTarget | null) => void
|
|
@@ -197,14 +320,23 @@ type EditorState = {
|
|
|
197
320
|
spaces: Record<string, Space>
|
|
198
321
|
setSpaces: (spaces: Record<string, Space>) => void
|
|
199
322
|
// Generic hole editing (works for slabs, ceilings, and any future polygon nodes)
|
|
200
|
-
editingHole:
|
|
201
|
-
setEditingHole: (hole:
|
|
323
|
+
editingHole: SurfaceHoleTarget | null
|
|
324
|
+
setEditingHole: (hole: SurfaceHoleTarget | null) => void
|
|
325
|
+
hoveredHole: SurfaceHoleTarget | null
|
|
326
|
+
setHoveredHole: (hole: SurfaceHoleTarget | null) => void
|
|
202
327
|
// Preview mode (viewer-like experience inside the editor)
|
|
203
328
|
isPreviewMode: boolean
|
|
204
329
|
setPreviewMode: (preview: boolean) => void
|
|
205
|
-
// Capture mode (snapshot toolbar — hides panels for clean framing)
|
|
330
|
+
// Capture mode (snapshot toolbar — hides panels for clean framing).
|
|
331
|
+
// `captureMode` is the canonical discriminated-union state; the boolean
|
|
332
|
+
// `isCaptureMode` is kept synced as a derived convenience for the many
|
|
333
|
+
// existing read sites that just gate chrome visibility on "is capture
|
|
334
|
+
// active". New write sites should pass a `CaptureMode` shape; passing a
|
|
335
|
+
// boolean is accepted as a back-compat shim (`true` → `'standard'`,
|
|
336
|
+
// `false` → `'idle'`).
|
|
337
|
+
captureMode: CaptureMode
|
|
206
338
|
isCaptureMode: boolean
|
|
207
|
-
setCaptureMode: (
|
|
339
|
+
setCaptureMode: (next: boolean | CaptureMode) => void
|
|
208
340
|
// View mode (3D only, 2D only, or split 2D+3D)
|
|
209
341
|
viewMode: ViewMode
|
|
210
342
|
setViewMode: (mode: ViewMode) => void
|
|
@@ -216,10 +348,17 @@ type EditorState = {
|
|
|
216
348
|
toggleFloorplanOpen: () => void
|
|
217
349
|
isFloorplanHovered: boolean
|
|
218
350
|
setFloorplanHovered: (hovered: boolean) => void
|
|
351
|
+
navigationSyncPose: NavigationSyncPose | null
|
|
352
|
+
publishNavigationSyncPose: (pose: NavigationSyncPoseInput) => void
|
|
219
353
|
floorplanSelectionTool: FloorplanSelectionTool
|
|
220
354
|
setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
|
|
221
355
|
gridSnapStep: GridSnapStep
|
|
222
356
|
setGridSnapStep: (step: GridSnapStep) => void
|
|
357
|
+
// Magnetic snapping while drafting — snaps wall endpoints onto existing
|
|
358
|
+
// wall corners / wall bodies (the "magnetic" beacon). Independent of grid
|
|
359
|
+
// snap. On by default; toggled from the Display menu.
|
|
360
|
+
magneticSnap: boolean
|
|
361
|
+
setMagneticSnap: (enabled: boolean) => void
|
|
223
362
|
showReferenceFloor: boolean
|
|
224
363
|
toggleReferenceFloor: () => void
|
|
225
364
|
setShowReferenceFloor: (show: boolean) => void
|
|
@@ -234,9 +373,14 @@ type EditorState = {
|
|
|
234
373
|
isFirstPersonMode: boolean
|
|
235
374
|
_viewModeBeforeFirstPerson: ViewMode | null
|
|
236
375
|
setFirstPersonMode: (enabled: boolean) => void
|
|
376
|
+
// Workspace mode: 'edit' is the full editing surface; 'studio' is the
|
|
377
|
+
// render/snapshot surface (clean canvas, no editing chrome or selection).
|
|
378
|
+
// Entering studio forces a 3D-only view and restores the prior view on exit.
|
|
379
|
+
workspaceMode: WorkspaceMode
|
|
380
|
+
_viewModeBeforeStudio: ViewMode | null
|
|
381
|
+
setWorkspaceMode: (mode: WorkspaceMode) => void
|
|
237
382
|
activeSidebarPanel: string
|
|
238
383
|
setActiveSidebarPanel: (id: string) => void
|
|
239
|
-
setIsCaptureMode: (enabled: boolean) => void
|
|
240
384
|
floorplanPaneRatio: number
|
|
241
385
|
setFloorplanPaneRatio: (ratio: number) => void
|
|
242
386
|
// Mobile-only: pixel height of the secondary panel sheet while open (0 when closed).
|
|
@@ -257,6 +401,7 @@ type PersistedEditorLayoutState = Pick<
|
|
|
257
401
|
| 'splitOrientation'
|
|
258
402
|
| 'floorplanSelectionTool'
|
|
259
403
|
| 'gridSnapStep'
|
|
404
|
+
| 'magneticSnap'
|
|
260
405
|
| 'showReferenceFloor'
|
|
261
406
|
| 'referenceFloorOffset'
|
|
262
407
|
| 'referenceFloorOpacity'
|
|
@@ -279,6 +424,7 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
279
424
|
splitOrientation: 'horizontal',
|
|
280
425
|
floorplanSelectionTool: 'click',
|
|
281
426
|
gridSnapStep: 0.5,
|
|
427
|
+
magneticSnap: true,
|
|
282
428
|
showReferenceFloor: false,
|
|
283
429
|
referenceFloorOffset: 1,
|
|
284
430
|
referenceFloorOpacity: 0.35,
|
|
@@ -286,6 +432,10 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
286
432
|
|
|
287
433
|
const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
|
|
288
434
|
|
|
435
|
+
type SelectDefaultBuildingAndLevelOptions = {
|
|
436
|
+
forceGroundLevel?: boolean
|
|
437
|
+
}
|
|
438
|
+
|
|
289
439
|
function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
|
|
290
440
|
if (phase === 'site') {
|
|
291
441
|
return 'select'
|
|
@@ -391,6 +541,8 @@ function normalizePersistedEditorLayoutState(
|
|
|
391
541
|
gridSnapStep: GRID_SNAP_STEPS.includes(state?.gridSnapStep as GridSnapStep)
|
|
392
542
|
? (state?.gridSnapStep as GridSnapStep)
|
|
393
543
|
: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
544
|
+
// Default on: only an explicit persisted `false` disables it.
|
|
545
|
+
magneticSnap: state?.magneticSnap !== false,
|
|
394
546
|
showReferenceFloor: state?.showReferenceFloor === true,
|
|
395
547
|
referenceFloorOffset:
|
|
396
548
|
typeof state?.referenceFloorOffset === 'number' && state.referenceFloorOffset >= 1
|
|
@@ -420,44 +572,94 @@ export function hasCustomPersistedEditorUiState(
|
|
|
420
572
|
)
|
|
421
573
|
}
|
|
422
574
|
|
|
575
|
+
function getDefaultLevelId(
|
|
576
|
+
buildingNode: BuildingNode,
|
|
577
|
+
nodes: Record<string, AnyNode>,
|
|
578
|
+
): LevelNode['id'] | null {
|
|
579
|
+
const levels = buildingNode.children
|
|
580
|
+
.map((childId) => nodes[childId as AnyNodeId])
|
|
581
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
582
|
+
|
|
583
|
+
if (levels.length === 0) {
|
|
584
|
+
return null
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const groundLevel = levels.find((level) => level.level === 0)
|
|
588
|
+
if (groundLevel) {
|
|
589
|
+
return groundLevel.id
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const firstLevel = levels[0]
|
|
593
|
+
if (!firstLevel) {
|
|
594
|
+
return null
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let lowestLevel = firstLevel
|
|
598
|
+
for (const level of levels.slice(1)) {
|
|
599
|
+
if (level.level < lowestLevel.level) {
|
|
600
|
+
lowestLevel = level
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return lowestLevel.id
|
|
605
|
+
}
|
|
606
|
+
|
|
423
607
|
/**
|
|
424
608
|
* Selects the first building and level 0 in the scene.
|
|
425
609
|
* Safe to call any time — no-ops if already selected or scene is empty.
|
|
426
610
|
*/
|
|
427
|
-
export function selectDefaultBuildingAndLevel() {
|
|
611
|
+
export function selectDefaultBuildingAndLevel(options: SelectDefaultBuildingAndLevelOptions = {}) {
|
|
428
612
|
const viewer = useViewer.getState()
|
|
429
613
|
const scene = useScene.getState()
|
|
430
614
|
|
|
431
|
-
|
|
615
|
+
const selectedBuilding = viewer.selection.buildingId
|
|
616
|
+
? scene.nodes[viewer.selection.buildingId]
|
|
617
|
+
: null
|
|
618
|
+
let buildingNode =
|
|
619
|
+
selectedBuilding?.type === 'building' ? (selectedBuilding as BuildingNode) : null
|
|
432
620
|
|
|
433
621
|
// If no building selected, find the first one from site's children
|
|
434
|
-
if (!
|
|
622
|
+
if (!buildingNode) {
|
|
435
623
|
const siteNode = scene.rootNodeIds[0] ? scene.nodes[scene.rootNodeIds[0]] : null
|
|
436
624
|
if (siteNode?.type === 'site') {
|
|
437
|
-
|
|
438
|
-
.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
buildingId = firstBuilding.id as BuildingNode['id']
|
|
442
|
-
viewer.setSelection({ buildingId })
|
|
443
|
-
}
|
|
625
|
+
buildingNode =
|
|
626
|
+
siteNode.children
|
|
627
|
+
.map((childId) => scene.nodes[childId as AnyNodeId])
|
|
628
|
+
.find((node): node is BuildingNode => node?.type === 'building') ?? null
|
|
444
629
|
}
|
|
445
630
|
}
|
|
446
631
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
632
|
+
if (!buildingNode) {
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const selectedLevel = viewer.selection.levelId ? scene.nodes[viewer.selection.levelId] : null
|
|
637
|
+
const selectedLevelBelongsToBuilding =
|
|
638
|
+
selectedLevel?.type === 'level' && selectedLevel.parentId === buildingNode.id
|
|
639
|
+
const shouldSelectDefaultLevel = options.forceGroundLevel || !selectedLevelBelongsToBuilding
|
|
640
|
+
const defaultLevelId = shouldSelectDefaultLevel
|
|
641
|
+
? getDefaultLevelId(buildingNode, scene.nodes as Record<string, AnyNode>)
|
|
642
|
+
: null
|
|
643
|
+
|
|
644
|
+
const selectionUpdate: Parameters<typeof viewer.setSelection>[0] = {}
|
|
645
|
+
if (viewer.selection.buildingId !== buildingNode.id) {
|
|
646
|
+
selectionUpdate.buildingId = buildingNode.id
|
|
647
|
+
}
|
|
648
|
+
if (defaultLevelId) {
|
|
649
|
+
selectionUpdate.levelId = defaultLevelId
|
|
460
650
|
}
|
|
651
|
+
|
|
652
|
+
if (Object.keys(selectionUpdate).length > 0) {
|
|
653
|
+
viewer.setSelection(selectionUpdate)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function selectSiteFloorplanContext() {
|
|
658
|
+
selectDefaultBuildingAndLevel({ forceGroundLevel: true })
|
|
659
|
+
useViewer.getState().setSelection({
|
|
660
|
+
selectedIds: [],
|
|
661
|
+
zoneId: null,
|
|
662
|
+
})
|
|
461
663
|
}
|
|
462
664
|
|
|
463
665
|
const useEditor = create<EditorState>()(
|
|
@@ -488,12 +690,9 @@ const useEditor = create<EditorState>()(
|
|
|
488
690
|
set({ mode: 'select', tool: null, catalogCategory: null })
|
|
489
691
|
}
|
|
490
692
|
|
|
491
|
-
const viewer = useViewer.getState()
|
|
492
|
-
|
|
493
693
|
switch (phase) {
|
|
494
694
|
case 'site':
|
|
495
|
-
|
|
496
|
-
viewer.resetSelection()
|
|
695
|
+
selectSiteFloorplanContext()
|
|
497
696
|
break
|
|
498
697
|
|
|
499
698
|
case 'structure':
|
|
@@ -534,6 +733,17 @@ const useEditor = create<EditorState>()(
|
|
|
534
733
|
},
|
|
535
734
|
tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
|
|
536
735
|
setTool: (tool) => set({ tool }),
|
|
736
|
+
toolDefaults: {},
|
|
737
|
+
setToolDefaults: (tool, defaults) =>
|
|
738
|
+
set((state) => {
|
|
739
|
+
const next = { ...state.toolDefaults }
|
|
740
|
+
if (defaults === null) {
|
|
741
|
+
delete next[tool]
|
|
742
|
+
} else {
|
|
743
|
+
next[tool] = defaults
|
|
744
|
+
}
|
|
745
|
+
return { toolDefaults: next }
|
|
746
|
+
}),
|
|
537
747
|
structureLayer: DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer,
|
|
538
748
|
setStructureLayer: (layer) => {
|
|
539
749
|
const { mode } = get()
|
|
@@ -559,21 +769,40 @@ const useEditor = create<EditorState>()(
|
|
|
559
769
|
| ItemNode
|
|
560
770
|
| WindowNode
|
|
561
771
|
| DoorNode
|
|
772
|
+
| ElevatorNode
|
|
562
773
|
| CeilingNode
|
|
774
|
+
| ColumnNode
|
|
563
775
|
| SlabNode
|
|
564
776
|
| WallNode
|
|
565
777
|
| FenceNode
|
|
566
778
|
| RoofNode
|
|
567
779
|
| RoofSegmentNode
|
|
780
|
+
| SpawnNode
|
|
568
781
|
| StairNode
|
|
569
782
|
| StairSegmentNode
|
|
570
783
|
| BuildingNode
|
|
571
784
|
| null,
|
|
572
|
-
|
|
785
|
+
placementDragMode: false,
|
|
786
|
+
setPlacementDragMode: (dragMode) => set({ placementDragMode: dragMode }),
|
|
787
|
+
setMovingNode: (node) =>
|
|
788
|
+
set(
|
|
789
|
+
node === null
|
|
790
|
+
? // Preserve `movingNodeOrigin` across the clear so the
|
|
791
|
+
// non-owning side's effect cleanup — which fires after
|
|
792
|
+
// `setMovingNode(null)` propagates — can still read who
|
|
793
|
+
// finalised. The next non-null `setMovingNode` resets it.
|
|
794
|
+
// Always clear the press-drag flag when a move ends.
|
|
795
|
+
{ movingNode: null, placementDragMode: false }
|
|
796
|
+
: { movingNode: node, movingNodeOrigin: null },
|
|
797
|
+
),
|
|
798
|
+
movingNodeOrigin: null as '2d' | '3d' | null,
|
|
799
|
+
setMovingNodeOrigin: (origin) => set({ movingNodeOrigin: origin }),
|
|
573
800
|
movingWallEndpoint: null,
|
|
574
801
|
setMovingWallEndpoint: (value) => set({ movingWallEndpoint: value }),
|
|
575
802
|
movingFenceEndpoint: null,
|
|
576
803
|
setMovingFenceEndpoint: (value) => set({ movingFenceEndpoint: value }),
|
|
804
|
+
activeHandleDrag: null,
|
|
805
|
+
setActiveHandleDrag: (drag) => set({ activeHandleDrag: drag }),
|
|
577
806
|
curvingWall: null,
|
|
578
807
|
setCurvingWall: (wall) => set({ curvingWall: wall }),
|
|
579
808
|
curvingFence: null,
|
|
@@ -581,12 +810,17 @@ const useEditor = create<EditorState>()(
|
|
|
581
810
|
selectedMaterialTarget: null,
|
|
582
811
|
setSelectedMaterialTarget: (target) => set({ selectedMaterialTarget: target }),
|
|
583
812
|
activePaintMaterial: null,
|
|
584
|
-
|
|
813
|
+
// Picking a material implies paint, not erase — clear the eraser so the
|
|
814
|
+
// next click applies the chosen material.
|
|
815
|
+
setActivePaintMaterial: (material) =>
|
|
816
|
+
set({ activePaintMaterial: material, paintEraser: false }),
|
|
585
817
|
activePaintTarget: 'wall',
|
|
586
818
|
setActivePaintTarget: (target) =>
|
|
587
819
|
set((state) =>
|
|
588
820
|
state.activePaintTarget === target ? state : { activePaintTarget: target },
|
|
589
821
|
),
|
|
822
|
+
paintEraser: false,
|
|
823
|
+
setPaintEraser: (eraser) => set({ paintEraser: eraser }),
|
|
590
824
|
primeMaterialPaintFromSelection: () => {
|
|
591
825
|
const selectedId =
|
|
592
826
|
useViewer.getState().selection.selectedIds.length === 1
|
|
@@ -657,6 +891,14 @@ const useEditor = create<EditorState>()(
|
|
|
657
891
|
setSpaces: (spaces) => set({ spaces }),
|
|
658
892
|
editingHole: null,
|
|
659
893
|
setEditingHole: (hole) => set({ editingHole: hole }),
|
|
894
|
+
hoveredHole: null,
|
|
895
|
+
setHoveredHole: (hole) =>
|
|
896
|
+
set((state) =>
|
|
897
|
+
state.hoveredHole?.nodeId === hole?.nodeId &&
|
|
898
|
+
state.hoveredHole?.holeIndex === hole?.holeIndex
|
|
899
|
+
? state
|
|
900
|
+
: { hoveredHole: hole },
|
|
901
|
+
),
|
|
660
902
|
isPreviewMode: false,
|
|
661
903
|
setPreviewMode: (preview) => {
|
|
662
904
|
if (preview) {
|
|
@@ -667,8 +909,13 @@ const useEditor = create<EditorState>()(
|
|
|
667
909
|
set({ isPreviewMode: false })
|
|
668
910
|
}
|
|
669
911
|
},
|
|
912
|
+
captureMode: { mode: 'idle' } as CaptureMode,
|
|
670
913
|
isCaptureMode: false,
|
|
671
|
-
setCaptureMode: (
|
|
914
|
+
setCaptureMode: (next) => {
|
|
915
|
+
const resolved: CaptureMode =
|
|
916
|
+
typeof next === 'boolean' ? { mode: next ? 'standard' : 'idle' } : next
|
|
917
|
+
set({ captureMode: resolved, isCaptureMode: resolved.mode !== 'idle' })
|
|
918
|
+
},
|
|
672
919
|
viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode,
|
|
673
920
|
setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }),
|
|
674
921
|
splitOrientation: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.splitOrientation,
|
|
@@ -682,10 +929,20 @@ const useEditor = create<EditorState>()(
|
|
|
682
929
|
}),
|
|
683
930
|
isFloorplanHovered: false,
|
|
684
931
|
setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
|
|
932
|
+
navigationSyncPose: null,
|
|
933
|
+
publishNavigationSyncPose: (pose) =>
|
|
934
|
+
set((state) => ({
|
|
935
|
+
navigationSyncPose: {
|
|
936
|
+
...pose,
|
|
937
|
+
revision: (state.navigationSyncPose?.revision ?? 0) + 1,
|
|
938
|
+
},
|
|
939
|
+
})),
|
|
685
940
|
floorplanSelectionTool: 'click' as FloorplanSelectionTool,
|
|
686
941
|
setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
|
|
687
942
|
gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
688
943
|
setGridSnapStep: (step) => set({ gridSnapStep: step }),
|
|
944
|
+
magneticSnap: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.magneticSnap,
|
|
945
|
+
setMagneticSnap: (enabled) => set({ magneticSnap: enabled }),
|
|
689
946
|
showReferenceFloor: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showReferenceFloor,
|
|
690
947
|
toggleReferenceFloor: () =>
|
|
691
948
|
set((state) => ({ showReferenceFloor: !state.showReferenceFloor })),
|
|
@@ -721,9 +978,34 @@ const useEditor = create<EditorState>()(
|
|
|
721
978
|
})
|
|
722
979
|
}
|
|
723
980
|
},
|
|
981
|
+
workspaceMode: 'edit' as WorkspaceMode,
|
|
982
|
+
_viewModeBeforeStudio: null as ViewMode | null,
|
|
983
|
+
setWorkspaceMode: (mode) => {
|
|
984
|
+
if (get().workspaceMode === mode) return
|
|
985
|
+
if (mode === 'studio') {
|
|
986
|
+
const currentViewMode = get().viewMode
|
|
987
|
+
set({
|
|
988
|
+
workspaceMode: 'studio',
|
|
989
|
+
_viewModeBeforeStudio: currentViewMode,
|
|
990
|
+
viewMode: '3d',
|
|
991
|
+
isFloorplanOpen: false,
|
|
992
|
+
mode: 'select',
|
|
993
|
+
tool: null,
|
|
994
|
+
catalogCategory: null,
|
|
995
|
+
})
|
|
996
|
+
// Clear selection so no edit affordances bleed into the clean canvas.
|
|
997
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
998
|
+
} else {
|
|
999
|
+
const prevMode = get()._viewModeBeforeStudio
|
|
1000
|
+
set({
|
|
1001
|
+
workspaceMode: 'edit',
|
|
1002
|
+
_viewModeBeforeStudio: null,
|
|
1003
|
+
...(prevMode ? { viewMode: prevMode, isFloorplanOpen: prevMode !== '3d' } : {}),
|
|
1004
|
+
})
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
724
1007
|
activeSidebarPanel: DEFAULT_ACTIVE_SIDEBAR_PANEL,
|
|
725
1008
|
setActiveSidebarPanel: (id) => set({ activeSidebarPanel: id }),
|
|
726
|
-
setIsCaptureMode: (enabled) => set({ isCaptureMode: enabled }),
|
|
727
1009
|
floorplanPaneRatio: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.floorplanPaneRatio,
|
|
728
1010
|
setFloorplanPaneRatio: (ratio) =>
|
|
729
1011
|
set({ floorplanPaneRatio: normalizeFloorplanPaneRatio(ratio) }),
|
|
@@ -750,6 +1032,7 @@ const useEditor = create<EditorState>()(
|
|
|
750
1032
|
splitOrientation: state.splitOrientation,
|
|
751
1033
|
floorplanSelectionTool: state.floorplanSelectionTool,
|
|
752
1034
|
gridSnapStep: state.gridSnapStep,
|
|
1035
|
+
magneticSnap: state.magneticSnap,
|
|
753
1036
|
showReferenceFloor: state.showReferenceFloor,
|
|
754
1037
|
referenceFloorOffset: state.referenceFloorOffset,
|
|
755
1038
|
referenceFloorOpacity: state.referenceFloorOpacity,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Ephemeral store for a placement tool's 2D floor-plan ghost. A registry
|
|
2
|
+
// placement tool (e.g. column / elevator) publishes a fully-positioned,
|
|
3
|
+
// transient preview node on each `grid:move`; the floor-plan
|
|
4
|
+
// placement-preview layer subscribes and renders the node's `def.floorplan`
|
|
5
|
+
// footprint as a faint ghost that follows the cursor. The 3D view already
|
|
6
|
+
// shows a translucent mesh preview, so this only feeds the 2D layer.
|
|
7
|
+
//
|
|
8
|
+
// Editor-only: the read-only viewer route never places nodes. Lives here
|
|
9
|
+
// rather than in `core` for that reason; node-kind tools (e.g. column) reach
|
|
10
|
+
// it through the `@pascal-app/editor` public surface, the same way they
|
|
11
|
+
// already consume `triggerSFX`. Producers clear on commit, cancel, and
|
|
12
|
+
// unmount.
|
|
13
|
+
|
|
14
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
15
|
+
import { create } from 'zustand'
|
|
16
|
+
|
|
17
|
+
type PlacementPreviewState = {
|
|
18
|
+
/** Transient preview node, already positioned + rotated at the (snapped,
|
|
19
|
+
* aligned) cursor. `null` when no placement is active. */
|
|
20
|
+
node: AnyNode | null
|
|
21
|
+
set(node: AnyNode | null): void
|
|
22
|
+
clear(): void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const usePlacementPreview = create<PlacementPreviewState>((set) => ({
|
|
26
|
+
node: null,
|
|
27
|
+
set: (node) => set({ node }),
|
|
28
|
+
clear: () => set({ node: null }),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
export default usePlacementPreview
|