@pascal-app/editor 0.8.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- 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 +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- 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 +5829 -12005
- 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 +178 -149
- 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 +433 -156
- 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 +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- 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-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- 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 +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- 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 +1 -1
- 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 +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- 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 -538
- 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 +56 -0
- 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 +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- 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 +4 -6
- 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 +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -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 +10 -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 +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- 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.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- 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 +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- 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 +320 -37
- 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/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- 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 -264
- 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 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- 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 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- 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 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- 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 -280
- 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 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
3
3
|
import { Layers } from 'lucide-react'
|
|
4
4
|
import { memo, useCallback, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
|
+
import { getDefaultLevelName } from '../../../../../lib/level-name'
|
|
6
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
7
8
|
import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
8
9
|
import { TreeNodeActions } from './tree-node-actions'
|
|
@@ -35,7 +36,7 @@ export const LevelTreeNode = memo(function LevelTreeNode({
|
|
|
35
36
|
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
36
37
|
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
37
38
|
|
|
38
|
-
const defaultName =
|
|
39
|
+
const defaultName = getDefaultLevelName(level)
|
|
39
40
|
|
|
40
41
|
return (
|
|
41
42
|
<TreeNodeWrapper
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import useEditor from './../../../../../store/use-editor'
|
|
6
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
7
|
+
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
8
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
9
|
+
|
|
10
|
+
interface RegistryTreeNodeProps {
|
|
11
|
+
nodeId: AnyNodeId
|
|
12
|
+
depth: number
|
|
13
|
+
isLast?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generic, leaf tree-node row driven entirely by the kind's
|
|
18
|
+
* `def.presentation` (icon + label). Replaces the per-kind boilerplate
|
|
19
|
+
* components that differed only in their default name and icon — today the
|
|
20
|
+
* roof vents (box / ridge / turbine / cupola / eyebrow). Register a kind in
|
|
21
|
+
* `treeNodeByType` against this component instead of authoring another copy.
|
|
22
|
+
*/
|
|
23
|
+
export const RegistryTreeNode = memo(function RegistryTreeNode({
|
|
24
|
+
nodeId,
|
|
25
|
+
depth,
|
|
26
|
+
isLast,
|
|
27
|
+
}: RegistryTreeNodeProps) {
|
|
28
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
29
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
30
|
+
const node = useScene((s) => s.nodes[nodeId])
|
|
31
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
32
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
33
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
34
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
35
|
+
|
|
36
|
+
const presentation = node ? nodeRegistry.get(node.type)?.presentation : undefined
|
|
37
|
+
const icon = presentation?.icon
|
|
38
|
+
const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.png'
|
|
39
|
+
const defaultName = node?.name || presentation?.label || 'Node'
|
|
40
|
+
|
|
41
|
+
const handleClick = useCallback(
|
|
42
|
+
(e: React.MouseEvent) => {
|
|
43
|
+
e.stopPropagation()
|
|
44
|
+
const handled = handleTreeSelection(
|
|
45
|
+
e,
|
|
46
|
+
nodeId,
|
|
47
|
+
useViewer.getState().selection.selectedIds,
|
|
48
|
+
setSelection,
|
|
49
|
+
)
|
|
50
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
51
|
+
useEditor.getState().setPhase('structure')
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
[nodeId, setSelection],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<TreeNodeWrapper
|
|
59
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
60
|
+
depth={depth}
|
|
61
|
+
expanded={false}
|
|
62
|
+
hasChildren={false}
|
|
63
|
+
icon={
|
|
64
|
+
<Image
|
|
65
|
+
alt=""
|
|
66
|
+
className="object-contain opacity-60"
|
|
67
|
+
height={14}
|
|
68
|
+
src={iconSrc}
|
|
69
|
+
width={14}
|
|
70
|
+
/>
|
|
71
|
+
}
|
|
72
|
+
isHovered={isHovered}
|
|
73
|
+
isLast={isLast}
|
|
74
|
+
isSelected={isSelected}
|
|
75
|
+
isVisible={isVisible}
|
|
76
|
+
label={
|
|
77
|
+
<InlineRenameInput
|
|
78
|
+
defaultName={defaultName}
|
|
79
|
+
isEditing={isEditing}
|
|
80
|
+
nodeId={nodeId}
|
|
81
|
+
onStartEditing={() => setIsEditing(true)}
|
|
82
|
+
onStopEditing={() => setIsEditing(false)}
|
|
83
|
+
/>
|
|
84
|
+
}
|
|
85
|
+
nodeId={nodeId}
|
|
86
|
+
onClick={handleClick}
|
|
87
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
88
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
89
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
90
|
+
onToggle={() => {}}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
})
|
|
@@ -6,7 +6,7 @@ import { memo, useCallback, useEffect, useState } from 'react'
|
|
|
6
6
|
import { useShallow } from 'zustand/react/shallow'
|
|
7
7
|
import useEditor from '../../../../../store/use-editor'
|
|
8
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
9
|
-
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
9
|
+
import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
10
10
|
import { TreeNodeActions } from './tree-node-actions'
|
|
11
11
|
import { DropIndicatorLine, useTreeNodeDrag } from './tree-node-drag'
|
|
12
12
|
|
|
@@ -163,12 +163,36 @@ function RoofSegmentTreeNode({
|
|
|
163
163
|
isLast?: boolean
|
|
164
164
|
}) {
|
|
165
165
|
const [isEditing, setIsEditing] = useState(false)
|
|
166
|
+
const [expanded, setExpanded] = useState(true)
|
|
167
|
+
const accessoryIds = useScene(
|
|
168
|
+
useShallow(
|
|
169
|
+
(s) => (s.nodes[node.id] as RoofSegmentNode | undefined)?.children ?? ([] as string[]),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
166
172
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id))
|
|
167
173
|
const isHovered = useViewer((state) => state.hoveredId === node.id)
|
|
168
174
|
const setSelection = useViewer((state) => state.setSelection)
|
|
169
175
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
170
176
|
const { startDrag, isDragging } = useTreeNodeDrag()
|
|
171
177
|
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
return useViewer.subscribe((state) => {
|
|
180
|
+
const { selectedIds } = state.selection
|
|
181
|
+
if (selectedIds.length === 0) return
|
|
182
|
+
const nodes = useScene.getState().nodes
|
|
183
|
+
for (const id of selectedIds) {
|
|
184
|
+
let current = nodes[id as AnyNodeId]
|
|
185
|
+
while (current?.parentId) {
|
|
186
|
+
if (current.parentId === node.id) {
|
|
187
|
+
setExpanded(true)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
current = nodes[current.parentId as AnyNodeId]
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}, [node.id])
|
|
195
|
+
|
|
172
196
|
const handleClick = useCallback(
|
|
173
197
|
(e: React.MouseEvent) => {
|
|
174
198
|
if (isDragging) return
|
|
@@ -192,13 +216,15 @@ function RoofSegmentTreeNode({
|
|
|
192
216
|
|
|
193
217
|
const defaultName = `${node.roofType.charAt(0).toUpperCase() + node.roofType.slice(1)} (${node.width.toFixed(1)}x${node.depth.toFixed(1)}m)`
|
|
194
218
|
|
|
219
|
+
const hasAccessories = accessoryIds.length > 0
|
|
220
|
+
|
|
195
221
|
return (
|
|
196
222
|
<div data-drop-child={node.id}>
|
|
197
223
|
<TreeNodeWrapper
|
|
198
224
|
actions={<TreeNodeActions nodeId={node.id} />}
|
|
199
225
|
depth={depth}
|
|
200
|
-
expanded={
|
|
201
|
-
hasChildren={
|
|
226
|
+
expanded={expanded}
|
|
227
|
+
hasChildren={hasAccessories}
|
|
202
228
|
icon={
|
|
203
229
|
<Image
|
|
204
230
|
alt=""
|
|
@@ -228,8 +254,18 @@ function RoofSegmentTreeNode({
|
|
|
228
254
|
onMouseEnter={() => setHoveredId(node.id)}
|
|
229
255
|
onMouseLeave={() => setHoveredId(null)}
|
|
230
256
|
onPointerDown={handlePointerDown}
|
|
231
|
-
onToggle={() =>
|
|
232
|
-
|
|
257
|
+
onToggle={() => setExpanded((prev) => !prev)}
|
|
258
|
+
>
|
|
259
|
+
{hasAccessories &&
|
|
260
|
+
accessoryIds.map((childId, index) => (
|
|
261
|
+
<TreeNode
|
|
262
|
+
depth={depth + 1}
|
|
263
|
+
isLast={index === accessoryIds.length - 1}
|
|
264
|
+
key={childId}
|
|
265
|
+
nodeId={childId as AnyNodeId}
|
|
266
|
+
/>
|
|
267
|
+
))}
|
|
268
|
+
</TreeNodeWrapper>
|
|
233
269
|
</div>
|
|
234
270
|
)
|
|
235
271
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, type ShelfNode, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import Image from 'next/image'
|
|
6
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
7
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
8
|
+
import useEditor from './../../../../../store/use-editor'
|
|
9
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
10
|
+
import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
11
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
12
|
+
|
|
13
|
+
interface ShelfTreeNodeProps {
|
|
14
|
+
nodeId: ShelfNode['id']
|
|
15
|
+
depth: number
|
|
16
|
+
isLast?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sidebar tree entry for shelf. Mirrors `item-tree-node`'s shape so the
|
|
21
|
+
* shelf's hosted items list as collapsible children — same pattern items
|
|
22
|
+
* use for their nested items. The shelf has its own `children: ItemNode[`id`]`
|
|
23
|
+
* field on the schema; items reparent into it via `def.surfaces` + the
|
|
24
|
+
* placement coordinator's shelf strategy.
|
|
25
|
+
*/
|
|
26
|
+
export const ShelfTreeNode = memo(function ShelfTreeNode({
|
|
27
|
+
nodeId,
|
|
28
|
+
depth,
|
|
29
|
+
isLast,
|
|
30
|
+
}: ShelfTreeNodeProps) {
|
|
31
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
32
|
+
const [expanded, setExpanded] = useState(true)
|
|
33
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
34
|
+
const children = useScene(
|
|
35
|
+
useShallow((s) => (s.nodes[nodeId] as ShelfNode | undefined)?.children ?? []),
|
|
36
|
+
)
|
|
37
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
38
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
39
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
40
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
41
|
+
|
|
42
|
+
// Expand when a descendant is selected — same imperative subscription
|
|
43
|
+
// the item tree-node uses, so we don't re-render when unrelated
|
|
44
|
+
// selection-state ticks.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
return useViewer.subscribe((state) => {
|
|
47
|
+
const { selectedIds } = state.selection
|
|
48
|
+
if (selectedIds.length === 0) return
|
|
49
|
+
const nodes = useScene.getState().nodes
|
|
50
|
+
for (const id of selectedIds) {
|
|
51
|
+
let current = nodes[id as AnyNodeId]
|
|
52
|
+
while (current?.parentId) {
|
|
53
|
+
if (current.parentId === nodeId) {
|
|
54
|
+
setExpanded(true)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
current = nodes[current.parentId as AnyNodeId]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}, [nodeId])
|
|
62
|
+
|
|
63
|
+
const handleClick = useCallback(
|
|
64
|
+
(e: React.MouseEvent) => {
|
|
65
|
+
e.stopPropagation()
|
|
66
|
+
const handled = handleTreeSelection(
|
|
67
|
+
e,
|
|
68
|
+
nodeId,
|
|
69
|
+
useViewer.getState().selection.selectedIds,
|
|
70
|
+
setSelection,
|
|
71
|
+
)
|
|
72
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
73
|
+
useEditor.getState().setPhase('structure')
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[nodeId, setSelection],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
80
|
+
const handleMouseEnter = useCallback(() => setHoveredId(nodeId), [nodeId, setHoveredId])
|
|
81
|
+
const handleMouseLeave = useCallback(() => setHoveredId(null), [setHoveredId])
|
|
82
|
+
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
|
|
83
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
84
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
85
|
+
|
|
86
|
+
const hasChildren = children.length > 0
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<TreeNodeWrapper
|
|
90
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
91
|
+
depth={depth}
|
|
92
|
+
expanded={expanded}
|
|
93
|
+
hasChildren={hasChildren}
|
|
94
|
+
icon={
|
|
95
|
+
<Image alt="" className="object-contain" height={14} src="/icons/shelf.png" width={14} />
|
|
96
|
+
}
|
|
97
|
+
isHovered={isHovered}
|
|
98
|
+
isLast={isLast}
|
|
99
|
+
isSelected={isSelected}
|
|
100
|
+
isVisible={isVisible}
|
|
101
|
+
label={
|
|
102
|
+
<InlineRenameInput
|
|
103
|
+
defaultName="Shelf"
|
|
104
|
+
isEditing={isEditing}
|
|
105
|
+
nodeId={nodeId}
|
|
106
|
+
onStartEditing={handleStartEditing}
|
|
107
|
+
onStopEditing={handleStopEditing}
|
|
108
|
+
/>
|
|
109
|
+
}
|
|
110
|
+
nodeId={nodeId}
|
|
111
|
+
onClick={handleClick}
|
|
112
|
+
onDoubleClick={handleDoubleClick}
|
|
113
|
+
onMouseEnter={handleMouseEnter}
|
|
114
|
+
onMouseLeave={handleMouseLeave}
|
|
115
|
+
onToggle={handleToggle}
|
|
116
|
+
>
|
|
117
|
+
{hasChildren &&
|
|
118
|
+
children.map((childId, index) => (
|
|
119
|
+
<TreeNode
|
|
120
|
+
depth={depth + 1}
|
|
121
|
+
isLast={index === children.length - 1}
|
|
122
|
+
key={childId}
|
|
123
|
+
nodeId={childId}
|
|
124
|
+
/>
|
|
125
|
+
))}
|
|
126
|
+
</TreeNodeWrapper>
|
|
127
|
+
)
|
|
128
|
+
})
|
|
@@ -91,8 +91,11 @@ function calculatePolygonArea(polygon: Array<[number, number]>): number {
|
|
|
91
91
|
|
|
92
92
|
for (let i = 0; i < n; i++) {
|
|
93
93
|
const j = (i + 1) % n
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const pi = polygon[i]
|
|
95
|
+
const pj = polygon[j]
|
|
96
|
+
if (!(pi && pj)) continue
|
|
97
|
+
area += pi[0] * pj[1]
|
|
98
|
+
area -= pj[0] * pi[1]
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
return Math.abs(area) / 2
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type AnyNodeId, type SolarPanelNode, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import useEditor from './../../../../../store/use-editor'
|
|
6
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
7
|
+
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
8
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
9
|
+
|
|
10
|
+
interface SolarPanelTreeNodeProps {
|
|
11
|
+
nodeId: AnyNodeId
|
|
12
|
+
depth: number
|
|
13
|
+
isLast?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SolarPanelTreeNode = memo(function SolarPanelTreeNode({
|
|
17
|
+
nodeId,
|
|
18
|
+
depth,
|
|
19
|
+
isLast,
|
|
20
|
+
}: SolarPanelTreeNodeProps) {
|
|
21
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
22
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
23
|
+
const node = useScene((s) => s.nodes[nodeId] as SolarPanelNode | undefined)
|
|
24
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
25
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
26
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
27
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
28
|
+
|
|
29
|
+
const handleClick = useCallback(
|
|
30
|
+
(e: React.MouseEvent) => {
|
|
31
|
+
e.stopPropagation()
|
|
32
|
+
const handled = handleTreeSelection(
|
|
33
|
+
e,
|
|
34
|
+
nodeId,
|
|
35
|
+
useViewer.getState().selection.selectedIds,
|
|
36
|
+
setSelection,
|
|
37
|
+
)
|
|
38
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
39
|
+
useEditor.getState().setPhase('structure')
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[nodeId, setSelection],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const defaultName = node?.name || 'Solar Panel'
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<TreeNodeWrapper
|
|
49
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
50
|
+
depth={depth}
|
|
51
|
+
expanded={false}
|
|
52
|
+
hasChildren={false}
|
|
53
|
+
icon={
|
|
54
|
+
<Image
|
|
55
|
+
alt=""
|
|
56
|
+
className="object-contain opacity-60"
|
|
57
|
+
height={14}
|
|
58
|
+
src="/icons/roof.png"
|
|
59
|
+
width={14}
|
|
60
|
+
/>
|
|
61
|
+
}
|
|
62
|
+
isHovered={isHovered}
|
|
63
|
+
isLast={isLast}
|
|
64
|
+
isSelected={isSelected}
|
|
65
|
+
isVisible={isVisible}
|
|
66
|
+
label={
|
|
67
|
+
<InlineRenameInput
|
|
68
|
+
defaultName={defaultName}
|
|
69
|
+
isEditing={isEditing}
|
|
70
|
+
nodeId={nodeId}
|
|
71
|
+
onStartEditing={() => setIsEditing(true)}
|
|
72
|
+
onStopEditing={() => setIsEditing(false)}
|
|
73
|
+
/>
|
|
74
|
+
}
|
|
75
|
+
nodeId={nodeId}
|
|
76
|
+
onClick={handleClick}
|
|
77
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
78
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
79
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
80
|
+
onToggle={() => {}}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
})
|
|
@@ -50,7 +50,13 @@ export const SpawnTreeNode = memo(function SpawnTreeNode({
|
|
|
50
50
|
expanded={false}
|
|
51
51
|
hasChildren={false}
|
|
52
52
|
icon={
|
|
53
|
-
<Image
|
|
53
|
+
<Image
|
|
54
|
+
alt=""
|
|
55
|
+
className="object-contain"
|
|
56
|
+
height={14}
|
|
57
|
+
src="/icons/spawn-point.png"
|
|
58
|
+
width={14}
|
|
59
|
+
/>
|
|
54
60
|
}
|
|
55
61
|
isHovered={isHovered}
|
|
56
62
|
isLast={isLast}
|
|
@@ -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,45 +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 (
|
|
86
|
-
<BuildingTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `building_${string}`} />
|
|
87
|
-
)
|
|
88
|
-
case 'ceiling':
|
|
89
|
-
return <CeilingTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
90
|
-
case 'column':
|
|
91
|
-
return <ColumnTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
92
|
-
case 'level':
|
|
93
|
-
return <LevelTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `level_${string}`} />
|
|
94
|
-
case 'slab':
|
|
95
|
-
return <SlabTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
96
|
-
case 'spawn':
|
|
97
|
-
return <SpawnTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `spawn_${string}`} />
|
|
98
|
-
case 'wall':
|
|
99
|
-
return <WallTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
100
|
-
case 'fence':
|
|
101
|
-
return <FenceTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
102
|
-
case 'roof':
|
|
103
|
-
return <RoofTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
104
|
-
case 'stair':
|
|
105
|
-
return <StairTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
106
|
-
case 'item':
|
|
107
|
-
return <ItemTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
108
|
-
case 'door':
|
|
109
|
-
return <DoorTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
110
|
-
case 'window':
|
|
111
|
-
return <WindowTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
112
|
-
case 'zone':
|
|
113
|
-
return <ZoneTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `zone_${string}`} />
|
|
114
|
-
default:
|
|
115
|
-
return null
|
|
116
|
-
}
|
|
146
|
+
const Component = treeNodeByType[nodeType]
|
|
147
|
+
if (!Component) return null
|
|
148
|
+
return <Component depth={depth} isLast={isLast} nodeId={nodeId} />
|
|
117
149
|
})
|
|
118
150
|
|
|
119
151
|
interface TreeNodeWrapperProps {
|
|
@@ -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 })
|
|
@@ -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
|
}
|