@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
@@ -4,7 +4,6 @@ import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
6
  type AttachmentSide,
7
- type MaterialSchema,
8
7
  type StairSegmentNode,
9
8
  StairSegmentNode as StairSegmentNodeSchema,
10
9
  type StairSegmentType,
@@ -16,8 +15,6 @@ import { useCallback } from 'react'
16
15
  import { sfxEmitter } from '../../../lib/sfx-bus'
17
16
  import useEditor from '../../../store/use-editor'
18
17
  import { ActionButton, ActionGroup } from '../controls/action-button'
19
- import { MaterialPicker } from '../controls/material-picker'
20
- import { MetricControl } from '../controls/metric-control'
21
18
  import { PanelSection } from '../controls/panel-section'
22
19
  import { SegmentedControl } from '../controls/segmented-control'
23
20
  import { SliderControl } from '../controls/slider-control'
@@ -35,25 +32,24 @@ const ATTACHMENT_SIDE_OPTIONS: { label: string; value: AttachmentSide }[] = [
35
32
  ]
36
33
 
37
34
  export function StairSegmentPanel() {
38
- const selectedIds = useViewer((s) => s.selection.selectedIds)
35
+ const selectedId = useViewer((s) => s.selection.selectedIds[0])
39
36
  const setSelection = useViewer((s) => s.setSelection)
40
- const nodes = useScene((s) => s.nodes)
41
37
  const updateNode = useScene((s) => s.updateNode)
42
38
  const setMovingNode = useEditor((s) => s.setMovingNode)
43
39
 
44
- const selectedId = selectedIds[0]
45
- const node = selectedId
46
- ? (nodes[selectedId as AnyNode['id']] as StairSegmentNode | undefined)
47
- : undefined
40
+ const node = useScene((s) =>
41
+ selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairSegmentNode | undefined) : undefined,
42
+ )
48
43
 
49
- // Check if this is the first segment in the parent stair
50
- const isFirstSegment = (() => {
44
+ // Boolean selector re-renders only when this segment's position among the
45
+ // parent stair's children flips to/from "first".
46
+ const isFirstSegment = useScene((s) => {
51
47
  if (!node?.parentId) return true
52
- const parent = nodes[node.parentId as AnyNodeId]
48
+ const parent = s.nodes[node.parentId as AnyNodeId]
53
49
  if (!parent || parent.type !== 'stair') return true
54
50
  const children = (parent as any).children ?? []
55
51
  return children[0] === node.id
56
- })()
52
+ })
57
53
 
58
54
  const handleUpdate = useCallback(
59
55
  (updates: Partial<StairSegmentNode>) => {
@@ -63,13 +59,6 @@ export function StairSegmentPanel() {
63
59
  [selectedId, updateNode],
64
60
  )
65
61
 
66
- const handleMaterialChange = useCallback(
67
- (material: MaterialSchema) => {
68
- handleUpdate({ material })
69
- },
70
- [handleUpdate],
71
- )
72
-
73
62
  const handleClose = useCallback(() => {
74
63
  setSelection({ selectedIds: [] })
75
64
  }, [setSelection])
@@ -124,7 +113,7 @@ export function StairSegmentPanel() {
124
113
  }
125
114
  }, [selectedId, node, setSelection])
126
115
 
127
- if (!node || node.type !== 'stair-segment' || selectedIds.length !== 1) return null
116
+ if (!(node && node.type === 'stair-segment' && selectedId)) return null
128
117
 
129
118
  return (
130
119
  <PanelWrapper
@@ -243,7 +232,7 @@ export function StairSegmentPanel() {
243
232
  </PanelSection>
244
233
 
245
234
  <PanelSection title="Position">
246
- <MetricControl
235
+ <SliderControl
247
236
  label="X"
248
237
  max={50}
249
238
  min={-50}
@@ -257,7 +246,7 @@ export function StairSegmentPanel() {
257
246
  unit="m"
258
247
  value={Math.round(node.position[0] * 100) / 100}
259
248
  />
260
- <MetricControl
249
+ <SliderControl
261
250
  label="Y"
262
251
  max={50}
263
252
  min={-50}
@@ -271,7 +260,7 @@ export function StairSegmentPanel() {
271
260
  unit="m"
272
261
  value={Math.round(node.position[1] * 100) / 100}
273
262
  />
274
- <MetricControl
263
+ <SliderControl
275
264
  label="Z"
276
265
  max={50}
277
266
  min={-50}
@@ -331,9 +320,6 @@ export function StairSegmentPanel() {
331
320
  />
332
321
  </ActionGroup>
333
322
  </PanelSection>
334
- <PanelSection title="Material">
335
- <MaterialPicker onChange={handleMaterialChange} value={node.material} />
336
- </PanelSection>
337
323
  </PanelWrapper>
338
324
  )
339
325
  }
@@ -3,25 +3,49 @@
3
3
  import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
- type MaterialSchema,
6
+ getClampedWallCurveOffset,
7
+ getMaxWallCurveOffset,
8
+ getWallCurveLength,
9
+ normalizeWallCurveOffset,
7
10
  useScene,
8
11
  type WallNode,
9
12
  } from '@pascal-app/core'
10
13
  import { useViewer } from '@pascal-app/viewer'
14
+ import { Move, Spline } from 'lucide-react'
11
15
  import { useCallback } from 'react'
12
- import { MaterialPicker } from '../controls/material-picker'
16
+ import { sfxEmitter } from '../../../lib/sfx-bus'
17
+ import useEditor from '../../../store/use-editor'
18
+ import { ActionButton, ActionGroup } from '../controls/action-button'
13
19
  import { PanelSection } from '../controls/panel-section'
14
20
  import { SliderControl } from '../controls/slider-control'
15
21
  import { PanelWrapper } from './panel-wrapper'
16
22
 
17
23
  export function WallPanel() {
18
- const selectedIds = useViewer((s) => s.selection.selectedIds)
24
+ const selectedId = useViewer((s) => s.selection.selectedIds[0])
19
25
  const setSelection = useViewer((s) => s.setSelection)
20
- const nodes = useScene((s) => s.nodes)
21
26
  const updateNode = useScene((s) => s.updateNode)
27
+ const setMovingNode = useEditor((s) => s.setMovingNode)
28
+ const setCurvingWall = useEditor((s) => s.setCurvingWall)
22
29
 
23
- const selectedId = selectedIds[0]
24
- const node = selectedId ? (nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined
30
+ const node = useScene((s) =>
31
+ selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined,
32
+ )
33
+
34
+ // Boolean selector — re-renders only when this specific wall's child
35
+ // composition crosses the "has a door/window/wall-item" threshold.
36
+ const hasWallChildrenBlockingCurve = useScene((s) => {
37
+ if (!node) return false
38
+ return (node.children ?? []).some((childId) => {
39
+ const child = s.nodes[childId as AnyNodeId]
40
+ if (!child) return false
41
+ if (child.type === 'door' || child.type === 'window') return true
42
+ if (child.type === 'item') {
43
+ const attachTo = child.asset?.attachTo
44
+ return attachTo === 'wall' || attachTo === 'wall-side'
45
+ }
46
+ return false
47
+ })
48
+ })
25
49
 
26
50
  const handleUpdate = useCallback(
27
51
  (updates: Partial<WallNode>) => {
@@ -55,25 +79,34 @@ export function WallPanel() {
55
79
  [node, handleUpdate],
56
80
  )
57
81
 
58
- const handleMaterialChange = useCallback(
59
- (material: MaterialSchema) => {
60
- handleUpdate({ material })
61
- },
62
- [handleUpdate],
63
- )
64
-
65
82
  const handleClose = useCallback(() => {
66
83
  setSelection({ selectedIds: [] })
67
84
  }, [setSelection])
68
85
 
69
- if (!node || node.type !== 'wall' || selectedIds.length !== 1) return null
86
+ const handleMove = useCallback(() => {
87
+ if (!node) return
88
+ sfxEmitter.emit('sfx:item-pick')
89
+ setMovingNode(node)
90
+ setSelection({ selectedIds: [] })
91
+ }, [node, setMovingNode, setSelection])
92
+
93
+ const handleCurve = useCallback(() => {
94
+ if (!node) return
95
+ sfxEmitter.emit('sfx:item-pick')
96
+ setCurvingWall(node)
97
+ setSelection({ selectedIds: [] })
98
+ }, [node, setCurvingWall, setSelection])
99
+
100
+ if (!(node && node.type === 'wall' && selectedId)) return null
70
101
 
71
102
  const dx = node.end[0] - node.start[0]
72
103
  const dz = node.end[1] - node.start[1]
73
- const length = Math.sqrt(dx * dx + dz * dz)
104
+ const length = getWallCurveLength(node)
74
105
 
75
106
  const height = node.height ?? 2.5
76
107
  const thickness = node.thickness ?? 0.1
108
+ const curveOffset = getClampedWallCurveOffset(node)
109
+ const maxCurveOffset = getMaxWallCurveOffset(node)
77
110
 
78
111
  return (
79
112
  <PanelWrapper
@@ -113,10 +146,31 @@ export function WallPanel() {
113
146
  unit="m"
114
147
  value={Math.round(thickness * 1000) / 1000}
115
148
  />
149
+ {!hasWallChildrenBlockingCurve && (
150
+ <SliderControl
151
+ label="Curve"
152
+ max={Math.max(0.01, maxCurveOffset)}
153
+ min={-Math.max(0.01, maxCurveOffset)}
154
+ onChange={(v) => handleUpdate({ curveOffset: normalizeWallCurveOffset(node, v) })}
155
+ precision={2}
156
+ step={0.1}
157
+ unit="m"
158
+ value={Math.round(curveOffset * 100) / 100}
159
+ />
160
+ )}
116
161
  </PanelSection>
117
162
 
118
- <PanelSection title="Material">
119
- <MaterialPicker onChange={handleMaterialChange} value={node.material} />
163
+ <PanelSection title="Actions">
164
+ <ActionGroup>
165
+ <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
166
+ {!hasWallChildrenBlockingCurve && (
167
+ <ActionButton
168
+ icon={<Spline className="h-3.5 w-3.5" />}
169
+ label="Curve"
170
+ onClick={handleCurve}
171
+ />
172
+ )}
173
+ </ActionGroup>
120
174
  </PanelSection>
121
175
  </PanelWrapper>
122
176
  )