@pascal-app/editor 0.8.0 → 0.9.1

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 (223) hide show
  1. package/package.json +8 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +754 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +58 -51
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +201 -83
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
  10. package/src/components/editor/first-person-controls.tsx +825 -20
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5829 -12005
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +178 -149
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +433 -156
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +136 -62
  28. package/src/components/editor/thumbnail-generator.tsx +9 -3
  29. package/src/components/editor/use-floorplan-background-placement.ts +119 -32
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  45. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  46. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  47. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  48. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  49. package/src/components/editor-2d/svg-paths.ts +10 -2
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
  51. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  52. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  53. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  54. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  55. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  56. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  57. package/src/components/tools/fence/fence-drafting.ts +64 -22
  58. package/src/components/tools/item/move-tool.tsx +41 -105
  59. package/src/components/tools/item/placement-math.test.ts +10 -0
  60. package/src/components/tools/item/placement-math.ts +31 -5
  61. package/src/components/tools/item/placement-strategies.ts +190 -15
  62. package/src/components/tools/item/placement-types.ts +10 -1
  63. package/src/components/tools/item/use-draft-node.ts +20 -1
  64. package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
  65. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  66. package/src/components/tools/roof/roof-tool.tsx +108 -12
  67. package/src/components/tools/select/box-select-state.ts +77 -0
  68. package/src/components/tools/select/box-select-tool.tsx +324 -538
  69. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  70. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  71. package/src/components/tools/select/select-candidates.test.ts +109 -0
  72. package/src/components/tools/select/select-candidates.ts +85 -0
  73. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  74. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  75. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  76. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  77. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  78. package/src/components/tools/shared/placement-box.tsx +123 -0
  79. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  80. package/src/components/tools/shared/segment-angle.ts +56 -0
  81. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  82. package/src/components/tools/site/site-flag-model.ts +18 -0
  83. package/src/components/tools/stair/stair-defaults.ts +1 -1
  84. package/src/components/tools/stair/stair-tool.tsx +269 -35
  85. package/src/components/tools/tool-manager.tsx +172 -56
  86. package/src/components/tools/wall/wall-drafting.ts +98 -98
  87. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  88. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  89. package/src/components/tools/zone/zone-tool.tsx +16 -6
  90. package/src/components/ui/action-menu/action-button.tsx +21 -1
  91. package/src/components/ui/action-menu/control-modes.tsx +5 -82
  92. package/src/components/ui/action-menu/index.tsx +1 -113
  93. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  94. package/src/components/ui/action-menu/view-toggles.tsx +243 -6
  95. package/src/components/ui/command-palette/editor-commands.tsx +18 -13
  96. package/src/components/ui/command-palette/index.tsx +2 -1
  97. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  98. package/src/components/ui/controls/material-picker.tsx +11 -32
  99. package/src/components/ui/controls/metric-control.tsx +33 -11
  100. package/src/components/ui/floating-level-selector.tsx +47 -10
  101. package/src/components/ui/helpers/helper-manager.tsx +16 -17
  102. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  103. package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
  104. package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
  105. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  106. package/src/components/ui/panels/node-display.ts +1 -0
  107. package/src/components/ui/panels/panel-manager.tsx +35 -47
  108. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  109. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  110. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  111. package/src/components/ui/scene-loader.tsx +1 -3
  112. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  113. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  114. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  115. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  116. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  117. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  118. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  119. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  120. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  121. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  122. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  123. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  125. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  126. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  127. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  128. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
  133. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  134. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  135. package/src/components/viewer-overlay.tsx +169 -52
  136. package/src/components/viewer-zone-system.tsx +1 -1
  137. package/src/hooks/use-auto-save.ts +10 -3
  138. package/src/hooks/use-ceiling-events.ts +175 -0
  139. package/src/hooks/use-drag-action.ts +124 -0
  140. package/src/hooks/use-keyboard.ts +115 -62
  141. package/src/hooks/use-selection.ts +64 -0
  142. package/src/index.tsx +237 -5
  143. package/src/lib/constants.ts +6 -2
  144. package/src/lib/editor-api.ts +62 -0
  145. package/src/lib/elevator-support.ts +96 -0
  146. package/src/lib/floorplan/apply-alignment.ts +111 -0
  147. package/src/lib/floorplan/index.ts +7 -0
  148. package/src/lib/floorplan/selection-tool.ts +40 -0
  149. package/src/lib/floorplan/stairs.ts +12 -1
  150. package/src/lib/fresh-planar-placement.test.ts +102 -0
  151. package/src/lib/fresh-planar-placement.ts +63 -0
  152. package/src/lib/history.ts +2 -1
  153. package/src/lib/level-duplication.ts +54 -42
  154. package/src/lib/level-name.ts +11 -0
  155. package/src/lib/material-paint.ts +211 -19
  156. package/src/lib/placement-metadata.ts +29 -0
  157. package/src/lib/planar-cursor-placement.test.ts +43 -0
  158. package/src/lib/planar-cursor-placement.ts +42 -0
  159. package/src/lib/roof-duplication.ts +1 -1
  160. package/src/lib/scene-clipboard.ts +267 -0
  161. package/src/lib/scene.ts +13 -4
  162. package/src/lib/sfx-bus.ts +23 -1
  163. package/src/lib/sfx-player.ts +70 -22
  164. package/src/lib/site-boundary.ts +1 -0
  165. package/src/lib/stair-levels.test.ts +146 -0
  166. package/src/lib/stair-levels.ts +177 -0
  167. package/src/lib/world-grid-snap.ts +199 -0
  168. package/src/lib/zone-content.ts +130 -0
  169. package/src/store/use-alignment-guides.ts +21 -0
  170. package/src/store/use-editor.tsx +320 -37
  171. package/src/store/use-placement-preview.ts +31 -0
  172. package/src/store/use-wall-move-ghosts.ts +36 -0
  173. package/src/store/use-wall-snap-indicator.ts +32 -0
  174. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  175. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  176. package/src/components/tools/building/move-building-tool.tsx +0 -157
  177. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  178. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  179. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  180. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  181. package/src/components/tools/column/column-tool.tsx +0 -97
  182. package/src/components/tools/column/move-column-tool.tsx +0 -105
  183. package/src/components/tools/door/door-math.ts +0 -110
  184. package/src/components/tools/door/door-tool.tsx +0 -324
  185. package/src/components/tools/door/move-door-tool.tsx +0 -412
  186. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  187. package/src/components/tools/fence/fence-tool.tsx +0 -346
  188. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  189. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  190. package/src/components/tools/item/item-tool.tsx +0 -26
  191. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  192. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  193. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  194. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  195. package/src/components/tools/slab/slab-tool.tsx +0 -322
  196. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  197. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  198. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  199. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  200. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  201. package/src/components/tools/wall/wall-tool.tsx +0 -332
  202. package/src/components/tools/window/move-window-tool.tsx +0 -447
  203. package/src/components/tools/window/window-math.ts +0 -117
  204. package/src/components/tools/window/window-tool.tsx +0 -332
  205. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  206. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  207. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  208. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  209. package/src/components/ui/panels/column-panel.tsx +0 -759
  210. package/src/components/ui/panels/door-panel.tsx +0 -1299
  211. package/src/components/ui/panels/fence-panel.tsx +0 -222
  212. package/src/components/ui/panels/item-panel.tsx +0 -306
  213. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  214. package/src/components/ui/panels/roof-panel.tsx +0 -280
  215. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  216. package/src/components/ui/panels/slab-panel.tsx +0 -250
  217. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  218. package/src/components/ui/panels/stair-panel.tsx +0 -577
  219. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  220. package/src/components/ui/panels/wall-panel.tsx +0 -273
  221. package/src/components/ui/panels/window-panel.tsx +0 -968
  222. package/src/contexts/presets-context.tsx +0 -121
  223. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,759 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- COLUMN_PRESETS,
6
- type ColumnNode,
7
- type ColumnPresetId,
8
- useScene,
9
- } from '@pascal-app/core'
10
- import { useViewer } from '@pascal-app/viewer'
11
- import { Move, Trash2 } from 'lucide-react'
12
- import { useCallback } from 'react'
13
- import { sfxEmitter } from '../../../lib/sfx-bus'
14
- import useEditor from '../../../store/use-editor'
15
- import { ActionButton, ActionGroup } from '../controls/action-button'
16
- import { PanelSection } from '../controls/panel-section'
17
- import { SliderControl } from '../controls/slider-control'
18
- import { PanelWrapper } from './panel-wrapper'
19
-
20
- const SELECT_CLASS =
21
- 'h-10 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm text-foreground outline-none transition-colors hover:bg-[#3e3e3e] focus:ring-1 focus:ring-border'
22
-
23
- const COLUMN_PRESET_OPTIONS = Object.entries(COLUMN_PRESETS).map(([value, preset]) => ({
24
- value: value as ColumnPresetId,
25
- label: preset.label,
26
- }))
27
-
28
- const COLUMN_PROPORTION_PRESETS = {
29
- slender: {
30
- label: 'Slender',
31
- height: 3.6,
32
- width: 0.34,
33
- baseHeight: 0.18,
34
- capitalHeight: 0.16,
35
- baseWidthScale: 1.18,
36
- capitalWidthScale: 1.16,
37
- edgeSoftness: 0.02,
38
- },
39
- standard: {
40
- label: 'Standard',
41
- height: 2.9,
42
- width: 0.44,
43
- baseHeight: 0.22,
44
- capitalHeight: 0.2,
45
- baseWidthScale: 1.24,
46
- capitalWidthScale: 1.22,
47
- edgeSoftness: 0.025,
48
- },
49
- heavy: {
50
- label: 'Heavy',
51
- height: 3,
52
- width: 0.58,
53
- baseHeight: 0.28,
54
- capitalHeight: 0.26,
55
- baseWidthScale: 1.34,
56
- capitalWidthScale: 1.3,
57
- edgeSoftness: 0.035,
58
- },
59
- stout: {
60
- label: 'Short / Stout',
61
- height: 2.2,
62
- width: 0.62,
63
- baseHeight: 0.3,
64
- capitalHeight: 0.28,
65
- baseWidthScale: 1.38,
66
- capitalWidthScale: 1.34,
67
- edgeSoftness: 0.04,
68
- },
69
- } as const
70
-
71
- type ColumnProportionPresetId = keyof typeof COLUMN_PROPORTION_PRESETS
72
-
73
- const COLUMN_PROPORTION_OPTIONS = Object.entries(COLUMN_PROPORTION_PRESETS).map(
74
- ([value, preset]) => ({
75
- value: value as ColumnProportionPresetId,
76
- label: preset.label,
77
- }),
78
- )
79
-
80
- function clamp(value: number, min: number, max: number) {
81
- return Math.min(max, Math.max(min, value))
82
- }
83
-
84
- function presetUpdates(presetId: ColumnPresetId): Partial<ColumnNode> {
85
- const { label, ...preset } = COLUMN_PRESETS[presetId]
86
- return {
87
- name: label,
88
- ...preset,
89
- }
90
- }
91
-
92
- function proportionUpdates(
93
- node: ColumnNode,
94
- presetId: ColumnProportionPresetId,
95
- ): Partial<ColumnNode> {
96
- const preset = COLUMN_PROPORTION_PRESETS[presetId]
97
- const depth =
98
- node.crossSection === 'rectangular'
99
- ? clamp(preset.width * (node.depth / Math.max(node.width, 0.01)), 0.12, 1.6)
100
- : preset.width
101
- const shaftCornerRadius = Math.min(node.shaftCornerRadius ?? 0.035, preset.width * 0.18)
102
-
103
- return {
104
- height: preset.height,
105
- width: preset.width,
106
- depth,
107
- radius: preset.width / 2,
108
- baseHeight: preset.baseHeight,
109
- capitalHeight: preset.capitalHeight,
110
- baseWidthScale: preset.baseWidthScale,
111
- baseDepthScale: preset.baseWidthScale,
112
- capitalWidthScale: preset.capitalWidthScale,
113
- capitalDepthScale: preset.capitalWidthScale,
114
- edgeSoftness: preset.edgeSoftness,
115
- shaftCornerRadius,
116
- }
117
- }
118
-
119
- function shaftProfileUpdates(shaftProfile: ColumnNode['shaftProfile']): Partial<ColumnNode> {
120
- if (shaftProfile === 'tapered') {
121
- return {
122
- shaftProfile,
123
- shaftTaper: 0.14,
124
- shaftBulge: 0,
125
- shaftStartScale: 0.82,
126
- shaftEndScale: 0.72,
127
- shaftSegmentCount: 32,
128
- }
129
- }
130
-
131
- if (shaftProfile === 'bulged') {
132
- return {
133
- shaftProfile,
134
- shaftTaper: 0,
135
- shaftBulge: 0.12,
136
- shaftStartScale: 0.68,
137
- shaftEndScale: 0.68,
138
- shaftSegmentCount: 32,
139
- }
140
- }
141
-
142
- if (shaftProfile === 'hourglass') {
143
- return {
144
- shaftProfile,
145
- shaftTaper: 0,
146
- shaftBulge: 0.12,
147
- shaftStartScale: 0.84,
148
- shaftEndScale: 0.84,
149
- shaftSegmentCount: 32,
150
- }
151
- }
152
-
153
- return {
154
- shaftProfile,
155
- shaftTaper: 0,
156
- shaftBulge: 0,
157
- shaftStartScale: 0.72,
158
- shaftEndScale: 0.72,
159
- shaftSegmentCount: 1,
160
- shaftTwistStep: 0,
161
- }
162
- }
163
-
164
- export function ColumnPanel() {
165
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
166
- const selectedCount = useViewer((s) => s.selection.selectedIds.length)
167
- const setSelection = useViewer((s) => s.setSelection)
168
- const updateNode = useScene((s) => s.updateNode)
169
- const deleteNode = useScene((s) => s.deleteNode)
170
- const setMovingNode = useEditor((s) => s.setMovingNode)
171
-
172
- const node = useScene((s) =>
173
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as ColumnNode | undefined) : undefined,
174
- )
175
-
176
- const handleUpdate = useCallback(
177
- (updates: Partial<ColumnNode>) => {
178
- if (!selectedId) return
179
- updateNode(selectedId as AnyNode['id'], updates)
180
- },
181
- [selectedId, updateNode],
182
- )
183
-
184
- const handleClose = useCallback(() => {
185
- setSelection({ selectedIds: [] })
186
- }, [setSelection])
187
-
188
- const handleDelete = useCallback(() => {
189
- if (!selectedId) return
190
- sfxEmitter.emit('sfx:structure-delete')
191
- deleteNode(selectedId as AnyNode['id'])
192
- setSelection({ selectedIds: [] })
193
- }, [deleteNode, selectedId, setSelection])
194
-
195
- const handleMove = useCallback(() => {
196
- if (!node) return
197
- sfxEmitter.emit('sfx:item-pick')
198
- setMovingNode(node)
199
- setSelection({ selectedIds: [] })
200
- }, [node, setMovingNode, setSelection])
201
-
202
- if (!(node && node.type === 'column' && selectedId && selectedCount === 1)) return null
203
- const shaftProfile = node.shaftProfile ?? 'straight'
204
-
205
- return (
206
- <PanelWrapper
207
- icon="/icons/column.png"
208
- onClose={handleClose}
209
- title={node.name || 'Column'}
210
- width={300}
211
- >
212
- <PanelSection title="Preset">
213
- <select
214
- className={SELECT_CLASS}
215
- onChange={(event) => {
216
- if (!event.target.value) return
217
- handleUpdate(presetUpdates(event.target.value as ColumnPresetId))
218
- }}
219
- value=""
220
- >
221
- <option value="">Apply preset...</option>
222
- {COLUMN_PRESET_OPTIONS.map((option) => (
223
- <option key={option.value} value={option.value}>
224
- {option.label}
225
- </option>
226
- ))}
227
- </select>
228
- </PanelSection>
229
-
230
- <PanelSection title="Shape">
231
- <select
232
- className={SELECT_CLASS}
233
- onChange={(event) =>
234
- handleUpdate({ crossSection: event.target.value as ColumnNode['crossSection'] })
235
- }
236
- value={node.crossSection}
237
- >
238
- <option value="round">Round</option>
239
- <option value="square">Square</option>
240
- <option value="rectangular">Rectangular</option>
241
- </select>
242
- <SliderControl
243
- label="Edge Softness"
244
- max={0.12}
245
- min={0}
246
- onChange={(value) => handleUpdate({ edgeSoftness: value })}
247
- precision={3}
248
- step={0.005}
249
- unit="m"
250
- value={node.edgeSoftness ?? 0.025}
251
- />
252
- {(node.crossSection === 'square' || node.crossSection === 'rectangular') && (
253
- <SliderControl
254
- label="Shaft Corner Radius"
255
- max={0.3}
256
- min={0}
257
- onChange={(value) => handleUpdate({ shaftCornerRadius: value })}
258
- precision={3}
259
- step={0.005}
260
- unit="m"
261
- value={node.shaftCornerRadius ?? 0.035}
262
- />
263
- )}
264
- </PanelSection>
265
-
266
- <PanelSection title="Dimensions">
267
- <select
268
- className={SELECT_CLASS}
269
- onChange={(event) => {
270
- if (!event.target.value) return
271
- handleUpdate(proportionUpdates(node, event.target.value as ColumnProportionPresetId))
272
- }}
273
- value=""
274
- >
275
- <option value="">Apply proportion...</option>
276
- {COLUMN_PROPORTION_OPTIONS.map((option) => (
277
- <option key={option.value} value={option.value}>
278
- {option.label}
279
- </option>
280
- ))}
281
- </select>
282
- <SliderControl
283
- label="Height"
284
- max={6}
285
- min={0.8}
286
- onChange={(value) => handleUpdate({ height: value })}
287
- precision={2}
288
- step={0.05}
289
- unit="m"
290
- value={node.height}
291
- />
292
- <SliderControl
293
- label="Width"
294
- max={1.6}
295
- min={0.12}
296
- onChange={(value) =>
297
- handleUpdate({
298
- width: value,
299
- radius: value / 2,
300
- ...(node.crossSection === 'rectangular' ? {} : { depth: value }),
301
- })
302
- }
303
- precision={2}
304
- step={0.02}
305
- unit="m"
306
- value={node.width}
307
- />
308
- {node.crossSection === 'rectangular' && (
309
- <SliderControl
310
- label="Depth"
311
- max={1.6}
312
- min={0.12}
313
- onChange={(value) => handleUpdate({ depth: value })}
314
- precision={2}
315
- step={0.02}
316
- unit="m"
317
- value={node.depth}
318
- />
319
- )}
320
- </PanelSection>
321
-
322
- <PanelSection title="Shaft">
323
- <select
324
- className={SELECT_CLASS}
325
- onChange={(event) =>
326
- handleUpdate(shaftProfileUpdates(event.target.value as ColumnNode['shaftProfile']))
327
- }
328
- value={shaftProfile}
329
- >
330
- <option value="straight">Straight</option>
331
- <option value="tapered">Tapered</option>
332
- <option value="bulged">Bulged</option>
333
- <option value="hourglass">Hourglass</option>
334
- </select>
335
- {shaftProfile === 'straight' && (
336
- <SliderControl
337
- label="Shaft Width"
338
- max={1.2}
339
- min={0.3}
340
- onChange={(value) => handleUpdate({ shaftStartScale: value, shaftEndScale: value })}
341
- precision={2}
342
- step={0.02}
343
- value={node.shaftStartScale ?? 0.72}
344
- />
345
- )}
346
- {shaftProfile === 'tapered' && (
347
- <>
348
- <SliderControl
349
- label="Bottom Width"
350
- max={1.2}
351
- min={0.3}
352
- onChange={(value) => handleUpdate({ shaftStartScale: value })}
353
- precision={2}
354
- step={0.02}
355
- value={node.shaftStartScale ?? 0.82}
356
- />
357
- <SliderControl
358
- label="Top Width"
359
- max={1.2}
360
- min={0.3}
361
- onChange={(value) => handleUpdate({ shaftEndScale: value })}
362
- precision={2}
363
- step={0.02}
364
- value={node.shaftEndScale ?? 0.72}
365
- />
366
- <SliderControl
367
- label="Taper"
368
- max={0.45}
369
- min={0}
370
- onChange={(value) => handleUpdate({ shaftTaper: value })}
371
- precision={2}
372
- step={0.01}
373
- value={node.shaftTaper ?? 0.14}
374
- />
375
- </>
376
- )}
377
- {shaftProfile === 'bulged' && (
378
- <>
379
- <SliderControl
380
- label="End Width"
381
- max={1.2}
382
- min={0.3}
383
- onChange={(value) => handleUpdate({ shaftStartScale: value, shaftEndScale: value })}
384
- precision={2}
385
- step={0.02}
386
- value={node.shaftStartScale ?? 0.68}
387
- />
388
- <SliderControl
389
- label="Bulge"
390
- max={0.35}
391
- min={0}
392
- onChange={(value) => handleUpdate({ shaftBulge: value })}
393
- precision={2}
394
- step={0.01}
395
- value={node.shaftBulge ?? 0.12}
396
- />
397
- </>
398
- )}
399
- {shaftProfile === 'hourglass' && (
400
- <>
401
- <SliderControl
402
- label="End Width"
403
- max={1.2}
404
- min={0.3}
405
- onChange={(value) => handleUpdate({ shaftStartScale: value, shaftEndScale: value })}
406
- precision={2}
407
- step={0.02}
408
- value={node.shaftStartScale ?? 0.84}
409
- />
410
- <SliderControl
411
- label="Waist"
412
- max={0.35}
413
- min={0}
414
- onChange={(value) => handleUpdate({ shaftBulge: value })}
415
- precision={2}
416
- step={0.01}
417
- value={node.shaftBulge ?? 0.12}
418
- />
419
- </>
420
- )}
421
- <SliderControl
422
- label="Segment Twist"
423
- max={90}
424
- min={-90}
425
- onChange={(value) =>
426
- handleUpdate({
427
- shaftTwistStep: value,
428
- ...(Math.abs(value) > 0.001 && (node.shaftSegmentCount ?? 1) < 8
429
- ? { shaftSegmentCount: 12 }
430
- : {}),
431
- })
432
- }
433
- precision={0}
434
- step={5}
435
- unit="°"
436
- value={node.shaftTwistStep ?? 0}
437
- />
438
- {Math.abs(node.shaftTwistStep ?? 0) > 0.001 && (
439
- <SliderControl
440
- label="Twist Segments"
441
- max={48}
442
- min={4}
443
- onChange={(value) => handleUpdate({ shaftSegmentCount: Math.round(value) })}
444
- precision={0}
445
- step={1}
446
- value={node.shaftSegmentCount ?? 12}
447
- />
448
- )}
449
- <SliderControl
450
- label="Ring Pairs"
451
- max={4}
452
- min={0}
453
- onChange={(value) =>
454
- handleUpdate({
455
- ringCount: Math.round(value) * 2,
456
- ringPlacement: 'ends',
457
- ringSpread: node.ringSpread ?? 0.16,
458
- ringThickness: node.ringThickness ?? 0.055,
459
- })
460
- }
461
- precision={0}
462
- step={1}
463
- value={Math.ceil((node.ringCount ?? 0) / 2)}
464
- />
465
- {(node.ringCount ?? 0) > 0 && (
466
- <SliderControl
467
- label="Ring Thickness"
468
- max={0.14}
469
- min={0.01}
470
- onChange={(value) => handleUpdate({ ringThickness: value })}
471
- precision={3}
472
- step={0.005}
473
- unit="m"
474
- value={node.ringThickness ?? 0.055}
475
- />
476
- )}
477
- {(node.ringCount ?? 0) > 0 && (
478
- <SliderControl
479
- label="Ring Spread"
480
- max={0.45}
481
- min={0.04}
482
- onChange={(value) => handleUpdate({ ringSpread: value, ringPlacement: 'ends' })}
483
- precision={2}
484
- step={0.01}
485
- value={node.ringSpread ?? 0.16}
486
- />
487
- )}
488
- </PanelSection>
489
-
490
- <PanelSection title="Ends">
491
- <select
492
- className={SELECT_CLASS}
493
- onChange={(event) => {
494
- const capitalStyle = event.target.value as ColumnNode['capitalStyle']
495
- handleUpdate({
496
- capitalStyle,
497
- ...(capitalStyle === 'none'
498
- ? {}
499
- : {
500
- capitalHeight: Math.max(node.capitalHeight, 0.12),
501
- capitalTierCount:
502
- capitalStyle === 'stepped'
503
- ? Math.max(node.capitalTierCount ?? 3, 3)
504
- : node.capitalTierCount,
505
- capitalWidthScale: Math.max(
506
- node.capitalWidthScale ?? 1.3,
507
- capitalStyle === 'stepped' ? 1.42 : 1.28,
508
- ),
509
- capitalDepthScale: Math.max(
510
- node.capitalDepthScale ?? 1.3,
511
- capitalStyle === 'stepped' ? 1.42 : 1.28,
512
- ),
513
- capitalStepSpread:
514
- capitalStyle === 'stepped'
515
- ? Math.max(node.capitalStepSpread ?? 0.34, 0.34)
516
- : node.capitalStepSpread,
517
- }),
518
- })
519
- }}
520
- value={node.capitalStyle === 'simple-slab' ? 'simple' : (node.capitalStyle ?? 'simple')}
521
- >
522
- <option value="none">No Top</option>
523
- <option value="simple">Simple Top</option>
524
- <option value="stepped">Stepped Top</option>
525
- <option value="rounded">Rounded Top</option>
526
- </select>
527
- {node.capitalStyle !== 'none' && (
528
- <SliderControl
529
- label="Top Height"
530
- max={0.8}
531
- min={0.06}
532
- onChange={(value) => handleUpdate({ capitalHeight: value })}
533
- precision={2}
534
- step={0.02}
535
- unit="m"
536
- value={node.capitalHeight}
537
- />
538
- )}
539
- {node.capitalStyle !== 'none' && (
540
- <SliderControl
541
- label="Top Width"
542
- max={2.4}
543
- min={0.6}
544
- onChange={(value) =>
545
- handleUpdate({
546
- capitalWidthScale: value,
547
- ...(node.crossSection === 'rectangular' ? {} : { capitalDepthScale: value }),
548
- })
549
- }
550
- precision={2}
551
- step={0.02}
552
- value={node.capitalWidthScale ?? 1.28}
553
- />
554
- )}
555
- {node.capitalStyle !== 'none' && node.crossSection === 'rectangular' && (
556
- <SliderControl
557
- label="Top Depth"
558
- max={2.4}
559
- min={0.6}
560
- onChange={(value) => handleUpdate({ capitalDepthScale: value })}
561
- precision={2}
562
- step={0.02}
563
- value={node.capitalDepthScale ?? node.capitalWidthScale ?? 1.28}
564
- />
565
- )}
566
- {node.capitalStyle === 'stepped' && (
567
- <SliderControl
568
- label="Top Tiers"
569
- max={8}
570
- min={3}
571
- onChange={(value) => handleUpdate({ capitalTierCount: Math.round(value) })}
572
- precision={0}
573
- step={1}
574
- value={node.capitalTierCount ?? 3}
575
- />
576
- )}
577
- {node.capitalStyle === 'stepped' && (
578
- <SliderControl
579
- label="Top Step Spread"
580
- max={0.9}
581
- min={0.05}
582
- onChange={(value) => handleUpdate({ capitalStepSpread: value })}
583
- precision={2}
584
- step={0.01}
585
- value={node.capitalStepSpread ?? 0.34}
586
- />
587
- )}
588
- <select
589
- className={`${SELECT_CLASS} mt-2`}
590
- onChange={(event) => {
591
- const baseStyle = event.target.value as ColumnNode['baseStyle']
592
- handleUpdate({
593
- baseStyle,
594
- ...(baseStyle === 'none'
595
- ? {}
596
- : {
597
- baseHeight: Math.max(node.baseHeight, 0.12),
598
- baseTierCount:
599
- baseStyle === 'stepped-square'
600
- ? Math.max(node.baseTierCount ?? 3, 3)
601
- : node.baseTierCount,
602
- baseWidthScale: Math.max(
603
- node.baseWidthScale ?? 1.24,
604
- baseStyle === 'stepped-square' ? 1.42 : 1.24,
605
- ),
606
- baseDepthScale: Math.max(
607
- node.baseDepthScale ?? 1.24,
608
- baseStyle === 'stepped-square' ? 1.42 : 1.24,
609
- ),
610
- baseStepSpread:
611
- baseStyle === 'stepped-square'
612
- ? Math.max(node.baseStepSpread ?? 0.34, 0.34)
613
- : node.baseStepSpread,
614
- basePlinthHeightRatio:
615
- baseStyle === 'round-rings'
616
- ? (node.basePlinthHeightRatio ?? 0.44)
617
- : node.basePlinthHeightRatio,
618
- baseRoundBandScale:
619
- baseStyle === 'round-rings'
620
- ? (node.baseRoundBandScale ?? 0.92)
621
- : node.baseRoundBandScale,
622
- baseNeckScale:
623
- baseStyle === 'round-rings'
624
- ? (node.baseNeckScale ?? 0.72)
625
- : node.baseNeckScale,
626
- }),
627
- })
628
- }}
629
- value={node.baseStyle ?? 'square-plinth'}
630
- >
631
- <option value="none">No Bottom</option>
632
- <option value="simple-square">Simple Block Bottom</option>
633
- <option value="square-plinth">Square Plinth Bottom</option>
634
- <option value="stepped-square">Stepped Bottom</option>
635
- <option value="round-rings">Rounded Bottom</option>
636
- </select>
637
- {node.baseStyle !== 'none' && (
638
- <SliderControl
639
- label="Bottom Height"
640
- max={0.8}
641
- min={0.06}
642
- onChange={(value) => handleUpdate({ baseHeight: value })}
643
- precision={2}
644
- step={0.02}
645
- unit="m"
646
- value={node.baseHeight}
647
- />
648
- )}
649
- {node.baseStyle !== 'none' && (
650
- <SliderControl
651
- label="Bottom Width"
652
- max={2.4}
653
- min={0.6}
654
- onChange={(value) =>
655
- handleUpdate({
656
- baseWidthScale: value,
657
- ...(node.crossSection === 'rectangular' ? {} : { baseDepthScale: value }),
658
- })
659
- }
660
- precision={2}
661
- step={0.02}
662
- value={node.baseWidthScale ?? 1.24}
663
- />
664
- )}
665
- {node.baseStyle !== 'none' && node.crossSection === 'rectangular' && (
666
- <SliderControl
667
- label="Bottom Depth"
668
- max={2.4}
669
- min={0.6}
670
- onChange={(value) => handleUpdate({ baseDepthScale: value })}
671
- precision={2}
672
- step={0.02}
673
- value={node.baseDepthScale ?? node.baseWidthScale ?? 1.24}
674
- />
675
- )}
676
- {node.baseStyle === 'round-rings' && (
677
- <SliderControl
678
- label="Plinth Thickness"
679
- max={0.7}
680
- min={0.2}
681
- onChange={(value) => handleUpdate({ basePlinthHeightRatio: value })}
682
- precision={2}
683
- step={0.01}
684
- value={node.basePlinthHeightRatio ?? 0.44}
685
- />
686
- )}
687
- {node.baseStyle === 'round-rings' && (
688
- <SliderControl
689
- label="Round Band Width"
690
- max={1.2}
691
- min={0.5}
692
- onChange={(value) => handleUpdate({ baseRoundBandScale: value })}
693
- precision={2}
694
- step={0.01}
695
- value={node.baseRoundBandScale ?? 0.92}
696
- />
697
- )}
698
- {node.baseStyle === 'round-rings' && (
699
- <SliderControl
700
- label="Neck Width"
701
- max={1}
702
- min={0.35}
703
- onChange={(value) => handleUpdate({ baseNeckScale: value })}
704
- precision={2}
705
- step={0.01}
706
- value={node.baseNeckScale ?? 0.72}
707
- />
708
- )}
709
- {node.baseStyle === 'stepped-square' && (
710
- <SliderControl
711
- label="Bottom Tiers"
712
- max={8}
713
- min={3}
714
- onChange={(value) => handleUpdate({ baseTierCount: Math.round(value) })}
715
- precision={0}
716
- step={1}
717
- value={node.baseTierCount ?? 3}
718
- />
719
- )}
720
- {node.baseStyle === 'stepped-square' && (
721
- <SliderControl
722
- label="Bottom Step Spread"
723
- max={0.9}
724
- min={0.05}
725
- onChange={(value) => handleUpdate({ baseStepSpread: value })}
726
- precision={2}
727
- step={0.01}
728
- value={node.baseStepSpread ?? 0.34}
729
- />
730
- )}
731
- </PanelSection>
732
-
733
- <PanelSection title="Transform">
734
- <SliderControl
735
- label="Yaw"
736
- max={180}
737
- min={-180}
738
- onChange={(value) => handleUpdate({ rotation: (value * Math.PI) / 180 })}
739
- precision={0}
740
- step={1}
741
- unit="°"
742
- value={Math.round((node.rotation * 180) / Math.PI)}
743
- />
744
- </PanelSection>
745
-
746
- <PanelSection title="Actions">
747
- <ActionGroup>
748
- <ActionButton icon={<Move className="h-4 w-4" />} label="Move" onClick={handleMove} />
749
- <ActionButton
750
- className="border-red-500/40 text-red-200 hover:bg-red-500/15"
751
- icon={<Trash2 className="h-4 w-4" />}
752
- label="Delete"
753
- onClick={handleDelete}
754
- />
755
- </ActionGroup>
756
- </PanelSection>
757
- </PanelWrapper>
758
- )
759
- }