@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
@@ -16,7 +16,6 @@ import { sfxEmitter } from '../../../lib/sfx-bus'
16
16
  import useEditor from '../../../store/use-editor'
17
17
  import { ActionButton, ActionGroup } from '../controls/action-button'
18
18
  import { MaterialPicker } from '../controls/material-picker'
19
- import { MetricControl } from '../controls/metric-control'
20
19
  import { PanelSection } from '../controls/panel-section'
21
20
  import { SegmentedControl } from '../controls/segmented-control'
22
21
  import { SliderControl } from '../controls/slider-control'
@@ -36,16 +35,14 @@ const ROOF_TYPE_OPTIONS_2: { label: string; value: RoofType }[] = [
36
35
  ]
37
36
 
38
37
  export function RoofSegmentPanel() {
39
- const selectedIds = useViewer((s) => s.selection.selectedIds)
38
+ const selectedId = useViewer((s) => s.selection.selectedIds[0])
40
39
  const setSelection = useViewer((s) => s.setSelection)
41
- const nodes = useScene((s) => s.nodes)
42
40
  const updateNode = useScene((s) => s.updateNode)
43
41
  const setMovingNode = useEditor((s) => s.setMovingNode)
44
42
 
45
- const selectedId = selectedIds[0]
46
- const node = selectedId
47
- ? (nodes[selectedId as AnyNode['id']] as RoofSegmentNode | undefined)
48
- : undefined
43
+ const node = useScene((s) =>
44
+ selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofSegmentNode | undefined) : undefined,
45
+ )
49
46
 
50
47
  const handleUpdate = useCallback(
51
48
  (updates: Partial<RoofSegmentNode>) => {
@@ -57,7 +54,14 @@ export function RoofSegmentPanel() {
57
54
 
58
55
  const handleMaterialChange = useCallback(
59
56
  (material: MaterialSchema) => {
60
- handleUpdate({ material })
57
+ handleUpdate({ material, materialPreset: undefined })
58
+ },
59
+ [handleUpdate],
60
+ )
61
+
62
+ const handleMaterialPresetChange = useCallback(
63
+ (materialPreset: string) => {
64
+ handleUpdate({ materialPreset, material: undefined })
61
65
  },
62
66
  [handleUpdate],
63
67
  )
@@ -117,7 +121,7 @@ export function RoofSegmentPanel() {
117
121
  }
118
122
  }, [selectedId, node, setSelection])
119
123
 
120
- if (!node || node.type !== 'roof-segment' || selectedIds.length !== 1) return null
124
+ if (!(node && node.type === 'roof-segment' && selectedId)) return null
121
125
 
122
126
  return (
123
127
  <PanelWrapper
@@ -230,7 +234,7 @@ export function RoofSegmentPanel() {
230
234
  </PanelSection>
231
235
 
232
236
  <PanelSection title="Position">
233
- <MetricControl
237
+ <SliderControl
234
238
  label="X"
235
239
  max={50}
236
240
  min={-50}
@@ -244,7 +248,7 @@ export function RoofSegmentPanel() {
244
248
  unit="m"
245
249
  value={Math.round(node.position[0] * 100) / 100}
246
250
  />
247
- <MetricControl
251
+ <SliderControl
248
252
  label="Y"
249
253
  max={50}
250
254
  min={-50}
@@ -258,7 +262,7 @@ export function RoofSegmentPanel() {
258
262
  unit="m"
259
263
  value={Math.round(node.position[1] * 100) / 100}
260
264
  />
261
- <MetricControl
265
+ <SliderControl
262
266
  label="Z"
263
267
  max={50}
264
268
  min={-50}
@@ -319,7 +323,13 @@ export function RoofSegmentPanel() {
319
323
  </ActionGroup>
320
324
  </PanelSection>
321
325
  <PanelSection title="Material">
322
- <MaterialPicker onChange={handleMaterialChange} value={node.material} />
326
+ <MaterialPicker
327
+ nodeType="roof-segment"
328
+ onChange={handleMaterialChange}
329
+ onSelectMaterialPreset={handleMaterialPresetChange}
330
+ selectedMaterialPreset={node.materialPreset}
331
+ value={node.material}
332
+ />
323
333
  </PanelSection>
324
334
  </PanelWrapper>
325
335
  )
@@ -2,8 +2,9 @@
2
2
 
3
3
  import { type AnyNode, type MaterialSchema, type SlabNode, useScene } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
- import { Edit, Plus, Trash2 } from 'lucide-react'
5
+ import { Edit, Move, Plus, Trash2 } from 'lucide-react'
6
6
  import { useCallback, useEffect } from 'react'
7
+ import { sfxEmitter } from '../../../lib/sfx-bus'
7
8
  import useEditor from '../../../store/use-editor'
8
9
  import { ActionButton, ActionGroup } from '../controls/action-button'
9
10
  import { MaterialPicker } from '../controls/material-picker'
@@ -12,15 +13,16 @@ import { SliderControl } from '../controls/slider-control'
12
13
  import { PanelWrapper } from './panel-wrapper'
13
14
 
14
15
  export function SlabPanel() {
15
- const selectedIds = useViewer((s) => s.selection.selectedIds)
16
+ const selectedId = useViewer((s) => s.selection.selectedIds[0])
16
17
  const setSelection = useViewer((s) => s.setSelection)
17
- const nodes = useScene((s) => s.nodes)
18
18
  const updateNode = useScene((s) => s.updateNode)
19
19
  const editingHole = useEditor((s) => s.editingHole)
20
20
  const setEditingHole = useEditor((s) => s.setEditingHole)
21
+ const setMovingNode = useEditor((s) => s.setMovingNode)
21
22
 
22
- const selectedId = selectedIds[0]
23
- const node = selectedId ? (nodes[selectedId as AnyNode['id']] as SlabNode | undefined) : undefined
23
+ const node = useScene((s) =>
24
+ selectedId ? (s.nodes[selectedId as AnyNode['id']] as SlabNode | undefined) : undefined,
25
+ )
24
26
 
25
27
  const handleUpdate = useCallback(
26
28
  (updates: Partial<SlabNode>) => {
@@ -30,9 +32,16 @@ export function SlabPanel() {
30
32
  [selectedId, updateNode],
31
33
  )
32
34
 
33
- const handleMaterialChange = useCallback(
35
+ const handleMaterialPresetChange = useCallback(
36
+ (materialPreset: string) => {
37
+ handleUpdate({ materialPreset, material: undefined })
38
+ },
39
+ [handleUpdate],
40
+ )
41
+
42
+ const handleCustomMaterialChange = useCallback(
34
43
  (material: MaterialSchema) => {
35
- handleUpdate({ material })
44
+ handleUpdate({ material, materialPreset: undefined })
36
45
  },
37
46
  [handleUpdate],
38
47
  )
@@ -75,7 +84,13 @@ export function SlabPanel() {
75
84
  [cx - holeSize, cz + holeSize],
76
85
  ]
77
86
  const currentHoles = node?.holes || []
78
- handleUpdate({ holes: [...currentHoles, newHole] })
87
+ const currentMetadata = currentHoles.map(
88
+ (_, index) => node?.holeMetadata?.[index] ?? { source: 'manual' as const },
89
+ )
90
+ handleUpdate({
91
+ holes: [...currentHoles, newHole],
92
+ holeMetadata: [...currentMetadata, { source: 'manual' }],
93
+ })
79
94
  setEditingHole({ nodeId: selectedId, holeIndex: currentHoles.length })
80
95
  }, [node, selectedId, handleUpdate, setEditingHole])
81
96
 
@@ -91,16 +106,28 @@ export function SlabPanel() {
91
106
  (index: number) => {
92
107
  if (!selectedId) return
93
108
  const currentHoles = node?.holes || []
109
+ if (node?.holeMetadata?.[index]?.source === 'stair') return
94
110
  const newHoles = currentHoles.filter((_, i) => i !== index)
95
- handleUpdate({ holes: newHoles })
111
+ const currentMetadata = currentHoles.map(
112
+ (_, metadataIndex) => node?.holeMetadata?.[metadataIndex] ?? { source: 'manual' as const },
113
+ )
114
+ const newMetadata = currentMetadata.filter((_, i) => i !== index)
115
+ handleUpdate({ holes: newHoles, holeMetadata: newMetadata })
96
116
  if (editingHole?.nodeId === selectedId && editingHole?.holeIndex === index) {
97
117
  setEditingHole(null)
98
118
  }
99
119
  },
100
- [selectedId, node?.holes, handleUpdate, editingHole, setEditingHole],
120
+ [selectedId, node?.holes, node?.holeMetadata, handleUpdate, editingHole, setEditingHole],
101
121
  )
102
122
 
103
- if (!node || node.type !== 'slab' || selectedIds.length !== 1) return null
123
+ const handleMove = useCallback(() => {
124
+ if (!node) return
125
+ sfxEmitter.emit('sfx:item-pick')
126
+ setMovingNode(node)
127
+ setSelection({ selectedIds: [] })
128
+ }, [node, setMovingNode, setSelection])
129
+
130
+ if (!(node && node.type === 'slab' && selectedId)) return null
104
131
 
105
132
  const calculateArea = (polygon: Array<[number, number]>): number => {
106
133
  if (polygon.length < 3) return 0
@@ -108,8 +135,11 @@ export function SlabPanel() {
108
135
  const n = polygon.length
109
136
  for (let i = 0; i < n; i++) {
110
137
  const j = (i + 1) % n
111
- area += polygon[i]?.[0] * polygon[j]?.[1]
112
- area -= polygon[j]?.[0] * polygon[i]?.[1]
138
+ const current = polygon[i]
139
+ const next = polygon[j]
140
+ if (!(current && next)) continue
141
+ area += current[0] * next[1]
142
+ area -= next[0] * current[1]
113
143
  }
114
144
  return Math.abs(area) / 2
115
145
  }
@@ -157,6 +187,8 @@ export function SlabPanel() {
157
187
  const holeArea = calculateArea(hole)
158
188
  const isEditing =
159
189
  editingHole?.nodeId === selectedId && editingHole?.holeIndex === index
190
+ const source = node.holeMetadata?.[index]?.source ?? 'manual'
191
+ const isAutoHole = source === 'stair'
160
192
  return (
161
193
  <div
162
194
  className={`flex items-center justify-between rounded-lg border p-2 transition-colors ${
@@ -173,7 +205,8 @@ export function SlabPanel() {
173
205
  Hole {index + 1} {isEditing && '(Editing)'}
174
206
  </p>
175
207
  <p className="text-[10px] text-muted-foreground">
176
- {holeArea.toFixed(2)} m² · {hole.length} pts
208
+ {holeArea.toFixed(2)} m² · {hole.length} pts ·{' '}
209
+ {isAutoHole ? 'Auto stair cutout' : 'Manual'}
177
210
  </p>
178
211
  </div>
179
212
  <div className="flex items-center gap-1">
@@ -183,6 +216,10 @@ export function SlabPanel() {
183
216
  label="Done"
184
217
  onClick={() => setEditingHole(null)}
185
218
  />
219
+ ) : isAutoHole ? (
220
+ <div className="rounded-md bg-[#2C2C2E] px-2 py-1 text-[10px] text-muted-foreground">
221
+ Auto
222
+ </div>
186
223
  ) : (
187
224
  <>
188
225
  <button
@@ -221,7 +258,18 @@ export function SlabPanel() {
221
258
  </div>
222
259
  </PanelSection>
223
260
  <PanelSection title="Material">
224
- <MaterialPicker onChange={handleMaterialChange} value={node.material} />
261
+ <MaterialPicker
262
+ nodeType="slab"
263
+ onChange={handleCustomMaterialChange}
264
+ onSelectMaterialPreset={handleMaterialPresetChange}
265
+ selectedMaterialPreset={node.materialPreset}
266
+ value={node.material}
267
+ />
268
+ </PanelSection>
269
+ <PanelSection title="Actions">
270
+ <ActionGroup>
271
+ <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
272
+ </ActionGroup>
225
273
  </PanelSection>
226
274
  </PanelWrapper>
227
275
  )