@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,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from './../../../lib/utils'
|
|
4
|
+
|
|
5
|
+
export type SidebarTab = {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TabBarProps {
|
|
11
|
+
tabs: SidebarTab[]
|
|
12
|
+
activeTab: string
|
|
13
|
+
onTabChange: (id: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex h-10 shrink-0 items-center gap-0.5 border-border/50 border-b px-2">
|
|
19
|
+
{tabs.map((tab) => {
|
|
20
|
+
const isActive = activeTab === tab.id
|
|
21
|
+
return (
|
|
22
|
+
<button
|
|
23
|
+
className={cn(
|
|
24
|
+
'relative h-7 rounded-md px-3 font-medium text-sm transition-colors',
|
|
25
|
+
isActive
|
|
26
|
+
? 'bg-accent text-foreground'
|
|
27
|
+
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
|
|
28
|
+
)}
|
|
29
|
+
key={tab.id}
|
|
30
|
+
onClick={() => onTabChange(tab.id)}
|
|
31
|
+
type="button"
|
|
32
|
+
>
|
|
33
|
+
{tab.label}
|
|
34
|
+
</button>
|
|
35
|
+
)
|
|
36
|
+
})}
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import NumberFlow from '@number-flow/react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Slider } from './../../components/ui/slider'
|
|
5
|
+
|
|
6
|
+
export function SliderDemo() {
|
|
7
|
+
const [value, setValue] = useState<number[]>([28.1])
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex min-h-screen items-center justify-center bg-[#ededed] px-8">
|
|
11
|
+
<section className="w-full max-w-lg">
|
|
12
|
+
<div className="mb-2 flex items-end justify-between">
|
|
13
|
+
<h2 className="font-semibold text-black text-xl tracking-tight">Temperature</h2>
|
|
14
|
+
<NumberFlow
|
|
15
|
+
className="font-medium text-black/45 text-xl"
|
|
16
|
+
format={{ minimumFractionDigits: 1, maximumFractionDigits: 1 }}
|
|
17
|
+
suffix="%"
|
|
18
|
+
value={value[0] ?? 50}
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<Slider
|
|
23
|
+
aria-label="Temperature"
|
|
24
|
+
max={100}
|
|
25
|
+
min={0}
|
|
26
|
+
onValueChange={setValue}
|
|
27
|
+
step={0.1}
|
|
28
|
+
value={value}
|
|
29
|
+
variant="temperature"
|
|
30
|
+
/>
|
|
31
|
+
</section>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default SliderDemo
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as SliderPrimitive from '@radix-ui/react-slider'
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
3
|
+
import type * as React from 'react'
|
|
4
|
+
|
|
5
|
+
import { cn } from './../../lib/utils'
|
|
6
|
+
|
|
7
|
+
const sliderVariants = cva(
|
|
8
|
+
'relative flex w-full touch-none select-none items-center overflow-hidden',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: '',
|
|
13
|
+
temperature: `
|
|
14
|
+
h-16
|
|
15
|
+
[&_[data-slot=slider-track]]:h-14
|
|
16
|
+
[&_[data-slot=slider-track]]:rounded-xl
|
|
17
|
+
[&_[data-slot=slider-track]]:border
|
|
18
|
+
[&_[data-slot=slider-track]]:border-neutral-300
|
|
19
|
+
[&_[data-slot=slider-track]]:bg-white/50
|
|
20
|
+
[&_[data-slot=slider-track]]:shadow-[0_1px_2px_0px_rgba(0,0,0,0.1)]
|
|
21
|
+
[&_[data-slot=slider-track]]:ring-1
|
|
22
|
+
[&_[data-slot=slider-track]]:ring-white
|
|
23
|
+
[&_[data-slot=slider-track]]:ring-inset
|
|
24
|
+
[&_[data-slot=slider-range]]:inset-y-0.5
|
|
25
|
+
[&_[data-slot=slider-range]]:h-auto
|
|
26
|
+
[&_[data-slot=slider-range]]:ml-0.5
|
|
27
|
+
[&_[data-slot=slider-range]]:mr-0.5
|
|
28
|
+
[&_[data-slot=slider-range]]:overflow-hidden
|
|
29
|
+
[&_[data-slot=slider-range]]:rounded-lg
|
|
30
|
+
[&_[data-slot=slider-range]]:border
|
|
31
|
+
[&_[data-slot=slider-range]]:border-neutral-300
|
|
32
|
+
[&_[data-slot=slider-range]]:bg-white
|
|
33
|
+
[&_[data-slot=slider-range]]:shadow-xs
|
|
34
|
+
[&_[data-slot=slider-thumb]]:h-7
|
|
35
|
+
[&_[data-slot=slider-thumb]]:w-[3px]
|
|
36
|
+
[&_[data-slot=slider-thumb]]:rounded-xl
|
|
37
|
+
[&_[data-slot=slider-thumb]]:border-0
|
|
38
|
+
[&_[data-slot=slider-thumb]]:bg-neutral-100
|
|
39
|
+
[&_[data-slot=slider-thumb]]:shadow-none
|
|
40
|
+
[&_[data-slot=slider-thumb]]:cursor-ew-resize
|
|
41
|
+
[&_[data-slot=slider-thumb]]:[transform:translateX(-8px)]
|
|
42
|
+
[&_[data-slot=slider-thumb]]:ring-0
|
|
43
|
+
[&_[data-slot=slider-thumb]]:hover:ring-0
|
|
44
|
+
[&_[data-slot=slider-thumb]]:focus-visible:ring-0
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultVariants: {
|
|
49
|
+
variant: 'default',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
type SliderProps = React.ComponentProps<typeof SliderPrimitive.Root> &
|
|
55
|
+
VariantProps<typeof sliderVariants>
|
|
56
|
+
|
|
57
|
+
function Slider({ variant, className, ...props }: SliderProps) {
|
|
58
|
+
return (
|
|
59
|
+
<SliderPrimitive.Root
|
|
60
|
+
className={cn(sliderVariants({ variant }), className)}
|
|
61
|
+
data-slot="slider"
|
|
62
|
+
{...props}
|
|
63
|
+
>
|
|
64
|
+
<SliderPrimitive.Track
|
|
65
|
+
className="relative h-3 w-full grow overflow-hidden rounded-full bg-muted"
|
|
66
|
+
data-slot="slider-track"
|
|
67
|
+
>
|
|
68
|
+
<SliderPrimitive.Range className="absolute h-full bg-primary" data-slot="slider-range" />
|
|
69
|
+
</SliderPrimitive.Track>
|
|
70
|
+
<SliderPrimitive.Thumb
|
|
71
|
+
className={cn(
|
|
72
|
+
'block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm ring-ring/50',
|
|
73
|
+
'transition-[color,box-shadow] hover:ring-4 focus-visible:outline-none focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50',
|
|
74
|
+
)}
|
|
75
|
+
data-slot="slider-thumb"
|
|
76
|
+
/>
|
|
77
|
+
</SliderPrimitive.Root>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { Slider }
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Icon as IconifyIcon } from '@iconify/react'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { ChevronsLeft, ChevronsRight, Columns2, Eye, Footprints, Moon, Sun } from 'lucide-react'
|
|
6
|
+
import { useCallback } from 'react'
|
|
7
|
+
import { cn } from '../../lib/utils'
|
|
8
|
+
import useEditor from '../../store/use-editor'
|
|
9
|
+
import type { ViewMode } from '../../store/use-editor'
|
|
10
|
+
import { useSidebarStore } from './primitives/sidebar'
|
|
11
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './primitives/tooltip'
|
|
12
|
+
|
|
13
|
+
// ── Shared styles ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** Container for a group of buttons — no padding, overflow-hidden clips children flush. */
|
|
16
|
+
const TOOLBAR_CONTAINER =
|
|
17
|
+
'inline-flex h-8 items-stretch overflow-hidden rounded-xl border border-border bg-background/90 shadow-2xl backdrop-blur-md'
|
|
18
|
+
|
|
19
|
+
/** Ghost button inside a container — flush edges, no individual border/radius. */
|
|
20
|
+
const TOOLBAR_BTN =
|
|
21
|
+
'flex items-center justify-center w-8 text-muted-foreground/80 transition-colors hover:bg-white/8 hover:text-foreground/90'
|
|
22
|
+
|
|
23
|
+
// ── View mode segmented control ─────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [
|
|
26
|
+
{
|
|
27
|
+
id: '3d',
|
|
28
|
+
label: '3D',
|
|
29
|
+
icon: <img alt="" className="h-3.5 w-3.5 object-contain" src="/icons/building.png" />,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: '2d',
|
|
33
|
+
label: '2D',
|
|
34
|
+
icon: <img alt="" className="h-3.5 w-3.5 object-contain" src="/icons/blueprint.png" />,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'split',
|
|
38
|
+
label: 'Split',
|
|
39
|
+
icon: <Columns2 className="h-3 w-3" />,
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
function ViewModeControl() {
|
|
44
|
+
const viewMode = useEditor((s) => s.viewMode)
|
|
45
|
+
const setViewMode = useEditor((s) => s.setViewMode)
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={TOOLBAR_CONTAINER}>
|
|
49
|
+
{VIEW_MODES.map((mode) => {
|
|
50
|
+
const isActive = viewMode === mode.id
|
|
51
|
+
return (
|
|
52
|
+
<button
|
|
53
|
+
className={cn(
|
|
54
|
+
'flex items-center justify-center gap-1.5 px-2.5 font-medium text-xs transition-colors',
|
|
55
|
+
isActive
|
|
56
|
+
? 'bg-white/10 text-foreground'
|
|
57
|
+
: 'text-muted-foreground/70 hover:bg-white/8 hover:text-muted-foreground',
|
|
58
|
+
)}
|
|
59
|
+
key={mode.id}
|
|
60
|
+
onClick={() => setViewMode(mode.id)}
|
|
61
|
+
type="button"
|
|
62
|
+
>
|
|
63
|
+
{mode.icon}
|
|
64
|
+
<span>{mode.label}</span>
|
|
65
|
+
</button>
|
|
66
|
+
)
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Collapse sidebar button ─────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function CollapseSidebarButton() {
|
|
75
|
+
const isCollapsed = useSidebarStore((s) => s.isCollapsed)
|
|
76
|
+
const setIsCollapsed = useSidebarStore((s) => s.setIsCollapsed)
|
|
77
|
+
|
|
78
|
+
const toggle = useCallback(() => {
|
|
79
|
+
setIsCollapsed(!isCollapsed)
|
|
80
|
+
}, [isCollapsed, setIsCollapsed])
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={TOOLBAR_CONTAINER}>
|
|
84
|
+
<button
|
|
85
|
+
className={TOOLBAR_BTN}
|
|
86
|
+
onClick={toggle}
|
|
87
|
+
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
88
|
+
type="button"
|
|
89
|
+
>
|
|
90
|
+
{isCollapsed ? <ChevronsRight className="h-4 w-4" /> : <ChevronsLeft className="h-4 w-4" />}
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Right toolbar buttons ───────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function WalkthroughButton() {
|
|
99
|
+
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
100
|
+
const setFirstPersonMode = useEditor((s) => s.setFirstPersonMode)
|
|
101
|
+
|
|
102
|
+
const toggle = () => {
|
|
103
|
+
setFirstPersonMode(!isFirstPersonMode)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Tooltip>
|
|
108
|
+
<TooltipTrigger asChild>
|
|
109
|
+
<button
|
|
110
|
+
className={cn(
|
|
111
|
+
TOOLBAR_BTN,
|
|
112
|
+
isFirstPersonMode && 'bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/20',
|
|
113
|
+
)}
|
|
114
|
+
onClick={toggle}
|
|
115
|
+
type="button"
|
|
116
|
+
>
|
|
117
|
+
<Footprints className="h-4 w-4" />
|
|
118
|
+
</button>
|
|
119
|
+
</TooltipTrigger>
|
|
120
|
+
<TooltipContent side="bottom">Walkthrough</TooltipContent>
|
|
121
|
+
</Tooltip>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function UnitToggle() {
|
|
126
|
+
const unit = useViewer((s) => s.unit)
|
|
127
|
+
const setUnit = useViewer((s) => s.setUnit)
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Tooltip>
|
|
131
|
+
<TooltipTrigger asChild>
|
|
132
|
+
<button
|
|
133
|
+
className={TOOLBAR_BTN}
|
|
134
|
+
onClick={() => setUnit(unit === 'metric' ? 'imperial' : 'metric')}
|
|
135
|
+
type="button"
|
|
136
|
+
>
|
|
137
|
+
<span className="font-semibold text-[10px]">{unit === 'metric' ? 'm' : 'ft'}</span>
|
|
138
|
+
</button>
|
|
139
|
+
</TooltipTrigger>
|
|
140
|
+
<TooltipContent side="bottom">
|
|
141
|
+
{unit === 'metric' ? 'Metric (m)' : 'Imperial (ft)'}
|
|
142
|
+
</TooltipContent>
|
|
143
|
+
</Tooltip>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ThemeToggle() {
|
|
148
|
+
const theme = useViewer((s) => s.theme)
|
|
149
|
+
const setTheme = useViewer((s) => s.setTheme)
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Tooltip>
|
|
153
|
+
<TooltipTrigger asChild>
|
|
154
|
+
<button
|
|
155
|
+
className={cn(TOOLBAR_BTN, theme === 'dark' ? 'text-indigo-400/60' : 'text-amber-400/60')}
|
|
156
|
+
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
157
|
+
type="button"
|
|
158
|
+
>
|
|
159
|
+
{theme === 'dark' ? <Moon className="h-3.5 w-3.5" /> : <Sun className="h-3.5 w-3.5" />}
|
|
160
|
+
</button>
|
|
161
|
+
</TooltipTrigger>
|
|
162
|
+
<TooltipContent side="bottom">{theme === 'dark' ? 'Dark' : 'Light'}</TooltipContent>
|
|
163
|
+
</Tooltip>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Level mode toggle ───────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
const levelModeOrder = ['stacked', 'exploded', 'solo'] as const
|
|
170
|
+
const levelModeLabels: Record<string, string> = {
|
|
171
|
+
manual: 'Stack',
|
|
172
|
+
stacked: 'Stack',
|
|
173
|
+
exploded: 'Exploded',
|
|
174
|
+
solo: 'Solo',
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function LevelModeToggle() {
|
|
178
|
+
const levelMode = useViewer((s) => s.levelMode)
|
|
179
|
+
const setLevelMode = useViewer((s) => s.setLevelMode)
|
|
180
|
+
|
|
181
|
+
const cycle = () => {
|
|
182
|
+
if (levelMode === 'manual') {
|
|
183
|
+
setLevelMode('stacked')
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
const idx = levelModeOrder.indexOf(levelMode as (typeof levelModeOrder)[number])
|
|
187
|
+
const next = levelModeOrder[(idx + 1) % levelModeOrder.length]
|
|
188
|
+
if (next) setLevelMode(next)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const isDefault = levelMode === 'stacked' || levelMode === 'manual'
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<Tooltip>
|
|
195
|
+
<TooltipTrigger asChild>
|
|
196
|
+
<button
|
|
197
|
+
className={cn(
|
|
198
|
+
TOOLBAR_BTN,
|
|
199
|
+
'w-auto gap-1.5 px-2.5',
|
|
200
|
+
!isDefault && 'bg-white/10 text-foreground/90',
|
|
201
|
+
)}
|
|
202
|
+
onClick={cycle}
|
|
203
|
+
type="button"
|
|
204
|
+
>
|
|
205
|
+
{levelMode === 'solo' ? (
|
|
206
|
+
<IconifyIcon height={14} icon="lucide:diamond" width={14} />
|
|
207
|
+
) : levelMode === 'exploded' ? (
|
|
208
|
+
<IconifyIcon height={14} icon="charm:stack-pop" width={14} />
|
|
209
|
+
) : (
|
|
210
|
+
<IconifyIcon height={14} icon="charm:stack-push" width={14} />
|
|
211
|
+
)}
|
|
212
|
+
<span className="font-medium text-xs">{levelModeLabels[levelMode] ?? 'Stack'}</span>
|
|
213
|
+
</button>
|
|
214
|
+
</TooltipTrigger>
|
|
215
|
+
<TooltipContent side="bottom">
|
|
216
|
+
Levels: {levelMode === 'manual' ? 'Manual' : levelModeLabels[levelMode]}
|
|
217
|
+
</TooltipContent>
|
|
218
|
+
</Tooltip>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Wall mode toggle ────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
const wallModeOrder = ['cutaway', 'up', 'down'] as const
|
|
225
|
+
const wallModeConfig: Record<string, { icon: string; label: string }> = {
|
|
226
|
+
up: { icon: '/icons/room.png', label: 'Full height' },
|
|
227
|
+
cutaway: { icon: '/icons/wallcut.png', label: 'Cutaway' },
|
|
228
|
+
down: { icon: '/icons/walllow.png', label: 'Low' },
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function WallModeToggle() {
|
|
232
|
+
const wallMode = useViewer((s) => s.wallMode)
|
|
233
|
+
const setWallMode = useViewer((s) => s.setWallMode)
|
|
234
|
+
|
|
235
|
+
const cycle = () => {
|
|
236
|
+
const idx = wallModeOrder.indexOf(wallMode as (typeof wallModeOrder)[number])
|
|
237
|
+
const next = wallModeOrder[(idx + 1) % wallModeOrder.length]
|
|
238
|
+
if (next) setWallMode(next)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const config = wallModeConfig[wallMode] ?? wallModeConfig.cutaway!
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<Tooltip>
|
|
245
|
+
<TooltipTrigger asChild>
|
|
246
|
+
<button
|
|
247
|
+
className={cn(
|
|
248
|
+
TOOLBAR_BTN,
|
|
249
|
+
'w-auto gap-1.5 px-2.5',
|
|
250
|
+
wallMode !== 'cutaway'
|
|
251
|
+
? 'bg-white/10'
|
|
252
|
+
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0',
|
|
253
|
+
)}
|
|
254
|
+
onClick={cycle}
|
|
255
|
+
type="button"
|
|
256
|
+
>
|
|
257
|
+
<img alt={config.label} className="h-4 w-4 object-contain" src={config.icon} />
|
|
258
|
+
<span className="font-medium text-xs">{config.label}</span>
|
|
259
|
+
</button>
|
|
260
|
+
</TooltipTrigger>
|
|
261
|
+
<TooltipContent side="bottom">Walls: {config.label}</TooltipContent>
|
|
262
|
+
</Tooltip>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Camera mode toggle ──────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
function CameraModeToggle() {
|
|
269
|
+
const cameraMode = useViewer((s) => s.cameraMode)
|
|
270
|
+
const setCameraMode = useViewer((s) => s.setCameraMode)
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<Tooltip>
|
|
274
|
+
<TooltipTrigger asChild>
|
|
275
|
+
<button
|
|
276
|
+
className={cn(
|
|
277
|
+
TOOLBAR_BTN,
|
|
278
|
+
cameraMode === 'orthographic' && 'bg-white/10 text-foreground/90',
|
|
279
|
+
)}
|
|
280
|
+
onClick={() =>
|
|
281
|
+
setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')
|
|
282
|
+
}
|
|
283
|
+
type="button"
|
|
284
|
+
>
|
|
285
|
+
{cameraMode === 'perspective' ? (
|
|
286
|
+
<IconifyIcon height={16} icon="icon-park-outline:perspective" width={16} />
|
|
287
|
+
) : (
|
|
288
|
+
<IconifyIcon height={16} icon="vaadin:grid" width={16} />
|
|
289
|
+
)}
|
|
290
|
+
</button>
|
|
291
|
+
</TooltipTrigger>
|
|
292
|
+
<TooltipContent side="bottom">
|
|
293
|
+
{cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}
|
|
294
|
+
</TooltipContent>
|
|
295
|
+
</Tooltip>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function PreviewButton() {
|
|
300
|
+
return (
|
|
301
|
+
<Tooltip>
|
|
302
|
+
<TooltipTrigger asChild>
|
|
303
|
+
<button
|
|
304
|
+
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"
|
|
305
|
+
onClick={() => useEditor.getState().setPreviewMode(true)}
|
|
306
|
+
type="button"
|
|
307
|
+
>
|
|
308
|
+
<Eye className="h-3.5 w-3.5 shrink-0" />
|
|
309
|
+
<span>Preview</span>
|
|
310
|
+
</button>
|
|
311
|
+
</TooltipTrigger>
|
|
312
|
+
<TooltipContent side="bottom">Preview mode</TooltipContent>
|
|
313
|
+
</Tooltip>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Composed toolbar sections ───────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
export function ViewerToolbarLeft() {
|
|
320
|
+
return (
|
|
321
|
+
<>
|
|
322
|
+
<CollapseSidebarButton />
|
|
323
|
+
<ViewModeControl />
|
|
324
|
+
</>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function ViewerToolbarRight() {
|
|
329
|
+
return (
|
|
330
|
+
<div className={TOOLBAR_CONTAINER}>
|
|
331
|
+
<LevelModeToggle />
|
|
332
|
+
<WallModeToggle />
|
|
333
|
+
<div className="my-1.5 w-px bg-border/50" />
|
|
334
|
+
<UnitToggle />
|
|
335
|
+
<ThemeToggle />
|
|
336
|
+
<CameraModeToggle />
|
|
337
|
+
<div className="my-1.5 w-px bg-border/50" />
|
|
338
|
+
<WalkthroughButton />
|
|
339
|
+
<PreviewButton />
|
|
340
|
+
</div>
|
|
341
|
+
)
|
|
342
|
+
}
|