@pascal-app/editor 0.4.0 → 0.5.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 +5 -5
- package/src/components/editor/floating-action-menu.tsx +101 -29
- package/src/components/editor/floating-building-action-menu.tsx +69 -0
- package/src/components/editor/floorplan-panel.tsx +31 -13
- package/src/components/editor/index.tsx +219 -167
- package/src/components/editor/node-action-menu.tsx +26 -10
- package/src/components/editor/selection-manager.tsx +38 -2
- package/src/components/editor/thumbnail-generator.tsx +245 -64
- package/src/components/systems/stair/stair-edit-system.tsx +27 -5
- package/src/components/tools/building/move-building-tool.tsx +157 -0
- package/src/components/tools/door/door-math.ts +1 -1
- package/src/components/tools/door/door-tool.tsx +19 -7
- package/src/components/tools/door/move-door-tool.tsx +17 -8
- package/src/components/tools/fence/fence-drafting.ts +125 -0
- package/src/components/tools/fence/fence-tool.tsx +190 -0
- package/src/components/tools/fence/move-fence-tool.tsx +223 -0
- package/src/components/tools/item/item-tool.tsx +3 -3
- package/src/components/tools/item/move-tool.tsx +7 -0
- package/src/components/tools/item/placement-strategies.ts +15 -7
- package/src/components/tools/item/use-placement-coordinator.tsx +89 -14
- package/src/components/tools/roof/move-roof-tool.tsx +5 -2
- package/src/components/tools/roof/roof-tool.tsx +6 -6
- package/src/components/tools/select/box-select-tool.tsx +2 -2
- package/src/components/tools/shared/polygon-editor.tsx +2 -2
- package/src/components/tools/slab/slab-tool.tsx +4 -4
- package/src/components/tools/stair/stair-defaults.ts +10 -0
- package/src/components/tools/stair/stair-tool.tsx +29 -6
- package/src/components/tools/tool-manager.tsx +42 -14
- package/src/components/tools/wall/wall-tool.tsx +19 -29
- package/src/components/tools/window/move-window-tool.tsx +17 -8
- package/src/components/tools/window/window-math.ts +1 -1
- package/src/components/tools/window/window-tool.tsx +19 -7
- package/src/components/tools/zone/zone-tool.tsx +7 -7
- package/src/components/ui/action-menu/structure-tools.tsx +1 -0
- package/src/components/ui/helpers/building-helper.tsx +32 -0
- package/src/components/ui/helpers/helper-manager.tsx +2 -0
- package/src/components/ui/panels/fence-panel.tsx +184 -0
- package/src/components/ui/panels/panel-manager.tsx +3 -0
- package/src/components/ui/panels/stair-panel.tsx +206 -33
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +22 -15
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +60 -52
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +35 -24
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +59 -40
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +14 -13
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +59 -52
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +27 -22
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +66 -49
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +35 -36
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +66 -49
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +11 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +17 -14
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +57 -53
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +35 -24
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +22 -27
- package/src/components/viewer-overlay.tsx +1 -0
- package/src/hooks/use-auto-save.ts +3 -6
- package/src/hooks/use-contextual-tools.ts +10 -2
- package/src/hooks/use-grid-events.ts +13 -1
- package/src/hooks/use-keyboard.ts +4 -0
- package/src/store/use-editor.tsx +7 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type AnyNodeId, type ItemNode, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import Image from 'next/image'
|
|
4
|
-
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
5
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
5
6
|
import useEditor from './../../../../../store/use-editor'
|
|
6
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
7
8
|
import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
@@ -18,67 +19,73 @@ const CATEGORY_ICONS: Record<string, string> = {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
interface ItemTreeNodeProps {
|
|
21
|
-
|
|
22
|
+
nodeId: AnyNodeId
|
|
22
23
|
depth: number
|
|
23
24
|
isLast?: boolean
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function ItemTreeNode({
|
|
27
|
+
export function ItemTreeNode({ nodeId, depth, isLast }: ItemTreeNodeProps) {
|
|
27
28
|
const [isEditing, setIsEditing] = useState(false)
|
|
28
29
|
const [expanded, setExpanded] = useState(true)
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
31
|
+
const children = useScene(
|
|
32
|
+
useShallow((s) => (s.nodes[nodeId] as ItemNode | undefined)?.children ?? []),
|
|
33
|
+
)
|
|
34
|
+
const asset = useScene((s) => (s.nodes[nodeId] as ItemNode | undefined)?.asset)
|
|
35
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
36
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
33
37
|
const setSelection = useViewer((state) => state.setSelection)
|
|
34
38
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
35
39
|
|
|
40
|
+
// Expand when a descendant is selected — imperative to avoid subscribing to the full selectedIds array
|
|
36
41
|
useEffect(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
return useViewer.subscribe((state) => {
|
|
43
|
+
const { selectedIds } = state.selection
|
|
44
|
+
if (selectedIds.length === 0) return
|
|
45
|
+
const nodes = useScene.getState().nodes
|
|
46
|
+
for (const id of selectedIds) {
|
|
47
|
+
let current = nodes[id as AnyNodeId]
|
|
48
|
+
while (current?.parentId) {
|
|
49
|
+
if (current.parentId === nodeId) {
|
|
50
|
+
setExpanded(true)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
current = nodes[current.parentId as AnyNodeId]
|
|
46
54
|
}
|
|
47
|
-
current = nodes[current.parentId as AnyNodeId]
|
|
48
55
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (isDescendant) {
|
|
52
|
-
setExpanded(true)
|
|
53
|
-
}
|
|
54
|
-
}, [selectedIds, node.id])
|
|
55
|
-
|
|
56
|
-
const handleClick = (e: React.MouseEvent) => {
|
|
57
|
-
e.stopPropagation()
|
|
58
|
-
const handled = handleTreeSelection(e, node.id, selectedIds, setSelection)
|
|
59
|
-
if (!handled && useEditor.getState().phase === 'structure') {
|
|
60
|
-
useEditor.getState().setPhase('furnish')
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const handleDoubleClick = () => {
|
|
65
|
-
focusTreeNode(node.id)
|
|
66
|
-
}
|
|
56
|
+
})
|
|
57
|
+
}, [nodeId])
|
|
67
58
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
const handleClick = useCallback(
|
|
60
|
+
(e: React.MouseEvent) => {
|
|
61
|
+
e.stopPropagation()
|
|
62
|
+
const handled = handleTreeSelection(
|
|
63
|
+
e,
|
|
64
|
+
nodeId,
|
|
65
|
+
useViewer.getState().selection.selectedIds,
|
|
66
|
+
setSelection,
|
|
67
|
+
)
|
|
68
|
+
if (!handled && useEditor.getState().phase === 'structure') {
|
|
69
|
+
useEditor.getState().setPhase('furnish')
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[nodeId, setSelection],
|
|
73
|
+
)
|
|
71
74
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
76
|
+
const handleMouseEnter = useCallback(() => setHoveredId(nodeId), [nodeId, setHoveredId])
|
|
77
|
+
const handleMouseLeave = useCallback(() => setHoveredId(null), [setHoveredId])
|
|
78
|
+
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
|
|
79
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
80
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
75
81
|
|
|
76
|
-
const
|
|
77
|
-
const
|
|
82
|
+
const iconSrc = CATEGORY_ICONS[asset?.category ?? ''] || '/icons/couch.png'
|
|
83
|
+
const defaultName = asset?.name || 'Item'
|
|
84
|
+
const hasChildren = children.length > 0
|
|
78
85
|
|
|
79
86
|
return (
|
|
80
87
|
<TreeNodeWrapper
|
|
81
|
-
actions={<TreeNodeActions
|
|
88
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
82
89
|
depth={depth}
|
|
83
90
|
expanded={expanded}
|
|
84
91
|
hasChildren={hasChildren}
|
|
@@ -86,28 +93,28 @@ export function ItemTreeNode({ node, depth, isLast }: ItemTreeNodeProps) {
|
|
|
86
93
|
isHovered={isHovered}
|
|
87
94
|
isLast={isLast}
|
|
88
95
|
isSelected={isSelected}
|
|
89
|
-
isVisible={
|
|
96
|
+
isVisible={isVisible}
|
|
90
97
|
label={
|
|
91
98
|
<InlineRenameInput
|
|
92
99
|
defaultName={defaultName}
|
|
93
100
|
isEditing={isEditing}
|
|
94
|
-
|
|
95
|
-
onStartEditing={
|
|
96
|
-
onStopEditing={
|
|
101
|
+
nodeId={nodeId}
|
|
102
|
+
onStartEditing={handleStartEditing}
|
|
103
|
+
onStopEditing={handleStopEditing}
|
|
97
104
|
/>
|
|
98
105
|
}
|
|
99
|
-
nodeId={
|
|
106
|
+
nodeId={nodeId}
|
|
100
107
|
onClick={handleClick}
|
|
101
108
|
onDoubleClick={handleDoubleClick}
|
|
102
109
|
onMouseEnter={handleMouseEnter}
|
|
103
110
|
onMouseLeave={handleMouseLeave}
|
|
104
|
-
onToggle={
|
|
111
|
+
onToggle={handleToggle}
|
|
105
112
|
>
|
|
106
113
|
{hasChildren &&
|
|
107
|
-
|
|
114
|
+
children.map((childId, index) => (
|
|
108
115
|
<TreeNode
|
|
109
116
|
depth={depth + 1}
|
|
110
|
-
isLast={index ===
|
|
117
|
+
isLast={index === children.length - 1}
|
|
111
118
|
key={childId}
|
|
112
119
|
nodeId={childId}
|
|
113
120
|
/>
|
|
@@ -1,61 +1,66 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnyNodeId, type LevelNode, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { Layers } from 'lucide-react'
|
|
4
|
-
import { useState } from 'react'
|
|
4
|
+
import { useCallback, useState } from 'react'
|
|
5
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
5
6
|
import { InlineRenameInput } from './inline-rename-input'
|
|
6
7
|
import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
7
8
|
import { TreeNodeActions } from './tree-node-actions'
|
|
8
9
|
|
|
9
10
|
interface LevelTreeNodeProps {
|
|
10
|
-
|
|
11
|
+
nodeId: AnyNodeId
|
|
11
12
|
depth: number
|
|
12
13
|
isLast?: boolean
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function LevelTreeNode({
|
|
16
|
+
export function LevelTreeNode({ nodeId, depth, isLast }: LevelTreeNodeProps) {
|
|
16
17
|
const [expanded, setExpanded] = useState(true)
|
|
17
18
|
const [isEditing, setIsEditing] = useState(false)
|
|
18
|
-
const
|
|
19
|
-
const
|
|
19
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
20
|
+
const children = useScene(
|
|
21
|
+
useShallow((s) => (s.nodes[nodeId] as LevelNode | undefined)?.children ?? []),
|
|
22
|
+
)
|
|
23
|
+
const level = useScene((s) => (s.nodes[nodeId] as LevelNode | undefined)?.level ?? 0)
|
|
24
|
+
const isSelected = useViewer((state) => state.selection.levelId === nodeId)
|
|
25
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
20
26
|
const setSelection = useViewer((state) => state.setSelection)
|
|
21
27
|
|
|
22
|
-
const handleClick = () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
focusTreeNode(node.id)
|
|
28
|
-
}
|
|
28
|
+
const handleClick = useCallback(() => setSelection({ levelId: nodeId }), [nodeId, setSelection])
|
|
29
|
+
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
30
|
+
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
|
|
31
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
32
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
29
33
|
|
|
30
|
-
const defaultName = `Level ${
|
|
34
|
+
const defaultName = `Level ${level}`
|
|
31
35
|
|
|
32
36
|
return (
|
|
33
37
|
<TreeNodeWrapper
|
|
34
|
-
actions={<TreeNodeActions
|
|
38
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
35
39
|
depth={depth}
|
|
36
40
|
expanded={expanded}
|
|
37
|
-
hasChildren={
|
|
41
|
+
hasChildren={children.length > 0}
|
|
38
42
|
icon={<Layers className="h-3.5 w-3.5" />}
|
|
39
43
|
isHovered={isHovered}
|
|
40
44
|
isLast={isLast}
|
|
41
45
|
isSelected={isSelected}
|
|
46
|
+
isVisible={isVisible}
|
|
42
47
|
label={
|
|
43
48
|
<InlineRenameInput
|
|
44
49
|
defaultName={defaultName}
|
|
45
50
|
isEditing={isEditing}
|
|
46
|
-
|
|
47
|
-
onStartEditing={
|
|
48
|
-
onStopEditing={
|
|
51
|
+
nodeId={nodeId}
|
|
52
|
+
onStartEditing={handleStartEditing}
|
|
53
|
+
onStopEditing={handleStopEditing}
|
|
49
54
|
/>
|
|
50
55
|
}
|
|
51
56
|
onClick={handleClick}
|
|
52
57
|
onDoubleClick={handleDoubleClick}
|
|
53
|
-
onToggle={
|
|
58
|
+
onToggle={handleToggle}
|
|
54
59
|
>
|
|
55
|
-
{
|
|
60
|
+
{children.map((childId, index) => (
|
|
56
61
|
<TreeNode
|
|
57
62
|
depth={depth + 1}
|
|
58
|
-
isLast={index ===
|
|
63
|
+
isLast={index === children.length - 1}
|
|
59
64
|
key={childId}
|
|
60
65
|
nodeId={childId}
|
|
61
66
|
/>
|
|
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
3
3
|
import { AnimatePresence } from 'motion/react'
|
|
4
4
|
import Image from 'next/image'
|
|
5
5
|
import { useCallback, useEffect, useState } from 'react'
|
|
6
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
6
7
|
import useEditor from '../../../../../store/use-editor'
|
|
7
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
8
9
|
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
@@ -10,47 +11,58 @@ import { TreeNodeActions } from './tree-node-actions'
|
|
|
10
11
|
import { DropIndicatorLine, useTreeNodeDrag } from './tree-node-drag'
|
|
11
12
|
|
|
12
13
|
interface RoofTreeNodeProps {
|
|
13
|
-
|
|
14
|
+
nodeId: AnyNodeId
|
|
14
15
|
depth: number
|
|
15
16
|
isLast?: boolean
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export function RoofTreeNode({
|
|
19
|
+
export function RoofTreeNode({ nodeId, depth, isLast }: RoofTreeNodeProps) {
|
|
19
20
|
const [isEditing, setIsEditing] = useState(false)
|
|
20
21
|
const [expanded, setExpanded] = useState(false)
|
|
21
|
-
const
|
|
22
|
-
const isSelected = selectedIds.includes(
|
|
23
|
-
const isHovered = useViewer((state) => state.hoveredId ===
|
|
22
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
23
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
24
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
24
25
|
const setSelection = useViewer((state) => state.setSelection)
|
|
25
26
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
26
|
-
const nodes = useScene((state) => state.nodes)
|
|
27
27
|
const { drag, dropTarget } = useTreeNodeDrag()
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
focusTreeNode(node.id)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const handleMouseEnter = () => {
|
|
42
|
-
setHoveredId(node.id)
|
|
43
|
-
}
|
|
29
|
+
const segments = useScene(
|
|
30
|
+
useShallow((s) => {
|
|
31
|
+
const n = s.nodes[nodeId] as RoofNode | undefined
|
|
32
|
+
if (!n) return [] as RoofSegmentNode[]
|
|
33
|
+
return (n.children ?? [])
|
|
34
|
+
.map((childId) => s.nodes[childId as AnyNodeId] as RoofSegmentNode | undefined)
|
|
35
|
+
.filter((n): n is RoofSegmentNode => n?.type === 'roof-segment')
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
// Targeted selector — only re-renders when a segment of THIS roof is selected/deselected
|
|
40
|
+
const hasSelectedChild = useViewer((state) =>
|
|
41
|
+
segments.some((seg) => state.selection.selectedIds.includes(seg.id)),
|
|
42
|
+
)
|
|
48
43
|
|
|
49
|
-
const
|
|
50
|
-
.
|
|
51
|
-
|
|
44
|
+
const handleClick = useCallback(
|
|
45
|
+
(e: React.MouseEvent) => {
|
|
46
|
+
e.stopPropagation()
|
|
47
|
+
const handled = handleTreeSelection(
|
|
48
|
+
e,
|
|
49
|
+
nodeId,
|
|
50
|
+
useViewer.getState().selection.selectedIds,
|
|
51
|
+
setSelection,
|
|
52
|
+
)
|
|
53
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
54
|
+
useEditor.getState().setPhase('structure')
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[nodeId, setSelection],
|
|
58
|
+
)
|
|
52
59
|
|
|
53
|
-
const
|
|
60
|
+
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
61
|
+
const handleMouseEnter = useCallback(() => setHoveredId(nodeId), [nodeId, setHoveredId])
|
|
62
|
+
const handleMouseLeave = useCallback(() => setHoveredId(null), [setHoveredId])
|
|
63
|
+
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
|
|
64
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
65
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
54
66
|
|
|
55
67
|
useEffect(() => {
|
|
56
68
|
if (isSelected || hasSelectedChild) {
|
|
@@ -59,7 +71,7 @@ export function RoofTreeNode({ node, depth, isLast }: RoofTreeNodeProps) {
|
|
|
59
71
|
}, [isSelected, hasSelectedChild])
|
|
60
72
|
|
|
61
73
|
// Auto-expand when a segment is being dragged over this roof
|
|
62
|
-
const isDropTarget = drag !== null && dropTarget?.parentId ===
|
|
74
|
+
const isDropTarget = drag !== null && dropTarget?.parentId === nodeId
|
|
63
75
|
useEffect(() => {
|
|
64
76
|
if (isDropTarget && !expanded) {
|
|
65
77
|
setExpanded(true)
|
|
@@ -72,12 +84,12 @@ export function RoofTreeNode({ node, depth, isLast }: RoofTreeNodeProps) {
|
|
|
72
84
|
// Hide the dragged segment from every roof while dragging
|
|
73
85
|
const visibleSegments = drag ? segments.filter((seg) => seg.id !== drag.nodeId) : segments
|
|
74
86
|
|
|
75
|
-
const isValidDropTarget = drag !== null && drag.nodeId !==
|
|
87
|
+
const isValidDropTarget = drag !== null && drag.nodeId !== nodeId
|
|
76
88
|
|
|
77
89
|
return (
|
|
78
|
-
<div data-drop-target={
|
|
90
|
+
<div data-drop-target={nodeId}>
|
|
79
91
|
<TreeNodeWrapper
|
|
80
|
-
actions={<TreeNodeActions
|
|
92
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
81
93
|
depth={depth}
|
|
82
94
|
expanded={expanded}
|
|
83
95
|
hasChildren={segments.length > 0}
|
|
@@ -88,22 +100,22 @@ export function RoofTreeNode({ node, depth, isLast }: RoofTreeNodeProps) {
|
|
|
88
100
|
isHovered={isHovered || isDropTarget}
|
|
89
101
|
isLast={isLast && !expanded}
|
|
90
102
|
isSelected={isSelected}
|
|
91
|
-
isVisible={
|
|
103
|
+
isVisible={isVisible}
|
|
92
104
|
label={
|
|
93
105
|
<InlineRenameInput
|
|
94
106
|
defaultName={defaultName}
|
|
95
107
|
isEditing={isEditing}
|
|
96
|
-
|
|
97
|
-
onStartEditing={
|
|
98
|
-
onStopEditing={
|
|
108
|
+
nodeId={nodeId}
|
|
109
|
+
onStartEditing={handleStartEditing}
|
|
110
|
+
onStopEditing={handleStopEditing}
|
|
99
111
|
/>
|
|
100
112
|
}
|
|
101
|
-
nodeId={
|
|
113
|
+
nodeId={nodeId}
|
|
102
114
|
onClick={handleClick}
|
|
103
115
|
onDoubleClick={handleDoubleClick}
|
|
104
116
|
onMouseEnter={handleMouseEnter}
|
|
105
117
|
onMouseLeave={handleMouseLeave}
|
|
106
|
-
onToggle={
|
|
118
|
+
onToggle={handleToggle}
|
|
107
119
|
>
|
|
108
120
|
{visibleSegments.map((seg, i) => {
|
|
109
121
|
const showIndicatorBefore = isDropTarget && dropTarget?.insertIndex === i
|
|
@@ -147,18 +159,20 @@ function RoofSegmentTreeNode({
|
|
|
147
159
|
isLast?: boolean
|
|
148
160
|
}) {
|
|
149
161
|
const [isEditing, setIsEditing] = useState(false)
|
|
150
|
-
const
|
|
151
|
-
const isSelected = selectedIds.includes(node.id)
|
|
162
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id))
|
|
152
163
|
const isHovered = useViewer((state) => state.hoveredId === node.id)
|
|
153
164
|
const setSelection = useViewer((state) => state.setSelection)
|
|
154
165
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
155
166
|
const { startDrag, isDragging } = useTreeNodeDrag()
|
|
156
167
|
|
|
157
|
-
const handleClick = (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
const handleClick = useCallback(
|
|
169
|
+
(e: React.MouseEvent) => {
|
|
170
|
+
if (isDragging) return
|
|
171
|
+
e.stopPropagation()
|
|
172
|
+
handleTreeSelection(e, node.id, useViewer.getState().selection.selectedIds, setSelection)
|
|
173
|
+
},
|
|
174
|
+
[node.id, isDragging, setSelection],
|
|
175
|
+
)
|
|
162
176
|
|
|
163
177
|
const handlePointerDown = useCallback(
|
|
164
178
|
(e: React.PointerEvent) => {
|
|
@@ -169,12 +183,15 @@ function RoofSegmentTreeNode({
|
|
|
169
183
|
[node.id, node.type, node.parentId, node.roofType, node.width, node.depth, startDrag],
|
|
170
184
|
)
|
|
171
185
|
|
|
186
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
187
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
188
|
+
|
|
172
189
|
const defaultName = `${node.roofType.charAt(0).toUpperCase() + node.roofType.slice(1)} (${node.width.toFixed(1)}x${node.depth.toFixed(1)}m)`
|
|
173
190
|
|
|
174
191
|
return (
|
|
175
192
|
<div data-drop-child={node.id}>
|
|
176
193
|
<TreeNodeWrapper
|
|
177
|
-
actions={<TreeNodeActions
|
|
194
|
+
actions={<TreeNodeActions nodeId={node.id} />}
|
|
178
195
|
depth={depth}
|
|
179
196
|
expanded={false}
|
|
180
197
|
hasChildren={false}
|
|
@@ -196,9 +213,9 @@ function RoofSegmentTreeNode({
|
|
|
196
213
|
<InlineRenameInput
|
|
197
214
|
defaultName={defaultName}
|
|
198
215
|
isEditing={isEditing}
|
|
199
|
-
|
|
200
|
-
onStartEditing={
|
|
201
|
-
onStopEditing={
|
|
216
|
+
nodeId={node.id}
|
|
217
|
+
onStartEditing={handleStartEditing}
|
|
218
|
+
onStopEditing={handleStopEditing}
|
|
202
219
|
/>
|
|
203
220
|
}
|
|
204
221
|
nodeId={node.id}
|
|
@@ -1,53 +1,52 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnyNodeId, type SlabNode, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import Image from 'next/image'
|
|
4
|
-
import { useState } from 'react'
|
|
4
|
+
import { useCallback, useState } from 'react'
|
|
5
5
|
import useEditor from './../../../../../store/use-editor'
|
|
6
6
|
import { InlineRenameInput } from './inline-rename-input'
|
|
7
7
|
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
8
8
|
import { TreeNodeActions } from './tree-node-actions'
|
|
9
9
|
|
|
10
10
|
interface SlabTreeNodeProps {
|
|
11
|
-
|
|
11
|
+
nodeId: AnyNodeId
|
|
12
12
|
depth: number
|
|
13
13
|
isLast?: boolean
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function SlabTreeNode({
|
|
16
|
+
export function SlabTreeNode({ nodeId, depth, isLast }: SlabTreeNodeProps) {
|
|
17
17
|
const [isEditing, setIsEditing] = useState(false)
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
18
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
19
|
+
const polygon = useScene((s) => (s.nodes[nodeId] as SlabNode | undefined)?.polygon ?? [])
|
|
20
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
21
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
21
22
|
const setSelection = useViewer((state) => state.setSelection)
|
|
22
23
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
23
24
|
|
|
24
|
-
const handleClick = (
|
|
25
|
-
e.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
const handleClick = useCallback(
|
|
26
|
+
(e: React.MouseEvent) => {
|
|
27
|
+
e.stopPropagation()
|
|
28
|
+
const handled = handleTreeSelection(
|
|
29
|
+
e,
|
|
30
|
+
nodeId,
|
|
31
|
+
useViewer.getState().selection.selectedIds,
|
|
32
|
+
setSelection,
|
|
33
|
+
)
|
|
34
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
35
|
+
useEditor.getState().setPhase('structure')
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
[nodeId, setSelection],
|
|
39
|
+
)
|
|
39
40
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
}
|
|
41
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
42
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
const area = calculatePolygonArea(node.polygon).toFixed(1)
|
|
44
|
+
const area = calculatePolygonArea(polygon).toFixed(1)
|
|
46
45
|
const defaultName = `Slab (${area}m²)`
|
|
47
46
|
|
|
48
47
|
return (
|
|
49
48
|
<TreeNodeWrapper
|
|
50
|
-
actions={<TreeNodeActions
|
|
49
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
51
50
|
depth={depth}
|
|
52
51
|
expanded={false}
|
|
53
52
|
hasChildren={false}
|
|
@@ -57,21 +56,21 @@ export function SlabTreeNode({ node, depth, isLast }: SlabTreeNodeProps) {
|
|
|
57
56
|
isHovered={isHovered}
|
|
58
57
|
isLast={isLast}
|
|
59
58
|
isSelected={isSelected}
|
|
60
|
-
isVisible={
|
|
59
|
+
isVisible={isVisible}
|
|
61
60
|
label={
|
|
62
61
|
<InlineRenameInput
|
|
63
62
|
defaultName={defaultName}
|
|
64
63
|
isEditing={isEditing}
|
|
65
|
-
|
|
66
|
-
onStartEditing={
|
|
67
|
-
onStopEditing={
|
|
64
|
+
nodeId={nodeId}
|
|
65
|
+
onStartEditing={handleStartEditing}
|
|
66
|
+
onStopEditing={handleStopEditing}
|
|
68
67
|
/>
|
|
69
68
|
}
|
|
70
|
-
nodeId={
|
|
69
|
+
nodeId={nodeId}
|
|
71
70
|
onClick={handleClick}
|
|
72
|
-
onDoubleClick={
|
|
73
|
-
onMouseEnter={
|
|
74
|
-
onMouseLeave={
|
|
71
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
72
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
73
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
75
74
|
onToggle={() => {}}
|
|
76
75
|
/>
|
|
77
76
|
)
|