@pascal-app/editor 0.7.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 +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- 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 +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- 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 +5837 -12006
- 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 +234 -190
- 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 +435 -158
- 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 +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- 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-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- 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 +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- 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 +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- 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 +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- 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 -543
- 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 +57 -1
- 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 +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- 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 +8 -12
- 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 +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -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 +24 -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 +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- 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.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- 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 +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- 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 +342 -91
- 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/tsconfig.json +2 -1
- 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/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- 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 -257
- 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 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- 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 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- 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 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- 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 -230
- 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 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
package/src/store/use-editor.tsx
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { AssetInput } from '@pascal-app/core'
|
|
3
4
|
import {
|
|
5
|
+
type AnyNode,
|
|
4
6
|
type AnyNodeId,
|
|
5
|
-
type AssetInput,
|
|
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,
|
|
@@ -36,13 +41,37 @@ import {
|
|
|
36
41
|
type SingleSurfaceMaterialRole,
|
|
37
42
|
} from '../lib/material-paint'
|
|
38
43
|
|
|
39
|
-
const DEFAULT_ACTIVE_SIDEBAR_PANEL = '
|
|
44
|
+
const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'ai'
|
|
40
45
|
const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
|
|
41
46
|
const MIN_FLOORPLAN_PANE_RATIO = 0.15
|
|
42
47
|
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,11 +213,14 @@ type EditorState = {
|
|
|
138
213
|
| ItemNode
|
|
139
214
|
| WindowNode
|
|
140
215
|
| DoorNode
|
|
141
|
-
|
|
|
216
|
+
| ElevatorNode
|
|
142
217
|
| CeilingNode
|
|
218
|
+
| ChimneyNode
|
|
143
219
|
| ColumnNode
|
|
220
|
+
| DormerNode
|
|
144
221
|
| SlabNode
|
|
145
222
|
| WallNode
|
|
223
|
+
| FenceNode
|
|
146
224
|
| RoofNode
|
|
147
225
|
| RoofSegmentNode
|
|
148
226
|
| SpawnNode
|
|
@@ -150,16 +228,26 @@ 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
|
|
158
|
-
|
|
|
243
|
+
| ElevatorNode
|
|
159
244
|
| CeilingNode
|
|
245
|
+
| ChimneyNode
|
|
160
246
|
| ColumnNode
|
|
247
|
+
| DormerNode
|
|
161
248
|
| SlabNode
|
|
162
249
|
| WallNode
|
|
250
|
+
| FenceNode
|
|
163
251
|
| RoofNode
|
|
164
252
|
| RoofSegmentNode
|
|
165
253
|
| SpawnNode
|
|
@@ -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,11 +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
|
|
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
|
|
338
|
+
isCaptureMode: boolean
|
|
339
|
+
setCaptureMode: (next: boolean | CaptureMode) => void
|
|
205
340
|
// View mode (3D only, 2D only, or split 2D+3D)
|
|
206
341
|
viewMode: ViewMode
|
|
207
342
|
setViewMode: (mode: ViewMode) => void
|
|
@@ -213,10 +348,17 @@ type EditorState = {
|
|
|
213
348
|
toggleFloorplanOpen: () => void
|
|
214
349
|
isFloorplanHovered: boolean
|
|
215
350
|
setFloorplanHovered: (hovered: boolean) => void
|
|
351
|
+
navigationSyncPose: NavigationSyncPose | null
|
|
352
|
+
publishNavigationSyncPose: (pose: NavigationSyncPoseInput) => void
|
|
216
353
|
floorplanSelectionTool: FloorplanSelectionTool
|
|
217
354
|
setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
|
|
218
355
|
gridSnapStep: GridSnapStep
|
|
219
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
|
|
220
362
|
showReferenceFloor: boolean
|
|
221
363
|
toggleReferenceFloor: () => void
|
|
222
364
|
setShowReferenceFloor: (show: boolean) => void
|
|
@@ -224,21 +366,27 @@ type EditorState = {
|
|
|
224
366
|
setReferenceFloorOffset: (offset: number) => void
|
|
225
367
|
referenceFloorOpacity: number
|
|
226
368
|
setReferenceFloorOpacity: (opacity: number) => void
|
|
369
|
+
// Development-only camera debug flag for inspecting underside geometry
|
|
370
|
+
allowUndergroundCamera: boolean
|
|
371
|
+
setAllowUndergroundCamera: (enabled: boolean) => void
|
|
227
372
|
// First-person walkthrough mode (street view)
|
|
228
373
|
isFirstPersonMode: boolean
|
|
229
374
|
_viewModeBeforeFirstPerson: ViewMode | null
|
|
230
375
|
setFirstPersonMode: (enabled: boolean) => void
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
234
382
|
activeSidebarPanel: string
|
|
235
383
|
setActiveSidebarPanel: (id: string) => void
|
|
236
|
-
mobilePanelSheetHeight: number
|
|
237
|
-
setMobilePanelSheetHeight: (height: number) => void
|
|
238
|
-
isCaptureMode: boolean
|
|
239
|
-
setIsCaptureMode: (enabled: boolean) => void
|
|
240
384
|
floorplanPaneRatio: number
|
|
241
385
|
setFloorplanPaneRatio: (ratio: number) => void
|
|
386
|
+
// Mobile-only: pixel height of the secondary panel sheet while open (0 when closed).
|
|
387
|
+
// Read by the mobile layout so the viewer container can shrink to preview edits.
|
|
388
|
+
mobilePanelSheetHeight: number
|
|
389
|
+
setMobilePanelSheetHeight: (px: number) => void
|
|
242
390
|
}
|
|
243
391
|
|
|
244
392
|
export type PersistedEditorUiState = Pick<
|
|
@@ -253,6 +401,7 @@ type PersistedEditorLayoutState = Pick<
|
|
|
253
401
|
| 'splitOrientation'
|
|
254
402
|
| 'floorplanSelectionTool'
|
|
255
403
|
| 'gridSnapStep'
|
|
404
|
+
| 'magneticSnap'
|
|
256
405
|
| 'showReferenceFloor'
|
|
257
406
|
| 'referenceFloorOffset'
|
|
258
407
|
| 'referenceFloorOpacity'
|
|
@@ -275,6 +424,7 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
275
424
|
splitOrientation: 'horizontal',
|
|
276
425
|
floorplanSelectionTool: 'click',
|
|
277
426
|
gridSnapStep: 0.5,
|
|
427
|
+
magneticSnap: true,
|
|
278
428
|
showReferenceFloor: false,
|
|
279
429
|
referenceFloorOffset: 1,
|
|
280
430
|
referenceFloorOpacity: 0.35,
|
|
@@ -282,6 +432,10 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
282
432
|
|
|
283
433
|
const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
|
|
284
434
|
|
|
435
|
+
type SelectDefaultBuildingAndLevelOptions = {
|
|
436
|
+
forceGroundLevel?: boolean
|
|
437
|
+
}
|
|
438
|
+
|
|
285
439
|
function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
|
|
286
440
|
if (phase === 'site') {
|
|
287
441
|
return 'select'
|
|
@@ -387,6 +541,8 @@ function normalizePersistedEditorLayoutState(
|
|
|
387
541
|
gridSnapStep: GRID_SNAP_STEPS.includes(state?.gridSnapStep as GridSnapStep)
|
|
388
542
|
? (state?.gridSnapStep as GridSnapStep)
|
|
389
543
|
: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
544
|
+
// Default on: only an explicit persisted `false` disables it.
|
|
545
|
+
magneticSnap: state?.magneticSnap !== false,
|
|
390
546
|
showReferenceFloor: state?.showReferenceFloor === true,
|
|
391
547
|
referenceFloorOffset:
|
|
392
548
|
typeof state?.referenceFloorOffset === 'number' && state.referenceFloorOffset >= 1
|
|
@@ -416,48 +572,94 @@ export function hasCustomPersistedEditorUiState(
|
|
|
416
572
|
)
|
|
417
573
|
}
|
|
418
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
|
+
|
|
419
607
|
/**
|
|
420
608
|
* Selects the first building and level 0 in the scene.
|
|
421
609
|
* Safe to call any time — no-ops if already selected or scene is empty.
|
|
422
610
|
*/
|
|
423
|
-
export function selectDefaultBuildingAndLevel() {
|
|
611
|
+
export function selectDefaultBuildingAndLevel(options: SelectDefaultBuildingAndLevelOptions = {}) {
|
|
424
612
|
const viewer = useViewer.getState()
|
|
425
613
|
const scene = useScene.getState()
|
|
426
614
|
|
|
427
|
-
|
|
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
|
|
428
620
|
|
|
429
621
|
// If no building selected, find the first one from site's children
|
|
430
|
-
if (!
|
|
622
|
+
if (!buildingNode) {
|
|
431
623
|
const siteNode = scene.rootNodeIds[0] ? scene.nodes[scene.rootNodeIds[0]] : null
|
|
432
624
|
if (siteNode?.type === 'site') {
|
|
433
|
-
|
|
434
|
-
.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
buildingId = firstBuilding.id as BuildingNode['id']
|
|
438
|
-
viewer.setSelection({ buildingId })
|
|
439
|
-
}
|
|
625
|
+
buildingNode =
|
|
626
|
+
siteNode.children
|
|
627
|
+
.map((childId) => scene.nodes[childId as AnyNodeId])
|
|
628
|
+
.find((node): node is BuildingNode => node?.type === 'building') ?? null
|
|
440
629
|
}
|
|
441
630
|
}
|
|
442
631
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (Object.keys(selectionUpdate).length > 0) {
|
|
653
|
+
viewer.setSelection(selectionUpdate)
|
|
456
654
|
}
|
|
457
655
|
}
|
|
458
656
|
|
|
459
|
-
function
|
|
460
|
-
|
|
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>()(
|
|
@@ -481,23 +683,16 @@ const useEditor = create<EditorState>()(
|
|
|
481
683
|
} else if (phase === 'structure') {
|
|
482
684
|
set({ tool: 'wall', catalogCategory: null })
|
|
483
685
|
} else if (phase === 'furnish') {
|
|
484
|
-
set({
|
|
485
|
-
tool: 'item',
|
|
486
|
-
catalogCategory: 'furniture',
|
|
487
|
-
selectedItem: getDefaultSelectedItemForCategory('furniture'),
|
|
488
|
-
})
|
|
686
|
+
set({ tool: 'item', catalogCategory: 'furniture' })
|
|
489
687
|
}
|
|
490
688
|
} else {
|
|
491
689
|
// Reset to select mode and clear tool/catalog when switching phases
|
|
492
690
|
set({ mode: 'select', tool: null, catalogCategory: null })
|
|
493
691
|
}
|
|
494
692
|
|
|
495
|
-
const viewer = useViewer.getState()
|
|
496
|
-
|
|
497
693
|
switch (phase) {
|
|
498
694
|
case 'site':
|
|
499
|
-
|
|
500
|
-
viewer.resetSelection()
|
|
695
|
+
selectSiteFloorplanContext()
|
|
501
696
|
break
|
|
502
697
|
|
|
503
698
|
case 'structure':
|
|
@@ -525,15 +720,8 @@ const useEditor = create<EditorState>()(
|
|
|
525
720
|
} else if (phase === 'structure' && structureLayer === 'elements') {
|
|
526
721
|
set({ tool: 'wall' })
|
|
527
722
|
} else if (phase === 'furnish') {
|
|
528
|
-
set({
|
|
529
|
-
tool: 'item',
|
|
530
|
-
catalogCategory: 'furniture',
|
|
531
|
-
selectedItem: getDefaultSelectedItemForCategory('furniture'),
|
|
532
|
-
})
|
|
723
|
+
set({ tool: 'item', catalogCategory: 'furniture' })
|
|
533
724
|
}
|
|
534
|
-
} else if (phase === 'furnish' && tool === 'item' && !get().selectedItem) {
|
|
535
|
-
const category = get().catalogCategory ?? 'furniture'
|
|
536
|
-
set({ selectedItem: getDefaultSelectedItemForCategory(category) })
|
|
537
725
|
}
|
|
538
726
|
} else if (mode === 'material-paint') {
|
|
539
727
|
get().primeMaterialPaintFromSelection()
|
|
@@ -545,6 +733,17 @@ const useEditor = create<EditorState>()(
|
|
|
545
733
|
},
|
|
546
734
|
tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
|
|
547
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
|
+
}),
|
|
548
747
|
structureLayer: DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer,
|
|
549
748
|
setStructureLayer: (layer) => {
|
|
550
749
|
const { mode } = get()
|
|
@@ -563,38 +762,47 @@ const useEditor = create<EditorState>()(
|
|
|
563
762
|
})
|
|
564
763
|
},
|
|
565
764
|
catalogCategory: DEFAULT_PERSISTED_EDITOR_UI_STATE.catalogCategory,
|
|
566
|
-
setCatalogCategory: (category) =>
|
|
567
|
-
set((state) => ({
|
|
568
|
-
catalogCategory: category,
|
|
569
|
-
selectedItem:
|
|
570
|
-
category !== null &&
|
|
571
|
-
state.phase === 'furnish' &&
|
|
572
|
-
state.mode === 'build' &&
|
|
573
|
-
state.tool === 'item'
|
|
574
|
-
? getDefaultSelectedItemForCategory(category)
|
|
575
|
-
: state.selectedItem,
|
|
576
|
-
})),
|
|
765
|
+
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
|
577
766
|
selectedItem: null,
|
|
578
767
|
setSelectedItem: (item) => set({ selectedItem: item }),
|
|
579
768
|
movingNode: null as
|
|
580
769
|
| ItemNode
|
|
581
770
|
| WindowNode
|
|
582
771
|
| DoorNode
|
|
583
|
-
|
|
|
772
|
+
| ElevatorNode
|
|
584
773
|
| CeilingNode
|
|
774
|
+
| ColumnNode
|
|
585
775
|
| SlabNode
|
|
586
776
|
| WallNode
|
|
777
|
+
| FenceNode
|
|
587
778
|
| RoofNode
|
|
588
779
|
| RoofSegmentNode
|
|
780
|
+
| SpawnNode
|
|
589
781
|
| StairNode
|
|
590
782
|
| StairSegmentNode
|
|
591
783
|
| BuildingNode
|
|
592
784
|
| null,
|
|
593
|
-
|
|
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 }),
|
|
594
800
|
movingWallEndpoint: null,
|
|
595
801
|
setMovingWallEndpoint: (value) => set({ movingWallEndpoint: value }),
|
|
596
802
|
movingFenceEndpoint: null,
|
|
597
803
|
setMovingFenceEndpoint: (value) => set({ movingFenceEndpoint: value }),
|
|
804
|
+
activeHandleDrag: null,
|
|
805
|
+
setActiveHandleDrag: (drag) => set({ activeHandleDrag: drag }),
|
|
598
806
|
curvingWall: null,
|
|
599
807
|
setCurvingWall: (wall) => set({ curvingWall: wall }),
|
|
600
808
|
curvingFence: null,
|
|
@@ -602,12 +810,17 @@ const useEditor = create<EditorState>()(
|
|
|
602
810
|
selectedMaterialTarget: null,
|
|
603
811
|
setSelectedMaterialTarget: (target) => set({ selectedMaterialTarget: target }),
|
|
604
812
|
activePaintMaterial: null,
|
|
605
|
-
|
|
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 }),
|
|
606
817
|
activePaintTarget: 'wall',
|
|
607
818
|
setActivePaintTarget: (target) =>
|
|
608
819
|
set((state) =>
|
|
609
820
|
state.activePaintTarget === target ? state : { activePaintTarget: target },
|
|
610
821
|
),
|
|
822
|
+
paintEraser: false,
|
|
823
|
+
setPaintEraser: (eraser) => set({ paintEraser: eraser }),
|
|
611
824
|
primeMaterialPaintFromSelection: () => {
|
|
612
825
|
const selectedId =
|
|
613
826
|
useViewer.getState().selection.selectedIds.length === 1
|
|
@@ -678,6 +891,14 @@ const useEditor = create<EditorState>()(
|
|
|
678
891
|
setSpaces: (spaces) => set({ spaces }),
|
|
679
892
|
editingHole: null,
|
|
680
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
|
+
),
|
|
681
902
|
isPreviewMode: false,
|
|
682
903
|
setPreviewMode: (preview) => {
|
|
683
904
|
if (preview) {
|
|
@@ -688,6 +909,13 @@ const useEditor = create<EditorState>()(
|
|
|
688
909
|
set({ isPreviewMode: false })
|
|
689
910
|
}
|
|
690
911
|
},
|
|
912
|
+
captureMode: { mode: 'idle' } as CaptureMode,
|
|
913
|
+
isCaptureMode: false,
|
|
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
|
+
},
|
|
691
919
|
viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode,
|
|
692
920
|
setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }),
|
|
693
921
|
splitOrientation: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.splitOrientation,
|
|
@@ -701,10 +929,20 @@ const useEditor = create<EditorState>()(
|
|
|
701
929
|
}),
|
|
702
930
|
isFloorplanHovered: false,
|
|
703
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
|
+
})),
|
|
704
940
|
floorplanSelectionTool: 'click' as FloorplanSelectionTool,
|
|
705
941
|
setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
|
|
706
942
|
gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
707
943
|
setGridSnapStep: (step) => set({ gridSnapStep: step }),
|
|
944
|
+
magneticSnap: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.magneticSnap,
|
|
945
|
+
setMagneticSnap: (enabled) => set({ magneticSnap: enabled }),
|
|
708
946
|
showReferenceFloor: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showReferenceFloor,
|
|
709
947
|
toggleReferenceFloor: () =>
|
|
710
948
|
set((state) => ({ showReferenceFloor: !state.showReferenceFloor })),
|
|
@@ -740,35 +978,47 @@ const useEditor = create<EditorState>()(
|
|
|
740
978
|
})
|
|
741
979
|
}
|
|
742
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
|
+
},
|
|
743
1007
|
activeSidebarPanel: DEFAULT_ACTIVE_SIDEBAR_PANEL,
|
|
744
1008
|
setActiveSidebarPanel: (id) => set({ activeSidebarPanel: id }),
|
|
745
|
-
mobilePanelSheetHeight: 0,
|
|
746
|
-
setMobilePanelSheetHeight: (height) => set({ mobilePanelSheetHeight: height }),
|
|
747
|
-
isCaptureMode: false,
|
|
748
|
-
setIsCaptureMode: (enabled) => set({ isCaptureMode: enabled }),
|
|
749
1009
|
floorplanPaneRatio: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.floorplanPaneRatio,
|
|
750
1010
|
setFloorplanPaneRatio: (ratio) =>
|
|
751
1011
|
set({ floorplanPaneRatio: normalizeFloorplanPaneRatio(ratio) }),
|
|
1012
|
+
mobilePanelSheetHeight: 0,
|
|
1013
|
+
setMobilePanelSheetHeight: (px) => set({ mobilePanelSheetHeight: Math.max(0, px) }),
|
|
752
1014
|
}),
|
|
753
1015
|
{
|
|
754
1016
|
name: 'pascal-editor-ui-preferences',
|
|
755
|
-
merge: (persistedState, currentState) => {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
return {
|
|
763
|
-
...mergedState,
|
|
764
|
-
selectedItem:
|
|
765
|
-
mergedState.phase === 'furnish' &&
|
|
766
|
-
mergedState.mode === 'build' &&
|
|
767
|
-
mergedState.tool === 'item'
|
|
768
|
-
? getDefaultSelectedItemForCategory(mergedState.catalogCategory ?? 'furniture')
|
|
769
|
-
: currentState.selectedItem,
|
|
770
|
-
}
|
|
771
|
-
},
|
|
1017
|
+
merge: (persistedState, currentState) => ({
|
|
1018
|
+
...currentState,
|
|
1019
|
+
...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
|
|
1020
|
+
...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
|
|
1021
|
+
}),
|
|
772
1022
|
partialize: (state) => ({
|
|
773
1023
|
phase: state.phase,
|
|
774
1024
|
mode: state.mode,
|
|
@@ -782,6 +1032,7 @@ const useEditor = create<EditorState>()(
|
|
|
782
1032
|
splitOrientation: state.splitOrientation,
|
|
783
1033
|
floorplanSelectionTool: state.floorplanSelectionTool,
|
|
784
1034
|
gridSnapStep: state.gridSnapStep,
|
|
1035
|
+
magneticSnap: state.magneticSnap,
|
|
785
1036
|
showReferenceFloor: state.showReferenceFloor,
|
|
786
1037
|
referenceFloorOffset: state.referenceFloorOffset,
|
|
787
1038
|
referenceFloorOpacity: state.referenceFloorOpacity,
|