@pascal-app/editor 0.5.1 → 0.6.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 (79) hide show
  1. package/package.json +8 -7
  2. package/src/components/editor/editor-layout-v2.tsx +9 -0
  3. package/src/components/editor/floating-action-menu.tsx +255 -34
  4. package/src/components/editor/floating-building-action-menu.tsx +4 -3
  5. package/src/components/editor/floorplan-panel.tsx +1323 -713
  6. package/src/components/editor/index.tsx +2 -0
  7. package/src/components/editor/node-action-menu.tsx +14 -1
  8. package/src/components/editor/selection-manager.tsx +200 -8
  9. package/src/components/editor/site-edge-labels.tsx +9 -3
  10. package/src/components/editor/thumbnail-generator.tsx +319 -157
  11. package/src/components/editor/wall-measurement-label.tsx +120 -32
  12. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
  13. package/src/components/systems/roof/roof-edit-system.tsx +5 -5
  14. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
  15. package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
  16. package/src/components/tools/door/door-tool.tsx +12 -0
  17. package/src/components/tools/door/move-door-tool.tsx +10 -0
  18. package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
  19. package/src/components/tools/fence/fence-drafting.ts +19 -7
  20. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
  21. package/src/components/tools/fence/move-fence-tool.tsx +8 -0
  22. package/src/components/tools/item/move-tool.tsx +9 -0
  23. package/src/components/tools/item/placement-math.ts +14 -6
  24. package/src/components/tools/item/placement-strategies.ts +2 -2
  25. package/src/components/tools/item/use-placement-coordinator.tsx +42 -10
  26. package/src/components/tools/roof/move-roof-tool.tsx +89 -28
  27. package/src/components/tools/shared/polygon-editor.tsx +98 -8
  28. package/src/components/tools/slab/move-slab-tool.tsx +182 -0
  29. package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
  30. package/src/components/tools/stair/stair-tool.tsx +11 -3
  31. package/src/components/tools/tool-manager.tsx +12 -0
  32. package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
  33. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +322 -0
  34. package/src/components/tools/wall/move-wall-tool.tsx +356 -0
  35. package/src/components/tools/wall/wall-drafting.ts +331 -9
  36. package/src/components/tools/window/move-window-tool.tsx +10 -0
  37. package/src/components/tools/window/window-tool.tsx +12 -0
  38. package/src/components/ui/action-menu/control-modes.tsx +9 -4
  39. package/src/components/ui/command-palette/editor-commands.tsx +9 -4
  40. package/src/components/ui/command-palette/index.tsx +0 -1
  41. package/src/components/ui/controls/material-picker.tsx +127 -94
  42. package/src/components/ui/controls/slider-control.tsx +28 -14
  43. package/src/components/ui/item-catalog/catalog-items.tsx +5 -0
  44. package/src/components/ui/panels/ceiling-panel.tsx +61 -17
  45. package/src/components/ui/panels/door-panel.tsx +5 -5
  46. package/src/components/ui/panels/fence-panel.tsx +97 -12
  47. package/src/components/ui/panels/item-panel.tsx +5 -5
  48. package/src/components/ui/panels/panel-manager.tsx +31 -29
  49. package/src/components/ui/panels/reference-panel.tsx +5 -4
  50. package/src/components/ui/panels/roof-panel.tsx +91 -22
  51. package/src/components/ui/panels/roof-segment-panel.tsx +23 -13
  52. package/src/components/ui/panels/slab-panel.tsx +63 -15
  53. package/src/components/ui/panels/stair-panel.tsx +173 -19
  54. package/src/components/ui/panels/stair-segment-panel.tsx +28 -17
  55. package/src/components/ui/panels/wall-panel.tsx +159 -11
  56. package/src/components/ui/panels/window-panel.tsx +5 -7
  57. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +7 -3
  58. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
  59. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
  60. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
  61. package/src/components/ui/sidebar/panels/site-panel/index.tsx +29 -32
  62. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
  63. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -3
  64. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  65. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
  66. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
  67. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
  68. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -3
  69. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
  70. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
  71. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +7 -3
  72. package/src/components/ui/viewer-toolbar.tsx +55 -2
  73. package/src/components/viewer-overlay.tsx +25 -19
  74. package/src/hooks/use-contextual-tools.ts +14 -13
  75. package/src/hooks/use-keyboard.ts +3 -2
  76. package/src/index.tsx +2 -1
  77. package/src/lib/history.ts +20 -0
  78. package/src/lib/sfx-player.ts +96 -13
  79. package/src/store/use-editor.tsx +118 -10
@@ -1,23 +1,31 @@
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,
6
8
  type DoorNode,
7
9
  type FenceNode,
8
10
  type ItemNode,
9
11
  type LevelNode,
12
+ type RoofSurfaceMaterialRole,
10
13
  type RoofNode,
11
14
  type RoofSegmentNode,
15
+ type SlabNode,
12
16
  type Space,
17
+ type StairSurfaceMaterialRole,
13
18
  type StairNode,
14
19
  type StairSegmentNode,
15
20
  useScene,
21
+ type WallNode,
22
+ type WallSurfaceSide,
16
23
  type WindowNode,
17
24
  } from '@pascal-app/core'
18
25
  import { useViewer } from '@pascal-app/viewer'
19
26
  import { create } from 'zustand'
20
27
  import { persist } from 'zustand/middleware'
28
+ import { getDefaultCatalogItem } from '../components/ui/item-catalog/catalog-items'
21
29
 
22
30
  const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'site'
23
31
  const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
@@ -66,10 +74,28 @@ export type CatalogCategory =
66
74
  export type StructureLayer = 'zones' | 'elements'
67
75
 
68
76
  export type FloorplanSelectionTool = 'click' | 'marquee'
77
+ export type GridSnapStep = 0.5 | 0.25 | 0.1 | 0.05
69
78
 
70
79
  // Combined tool type
71
80
  export type Tool = SiteTool | StructureTool | FurnishTool
72
81
 
82
+ export type MovingWallEndpoint = {
83
+ wall: WallNode
84
+ endpoint: 'start' | 'end'
85
+ }
86
+
87
+ export type MovingFenceEndpoint = {
88
+ fence: FenceNode
89
+ endpoint: 'start' | 'end'
90
+ }
91
+
92
+ export type MaterialTargetRole = WallSurfaceSide | StairSurfaceMaterialRole | RoofSurfaceMaterialRole
93
+
94
+ export type SelectedMaterialTarget = {
95
+ nodeId: AnyNodeId
96
+ role: MaterialTargetRole
97
+ }
98
+
73
99
  type EditorState = {
74
100
  phase: Phase
75
101
  setPhase: (phase: Phase) => void
@@ -88,6 +114,9 @@ type EditorState = {
88
114
  | WindowNode
89
115
  | DoorNode
90
116
  | FenceNode
117
+ | CeilingNode
118
+ | SlabNode
119
+ | WallNode
91
120
  | RoofNode
92
121
  | RoofSegmentNode
93
122
  | StairNode
@@ -100,6 +129,9 @@ type EditorState = {
100
129
  | WindowNode
101
130
  | DoorNode
102
131
  | FenceNode
132
+ | CeilingNode
133
+ | SlabNode
134
+ | WallNode
103
135
  | RoofNode
104
136
  | RoofSegmentNode
105
137
  | StairNode
@@ -107,6 +139,16 @@ type EditorState = {
107
139
  | BuildingNode
108
140
  | null,
109
141
  ) => void
142
+ movingWallEndpoint: MovingWallEndpoint | null
143
+ setMovingWallEndpoint: (value: MovingWallEndpoint | null) => void
144
+ movingFenceEndpoint: MovingFenceEndpoint | null
145
+ setMovingFenceEndpoint: (value: MovingFenceEndpoint | null) => void
146
+ curvingWall: WallNode | null
147
+ setCurvingWall: (wall: WallNode | null) => void
148
+ curvingFence: FenceNode | null
149
+ setCurvingFence: (fence: FenceNode | null) => void
150
+ selectedMaterialTarget: SelectedMaterialTarget | null
151
+ setSelectedMaterialTarget: (target: SelectedMaterialTarget | null) => void
110
152
  selectedReferenceId: string | null
111
153
  setSelectedReferenceId: (id: string | null) => void
112
154
  // Space detection for cutaway mode
@@ -131,6 +173,8 @@ type EditorState = {
131
173
  setFloorplanHovered: (hovered: boolean) => void
132
174
  floorplanSelectionTool: FloorplanSelectionTool
133
175
  setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
176
+ gridSnapStep: GridSnapStep
177
+ setGridSnapStep: (step: GridSnapStep) => void
134
178
  // First-person walkthrough mode (street view)
135
179
  isFirstPersonMode: boolean
136
180
  _viewModeBeforeFirstPerson: ViewMode | null
@@ -151,7 +195,11 @@ export type PersistedEditorUiState = Pick<
151
195
 
152
196
  type PersistedEditorLayoutState = Pick<
153
197
  EditorState,
154
- 'activeSidebarPanel' | 'floorplanPaneRatio' | 'splitOrientation' | 'floorplanSelectionTool'
198
+ | 'activeSidebarPanel'
199
+ | 'floorplanPaneRatio'
200
+ | 'splitOrientation'
201
+ | 'floorplanSelectionTool'
202
+ | 'gridSnapStep'
155
203
  >
156
204
  type PersistedEditorState = PersistedEditorUiState & PersistedEditorLayoutState
157
205
 
@@ -170,8 +218,11 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
170
218
  floorplanPaneRatio: DEFAULT_FLOORPLAN_PANE_RATIO,
171
219
  splitOrientation: 'horizontal',
172
220
  floorplanSelectionTool: 'click',
221
+ gridSnapStep: 0.5,
173
222
  }
174
223
 
224
+ const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
225
+
175
226
  function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
176
227
  if (phase === 'site') {
177
228
  return 'select'
@@ -274,6 +325,9 @@ function normalizePersistedEditorLayoutState(
274
325
  floorplanPaneRatio: normalizeFloorplanPaneRatio(state?.floorplanPaneRatio),
275
326
  splitOrientation: state?.splitOrientation === 'vertical' ? 'vertical' : 'horizontal',
276
327
  floorplanSelectionTool: state?.floorplanSelectionTool === 'marquee' ? 'marquee' : 'click',
328
+ gridSnapStep: GRID_SNAP_STEPS.includes(state?.gridSnapStep as GridSnapStep)
329
+ ? (state?.gridSnapStep as GridSnapStep)
330
+ : DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
277
331
  }
278
332
  }
279
333
 
@@ -333,6 +387,10 @@ export function selectDefaultBuildingAndLevel() {
333
387
  }
334
388
  }
335
389
 
390
+ function getDefaultSelectedItemForCategory(category: CatalogCategory | null): AssetInput | null {
391
+ return getDefaultCatalogItem(category)
392
+ }
393
+
336
394
  const useEditor = create<EditorState>()(
337
395
  persist(
338
396
  (set, get) => ({
@@ -354,7 +412,11 @@ const useEditor = create<EditorState>()(
354
412
  } else if (phase === 'structure') {
355
413
  set({ tool: 'wall', catalogCategory: null })
356
414
  } else if (phase === 'furnish') {
357
- set({ tool: 'item', catalogCategory: 'furniture' })
415
+ set({
416
+ tool: 'item',
417
+ catalogCategory: 'furniture',
418
+ selectedItem: getDefaultSelectedItemForCategory('furniture'),
419
+ })
358
420
  }
359
421
  } else {
360
422
  // Reset to select mode and clear tool/catalog when switching phases
@@ -394,8 +456,15 @@ const useEditor = create<EditorState>()(
394
456
  } else if (phase === 'structure' && structureLayer === 'elements') {
395
457
  set({ tool: 'wall' })
396
458
  } else if (phase === 'furnish') {
397
- set({ tool: 'item', catalogCategory: 'furniture' })
459
+ set({
460
+ tool: 'item',
461
+ catalogCategory: 'furniture',
462
+ selectedItem: getDefaultSelectedItemForCategory('furniture'),
463
+ })
398
464
  }
465
+ } else if (phase === 'furnish' && tool === 'item' && !get().selectedItem) {
466
+ const category = get().catalogCategory ?? 'furniture'
467
+ set({ selectedItem: getDefaultSelectedItemForCategory(category) })
399
468
  }
400
469
  }
401
470
  // When leaving build mode, clear tool
@@ -423,13 +492,27 @@ const useEditor = create<EditorState>()(
423
492
  })
424
493
  },
425
494
  catalogCategory: DEFAULT_PERSISTED_EDITOR_UI_STATE.catalogCategory,
426
- setCatalogCategory: (category) => set({ catalogCategory: category }),
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
+ })),
427
506
  selectedItem: null,
428
507
  setSelectedItem: (item) => set({ selectedItem: item }),
429
508
  movingNode: null as
430
509
  | ItemNode
431
510
  | WindowNode
432
511
  | DoorNode
512
+ | FenceNode
513
+ | CeilingNode
514
+ | SlabNode
515
+ | WallNode
433
516
  | RoofNode
434
517
  | RoofSegmentNode
435
518
  | StairNode
@@ -437,6 +520,16 @@ const useEditor = create<EditorState>()(
437
520
  | BuildingNode
438
521
  | null,
439
522
  setMovingNode: (node) => set({ movingNode: node }),
523
+ movingWallEndpoint: null,
524
+ setMovingWallEndpoint: (value) => set({ movingWallEndpoint: value }),
525
+ movingFenceEndpoint: null,
526
+ setMovingFenceEndpoint: (value) => set({ movingFenceEndpoint: value }),
527
+ curvingWall: null,
528
+ setCurvingWall: (wall) => set({ curvingWall: wall }),
529
+ curvingFence: null,
530
+ setCurvingFence: (fence) => set({ curvingFence: fence }),
531
+ selectedMaterialTarget: null,
532
+ setSelectedMaterialTarget: (target) => set({ selectedMaterialTarget: target }),
440
533
  selectedReferenceId: null,
441
534
  setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
442
535
  spaces: {},
@@ -468,6 +561,8 @@ const useEditor = create<EditorState>()(
468
561
  setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
469
562
  floorplanSelectionTool: 'click' as FloorplanSelectionTool,
470
563
  setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
564
+ gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
565
+ setGridSnapStep: (step) => set({ gridSnapStep: step }),
471
566
  allowUndergroundCamera: false,
472
567
  setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
473
568
  isFirstPersonMode: false,
@@ -504,11 +599,23 @@ const useEditor = create<EditorState>()(
504
599
  }),
505
600
  {
506
601
  name: 'pascal-editor-ui-preferences',
507
- merge: (persistedState, currentState) => ({
508
- ...currentState,
509
- ...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
510
- ...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
511
- }),
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
+ },
512
619
  partialize: (state) => ({
513
620
  phase: state.phase,
514
621
  mode: state.mode,
@@ -521,6 +628,7 @@ const useEditor = create<EditorState>()(
521
628
  floorplanPaneRatio: state.floorplanPaneRatio,
522
629
  splitOrientation: state.splitOrientation,
523
630
  floorplanSelectionTool: state.floorplanSelectionTool,
631
+ gridSnapStep: state.gridSnapStep,
524
632
  }),
525
633
  },
526
634
  ),