@pascal-app/editor 0.4.0 → 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 (97) 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 +341 -48
  4. package/src/components/editor/floating-building-action-menu.tsx +70 -0
  5. package/src/components/editor/floorplan-panel.tsx +1350 -722
  6. package/src/components/editor/index.tsx +221 -167
  7. package/src/components/editor/node-action-menu.tsx +40 -11
  8. package/src/components/editor/selection-manager.tsx +238 -10
  9. package/src/components/editor/site-edge-labels.tsx +9 -3
  10. package/src/components/editor/thumbnail-generator.tsx +422 -79
  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/systems/stair/stair-edit-system.tsx +27 -5
  15. package/src/components/tools/building/move-building-tool.tsx +157 -0
  16. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
  17. package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
  18. package/src/components/tools/door/door-math.ts +1 -1
  19. package/src/components/tools/door/door-tool.tsx +31 -7
  20. package/src/components/tools/door/move-door-tool.tsx +27 -8
  21. package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
  22. package/src/components/tools/fence/fence-drafting.ts +137 -0
  23. package/src/components/tools/fence/fence-tool.tsx +190 -0
  24. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
  25. package/src/components/tools/fence/move-fence-tool.tsx +231 -0
  26. package/src/components/tools/item/item-tool.tsx +3 -3
  27. package/src/components/tools/item/move-tool.tsx +16 -0
  28. package/src/components/tools/item/placement-math.ts +14 -6
  29. package/src/components/tools/item/placement-strategies.ts +17 -9
  30. package/src/components/tools/item/use-placement-coordinator.tsx +123 -16
  31. package/src/components/tools/roof/move-roof-tool.tsx +90 -26
  32. package/src/components/tools/roof/roof-tool.tsx +6 -6
  33. package/src/components/tools/select/box-select-tool.tsx +2 -2
  34. package/src/components/tools/shared/polygon-editor.tsx +98 -8
  35. package/src/components/tools/slab/move-slab-tool.tsx +182 -0
  36. package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
  37. package/src/components/tools/slab/slab-tool.tsx +4 -4
  38. package/src/components/tools/stair/stair-defaults.ts +10 -0
  39. package/src/components/tools/stair/stair-tool.tsx +39 -8
  40. package/src/components/tools/tool-manager.tsx +54 -14
  41. package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
  42. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +322 -0
  43. package/src/components/tools/wall/move-wall-tool.tsx +356 -0
  44. package/src/components/tools/wall/wall-drafting.ts +331 -9
  45. package/src/components/tools/wall/wall-tool.tsx +19 -29
  46. package/src/components/tools/window/move-window-tool.tsx +27 -8
  47. package/src/components/tools/window/window-math.ts +1 -1
  48. package/src/components/tools/window/window-tool.tsx +31 -7
  49. package/src/components/tools/zone/zone-tool.tsx +7 -7
  50. package/src/components/ui/action-menu/control-modes.tsx +9 -4
  51. package/src/components/ui/action-menu/structure-tools.tsx +1 -0
  52. package/src/components/ui/command-palette/editor-commands.tsx +9 -4
  53. package/src/components/ui/command-palette/index.tsx +0 -1
  54. package/src/components/ui/controls/material-picker.tsx +127 -94
  55. package/src/components/ui/controls/slider-control.tsx +28 -14
  56. package/src/components/ui/helpers/building-helper.tsx +32 -0
  57. package/src/components/ui/helpers/helper-manager.tsx +2 -0
  58. package/src/components/ui/item-catalog/catalog-items.tsx +5 -0
  59. package/src/components/ui/panels/ceiling-panel.tsx +61 -17
  60. package/src/components/ui/panels/door-panel.tsx +5 -5
  61. package/src/components/ui/panels/fence-panel.tsx +269 -0
  62. package/src/components/ui/panels/item-panel.tsx +5 -5
  63. package/src/components/ui/panels/panel-manager.tsx +32 -27
  64. package/src/components/ui/panels/reference-panel.tsx +5 -4
  65. package/src/components/ui/panels/roof-panel.tsx +91 -22
  66. package/src/components/ui/panels/roof-segment-panel.tsx +23 -13
  67. package/src/components/ui/panels/slab-panel.tsx +63 -15
  68. package/src/components/ui/panels/stair-panel.tsx +377 -50
  69. package/src/components/ui/panels/stair-segment-panel.tsx +28 -17
  70. package/src/components/ui/panels/wall-panel.tsx +159 -11
  71. package/src/components/ui/panels/window-panel.tsx +5 -7
  72. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +28 -17
  73. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +65 -53
  74. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +40 -25
  75. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +69 -0
  76. package/src/components/ui/sidebar/panels/site-panel/index.tsx +88 -72
  77. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +14 -13
  78. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +64 -53
  79. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +32 -23
  80. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +72 -51
  81. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +40 -37
  82. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +72 -51
  83. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +13 -13
  84. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -17
  85. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +62 -54
  86. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +40 -25
  87. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +27 -28
  88. package/src/components/ui/viewer-toolbar.tsx +55 -2
  89. package/src/components/viewer-overlay.tsx +26 -19
  90. package/src/hooks/use-auto-save.ts +3 -6
  91. package/src/hooks/use-contextual-tools.ts +25 -16
  92. package/src/hooks/use-grid-events.ts +13 -1
  93. package/src/hooks/use-keyboard.ts +7 -2
  94. package/src/index.tsx +2 -1
  95. package/src/lib/history.ts +20 -0
  96. package/src/lib/sfx-player.ts +96 -13
  97. package/src/store/use-editor.tsx +125 -10
@@ -1,22 +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,
9
+ type FenceNode,
7
10
  type ItemNode,
8
11
  type LevelNode,
12
+ type RoofSurfaceMaterialRole,
9
13
  type RoofNode,
10
14
  type RoofSegmentNode,
15
+ type SlabNode,
11
16
  type Space,
17
+ type StairSurfaceMaterialRole,
12
18
  type StairNode,
13
19
  type StairSegmentNode,
14
20
  useScene,
21
+ type WallNode,
22
+ type WallSurfaceSide,
15
23
  type WindowNode,
16
24
  } from '@pascal-app/core'
17
25
  import { useViewer } from '@pascal-app/viewer'
18
26
  import { create } from 'zustand'
19
27
  import { persist } from 'zustand/middleware'
28
+ import { getDefaultCatalogItem } from '../components/ui/item-catalog/catalog-items'
20
29
 
21
30
  const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'site'
22
31
  const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
@@ -33,6 +42,7 @@ export type Mode = 'select' | 'edit' | 'delete' | 'build'
33
42
  // Structure mode tools (building elements)
34
43
  export type StructureTool =
35
44
  | 'wall'
45
+ | 'fence'
36
46
  | 'room'
37
47
  | 'custom-room'
38
48
  | 'slab'
@@ -64,10 +74,28 @@ export type CatalogCategory =
64
74
  export type StructureLayer = 'zones' | 'elements'
65
75
 
66
76
  export type FloorplanSelectionTool = 'click' | 'marquee'
77
+ export type GridSnapStep = 0.5 | 0.25 | 0.1 | 0.05
67
78
 
68
79
  // Combined tool type
69
80
  export type Tool = SiteTool | StructureTool | FurnishTool
70
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
+
71
99
  type EditorState = {
72
100
  phase: Phase
73
101
  setPhase: (phase: Phase) => void
@@ -85,22 +113,42 @@ type EditorState = {
85
113
  | ItemNode
86
114
  | WindowNode
87
115
  | DoorNode
116
+ | FenceNode
117
+ | CeilingNode
118
+ | SlabNode
119
+ | WallNode
88
120
  | RoofNode
89
121
  | RoofSegmentNode
90
122
  | StairNode
91
123
  | StairSegmentNode
124
+ | BuildingNode
92
125
  | null
93
126
  setMovingNode: (
94
127
  node:
95
128
  | ItemNode
96
129
  | WindowNode
97
130
  | DoorNode
131
+ | FenceNode
132
+ | CeilingNode
133
+ | SlabNode
134
+ | WallNode
98
135
  | RoofNode
99
136
  | RoofSegmentNode
100
137
  | StairNode
101
138
  | StairSegmentNode
139
+ | BuildingNode
102
140
  | null,
103
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
104
152
  selectedReferenceId: string | null
105
153
  setSelectedReferenceId: (id: string | null) => void
106
154
  // Space detection for cutaway mode
@@ -125,6 +173,8 @@ type EditorState = {
125
173
  setFloorplanHovered: (hovered: boolean) => void
126
174
  floorplanSelectionTool: FloorplanSelectionTool
127
175
  setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
176
+ gridSnapStep: GridSnapStep
177
+ setGridSnapStep: (step: GridSnapStep) => void
128
178
  // First-person walkthrough mode (street view)
129
179
  isFirstPersonMode: boolean
130
180
  _viewModeBeforeFirstPerson: ViewMode | null
@@ -145,7 +195,11 @@ export type PersistedEditorUiState = Pick<
145
195
 
146
196
  type PersistedEditorLayoutState = Pick<
147
197
  EditorState,
148
- 'activeSidebarPanel' | 'floorplanPaneRatio' | 'splitOrientation' | 'floorplanSelectionTool'
198
+ | 'activeSidebarPanel'
199
+ | 'floorplanPaneRatio'
200
+ | 'splitOrientation'
201
+ | 'floorplanSelectionTool'
202
+ | 'gridSnapStep'
149
203
  >
150
204
  type PersistedEditorState = PersistedEditorUiState & PersistedEditorLayoutState
151
205
 
@@ -164,8 +218,11 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
164
218
  floorplanPaneRatio: DEFAULT_FLOORPLAN_PANE_RATIO,
165
219
  splitOrientation: 'horizontal',
166
220
  floorplanSelectionTool: 'click',
221
+ gridSnapStep: 0.5,
167
222
  }
168
223
 
224
+ const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
225
+
169
226
  function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
170
227
  if (phase === 'site') {
171
228
  return 'select'
@@ -268,6 +325,9 @@ function normalizePersistedEditorLayoutState(
268
325
  floorplanPaneRatio: normalizeFloorplanPaneRatio(state?.floorplanPaneRatio),
269
326
  splitOrientation: state?.splitOrientation === 'vertical' ? 'vertical' : 'horizontal',
270
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,
271
331
  }
272
332
  }
273
333
 
@@ -327,6 +387,10 @@ export function selectDefaultBuildingAndLevel() {
327
387
  }
328
388
  }
329
389
 
390
+ function getDefaultSelectedItemForCategory(category: CatalogCategory | null): AssetInput | null {
391
+ return getDefaultCatalogItem(category)
392
+ }
393
+
330
394
  const useEditor = create<EditorState>()(
331
395
  persist(
332
396
  (set, get) => ({
@@ -348,7 +412,11 @@ const useEditor = create<EditorState>()(
348
412
  } else if (phase === 'structure') {
349
413
  set({ tool: 'wall', catalogCategory: null })
350
414
  } else if (phase === 'furnish') {
351
- set({ tool: 'item', catalogCategory: 'furniture' })
415
+ set({
416
+ tool: 'item',
417
+ catalogCategory: 'furniture',
418
+ selectedItem: getDefaultSelectedItemForCategory('furniture'),
419
+ })
352
420
  }
353
421
  } else {
354
422
  // Reset to select mode and clear tool/catalog when switching phases
@@ -388,8 +456,15 @@ const useEditor = create<EditorState>()(
388
456
  } else if (phase === 'structure' && structureLayer === 'elements') {
389
457
  set({ tool: 'wall' })
390
458
  } else if (phase === 'furnish') {
391
- set({ tool: 'item', catalogCategory: 'furniture' })
459
+ set({
460
+ tool: 'item',
461
+ catalogCategory: 'furniture',
462
+ selectedItem: getDefaultSelectedItemForCategory('furniture'),
463
+ })
392
464
  }
465
+ } else if (phase === 'furnish' && tool === 'item' && !get().selectedItem) {
466
+ const category = get().catalogCategory ?? 'furniture'
467
+ set({ selectedItem: getDefaultSelectedItemForCategory(category) })
393
468
  }
394
469
  }
395
470
  // When leaving build mode, clear tool
@@ -417,19 +492,44 @@ const useEditor = create<EditorState>()(
417
492
  })
418
493
  },
419
494
  catalogCategory: DEFAULT_PERSISTED_EDITOR_UI_STATE.catalogCategory,
420
- 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
+ })),
421
506
  selectedItem: null,
422
507
  setSelectedItem: (item) => set({ selectedItem: item }),
423
508
  movingNode: null as
424
509
  | ItemNode
425
510
  | WindowNode
426
511
  | DoorNode
512
+ | FenceNode
513
+ | CeilingNode
514
+ | SlabNode
515
+ | WallNode
427
516
  | RoofNode
428
517
  | RoofSegmentNode
429
518
  | StairNode
430
519
  | StairSegmentNode
520
+ | BuildingNode
431
521
  | null,
432
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 }),
433
533
  selectedReferenceId: null,
434
534
  setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
435
535
  spaces: {},
@@ -461,6 +561,8 @@ const useEditor = create<EditorState>()(
461
561
  setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
462
562
  floorplanSelectionTool: 'click' as FloorplanSelectionTool,
463
563
  setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
564
+ gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
565
+ setGridSnapStep: (step) => set({ gridSnapStep: step }),
464
566
  allowUndergroundCamera: false,
465
567
  setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
466
568
  isFirstPersonMode: false,
@@ -497,11 +599,23 @@ const useEditor = create<EditorState>()(
497
599
  }),
498
600
  {
499
601
  name: 'pascal-editor-ui-preferences',
500
- merge: (persistedState, currentState) => ({
501
- ...currentState,
502
- ...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
503
- ...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
504
- }),
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
+ },
505
619
  partialize: (state) => ({
506
620
  phase: state.phase,
507
621
  mode: state.mode,
@@ -514,6 +628,7 @@ const useEditor = create<EditorState>()(
514
628
  floorplanPaneRatio: state.floorplanPaneRatio,
515
629
  splitOrientation: state.splitOrientation,
516
630
  floorplanSelectionTool: state.floorplanSelectionTool,
631
+ gridSnapStep: state.gridSnapStep,
517
632
  }),
518
633
  },
519
634
  ),