@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.
- package/package.json +9 -5
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +20 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9855 -3298
- package/src/components/editor/index.tsx +269 -21
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/thumbnail-generator.tsx +38 -7
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +267 -36
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +7 -0
- package/src/components/tools/door/move-door-tool.tsx +28 -8
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
- package/src/components/tools/fence/move-fence-tool.tsx +101 -34
- package/src/components/tools/item/move-tool.tsx +10 -1
- package/src/components/tools/item/placement-math.ts +30 -1
- package/src/components/tools/item/placement-strategies.ts +109 -31
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +660 -52
- package/src/components/tools/roof/move-roof-tool.tsx +22 -15
- package/src/components/tools/shared/polygon-editor.tsx +153 -28
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/tool-manager.tsx +18 -3
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +28 -1
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +18 -1
- package/src/components/ui/controls/material-picker.tsx +152 -165
- package/src/components/ui/controls/slider-control.tsx +66 -18
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1116 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +1 -25
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +981 -289
- package/src/components/ui/panels/fence-panel.tsx +3 -45
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +210 -1
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +238 -5
- package/src/components/ui/panels/roof-panel.tsx +4 -105
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
- package/src/components/ui/panels/slab-panel.tsx +4 -30
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +11 -117
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +1 -95
- package/src/components/ui/panels/window-panel.tsx +660 -139
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +42 -1
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-keyboard.ts +64 -7
- package/src/hooks/use-mobile.ts +12 -12
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- 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
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 =
|
|
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 {
|
|
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() {
|