@pascal-app/editor 0.7.0 → 0.9.1
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 +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- 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/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- 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/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -56,13 +56,20 @@ export function focusTreeNode(nodeId: AnyNodeId) {
|
|
|
56
56
|
import { cn } from '../../../../../lib/utils'
|
|
57
57
|
import { BuildingTreeNode } from './building-tree-node'
|
|
58
58
|
import { CeilingTreeNode } from './ceiling-tree-node'
|
|
59
|
+
import { ChimneyTreeNode } from './chimney-tree-node'
|
|
59
60
|
import { ColumnTreeNode } from './column-tree-node'
|
|
60
61
|
import { DoorTreeNode } from './door-tree-node'
|
|
62
|
+
import { DormerTreeNode } from './dormer-tree-node'
|
|
63
|
+
import { ElevatorTreeNode } from './elevator-tree-node'
|
|
61
64
|
import { FenceTreeNode } from './fence-tree-node'
|
|
65
|
+
import { GutterTreeNode } from './gutter-tree-node'
|
|
62
66
|
import { ItemTreeNode } from './item-tree-node'
|
|
63
67
|
import { LevelTreeNode } from './level-tree-node'
|
|
68
|
+
import { RegistryTreeNode } from './registry-tree-node'
|
|
64
69
|
import { RoofTreeNode } from './roof-tree-node'
|
|
70
|
+
import { ShelfTreeNode } from './shelf-tree-node'
|
|
65
71
|
import { SlabTreeNode } from './slab-tree-node'
|
|
72
|
+
import { SolarPanelTreeNode } from './solar-panel-tree-node'
|
|
66
73
|
import { SpawnTreeNode } from './spawn-tree-node'
|
|
67
74
|
import { StairTreeNode } from './stair-tree-node'
|
|
68
75
|
import { WallTreeNode } from './wall-tree-node'
|
|
@@ -75,43 +82,70 @@ interface TreeNodeProps {
|
|
|
75
82
|
isLast?: boolean
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
// Per-kind tree-node components keyed by `node.type`. Lookup replaces
|
|
86
|
+
// the legacy switch — adding a kind to this map is now the only edit
|
|
87
|
+
// needed in this file (the switch's `case '<kind>':` clauses were
|
|
88
|
+
// flagged by the Phase 6 grep gate as the last per-kind dispatch
|
|
89
|
+
// outside the registry; future work moves these to a
|
|
90
|
+
// `def.presentation`-driven generic tree-node and removes this map
|
|
91
|
+
// entirely).
|
|
92
|
+
const treeNodeByType: Record<
|
|
93
|
+
string,
|
|
94
|
+
React.ComponentType<{ depth: number; isLast?: boolean; nodeId: AnyNodeId }>
|
|
95
|
+
> = {
|
|
96
|
+
building: BuildingTreeNode as React.ComponentType<{
|
|
97
|
+
depth: number
|
|
98
|
+
isLast?: boolean
|
|
99
|
+
nodeId: AnyNodeId
|
|
100
|
+
}>,
|
|
101
|
+
'box-vent': RegistryTreeNode,
|
|
102
|
+
ceiling: CeilingTreeNode,
|
|
103
|
+
chimney: ChimneyTreeNode,
|
|
104
|
+
dormer: DormerTreeNode,
|
|
105
|
+
'solar-panel': SolarPanelTreeNode,
|
|
106
|
+
column: ColumnTreeNode,
|
|
107
|
+
elevator: ElevatorTreeNode,
|
|
108
|
+
level: LevelTreeNode as React.ComponentType<{
|
|
109
|
+
depth: number
|
|
110
|
+
isLast?: boolean
|
|
111
|
+
nodeId: AnyNodeId
|
|
112
|
+
}>,
|
|
113
|
+
shelf: ShelfTreeNode as React.ComponentType<{
|
|
114
|
+
depth: number
|
|
115
|
+
isLast?: boolean
|
|
116
|
+
nodeId: AnyNodeId
|
|
117
|
+
}>,
|
|
118
|
+
slab: SlabTreeNode,
|
|
119
|
+
spawn: SpawnTreeNode as React.ComponentType<{
|
|
120
|
+
depth: number
|
|
121
|
+
isLast?: boolean
|
|
122
|
+
nodeId: AnyNodeId
|
|
123
|
+
}>,
|
|
124
|
+
wall: WallTreeNode,
|
|
125
|
+
fence: FenceTreeNode,
|
|
126
|
+
gutter: GutterTreeNode,
|
|
127
|
+
'ridge-vent': RegistryTreeNode,
|
|
128
|
+
'turbine-vent': RegistryTreeNode,
|
|
129
|
+
cupola: RegistryTreeNode,
|
|
130
|
+
'eyebrow-vent': RegistryTreeNode,
|
|
131
|
+
roof: RoofTreeNode,
|
|
132
|
+
stair: StairTreeNode,
|
|
133
|
+
door: DoorTreeNode,
|
|
134
|
+
window: WindowTreeNode,
|
|
135
|
+
zone: ZoneTreeNode as React.ComponentType<{
|
|
136
|
+
depth: number
|
|
137
|
+
isLast?: boolean
|
|
138
|
+
nodeId: AnyNodeId
|
|
139
|
+
}>,
|
|
140
|
+
item: ItemTreeNode,
|
|
141
|
+
}
|
|
142
|
+
|
|
78
143
|
export const TreeNode = memo(function TreeNode({ nodeId, depth = 0, isLast }: TreeNodeProps) {
|
|
79
144
|
const nodeType = useScene((state) => state.nodes[nodeId]?.type)
|
|
80
|
-
|
|
81
145
|
if (!nodeType) return null
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return <BuildingTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `building_${string}`} />
|
|
86
|
-
case 'ceiling':
|
|
87
|
-
return <CeilingTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
88
|
-
case 'column':
|
|
89
|
-
return <ColumnTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
90
|
-
case 'level':
|
|
91
|
-
return <LevelTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `level_${string}`} />
|
|
92
|
-
case 'slab':
|
|
93
|
-
return <SlabTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
94
|
-
case 'spawn':
|
|
95
|
-
return <SpawnTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `spawn_${string}`} />
|
|
96
|
-
case 'wall':
|
|
97
|
-
return <WallTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
98
|
-
case 'fence':
|
|
99
|
-
return <FenceTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
100
|
-
case 'roof':
|
|
101
|
-
return <RoofTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
102
|
-
case 'stair':
|
|
103
|
-
return <StairTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
104
|
-
case 'item':
|
|
105
|
-
return <ItemTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
106
|
-
case 'door':
|
|
107
|
-
return <DoorTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
108
|
-
case 'window':
|
|
109
|
-
return <WindowTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
110
|
-
case 'zone':
|
|
111
|
-
return <ZoneTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `zone_${string}`} />
|
|
112
|
-
default:
|
|
113
|
-
return null
|
|
114
|
-
}
|
|
146
|
+
const Component = treeNodeByType[nodeType]
|
|
147
|
+
if (!Component) return null
|
|
148
|
+
return <Component depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
115
149
|
})
|
|
116
150
|
|
|
117
151
|
interface TreeNodeWrapperProps {
|
|
@@ -44,7 +44,9 @@ export const ZoneTreeNode = memo(function ZoneTreeNode({
|
|
|
44
44
|
depth={depth}
|
|
45
45
|
expanded={false}
|
|
46
46
|
hasChildren={false}
|
|
47
|
-
icon={
|
|
47
|
+
icon={
|
|
48
|
+
<ColorDot color={color ?? '#3b82f6'} onChange={(c) => updateNode(nodeId, { color: c })} />
|
|
49
|
+
}
|
|
48
50
|
isHovered={isHovered}
|
|
49
51
|
isLast={isLast}
|
|
50
52
|
isSelected={isSelected}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
emitter,
|
|
4
|
+
useScene,
|
|
5
|
+
type ZoneNode,
|
|
6
|
+
} from '@pascal-app/core'
|
|
2
7
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { Camera, Hexagon, Trash2 } from 'lucide-react'
|
|
8
|
+
import { Camera, Hexagon, Save, Trash2 } from 'lucide-react'
|
|
4
9
|
import { useState } from 'react'
|
|
10
|
+
import { sfxEmitter } from './../../../../../lib/sfx-bus'
|
|
11
|
+
import { collectZoneContentIds } from './../../../../../lib/zone-content'
|
|
5
12
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
6
13
|
import {
|
|
7
14
|
Popover,
|
|
@@ -10,6 +17,8 @@ import {
|
|
|
10
17
|
} from './../../../../../components/ui/primitives/popover'
|
|
11
18
|
import { cn } from './../../../../../lib/utils'
|
|
12
19
|
import useEditor from './../../../../../store/use-editor'
|
|
20
|
+
import { ActionButton } from '../../../controls/action-button'
|
|
21
|
+
import { PanelSection } from '../../../controls/panel-section'
|
|
13
22
|
|
|
14
23
|
function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|
15
24
|
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
|
|
@@ -26,6 +35,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|
|
26
35
|
|
|
27
36
|
const handleDelete = (e: React.MouseEvent) => {
|
|
28
37
|
e.stopPropagation()
|
|
38
|
+
sfxEmitter.emit('sfx:structure-delete')
|
|
29
39
|
deleteNode(zone.id)
|
|
30
40
|
if (isSelected) {
|
|
31
41
|
setSelection({ zoneId: null })
|
|
@@ -41,7 +51,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|
|
41
51
|
className={cn(
|
|
42
52
|
'group/row mx-1 mb-0.5 flex h-8 cursor-pointer select-none items-center rounded-lg border px-2 text-sm transition-all duration-200',
|
|
43
53
|
isSelected
|
|
44
|
-
? 'border-neutral-200/60 bg-white text-foreground shadow-
|
|
54
|
+
? 'border-neutral-200/60 bg-white text-foreground shadow-elevation-0 ring-1 ring-white/50 ring-inset dark:border-border/50 dark:bg-accent/50 dark:ring-white/10'
|
|
45
55
|
: 'border-transparent text-muted-foreground hover:border-neutral-200/50 hover:bg-white/40 hover:text-foreground dark:hover:border-border/40 dark:hover:bg-accent/30',
|
|
46
56
|
)}
|
|
47
57
|
onClick={handleClick}
|
|
@@ -125,6 +135,8 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|
|
125
135
|
export function ZonePanel() {
|
|
126
136
|
const nodes = useScene((state) => state.nodes)
|
|
127
137
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
138
|
+
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
|
139
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
128
140
|
const setPhase = useEditor((state) => state.setPhase)
|
|
129
141
|
const setMode = useEditor((state) => state.setMode)
|
|
130
142
|
const setTool = useEditor((state) => state.setTool)
|
|
@@ -133,6 +145,7 @@ export function ZonePanel() {
|
|
|
133
145
|
const levelZones = Object.values(nodes).filter(
|
|
134
146
|
(node): node is ZoneNode => node.type === 'zone' && node.parentId === currentLevelId,
|
|
135
147
|
)
|
|
148
|
+
const selectedZone = levelZones.find((zone) => zone.id === selectedZoneId)
|
|
136
149
|
|
|
137
150
|
const handleAddZone = () => {
|
|
138
151
|
if (currentLevelId) {
|
|
@@ -142,6 +155,17 @@ export function ZonePanel() {
|
|
|
142
155
|
}
|
|
143
156
|
}
|
|
144
157
|
|
|
158
|
+
const deleteSelectedZone = (withContent: boolean) => {
|
|
159
|
+
if (!selectedZone) return
|
|
160
|
+
const scene = useScene.getState()
|
|
161
|
+
const ids = withContent
|
|
162
|
+
? [selectedZone.id as AnyNodeId, ...collectZoneContentIds(scene.nodes, selectedZone)]
|
|
163
|
+
: [selectedZone.id as AnyNodeId]
|
|
164
|
+
sfxEmitter.emit('sfx:structure-delete')
|
|
165
|
+
scene.deleteNodes(Array.from(new Set(ids)))
|
|
166
|
+
setSelection({ selectedIds: [], zoneId: null })
|
|
167
|
+
}
|
|
168
|
+
|
|
145
169
|
if (!currentLevelId) {
|
|
146
170
|
return (
|
|
147
171
|
<div className="px-3 py-4 text-muted-foreground text-sm">
|
|
@@ -162,6 +186,31 @@ export function ZonePanel() {
|
|
|
162
186
|
) : (
|
|
163
187
|
levelZones.map((zone) => <ZoneItem key={zone.id} zone={zone} />)
|
|
164
188
|
)}
|
|
189
|
+
{selectedZone ? (
|
|
190
|
+
<PanelSection className="mt-2 border-t" title="Actions">
|
|
191
|
+
<ActionButton
|
|
192
|
+
className="w-full flex-none"
|
|
193
|
+
icon={<Save className="h-4 w-4" />}
|
|
194
|
+
label="Save to catalog"
|
|
195
|
+
onClick={() => emitter.emit('room-preset:create', { zoneId: selectedZone.id })}
|
|
196
|
+
type="button"
|
|
197
|
+
/>
|
|
198
|
+
<ActionButton
|
|
199
|
+
className="w-full flex-none"
|
|
200
|
+
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
201
|
+
label="Delete"
|
|
202
|
+
onClick={() => deleteSelectedZone(false)}
|
|
203
|
+
type="button"
|
|
204
|
+
/>
|
|
205
|
+
<ActionButton
|
|
206
|
+
className="w-full flex-none"
|
|
207
|
+
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
208
|
+
label="Delete with contents"
|
|
209
|
+
onClick={() => deleteSelectedZone(true)}
|
|
210
|
+
type="button"
|
|
211
|
+
/>
|
|
212
|
+
</PanelSection>
|
|
213
|
+
) : null}
|
|
165
214
|
</div>
|
|
166
215
|
)
|
|
167
216
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
|
+
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
4
5
|
import { cn } from './../../../lib/utils'
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip'
|
|
5
7
|
|
|
6
8
|
export type SidebarTab = {
|
|
7
9
|
id: string
|
|
8
10
|
label: string
|
|
9
11
|
mobileDefaultSnap?: number
|
|
10
12
|
mobileIcon?: ReactNode
|
|
13
|
+
/** Desktop icon shown in the vertical rail (v2 layout). */
|
|
14
|
+
icon?: ReactNode
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
interface TabBarProps {
|
|
@@ -30,7 +34,11 @@ export function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {
|
|
|
30
34
|
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
|
|
31
35
|
)}
|
|
32
36
|
key={tab.id}
|
|
33
|
-
onClick={() =>
|
|
37
|
+
onClick={() => {
|
|
38
|
+
triggerSFX('sfx:menu-click')
|
|
39
|
+
onTabChange(tab.id)
|
|
40
|
+
}}
|
|
41
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
34
42
|
type="button"
|
|
35
43
|
>
|
|
36
44
|
{tab.label}
|
|
@@ -40,3 +48,55 @@ export function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {
|
|
|
40
48
|
</div>
|
|
41
49
|
)
|
|
42
50
|
}
|
|
51
|
+
|
|
52
|
+
interface IconRailProps {
|
|
53
|
+
tabs: SidebarTab[]
|
|
54
|
+
/** Highlighted tab. Stays highlighted while the panel is collapsed. */
|
|
55
|
+
activeTab: string
|
|
56
|
+
/** True when the panel beside the rail is collapsed. */
|
|
57
|
+
collapsed: boolean
|
|
58
|
+
/** Clicking a rail icon: switch tab, or toggle the panel (see layout). */
|
|
59
|
+
onIconClick: (id: string) => void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Vertical icon rail for the v2 left column. Always visible (even when the
|
|
64
|
+
* panel is collapsed) so the user can reopen the panel by clicking an icon.
|
|
65
|
+
* The label renders as a hover tooltip on the right.
|
|
66
|
+
*/
|
|
67
|
+
export function IconRail({ tabs, activeTab, collapsed, onIconClick }: IconRailProps) {
|
|
68
|
+
return (
|
|
69
|
+
<TooltipProvider delayDuration={0} disableHoverableContent>
|
|
70
|
+
<div className="flex h-full w-14 shrink-0 flex-col items-center gap-1 border-border/50 border-r py-2">
|
|
71
|
+
{tabs.map((tab) => {
|
|
72
|
+
// Only show the active highlight while the panel is open. When
|
|
73
|
+
// collapsed nothing is "open", so every icon reads as unselected.
|
|
74
|
+
const showActive = activeTab === tab.id && !collapsed
|
|
75
|
+
return (
|
|
76
|
+
<Tooltip key={tab.id}>
|
|
77
|
+
<TooltipTrigger asChild>
|
|
78
|
+
<button
|
|
79
|
+
className={cn(
|
|
80
|
+
'group flex h-11 w-11 items-center justify-center rounded-xl transition-all duration-200 [&_img]:transition-[opacity,filter] [&_img]:duration-200',
|
|
81
|
+
showActive
|
|
82
|
+
? 'bg-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
|
|
83
|
+
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
|
|
84
|
+
)}
|
|
85
|
+
onClick={() => {
|
|
86
|
+
triggerSFX('sfx:menu-click')
|
|
87
|
+
onIconClick(tab.id)
|
|
88
|
+
}}
|
|
89
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
90
|
+
type="button"
|
|
91
|
+
>
|
|
92
|
+
{tab.icon ?? tab.label.charAt(0)}
|
|
93
|
+
</button>
|
|
94
|
+
</TooltipTrigger>
|
|
95
|
+
<TooltipContent side="right">{tab.label}</TooltipContent>
|
|
96
|
+
</Tooltip>
|
|
97
|
+
)
|
|
98
|
+
})}
|
|
99
|
+
</div>
|
|
100
|
+
</TooltipProvider>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -17,7 +17,7 @@ const sliderVariants = cva(
|
|
|
17
17
|
[&_[data-slot=slider-track]]:border
|
|
18
18
|
[&_[data-slot=slider-track]]:border-neutral-300
|
|
19
19
|
[&_[data-slot=slider-track]]:bg-white/50
|
|
20
|
-
[&_[data-slot=slider-track]]:shadow-
|
|
20
|
+
[&_[data-slot=slider-track]]:shadow-elevation-0
|
|
21
21
|
[&_[data-slot=slider-track]]:ring-1
|
|
22
22
|
[&_[data-slot=slider-track]]:ring-white
|
|
23
23
|
[&_[data-slot=slider-track]]:ring-inset
|
|
@@ -10,13 +10,35 @@ import {
|
|
|
10
10
|
useScene,
|
|
11
11
|
type ZoneNode,
|
|
12
12
|
} from '@pascal-app/core'
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import {
|
|
14
|
+
CLAY_PALETTE,
|
|
15
|
+
type EdgeMode,
|
|
16
|
+
getSceneTheme,
|
|
17
|
+
SCENE_THEMES,
|
|
18
|
+
useViewer,
|
|
19
|
+
} from '@pascal-app/viewer'
|
|
20
|
+
import {
|
|
21
|
+
ArrowLeft,
|
|
22
|
+
Box,
|
|
23
|
+
Camera,
|
|
24
|
+
Check,
|
|
25
|
+
ChevronRight,
|
|
26
|
+
Diamond,
|
|
27
|
+
Layers,
|
|
28
|
+
PenLine,
|
|
29
|
+
Sparkles,
|
|
30
|
+
} from 'lucide-react'
|
|
16
31
|
import Link from 'next/link'
|
|
17
32
|
import { useShallow } from 'zustand/react/shallow'
|
|
33
|
+
import { getLevelDisplayName } from '../lib/level-name'
|
|
18
34
|
import { cn } from '../lib/utils'
|
|
19
35
|
import { ActionButton } from './ui/action-menu/action-button'
|
|
36
|
+
import {
|
|
37
|
+
DropdownMenu,
|
|
38
|
+
DropdownMenuContent,
|
|
39
|
+
DropdownMenuItem,
|
|
40
|
+
DropdownMenuTrigger,
|
|
41
|
+
} from './ui/primitives/dropdown-menu'
|
|
20
42
|
import { TooltipProvider } from './ui/primitives/tooltip'
|
|
21
43
|
|
|
22
44
|
type ProjectOwner = {
|
|
@@ -60,6 +82,143 @@ const wallModeConfig = {
|
|
|
60
82
|
},
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
const SHADING_OPTIONS = [
|
|
86
|
+
{ id: 'solid', name: 'Solid', detail: 'Flat and fast — no ambient occlusion', icon: Box },
|
|
87
|
+
{ id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles },
|
|
88
|
+
] as const
|
|
89
|
+
|
|
90
|
+
function RenderModeMenu() {
|
|
91
|
+
const shading = useViewer((s) => s.shading)
|
|
92
|
+
const active = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0]
|
|
93
|
+
const ActiveIcon = active.icon
|
|
94
|
+
return (
|
|
95
|
+
<DropdownMenu>
|
|
96
|
+
<DropdownMenuTrigger asChild>
|
|
97
|
+
<ActionButton
|
|
98
|
+
className="text-muted-foreground/80 hover:bg-white/5 hover:text-foreground"
|
|
99
|
+
label={`Render: ${active.name}`}
|
|
100
|
+
size="icon"
|
|
101
|
+
tooltipSide="top"
|
|
102
|
+
variant="ghost"
|
|
103
|
+
>
|
|
104
|
+
<ActiveIcon className="h-6 w-6" />
|
|
105
|
+
</ActionButton>
|
|
106
|
+
</DropdownMenuTrigger>
|
|
107
|
+
<DropdownMenuContent align="center" className="min-w-56" side="top">
|
|
108
|
+
{SHADING_OPTIONS.map((option) => {
|
|
109
|
+
const OptionIcon = option.icon
|
|
110
|
+
return (
|
|
111
|
+
<DropdownMenuItem
|
|
112
|
+
key={option.id}
|
|
113
|
+
onSelect={() => useViewer.getState().setShading(option.id)}
|
|
114
|
+
>
|
|
115
|
+
<OptionIcon />
|
|
116
|
+
<div className="flex flex-col">
|
|
117
|
+
<span className="text-foreground">{option.name}</span>
|
|
118
|
+
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
119
|
+
</div>
|
|
120
|
+
{shading === option.id ? <Check className="ml-auto text-foreground" /> : null}
|
|
121
|
+
</DropdownMenuItem>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
</DropdownMenuContent>
|
|
125
|
+
</DropdownMenu>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function SceneThemeMenu() {
|
|
130
|
+
const sceneTheme = useViewer((s) => s.sceneTheme)
|
|
131
|
+
const active = getSceneTheme(sceneTheme)
|
|
132
|
+
return (
|
|
133
|
+
<DropdownMenu>
|
|
134
|
+
<DropdownMenuTrigger asChild>
|
|
135
|
+
<ActionButton
|
|
136
|
+
className="text-muted-foreground/80 hover:bg-white/5 hover:text-foreground"
|
|
137
|
+
label={`Theme: ${active.name}`}
|
|
138
|
+
size="icon"
|
|
139
|
+
tooltipSide="top"
|
|
140
|
+
variant="ghost"
|
|
141
|
+
>
|
|
142
|
+
<Icon color="currentColor" height={24} icon="lucide:swatch-book" width={24} />
|
|
143
|
+
</ActionButton>
|
|
144
|
+
</DropdownMenuTrigger>
|
|
145
|
+
<DropdownMenuContent align="center" className="min-w-48" side="top">
|
|
146
|
+
{SCENE_THEMES.map((sceneThemeOption) => {
|
|
147
|
+
const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
|
|
148
|
+
(role) => sceneThemeOption.clayTints?.[role] ?? CLAY_PALETTE[role],
|
|
149
|
+
)
|
|
150
|
+
return (
|
|
151
|
+
<DropdownMenuItem
|
|
152
|
+
key={sceneThemeOption.id}
|
|
153
|
+
onSelect={() => useViewer.getState().setSceneTheme(sceneThemeOption.id)}
|
|
154
|
+
>
|
|
155
|
+
<span
|
|
156
|
+
className="grid h-5 w-5 shrink-0 grid-cols-2 overflow-hidden rounded-sm border border-black/10"
|
|
157
|
+
style={{ backgroundColor: sceneThemeOption.background }}
|
|
158
|
+
>
|
|
159
|
+
{swatches.map((color, index) => (
|
|
160
|
+
<span
|
|
161
|
+
key={`${sceneThemeOption.id}-${index}`}
|
|
162
|
+
style={{ backgroundColor: color }}
|
|
163
|
+
/>
|
|
164
|
+
))}
|
|
165
|
+
</span>
|
|
166
|
+
<span className="text-foreground">{sceneThemeOption.name}</span>
|
|
167
|
+
{sceneTheme === sceneThemeOption.id ? (
|
|
168
|
+
<Check className="ml-auto text-foreground" />
|
|
169
|
+
) : null}
|
|
170
|
+
</DropdownMenuItem>
|
|
171
|
+
)
|
|
172
|
+
})}
|
|
173
|
+
</DropdownMenuContent>
|
|
174
|
+
</DropdownMenu>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const EDGE_OPTIONS = [
|
|
179
|
+
{ id: 'off', name: 'Off', detail: 'No edge lines' },
|
|
180
|
+
{ id: 'soft', name: 'Soft', detail: 'Faint outline of major creases' },
|
|
181
|
+
{ id: 'strong', name: 'Strong', detail: 'Crisp, opaque edge lines' },
|
|
182
|
+
] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[]
|
|
183
|
+
|
|
184
|
+
function EdgesMenu() {
|
|
185
|
+
const edges = useViewer((s) => s.edges)
|
|
186
|
+
const active = EDGE_OPTIONS.find((o) => o.id === edges) ?? EDGE_OPTIONS[0]
|
|
187
|
+
return (
|
|
188
|
+
<DropdownMenu>
|
|
189
|
+
<DropdownMenuTrigger asChild>
|
|
190
|
+
<ActionButton
|
|
191
|
+
className={
|
|
192
|
+
edges === 'off'
|
|
193
|
+
? 'text-muted-foreground/80 hover:bg-white/5 hover:text-foreground'
|
|
194
|
+
: 'bg-white/10 text-foreground'
|
|
195
|
+
}
|
|
196
|
+
label={`Edges: ${active.name}`}
|
|
197
|
+
size="icon"
|
|
198
|
+
tooltipSide="top"
|
|
199
|
+
variant="ghost"
|
|
200
|
+
>
|
|
201
|
+
<PenLine className="h-6 w-6" />
|
|
202
|
+
</ActionButton>
|
|
203
|
+
</DropdownMenuTrigger>
|
|
204
|
+
<DropdownMenuContent align="center" className="min-w-56" side="top">
|
|
205
|
+
{EDGE_OPTIONS.map((option) => (
|
|
206
|
+
<DropdownMenuItem
|
|
207
|
+
key={option.id}
|
|
208
|
+
onSelect={() => useViewer.getState().setEdges(option.id)}
|
|
209
|
+
>
|
|
210
|
+
<div className="flex flex-col">
|
|
211
|
+
<span className="text-foreground">{option.name}</span>
|
|
212
|
+
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
213
|
+
</div>
|
|
214
|
+
{edges === option.id ? <Check className="ml-auto text-foreground" /> : null}
|
|
215
|
+
</DropdownMenuItem>
|
|
216
|
+
))}
|
|
217
|
+
</DropdownMenuContent>
|
|
218
|
+
</DropdownMenu>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
63
222
|
const getNodeName = (node: AnyNode): string => {
|
|
64
223
|
if ('name' in node && node.name) return node.name
|
|
65
224
|
if (node.type === 'wall') return 'Wall'
|
|
@@ -93,7 +252,6 @@ export const ViewerOverlay = ({
|
|
|
93
252
|
const cameraMode = useViewer((s) => s.cameraMode)
|
|
94
253
|
const levelMode = useViewer((s) => s.levelMode)
|
|
95
254
|
const wallMode = useViewer((s) => s.wallMode)
|
|
96
|
-
const theme = useViewer((s) => s.theme)
|
|
97
255
|
|
|
98
256
|
// Subscribe only to the specific nodes we read so that creating an unrelated
|
|
99
257
|
// node elsewhere in the scene doesn't re-render this overlay.
|
|
@@ -206,7 +364,7 @@ export const ViewerOverlay = ({
|
|
|
206
364
|
className={`truncate transition-colors ${zone ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
|
|
207
365
|
onClick={() => handleBreadcrumbClick('level')}
|
|
208
366
|
>
|
|
209
|
-
{level
|
|
367
|
+
{getLevelDisplayName(level)}
|
|
210
368
|
</button>
|
|
211
369
|
</>
|
|
212
370
|
)}
|
|
@@ -280,53 +438,6 @@ export const ViewerOverlay = ({
|
|
|
280
438
|
<div className="dark absolute bottom-6 left-1/2 z-20 -translate-x-1/2 text-foreground">
|
|
281
439
|
<TooltipProvider delayDuration={0}>
|
|
282
440
|
<div className="pointer-events-auto flex h-14 flex-row items-center justify-center gap-1.5 rounded-2xl border border-border/40 bg-background/95 p-1.5 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out">
|
|
283
|
-
{/* Theme Toggle */}
|
|
284
|
-
<button
|
|
285
|
-
aria-label="Toggle theme"
|
|
286
|
-
className="flex h-[36px] shrink-0 cursor-pointer items-center rounded-full border border-border/50 bg-accent/50 p-1"
|
|
287
|
-
onClick={() => useViewer.getState().setTheme(theme === 'dark' ? 'light' : 'dark')}
|
|
288
|
-
type="button"
|
|
289
|
-
>
|
|
290
|
-
<div className="relative flex">
|
|
291
|
-
{/* Sliding Background */}
|
|
292
|
-
<motion.div
|
|
293
|
-
animate={{
|
|
294
|
-
x: theme === 'light' ? '100%' : '0%',
|
|
295
|
-
}}
|
|
296
|
-
className="absolute inset-0 rounded-full bg-white shadow-sm dark:bg-white/20"
|
|
297
|
-
initial={false}
|
|
298
|
-
style={{ width: '50%' }}
|
|
299
|
-
transition={{
|
|
300
|
-
type: 'spring',
|
|
301
|
-
stiffness: 500,
|
|
302
|
-
damping: 35,
|
|
303
|
-
}}
|
|
304
|
-
/>
|
|
305
|
-
|
|
306
|
-
{/* Dark Mode Icon */}
|
|
307
|
-
<div
|
|
308
|
-
className={cn(
|
|
309
|
-
'pointer-events-none relative z-10 flex h-7 w-9 items-center justify-center rounded-full transition-colors duration-200',
|
|
310
|
-
theme === 'dark' ? 'text-foreground' : 'text-muted-foreground',
|
|
311
|
-
)}
|
|
312
|
-
>
|
|
313
|
-
<Moon className="h-4 w-4" />
|
|
314
|
-
</div>
|
|
315
|
-
|
|
316
|
-
{/* Light Mode Icon */}
|
|
317
|
-
<div
|
|
318
|
-
className={cn(
|
|
319
|
-
'pointer-events-none relative z-10 flex h-7 w-9 items-center justify-center rounded-full transition-colors duration-200',
|
|
320
|
-
theme === 'light' ? 'text-foreground' : 'text-muted-foreground',
|
|
321
|
-
)}
|
|
322
|
-
>
|
|
323
|
-
<Sun className="h-4 w-4" />
|
|
324
|
-
</div>
|
|
325
|
-
</div>
|
|
326
|
-
</button>
|
|
327
|
-
|
|
328
|
-
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
329
|
-
|
|
330
441
|
{/* Scans and Guides Visibility */}
|
|
331
442
|
{canShowScans && (
|
|
332
443
|
<ActionButton
|
|
@@ -392,6 +503,12 @@ export const ViewerOverlay = ({
|
|
|
392
503
|
<Camera className="h-6 w-6" />
|
|
393
504
|
</ActionButton>
|
|
394
505
|
|
|
506
|
+
<RenderModeMenu />
|
|
507
|
+
|
|
508
|
+
<SceneThemeMenu />
|
|
509
|
+
|
|
510
|
+
<EdgesMenu />
|
|
511
|
+
|
|
395
512
|
{/* Level Mode */}
|
|
396
513
|
<ActionButton
|
|
397
514
|
className={cn(
|
|
@@ -12,7 +12,7 @@ export const ViewerZoneSystem = () => {
|
|
|
12
12
|
const structureLayer = useEditor.getState().structureLayer
|
|
13
13
|
const nodes = useScene.getState().nodes
|
|
14
14
|
|
|
15
|
-
sceneRegistry.byType.zone
|
|
15
|
+
sceneRegistry.byType.zone!.forEach((id) => {
|
|
16
16
|
const obj = sceneRegistry.nodes.get(id)
|
|
17
17
|
if (!obj) return
|
|
18
18
|
|