@pascal-app/editor 0.5.1 → 0.7.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.
Files changed (150) hide show
  1. package/package.json +12 -7
  2. package/src/components/editor/bottom-sheet.tsx +149 -0
  3. package/src/components/editor/custom-camera-controls.tsx +75 -7
  4. package/src/components/editor/editor-layout-mobile.tsx +264 -0
  5. package/src/components/editor/editor-layout-v2.tsx +29 -0
  6. package/src/components/editor/first-person/build-collider-world.ts +365 -0
  7. package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
  8. package/src/components/editor/first-person-controls.tsx +496 -143
  9. package/src/components/editor/floating-action-menu.tsx +281 -83
  10. package/src/components/editor/floating-building-action-menu.tsx +4 -3
  11. package/src/components/editor/floorplan-background-selection.ts +113 -0
  12. package/src/components/editor/floorplan-panel.tsx +10442 -3275
  13. package/src/components/editor/index.tsx +270 -20
  14. package/src/components/editor/node-action-menu.tsx +14 -1
  15. package/src/components/editor/selection-manager.tsx +766 -12
  16. package/src/components/editor/site-edge-labels.tsx +9 -3
  17. package/src/components/editor/thumbnail-generator.tsx +350 -157
  18. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  19. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  20. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  21. package/src/components/editor/wall-measurement-label.tsx +377 -58
  22. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  23. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  24. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  25. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
  26. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  27. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
  28. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  29. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  30. package/src/components/editor-2d/svg-paths.ts +119 -0
  31. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
  32. package/src/components/systems/roof/roof-edit-system.tsx +5 -5
  33. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  34. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
  35. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  36. package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
  37. package/src/components/tools/column/column-tool.tsx +97 -0
  38. package/src/components/tools/column/move-column-tool.tsx +105 -0
  39. package/src/components/tools/door/door-tool.tsx +19 -0
  40. package/src/components/tools/door/move-door-tool.tsx +38 -8
  41. package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
  42. package/src/components/tools/fence/fence-drafting.ts +27 -8
  43. package/src/components/tools/fence/fence-tool.tsx +159 -3
  44. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
  45. package/src/components/tools/fence/move-fence-tool.tsx +102 -27
  46. package/src/components/tools/item/move-tool.tsx +19 -1
  47. package/src/components/tools/item/placement-math.ts +44 -7
  48. package/src/components/tools/item/placement-strategies.ts +111 -33
  49. package/src/components/tools/item/placement-types.ts +7 -0
  50. package/src/components/tools/item/use-draft-node.ts +2 -0
  51. package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
  52. package/src/components/tools/roof/move-roof-tool.tsx +111 -43
  53. package/src/components/tools/shared/polygon-editor.tsx +244 -29
  54. package/src/components/tools/shared/segment-angle.ts +156 -0
  55. package/src/components/tools/slab/move-slab-tool.tsx +182 -0
  56. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  57. package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
  58. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  59. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  60. package/src/components/tools/stair/stair-tool.tsx +11 -3
  61. package/src/components/tools/tool-manager.tsx +30 -3
  62. package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
  63. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
  64. package/src/components/tools/wall/move-wall-tool.tsx +356 -0
  65. package/src/components/tools/wall/wall-drafting.ts +348 -17
  66. package/src/components/tools/wall/wall-tool.tsx +134 -2
  67. package/src/components/tools/window/move-window-tool.tsx +28 -0
  68. package/src/components/tools/window/window-tool.tsx +17 -0
  69. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  70. package/src/components/ui/action-menu/control-modes.tsx +37 -5
  71. package/src/components/ui/action-menu/index.tsx +91 -1
  72. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  73. package/src/components/ui/action-menu/view-toggles.tsx +424 -35
  74. package/src/components/ui/command-palette/editor-commands.tsx +27 -5
  75. package/src/components/ui/command-palette/index.tsx +0 -1
  76. package/src/components/ui/controls/material-picker.tsx +189 -169
  77. package/src/components/ui/controls/slider-control.tsx +88 -26
  78. package/src/components/ui/floating-level-selector.tsx +286 -55
  79. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  80. package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
  81. package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
  82. package/src/components/ui/level-duplicate-dialog.tsx +115 -0
  83. package/src/components/ui/panels/ceiling-panel.tsx +47 -27
  84. package/src/components/ui/panels/column-panel.tsx +715 -0
  85. package/src/components/ui/panels/door-panel.tsx +986 -294
  86. package/src/components/ui/panels/fence-panel.tsx +55 -12
  87. package/src/components/ui/panels/item-panel.tsx +5 -5
  88. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  89. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  90. package/src/components/ui/panels/node-display.ts +39 -0
  91. package/src/components/ui/panels/paint-panel.tsx +138 -0
  92. package/src/components/ui/panels/panel-manager.tsx +241 -30
  93. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  94. package/src/components/ui/panels/reference-panel.tsx +243 -9
  95. package/src/components/ui/panels/roof-panel.tsx +30 -62
  96. package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
  97. package/src/components/ui/panels/slab-panel.tsx +46 -24
  98. package/src/components/ui/panels/spawn-panel.tsx +155 -0
  99. package/src/components/ui/panels/stair-panel.tsx +117 -69
  100. package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
  101. package/src/components/ui/panels/wall-panel.tsx +71 -17
  102. package/src/components/ui/panels/window-panel.tsx +665 -146
  103. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  104. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  105. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
  106. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
  107. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  108. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
  109. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
  110. package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
  111. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
  112. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
  113. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  114. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
  115. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
  116. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
  117. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
  118. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
  119. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
  120. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
  121. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
  122. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  123. package/src/components/ui/viewer-toolbar.tsx +96 -2
  124. package/src/components/viewer-overlay.tsx +25 -19
  125. package/src/hooks/use-auto-frame.ts +45 -0
  126. package/src/hooks/use-contextual-tools.ts +14 -13
  127. package/src/hooks/use-keyboard.ts +67 -9
  128. package/src/hooks/use-mobile.ts +12 -12
  129. package/src/index.tsx +2 -1
  130. package/src/lib/door-interaction.ts +88 -0
  131. package/src/lib/floorplan/geometry.ts +263 -0
  132. package/src/lib/floorplan/index.ts +38 -0
  133. package/src/lib/floorplan/items.ts +179 -0
  134. package/src/lib/floorplan/selection-tool.ts +231 -0
  135. package/src/lib/floorplan/stairs.ts +478 -0
  136. package/src/lib/floorplan/types.ts +57 -0
  137. package/src/lib/floorplan/walls.ts +23 -0
  138. package/src/lib/guide-events.ts +10 -0
  139. package/src/lib/history.ts +20 -0
  140. package/src/lib/level-duplication.test.ts +72 -0
  141. package/src/lib/level-duplication.ts +153 -0
  142. package/src/lib/local-guide-image.ts +42 -0
  143. package/src/lib/material-paint.ts +284 -0
  144. package/src/lib/roof-duplication.ts +214 -0
  145. package/src/lib/scene-bounds.test.ts +183 -0
  146. package/src/lib/scene-bounds.ts +169 -0
  147. package/src/lib/sfx-player.ts +96 -13
  148. package/src/lib/stair-duplication.ts +126 -0
  149. package/src/lib/window-interaction.ts +86 -0
  150. package/src/store/use-editor.tsx +279 -15
@@ -1,23 +1,40 @@
1
1
  'use client'
2
2
 
3
- import type { AssetInput } from '@pascal-app/core'
4
3
  import {
4
+ type AnyNodeId,
5
+ type AssetInput,
5
6
  type BuildingNode,
7
+ type CeilingNode,
8
+ type ColumnNode,
6
9
  type DoorNode,
7
10
  type FenceNode,
8
11
  type ItemNode,
9
12
  type LevelNode,
10
13
  type RoofNode,
11
14
  type RoofSegmentNode,
15
+ type RoofSurfaceMaterialRole,
16
+ type SlabNode,
12
17
  type Space,
18
+ type SpawnNode,
13
19
  type StairNode,
14
20
  type StairSegmentNode,
21
+ type StairSurfaceMaterialRole,
15
22
  useScene,
23
+ type WallNode,
24
+ type WallSurfaceSide,
16
25
  type WindowNode,
17
26
  } from '@pascal-app/core'
18
27
  import { useViewer } from '@pascal-app/viewer'
19
28
  import { create } from 'zustand'
20
29
  import { persist } from 'zustand/middleware'
30
+ import { getDefaultCatalogItem } from '../components/ui/item-catalog/catalog-items'
31
+ import {
32
+ type ActivePaintMaterial,
33
+ type PaintableMaterialTarget,
34
+ resolveActivePaintMaterialFromSelection,
35
+ resolvePaintTargetFromSelection,
36
+ type SingleSurfaceMaterialRole,
37
+ } from '../lib/material-paint'
21
38
 
22
39
  const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'site'
23
40
  const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
@@ -29,7 +46,7 @@ export type SplitOrientation = 'horizontal' | 'vertical'
29
46
 
30
47
  export type Phase = 'site' | 'structure' | 'furnish'
31
48
 
32
- export type Mode = 'select' | 'edit' | 'delete' | 'build'
49
+ export type Mode = 'select' | 'edit' | 'delete' | 'build' | 'material-paint'
33
50
 
34
51
  // Structure mode tools (building elements)
35
52
  export type StructureTool =
@@ -44,6 +61,7 @@ export type StructureTool =
44
61
  | 'stair'
45
62
  | 'item'
46
63
  | 'zone'
64
+ | 'spawn'
47
65
  | 'window'
48
66
  | 'door'
49
67
 
@@ -66,10 +84,43 @@ export type CatalogCategory =
66
84
  export type StructureLayer = 'zones' | 'elements'
67
85
 
68
86
  export type FloorplanSelectionTool = 'click' | 'marquee'
87
+ export type GridSnapStep = 0.5 | 0.25 | 0.1 | 0.05
69
88
 
70
89
  // Combined tool type
71
90
  export type Tool = SiteTool | StructureTool | FurnishTool
72
91
 
92
+ export type MovingWallEndpoint = {
93
+ wall: WallNode
94
+ endpoint: 'start' | 'end'
95
+ }
96
+
97
+ export type MovingFenceEndpoint = {
98
+ fence: FenceNode
99
+ endpoint: 'start' | 'end'
100
+ }
101
+
102
+ export type MaterialTargetRole =
103
+ | WallSurfaceSide
104
+ | StairSurfaceMaterialRole
105
+ | RoofSurfaceMaterialRole
106
+ | SingleSurfaceMaterialRole
107
+
108
+ export type SelectedMaterialTarget = {
109
+ nodeId: AnyNodeId
110
+ role: MaterialTargetRole
111
+ }
112
+
113
+ type MaterialPaintSelectionSnapshot = {
114
+ selectedId: string | null
115
+ activePaintTarget: PaintableMaterialTarget
116
+ activePaintMaterial: ActivePaintMaterial | null
117
+ }
118
+
119
+ export type GuideUiState = {
120
+ locked?: boolean
121
+ scaleReferenceVisible?: boolean
122
+ }
123
+
73
124
  type EditorState = {
74
125
  phase: Phase
75
126
  setPhase: (phase: Phase) => void
@@ -88,8 +139,13 @@ type EditorState = {
88
139
  | WindowNode
89
140
  | DoorNode
90
141
  | FenceNode
142
+ | CeilingNode
143
+ | ColumnNode
144
+ | SlabNode
145
+ | WallNode
91
146
  | RoofNode
92
147
  | RoofSegmentNode
148
+ | SpawnNode
93
149
  | StairNode
94
150
  | StairSegmentNode
95
151
  | BuildingNode
@@ -100,15 +156,43 @@ type EditorState = {
100
156
  | WindowNode
101
157
  | DoorNode
102
158
  | FenceNode
159
+ | CeilingNode
160
+ | ColumnNode
161
+ | SlabNode
162
+ | WallNode
103
163
  | RoofNode
104
164
  | RoofSegmentNode
165
+ | SpawnNode
105
166
  | StairNode
106
167
  | StairSegmentNode
107
168
  | BuildingNode
108
169
  | null,
109
170
  ) => void
171
+ movingWallEndpoint: MovingWallEndpoint | null
172
+ setMovingWallEndpoint: (value: MovingWallEndpoint | null) => void
173
+ movingFenceEndpoint: MovingFenceEndpoint | null
174
+ setMovingFenceEndpoint: (value: MovingFenceEndpoint | null) => void
175
+ curvingWall: WallNode | null
176
+ setCurvingWall: (wall: WallNode | null) => void
177
+ curvingFence: FenceNode | null
178
+ setCurvingFence: (fence: FenceNode | null) => void
179
+ selectedMaterialTarget: SelectedMaterialTarget | null
180
+ setSelectedMaterialTarget: (target: SelectedMaterialTarget | null) => void
181
+ activePaintMaterial: ActivePaintMaterial | null
182
+ setActivePaintMaterial: (material: ActivePaintMaterial | null) => void
183
+ activePaintTarget: PaintableMaterialTarget
184
+ setActivePaintTarget: (target: PaintableMaterialTarget) => void
185
+ primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
186
+ hoveredPaintTarget: PaintableMaterialTarget | null
187
+ setHoveredPaintTarget: (target: PaintableMaterialTarget | null) => void
188
+ isPaintPanelOpen: boolean
189
+ setPaintPanelOpen: (open: boolean) => void
110
190
  selectedReferenceId: string | null
111
191
  setSelectedReferenceId: (id: string | null) => void
192
+ guideUi: Record<string, GuideUiState>
193
+ setGuideLocked: (guideId: string, locked: boolean) => void
194
+ setGuideScaleReferenceVisible: (guideId: string, visible: boolean) => void
195
+ clearGuideUi: (guideId: string) => void
112
196
  // Space detection for cutaway mode
113
197
  spaces: Record<string, Space>
114
198
  setSpaces: (spaces: Record<string, Space>) => void
@@ -131,6 +215,15 @@ type EditorState = {
131
215
  setFloorplanHovered: (hovered: boolean) => void
132
216
  floorplanSelectionTool: FloorplanSelectionTool
133
217
  setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
218
+ gridSnapStep: GridSnapStep
219
+ setGridSnapStep: (step: GridSnapStep) => void
220
+ showReferenceFloor: boolean
221
+ toggleReferenceFloor: () => void
222
+ setShowReferenceFloor: (show: boolean) => void
223
+ referenceFloorOffset: number
224
+ setReferenceFloorOffset: (offset: number) => void
225
+ referenceFloorOpacity: number
226
+ setReferenceFloorOpacity: (opacity: number) => void
134
227
  // First-person walkthrough mode (street view)
135
228
  isFirstPersonMode: boolean
136
229
  _viewModeBeforeFirstPerson: ViewMode | null
@@ -140,6 +233,10 @@ type EditorState = {
140
233
  setAllowUndergroundCamera: (enabled: boolean) => void
141
234
  activeSidebarPanel: string
142
235
  setActiveSidebarPanel: (id: string) => void
236
+ mobilePanelSheetHeight: number
237
+ setMobilePanelSheetHeight: (height: number) => void
238
+ isCaptureMode: boolean
239
+ setIsCaptureMode: (enabled: boolean) => void
143
240
  floorplanPaneRatio: number
144
241
  setFloorplanPaneRatio: (ratio: number) => void
145
242
  }
@@ -151,7 +248,14 @@ export type PersistedEditorUiState = Pick<
151
248
 
152
249
  type PersistedEditorLayoutState = Pick<
153
250
  EditorState,
154
- 'activeSidebarPanel' | 'floorplanPaneRatio' | 'splitOrientation' | 'floorplanSelectionTool'
251
+ | 'activeSidebarPanel'
252
+ | 'floorplanPaneRatio'
253
+ | 'splitOrientation'
254
+ | 'floorplanSelectionTool'
255
+ | 'gridSnapStep'
256
+ | 'showReferenceFloor'
257
+ | 'referenceFloorOffset'
258
+ | 'referenceFloorOpacity'
155
259
  >
156
260
  type PersistedEditorState = PersistedEditorUiState & PersistedEditorLayoutState
157
261
 
@@ -170,14 +274,20 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
170
274
  floorplanPaneRatio: DEFAULT_FLOORPLAN_PANE_RATIO,
171
275
  splitOrientation: 'horizontal',
172
276
  floorplanSelectionTool: 'click',
277
+ gridSnapStep: 0.5,
278
+ showReferenceFloor: false,
279
+ referenceFloorOffset: 1,
280
+ referenceFloorOpacity: 0.35,
173
281
  }
174
282
 
283
+ const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
284
+
175
285
  function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
176
286
  if (phase === 'site') {
177
287
  return 'select'
178
288
  }
179
289
 
180
- return mode === 'build' || mode === 'delete' ? mode : 'select'
290
+ return mode === 'build' || mode === 'delete' || mode === 'material-paint' ? mode : 'select'
181
291
  }
182
292
 
183
293
  function normalizeFloorplanPaneRatio(value: unknown): number {
@@ -274,6 +384,19 @@ function normalizePersistedEditorLayoutState(
274
384
  floorplanPaneRatio: normalizeFloorplanPaneRatio(state?.floorplanPaneRatio),
275
385
  splitOrientation: state?.splitOrientation === 'vertical' ? 'vertical' : 'horizontal',
276
386
  floorplanSelectionTool: state?.floorplanSelectionTool === 'marquee' ? 'marquee' : 'click',
387
+ gridSnapStep: GRID_SNAP_STEPS.includes(state?.gridSnapStep as GridSnapStep)
388
+ ? (state?.gridSnapStep as GridSnapStep)
389
+ : DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
390
+ showReferenceFloor: state?.showReferenceFloor === true,
391
+ referenceFloorOffset:
392
+ typeof state?.referenceFloorOffset === 'number' && state.referenceFloorOffset >= 1
393
+ ? Math.floor(state.referenceFloorOffset)
394
+ : DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOffset,
395
+ referenceFloorOpacity:
396
+ typeof state?.referenceFloorOpacity === 'number' &&
397
+ Number.isFinite(state.referenceFloorOpacity)
398
+ ? Math.min(0.8, Math.max(0.1, state.referenceFloorOpacity))
399
+ : DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOpacity,
277
400
  }
278
401
  }
279
402
 
@@ -333,6 +456,10 @@ export function selectDefaultBuildingAndLevel() {
333
456
  }
334
457
  }
335
458
 
459
+ function getDefaultSelectedItemForCategory(category: CatalogCategory | null): AssetInput | null {
460
+ return getDefaultCatalogItem(category)
461
+ }
462
+
336
463
  const useEditor = create<EditorState>()(
337
464
  persist(
338
465
  (set, get) => ({
@@ -354,7 +481,11 @@ const useEditor = create<EditorState>()(
354
481
  } else if (phase === 'structure') {
355
482
  set({ tool: 'wall', catalogCategory: null })
356
483
  } else if (phase === 'furnish') {
357
- set({ tool: 'item', catalogCategory: 'furniture' })
484
+ set({
485
+ tool: 'item',
486
+ catalogCategory: 'furniture',
487
+ selectedItem: getDefaultSelectedItemForCategory('furniture'),
488
+ })
358
489
  }
359
490
  } else {
360
491
  // Reset to select mode and clear tool/catalog when switching phases
@@ -394,9 +525,18 @@ const useEditor = create<EditorState>()(
394
525
  } else if (phase === 'structure' && structureLayer === 'elements') {
395
526
  set({ tool: 'wall' })
396
527
  } else if (phase === 'furnish') {
397
- set({ tool: 'item', catalogCategory: 'furniture' })
528
+ set({
529
+ tool: 'item',
530
+ catalogCategory: 'furniture',
531
+ selectedItem: getDefaultSelectedItemForCategory('furniture'),
532
+ })
398
533
  }
534
+ } else if (phase === 'furnish' && tool === 'item' && !get().selectedItem) {
535
+ const category = get().catalogCategory ?? 'furniture'
536
+ set({ selectedItem: getDefaultSelectedItemForCategory(category) })
399
537
  }
538
+ } else if (mode === 'material-paint') {
539
+ get().primeMaterialPaintFromSelection()
400
540
  }
401
541
  // When leaving build mode, clear tool
402
542
  else if (tool) {
@@ -423,13 +563,27 @@ const useEditor = create<EditorState>()(
423
563
  })
424
564
  },
425
565
  catalogCategory: DEFAULT_PERSISTED_EDITOR_UI_STATE.catalogCategory,
426
- setCatalogCategory: (category) => set({ catalogCategory: category }),
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
+ })),
427
577
  selectedItem: null,
428
578
  setSelectedItem: (item) => set({ selectedItem: item }),
429
579
  movingNode: null as
430
580
  | ItemNode
431
581
  | WindowNode
432
582
  | DoorNode
583
+ | FenceNode
584
+ | CeilingNode
585
+ | SlabNode
586
+ | WallNode
433
587
  | RoofNode
434
588
  | RoofSegmentNode
435
589
  | StairNode
@@ -437,8 +591,89 @@ const useEditor = create<EditorState>()(
437
591
  | BuildingNode
438
592
  | null,
439
593
  setMovingNode: (node) => set({ movingNode: node }),
594
+ movingWallEndpoint: null,
595
+ setMovingWallEndpoint: (value) => set({ movingWallEndpoint: value }),
596
+ movingFenceEndpoint: null,
597
+ setMovingFenceEndpoint: (value) => set({ movingFenceEndpoint: value }),
598
+ curvingWall: null,
599
+ setCurvingWall: (wall) => set({ curvingWall: wall }),
600
+ curvingFence: null,
601
+ setCurvingFence: (fence) => set({ curvingFence: fence }),
602
+ selectedMaterialTarget: null,
603
+ setSelectedMaterialTarget: (target) => set({ selectedMaterialTarget: target }),
604
+ activePaintMaterial: null,
605
+ setActivePaintMaterial: (material) => set({ activePaintMaterial: material }),
606
+ activePaintTarget: 'wall',
607
+ setActivePaintTarget: (target) =>
608
+ set((state) =>
609
+ state.activePaintTarget === target ? state : { activePaintTarget: target },
610
+ ),
611
+ primeMaterialPaintFromSelection: () => {
612
+ const selectedId =
613
+ useViewer.getState().selection.selectedIds.length === 1
614
+ ? (useViewer.getState().selection.selectedIds[0] ?? null)
615
+ : null
616
+ const activePaintTarget =
617
+ resolvePaintTargetFromSelection({
618
+ nodes: useScene.getState().nodes,
619
+ selectedId,
620
+ }) ?? get().activePaintTarget
621
+ const activePaintMaterial = resolveActivePaintMaterialFromSelection({
622
+ nodes: useScene.getState().nodes,
623
+ selectedId,
624
+ selectedMaterialTarget: get().selectedMaterialTarget,
625
+ })
626
+
627
+ set({
628
+ activePaintTarget,
629
+ ...(activePaintMaterial ? { activePaintMaterial } : {}),
630
+ })
631
+
632
+ return {
633
+ selectedId,
634
+ activePaintTarget,
635
+ activePaintMaterial: activePaintMaterial ?? get().activePaintMaterial,
636
+ }
637
+ },
638
+ hoveredPaintTarget: null,
639
+ setHoveredPaintTarget: (target) =>
640
+ set((state) =>
641
+ state.hoveredPaintTarget === target ? state : { hoveredPaintTarget: target },
642
+ ),
643
+ isPaintPanelOpen: false,
644
+ setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }),
440
645
  selectedReferenceId: null,
441
646
  setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
647
+ guideUi: {},
648
+ setGuideLocked: (guideId, locked) =>
649
+ set((state) => ({
650
+ guideUi: {
651
+ ...state.guideUi,
652
+ [guideId]: {
653
+ ...state.guideUi[guideId],
654
+ locked,
655
+ },
656
+ },
657
+ })),
658
+ setGuideScaleReferenceVisible: (guideId, visible) =>
659
+ set((state) => ({
660
+ guideUi: {
661
+ ...state.guideUi,
662
+ [guideId]: {
663
+ ...state.guideUi[guideId],
664
+ scaleReferenceVisible: visible,
665
+ },
666
+ },
667
+ })),
668
+ clearGuideUi: (guideId) =>
669
+ set((state) => {
670
+ if (!state.guideUi[guideId]) {
671
+ return state
672
+ }
673
+ const guideUi = { ...state.guideUi }
674
+ delete guideUi[guideId]
675
+ return { guideUi }
676
+ }),
442
677
  spaces: {},
443
678
  setSpaces: (spaces) => set({ spaces }),
444
679
  editingHole: null,
@@ -468,6 +703,18 @@ const useEditor = create<EditorState>()(
468
703
  setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
469
704
  floorplanSelectionTool: 'click' as FloorplanSelectionTool,
470
705
  setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
706
+ gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
707
+ setGridSnapStep: (step) => set({ gridSnapStep: step }),
708
+ showReferenceFloor: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showReferenceFloor,
709
+ toggleReferenceFloor: () =>
710
+ set((state) => ({ showReferenceFloor: !state.showReferenceFloor })),
711
+ setShowReferenceFloor: (show) => set({ showReferenceFloor: show }),
712
+ referenceFloorOffset: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOffset,
713
+ setReferenceFloorOffset: (offset) =>
714
+ set({ referenceFloorOffset: Math.max(1, Math.floor(offset)) }),
715
+ referenceFloorOpacity: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOpacity,
716
+ setReferenceFloorOpacity: (opacity) =>
717
+ set({ referenceFloorOpacity: Math.min(0.8, Math.max(0.1, opacity)) }),
471
718
  allowUndergroundCamera: false,
472
719
  setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
473
720
  isFirstPersonMode: false,
@@ -475,8 +722,6 @@ const useEditor = create<EditorState>()(
475
722
  setFirstPersonMode: (enabled) => {
476
723
  if (enabled) {
477
724
  const currentViewMode = get().viewMode
478
- useViewer.getState().setCameraMode('perspective')
479
- useViewer.getState().setWallMode('up')
480
725
  set({
481
726
  isFirstPersonMode: true,
482
727
  _viewModeBeforeFirstPerson: currentViewMode,
@@ -486,7 +731,6 @@ const useEditor = create<EditorState>()(
486
731
  tool: null,
487
732
  catalogCategory: null,
488
733
  })
489
- useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
490
734
  } else {
491
735
  const prevMode = get()._viewModeBeforeFirstPerson
492
736
  set({
@@ -498,17 +742,33 @@ const useEditor = create<EditorState>()(
498
742
  },
499
743
  activeSidebarPanel: DEFAULT_ACTIVE_SIDEBAR_PANEL,
500
744
  setActiveSidebarPanel: (id) => set({ activeSidebarPanel: id }),
745
+ mobilePanelSheetHeight: 0,
746
+ setMobilePanelSheetHeight: (height) => set({ mobilePanelSheetHeight: height }),
747
+ isCaptureMode: false,
748
+ setIsCaptureMode: (enabled) => set({ isCaptureMode: enabled }),
501
749
  floorplanPaneRatio: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.floorplanPaneRatio,
502
750
  setFloorplanPaneRatio: (ratio) =>
503
751
  set({ floorplanPaneRatio: normalizeFloorplanPaneRatio(ratio) }),
504
752
  }),
505
753
  {
506
754
  name: 'pascal-editor-ui-preferences',
507
- merge: (persistedState, currentState) => ({
508
- ...currentState,
509
- ...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
510
- ...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
511
- }),
755
+ merge: (persistedState, currentState) => {
756
+ const mergedState = {
757
+ ...currentState,
758
+ ...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
759
+ ...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
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
+ },
512
772
  partialize: (state) => ({
513
773
  phase: state.phase,
514
774
  mode: state.mode,
@@ -521,6 +781,10 @@ const useEditor = create<EditorState>()(
521
781
  floorplanPaneRatio: state.floorplanPaneRatio,
522
782
  splitOrientation: state.splitOrientation,
523
783
  floorplanSelectionTool: state.floorplanSelectionTool,
784
+ gridSnapStep: state.gridSnapStep,
785
+ showReferenceFloor: state.showReferenceFloor,
786
+ referenceFloorOffset: state.referenceFloorOffset,
787
+ referenceFloorOpacity: state.referenceFloorOpacity,
524
788
  }),
525
789
  },
526
790
  ),