@pascal-app/editor 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- package/src/components/editor/editor-layout-v2.tsx +9 -0
- package/src/components/editor/floating-action-menu.tsx +341 -48
- package/src/components/editor/floating-building-action-menu.tsx +70 -0
- package/src/components/editor/floorplan-panel.tsx +1350 -722
- package/src/components/editor/index.tsx +221 -167
- package/src/components/editor/node-action-menu.tsx +40 -11
- package/src/components/editor/selection-manager.tsx +238 -10
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +422 -79
- package/src/components/editor/wall-measurement-label.tsx +120 -32
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- 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/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/door/door-math.ts +1 -1
- package/src/components/tools/door/door-tool.tsx +31 -7
- package/src/components/tools/door/move-door-tool.tsx +27 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +137 -0
- package/src/components/tools/fence/fence-tool.tsx +190 -0
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
- package/src/components/tools/fence/move-fence-tool.tsx +231 -0
- package/src/components/tools/item/item-tool.tsx +3 -3
- package/src/components/tools/item/move-tool.tsx +16 -0
- package/src/components/tools/item/placement-math.ts +14 -6
- package/src/components/tools/item/placement-strategies.ts +17 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +123 -16
- package/src/components/tools/roof/move-roof-tool.tsx +90 -26
- 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 +98 -8
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- 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 +39 -8
- package/src/components/tools/tool-manager.tsx +54 -14
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +322 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +331 -9
- package/src/components/tools/wall/wall-tool.tsx +19 -29
- package/src/components/tools/window/move-window-tool.tsx +27 -8
- package/src/components/tools/window/window-math.ts +1 -1
- package/src/components/tools/window/window-tool.tsx +31 -7
- package/src/components/tools/zone/zone-tool.tsx +7 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -4
- package/src/components/ui/action-menu/structure-tools.tsx +1 -0
- package/src/components/ui/command-palette/editor-commands.tsx +9 -4
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +127 -94
- package/src/components/ui/controls/slider-control.tsx +28 -14
- package/src/components/ui/helpers/building-helper.tsx +32 -0
- package/src/components/ui/helpers/helper-manager.tsx +2 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +5 -0
- package/src/components/ui/panels/ceiling-panel.tsx +61 -17
- package/src/components/ui/panels/door-panel.tsx +5 -5
- package/src/components/ui/panels/fence-panel.tsx +269 -0
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/panel-manager.tsx +32 -27
- package/src/components/ui/panels/reference-panel.tsx +5 -4
- package/src/components/ui/panels/roof-panel.tsx +91 -22
- package/src/components/ui/panels/roof-segment-panel.tsx +23 -13
- package/src/components/ui/panels/slab-panel.tsx +63 -15
- package/src/components/ui/panels/stair-panel.tsx +377 -50
- package/src/components/ui/panels/stair-segment-panel.tsx +28 -17
- package/src/components/ui/panels/wall-panel.tsx +159 -11
- package/src/components/ui/panels/window-panel.tsx +5 -7
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +28 -17
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +65 -53
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +40 -25
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +69 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +88 -72
- 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 +64 -53
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +32 -23
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +72 -51
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +40 -37
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +72 -51
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +13 -13
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -17
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +62 -54
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +40 -25
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +27 -28
- package/src/components/ui/viewer-toolbar.tsx +55 -2
- package/src/components/viewer-overlay.tsx +26 -19
- package/src/hooks/use-auto-save.ts +3 -6
- package/src/hooks/use-contextual-tools.ts +25 -16
- package/src/hooks/use-grid-events.ts +13 -1
- package/src/hooks/use-keyboard.ts +7 -2
- package/src/index.tsx +2 -1
- package/src/lib/history.ts +20 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/store/use-editor.tsx +125 -10
|
@@ -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 { memo, 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,77 @@ 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
|
|
27
|
+
export const ItemTreeNode = memo(function ItemTreeNode({
|
|
28
|
+
nodeId,
|
|
29
|
+
depth,
|
|
30
|
+
isLast,
|
|
31
|
+
}: ItemTreeNodeProps) {
|
|
27
32
|
const [isEditing, setIsEditing] = useState(false)
|
|
28
33
|
const [expanded, setExpanded] = useState(true)
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
35
|
+
const children = useScene(
|
|
36
|
+
useShallow((s) => (s.nodes[nodeId] as ItemNode | undefined)?.children ?? []),
|
|
37
|
+
)
|
|
38
|
+
const asset = useScene((s) => (s.nodes[nodeId] as ItemNode | undefined)?.asset)
|
|
39
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
40
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
33
41
|
const setSelection = useViewer((state) => state.setSelection)
|
|
34
42
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
35
43
|
|
|
44
|
+
// Expand when a descendant is selected — imperative to avoid subscribing to the full selectedIds array
|
|
36
45
|
useEffect(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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]
|
|
46
58
|
}
|
|
47
|
-
current = nodes[current.parentId as AnyNodeId]
|
|
48
59
|
}
|
|
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
|
-
}
|
|
60
|
+
})
|
|
61
|
+
}, [nodeId])
|
|
67
62
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
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 === 'structure') {
|
|
73
|
+
useEditor.getState().setPhase('furnish')
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[nodeId, setSelection],
|
|
77
|
+
)
|
|
71
78
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
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), [])
|
|
75
85
|
|
|
76
|
-
const
|
|
77
|
-
const
|
|
86
|
+
const iconSrc = CATEGORY_ICONS[asset?.category ?? ''] || '/icons/couch.png'
|
|
87
|
+
const defaultName = asset?.name || 'Item'
|
|
88
|
+
const hasChildren = children.length > 0
|
|
78
89
|
|
|
79
90
|
return (
|
|
80
91
|
<TreeNodeWrapper
|
|
81
|
-
actions={<TreeNodeActions
|
|
92
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
82
93
|
depth={depth}
|
|
83
94
|
expanded={expanded}
|
|
84
95
|
hasChildren={hasChildren}
|
|
@@ -86,32 +97,32 @@ export function ItemTreeNode({ node, depth, isLast }: ItemTreeNodeProps) {
|
|
|
86
97
|
isHovered={isHovered}
|
|
87
98
|
isLast={isLast}
|
|
88
99
|
isSelected={isSelected}
|
|
89
|
-
isVisible={
|
|
100
|
+
isVisible={isVisible}
|
|
90
101
|
label={
|
|
91
102
|
<InlineRenameInput
|
|
92
103
|
defaultName={defaultName}
|
|
93
104
|
isEditing={isEditing}
|
|
94
|
-
|
|
95
|
-
onStartEditing={
|
|
96
|
-
onStopEditing={
|
|
105
|
+
nodeId={nodeId}
|
|
106
|
+
onStartEditing={handleStartEditing}
|
|
107
|
+
onStopEditing={handleStopEditing}
|
|
97
108
|
/>
|
|
98
109
|
}
|
|
99
|
-
nodeId={
|
|
110
|
+
nodeId={nodeId}
|
|
100
111
|
onClick={handleClick}
|
|
101
112
|
onDoubleClick={handleDoubleClick}
|
|
102
113
|
onMouseEnter={handleMouseEnter}
|
|
103
114
|
onMouseLeave={handleMouseLeave}
|
|
104
|
-
onToggle={
|
|
115
|
+
onToggle={handleToggle}
|
|
105
116
|
>
|
|
106
117
|
{hasChildren &&
|
|
107
|
-
|
|
118
|
+
children.map((childId, index) => (
|
|
108
119
|
<TreeNode
|
|
109
120
|
depth={depth + 1}
|
|
110
|
-
isLast={index ===
|
|
121
|
+
isLast={index === children.length - 1}
|
|
111
122
|
key={childId}
|
|
112
123
|
nodeId={childId}
|
|
113
124
|
/>
|
|
114
125
|
))}
|
|
115
126
|
</TreeNodeWrapper>
|
|
116
127
|
)
|
|
117
|
-
}
|
|
128
|
+
})
|
|
@@ -1,65 +1,74 @@
|
|
|
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 { memo, 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
|
|
16
|
+
export const LevelTreeNode = memo(function LevelTreeNode({
|
|
17
|
+
nodeId,
|
|
18
|
+
depth,
|
|
19
|
+
isLast,
|
|
20
|
+
}: LevelTreeNodeProps) {
|
|
16
21
|
const [expanded, setExpanded] = useState(true)
|
|
17
22
|
const [isEditing, setIsEditing] = useState(false)
|
|
18
|
-
const
|
|
19
|
-
const
|
|
23
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
24
|
+
const children = useScene(
|
|
25
|
+
useShallow((s) => (s.nodes[nodeId] as LevelNode | undefined)?.children ?? []),
|
|
26
|
+
)
|
|
27
|
+
const level = useScene((s) => (s.nodes[nodeId] as LevelNode | undefined)?.level ?? 0)
|
|
28
|
+
const isSelected = useViewer((state) => state.selection.levelId === nodeId)
|
|
29
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
20
30
|
const setSelection = useViewer((state) => state.setSelection)
|
|
21
31
|
|
|
22
|
-
const handleClick = () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
focusTreeNode(node.id)
|
|
28
|
-
}
|
|
32
|
+
const handleClick = useCallback(() => setSelection({ levelId: nodeId }), [nodeId, setSelection])
|
|
33
|
+
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
34
|
+
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
|
|
35
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
36
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
29
37
|
|
|
30
|
-
const defaultName = `Level ${
|
|
38
|
+
const defaultName = `Level ${level}`
|
|
31
39
|
|
|
32
40
|
return (
|
|
33
41
|
<TreeNodeWrapper
|
|
34
|
-
actions={<TreeNodeActions
|
|
42
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
35
43
|
depth={depth}
|
|
36
44
|
expanded={expanded}
|
|
37
|
-
hasChildren={
|
|
45
|
+
hasChildren={children.length > 0}
|
|
38
46
|
icon={<Layers className="h-3.5 w-3.5" />}
|
|
39
47
|
isHovered={isHovered}
|
|
40
48
|
isLast={isLast}
|
|
41
49
|
isSelected={isSelected}
|
|
50
|
+
isVisible={isVisible}
|
|
42
51
|
label={
|
|
43
52
|
<InlineRenameInput
|
|
44
53
|
defaultName={defaultName}
|
|
45
54
|
isEditing={isEditing}
|
|
46
|
-
|
|
47
|
-
onStartEditing={
|
|
48
|
-
onStopEditing={
|
|
55
|
+
nodeId={nodeId}
|
|
56
|
+
onStartEditing={handleStartEditing}
|
|
57
|
+
onStopEditing={handleStopEditing}
|
|
49
58
|
/>
|
|
50
59
|
}
|
|
51
60
|
onClick={handleClick}
|
|
52
61
|
onDoubleClick={handleDoubleClick}
|
|
53
|
-
onToggle={
|
|
62
|
+
onToggle={handleToggle}
|
|
54
63
|
>
|
|
55
|
-
{
|
|
64
|
+
{children.map((childId, index) => (
|
|
56
65
|
<TreeNode
|
|
57
66
|
depth={depth + 1}
|
|
58
|
-
isLast={index ===
|
|
67
|
+
isLast={index === children.length - 1}
|
|
59
68
|
key={childId}
|
|
60
69
|
nodeId={childId}
|
|
61
70
|
/>
|
|
62
71
|
))}
|
|
63
72
|
</TreeNodeWrapper>
|
|
64
73
|
)
|
|
65
|
-
}
|
|
74
|
+
})
|
|
@@ -2,7 +2,8 @@ import { type AnyNodeId, type RoofNode, type RoofSegmentNode, useScene } from '@
|
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { AnimatePresence } from 'motion/react'
|
|
4
4
|
import Image from 'next/image'
|
|
5
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
5
|
+
import { memo, 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,62 @@ 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
|
|
19
|
+
export const RoofTreeNode = memo(function RoofTreeNode({
|
|
20
|
+
nodeId,
|
|
21
|
+
depth,
|
|
22
|
+
isLast,
|
|
23
|
+
}: RoofTreeNodeProps) {
|
|
19
24
|
const [isEditing, setIsEditing] = useState(false)
|
|
20
25
|
const [expanded, setExpanded] = useState(false)
|
|
21
|
-
const
|
|
22
|
-
const isSelected = selectedIds.includes(
|
|
23
|
-
const isHovered = useViewer((state) => state.hoveredId ===
|
|
26
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
27
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
28
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
24
29
|
const setSelection = useViewer((state) => state.setSelection)
|
|
25
30
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
26
|
-
const nodes = useScene((state) => state.nodes)
|
|
27
31
|
const { drag, dropTarget } = useTreeNodeDrag()
|
|
28
32
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
focusTreeNode(node.id)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const handleMouseEnter = () => {
|
|
42
|
-
setHoveredId(node.id)
|
|
43
|
-
}
|
|
33
|
+
const segments = useScene(
|
|
34
|
+
useShallow((s) => {
|
|
35
|
+
const n = s.nodes[nodeId] as RoofNode | undefined
|
|
36
|
+
if (!n) return [] as RoofSegmentNode[]
|
|
37
|
+
return (n.children ?? [])
|
|
38
|
+
.map((childId) => s.nodes[childId as AnyNodeId] as RoofSegmentNode | undefined)
|
|
39
|
+
.filter((n): n is RoofSegmentNode => n?.type === 'roof-segment')
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
// Targeted selector — only re-renders when a segment of THIS roof is selected/deselected
|
|
44
|
+
const hasSelectedChild = useViewer((state) =>
|
|
45
|
+
segments.some((seg) => state.selection.selectedIds.includes(seg.id)),
|
|
46
|
+
)
|
|
48
47
|
|
|
49
|
-
const
|
|
50
|
-
.
|
|
51
|
-
|
|
48
|
+
const handleClick = useCallback(
|
|
49
|
+
(e: React.MouseEvent) => {
|
|
50
|
+
e.stopPropagation()
|
|
51
|
+
const handled = handleTreeSelection(
|
|
52
|
+
e,
|
|
53
|
+
nodeId,
|
|
54
|
+
useViewer.getState().selection.selectedIds,
|
|
55
|
+
setSelection,
|
|
56
|
+
)
|
|
57
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
58
|
+
useEditor.getState().setPhase('structure')
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[nodeId, setSelection],
|
|
62
|
+
)
|
|
52
63
|
|
|
53
|
-
const
|
|
64
|
+
const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
|
|
65
|
+
const handleMouseEnter = useCallback(() => setHoveredId(nodeId), [nodeId, setHoveredId])
|
|
66
|
+
const handleMouseLeave = useCallback(() => setHoveredId(null), [setHoveredId])
|
|
67
|
+
const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
|
|
68
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
69
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
54
70
|
|
|
55
71
|
useEffect(() => {
|
|
56
72
|
if (isSelected || hasSelectedChild) {
|
|
@@ -59,7 +75,7 @@ export function RoofTreeNode({ node, depth, isLast }: RoofTreeNodeProps) {
|
|
|
59
75
|
}, [isSelected, hasSelectedChild])
|
|
60
76
|
|
|
61
77
|
// Auto-expand when a segment is being dragged over this roof
|
|
62
|
-
const isDropTarget = drag !== null && dropTarget?.parentId ===
|
|
78
|
+
const isDropTarget = drag !== null && dropTarget?.parentId === nodeId
|
|
63
79
|
useEffect(() => {
|
|
64
80
|
if (isDropTarget && !expanded) {
|
|
65
81
|
setExpanded(true)
|
|
@@ -72,12 +88,12 @@ export function RoofTreeNode({ node, depth, isLast }: RoofTreeNodeProps) {
|
|
|
72
88
|
// Hide the dragged segment from every roof while dragging
|
|
73
89
|
const visibleSegments = drag ? segments.filter((seg) => seg.id !== drag.nodeId) : segments
|
|
74
90
|
|
|
75
|
-
const isValidDropTarget = drag !== null && drag.nodeId !==
|
|
91
|
+
const isValidDropTarget = drag !== null && drag.nodeId !== nodeId
|
|
76
92
|
|
|
77
93
|
return (
|
|
78
|
-
<div data-drop-target={
|
|
94
|
+
<div data-drop-target={nodeId}>
|
|
79
95
|
<TreeNodeWrapper
|
|
80
|
-
actions={<TreeNodeActions
|
|
96
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
81
97
|
depth={depth}
|
|
82
98
|
expanded={expanded}
|
|
83
99
|
hasChildren={segments.length > 0}
|
|
@@ -88,22 +104,22 @@ export function RoofTreeNode({ node, depth, isLast }: RoofTreeNodeProps) {
|
|
|
88
104
|
isHovered={isHovered || isDropTarget}
|
|
89
105
|
isLast={isLast && !expanded}
|
|
90
106
|
isSelected={isSelected}
|
|
91
|
-
isVisible={
|
|
107
|
+
isVisible={isVisible}
|
|
92
108
|
label={
|
|
93
109
|
<InlineRenameInput
|
|
94
110
|
defaultName={defaultName}
|
|
95
111
|
isEditing={isEditing}
|
|
96
|
-
|
|
97
|
-
onStartEditing={
|
|
98
|
-
onStopEditing={
|
|
112
|
+
nodeId={nodeId}
|
|
113
|
+
onStartEditing={handleStartEditing}
|
|
114
|
+
onStopEditing={handleStopEditing}
|
|
99
115
|
/>
|
|
100
116
|
}
|
|
101
|
-
nodeId={
|
|
117
|
+
nodeId={nodeId}
|
|
102
118
|
onClick={handleClick}
|
|
103
119
|
onDoubleClick={handleDoubleClick}
|
|
104
120
|
onMouseEnter={handleMouseEnter}
|
|
105
121
|
onMouseLeave={handleMouseLeave}
|
|
106
|
-
onToggle={
|
|
122
|
+
onToggle={handleToggle}
|
|
107
123
|
>
|
|
108
124
|
{visibleSegments.map((seg, i) => {
|
|
109
125
|
const showIndicatorBefore = isDropTarget && dropTarget?.insertIndex === i
|
|
@@ -135,7 +151,7 @@ export function RoofTreeNode({ node, depth, isLast }: RoofTreeNodeProps) {
|
|
|
135
151
|
</TreeNodeWrapper>
|
|
136
152
|
</div>
|
|
137
153
|
)
|
|
138
|
-
}
|
|
154
|
+
})
|
|
139
155
|
|
|
140
156
|
function RoofSegmentTreeNode({
|
|
141
157
|
node,
|
|
@@ -147,18 +163,20 @@ function RoofSegmentTreeNode({
|
|
|
147
163
|
isLast?: boolean
|
|
148
164
|
}) {
|
|
149
165
|
const [isEditing, setIsEditing] = useState(false)
|
|
150
|
-
const
|
|
151
|
-
const isSelected = selectedIds.includes(node.id)
|
|
166
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id))
|
|
152
167
|
const isHovered = useViewer((state) => state.hoveredId === node.id)
|
|
153
168
|
const setSelection = useViewer((state) => state.setSelection)
|
|
154
169
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
155
170
|
const { startDrag, isDragging } = useTreeNodeDrag()
|
|
156
171
|
|
|
157
|
-
const handleClick = (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
const handleClick = useCallback(
|
|
173
|
+
(e: React.MouseEvent) => {
|
|
174
|
+
if (isDragging) return
|
|
175
|
+
e.stopPropagation()
|
|
176
|
+
handleTreeSelection(e, node.id, useViewer.getState().selection.selectedIds, setSelection)
|
|
177
|
+
},
|
|
178
|
+
[node.id, isDragging, setSelection],
|
|
179
|
+
)
|
|
162
180
|
|
|
163
181
|
const handlePointerDown = useCallback(
|
|
164
182
|
(e: React.PointerEvent) => {
|
|
@@ -169,12 +187,15 @@ function RoofSegmentTreeNode({
|
|
|
169
187
|
[node.id, node.type, node.parentId, node.roofType, node.width, node.depth, startDrag],
|
|
170
188
|
)
|
|
171
189
|
|
|
190
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
191
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
192
|
+
|
|
172
193
|
const defaultName = `${node.roofType.charAt(0).toUpperCase() + node.roofType.slice(1)} (${node.width.toFixed(1)}x${node.depth.toFixed(1)}m)`
|
|
173
194
|
|
|
174
195
|
return (
|
|
175
196
|
<div data-drop-child={node.id}>
|
|
176
197
|
<TreeNodeWrapper
|
|
177
|
-
actions={<TreeNodeActions
|
|
198
|
+
actions={<TreeNodeActions nodeId={node.id} />}
|
|
178
199
|
depth={depth}
|
|
179
200
|
expanded={false}
|
|
180
201
|
hasChildren={false}
|
|
@@ -196,9 +217,9 @@ function RoofSegmentTreeNode({
|
|
|
196
217
|
<InlineRenameInput
|
|
197
218
|
defaultName={defaultName}
|
|
198
219
|
isEditing={isEditing}
|
|
199
|
-
|
|
200
|
-
onStartEditing={
|
|
201
|
-
onStopEditing={
|
|
220
|
+
nodeId={node.id}
|
|
221
|
+
onStartEditing={handleStartEditing}
|
|
222
|
+
onStopEditing={handleStopEditing}
|
|
202
223
|
/>
|
|
203
224
|
}
|
|
204
225
|
nodeId={node.id}
|
|
@@ -1,53 +1,56 @@
|
|
|
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 { memo, 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
|
|
16
|
+
export const SlabTreeNode = memo(function SlabTreeNode({
|
|
17
|
+
nodeId,
|
|
18
|
+
depth,
|
|
19
|
+
isLast,
|
|
20
|
+
}: SlabTreeNodeProps) {
|
|
17
21
|
const [isEditing, setIsEditing] = useState(false)
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
22
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
23
|
+
const polygon = useScene((s) => (s.nodes[nodeId] as SlabNode | undefined)?.polygon ?? [])
|
|
24
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
25
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
21
26
|
const setSelection = useViewer((state) => state.setSelection)
|
|
22
27
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
23
28
|
|
|
24
|
-
const handleClick = (
|
|
25
|
-
e.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
)
|
|
39
44
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
}
|
|
45
|
+
const handleStartEditing = useCallback(() => setIsEditing(true), [])
|
|
46
|
+
const handleStopEditing = useCallback(() => setIsEditing(false), [])
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
const area = calculatePolygonArea(node.polygon).toFixed(1)
|
|
48
|
+
const area = calculatePolygonArea(polygon).toFixed(1)
|
|
46
49
|
const defaultName = `Slab (${area}m²)`
|
|
47
50
|
|
|
48
51
|
return (
|
|
49
52
|
<TreeNodeWrapper
|
|
50
|
-
actions={<TreeNodeActions
|
|
53
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
51
54
|
depth={depth}
|
|
52
55
|
expanded={false}
|
|
53
56
|
hasChildren={false}
|
|
@@ -57,25 +60,25 @@ export function SlabTreeNode({ node, depth, isLast }: SlabTreeNodeProps) {
|
|
|
57
60
|
isHovered={isHovered}
|
|
58
61
|
isLast={isLast}
|
|
59
62
|
isSelected={isSelected}
|
|
60
|
-
isVisible={
|
|
63
|
+
isVisible={isVisible}
|
|
61
64
|
label={
|
|
62
65
|
<InlineRenameInput
|
|
63
66
|
defaultName={defaultName}
|
|
64
67
|
isEditing={isEditing}
|
|
65
|
-
|
|
66
|
-
onStartEditing={
|
|
67
|
-
onStopEditing={
|
|
68
|
+
nodeId={nodeId}
|
|
69
|
+
onStartEditing={handleStartEditing}
|
|
70
|
+
onStopEditing={handleStopEditing}
|
|
68
71
|
/>
|
|
69
72
|
}
|
|
70
|
-
nodeId={
|
|
73
|
+
nodeId={nodeId}
|
|
71
74
|
onClick={handleClick}
|
|
72
|
-
onDoubleClick={
|
|
73
|
-
onMouseEnter={
|
|
74
|
-
onMouseLeave={
|
|
75
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
76
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
77
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
75
78
|
onToggle={() => {}}
|
|
76
79
|
/>
|
|
77
80
|
)
|
|
78
|
-
}
|
|
81
|
+
})
|
|
79
82
|
|
|
80
83
|
/**
|
|
81
84
|
* Calculate the area of a polygon using the shoelace formula
|