@pascal-app/editor 0.7.0 → 0.8.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 +6 -6
- package/src/components/editor/custom-camera-controls.tsx +2 -1
- package/src/components/editor/editor-layout-v2.tsx +4 -3
- package/src/components/editor/first-person/build-collider-world.ts +5 -7
- package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
- package/src/components/editor/first-person-controls.tsx +11 -11
- package/src/components/editor/floating-action-menu.tsx +0 -0
- package/src/components/editor/floorplan-panel.tsx +44 -37
- package/src/components/editor/index.tsx +68 -53
- package/src/components/editor/selection-manager.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +18 -61
- package/src/components/editor/use-floorplan-background-placement.ts +3 -3
- package/src/components/editor/wall-measurement-label.tsx +0 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1 -1
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
- package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-tool.tsx +2 -2
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
- package/src/components/tools/fence/move-fence-tool.tsx +13 -9
- package/src/components/tools/item/move-tool.tsx +3 -6
- package/src/components/tools/item/placement-math.ts +2 -4
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/use-draft-node.ts +0 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
- package/src/components/tools/roof/move-roof-tool.tsx +7 -2
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- package/src/components/tools/shared/segment-angle.ts +1 -1
- package/src/components/tools/tool-manager.tsx +12 -12
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +0 -0
- package/src/components/tools/wall/wall-tool.tsx +3 -3
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +35 -86
- package/src/components/ui/action-menu/view-toggles.tsx +19 -31
- package/src/components/ui/command-palette/editor-commands.tsx +6 -4
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +8 -5
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
- package/src/components/ui/level-duplicate-dialog.tsx +3 -5
- package/src/components/ui/panels/ceiling-panel.tsx +2 -3
- package/src/components/ui/panels/column-panel.tsx +62 -18
- package/src/components/ui/panels/door-panel.tsx +272 -265
- package/src/components/ui/panels/fence-panel.tsx +0 -5
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +3 -32
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/panels/roof-panel.tsx +52 -2
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
- package/src/components/ui/panels/slab-panel.tsx +0 -0
- package/src/components/ui/panels/spawn-panel.tsx +10 -4
- package/src/components/ui/panels/stair-panel.tsx +66 -14
- package/src/components/ui/panels/wall-panel.tsx +97 -1
- package/src/components/ui/panels/window-panel.tsx +13 -5
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +0 -0
- package/src/components/viewer-zone-system.tsx +0 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +10 -0
- package/src/index.tsx +8 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene.ts +0 -0
- package/src/lib/sfx-bus.ts +2 -0
- package/src/lib/sfx-player.ts +5 -5
- package/src/lib/stair-duplication.ts +2 -2
- package/src/store/use-editor.tsx +27 -59
- package/tsconfig.json +2 -1
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/ui/viewer-toolbar.tsx +0 -436
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Icon as IconifyIcon } from '@iconify/react'
|
|
4
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
-
import {
|
|
6
|
-
Check,
|
|
7
|
-
ChevronsLeft,
|
|
8
|
-
ChevronsRight,
|
|
9
|
-
Columns2,
|
|
10
|
-
Eye,
|
|
11
|
-
EyeOff,
|
|
12
|
-
Footprints,
|
|
13
|
-
Grid2X2,
|
|
14
|
-
Moon,
|
|
15
|
-
Sun,
|
|
16
|
-
} from 'lucide-react'
|
|
17
|
-
import { useCallback } from 'react'
|
|
18
|
-
import { cn } from '../../lib/utils'
|
|
19
|
-
import useEditor from '../../store/use-editor'
|
|
20
|
-
import type { GridSnapStep, ViewMode } from '../../store/use-editor'
|
|
21
|
-
import {
|
|
22
|
-
DropdownMenu,
|
|
23
|
-
DropdownMenuContent,
|
|
24
|
-
DropdownMenuItem,
|
|
25
|
-
DropdownMenuTrigger,
|
|
26
|
-
} from './primitives/dropdown-menu'
|
|
27
|
-
import { useSidebarStore } from './primitives/sidebar'
|
|
28
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from './primitives/tooltip'
|
|
29
|
-
|
|
30
|
-
// ── Shared styles ───────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
/** Container for a group of buttons — no padding, overflow-hidden clips children flush. */
|
|
33
|
-
const TOOLBAR_CONTAINER =
|
|
34
|
-
'inline-flex h-8 items-stretch overflow-hidden rounded-xl border border-border bg-background/90 shadow-2xl backdrop-blur-md'
|
|
35
|
-
|
|
36
|
-
/** Ghost button inside a container — flush edges, no individual border/radius. */
|
|
37
|
-
const TOOLBAR_BTN =
|
|
38
|
-
'flex items-center justify-center w-8 text-muted-foreground/80 transition-colors hover:bg-white/8 hover:text-foreground/90'
|
|
39
|
-
|
|
40
|
-
// ── View mode segmented control ─────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [
|
|
43
|
-
{
|
|
44
|
-
id: '3d',
|
|
45
|
-
label: '3D',
|
|
46
|
-
icon: <img alt="" className="h-3.5 w-3.5 object-contain" src="/icons/building.png" />,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
id: '2d',
|
|
50
|
-
label: '2D',
|
|
51
|
-
icon: <img alt="" className="h-3.5 w-3.5 object-contain" src="/icons/blueprint.png" />,
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
id: 'split',
|
|
55
|
-
label: 'Split',
|
|
56
|
-
icon: <Columns2 className="h-3 w-3" />,
|
|
57
|
-
},
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
function ViewModeControl() {
|
|
61
|
-
const viewMode = useEditor((s) => s.viewMode)
|
|
62
|
-
const setViewMode = useEditor((s) => s.setViewMode)
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div className={TOOLBAR_CONTAINER}>
|
|
66
|
-
{VIEW_MODES.map((mode) => {
|
|
67
|
-
const isActive = viewMode === mode.id
|
|
68
|
-
return (
|
|
69
|
-
<button
|
|
70
|
-
className={cn(
|
|
71
|
-
'flex items-center justify-center gap-1.5 px-2.5 font-medium text-xs transition-colors',
|
|
72
|
-
isActive
|
|
73
|
-
? 'bg-white/10 text-foreground'
|
|
74
|
-
: 'text-muted-foreground/70 hover:bg-white/8 hover:text-muted-foreground',
|
|
75
|
-
)}
|
|
76
|
-
key={mode.id}
|
|
77
|
-
onClick={() => setViewMode(mode.id)}
|
|
78
|
-
type="button"
|
|
79
|
-
>
|
|
80
|
-
{mode.icon}
|
|
81
|
-
<span>{mode.label}</span>
|
|
82
|
-
</button>
|
|
83
|
-
)
|
|
84
|
-
})}
|
|
85
|
-
</div>
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ── Collapse sidebar button ─────────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
function CollapseSidebarButton() {
|
|
92
|
-
const isCollapsed = useSidebarStore((s) => s.isCollapsed)
|
|
93
|
-
const setIsCollapsed = useSidebarStore((s) => s.setIsCollapsed)
|
|
94
|
-
|
|
95
|
-
const toggle = useCallback(() => {
|
|
96
|
-
setIsCollapsed(!isCollapsed)
|
|
97
|
-
}, [isCollapsed, setIsCollapsed])
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<div className={TOOLBAR_CONTAINER}>
|
|
101
|
-
<button
|
|
102
|
-
className={TOOLBAR_BTN}
|
|
103
|
-
onClick={toggle}
|
|
104
|
-
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
105
|
-
type="button"
|
|
106
|
-
>
|
|
107
|
-
{isCollapsed ? <ChevronsRight className="h-4 w-4" /> : <ChevronsLeft className="h-4 w-4" />}
|
|
108
|
-
</button>
|
|
109
|
-
</div>
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── Right toolbar buttons ───────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
function WalkthroughButton() {
|
|
116
|
-
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
117
|
-
const setFirstPersonMode = useEditor((s) => s.setFirstPersonMode)
|
|
118
|
-
|
|
119
|
-
const toggle = () => {
|
|
120
|
-
setFirstPersonMode(!isFirstPersonMode)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<Tooltip>
|
|
125
|
-
<TooltipTrigger asChild>
|
|
126
|
-
<button
|
|
127
|
-
className={cn(
|
|
128
|
-
TOOLBAR_BTN,
|
|
129
|
-
isFirstPersonMode && 'bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/20',
|
|
130
|
-
)}
|
|
131
|
-
onClick={toggle}
|
|
132
|
-
type="button"
|
|
133
|
-
>
|
|
134
|
-
<Footprints className="h-4 w-4" />
|
|
135
|
-
</button>
|
|
136
|
-
</TooltipTrigger>
|
|
137
|
-
<TooltipContent side="bottom">Walkthrough</TooltipContent>
|
|
138
|
-
</Tooltip>
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function UnitToggle() {
|
|
143
|
-
const unit = useViewer((s) => s.unit)
|
|
144
|
-
const setUnit = useViewer((s) => s.setUnit)
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<Tooltip>
|
|
148
|
-
<TooltipTrigger asChild>
|
|
149
|
-
<button
|
|
150
|
-
className={TOOLBAR_BTN}
|
|
151
|
-
onClick={() => setUnit(unit === 'metric' ? 'imperial' : 'metric')}
|
|
152
|
-
type="button"
|
|
153
|
-
>
|
|
154
|
-
<span className="font-semibold text-[10px]">{unit === 'metric' ? 'm' : 'ft'}</span>
|
|
155
|
-
</button>
|
|
156
|
-
</TooltipTrigger>
|
|
157
|
-
<TooltipContent side="bottom">
|
|
158
|
-
{unit === 'metric' ? 'Metric (m)' : 'Imperial (ft)'}
|
|
159
|
-
</TooltipContent>
|
|
160
|
-
</Tooltip>
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function ThemeToggle() {
|
|
165
|
-
const theme = useViewer((s) => s.theme)
|
|
166
|
-
const setTheme = useViewer((s) => s.setTheme)
|
|
167
|
-
|
|
168
|
-
return (
|
|
169
|
-
<Tooltip>
|
|
170
|
-
<TooltipTrigger asChild>
|
|
171
|
-
<button
|
|
172
|
-
className={cn(TOOLBAR_BTN, theme === 'dark' ? 'text-indigo-400/60' : 'text-amber-400/60')}
|
|
173
|
-
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
174
|
-
type="button"
|
|
175
|
-
>
|
|
176
|
-
{theme === 'dark' ? <Moon className="h-3.5 w-3.5" /> : <Sun className="h-3.5 w-3.5" />}
|
|
177
|
-
</button>
|
|
178
|
-
</TooltipTrigger>
|
|
179
|
-
<TooltipContent side="bottom">{theme === 'dark' ? 'Dark' : 'Light'}</TooltipContent>
|
|
180
|
-
</Tooltip>
|
|
181
|
-
)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ── Level mode toggle ───────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
const levelModeOrder = ['stacked', 'exploded', 'solo'] as const
|
|
187
|
-
const levelModeLabels: Record<string, string> = {
|
|
188
|
-
manual: 'Stack',
|
|
189
|
-
stacked: 'Stack',
|
|
190
|
-
exploded: 'Exploded',
|
|
191
|
-
solo: 'Solo',
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const gridSnapOrder: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
|
|
195
|
-
const gridSnapLabels: Record<GridSnapStep, string> = {
|
|
196
|
-
0.5: '0.50',
|
|
197
|
-
0.25: '0.25',
|
|
198
|
-
0.1: '0.10',
|
|
199
|
-
0.05: '0.05',
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function formatGridSnapStep(step: GridSnapStep): string {
|
|
203
|
-
return gridSnapLabels[step]
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function LevelModeToggle() {
|
|
207
|
-
const levelMode = useViewer((s) => s.levelMode)
|
|
208
|
-
const setLevelMode = useViewer((s) => s.setLevelMode)
|
|
209
|
-
|
|
210
|
-
const cycle = () => {
|
|
211
|
-
if (levelMode === 'manual') {
|
|
212
|
-
setLevelMode('stacked')
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
const idx = levelModeOrder.indexOf(levelMode as (typeof levelModeOrder)[number])
|
|
216
|
-
const next = levelModeOrder[(idx + 1) % levelModeOrder.length]
|
|
217
|
-
if (next) setLevelMode(next)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const isDefault = levelMode === 'stacked' || levelMode === 'manual'
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<Tooltip>
|
|
224
|
-
<TooltipTrigger asChild>
|
|
225
|
-
<button
|
|
226
|
-
className={cn(
|
|
227
|
-
TOOLBAR_BTN,
|
|
228
|
-
'w-auto gap-1.5 px-2.5',
|
|
229
|
-
!isDefault && 'bg-white/10 text-foreground/90',
|
|
230
|
-
)}
|
|
231
|
-
onClick={cycle}
|
|
232
|
-
type="button"
|
|
233
|
-
>
|
|
234
|
-
{levelMode === 'solo' ? (
|
|
235
|
-
<IconifyIcon height={14} icon="lucide:diamond" width={14} />
|
|
236
|
-
) : levelMode === 'exploded' ? (
|
|
237
|
-
<IconifyIcon height={14} icon="charm:stack-pop" width={14} />
|
|
238
|
-
) : (
|
|
239
|
-
<IconifyIcon height={14} icon="charm:stack-push" width={14} />
|
|
240
|
-
)}
|
|
241
|
-
<span className="font-medium text-xs">{levelModeLabels[levelMode] ?? 'Stack'}</span>
|
|
242
|
-
</button>
|
|
243
|
-
</TooltipTrigger>
|
|
244
|
-
<TooltipContent side="bottom">
|
|
245
|
-
Levels: {levelMode === 'manual' ? 'Manual' : levelModeLabels[levelMode]}
|
|
246
|
-
</TooltipContent>
|
|
247
|
-
</Tooltip>
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function GridSnapToggle() {
|
|
252
|
-
const gridSnapStep = useEditor((s) => s.gridSnapStep)
|
|
253
|
-
const setGridSnapStep = useEditor((s) => s.setGridSnapStep)
|
|
254
|
-
|
|
255
|
-
return (
|
|
256
|
-
<DropdownMenu>
|
|
257
|
-
<Tooltip>
|
|
258
|
-
<TooltipTrigger asChild>
|
|
259
|
-
<DropdownMenuTrigger asChild>
|
|
260
|
-
<button className={cn(TOOLBAR_BTN, 'w-auto gap-1.5 px-2.5')} type="button">
|
|
261
|
-
<IconifyIcon height={14} icon="lucide:grid-2x2" width={14} />
|
|
262
|
-
<span className="font-medium text-xs">{formatGridSnapStep(gridSnapStep)}</span>
|
|
263
|
-
</button>
|
|
264
|
-
</DropdownMenuTrigger>
|
|
265
|
-
</TooltipTrigger>
|
|
266
|
-
<TooltipContent side="bottom">Grid snap: {formatGridSnapStep(gridSnapStep)}</TooltipContent>
|
|
267
|
-
</Tooltip>
|
|
268
|
-
<DropdownMenuContent align="center" side="bottom">
|
|
269
|
-
{gridSnapOrder.map((step) => {
|
|
270
|
-
const isActive = step === gridSnapStep
|
|
271
|
-
return (
|
|
272
|
-
<DropdownMenuItem key={step} onSelect={() => setGridSnapStep(step)}>
|
|
273
|
-
<span className="flex min-w-12 items-center justify-between gap-3">
|
|
274
|
-
<span>{formatGridSnapStep(step)}</span>
|
|
275
|
-
{isActive ? <Check className="h-3.5 w-3.5" /> : <span className="h-3.5 w-3.5" />}
|
|
276
|
-
</span>
|
|
277
|
-
</DropdownMenuItem>
|
|
278
|
-
)
|
|
279
|
-
})}
|
|
280
|
-
</DropdownMenuContent>
|
|
281
|
-
</DropdownMenu>
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function GridVisibilityToggle() {
|
|
286
|
-
const showGrid = useViewer((s) => s.showGrid)
|
|
287
|
-
const setShowGrid = useViewer((s) => s.setShowGrid)
|
|
288
|
-
|
|
289
|
-
return (
|
|
290
|
-
<Tooltip>
|
|
291
|
-
<TooltipTrigger asChild>
|
|
292
|
-
<button
|
|
293
|
-
aria-label={`Grid: ${showGrid ? 'Visible' : 'Hidden'}`}
|
|
294
|
-
aria-pressed={showGrid}
|
|
295
|
-
className={cn(
|
|
296
|
-
TOOLBAR_BTN,
|
|
297
|
-
'w-auto gap-1.5 px-2.5',
|
|
298
|
-
showGrid
|
|
299
|
-
? 'bg-white/10 text-foreground/90'
|
|
300
|
-
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0',
|
|
301
|
-
)}
|
|
302
|
-
onClick={() => setShowGrid(!showGrid)}
|
|
303
|
-
type="button"
|
|
304
|
-
>
|
|
305
|
-
<Grid2X2 className="h-3.5 w-3.5" />
|
|
306
|
-
{showGrid ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
|
|
307
|
-
</button>
|
|
308
|
-
</TooltipTrigger>
|
|
309
|
-
<TooltipContent side="bottom">Grid: {showGrid ? 'Visible' : 'Hidden'}</TooltipContent>
|
|
310
|
-
</Tooltip>
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// ── Wall mode toggle ────────────────────────────────────────────────────────
|
|
315
|
-
|
|
316
|
-
const wallModeOrder = ['cutaway', 'up', 'down'] as const
|
|
317
|
-
const wallModeConfig: Record<string, { icon: string; label: string }> = {
|
|
318
|
-
up: { icon: '/icons/room.png', label: 'Full height' },
|
|
319
|
-
cutaway: { icon: '/icons/wallcut.png', label: 'Cutaway' },
|
|
320
|
-
down: { icon: '/icons/walllow.png', label: 'Low' },
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function WallModeToggle() {
|
|
324
|
-
const wallMode = useViewer((s) => s.wallMode)
|
|
325
|
-
const setWallMode = useViewer((s) => s.setWallMode)
|
|
326
|
-
|
|
327
|
-
const cycle = () => {
|
|
328
|
-
const idx = wallModeOrder.indexOf(wallMode as (typeof wallModeOrder)[number])
|
|
329
|
-
const next = wallModeOrder[(idx + 1) % wallModeOrder.length]
|
|
330
|
-
if (next) setWallMode(next)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const config = wallModeConfig[wallMode] ?? wallModeConfig.cutaway!
|
|
334
|
-
|
|
335
|
-
return (
|
|
336
|
-
<Tooltip>
|
|
337
|
-
<TooltipTrigger asChild>
|
|
338
|
-
<button
|
|
339
|
-
className={cn(
|
|
340
|
-
TOOLBAR_BTN,
|
|
341
|
-
'w-auto gap-1.5 px-2.5',
|
|
342
|
-
wallMode !== 'cutaway'
|
|
343
|
-
? 'bg-white/10'
|
|
344
|
-
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0',
|
|
345
|
-
)}
|
|
346
|
-
onClick={cycle}
|
|
347
|
-
type="button"
|
|
348
|
-
>
|
|
349
|
-
<img alt={config.label} className="h-4 w-4 object-contain" src={config.icon} />
|
|
350
|
-
<span className="font-medium text-xs">{config.label}</span>
|
|
351
|
-
</button>
|
|
352
|
-
</TooltipTrigger>
|
|
353
|
-
<TooltipContent side="bottom">Walls: {config.label}</TooltipContent>
|
|
354
|
-
</Tooltip>
|
|
355
|
-
)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// ── Camera mode toggle ──────────────────────────────────────────────────────
|
|
359
|
-
|
|
360
|
-
function CameraModeToggle() {
|
|
361
|
-
const cameraMode = useViewer((s) => s.cameraMode)
|
|
362
|
-
const setCameraMode = useViewer((s) => s.setCameraMode)
|
|
363
|
-
|
|
364
|
-
return (
|
|
365
|
-
<Tooltip>
|
|
366
|
-
<TooltipTrigger asChild>
|
|
367
|
-
<button
|
|
368
|
-
className={cn(
|
|
369
|
-
TOOLBAR_BTN,
|
|
370
|
-
cameraMode === 'orthographic' && 'bg-white/10 text-foreground/90',
|
|
371
|
-
)}
|
|
372
|
-
onClick={() =>
|
|
373
|
-
setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')
|
|
374
|
-
}
|
|
375
|
-
type="button"
|
|
376
|
-
>
|
|
377
|
-
{cameraMode === 'perspective' ? (
|
|
378
|
-
<IconifyIcon height={16} icon="icon-park-outline:perspective" width={16} />
|
|
379
|
-
) : (
|
|
380
|
-
<IconifyIcon height={16} icon="vaadin:grid" width={16} />
|
|
381
|
-
)}
|
|
382
|
-
</button>
|
|
383
|
-
</TooltipTrigger>
|
|
384
|
-
<TooltipContent side="bottom">
|
|
385
|
-
{cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}
|
|
386
|
-
</TooltipContent>
|
|
387
|
-
</Tooltip>
|
|
388
|
-
)
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function PreviewButton() {
|
|
392
|
-
return (
|
|
393
|
-
<Tooltip>
|
|
394
|
-
<TooltipTrigger asChild>
|
|
395
|
-
<button
|
|
396
|
-
className="flex items-center gap-1.5 px-2.5 font-medium text-muted-foreground/80 text-xs transition-colors hover:bg-white/8 hover:text-foreground/90"
|
|
397
|
-
onClick={() => useEditor.getState().setPreviewMode(true)}
|
|
398
|
-
type="button"
|
|
399
|
-
>
|
|
400
|
-
<Eye className="h-3.5 w-3.5 shrink-0" />
|
|
401
|
-
<span>Preview</span>
|
|
402
|
-
</button>
|
|
403
|
-
</TooltipTrigger>
|
|
404
|
-
<TooltipContent side="bottom">Preview mode</TooltipContent>
|
|
405
|
-
</Tooltip>
|
|
406
|
-
)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ── Composed toolbar sections ───────────────────────────────────────────────
|
|
410
|
-
|
|
411
|
-
export function ViewerToolbarLeft() {
|
|
412
|
-
return (
|
|
413
|
-
<>
|
|
414
|
-
<CollapseSidebarButton />
|
|
415
|
-
<ViewModeControl />
|
|
416
|
-
</>
|
|
417
|
-
)
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export function ViewerToolbarRight() {
|
|
421
|
-
return (
|
|
422
|
-
<div className={TOOLBAR_CONTAINER}>
|
|
423
|
-
<LevelModeToggle />
|
|
424
|
-
<WallModeToggle />
|
|
425
|
-
<GridSnapToggle />
|
|
426
|
-
<GridVisibilityToggle />
|
|
427
|
-
<div className="my-1.5 w-px bg-border/50" />
|
|
428
|
-
<UnitToggle />
|
|
429
|
-
<ThemeToggle />
|
|
430
|
-
<CameraModeToggle />
|
|
431
|
-
<div className="my-1.5 w-px bg-border/50" />
|
|
432
|
-
<WalkthroughButton />
|
|
433
|
-
<PreviewButton />
|
|
434
|
-
</div>
|
|
435
|
-
)
|
|
436
|
-
}
|