@pascal-app/editor 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +62 -0
- package/src/components/editor/custom-camera-controls.tsx +387 -0
- package/src/components/editor/editor-layout-v2.tsx +220 -0
- package/src/components/editor/export-manager.tsx +78 -0
- package/src/components/editor/first-person-controls.tsx +249 -0
- package/src/components/editor/floating-action-menu.tsx +231 -0
- package/src/components/editor/floorplan-panel.tsx +9609 -0
- package/src/components/editor/grid.tsx +161 -0
- package/src/components/editor/index.tsx +928 -0
- package/src/components/editor/node-action-menu.tsx +66 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
- package/src/components/editor/selection-manager.tsx +897 -0
- package/src/components/editor/site-edge-labels.tsx +90 -0
- package/src/components/editor/thumbnail-generator.tsx +166 -0
- package/src/components/editor/wall-measurement-label.tsx +258 -0
- package/src/components/feedback-dialog.tsx +265 -0
- package/src/components/pascal-radio.tsx +280 -0
- package/src/components/preview-button.tsx +16 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
- package/src/components/systems/roof/roof-edit-system.tsx +69 -0
- package/src/components/systems/stair/stair-edit-system.tsx +69 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
- package/src/components/systems/zone/zone-system.tsx +87 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
- package/src/components/tools/door/door-math.ts +110 -0
- package/src/components/tools/door/door-tool.tsx +293 -0
- package/src/components/tools/door/move-door-tool.tsx +373 -0
- package/src/components/tools/item/item-tool.tsx +26 -0
- package/src/components/tools/item/move-tool.tsx +90 -0
- package/src/components/tools/item/placement-math.ts +85 -0
- package/src/components/tools/item/placement-strategies.ts +556 -0
- package/src/components/tools/item/placement-types.ts +117 -0
- package/src/components/tools/item/use-draft-node.ts +227 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
- package/src/components/tools/roof/move-roof-tool.tsx +288 -0
- package/src/components/tools/roof/roof-tool.tsx +318 -0
- package/src/components/tools/select/box-select-tool.tsx +626 -0
- package/src/components/tools/shared/cursor-sphere.tsx +119 -0
- package/src/components/tools/shared/polygon-editor.tsx +361 -0
- package/src/components/tools/site/site-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
- package/src/components/tools/slab/slab-tool.tsx +322 -0
- package/src/components/tools/stair/stair-defaults.ts +7 -0
- package/src/components/tools/stair/stair-tool.tsx +194 -0
- package/src/components/tools/tool-manager.tsx +120 -0
- package/src/components/tools/wall/wall-drafting.ts +140 -0
- package/src/components/tools/wall/wall-tool.tsx +210 -0
- package/src/components/tools/window/move-window-tool.tsx +410 -0
- package/src/components/tools/window/window-math.ts +117 -0
- package/src/components/tools/window/window-tool.tsx +303 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
- package/src/components/tools/zone/zone-tool.tsx +364 -0
- package/src/components/ui/action-menu/action-button.tsx +59 -0
- package/src/components/ui/action-menu/camera-actions.tsx +74 -0
- package/src/components/ui/action-menu/control-modes.tsx +240 -0
- package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
- package/src/components/ui/action-menu/index.tsx +152 -0
- package/src/components/ui/action-menu/structure-tools.tsx +100 -0
- package/src/components/ui/action-menu/view-toggles.tsx +397 -0
- package/src/components/ui/command-palette/editor-commands.tsx +396 -0
- package/src/components/ui/command-palette/index.tsx +730 -0
- package/src/components/ui/controls/action-button.tsx +33 -0
- package/src/components/ui/controls/material-picker.tsx +194 -0
- package/src/components/ui/controls/metric-control.tsx +262 -0
- package/src/components/ui/controls/panel-section.tsx +65 -0
- package/src/components/ui/controls/segmented-control.tsx +45 -0
- package/src/components/ui/controls/slider-control.tsx +245 -0
- package/src/components/ui/controls/toggle-control.tsx +38 -0
- package/src/components/ui/floating-level-selector.tsx +355 -0
- package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
- package/src/components/ui/helpers/helper-manager.tsx +33 -0
- package/src/components/ui/helpers/item-helper.tsx +40 -0
- package/src/components/ui/helpers/roof-helper.tsx +16 -0
- package/src/components/ui/helpers/slab-helper.tsx +20 -0
- package/src/components/ui/helpers/wall-helper.tsx +20 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
- package/src/components/ui/panels/ceiling-panel.tsx +230 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
- package/src/components/ui/panels/door-panel.tsx +600 -0
- package/src/components/ui/panels/item-panel.tsx +306 -0
- package/src/components/ui/panels/panel-manager.tsx +59 -0
- package/src/components/ui/panels/panel-wrapper.tsx +80 -0
- package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
- package/src/components/ui/panels/reference-panel.tsx +177 -0
- package/src/components/ui/panels/roof-panel.tsx +262 -0
- package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
- package/src/components/ui/panels/slab-panel.tsx +228 -0
- package/src/components/ui/panels/stair-panel.tsx +304 -0
- package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
- package/src/components/ui/panels/wall-panel.tsx +123 -0
- package/src/components/ui/panels/window-panel.tsx +441 -0
- package/src/components/ui/primitives/button.tsx +69 -0
- package/src/components/ui/primitives/card.tsx +75 -0
- package/src/components/ui/primitives/color-dot.tsx +61 -0
- package/src/components/ui/primitives/context-menu.tsx +227 -0
- package/src/components/ui/primitives/dialog.tsx +129 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
- package/src/components/ui/primitives/error-boundary.tsx +52 -0
- package/src/components/ui/primitives/input.tsx +21 -0
- package/src/components/ui/primitives/number-input.tsx +187 -0
- package/src/components/ui/primitives/opacity-control.tsx +79 -0
- package/src/components/ui/primitives/popover.tsx +42 -0
- package/src/components/ui/primitives/separator.tsx +28 -0
- package/src/components/ui/primitives/sheet.tsx +130 -0
- package/src/components/ui/primitives/shortcut-token.tsx +64 -0
- package/src/components/ui/primitives/sidebar.tsx +855 -0
- package/src/components/ui/primitives/skeleton.tsx +13 -0
- package/src/components/ui/primitives/slider.tsx +58 -0
- package/src/components/ui/primitives/switch.tsx +29 -0
- package/src/components/ui/primitives/tooltip.tsx +57 -0
- package/src/components/ui/scene-loader.tsx +40 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
- package/src/components/ui/sidebar/icon-rail.tsx +147 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
- package/src/components/ui/sidebar/tab-bar.tsx +39 -0
- package/src/components/ui/slider-demo.tsx +36 -0
- package/src/components/ui/slider.tsx +81 -0
- package/src/components/ui/viewer-toolbar.tsx +342 -0
- package/src/components/viewer-overlay.tsx +499 -0
- package/src/components/viewer-zone-system.tsx +48 -0
- package/src/contexts/presets-context.tsx +121 -0
- package/src/hooks/use-auto-save.ts +194 -0
- package/src/hooks/use-contextual-tools.ts +52 -0
- package/src/hooks/use-grid-events.ts +106 -0
- package/src/hooks/use-keyboard.ts +214 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-reduced-motion.ts +20 -0
- package/src/index.tsx +33 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/level-selection.ts +31 -0
- package/src/lib/scene.ts +394 -0
- package/src/lib/sfx/index.ts +2 -0
- package/src/lib/sfx-bus.ts +49 -0
- package/src/lib/sfx-player.ts +60 -0
- package/src/lib/utils.ts +43 -0
- package/src/store/use-audio.tsx +45 -0
- package/src/store/use-command-registry.ts +36 -0
- package/src/store/use-editor.tsx +522 -0
- package/src/store/use-palette-view-registry.ts +45 -0
- package/src/store/use-upload.ts +90 -0
- package/src/three-types.ts +3 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AssetInput } from '@pascal-app/core'
|
|
4
|
+
import {
|
|
5
|
+
type BuildingNode,
|
|
6
|
+
type DoorNode,
|
|
7
|
+
type ItemNode,
|
|
8
|
+
type LevelNode,
|
|
9
|
+
type RoofNode,
|
|
10
|
+
type RoofSegmentNode,
|
|
11
|
+
type Space,
|
|
12
|
+
type StairNode,
|
|
13
|
+
type StairSegmentNode,
|
|
14
|
+
useScene,
|
|
15
|
+
type WindowNode,
|
|
16
|
+
} from '@pascal-app/core'
|
|
17
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
18
|
+
import { create } from 'zustand'
|
|
19
|
+
import { persist } from 'zustand/middleware'
|
|
20
|
+
|
|
21
|
+
const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'site'
|
|
22
|
+
const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
|
|
23
|
+
const MIN_FLOORPLAN_PANE_RATIO = 0.15
|
|
24
|
+
const MAX_FLOORPLAN_PANE_RATIO = 0.85
|
|
25
|
+
|
|
26
|
+
export type ViewMode = '3d' | '2d' | 'split'
|
|
27
|
+
export type SplitOrientation = 'horizontal' | 'vertical'
|
|
28
|
+
|
|
29
|
+
export type Phase = 'site' | 'structure' | 'furnish'
|
|
30
|
+
|
|
31
|
+
export type Mode = 'select' | 'edit' | 'delete' | 'build'
|
|
32
|
+
|
|
33
|
+
// Structure mode tools (building elements)
|
|
34
|
+
export type StructureTool =
|
|
35
|
+
| 'wall'
|
|
36
|
+
| 'room'
|
|
37
|
+
| 'custom-room'
|
|
38
|
+
| 'slab'
|
|
39
|
+
| 'ceiling'
|
|
40
|
+
| 'roof'
|
|
41
|
+
| 'column'
|
|
42
|
+
| 'stair'
|
|
43
|
+
| 'item'
|
|
44
|
+
| 'zone'
|
|
45
|
+
| 'window'
|
|
46
|
+
| 'door'
|
|
47
|
+
|
|
48
|
+
// Furnish mode tools (items and decoration)
|
|
49
|
+
export type FurnishTool = 'item'
|
|
50
|
+
|
|
51
|
+
// Site mode tools
|
|
52
|
+
export type SiteTool = 'property-line'
|
|
53
|
+
|
|
54
|
+
// Catalog categories for furnish mode items
|
|
55
|
+
export type CatalogCategory =
|
|
56
|
+
| 'furniture'
|
|
57
|
+
| 'appliance'
|
|
58
|
+
| 'bathroom'
|
|
59
|
+
| 'kitchen'
|
|
60
|
+
| 'outdoor'
|
|
61
|
+
| 'window'
|
|
62
|
+
| 'door'
|
|
63
|
+
|
|
64
|
+
export type StructureLayer = 'zones' | 'elements'
|
|
65
|
+
|
|
66
|
+
export type FloorplanSelectionTool = 'click' | 'marquee'
|
|
67
|
+
|
|
68
|
+
// Combined tool type
|
|
69
|
+
export type Tool = SiteTool | StructureTool | FurnishTool
|
|
70
|
+
|
|
71
|
+
type EditorState = {
|
|
72
|
+
phase: Phase
|
|
73
|
+
setPhase: (phase: Phase) => void
|
|
74
|
+
mode: Mode
|
|
75
|
+
setMode: (mode: Mode) => void
|
|
76
|
+
tool: Tool | null
|
|
77
|
+
setTool: (tool: Tool | null) => void
|
|
78
|
+
structureLayer: StructureLayer
|
|
79
|
+
setStructureLayer: (layer: StructureLayer) => void
|
|
80
|
+
catalogCategory: CatalogCategory | null
|
|
81
|
+
setCatalogCategory: (category: CatalogCategory | null) => void
|
|
82
|
+
selectedItem: AssetInput | null
|
|
83
|
+
setSelectedItem: (item: AssetInput) => void
|
|
84
|
+
movingNode:
|
|
85
|
+
| ItemNode
|
|
86
|
+
| WindowNode
|
|
87
|
+
| DoorNode
|
|
88
|
+
| RoofNode
|
|
89
|
+
| RoofSegmentNode
|
|
90
|
+
| StairNode
|
|
91
|
+
| StairSegmentNode
|
|
92
|
+
| null
|
|
93
|
+
setMovingNode: (
|
|
94
|
+
node:
|
|
95
|
+
| ItemNode
|
|
96
|
+
| WindowNode
|
|
97
|
+
| DoorNode
|
|
98
|
+
| RoofNode
|
|
99
|
+
| RoofSegmentNode
|
|
100
|
+
| StairNode
|
|
101
|
+
| StairSegmentNode
|
|
102
|
+
| null,
|
|
103
|
+
) => void
|
|
104
|
+
selectedReferenceId: string | null
|
|
105
|
+
setSelectedReferenceId: (id: string | null) => void
|
|
106
|
+
// Space detection for cutaway mode
|
|
107
|
+
spaces: Record<string, Space>
|
|
108
|
+
setSpaces: (spaces: Record<string, Space>) => void
|
|
109
|
+
// Generic hole editing (works for slabs, ceilings, and any future polygon nodes)
|
|
110
|
+
editingHole: { nodeId: string; holeIndex: number } | null
|
|
111
|
+
setEditingHole: (hole: { nodeId: string; holeIndex: number } | null) => void
|
|
112
|
+
// Preview mode (viewer-like experience inside the editor)
|
|
113
|
+
isPreviewMode: boolean
|
|
114
|
+
setPreviewMode: (preview: boolean) => void
|
|
115
|
+
// View mode (3D only, 2D only, or split 2D+3D)
|
|
116
|
+
viewMode: ViewMode
|
|
117
|
+
setViewMode: (mode: ViewMode) => void
|
|
118
|
+
splitOrientation: SplitOrientation
|
|
119
|
+
setSplitOrientation: (orientation: SplitOrientation) => void
|
|
120
|
+
// Toggleable 2D floorplan overlay (backward compat — derived from viewMode)
|
|
121
|
+
isFloorplanOpen: boolean
|
|
122
|
+
setFloorplanOpen: (open: boolean) => void
|
|
123
|
+
toggleFloorplanOpen: () => void
|
|
124
|
+
isFloorplanHovered: boolean
|
|
125
|
+
setFloorplanHovered: (hovered: boolean) => void
|
|
126
|
+
floorplanSelectionTool: FloorplanSelectionTool
|
|
127
|
+
setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
|
|
128
|
+
// First-person walkthrough mode (street view)
|
|
129
|
+
isFirstPersonMode: boolean
|
|
130
|
+
_viewModeBeforeFirstPerson: ViewMode | null
|
|
131
|
+
setFirstPersonMode: (enabled: boolean) => void
|
|
132
|
+
// Development-only camera debug flag for inspecting underside geometry
|
|
133
|
+
allowUndergroundCamera: boolean
|
|
134
|
+
setAllowUndergroundCamera: (enabled: boolean) => void
|
|
135
|
+
activeSidebarPanel: string
|
|
136
|
+
setActiveSidebarPanel: (id: string) => void
|
|
137
|
+
floorplanPaneRatio: number
|
|
138
|
+
setFloorplanPaneRatio: (ratio: number) => void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type PersistedEditorUiState = Pick<
|
|
142
|
+
EditorState,
|
|
143
|
+
'phase' | 'mode' | 'tool' | 'structureLayer' | 'catalogCategory' | 'isFloorplanOpen' | 'viewMode'
|
|
144
|
+
>
|
|
145
|
+
|
|
146
|
+
type PersistedEditorLayoutState = Pick<
|
|
147
|
+
EditorState,
|
|
148
|
+
'activeSidebarPanel' | 'floorplanPaneRatio' | 'splitOrientation' | 'floorplanSelectionTool'
|
|
149
|
+
>
|
|
150
|
+
type PersistedEditorState = PersistedEditorUiState & PersistedEditorLayoutState
|
|
151
|
+
|
|
152
|
+
export const DEFAULT_PERSISTED_EDITOR_UI_STATE: PersistedEditorUiState = {
|
|
153
|
+
phase: 'site',
|
|
154
|
+
mode: 'select',
|
|
155
|
+
tool: null,
|
|
156
|
+
structureLayer: 'elements',
|
|
157
|
+
catalogCategory: null,
|
|
158
|
+
isFloorplanOpen: false,
|
|
159
|
+
viewMode: '3d',
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState = {
|
|
163
|
+
activeSidebarPanel: DEFAULT_ACTIVE_SIDEBAR_PANEL,
|
|
164
|
+
floorplanPaneRatio: DEFAULT_FLOORPLAN_PANE_RATIO,
|
|
165
|
+
splitOrientation: 'horizontal',
|
|
166
|
+
floorplanSelectionTool: 'click',
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
|
|
170
|
+
if (phase === 'site') {
|
|
171
|
+
return 'select'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return mode === 'build' || mode === 'delete' ? mode : 'select'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeFloorplanPaneRatio(value: unknown): number {
|
|
178
|
+
if (!(typeof value === 'number' && Number.isFinite(value))) {
|
|
179
|
+
return DEFAULT_FLOORPLAN_PANE_RATIO
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return Math.min(MAX_FLOORPLAN_PANE_RATIO, Math.max(MIN_FLOORPLAN_PANE_RATIO, value))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function normalizePersistedEditorUiState(
|
|
186
|
+
state: Partial<PersistedEditorUiState> | null | undefined,
|
|
187
|
+
): PersistedEditorUiState {
|
|
188
|
+
const phase = state?.phase === 'structure' || state?.phase === 'furnish' ? state.phase : 'site'
|
|
189
|
+
const mode = normalizeModeForPhase(phase, state?.mode)
|
|
190
|
+
|
|
191
|
+
// Migrate old isFloorplanOpen to viewMode
|
|
192
|
+
let viewMode: ViewMode = '3d'
|
|
193
|
+
if (state?.viewMode === '2d' || state?.viewMode === '3d' || state?.viewMode === 'split') {
|
|
194
|
+
viewMode = state.viewMode
|
|
195
|
+
} else if (state?.isFloorplanOpen) {
|
|
196
|
+
viewMode = 'split'
|
|
197
|
+
}
|
|
198
|
+
const isFloorplanOpen = viewMode !== '3d'
|
|
199
|
+
|
|
200
|
+
if (phase === 'site') {
|
|
201
|
+
return {
|
|
202
|
+
...DEFAULT_PERSISTED_EDITOR_UI_STATE,
|
|
203
|
+
phase,
|
|
204
|
+
mode,
|
|
205
|
+
viewMode,
|
|
206
|
+
isFloorplanOpen,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (phase === 'furnish') {
|
|
211
|
+
return {
|
|
212
|
+
phase,
|
|
213
|
+
mode,
|
|
214
|
+
tool: mode === 'build' ? 'item' : null,
|
|
215
|
+
structureLayer: 'elements',
|
|
216
|
+
catalogCategory: mode === 'build' ? (state?.catalogCategory ?? 'furniture') : null,
|
|
217
|
+
viewMode,
|
|
218
|
+
isFloorplanOpen,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const structureLayer = state?.structureLayer === 'zones' ? 'zones' : 'elements'
|
|
223
|
+
|
|
224
|
+
if (mode !== 'build') {
|
|
225
|
+
return {
|
|
226
|
+
phase,
|
|
227
|
+
mode,
|
|
228
|
+
tool: null,
|
|
229
|
+
structureLayer,
|
|
230
|
+
catalogCategory: null,
|
|
231
|
+
viewMode,
|
|
232
|
+
isFloorplanOpen,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (structureLayer === 'zones') {
|
|
237
|
+
return {
|
|
238
|
+
phase,
|
|
239
|
+
mode,
|
|
240
|
+
tool: 'zone',
|
|
241
|
+
structureLayer,
|
|
242
|
+
catalogCategory: null,
|
|
243
|
+
viewMode,
|
|
244
|
+
isFloorplanOpen,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
phase,
|
|
250
|
+
mode,
|
|
251
|
+
tool:
|
|
252
|
+
state?.tool && state.tool !== 'property-line' && state.tool !== 'zone' ? state.tool : 'wall',
|
|
253
|
+
structureLayer,
|
|
254
|
+
catalogCategory: state?.tool === 'item' ? (state.catalogCategory ?? null) : null,
|
|
255
|
+
viewMode,
|
|
256
|
+
isFloorplanOpen,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizePersistedEditorLayoutState(
|
|
261
|
+
state: Partial<PersistedEditorLayoutState> | null | undefined,
|
|
262
|
+
): PersistedEditorLayoutState {
|
|
263
|
+
return {
|
|
264
|
+
activeSidebarPanel:
|
|
265
|
+
typeof state?.activeSidebarPanel === 'string' && state.activeSidebarPanel.trim()
|
|
266
|
+
? state.activeSidebarPanel
|
|
267
|
+
: DEFAULT_ACTIVE_SIDEBAR_PANEL,
|
|
268
|
+
floorplanPaneRatio: normalizeFloorplanPaneRatio(state?.floorplanPaneRatio),
|
|
269
|
+
splitOrientation: state?.splitOrientation === 'vertical' ? 'vertical' : 'horizontal',
|
|
270
|
+
floorplanSelectionTool: state?.floorplanSelectionTool === 'marquee' ? 'marquee' : 'click',
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function hasCustomPersistedEditorUiState(
|
|
275
|
+
state: Partial<PersistedEditorUiState> | null | undefined,
|
|
276
|
+
): boolean {
|
|
277
|
+
const normalizedState = normalizePersistedEditorUiState(state)
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
normalizedState.phase !== DEFAULT_PERSISTED_EDITOR_UI_STATE.phase ||
|
|
281
|
+
normalizedState.mode !== DEFAULT_PERSISTED_EDITOR_UI_STATE.mode ||
|
|
282
|
+
normalizedState.tool !== DEFAULT_PERSISTED_EDITOR_UI_STATE.tool ||
|
|
283
|
+
normalizedState.structureLayer !== DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer ||
|
|
284
|
+
normalizedState.catalogCategory !== DEFAULT_PERSISTED_EDITOR_UI_STATE.catalogCategory ||
|
|
285
|
+
normalizedState.isFloorplanOpen !== DEFAULT_PERSISTED_EDITOR_UI_STATE.isFloorplanOpen ||
|
|
286
|
+
normalizedState.viewMode !== DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Selects the first building and level 0 in the scene.
|
|
292
|
+
* Safe to call any time — no-ops if already selected or scene is empty.
|
|
293
|
+
*/
|
|
294
|
+
export function selectDefaultBuildingAndLevel() {
|
|
295
|
+
const viewer = useViewer.getState()
|
|
296
|
+
const scene = useScene.getState()
|
|
297
|
+
|
|
298
|
+
let buildingId = viewer.selection.buildingId
|
|
299
|
+
|
|
300
|
+
// If no building selected, find the first one from site's children
|
|
301
|
+
if (!buildingId) {
|
|
302
|
+
const siteNode = scene.rootNodeIds[0] ? scene.nodes[scene.rootNodeIds[0]] : null
|
|
303
|
+
if (siteNode?.type === 'site') {
|
|
304
|
+
const firstBuilding = siteNode.children
|
|
305
|
+
.map((child) => (typeof child === 'string' ? scene.nodes[child] : child))
|
|
306
|
+
.find((node) => node?.type === 'building')
|
|
307
|
+
if (firstBuilding) {
|
|
308
|
+
buildingId = firstBuilding.id as BuildingNode['id']
|
|
309
|
+
viewer.setSelection({ buildingId })
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// If no level selected, find level 0 in the building
|
|
315
|
+
if (buildingId && !viewer.selection.levelId) {
|
|
316
|
+
const buildingNode = scene.nodes[buildingId] as BuildingNode
|
|
317
|
+
const level0Id = buildingNode.children.find((childId) => {
|
|
318
|
+
const levelNode = scene.nodes[childId] as LevelNode
|
|
319
|
+
return levelNode?.type === 'level' && levelNode.level === 0
|
|
320
|
+
})
|
|
321
|
+
if (level0Id) {
|
|
322
|
+
viewer.setSelection({ levelId: level0Id as LevelNode['id'] })
|
|
323
|
+
} else if (buildingNode.children[0]) {
|
|
324
|
+
// Fallback to first level if level 0 doesn't exist
|
|
325
|
+
viewer.setSelection({ levelId: buildingNode.children[0] as LevelNode['id'] })
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const useEditor = create<EditorState>()(
|
|
331
|
+
persist(
|
|
332
|
+
(set, get) => ({
|
|
333
|
+
phase: DEFAULT_PERSISTED_EDITOR_UI_STATE.phase,
|
|
334
|
+
setPhase: (phase) => {
|
|
335
|
+
const currentPhase = get().phase
|
|
336
|
+
if (currentPhase === phase) return
|
|
337
|
+
|
|
338
|
+
set({ phase })
|
|
339
|
+
|
|
340
|
+
const { mode, structureLayer } = get()
|
|
341
|
+
|
|
342
|
+
if (mode === 'build') {
|
|
343
|
+
// Stay in build mode, select the first tool for the new phase
|
|
344
|
+
if (phase === 'site') {
|
|
345
|
+
set({ tool: 'property-line', catalogCategory: null })
|
|
346
|
+
} else if (phase === 'structure' && structureLayer === 'zones') {
|
|
347
|
+
set({ tool: 'zone', catalogCategory: null })
|
|
348
|
+
} else if (phase === 'structure') {
|
|
349
|
+
set({ tool: 'wall', catalogCategory: null })
|
|
350
|
+
} else if (phase === 'furnish') {
|
|
351
|
+
set({ tool: 'item', catalogCategory: 'furniture' })
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
// Reset to select mode and clear tool/catalog when switching phases
|
|
355
|
+
set({ mode: 'select', tool: null, catalogCategory: null })
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const viewer = useViewer.getState()
|
|
359
|
+
|
|
360
|
+
switch (phase) {
|
|
361
|
+
case 'site':
|
|
362
|
+
// In Site mode, we zoom out and deselect specific levels/buildings
|
|
363
|
+
viewer.resetSelection()
|
|
364
|
+
break
|
|
365
|
+
|
|
366
|
+
case 'structure':
|
|
367
|
+
selectDefaultBuildingAndLevel()
|
|
368
|
+
break
|
|
369
|
+
|
|
370
|
+
case 'furnish':
|
|
371
|
+
selectDefaultBuildingAndLevel()
|
|
372
|
+
// Furnish mode only supports elements layer, not zones
|
|
373
|
+
set({ structureLayer: 'elements' })
|
|
374
|
+
break
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
mode: DEFAULT_PERSISTED_EDITOR_UI_STATE.mode,
|
|
378
|
+
setMode: (mode) => {
|
|
379
|
+
set({ mode })
|
|
380
|
+
|
|
381
|
+
const { phase, structureLayer, tool } = get()
|
|
382
|
+
|
|
383
|
+
if (mode === 'build') {
|
|
384
|
+
// Ensure a tool is selected in build mode
|
|
385
|
+
if (!tool) {
|
|
386
|
+
if (phase === 'structure' && structureLayer === 'zones') {
|
|
387
|
+
set({ tool: 'zone' })
|
|
388
|
+
} else if (phase === 'structure' && structureLayer === 'elements') {
|
|
389
|
+
set({ tool: 'wall' })
|
|
390
|
+
} else if (phase === 'furnish') {
|
|
391
|
+
set({ tool: 'item', catalogCategory: 'furniture' })
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// When leaving build mode, clear tool
|
|
396
|
+
else if (tool) {
|
|
397
|
+
set({ tool: null })
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
|
|
401
|
+
setTool: (tool) => set({ tool }),
|
|
402
|
+
structureLayer: DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer,
|
|
403
|
+
setStructureLayer: (layer) => {
|
|
404
|
+
const { mode } = get()
|
|
405
|
+
|
|
406
|
+
if (mode === 'build') {
|
|
407
|
+
const tool = layer === 'zones' ? 'zone' : 'wall'
|
|
408
|
+
set({ structureLayer: layer, tool })
|
|
409
|
+
} else {
|
|
410
|
+
set({ structureLayer: layer, mode: 'select', tool: null })
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const viewer = useViewer.getState()
|
|
414
|
+
viewer.setSelection({
|
|
415
|
+
selectedIds: [],
|
|
416
|
+
zoneId: null,
|
|
417
|
+
})
|
|
418
|
+
},
|
|
419
|
+
catalogCategory: DEFAULT_PERSISTED_EDITOR_UI_STATE.catalogCategory,
|
|
420
|
+
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
|
421
|
+
selectedItem: null,
|
|
422
|
+
setSelectedItem: (item) => set({ selectedItem: item }),
|
|
423
|
+
movingNode: null as
|
|
424
|
+
| ItemNode
|
|
425
|
+
| WindowNode
|
|
426
|
+
| DoorNode
|
|
427
|
+
| RoofNode
|
|
428
|
+
| RoofSegmentNode
|
|
429
|
+
| StairNode
|
|
430
|
+
| StairSegmentNode
|
|
431
|
+
| null,
|
|
432
|
+
setMovingNode: (node) => set({ movingNode: node }),
|
|
433
|
+
selectedReferenceId: null,
|
|
434
|
+
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
|
|
435
|
+
spaces: {},
|
|
436
|
+
setSpaces: (spaces) => set({ spaces }),
|
|
437
|
+
editingHole: null,
|
|
438
|
+
setEditingHole: (hole) => set({ editingHole: hole }),
|
|
439
|
+
isPreviewMode: false,
|
|
440
|
+
setPreviewMode: (preview) => {
|
|
441
|
+
if (preview) {
|
|
442
|
+
set({ isPreviewMode: true, mode: 'select', tool: null, catalogCategory: null })
|
|
443
|
+
// Clear zone/item selection for clean viewer drill-down hierarchy
|
|
444
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
445
|
+
} else {
|
|
446
|
+
set({ isPreviewMode: false })
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode,
|
|
450
|
+
setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }),
|
|
451
|
+
splitOrientation: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.splitOrientation,
|
|
452
|
+
setSplitOrientation: (orientation) => set({ splitOrientation: orientation }),
|
|
453
|
+
isFloorplanOpen: DEFAULT_PERSISTED_EDITOR_UI_STATE.isFloorplanOpen,
|
|
454
|
+
setFloorplanOpen: (open) => set({ isFloorplanOpen: open, viewMode: open ? 'split' : '3d' }),
|
|
455
|
+
toggleFloorplanOpen: () =>
|
|
456
|
+
set((state) => {
|
|
457
|
+
const open = !state.isFloorplanOpen
|
|
458
|
+
return { isFloorplanOpen: open, viewMode: open ? 'split' : '3d' }
|
|
459
|
+
}),
|
|
460
|
+
isFloorplanHovered: false,
|
|
461
|
+
setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
|
|
462
|
+
floorplanSelectionTool: 'click' as FloorplanSelectionTool,
|
|
463
|
+
setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
|
|
464
|
+
allowUndergroundCamera: false,
|
|
465
|
+
setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
|
|
466
|
+
isFirstPersonMode: false,
|
|
467
|
+
_viewModeBeforeFirstPerson: null as ViewMode | null,
|
|
468
|
+
setFirstPersonMode: (enabled) => {
|
|
469
|
+
if (enabled) {
|
|
470
|
+
const currentViewMode = get().viewMode
|
|
471
|
+
useViewer.getState().setCameraMode('perspective')
|
|
472
|
+
useViewer.getState().setWallMode('up')
|
|
473
|
+
set({
|
|
474
|
+
isFirstPersonMode: true,
|
|
475
|
+
_viewModeBeforeFirstPerson: currentViewMode,
|
|
476
|
+
viewMode: '3d',
|
|
477
|
+
isFloorplanOpen: false,
|
|
478
|
+
mode: 'select',
|
|
479
|
+
tool: null,
|
|
480
|
+
catalogCategory: null,
|
|
481
|
+
})
|
|
482
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
483
|
+
} else {
|
|
484
|
+
const prevMode = get()._viewModeBeforeFirstPerson
|
|
485
|
+
set({
|
|
486
|
+
isFirstPersonMode: false,
|
|
487
|
+
_viewModeBeforeFirstPerson: null,
|
|
488
|
+
...(prevMode ? { viewMode: prevMode, isFloorplanOpen: prevMode !== '3d' } : {}),
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
activeSidebarPanel: DEFAULT_ACTIVE_SIDEBAR_PANEL,
|
|
493
|
+
setActiveSidebarPanel: (id) => set({ activeSidebarPanel: id }),
|
|
494
|
+
floorplanPaneRatio: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.floorplanPaneRatio,
|
|
495
|
+
setFloorplanPaneRatio: (ratio) =>
|
|
496
|
+
set({ floorplanPaneRatio: normalizeFloorplanPaneRatio(ratio) }),
|
|
497
|
+
}),
|
|
498
|
+
{
|
|
499
|
+
name: 'pascal-editor-ui-preferences',
|
|
500
|
+
merge: (persistedState, currentState) => ({
|
|
501
|
+
...currentState,
|
|
502
|
+
...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
|
|
503
|
+
...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
|
|
504
|
+
}),
|
|
505
|
+
partialize: (state) => ({
|
|
506
|
+
phase: state.phase,
|
|
507
|
+
mode: state.mode,
|
|
508
|
+
tool: state.tool,
|
|
509
|
+
structureLayer: state.structureLayer,
|
|
510
|
+
catalogCategory: state.catalogCategory,
|
|
511
|
+
isFloorplanOpen: state.isFloorplanOpen,
|
|
512
|
+
viewMode: state.viewMode,
|
|
513
|
+
activeSidebarPanel: state.activeSidebarPanel,
|
|
514
|
+
floorplanPaneRatio: state.floorplanPaneRatio,
|
|
515
|
+
splitOrientation: state.splitOrientation,
|
|
516
|
+
floorplanSelectionTool: state.floorplanSelectionTool,
|
|
517
|
+
}),
|
|
518
|
+
},
|
|
519
|
+
),
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
export default useEditor
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ComponentType } from 'react'
|
|
2
|
+
import { create } from 'zustand'
|
|
3
|
+
|
|
4
|
+
export type PaletteViewProps = {
|
|
5
|
+
onClose: () => void
|
|
6
|
+
onBack: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type PaletteView = {
|
|
10
|
+
/** Unique key — matches a page name or a mode name. */
|
|
11
|
+
key: string
|
|
12
|
+
/**
|
|
13
|
+
* `'page'` — renders inside the cmdk Command shell (list area only).
|
|
14
|
+
* Filtering and keyboard navigation still work.
|
|
15
|
+
*
|
|
16
|
+
* `'mode'` — replaces the entire cmdk shell inside the Dialog.
|
|
17
|
+
* Used for full-screen states like ai-executing or ai-review.
|
|
18
|
+
*/
|
|
19
|
+
type: 'page' | 'mode'
|
|
20
|
+
/** Human-readable label shown as the breadcrumb for page views. */
|
|
21
|
+
label?: string
|
|
22
|
+
Component: ComponentType<PaletteViewProps>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PaletteViewRegistryStore {
|
|
26
|
+
views: Map<string, PaletteView>
|
|
27
|
+
register: (view: PaletteView) => () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const usePaletteViewRegistry = create<PaletteViewRegistryStore>((set) => ({
|
|
31
|
+
views: new Map(),
|
|
32
|
+
register: (view) => {
|
|
33
|
+
set((s) => {
|
|
34
|
+
const next = new Map(s.views)
|
|
35
|
+
next.set(view.key, view)
|
|
36
|
+
return { views: next }
|
|
37
|
+
})
|
|
38
|
+
return () =>
|
|
39
|
+
set((s) => {
|
|
40
|
+
const next = new Map(s.views)
|
|
41
|
+
next.delete(view.key)
|
|
42
|
+
return { views: next }
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
}))
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
export type UploadStatus = 'preparing' | 'uploading' | 'confirming' | 'done' | 'error'
|
|
4
|
+
|
|
5
|
+
export interface UploadEntry {
|
|
6
|
+
status: UploadStatus
|
|
7
|
+
assetType: 'scan' | 'guide'
|
|
8
|
+
fileName: string
|
|
9
|
+
progress: number // 0-100
|
|
10
|
+
error: string | null
|
|
11
|
+
resultUrl: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type UploadHandler = (
|
|
15
|
+
projectId: string,
|
|
16
|
+
levelId: string,
|
|
17
|
+
file: File,
|
|
18
|
+
type: 'scan' | 'guide',
|
|
19
|
+
) => void
|
|
20
|
+
|
|
21
|
+
interface UploadState {
|
|
22
|
+
uploads: Record<string, UploadEntry>
|
|
23
|
+
uploadHandler: UploadHandler | null
|
|
24
|
+
registerUploadHandler: (handler: UploadHandler) => void
|
|
25
|
+
unregisterUploadHandler: () => void
|
|
26
|
+
startUpload: (levelId: string, assetType: 'scan' | 'guide', fileName: string) => void
|
|
27
|
+
setProgress: (levelId: string, progress: number) => void
|
|
28
|
+
setStatus: (levelId: string, status: UploadStatus) => void
|
|
29
|
+
setError: (levelId: string, error: string) => void
|
|
30
|
+
setResult: (levelId: string, url: string) => void
|
|
31
|
+
clearUpload: (levelId: string) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const useUploadStore = create<UploadState>((set) => ({
|
|
35
|
+
uploads: {},
|
|
36
|
+
uploadHandler: null,
|
|
37
|
+
registerUploadHandler: (handler) => set({ uploadHandler: handler }),
|
|
38
|
+
unregisterUploadHandler: () => set({ uploadHandler: null }),
|
|
39
|
+
|
|
40
|
+
startUpload: (levelId, assetType, fileName) =>
|
|
41
|
+
set((s) => ({
|
|
42
|
+
uploads: {
|
|
43
|
+
...s.uploads,
|
|
44
|
+
[levelId]: {
|
|
45
|
+
status: 'preparing',
|
|
46
|
+
assetType,
|
|
47
|
+
fileName,
|
|
48
|
+
progress: 0,
|
|
49
|
+
error: null,
|
|
50
|
+
resultUrl: null,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
})),
|
|
54
|
+
|
|
55
|
+
setProgress: (levelId, progress) =>
|
|
56
|
+
set((s) => {
|
|
57
|
+
const entry = s.uploads[levelId]
|
|
58
|
+
if (!entry) return s
|
|
59
|
+
return { uploads: { ...s.uploads, [levelId]: { ...entry, progress } } }
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
setStatus: (levelId, status) =>
|
|
63
|
+
set((s) => {
|
|
64
|
+
const entry = s.uploads[levelId]
|
|
65
|
+
if (!entry) return s
|
|
66
|
+
return { uploads: { ...s.uploads, [levelId]: { ...entry, status } } }
|
|
67
|
+
}),
|
|
68
|
+
|
|
69
|
+
setError: (levelId, error) =>
|
|
70
|
+
set((s) => {
|
|
71
|
+
const entry = s.uploads[levelId]
|
|
72
|
+
if (!entry) return s
|
|
73
|
+
return { uploads: { ...s.uploads, [levelId]: { ...entry, status: 'error' as const, error } } }
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
setResult: (levelId, url) =>
|
|
77
|
+
set((s) => {
|
|
78
|
+
const entry = s.uploads[levelId]
|
|
79
|
+
if (!entry) return s
|
|
80
|
+
return {
|
|
81
|
+
uploads: { ...s.uploads, [levelId]: { ...entry, status: 'done' as const, resultUrl: url } },
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
|
|
85
|
+
clearUpload: (levelId) =>
|
|
86
|
+
set((s) => {
|
|
87
|
+
const { [levelId]: _, ...rest } = s.uploads
|
|
88
|
+
return { uploads: rest }
|
|
89
|
+
}),
|
|
90
|
+
}))
|