@pascal-app/editor 0.4.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 +62 -0
- package/src/components/editor/custom-camera-controls.tsx +387 -0
- package/src/components/editor/editor-layout-v2.tsx +220 -0
- package/src/components/editor/export-manager.tsx +78 -0
- package/src/components/editor/first-person-controls.tsx +249 -0
- package/src/components/editor/floating-action-menu.tsx +231 -0
- package/src/components/editor/floorplan-panel.tsx +9609 -0
- package/src/components/editor/grid.tsx +161 -0
- package/src/components/editor/index.tsx +928 -0
- package/src/components/editor/node-action-menu.tsx +66 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
- package/src/components/editor/selection-manager.tsx +897 -0
- package/src/components/editor/site-edge-labels.tsx +90 -0
- package/src/components/editor/thumbnail-generator.tsx +166 -0
- package/src/components/editor/wall-measurement-label.tsx +258 -0
- package/src/components/feedback-dialog.tsx +265 -0
- package/src/components/pascal-radio.tsx +280 -0
- package/src/components/preview-button.tsx +16 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
- package/src/components/systems/roof/roof-edit-system.tsx +69 -0
- package/src/components/systems/stair/stair-edit-system.tsx +69 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
- package/src/components/systems/zone/zone-system.tsx +87 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
- package/src/components/tools/door/door-math.ts +110 -0
- package/src/components/tools/door/door-tool.tsx +293 -0
- package/src/components/tools/door/move-door-tool.tsx +373 -0
- package/src/components/tools/item/item-tool.tsx +26 -0
- package/src/components/tools/item/move-tool.tsx +90 -0
- package/src/components/tools/item/placement-math.ts +85 -0
- package/src/components/tools/item/placement-strategies.ts +556 -0
- package/src/components/tools/item/placement-types.ts +117 -0
- package/src/components/tools/item/use-draft-node.ts +227 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
- package/src/components/tools/roof/move-roof-tool.tsx +288 -0
- package/src/components/tools/roof/roof-tool.tsx +318 -0
- package/src/components/tools/select/box-select-tool.tsx +626 -0
- package/src/components/tools/shared/cursor-sphere.tsx +119 -0
- package/src/components/tools/shared/polygon-editor.tsx +361 -0
- package/src/components/tools/site/site-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
- package/src/components/tools/slab/slab-tool.tsx +322 -0
- package/src/components/tools/stair/stair-defaults.ts +7 -0
- package/src/components/tools/stair/stair-tool.tsx +194 -0
- package/src/components/tools/tool-manager.tsx +120 -0
- package/src/components/tools/wall/wall-drafting.ts +140 -0
- package/src/components/tools/wall/wall-tool.tsx +210 -0
- package/src/components/tools/window/move-window-tool.tsx +410 -0
- package/src/components/tools/window/window-math.ts +117 -0
- package/src/components/tools/window/window-tool.tsx +303 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
- package/src/components/tools/zone/zone-tool.tsx +364 -0
- package/src/components/ui/action-menu/action-button.tsx +59 -0
- package/src/components/ui/action-menu/camera-actions.tsx +74 -0
- package/src/components/ui/action-menu/control-modes.tsx +240 -0
- package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
- package/src/components/ui/action-menu/index.tsx +152 -0
- package/src/components/ui/action-menu/structure-tools.tsx +100 -0
- package/src/components/ui/action-menu/view-toggles.tsx +397 -0
- package/src/components/ui/command-palette/editor-commands.tsx +396 -0
- package/src/components/ui/command-palette/index.tsx +730 -0
- package/src/components/ui/controls/action-button.tsx +33 -0
- package/src/components/ui/controls/material-picker.tsx +194 -0
- package/src/components/ui/controls/metric-control.tsx +262 -0
- package/src/components/ui/controls/panel-section.tsx +65 -0
- package/src/components/ui/controls/segmented-control.tsx +45 -0
- package/src/components/ui/controls/slider-control.tsx +245 -0
- package/src/components/ui/controls/toggle-control.tsx +38 -0
- package/src/components/ui/floating-level-selector.tsx +355 -0
- package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
- package/src/components/ui/helpers/helper-manager.tsx +33 -0
- package/src/components/ui/helpers/item-helper.tsx +40 -0
- package/src/components/ui/helpers/roof-helper.tsx +16 -0
- package/src/components/ui/helpers/slab-helper.tsx +20 -0
- package/src/components/ui/helpers/wall-helper.tsx +20 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
- package/src/components/ui/panels/ceiling-panel.tsx +230 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
- package/src/components/ui/panels/door-panel.tsx +600 -0
- package/src/components/ui/panels/item-panel.tsx +306 -0
- package/src/components/ui/panels/panel-manager.tsx +59 -0
- package/src/components/ui/panels/panel-wrapper.tsx +80 -0
- package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
- package/src/components/ui/panels/reference-panel.tsx +177 -0
- package/src/components/ui/panels/roof-panel.tsx +262 -0
- package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
- package/src/components/ui/panels/slab-panel.tsx +228 -0
- package/src/components/ui/panels/stair-panel.tsx +304 -0
- package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
- package/src/components/ui/panels/wall-panel.tsx +123 -0
- package/src/components/ui/panels/window-panel.tsx +441 -0
- package/src/components/ui/primitives/button.tsx +69 -0
- package/src/components/ui/primitives/card.tsx +75 -0
- package/src/components/ui/primitives/color-dot.tsx +61 -0
- package/src/components/ui/primitives/context-menu.tsx +227 -0
- package/src/components/ui/primitives/dialog.tsx +129 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
- package/src/components/ui/primitives/error-boundary.tsx +52 -0
- package/src/components/ui/primitives/input.tsx +21 -0
- package/src/components/ui/primitives/number-input.tsx +187 -0
- package/src/components/ui/primitives/opacity-control.tsx +79 -0
- package/src/components/ui/primitives/popover.tsx +42 -0
- package/src/components/ui/primitives/separator.tsx +28 -0
- package/src/components/ui/primitives/sheet.tsx +130 -0
- package/src/components/ui/primitives/shortcut-token.tsx +64 -0
- package/src/components/ui/primitives/sidebar.tsx +855 -0
- package/src/components/ui/primitives/skeleton.tsx +13 -0
- package/src/components/ui/primitives/slider.tsx +58 -0
- package/src/components/ui/primitives/switch.tsx +29 -0
- package/src/components/ui/primitives/tooltip.tsx +57 -0
- package/src/components/ui/scene-loader.tsx +40 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
- package/src/components/ui/sidebar/icon-rail.tsx +147 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
- package/src/components/ui/sidebar/tab-bar.tsx +39 -0
- package/src/components/ui/slider-demo.tsx +36 -0
- package/src/components/ui/slider.tsx +81 -0
- package/src/components/ui/viewer-toolbar.tsx +342 -0
- package/src/components/viewer-overlay.tsx +499 -0
- package/src/components/viewer-zone-system.tsx +48 -0
- package/src/contexts/presets-context.tsx +121 -0
- package/src/hooks/use-auto-save.ts +194 -0
- package/src/hooks/use-contextual-tools.ts +52 -0
- package/src/hooks/use-grid-events.ts +106 -0
- package/src/hooks/use-keyboard.ts +214 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-reduced-motion.ts +20 -0
- package/src/index.tsx +33 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/level-selection.ts +31 -0
- package/src/lib/scene.ts +394 -0
- package/src/lib/sfx/index.ts +2 -0
- package/src/lib/sfx-bus.ts +49 -0
- package/src/lib/sfx-player.ts +60 -0
- package/src/lib/utils.ts +43 -0
- package/src/store/use-audio.tsx +45 -0
- package/src/store/use-command-registry.ts +36 -0
- package/src/store/use-editor.tsx +522 -0
- package/src/store/use-palette-view-registry.ts +45 -0
- package/src/store/use-upload.ts +90 -0
- package/src/three-types.ts +3 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Button } from './../../../components/ui/primitives/button'
|
|
3
|
+
import {
|
|
4
|
+
Tooltip,
|
|
5
|
+
TooltipContent,
|
|
6
|
+
TooltipTrigger,
|
|
7
|
+
} from './../../../components/ui/primitives/tooltip'
|
|
8
|
+
import { cn } from './../../../lib/utils'
|
|
9
|
+
|
|
10
|
+
interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
|
11
|
+
label: string
|
|
12
|
+
shortcut?: string
|
|
13
|
+
isActive?: boolean
|
|
14
|
+
tooltipContent?: React.ReactNode
|
|
15
|
+
tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
|
|
19
|
+
(
|
|
20
|
+
{ className, children, label, shortcut, isActive, tooltipContent, tooltipSide, ...props },
|
|
21
|
+
ref,
|
|
22
|
+
) => {
|
|
23
|
+
return (
|
|
24
|
+
<Tooltip>
|
|
25
|
+
<TooltipTrigger asChild>
|
|
26
|
+
<Button
|
|
27
|
+
className={cn('relative h-11 w-11 transition-all', className)}
|
|
28
|
+
ref={ref}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
'flex h-full w-full items-center justify-center transition-transform',
|
|
34
|
+
shortcut && '-translate-x-0.5 -translate-y-0.5',
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
{shortcut && (
|
|
40
|
+
<div className="absolute right-1 bottom-1 rounded border border-border/40 bg-background/40 px-1 py-[2px] backdrop-blur-md">
|
|
41
|
+
<span className="block font-medium font-mono text-[9px] text-muted-foreground/70 leading-none">
|
|
42
|
+
{shortcut}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
</Button>
|
|
47
|
+
</TooltipTrigger>
|
|
48
|
+
<TooltipContent side={tooltipSide}>
|
|
49
|
+
{tooltipContent || (
|
|
50
|
+
<p>
|
|
51
|
+
{label} {shortcut && `(${shortcut})`}
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
54
|
+
</TooltipContent>
|
|
55
|
+
</Tooltip>
|
|
56
|
+
)
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
ActionButton.displayName = 'ActionButton'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { emitter } from '@pascal-app/core'
|
|
4
|
+
import Image from 'next/image'
|
|
5
|
+
import { ActionButton } from './action-button'
|
|
6
|
+
|
|
7
|
+
export function CameraActions() {
|
|
8
|
+
const goToTopView = () => {
|
|
9
|
+
emitter.emit('camera-controls:top-view')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const orbitCW = () => {
|
|
13
|
+
emitter.emit('camera-controls:orbit-cw')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const orbitCCW = () => {
|
|
17
|
+
emitter.emit('camera-controls:orbit-ccw')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
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>
|
|
38
|
+
|
|
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>
|
|
55
|
+
|
|
56
|
+
{/* Top View */}
|
|
57
|
+
<ActionButton
|
|
58
|
+
className="group hover:bg-white/5"
|
|
59
|
+
label="Top View"
|
|
60
|
+
onClick={goToTopView}
|
|
61
|
+
size="icon"
|
|
62
|
+
variant="ghost"
|
|
63
|
+
>
|
|
64
|
+
<Image
|
|
65
|
+
alt="Top View"
|
|
66
|
+
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
67
|
+
height={28}
|
|
68
|
+
src="/icons/topview.png"
|
|
69
|
+
width={28}
|
|
70
|
+
/>
|
|
71
|
+
</ActionButton>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Icon } from '@iconify/react'
|
|
4
|
+
import { type LevelNode, useScene } from '@pascal-app/core'
|
|
5
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
6
|
+
import { type LucideIcon, Trash2 } from 'lucide-react'
|
|
7
|
+
import Image from 'next/image'
|
|
8
|
+
import { cn } from './../../../lib/utils'
|
|
9
|
+
import useEditor from './../../../store/use-editor'
|
|
10
|
+
import { ActionButton } from './action-button'
|
|
11
|
+
|
|
12
|
+
type ControlId = 'select' | 'box-select' | 'site-edit' | 'build' | 'furnish' | 'zone' | 'delete'
|
|
13
|
+
|
|
14
|
+
type ControlConfig = {
|
|
15
|
+
id: ControlId
|
|
16
|
+
icon?: LucideIcon
|
|
17
|
+
iconifyIcon?: string
|
|
18
|
+
imageSrc?: string
|
|
19
|
+
label: string
|
|
20
|
+
shortcut?: string
|
|
21
|
+
color: string
|
|
22
|
+
activeColor: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Fixed set of controls — always visible, never morphs
|
|
26
|
+
const controls: ControlConfig[] = [
|
|
27
|
+
{
|
|
28
|
+
id: 'select',
|
|
29
|
+
imageSrc: '/icons/select.png',
|
|
30
|
+
label: 'Select',
|
|
31
|
+
shortcut: 'V',
|
|
32
|
+
color: 'hover:bg-blue-500/20 hover:text-blue-400',
|
|
33
|
+
activeColor: 'bg-blue-500/20 text-blue-400',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'box-select',
|
|
37
|
+
iconifyIcon: 'mdi:select-drag',
|
|
38
|
+
label: 'Box select',
|
|
39
|
+
color: 'hover:bg-white/5',
|
|
40
|
+
activeColor: 'bg-white/10 hover:bg-white/10',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'site-edit',
|
|
44
|
+
imageSrc: '/icons/site.png',
|
|
45
|
+
label: 'Edit site',
|
|
46
|
+
color: 'hover:bg-white/5',
|
|
47
|
+
activeColor: 'bg-white/10 hover:bg-white/10',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'build',
|
|
51
|
+
imageSrc: '/icons/build.png',
|
|
52
|
+
label: 'Build',
|
|
53
|
+
shortcut: 'B',
|
|
54
|
+
color: 'hover:bg-green-500/20 hover:text-green-400',
|
|
55
|
+
activeColor: 'bg-green-500/20 text-green-400',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'furnish',
|
|
59
|
+
imageSrc: '/icons/couch.png',
|
|
60
|
+
label: 'Furnish',
|
|
61
|
+
shortcut: 'F',
|
|
62
|
+
color: 'hover:bg-green-500/20 hover:text-green-400',
|
|
63
|
+
activeColor: 'bg-green-500/20 text-green-400',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'zone',
|
|
67
|
+
imageSrc: '/icons/zone.png',
|
|
68
|
+
label: 'Zone',
|
|
69
|
+
shortcut: 'Z',
|
|
70
|
+
color: 'hover:bg-green-500/20 hover:text-green-400',
|
|
71
|
+
activeColor: 'bg-green-500/20 text-green-400',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'delete',
|
|
75
|
+
icon: Trash2,
|
|
76
|
+
label: 'Delete',
|
|
77
|
+
shortcut: 'D',
|
|
78
|
+
color: 'hover:bg-red-500/20 hover:text-red-400',
|
|
79
|
+
activeColor: 'bg-red-500/20 text-red-400',
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
export function ControlModes() {
|
|
84
|
+
const mode = useEditor((state) => state.mode)
|
|
85
|
+
const phase = useEditor((state) => state.phase)
|
|
86
|
+
const selectionTool = useEditor((state) => state.floorplanSelectionTool)
|
|
87
|
+
const setMode = useEditor((state) => state.setMode)
|
|
88
|
+
const setPhase = useEditor((state) => state.setPhase)
|
|
89
|
+
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
90
|
+
const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
|
|
91
|
+
const levelId = useViewer((s) => s.selection.levelId)
|
|
92
|
+
|
|
93
|
+
const levelNode = useScene((state) =>
|
|
94
|
+
levelId ? (state.nodes[levelId] as LevelNode | undefined) : undefined,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const isSiteEditing = phase === 'site'
|
|
98
|
+
const isGroundFloor = levelNode?.type === 'level' && levelNode.level === 0
|
|
99
|
+
const canEnterSiteEdit = isGroundFloor || isSiteEditing
|
|
100
|
+
|
|
101
|
+
const structureLayer = useEditor((state) => state.structureLayer)
|
|
102
|
+
|
|
103
|
+
const getIsActive = (id: ControlId): boolean => {
|
|
104
|
+
if (isSiteEditing) return id === 'site-edit'
|
|
105
|
+
if (id === 'select') return mode === 'select' && selectionTool === 'click'
|
|
106
|
+
if (id === 'box-select') return mode === 'select' && selectionTool === 'marquee'
|
|
107
|
+
if (id === 'site-edit') return false
|
|
108
|
+
if (id === 'build')
|
|
109
|
+
return mode === 'build' && phase === 'structure' && structureLayer === 'elements'
|
|
110
|
+
if (id === 'furnish') return mode === 'build' && phase === 'furnish'
|
|
111
|
+
if (id === 'zone')
|
|
112
|
+
return mode === 'build' && phase === 'structure' && structureLayer === 'zones'
|
|
113
|
+
return mode === id
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const handleClick = (id: ControlId) => {
|
|
117
|
+
if (id === 'site-edit') {
|
|
118
|
+
if (isSiteEditing) {
|
|
119
|
+
// Toggle off → back to structure/select
|
|
120
|
+
setPhase('structure')
|
|
121
|
+
setMode('select')
|
|
122
|
+
setStructureLayer('elements')
|
|
123
|
+
} else if (isGroundFloor) {
|
|
124
|
+
// Enter site editing — set state directly to preserve level selection.
|
|
125
|
+
// setPhase('site') calls viewer.resetSelection() which clears levelId,
|
|
126
|
+
// breaking the 2D floorplan (it needs a level to render the SVG).
|
|
127
|
+
useEditor.setState({ phase: 'site', mode: 'select', tool: null, catalogCategory: null })
|
|
128
|
+
}
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Exit site editing first if needed
|
|
133
|
+
if (isSiteEditing) {
|
|
134
|
+
setPhase('structure')
|
|
135
|
+
setStructureLayer('elements')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (id === 'select') {
|
|
139
|
+
setMode('select')
|
|
140
|
+
setSelectionTool('click')
|
|
141
|
+
} else if (id === 'box-select') {
|
|
142
|
+
setMode('select')
|
|
143
|
+
setSelectionTool('marquee')
|
|
144
|
+
} else if (id === 'build') {
|
|
145
|
+
// Toggle: if already in structure build, go back to select
|
|
146
|
+
if (getIsActive('build')) {
|
|
147
|
+
setMode('select')
|
|
148
|
+
} else {
|
|
149
|
+
setPhase('structure')
|
|
150
|
+
setStructureLayer('elements')
|
|
151
|
+
setMode('build')
|
|
152
|
+
}
|
|
153
|
+
} else if (id === 'furnish') {
|
|
154
|
+
if (getIsActive('furnish')) {
|
|
155
|
+
setMode('select')
|
|
156
|
+
} else {
|
|
157
|
+
setPhase('furnish')
|
|
158
|
+
setMode('build')
|
|
159
|
+
}
|
|
160
|
+
} else if (id === 'zone') {
|
|
161
|
+
if (getIsActive('zone')) {
|
|
162
|
+
setMode('select')
|
|
163
|
+
} else {
|
|
164
|
+
setPhase('structure')
|
|
165
|
+
setStructureLayer('zones')
|
|
166
|
+
setMode('build')
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
setMode(id)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div className="flex items-center gap-1">
|
|
175
|
+
{controls.map((c) => {
|
|
176
|
+
const ModeIcon = c.icon
|
|
177
|
+
const isImageMode = Boolean(c.imageSrc)
|
|
178
|
+
const isSiteButton = c.id === 'site-edit'
|
|
179
|
+
const isActive = getIsActive(c.id)
|
|
180
|
+
const isDisabled = isSiteButton && !canEnterSiteEdit
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<ActionButton
|
|
184
|
+
className={cn(
|
|
185
|
+
'group text-muted-foreground',
|
|
186
|
+
isSiteButton
|
|
187
|
+
? isActive
|
|
188
|
+
? c.activeColor
|
|
189
|
+
: canEnterSiteEdit
|
|
190
|
+
? 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
|
|
191
|
+
: 'cursor-not-allowed opacity-35 grayscale'
|
|
192
|
+
: !(isImageMode || isActive) && c.color,
|
|
193
|
+
!(isSiteButton || isImageMode) && isActive && c.activeColor,
|
|
194
|
+
!isSiteButton && isImageMode && isActive && 'bg-white/10 hover:bg-white/10',
|
|
195
|
+
!isSiteButton && isImageMode && !isActive && 'hover:bg-white/5',
|
|
196
|
+
)}
|
|
197
|
+
disabled={isDisabled}
|
|
198
|
+
key={c.id}
|
|
199
|
+
label={
|
|
200
|
+
isSiteButton
|
|
201
|
+
? isActive
|
|
202
|
+
? 'Exit site editing'
|
|
203
|
+
: canEnterSiteEdit
|
|
204
|
+
? 'Edit site'
|
|
205
|
+
: 'Site editing (ground level only)'
|
|
206
|
+
: c.label
|
|
207
|
+
}
|
|
208
|
+
onClick={() => handleClick(c.id)}
|
|
209
|
+
shortcut={c.shortcut}
|
|
210
|
+
size="icon"
|
|
211
|
+
variant="ghost"
|
|
212
|
+
>
|
|
213
|
+
{c.imageSrc ? (
|
|
214
|
+
<Image
|
|
215
|
+
alt={c.label}
|
|
216
|
+
className={cn(
|
|
217
|
+
'h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200',
|
|
218
|
+
isSiteButton
|
|
219
|
+
? isActive
|
|
220
|
+
? 'opacity-100 grayscale-0'
|
|
221
|
+
: ''
|
|
222
|
+
: isActive
|
|
223
|
+
? 'opacity-100 grayscale-0'
|
|
224
|
+
: 'opacity-60 grayscale group-hover:opacity-100 group-hover:grayscale-0',
|
|
225
|
+
)}
|
|
226
|
+
height={28}
|
|
227
|
+
src={c.imageSrc}
|
|
228
|
+
width={28}
|
|
229
|
+
/>
|
|
230
|
+
) : c.iconifyIcon ? (
|
|
231
|
+
<Icon color="currentColor" height={18} icon={c.iconifyIcon} width={18} />
|
|
232
|
+
) : (
|
|
233
|
+
ModeIcon && <ModeIcon className="h-5 w-5" />
|
|
234
|
+
)}
|
|
235
|
+
</ActionButton>
|
|
236
|
+
)
|
|
237
|
+
})}
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import NextImage from 'next/image'
|
|
4
|
+
import { cn } from './../../../lib/utils'
|
|
5
|
+
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
|
6
|
+
import { ActionButton } from './action-button'
|
|
7
|
+
|
|
8
|
+
export type FurnishToolConfig = {
|
|
9
|
+
id: 'item'
|
|
10
|
+
iconSrc: string
|
|
11
|
+
label: string
|
|
12
|
+
catalogCategory: CatalogCategory
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Furnish mode tools: furniture, appliances, decoration (painting is now a control mode)
|
|
16
|
+
export const furnishTools: FurnishToolConfig[] = [
|
|
17
|
+
{
|
|
18
|
+
id: 'item',
|
|
19
|
+
iconSrc: '/icons/couch.png',
|
|
20
|
+
label: 'Furniture',
|
|
21
|
+
catalogCategory: 'furniture',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'item',
|
|
25
|
+
iconSrc: '/icons/appliance.png',
|
|
26
|
+
label: 'Appliance',
|
|
27
|
+
catalogCategory: 'appliance',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'item',
|
|
31
|
+
iconSrc: '/icons/kitchen.png',
|
|
32
|
+
label: 'Kitchen',
|
|
33
|
+
catalogCategory: 'kitchen',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'item',
|
|
37
|
+
iconSrc: '/icons/bathroom.png',
|
|
38
|
+
label: 'Bathroom',
|
|
39
|
+
catalogCategory: 'bathroom',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'item',
|
|
43
|
+
iconSrc: '/icons/tree.png',
|
|
44
|
+
label: 'Outdoor',
|
|
45
|
+
catalogCategory: 'outdoor',
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
export function FurnishTools() {
|
|
50
|
+
const mode = useEditor((state) => state.mode)
|
|
51
|
+
const activeTool = useEditor((state) => state.tool)
|
|
52
|
+
const setActiveTool = useEditor((state) => state.setTool)
|
|
53
|
+
const setMode = useEditor((state) => state.setMode)
|
|
54
|
+
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
55
|
+
const setCatalogCategory = useEditor((state) => state.setCatalogCategory)
|
|
56
|
+
|
|
57
|
+
const hasActiveTool = furnishTools.some(
|
|
58
|
+
(tool) => mode === 'build' && activeTool === 'item' && catalogCategory === tool.catalogCategory,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex items-center gap-1.5 px-1">
|
|
63
|
+
{furnishTools.map((tool, index) => {
|
|
64
|
+
// For item tools with catalog category, check both tool and category match
|
|
65
|
+
const isActive =
|
|
66
|
+
mode === 'build' && activeTool === 'item' && catalogCategory === tool.catalogCategory
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ActionButton
|
|
70
|
+
className={cn(
|
|
71
|
+
'rounded-lg duration-300',
|
|
72
|
+
isActive
|
|
73
|
+
? 'z-10 scale-110 bg-black/40 hover:bg-black/40'
|
|
74
|
+
: 'scale-95 bg-transparent opacity-60 grayscale hover:bg-black/20 hover:opacity-100 hover:grayscale-0',
|
|
75
|
+
)}
|
|
76
|
+
key={`${tool.id}-${tool.catalogCategory ?? index}`}
|
|
77
|
+
label={tool.label}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
if (!isActive) {
|
|
80
|
+
setCatalogCategory(tool.catalogCategory)
|
|
81
|
+
setActiveTool('item')
|
|
82
|
+
if (mode !== 'build') {
|
|
83
|
+
setMode('build')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
size="icon"
|
|
88
|
+
variant="ghost"
|
|
89
|
+
>
|
|
90
|
+
<NextImage
|
|
91
|
+
alt={tool.label}
|
|
92
|
+
className="size-full object-contain"
|
|
93
|
+
height={28}
|
|
94
|
+
src={tool.iconSrc}
|
|
95
|
+
width={28}
|
|
96
|
+
/>
|
|
97
|
+
</ActionButton>
|
|
98
|
+
)
|
|
99
|
+
})}
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { AnimatePresence, motion } from 'motion/react'
|
|
4
|
+
import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
|
|
5
|
+
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
|
|
6
|
+
import { cn } from './../../../lib/utils'
|
|
7
|
+
import useEditor from './../../../store/use-editor'
|
|
8
|
+
import { ItemCatalog } from '../item-catalog/item-catalog'
|
|
9
|
+
import { CameraActions } from './camera-actions'
|
|
10
|
+
import { ControlModes } from './control-modes'
|
|
11
|
+
import { FurnishTools } from './furnish-tools'
|
|
12
|
+
import { StructureTools } from './structure-tools'
|
|
13
|
+
import { ViewToggles } from './view-toggles'
|
|
14
|
+
|
|
15
|
+
export function ActionMenu({ className }: { className?: string }) {
|
|
16
|
+
const phase = useEditor((state) => state.phase)
|
|
17
|
+
const mode = useEditor((state) => state.mode)
|
|
18
|
+
const tool = useEditor((state) => state.tool)
|
|
19
|
+
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
20
|
+
const reducedMotion = useReducedMotion()
|
|
21
|
+
const transition = reducedMotion
|
|
22
|
+
? { duration: 0 }
|
|
23
|
+
: { type: 'spring' as const, bounce: 0.2, duration: 0.4 }
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<TooltipProvider>
|
|
27
|
+
<motion.div
|
|
28
|
+
className={cn(
|
|
29
|
+
'fixed bottom-6 left-1/2 z-50 -translate-x-1/2',
|
|
30
|
+
'rounded-2xl border border-border bg-background/90 shadow-2xl backdrop-blur-md',
|
|
31
|
+
'transition-colors duration-200 ease-out',
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
layout
|
|
35
|
+
transition={transition}
|
|
36
|
+
>
|
|
37
|
+
{/* Item Catalog Row - Only show when in build mode with item tool */}
|
|
38
|
+
<AnimatePresence>
|
|
39
|
+
{mode === 'build' && tool === 'item' && catalogCategory && (
|
|
40
|
+
<motion.div
|
|
41
|
+
animate={{
|
|
42
|
+
opacity: 1,
|
|
43
|
+
maxHeight: 160,
|
|
44
|
+
paddingTop: 8,
|
|
45
|
+
paddingBottom: 8,
|
|
46
|
+
borderBottomWidth: 1,
|
|
47
|
+
}}
|
|
48
|
+
className={cn('overflow-hidden border-border border-b px-2 py-2')}
|
|
49
|
+
exit={{
|
|
50
|
+
opacity: 0,
|
|
51
|
+
maxHeight: 0,
|
|
52
|
+
paddingTop: 0,
|
|
53
|
+
paddingBottom: 0,
|
|
54
|
+
borderBottomWidth: 0,
|
|
55
|
+
}}
|
|
56
|
+
initial={{
|
|
57
|
+
opacity: 0,
|
|
58
|
+
maxHeight: 0,
|
|
59
|
+
paddingTop: 0,
|
|
60
|
+
paddingBottom: 0,
|
|
61
|
+
borderBottomWidth: 0,
|
|
62
|
+
}}
|
|
63
|
+
transition={transition}
|
|
64
|
+
>
|
|
65
|
+
<ItemCatalog category={catalogCategory} key={catalogCategory} />
|
|
66
|
+
</motion.div>
|
|
67
|
+
)}
|
|
68
|
+
</AnimatePresence>
|
|
69
|
+
|
|
70
|
+
<AnimatePresence>
|
|
71
|
+
{phase === 'furnish' && mode === 'build' && (
|
|
72
|
+
<motion.div
|
|
73
|
+
animate={{
|
|
74
|
+
opacity: 1,
|
|
75
|
+
maxHeight: 80,
|
|
76
|
+
paddingTop: 8,
|
|
77
|
+
paddingBottom: 8,
|
|
78
|
+
borderBottomWidth: 1,
|
|
79
|
+
}}
|
|
80
|
+
className={cn(
|
|
81
|
+
'overflow-hidden border-border',
|
|
82
|
+
'max-h-20 border-b px-2 py-2 opacity-100',
|
|
83
|
+
)}
|
|
84
|
+
exit={{
|
|
85
|
+
opacity: 0,
|
|
86
|
+
maxHeight: 0,
|
|
87
|
+
paddingTop: 0,
|
|
88
|
+
paddingBottom: 0,
|
|
89
|
+
borderBottomWidth: 0,
|
|
90
|
+
}}
|
|
91
|
+
initial={{
|
|
92
|
+
opacity: 0,
|
|
93
|
+
maxHeight: 0,
|
|
94
|
+
paddingTop: 0,
|
|
95
|
+
paddingBottom: 0,
|
|
96
|
+
borderBottomWidth: 0,
|
|
97
|
+
}}
|
|
98
|
+
transition={transition}
|
|
99
|
+
>
|
|
100
|
+
<div className="mx-auto w-max">
|
|
101
|
+
<FurnishTools />
|
|
102
|
+
</div>
|
|
103
|
+
</motion.div>
|
|
104
|
+
)}
|
|
105
|
+
</AnimatePresence>
|
|
106
|
+
|
|
107
|
+
{/* Structure Tools Row - Animated */}
|
|
108
|
+
<AnimatePresence>
|
|
109
|
+
{phase === 'structure' && mode === 'build' && (
|
|
110
|
+
<motion.div
|
|
111
|
+
animate={{
|
|
112
|
+
opacity: 1,
|
|
113
|
+
maxHeight: 80,
|
|
114
|
+
paddingTop: 8,
|
|
115
|
+
paddingBottom: 8,
|
|
116
|
+
borderBottomWidth: 1,
|
|
117
|
+
}}
|
|
118
|
+
className={cn('max-h-20 overflow-hidden border-border border-b px-2 py-2')}
|
|
119
|
+
exit={{
|
|
120
|
+
opacity: 0,
|
|
121
|
+
maxHeight: 0,
|
|
122
|
+
paddingTop: 0,
|
|
123
|
+
paddingBottom: 0,
|
|
124
|
+
borderBottomWidth: 0,
|
|
125
|
+
}}
|
|
126
|
+
initial={{
|
|
127
|
+
opacity: 0,
|
|
128
|
+
maxHeight: 0,
|
|
129
|
+
paddingTop: 0,
|
|
130
|
+
paddingBottom: 0,
|
|
131
|
+
borderBottomWidth: 0,
|
|
132
|
+
}}
|
|
133
|
+
transition={transition}
|
|
134
|
+
>
|
|
135
|
+
<div className="w-max">
|
|
136
|
+
<StructureTools />
|
|
137
|
+
</div>
|
|
138
|
+
</motion.div>
|
|
139
|
+
)}
|
|
140
|
+
</AnimatePresence>
|
|
141
|
+
{/* Control Mode Row - Always visible, centered */}
|
|
142
|
+
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
|
|
143
|
+
<ControlModes />
|
|
144
|
+
<div className="mx-1 h-5 w-px bg-border" />
|
|
145
|
+
<ViewToggles />
|
|
146
|
+
<div className="mx-1 h-5 w-px bg-border" />
|
|
147
|
+
<CameraActions />
|
|
148
|
+
</div>
|
|
149
|
+
</motion.div>
|
|
150
|
+
</TooltipProvider>
|
|
151
|
+
)
|
|
152
|
+
}
|