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