@pascal-app/editor 0.6.0 → 0.8.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 (157) hide show
  1. package/package.json +13 -9
  2. package/src/components/editor/bottom-sheet.tsx +149 -0
  3. package/src/components/editor/custom-camera-controls.tsx +74 -5
  4. package/src/components/editor/editor-layout-mobile.tsx +264 -0
  5. package/src/components/editor/editor-layout-v2.tsx +24 -3
  6. package/src/components/editor/first-person/build-collider-world.ts +363 -0
  7. package/src/components/editor/first-person/bvh-ecctrl.tsx +860 -0
  8. package/src/components/editor/first-person-controls.tsx +496 -143
  9. package/src/components/editor/floating-action-menu.tsx +32 -55
  10. package/src/components/editor/floorplan-background-selection.ts +113 -0
  11. package/src/components/editor/floorplan-panel.tsx +9861 -3297
  12. package/src/components/editor/index.tsx +295 -32
  13. package/src/components/editor/selection-manager.tsx +575 -13
  14. package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
  15. package/src/components/editor/thumbnail-generator.tsx +56 -68
  16. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  17. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  18. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  19. package/src/components/editor/wall-measurement-label.tsx +267 -36
  20. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  21. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  22. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  23. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +124 -0
  24. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  25. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +202 -0
  26. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  27. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  28. package/src/components/editor-2d/svg-paths.ts +119 -0
  29. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
  30. package/src/components/systems/roof/roof-edit-system.tsx +1 -1
  31. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  32. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  33. package/src/components/systems/zone/zone-system.tsx +0 -0
  34. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  35. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
  36. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  37. package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
  38. package/src/components/tools/column/column-tool.tsx +97 -0
  39. package/src/components/tools/column/move-column-tool.tsx +105 -0
  40. package/src/components/tools/door/door-tool.tsx +7 -0
  41. package/src/components/tools/door/move-door-tool.tsx +28 -8
  42. package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
  43. package/src/components/tools/fence/fence-drafting.ts +10 -3
  44. package/src/components/tools/fence/fence-tool.tsx +160 -4
  45. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +139 -25
  46. package/src/components/tools/fence/move-fence-tool.tsx +111 -40
  47. package/src/components/tools/item/move-tool.tsx +7 -1
  48. package/src/components/tools/item/placement-math.ts +32 -5
  49. package/src/components/tools/item/placement-strategies.ts +110 -31
  50. package/src/components/tools/item/placement-types.ts +7 -0
  51. package/src/components/tools/item/use-draft-node.ts +1 -0
  52. package/src/components/tools/item/use-placement-coordinator.tsx +558 -52
  53. package/src/components/tools/roof/move-roof-tool.tsx +29 -17
  54. package/src/components/tools/select/box-select-tool.tsx +12 -17
  55. package/src/components/tools/shared/polygon-editor.tsx +153 -28
  56. package/src/components/tools/shared/segment-angle.ts +156 -0
  57. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  58. package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
  59. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  60. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  61. package/src/components/tools/tool-manager.tsx +20 -5
  62. package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
  63. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +131 -27
  64. package/src/components/tools/wall/move-wall-tool.tsx +6 -4
  65. package/src/components/tools/wall/wall-drafting.ts +18 -9
  66. package/src/components/tools/wall/wall-tool.tsx +136 -4
  67. package/src/components/tools/window/move-window-tool.tsx +18 -0
  68. package/src/components/tools/window/window-tool.tsx +5 -0
  69. package/src/components/tools/zone/zone-tool.tsx +20 -5
  70. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  71. package/src/components/ui/action-menu/control-modes.tsx +34 -1
  72. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  73. package/src/components/ui/action-menu/index.tsx +98 -59
  74. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  75. package/src/components/ui/action-menu/view-toggles.tsx +418 -41
  76. package/src/components/ui/command-palette/editor-commands.tsx +24 -5
  77. package/src/components/ui/command-palette/index.tsx +4 -255
  78. package/src/components/ui/controls/material-picker.tsx +154 -164
  79. package/src/components/ui/controls/slider-control.tsx +66 -18
  80. package/src/components/ui/floating-level-selector.tsx +286 -55
  81. package/src/components/ui/helpers/helper-manager.tsx +10 -0
  82. package/src/components/ui/item-catalog/catalog-items.tsx +2563 -1239
  83. package/src/components/ui/item-catalog/item-catalog.tsx +96 -187
  84. package/src/components/ui/level-duplicate-dialog.tsx +113 -0
  85. package/src/components/ui/panels/ceiling-panel.tsx +3 -28
  86. package/src/components/ui/panels/column-panel.tsx +759 -0
  87. package/src/components/ui/panels/door-panel.tsx +989 -290
  88. package/src/components/ui/panels/fence-panel.tsx +2 -49
  89. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  90. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  91. package/src/components/ui/panels/node-display.ts +39 -0
  92. package/src/components/ui/panels/paint-panel.tsx +163 -0
  93. package/src/components/ui/panels/panel-manager.tsx +208 -28
  94. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  95. package/src/components/ui/panels/reference-panel.tsx +253 -5
  96. package/src/components/ui/panels/roof-panel.tsx +13 -64
  97. package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
  98. package/src/components/ui/panels/slab-panel.tsx +4 -30
  99. package/src/components/ui/panels/spawn-panel.tsx +161 -0
  100. package/src/components/ui/panels/stair-panel.tsx +20 -74
  101. package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
  102. package/src/components/ui/panels/wall-panel.tsx +10 -8
  103. package/src/components/ui/panels/window-panel.tsx +668 -139
  104. package/src/components/ui/primitives/number-input.tsx +1 -1
  105. package/src/components/ui/primitives/sidebar.tsx +0 -0
  106. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  107. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  108. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  109. package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
  110. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
  111. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  112. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
  113. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
  114. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  115. package/src/components/ui/sidebar/panels/site-panel/index.tsx +105 -22
  116. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
  117. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
  118. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +76 -0
  119. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +11 -3
  120. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +10 -5
  121. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
  122. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  123. package/src/components/ui/slider.tsx +1 -1
  124. package/src/components/viewer-overlay.tsx +0 -0
  125. package/src/components/viewer-zone-system.tsx +0 -0
  126. package/src/hooks/use-auto-frame.ts +45 -0
  127. package/src/hooks/use-auto-save.ts +14 -0
  128. package/src/hooks/use-keyboard.ts +74 -7
  129. package/src/hooks/use-mobile.ts +12 -12
  130. package/src/index.tsx +8 -1
  131. package/src/lib/door-interaction.ts +88 -0
  132. package/src/lib/floorplan/geometry.ts +263 -0
  133. package/src/lib/floorplan/index.ts +38 -0
  134. package/src/lib/floorplan/items.ts +179 -0
  135. package/src/lib/floorplan/selection-tool.ts +231 -0
  136. package/src/lib/floorplan/stairs.ts +478 -0
  137. package/src/lib/floorplan/types.ts +57 -0
  138. package/src/lib/floorplan/walls.ts +23 -0
  139. package/src/lib/guide-events.ts +10 -0
  140. package/src/lib/level-duplication.test.ts +70 -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/scene.ts +0 -0
  148. package/src/lib/sfx-bus.ts +2 -0
  149. package/src/lib/sfx-player.ts +5 -5
  150. package/src/lib/stair-duplication.ts +126 -0
  151. package/src/lib/window-interaction.ts +86 -0
  152. package/src/store/use-editor.tsx +186 -62
  153. package/tsconfig.json +2 -1
  154. package/src/components/feedback-dialog.tsx +0 -265
  155. package/src/components/pascal-radio.tsx +0 -280
  156. package/src/components/preview-button.tsx +0 -16
  157. package/src/components/ui/viewer-toolbar.tsx +0 -395
@@ -1,22 +1,24 @@
1
1
  'use client'
2
2
 
3
+ import type { AssetInput } from '@pascal-app/core'
3
4
  import {
4
5
  type AnyNodeId,
5
- type AssetInput,
6
6
  type BuildingNode,
7
7
  type CeilingNode,
8
+ type ColumnNode,
8
9
  type DoorNode,
9
10
  type FenceNode,
10
11
  type ItemNode,
11
12
  type LevelNode,
12
- type RoofSurfaceMaterialRole,
13
13
  type RoofNode,
14
14
  type RoofSegmentNode,
15
+ type RoofSurfaceMaterialRole,
15
16
  type SlabNode,
16
17
  type Space,
17
- type StairSurfaceMaterialRole,
18
+ type SpawnNode,
18
19
  type StairNode,
19
20
  type StairSegmentNode,
21
+ type StairSurfaceMaterialRole,
20
22
  useScene,
21
23
  type WallNode,
22
24
  type WallSurfaceSide,
@@ -26,8 +28,15 @@ import { useViewer } from '@pascal-app/viewer'
26
28
  import { create } from 'zustand'
27
29
  import { persist } from 'zustand/middleware'
28
30
  import { getDefaultCatalogItem } from '../components/ui/item-catalog/catalog-items'
29
-
30
- const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'site'
31
+ import {
32
+ type ActivePaintMaterial,
33
+ type PaintableMaterialTarget,
34
+ resolveActivePaintMaterialFromSelection,
35
+ resolvePaintTargetFromSelection,
36
+ type SingleSurfaceMaterialRole,
37
+ } from '../lib/material-paint'
38
+
39
+ const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'ai'
31
40
  const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
32
41
  const MIN_FLOORPLAN_PANE_RATIO = 0.15
33
42
  const MAX_FLOORPLAN_PANE_RATIO = 0.85
@@ -37,7 +46,7 @@ export type SplitOrientation = 'horizontal' | 'vertical'
37
46
 
38
47
  export type Phase = 'site' | 'structure' | 'furnish'
39
48
 
40
- export type Mode = 'select' | 'edit' | 'delete' | 'build'
49
+ export type Mode = 'select' | 'edit' | 'delete' | 'build' | 'material-paint'
41
50
 
42
51
  // Structure mode tools (building elements)
43
52
  export type StructureTool =
@@ -52,6 +61,7 @@ export type StructureTool =
52
61
  | 'stair'
53
62
  | 'item'
54
63
  | 'zone'
64
+ | 'spawn'
55
65
  | 'window'
56
66
  | 'door'
57
67
 
@@ -89,13 +99,28 @@ export type MovingFenceEndpoint = {
89
99
  endpoint: 'start' | 'end'
90
100
  }
91
101
 
92
- export type MaterialTargetRole = WallSurfaceSide | StairSurfaceMaterialRole | RoofSurfaceMaterialRole
102
+ export type MaterialTargetRole =
103
+ | WallSurfaceSide
104
+ | StairSurfaceMaterialRole
105
+ | RoofSurfaceMaterialRole
106
+ | SingleSurfaceMaterialRole
93
107
 
94
108
  export type SelectedMaterialTarget = {
95
109
  nodeId: AnyNodeId
96
110
  role: MaterialTargetRole
97
111
  }
98
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
+
99
124
  type EditorState = {
100
125
  phase: Phase
101
126
  setPhase: (phase: Phase) => void
@@ -113,12 +138,14 @@ type EditorState = {
113
138
  | ItemNode
114
139
  | WindowNode
115
140
  | DoorNode
116
- | FenceNode
117
141
  | CeilingNode
142
+ | ColumnNode
118
143
  | SlabNode
119
144
  | WallNode
145
+ | FenceNode
120
146
  | RoofNode
121
147
  | RoofSegmentNode
148
+ | SpawnNode
122
149
  | StairNode
123
150
  | StairSegmentNode
124
151
  | BuildingNode
@@ -128,12 +155,14 @@ type EditorState = {
128
155
  | ItemNode
129
156
  | WindowNode
130
157
  | DoorNode
131
- | FenceNode
132
158
  | CeilingNode
159
+ | ColumnNode
133
160
  | SlabNode
134
161
  | WallNode
162
+ | FenceNode
135
163
  | RoofNode
136
164
  | RoofSegmentNode
165
+ | SpawnNode
137
166
  | StairNode
138
167
  | StairSegmentNode
139
168
  | BuildingNode
@@ -149,8 +178,21 @@ type EditorState = {
149
178
  setCurvingFence: (fence: FenceNode | null) => void
150
179
  selectedMaterialTarget: SelectedMaterialTarget | null
151
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
152
190
  selectedReferenceId: string | null
153
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
154
196
  // Space detection for cutaway mode
155
197
  spaces: Record<string, Space>
156
198
  setSpaces: (spaces: Record<string, Space>) => void
@@ -160,6 +202,9 @@ type EditorState = {
160
202
  // Preview mode (viewer-like experience inside the editor)
161
203
  isPreviewMode: boolean
162
204
  setPreviewMode: (preview: boolean) => void
205
+ // Capture mode (snapshot toolbar — hides panels for clean framing)
206
+ isCaptureMode: boolean
207
+ setCaptureMode: (active: boolean) => void
163
208
  // View mode (3D only, 2D only, or split 2D+3D)
164
209
  viewMode: ViewMode
165
210
  setViewMode: (mode: ViewMode) => void
@@ -175,17 +220,29 @@ type EditorState = {
175
220
  setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
176
221
  gridSnapStep: GridSnapStep
177
222
  setGridSnapStep: (step: GridSnapStep) => void
223
+ showReferenceFloor: boolean
224
+ toggleReferenceFloor: () => void
225
+ setShowReferenceFloor: (show: boolean) => void
226
+ referenceFloorOffset: number
227
+ setReferenceFloorOffset: (offset: number) => void
228
+ referenceFloorOpacity: number
229
+ setReferenceFloorOpacity: (opacity: number) => void
230
+ // Development-only camera debug flag for inspecting underside geometry
231
+ allowUndergroundCamera: boolean
232
+ setAllowUndergroundCamera: (enabled: boolean) => void
178
233
  // First-person walkthrough mode (street view)
179
234
  isFirstPersonMode: boolean
180
235
  _viewModeBeforeFirstPerson: ViewMode | null
181
236
  setFirstPersonMode: (enabled: boolean) => void
182
- // Development-only camera debug flag for inspecting underside geometry
183
- allowUndergroundCamera: boolean
184
- setAllowUndergroundCamera: (enabled: boolean) => void
185
237
  activeSidebarPanel: string
186
238
  setActiveSidebarPanel: (id: string) => void
239
+ setIsCaptureMode: (enabled: boolean) => void
187
240
  floorplanPaneRatio: number
188
241
  setFloorplanPaneRatio: (ratio: number) => void
242
+ // Mobile-only: pixel height of the secondary panel sheet while open (0 when closed).
243
+ // Read by the mobile layout so the viewer container can shrink to preview edits.
244
+ mobilePanelSheetHeight: number
245
+ setMobilePanelSheetHeight: (px: number) => void
189
246
  }
190
247
 
191
248
  export type PersistedEditorUiState = Pick<
@@ -200,6 +257,9 @@ type PersistedEditorLayoutState = Pick<
200
257
  | 'splitOrientation'
201
258
  | 'floorplanSelectionTool'
202
259
  | 'gridSnapStep'
260
+ | 'showReferenceFloor'
261
+ | 'referenceFloorOffset'
262
+ | 'referenceFloorOpacity'
203
263
  >
204
264
  type PersistedEditorState = PersistedEditorUiState & PersistedEditorLayoutState
205
265
 
@@ -219,6 +279,9 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
219
279
  splitOrientation: 'horizontal',
220
280
  floorplanSelectionTool: 'click',
221
281
  gridSnapStep: 0.5,
282
+ showReferenceFloor: false,
283
+ referenceFloorOffset: 1,
284
+ referenceFloorOpacity: 0.35,
222
285
  }
223
286
 
224
287
  const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
@@ -228,7 +291,7 @@ function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
228
291
  return 'select'
229
292
  }
230
293
 
231
- return mode === 'build' || mode === 'delete' ? mode : 'select'
294
+ return mode === 'build' || mode === 'delete' || mode === 'material-paint' ? mode : 'select'
232
295
  }
233
296
 
234
297
  function normalizeFloorplanPaneRatio(value: unknown): number {
@@ -328,6 +391,16 @@ function normalizePersistedEditorLayoutState(
328
391
  gridSnapStep: GRID_SNAP_STEPS.includes(state?.gridSnapStep as GridSnapStep)
329
392
  ? (state?.gridSnapStep as GridSnapStep)
330
393
  : DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
394
+ showReferenceFloor: state?.showReferenceFloor === true,
395
+ referenceFloorOffset:
396
+ typeof state?.referenceFloorOffset === 'number' && state.referenceFloorOffset >= 1
397
+ ? Math.floor(state.referenceFloorOffset)
398
+ : DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOffset,
399
+ referenceFloorOpacity:
400
+ typeof state?.referenceFloorOpacity === 'number' &&
401
+ Number.isFinite(state.referenceFloorOpacity)
402
+ ? Math.min(0.8, Math.max(0.1, state.referenceFloorOpacity))
403
+ : DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOpacity,
331
404
  }
332
405
  }
333
406
 
@@ -387,10 +460,6 @@ export function selectDefaultBuildingAndLevel() {
387
460
  }
388
461
  }
389
462
 
390
- function getDefaultSelectedItemForCategory(category: CatalogCategory | null): AssetInput | null {
391
- return getDefaultCatalogItem(category)
392
- }
393
-
394
463
  const useEditor = create<EditorState>()(
395
464
  persist(
396
465
  (set, get) => ({
@@ -412,11 +481,7 @@ const useEditor = create<EditorState>()(
412
481
  } else if (phase === 'structure') {
413
482
  set({ tool: 'wall', catalogCategory: null })
414
483
  } else if (phase === 'furnish') {
415
- set({
416
- tool: 'item',
417
- catalogCategory: 'furniture',
418
- selectedItem: getDefaultSelectedItemForCategory('furniture'),
419
- })
484
+ set({ tool: 'item', catalogCategory: 'furniture' })
420
485
  }
421
486
  } else {
422
487
  // Reset to select mode and clear tool/catalog when switching phases
@@ -456,16 +521,11 @@ const useEditor = create<EditorState>()(
456
521
  } else if (phase === 'structure' && structureLayer === 'elements') {
457
522
  set({ tool: 'wall' })
458
523
  } else if (phase === 'furnish') {
459
- set({
460
- tool: 'item',
461
- catalogCategory: 'furniture',
462
- selectedItem: getDefaultSelectedItemForCategory('furniture'),
463
- })
524
+ set({ tool: 'item', catalogCategory: 'furniture' })
464
525
  }
465
- } else if (phase === 'furnish' && tool === 'item' && !get().selectedItem) {
466
- const category = get().catalogCategory ?? 'furniture'
467
- set({ selectedItem: getDefaultSelectedItemForCategory(category) })
468
526
  }
527
+ } else if (mode === 'material-paint') {
528
+ get().primeMaterialPaintFromSelection()
469
529
  }
470
530
  // When leaving build mode, clear tool
471
531
  else if (tool) {
@@ -492,27 +552,17 @@ const useEditor = create<EditorState>()(
492
552
  })
493
553
  },
494
554
  catalogCategory: DEFAULT_PERSISTED_EDITOR_UI_STATE.catalogCategory,
495
- setCatalogCategory: (category) =>
496
- set((state) => ({
497
- catalogCategory: category,
498
- selectedItem:
499
- category !== null &&
500
- state.phase === 'furnish' &&
501
- state.mode === 'build' &&
502
- state.tool === 'item'
503
- ? getDefaultSelectedItemForCategory(category)
504
- : state.selectedItem,
505
- })),
555
+ setCatalogCategory: (category) => set({ catalogCategory: category }),
506
556
  selectedItem: null,
507
557
  setSelectedItem: (item) => set({ selectedItem: item }),
508
558
  movingNode: null as
509
559
  | ItemNode
510
560
  | WindowNode
511
561
  | DoorNode
512
- | FenceNode
513
562
  | CeilingNode
514
563
  | SlabNode
515
564
  | WallNode
565
+ | FenceNode
516
566
  | RoofNode
517
567
  | RoofSegmentNode
518
568
  | StairNode
@@ -530,8 +580,79 @@ const useEditor = create<EditorState>()(
530
580
  setCurvingFence: (fence) => set({ curvingFence: fence }),
531
581
  selectedMaterialTarget: null,
532
582
  setSelectedMaterialTarget: (target) => set({ selectedMaterialTarget: target }),
583
+ activePaintMaterial: null,
584
+ setActivePaintMaterial: (material) => set({ activePaintMaterial: material }),
585
+ activePaintTarget: 'wall',
586
+ setActivePaintTarget: (target) =>
587
+ set((state) =>
588
+ state.activePaintTarget === target ? state : { activePaintTarget: target },
589
+ ),
590
+ primeMaterialPaintFromSelection: () => {
591
+ const selectedId =
592
+ useViewer.getState().selection.selectedIds.length === 1
593
+ ? (useViewer.getState().selection.selectedIds[0] ?? null)
594
+ : null
595
+ const activePaintTarget =
596
+ resolvePaintTargetFromSelection({
597
+ nodes: useScene.getState().nodes,
598
+ selectedId,
599
+ }) ?? get().activePaintTarget
600
+ const activePaintMaterial = resolveActivePaintMaterialFromSelection({
601
+ nodes: useScene.getState().nodes,
602
+ selectedId,
603
+ selectedMaterialTarget: get().selectedMaterialTarget,
604
+ })
605
+
606
+ set({
607
+ activePaintTarget,
608
+ ...(activePaintMaterial ? { activePaintMaterial } : {}),
609
+ })
610
+
611
+ return {
612
+ selectedId,
613
+ activePaintTarget,
614
+ activePaintMaterial: activePaintMaterial ?? get().activePaintMaterial,
615
+ }
616
+ },
617
+ hoveredPaintTarget: null,
618
+ setHoveredPaintTarget: (target) =>
619
+ set((state) =>
620
+ state.hoveredPaintTarget === target ? state : { hoveredPaintTarget: target },
621
+ ),
622
+ isPaintPanelOpen: false,
623
+ setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }),
533
624
  selectedReferenceId: null,
534
625
  setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
626
+ guideUi: {},
627
+ setGuideLocked: (guideId, locked) =>
628
+ set((state) => ({
629
+ guideUi: {
630
+ ...state.guideUi,
631
+ [guideId]: {
632
+ ...state.guideUi[guideId],
633
+ locked,
634
+ },
635
+ },
636
+ })),
637
+ setGuideScaleReferenceVisible: (guideId, visible) =>
638
+ set((state) => ({
639
+ guideUi: {
640
+ ...state.guideUi,
641
+ [guideId]: {
642
+ ...state.guideUi[guideId],
643
+ scaleReferenceVisible: visible,
644
+ },
645
+ },
646
+ })),
647
+ clearGuideUi: (guideId) =>
648
+ set((state) => {
649
+ if (!state.guideUi[guideId]) {
650
+ return state
651
+ }
652
+ const guideUi = { ...state.guideUi }
653
+ delete guideUi[guideId]
654
+ return { guideUi }
655
+ }),
535
656
  spaces: {},
536
657
  setSpaces: (spaces) => set({ spaces }),
537
658
  editingHole: null,
@@ -546,6 +667,8 @@ const useEditor = create<EditorState>()(
546
667
  set({ isPreviewMode: false })
547
668
  }
548
669
  },
670
+ isCaptureMode: false,
671
+ setCaptureMode: (active) => set({ isCaptureMode: active }),
549
672
  viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode,
550
673
  setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }),
551
674
  splitOrientation: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.splitOrientation,
@@ -563,6 +686,16 @@ const useEditor = create<EditorState>()(
563
686
  setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
564
687
  gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
565
688
  setGridSnapStep: (step) => set({ gridSnapStep: step }),
689
+ showReferenceFloor: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showReferenceFloor,
690
+ toggleReferenceFloor: () =>
691
+ set((state) => ({ showReferenceFloor: !state.showReferenceFloor })),
692
+ setShowReferenceFloor: (show) => set({ showReferenceFloor: show }),
693
+ referenceFloorOffset: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOffset,
694
+ setReferenceFloorOffset: (offset) =>
695
+ set({ referenceFloorOffset: Math.max(1, Math.floor(offset)) }),
696
+ referenceFloorOpacity: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOpacity,
697
+ setReferenceFloorOpacity: (opacity) =>
698
+ set({ referenceFloorOpacity: Math.min(0.8, Math.max(0.1, opacity)) }),
566
699
  allowUndergroundCamera: false,
567
700
  setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
568
701
  isFirstPersonMode: false,
@@ -570,8 +703,6 @@ const useEditor = create<EditorState>()(
570
703
  setFirstPersonMode: (enabled) => {
571
704
  if (enabled) {
572
705
  const currentViewMode = get().viewMode
573
- useViewer.getState().setCameraMode('perspective')
574
- useViewer.getState().setWallMode('up')
575
706
  set({
576
707
  isFirstPersonMode: true,
577
708
  _viewModeBeforeFirstPerson: currentViewMode,
@@ -581,7 +712,6 @@ const useEditor = create<EditorState>()(
581
712
  tool: null,
582
713
  catalogCategory: null,
583
714
  })
584
- useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
585
715
  } else {
586
716
  const prevMode = get()._viewModeBeforeFirstPerson
587
717
  set({
@@ -593,29 +723,20 @@ const useEditor = create<EditorState>()(
593
723
  },
594
724
  activeSidebarPanel: DEFAULT_ACTIVE_SIDEBAR_PANEL,
595
725
  setActiveSidebarPanel: (id) => set({ activeSidebarPanel: id }),
726
+ setIsCaptureMode: (enabled) => set({ isCaptureMode: enabled }),
596
727
  floorplanPaneRatio: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.floorplanPaneRatio,
597
728
  setFloorplanPaneRatio: (ratio) =>
598
729
  set({ floorplanPaneRatio: normalizeFloorplanPaneRatio(ratio) }),
730
+ mobilePanelSheetHeight: 0,
731
+ setMobilePanelSheetHeight: (px) => set({ mobilePanelSheetHeight: Math.max(0, px) }),
599
732
  }),
600
733
  {
601
734
  name: 'pascal-editor-ui-preferences',
602
- merge: (persistedState, currentState) => {
603
- const mergedState = {
604
- ...currentState,
605
- ...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
606
- ...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
607
- }
608
-
609
- return {
610
- ...mergedState,
611
- selectedItem:
612
- mergedState.phase === 'furnish' &&
613
- mergedState.mode === 'build' &&
614
- mergedState.tool === 'item'
615
- ? getDefaultSelectedItemForCategory(mergedState.catalogCategory ?? 'furniture')
616
- : currentState.selectedItem,
617
- }
618
- },
735
+ merge: (persistedState, currentState) => ({
736
+ ...currentState,
737
+ ...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
738
+ ...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
739
+ }),
619
740
  partialize: (state) => ({
620
741
  phase: state.phase,
621
742
  mode: state.mode,
@@ -629,6 +750,9 @@ const useEditor = create<EditorState>()(
629
750
  splitOrientation: state.splitOrientation,
630
751
  floorplanSelectionTool: state.floorplanSelectionTool,
631
752
  gridSnapStep: state.gridSnapStep,
753
+ showReferenceFloor: state.showReferenceFloor,
754
+ referenceFloorOffset: state.referenceFloorOffset,
755
+ referenceFloorOpacity: state.referenceFloorOpacity,
632
756
  }),
633
757
  },
634
758
  ),
package/tsconfig.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "extends": "@pascal/typescript-config/react-library.json",
3
3
  "compilerOptions": {
4
4
  "rootDir": "src",
5
- "noEmit": true
5
+ "noEmit": true,
6
+ "types": ["bun"]
6
7
  },
7
8
  "include": ["src"],
8
9
  "exclude": ["node_modules"]