@pascal-app/editor 0.6.0 → 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 (122) hide show
  1. package/package.json +9 -5
  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 +20 -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 +32 -55
  10. package/src/components/editor/floorplan-background-selection.ts +113 -0
  11. package/src/components/editor/floorplan-panel.tsx +9855 -3298
  12. package/src/components/editor/index.tsx +269 -21
  13. package/src/components/editor/selection-manager.tsx +575 -13
  14. package/src/components/editor/thumbnail-generator.tsx +38 -7
  15. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  16. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  17. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  18. package/src/components/editor/wall-measurement-label.tsx +267 -36
  19. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  20. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  21. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  22. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
  23. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  24. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
  25. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  26. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  27. package/src/components/editor-2d/svg-paths.ts +119 -0
  28. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  29. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
  30. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  31. package/src/components/tools/column/column-tool.tsx +97 -0
  32. package/src/components/tools/column/move-column-tool.tsx +105 -0
  33. package/src/components/tools/door/door-tool.tsx +7 -0
  34. package/src/components/tools/door/move-door-tool.tsx +28 -8
  35. package/src/components/tools/fence/fence-drafting.ts +10 -3
  36. package/src/components/tools/fence/fence-tool.tsx +159 -3
  37. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
  38. package/src/components/tools/fence/move-fence-tool.tsx +101 -34
  39. package/src/components/tools/item/move-tool.tsx +10 -1
  40. package/src/components/tools/item/placement-math.ts +30 -1
  41. package/src/components/tools/item/placement-strategies.ts +109 -31
  42. package/src/components/tools/item/placement-types.ts +7 -0
  43. package/src/components/tools/item/use-draft-node.ts +2 -0
  44. package/src/components/tools/item/use-placement-coordinator.tsx +660 -52
  45. package/src/components/tools/roof/move-roof-tool.tsx +22 -15
  46. package/src/components/tools/shared/polygon-editor.tsx +153 -28
  47. package/src/components/tools/shared/segment-angle.ts +156 -0
  48. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  49. package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
  50. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  51. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  52. package/src/components/tools/tool-manager.tsx +18 -3
  53. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
  54. package/src/components/tools/wall/wall-drafting.ts +18 -9
  55. package/src/components/tools/wall/wall-tool.tsx +134 -2
  56. package/src/components/tools/window/move-window-tool.tsx +18 -0
  57. package/src/components/tools/window/window-tool.tsx +5 -0
  58. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  59. package/src/components/ui/action-menu/control-modes.tsx +28 -1
  60. package/src/components/ui/action-menu/index.tsx +91 -1
  61. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +424 -35
  63. package/src/components/ui/command-palette/editor-commands.tsx +18 -1
  64. package/src/components/ui/controls/material-picker.tsx +152 -165
  65. package/src/components/ui/controls/slider-control.tsx +66 -18
  66. package/src/components/ui/floating-level-selector.tsx +286 -55
  67. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  68. package/src/components/ui/item-catalog/catalog-items.tsx +1116 -1219
  69. package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
  70. package/src/components/ui/level-duplicate-dialog.tsx +115 -0
  71. package/src/components/ui/panels/ceiling-panel.tsx +1 -25
  72. package/src/components/ui/panels/column-panel.tsx +715 -0
  73. package/src/components/ui/panels/door-panel.tsx +981 -289
  74. package/src/components/ui/panels/fence-panel.tsx +3 -45
  75. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  76. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  77. package/src/components/ui/panels/node-display.ts +39 -0
  78. package/src/components/ui/panels/paint-panel.tsx +138 -0
  79. package/src/components/ui/panels/panel-manager.tsx +210 -1
  80. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  81. package/src/components/ui/panels/reference-panel.tsx +238 -5
  82. package/src/components/ui/panels/roof-panel.tsx +4 -105
  83. package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
  84. package/src/components/ui/panels/slab-panel.tsx +4 -30
  85. package/src/components/ui/panels/spawn-panel.tsx +155 -0
  86. package/src/components/ui/panels/stair-panel.tsx +11 -117
  87. package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
  88. package/src/components/ui/panels/wall-panel.tsx +1 -95
  89. package/src/components/ui/panels/window-panel.tsx +660 -139
  90. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  91. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  92. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
  93. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  94. package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
  95. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
  96. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
  97. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
  98. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
  99. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  100. package/src/components/ui/viewer-toolbar.tsx +42 -1
  101. package/src/hooks/use-auto-frame.ts +45 -0
  102. package/src/hooks/use-keyboard.ts +64 -7
  103. package/src/hooks/use-mobile.ts +12 -12
  104. package/src/lib/door-interaction.ts +88 -0
  105. package/src/lib/floorplan/geometry.ts +263 -0
  106. package/src/lib/floorplan/index.ts +38 -0
  107. package/src/lib/floorplan/items.ts +179 -0
  108. package/src/lib/floorplan/selection-tool.ts +231 -0
  109. package/src/lib/floorplan/stairs.ts +478 -0
  110. package/src/lib/floorplan/types.ts +57 -0
  111. package/src/lib/floorplan/walls.ts +23 -0
  112. package/src/lib/guide-events.ts +10 -0
  113. package/src/lib/level-duplication.test.ts +72 -0
  114. package/src/lib/level-duplication.ts +153 -0
  115. package/src/lib/local-guide-image.ts +42 -0
  116. package/src/lib/material-paint.ts +284 -0
  117. package/src/lib/roof-duplication.ts +214 -0
  118. package/src/lib/scene-bounds.test.ts +183 -0
  119. package/src/lib/scene-bounds.ts +169 -0
  120. package/src/lib/stair-duplication.ts +126 -0
  121. package/src/lib/window-interaction.ts +86 -0
  122. package/src/store/use-editor.tsx +164 -8
@@ -4,6 +4,7 @@ import {
4
4
  isCurvedWall,
5
5
  sceneRegistry,
6
6
  spatialGridManager,
7
+ useLiveTransforms,
7
8
  useScene,
8
9
  type WallEvent,
9
10
  WindowNode,
@@ -144,6 +145,10 @@ export const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWin
144
145
  parentId: event.node.id,
145
146
  wallId: event.node.id,
146
147
  })
148
+ useLiveTransforms.getState().set(movingWindowNode.id, {
149
+ position: [clampedX, clampedY, 0],
150
+ rotation: itemRotation,
151
+ })
147
152
 
148
153
  if (prevWallId && prevWallId !== event.node.id) markWallDirty(prevWallId)
149
154
  markWallDirty(event.node.id)
@@ -215,6 +220,10 @@ export const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWin
215
220
  windowMesh.updateMatrixWorld(true)
216
221
  }
217
222
  }
223
+ useLiveTransforms.getState().set(movingWindowNode.id, {
224
+ position: [clampedX, clampedY, 0],
225
+ rotation: itemRotation,
226
+ })
218
227
  markWallDirty(event.node.id)
219
228
 
220
229
  const valid = !hasWallChildOverlap(
@@ -285,6 +294,11 @@ export const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWin
285
294
  parentId: event.node.id,
286
295
  width: movingWindowNode.width,
287
296
  height: movingWindowNode.height,
297
+ windowType: movingWindowNode.windowType,
298
+ operationState: movingWindowNode.operationState,
299
+ awningDirection: movingWindowNode.awningDirection,
300
+ casementStyle: movingWindowNode.casementStyle,
301
+ hingesSide: movingWindowNode.hingesSide,
288
302
  frameThickness: movingWindowNode.frameThickness,
289
303
  frameDepth: movingWindowNode.frameDepth,
290
304
  columnRatios: movingWindowNode.columnRatios,
@@ -326,6 +340,7 @@ export const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWin
326
340
  }
327
341
 
328
342
  markWallDirty(event.node.id)
343
+ useLiveTransforms.getState().clear(movingWindowNode.id)
329
344
  useScene.temporal.getState().pause()
330
345
 
331
346
  sfxEmitter.emit('sfx:item-place')
@@ -337,6 +352,7 @@ export const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWin
337
352
 
338
353
  const onWallLeave = () => {
339
354
  hideCursor()
355
+ useLiveTransforms.getState().clear(movingWindowNode.id)
340
356
  if (isNew) return // No original to restore for duplicates
341
357
  // Move mode: restore to original position while off-wall
342
358
  if (currentWallId && currentWallId !== original.parentId) {
@@ -354,6 +370,7 @@ export const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWin
354
370
  }
355
371
 
356
372
  const onCancel = () => {
373
+ useLiveTransforms.getState().clear(movingWindowNode.id)
357
374
  if (isNew) {
358
375
  useScene.getState().deleteNode(movingWindowNode.id)
359
376
  if (currentWallId) markWallDirty(currentWallId)
@@ -401,6 +418,7 @@ export const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWin
401
418
  if (original.parentId) markWallDirty(original.parentId)
402
419
  }
403
420
  }
421
+ useLiveTransforms.getState().clear(movingWindowNode.id)
404
422
  useScene.temporal.getState().resume()
405
423
  emitter.off('wall:enter', onWallEnter)
406
424
  emitter.off('wall:move', onWallMove)
@@ -262,6 +262,11 @@ export const WindowTool: React.FC = () => {
262
262
  parentId: event.node.id,
263
263
  width: draft.width,
264
264
  height: draft.height,
265
+ windowType: draft.windowType,
266
+ operationState: draft.operationState,
267
+ awningDirection: draft.awningDirection,
268
+ casementStyle: draft.casementStyle,
269
+ hingesSide: draft.hingesSide,
265
270
  frameThickness: draft.frameThickness,
266
271
  frameDepth: draft.frameDepth,
267
272
  columnRatios: draft.columnRatios,
@@ -4,7 +4,7 @@ import { emitter } from '@pascal-app/core'
4
4
  import Image from 'next/image'
5
5
  import { ActionButton } from './action-button'
6
6
 
7
- export function CameraActions() {
7
+ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
8
8
  const goToTopView = () => {
9
9
  emitter.emit('camera-controls:top-view')
10
10
  }
@@ -19,39 +19,43 @@ export function CameraActions() {
19
19
 
20
20
  return (
21
21
  <div className="flex items-center gap-1">
22
- {/* Orbit CCW */}
23
- <ActionButton
24
- className="group hover:bg-white/5"
25
- label="Orbit Left"
26
- onClick={orbitCCW}
27
- size="icon"
28
- variant="ghost"
29
- >
30
- <Image
31
- alt="Orbit Left"
32
- className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
33
- height={28}
34
- src="/icons/rotate.png"
35
- width={28}
36
- />
37
- </ActionButton>
22
+ {!hideOrbit && (
23
+ <>
24
+ {/* Orbit CCW */}
25
+ <ActionButton
26
+ className="group hover:bg-white/5"
27
+ label="Orbit Left"
28
+ onClick={orbitCCW}
29
+ size="icon"
30
+ variant="ghost"
31
+ >
32
+ <Image
33
+ alt="Orbit Left"
34
+ className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
35
+ height={28}
36
+ src="/icons/rotate.png"
37
+ width={28}
38
+ />
39
+ </ActionButton>
38
40
 
39
- {/* Orbit CW */}
40
- <ActionButton
41
- className="group hover:bg-white/5"
42
- label="Orbit Right"
43
- onClick={orbitCW}
44
- size="icon"
45
- variant="ghost"
46
- >
47
- <Image
48
- alt="Orbit Right"
49
- className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
50
- height={28}
51
- src="/icons/rotate.png"
52
- width={28}
53
- />
54
- </ActionButton>
41
+ {/* Orbit CW */}
42
+ <ActionButton
43
+ className="group hover:bg-white/5"
44
+ label="Orbit Right"
45
+ onClick={orbitCW}
46
+ size="icon"
47
+ variant="ghost"
48
+ >
49
+ <Image
50
+ alt="Orbit Right"
51
+ className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
52
+ height={28}
53
+ src="/icons/rotate.png"
54
+ width={28}
55
+ />
56
+ </ActionButton>
57
+ </>
58
+ )}
55
59
 
56
60
  {/* Top View */}
57
61
  <ActionButton
@@ -9,7 +9,15 @@ import { cn } from './../../../lib/utils'
9
9
  import useEditor from './../../../store/use-editor'
10
10
  import { ActionButton } from './action-button'
11
11
 
12
- type ControlId = 'select' | 'box-select' | 'site-edit' | 'build' | 'furnish' | 'zone' | 'delete'
12
+ type ControlId =
13
+ | 'select'
14
+ | 'box-select'
15
+ | 'site-edit'
16
+ | 'build'
17
+ | 'material-paint'
18
+ | 'furnish'
19
+ | 'zone'
20
+ | 'delete'
13
21
 
14
22
  type ControlConfig = {
15
23
  id: ControlId
@@ -54,6 +62,14 @@ const controls: ControlConfig[] = [
54
62
  color: 'hover:bg-green-500/20 hover:text-green-400',
55
63
  activeColor: 'bg-green-500/20 text-green-400',
56
64
  },
65
+ {
66
+ id: 'material-paint',
67
+ imageSrc: '/icons/paint.png',
68
+ label: 'Material Paint',
69
+ shortcut: 'P',
70
+ color: 'hover:bg-amber-500/20 hover:text-amber-400',
71
+ activeColor: 'bg-amber-500/20 text-amber-400',
72
+ },
57
73
  {
58
74
  id: 'furnish',
59
75
  imageSrc: '/icons/couch.png',
@@ -88,6 +104,7 @@ export function ControlModes() {
88
104
  const setPhase = useEditor((state) => state.setPhase)
89
105
  const setStructureLayer = useEditor((state) => state.setStructureLayer)
90
106
  const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
107
+ const primeMaterialPaintFromSelection = useEditor((state) => state.primeMaterialPaintFromSelection)
91
108
  const levelId = useViewer((s) => s.selection.levelId)
92
109
 
93
110
  // Only subscribe to the primitive `level` number — when walls are added to
@@ -112,6 +129,7 @@ export function ControlModes() {
112
129
  if (id === 'site-edit') return false
113
130
  if (id === 'build')
114
131
  return mode === 'build' && phase === 'structure' && structureLayer === 'elements'
132
+ if (id === 'material-paint') return mode === 'material-paint'
115
133
  if (id === 'furnish') return mode === 'build' && phase === 'furnish'
116
134
  if (id === 'zone')
117
135
  return mode === 'build' && phase === 'structure' && structureLayer === 'zones'
@@ -155,6 +173,15 @@ export function ControlModes() {
155
173
  setStructureLayer('elements')
156
174
  setMode('build')
157
175
  }
176
+ } else if (id === 'material-paint') {
177
+ if (getIsActive('material-paint')) {
178
+ setMode('select')
179
+ } else {
180
+ primeMaterialPaintFromSelection()
181
+ setPhase('structure')
182
+ setStructureLayer('elements')
183
+ setMode('material-paint')
184
+ }
158
185
  } else if (id === 'furnish') {
159
186
  if (getIsActive('furnish')) {
160
187
  setMode('select')
@@ -1,8 +1,14 @@
1
1
  'use client'
2
2
 
3
+ import { useScene } from '@pascal-app/core'
3
4
  import { AnimatePresence, motion } from 'motion/react'
4
- import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
5
+ import { useEffect, useMemo } from 'react'
6
+ import { useViewer } from '@pascal-app/viewer'
5
7
  import { useReducedMotion } from './../../../hooks/use-reduced-motion'
8
+ import { useIsMobile } from './../../../hooks/use-mobile'
9
+ import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
10
+ import { MaterialPicker } from './../../../components/ui/controls/material-picker'
11
+ import { resolvePaintTargetFromSelection } from './../../../lib/material-paint'
6
12
  import { cn } from './../../../lib/utils'
7
13
  import useEditor from './../../../store/use-editor'
8
14
  import { ItemCatalog } from '../item-catalog/item-catalog'
@@ -12,12 +18,64 @@ import { FurnishTools } from './furnish-tools'
12
18
  import { StructureTools } from './structure-tools'
13
19
  import { ViewToggles } from './view-toggles'
14
20
 
21
+ function PaintMaterialTray() {
22
+ const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
23
+ const activePaintTarget = useEditor((state) => state.activePaintTarget)
24
+ const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
25
+ const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
26
+ const selectedIds = useViewer((state) => state.selection.selectedIds)
27
+ const nodes = useScene((state) => state.nodes)
28
+ const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null
29
+
30
+ useEffect(() => {
31
+ const selectedPaintTarget = resolvePaintTargetFromSelection({
32
+ nodes,
33
+ selectedId,
34
+ })
35
+
36
+ if (selectedPaintTarget) {
37
+ setActivePaintTarget(selectedPaintTarget)
38
+ }
39
+ }, [nodes, selectedId, setActivePaintTarget])
40
+
41
+ return (
42
+ <div className="w-[42rem] max-w-[calc(100vw-2rem)]">
43
+ <MaterialPicker
44
+ onChange={(material) => {
45
+ setActivePaintMaterial({ material, sourceTarget: activePaintTarget })
46
+ }}
47
+ onSelectMaterialPreset={(materialPreset) => {
48
+ setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
49
+ }}
50
+ selectedMaterialPreset={activePaintMaterial?.materialPreset}
51
+ value={activePaintMaterial?.material}
52
+ />
53
+ </div>
54
+ )
55
+ }
56
+
15
57
  export function ActionMenu({ className }: { className?: string }) {
16
58
  const phase = useEditor((state) => state.phase)
17
59
  const mode = useEditor((state) => state.mode)
18
60
  const tool = useEditor((state) => state.tool)
19
61
  const catalogCategory = useEditor((state) => state.catalogCategory)
62
+ const isMobile = useIsMobile()
63
+ const hasSelectionOnMobile = useViewer((s) => isMobile && s.selection.selectedIds.length > 0)
64
+ const hasReferenceOnMobile = useEditor((s) => isMobile && Boolean(s.selectedReferenceId))
65
+ const CONTEXTUAL_TABS = new Set(['ai', 'items', 'studio'])
66
+ const isContextualPanelOnMobile = useEditor(
67
+ (s) => isMobile && CONTEXTUAL_TABS.has(s.activeSidebarPanel),
68
+ )
20
69
  const reducedMotion = useReducedMotion()
70
+ const showPaintTray = useMemo(() => mode === 'material-paint', [mode])
71
+
72
+ // On mobile, defer the bottom rail to the selection bar when something
73
+ // is selected — the contextual actions take priority over mode controls.
74
+ // Also hide on Chat / Items / Studio tabs; those are contextual workflows
75
+ // (composing / picking furniture / generating renders) where the build
76
+ // menu is irrelevant.
77
+ if (hasSelectionOnMobile || hasReferenceOnMobile || isContextualPanelOnMobile) return null
78
+
21
79
  const transition = reducedMotion
22
80
  ? { duration: 0 }
23
81
  : { type: 'spring' as const, bounce: 0.2, duration: 0.4 }
@@ -138,6 +196,38 @@ export function ActionMenu({ className }: { className?: string }) {
138
196
  </motion.div>
139
197
  )}
140
198
  </AnimatePresence>
199
+
200
+ <AnimatePresence>
201
+ {showPaintTray && (
202
+ <motion.div
203
+ animate={{
204
+ opacity: 1,
205
+ maxHeight: 96,
206
+ paddingTop: 8,
207
+ paddingBottom: 8,
208
+ borderBottomWidth: 1,
209
+ }}
210
+ className={cn('overflow-hidden border-border border-b px-3')}
211
+ exit={{
212
+ opacity: 0,
213
+ maxHeight: 0,
214
+ paddingTop: 0,
215
+ paddingBottom: 0,
216
+ borderBottomWidth: 0,
217
+ }}
218
+ initial={{
219
+ opacity: 0,
220
+ maxHeight: 0,
221
+ paddingTop: 0,
222
+ paddingBottom: 0,
223
+ borderBottomWidth: 0,
224
+ }}
225
+ transition={transition}
226
+ >
227
+ <PaintMaterialTray />
228
+ </motion.div>
229
+ )}
230
+ </AnimatePresence>
141
231
  {/* Control Mode Row - Always visible, centered */}
142
232
  <div className="flex items-center justify-center gap-1 px-2 py-1.5">
143
233
  <ControlModes />
@@ -24,12 +24,14 @@ export const tools: ToolConfig[] = [
24
24
  // { id: 'custom-room', iconSrc: '/icons/custom-room.png', label: 'Custom Room' },
25
25
  { id: 'slab', iconSrc: '/icons/floor.png', label: 'Slab' },
26
26
  { id: 'ceiling', iconSrc: '/icons/ceiling.png', label: 'Ceiling' },
27
+ { id: 'column', iconSrc: '/icons/column.png', label: 'Column' },
27
28
  { id: 'roof', iconSrc: '/icons/roof.png', label: 'Gable Roof' },
28
29
  { id: 'stair', iconSrc: '/icons/stairs.png', label: 'Stairs' },
29
30
  { id: 'door', iconSrc: '/icons/door.png', label: 'Door' },
30
31
  { id: 'window', iconSrc: '/icons/window.png', label: 'Window' },
31
32
  { id: 'fence', iconSrc: '/icons/fence.png', label: 'Fence' },
32
33
  { id: 'zone', iconSrc: '/icons/zone.png', label: 'Zone' },
34
+ { id: 'spawn', iconSrc: '/icons/site.png', label: 'Spawn Point' },
33
35
  ]
34
36
 
35
37
  export function StructureTools() {