@pascal-app/editor 0.5.1 → 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 +255 -34
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-panel.tsx +1323 -713
- package/src/components/editor/index.tsx +2 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +200 -8
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +319 -157
- 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/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-tool.tsx +12 -0
- package/src/components/tools/door/move-door-tool.tsx +10 -0
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +19 -7
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
- package/src/components/tools/fence/move-fence-tool.tsx +8 -0
- package/src/components/tools/item/move-tool.tsx +9 -0
- package/src/components/tools/item/placement-math.ts +14 -6
- package/src/components/tools/item/placement-strategies.ts +2 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +42 -10
- package/src/components/tools/roof/move-roof-tool.tsx +89 -28
- 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/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +12 -0
- 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/window/move-window-tool.tsx +10 -0
- package/src/components/tools/window/window-tool.tsx +12 -0
- package/src/components/ui/action-menu/control-modes.tsx +9 -4
- 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/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 +97 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/panel-manager.tsx +31 -29
- 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 +173 -19
- 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 +7 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +29 -32
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +7 -3
- package/src/components/ui/viewer-toolbar.tsx +55 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +3 -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 +118 -10
|
@@ -3,25 +3,81 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
+
getEffectiveWallSurfaceMaterial,
|
|
7
|
+
getClampedWallCurveOffset,
|
|
8
|
+
getMaxWallCurveOffset,
|
|
9
|
+
getWallCurveLength,
|
|
10
|
+
getWallSurfaceMaterialSignature,
|
|
11
|
+
normalizeWallCurveOffset,
|
|
6
12
|
type MaterialSchema,
|
|
7
13
|
useScene,
|
|
14
|
+
type WallSurfaceSide,
|
|
8
15
|
type WallNode,
|
|
9
16
|
} from '@pascal-app/core'
|
|
10
17
|
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
-
import {
|
|
18
|
+
import { Move, Spline } from 'lucide-react'
|
|
19
|
+
import { useCallback, useMemo } from 'react'
|
|
20
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
21
|
+
import useEditor from '../../../store/use-editor'
|
|
22
|
+
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
12
23
|
import { MaterialPicker } from '../controls/material-picker'
|
|
13
24
|
import { PanelSection } from '../controls/panel-section'
|
|
14
25
|
import { SliderControl } from '../controls/slider-control'
|
|
15
26
|
import { PanelWrapper } from './panel-wrapper'
|
|
16
27
|
|
|
28
|
+
function buildWallSurfaceMaterialPatch(
|
|
29
|
+
node: WallNode,
|
|
30
|
+
targetSide: WallSurfaceSide | null,
|
|
31
|
+
material: MaterialSchema | undefined,
|
|
32
|
+
materialPreset: string | undefined,
|
|
33
|
+
): Partial<WallNode> {
|
|
34
|
+
const nextSurfaceMaterial = { material, materialPreset }
|
|
35
|
+
const nextInterior =
|
|
36
|
+
targetSide === null || targetSide === 'interior'
|
|
37
|
+
? nextSurfaceMaterial
|
|
38
|
+
: getEffectiveWallSurfaceMaterial(node, 'interior')
|
|
39
|
+
const nextExterior =
|
|
40
|
+
targetSide === null || targetSide === 'exterior'
|
|
41
|
+
? nextSurfaceMaterial
|
|
42
|
+
: getEffectiveWallSurfaceMaterial(node, 'exterior')
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
interiorMaterial: nextInterior.material,
|
|
46
|
+
interiorMaterialPreset: nextInterior.materialPreset,
|
|
47
|
+
exteriorMaterial: nextExterior.material,
|
|
48
|
+
exteriorMaterialPreset: nextExterior.materialPreset,
|
|
49
|
+
material: undefined,
|
|
50
|
+
materialPreset: undefined,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
17
54
|
export function WallPanel() {
|
|
18
|
-
const
|
|
55
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
19
56
|
const setSelection = useViewer((s) => s.setSelection)
|
|
20
|
-
const nodes = useScene((s) => s.nodes)
|
|
21
57
|
const updateNode = useScene((s) => s.updateNode)
|
|
58
|
+
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
59
|
+
const setCurvingWall = useEditor((s) => s.setCurvingWall)
|
|
60
|
+
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
61
|
+
|
|
62
|
+
const node = useScene((s) =>
|
|
63
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined,
|
|
64
|
+
)
|
|
22
65
|
|
|
23
|
-
|
|
24
|
-
|
|
66
|
+
// Boolean selector — re-renders only when this specific wall's child
|
|
67
|
+
// composition crosses the "has a door/window/wall-item" threshold.
|
|
68
|
+
const hasWallChildrenBlockingCurve = useScene((s) => {
|
|
69
|
+
if (!node) return false
|
|
70
|
+
return (node.children ?? []).some((childId) => {
|
|
71
|
+
const child = s.nodes[childId as AnyNodeId]
|
|
72
|
+
if (!child) return false
|
|
73
|
+
if (child.type === 'door' || child.type === 'window') return true
|
|
74
|
+
if (child.type === 'item') {
|
|
75
|
+
const attachTo = child.asset?.attachTo
|
|
76
|
+
return attachTo === 'wall' || attachTo === 'wall-side'
|
|
77
|
+
}
|
|
78
|
+
return false
|
|
79
|
+
})
|
|
80
|
+
})
|
|
25
81
|
|
|
26
82
|
const handleUpdate = useCallback(
|
|
27
83
|
(updates: Partial<WallNode>) => {
|
|
@@ -32,6 +88,35 @@ export function WallPanel() {
|
|
|
32
88
|
[selectedId, updateNode],
|
|
33
89
|
)
|
|
34
90
|
|
|
91
|
+
const effectiveInteriorMaterial = useMemo(
|
|
92
|
+
() => (node ? getEffectiveWallSurfaceMaterial(node, 'interior') : {}),
|
|
93
|
+
[node],
|
|
94
|
+
)
|
|
95
|
+
const effectiveExteriorMaterial = useMemo(
|
|
96
|
+
() => (node ? getEffectiveWallSurfaceMaterial(node, 'exterior') : {}),
|
|
97
|
+
[node],
|
|
98
|
+
)
|
|
99
|
+
const surfaceMaterialsMatch = useMemo(
|
|
100
|
+
() =>
|
|
101
|
+
getWallSurfaceMaterialSignature(effectiveInteriorMaterial) ===
|
|
102
|
+
getWallSurfaceMaterialSignature(effectiveExteriorMaterial),
|
|
103
|
+
[effectiveExteriorMaterial, effectiveInteriorMaterial],
|
|
104
|
+
)
|
|
105
|
+
const materialTargetSide =
|
|
106
|
+
selectedMaterialTarget &&
|
|
107
|
+
selectedMaterialTarget.nodeId === node?.id &&
|
|
108
|
+
(selectedMaterialTarget.role === 'interior' || selectedMaterialTarget.role === 'exterior')
|
|
109
|
+
? selectedMaterialTarget.role
|
|
110
|
+
: null
|
|
111
|
+
const materialPickerValue =
|
|
112
|
+
materialTargetSide === 'interior'
|
|
113
|
+
? effectiveInteriorMaterial
|
|
114
|
+
: materialTargetSide === 'exterior'
|
|
115
|
+
? effectiveExteriorMaterial
|
|
116
|
+
: surfaceMaterialsMatch
|
|
117
|
+
? effectiveInteriorMaterial
|
|
118
|
+
: {}
|
|
119
|
+
|
|
35
120
|
const handleUpdateLength = useCallback(
|
|
36
121
|
(newLength: number) => {
|
|
37
122
|
if (!node || newLength <= 0) return
|
|
@@ -55,25 +140,50 @@ export function WallPanel() {
|
|
|
55
140
|
[node, handleUpdate],
|
|
56
141
|
)
|
|
57
142
|
|
|
58
|
-
const
|
|
143
|
+
const handleMaterialPresetChange = useCallback(
|
|
144
|
+
(materialPreset: string) => {
|
|
145
|
+
if (!node || !materialTargetSide) return
|
|
146
|
+
handleUpdate(buildWallSurfaceMaterialPatch(node, materialTargetSide, undefined, materialPreset))
|
|
147
|
+
},
|
|
148
|
+
[handleUpdate, materialTargetSide, node],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const handleCustomMaterialChange = useCallback(
|
|
59
152
|
(material: MaterialSchema) => {
|
|
60
|
-
|
|
153
|
+
if (!node || !materialTargetSide) return
|
|
154
|
+
handleUpdate(buildWallSurfaceMaterialPatch(node, materialTargetSide, material, undefined))
|
|
61
155
|
},
|
|
62
|
-
[handleUpdate],
|
|
156
|
+
[handleUpdate, materialTargetSide, node],
|
|
63
157
|
)
|
|
64
158
|
|
|
65
159
|
const handleClose = useCallback(() => {
|
|
66
160
|
setSelection({ selectedIds: [] })
|
|
67
161
|
}, [setSelection])
|
|
68
162
|
|
|
69
|
-
|
|
163
|
+
const handleMove = useCallback(() => {
|
|
164
|
+
if (!node) return
|
|
165
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
166
|
+
setMovingNode(node)
|
|
167
|
+
setSelection({ selectedIds: [] })
|
|
168
|
+
}, [node, setMovingNode, setSelection])
|
|
169
|
+
|
|
170
|
+
const handleCurve = useCallback(() => {
|
|
171
|
+
if (!node) return
|
|
172
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
173
|
+
setCurvingWall(node)
|
|
174
|
+
setSelection({ selectedIds: [] })
|
|
175
|
+
}, [node, setCurvingWall, setSelection])
|
|
176
|
+
|
|
177
|
+
if (!(node && node.type === 'wall' && selectedId)) return null
|
|
70
178
|
|
|
71
179
|
const dx = node.end[0] - node.start[0]
|
|
72
180
|
const dz = node.end[1] - node.start[1]
|
|
73
|
-
const length =
|
|
181
|
+
const length = getWallCurveLength(node)
|
|
74
182
|
|
|
75
183
|
const height = node.height ?? 2.5
|
|
76
184
|
const thickness = node.thickness ?? 0.1
|
|
185
|
+
const curveOffset = getClampedWallCurveOffset(node)
|
|
186
|
+
const maxCurveOffset = getMaxWallCurveOffset(node)
|
|
77
187
|
|
|
78
188
|
return (
|
|
79
189
|
<PanelWrapper
|
|
@@ -113,10 +223,48 @@ export function WallPanel() {
|
|
|
113
223
|
unit="m"
|
|
114
224
|
value={Math.round(thickness * 1000) / 1000}
|
|
115
225
|
/>
|
|
226
|
+
{!hasWallChildrenBlockingCurve && (
|
|
227
|
+
<SliderControl
|
|
228
|
+
label="Curve"
|
|
229
|
+
max={Math.max(0.01, maxCurveOffset)}
|
|
230
|
+
min={-Math.max(0.01, maxCurveOffset)}
|
|
231
|
+
onChange={(v) => handleUpdate({ curveOffset: normalizeWallCurveOffset(node, v) })}
|
|
232
|
+
precision={2}
|
|
233
|
+
step={0.1}
|
|
234
|
+
unit="m"
|
|
235
|
+
value={Math.round(curveOffset * 100) / 100}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
116
238
|
</PanelSection>
|
|
117
239
|
|
|
118
240
|
<PanelSection title="Material">
|
|
119
|
-
|
|
241
|
+
{!materialTargetSide ? (
|
|
242
|
+
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
|
243
|
+
Click the wall face you want to edit. Materials now apply to one side at a time.
|
|
244
|
+
</div>
|
|
245
|
+
) : null}
|
|
246
|
+
<MaterialPicker
|
|
247
|
+
disabled={!materialTargetSide}
|
|
248
|
+
hideSideControl
|
|
249
|
+
nodeType="wall"
|
|
250
|
+
onChange={handleCustomMaterialChange}
|
|
251
|
+
onSelectMaterialPreset={handleMaterialPresetChange}
|
|
252
|
+
selectedMaterialPreset={materialPickerValue.materialPreset}
|
|
253
|
+
value={materialPickerValue.material}
|
|
254
|
+
/>
|
|
255
|
+
</PanelSection>
|
|
256
|
+
|
|
257
|
+
<PanelSection title="Actions">
|
|
258
|
+
<ActionGroup>
|
|
259
|
+
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
|
260
|
+
{!hasWallChildrenBlockingCurve && (
|
|
261
|
+
<ActionButton
|
|
262
|
+
icon={<Spline className="h-3.5 w-3.5" />}
|
|
263
|
+
label="Curve"
|
|
264
|
+
onClick={handleCurve}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
</ActionGroup>
|
|
120
268
|
</PanelSection>
|
|
121
269
|
</PanelWrapper>
|
|
122
270
|
)
|
|
@@ -24,19 +24,17 @@ import { PanelWrapper } from './panel-wrapper'
|
|
|
24
24
|
import { PresetsPopover } from './presets/presets-popover'
|
|
25
25
|
|
|
26
26
|
export function WindowPanel() {
|
|
27
|
-
const
|
|
27
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
28
28
|
const setSelection = useViewer((s) => s.setSelection)
|
|
29
|
-
const nodes = useScene((s) => s.nodes)
|
|
30
29
|
const updateNode = useScene((s) => s.updateNode)
|
|
31
30
|
const deleteNode = useScene((s) => s.deleteNode)
|
|
32
31
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
33
32
|
|
|
34
33
|
const adapter = usePresetsAdapter()
|
|
35
34
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
: undefined
|
|
35
|
+
const node = useScene((s) =>
|
|
36
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as WindowNode | undefined) : undefined,
|
|
37
|
+
)
|
|
40
38
|
|
|
41
39
|
const handleUpdate = useCallback(
|
|
42
40
|
(updates: Partial<WindowNode>) => {
|
|
@@ -153,7 +151,7 @@ export function WindowPanel() {
|
|
|
153
151
|
[handleUpdate],
|
|
154
152
|
)
|
|
155
153
|
|
|
156
|
-
if (!node
|
|
154
|
+
if (!(node && node.type === 'window' && selectedId)) return null
|
|
157
155
|
|
|
158
156
|
const numCols = node.columnRatios.length
|
|
159
157
|
const numRows = node.rowRatios.length
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AnyNodeId, type BuildingNode, LevelNode, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { Building2, Plus } from 'lucide-react'
|
|
4
|
-
import { useState } from 'react'
|
|
4
|
+
import { memo, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
6
|
import {
|
|
7
7
|
Tooltip,
|
|
@@ -17,7 +17,11 @@ interface BuildingTreeNodeProps {
|
|
|
17
17
|
isLast?: boolean
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export
|
|
20
|
+
export const BuildingTreeNode = memo(function BuildingTreeNode({
|
|
21
|
+
nodeId,
|
|
22
|
+
depth,
|
|
23
|
+
isLast,
|
|
24
|
+
}: BuildingTreeNodeProps) {
|
|
21
25
|
const [expanded, setExpanded] = useState(true)
|
|
22
26
|
const createNode = useScene((state) => state.createNode)
|
|
23
27
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
@@ -84,4 +88,4 @@ export function BuildingTreeNode({ nodeId, depth, isLast }: BuildingTreeNodeProp
|
|
|
84
88
|
))}
|
|
85
89
|
</TreeNodeWrapper>
|
|
86
90
|
)
|
|
87
|
-
}
|
|
91
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AnyNodeId, type CeilingNode, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import Image from 'next/image'
|
|
4
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
6
|
import useEditor from './../../../../../store/use-editor'
|
|
7
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
@@ -14,7 +14,11 @@ interface CeilingTreeNodeProps {
|
|
|
14
14
|
isLast?: boolean
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export const CeilingTreeNode = memo(function CeilingTreeNode({
|
|
18
|
+
nodeId,
|
|
19
|
+
depth,
|
|
20
|
+
isLast,
|
|
21
|
+
}: CeilingTreeNodeProps) {
|
|
18
22
|
const [expanded, setExpanded] = useState(false)
|
|
19
23
|
const [isEditing, setIsEditing] = useState(false)
|
|
20
24
|
const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
|
|
@@ -113,7 +117,7 @@ export function CeilingTreeNode({ nodeId, depth, isLast }: CeilingTreeNodeProps)
|
|
|
113
117
|
))}
|
|
114
118
|
</TreeNodeWrapper>
|
|
115
119
|
)
|
|
116
|
-
}
|
|
120
|
+
})
|
|
117
121
|
|
|
118
122
|
/**
|
|
119
123
|
* Calculate the area of a polygon using the shoelace formula
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import Image from 'next/image'
|
|
6
|
-
import { useCallback, useState } from 'react'
|
|
6
|
+
import { memo, useCallback, useState } from 'react'
|
|
7
7
|
import useEditor from './../../../../../store/use-editor'
|
|
8
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
9
9
|
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
@@ -15,7 +15,11 @@ interface DoorTreeNodeProps {
|
|
|
15
15
|
isLast?: boolean
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export
|
|
18
|
+
export const DoorTreeNode = memo(function DoorTreeNode({
|
|
19
|
+
nodeId,
|
|
20
|
+
depth,
|
|
21
|
+
isLast,
|
|
22
|
+
}: DoorTreeNodeProps) {
|
|
19
23
|
const [isEditing, setIsEditing] = useState(false)
|
|
20
24
|
const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
|
|
21
25
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
@@ -72,4 +76,4 @@ export function DoorTreeNode({ nodeId, depth, isLast }: DoorTreeNodeProps) {
|
|
|
72
76
|
onToggle={() => {}}
|
|
73
77
|
/>
|
|
74
78
|
)
|
|
75
|
-
}
|
|
79
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AnyNodeId, type FenceNode, 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, 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'
|
|
@@ -13,7 +13,11 @@ interface FenceTreeNodeProps {
|
|
|
13
13
|
isLast?: boolean
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export
|
|
16
|
+
export const FenceTreeNode = memo(function FenceTreeNode({
|
|
17
|
+
nodeId,
|
|
18
|
+
depth,
|
|
19
|
+
isLast,
|
|
20
|
+
}: FenceTreeNodeProps) {
|
|
17
21
|
const node = useScene((state) => state.nodes[nodeId]) as FenceNode | undefined
|
|
18
22
|
const [isEditing, setIsEditing] = useState(false)
|
|
19
23
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
@@ -62,4 +66,4 @@ export function FenceTreeNode({ nodeId, depth, isLast }: FenceTreeNodeProps) {
|
|
|
62
66
|
onToggle={() => {}}
|
|
63
67
|
/>
|
|
64
68
|
)
|
|
65
|
-
}
|
|
69
|
+
})
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
X,
|
|
24
24
|
} from 'lucide-react'
|
|
25
25
|
import { AnimatePresence, LayoutGroup, motion } from 'motion/react'
|
|
26
|
-
import { useEffect, useRef, useState } from 'react'
|
|
26
|
+
import { memo, useEffect, useRef, useState } from 'react'
|
|
27
27
|
import { useShallow } from 'zustand/react/shallow'
|
|
28
28
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
29
29
|
import {
|
|
@@ -80,7 +80,7 @@ function useSiteNode(): SiteNode | null {
|
|
|
80
80
|
)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function PropertyLineSection() {
|
|
83
|
+
const PropertyLineSection = memo(function PropertyLineSection() {
|
|
84
84
|
const siteNode = useSiteNode()
|
|
85
85
|
const updateNode = useScene((state) => state.updateNode)
|
|
86
86
|
const mode = useEditor((state) => state.mode)
|
|
@@ -218,13 +218,13 @@ function PropertyLineSection() {
|
|
|
218
218
|
)}
|
|
219
219
|
</div>
|
|
220
220
|
)
|
|
221
|
-
}
|
|
221
|
+
})
|
|
222
222
|
|
|
223
223
|
// ============================================================================
|
|
224
224
|
// SITE PHASE VIEW - Property line + building buttons
|
|
225
225
|
// ============================================================================
|
|
226
226
|
|
|
227
|
-
function CameraPopover({
|
|
227
|
+
const CameraPopover = memo(function CameraPopover({
|
|
228
228
|
nodeId,
|
|
229
229
|
hasCamera,
|
|
230
230
|
open,
|
|
@@ -303,9 +303,9 @@ function CameraPopover({
|
|
|
303
303
|
</PopoverContent>
|
|
304
304
|
</Popover>
|
|
305
305
|
)
|
|
306
|
-
}
|
|
306
|
+
})
|
|
307
307
|
|
|
308
|
-
function ReferenceItem({
|
|
308
|
+
const ReferenceItem = memo(function ReferenceItem({
|
|
309
309
|
refNode,
|
|
310
310
|
isLastRow,
|
|
311
311
|
setSelectedReferenceId,
|
|
@@ -375,7 +375,7 @@ function ReferenceItem({
|
|
|
375
375
|
</button>
|
|
376
376
|
</div>
|
|
377
377
|
)
|
|
378
|
-
}
|
|
378
|
+
})
|
|
379
379
|
|
|
380
380
|
const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
|
|
381
381
|
|
|
@@ -387,7 +387,7 @@ interface LevelReferencesProps {
|
|
|
387
387
|
onDeleteAsset?: (projectId: string, url: string) => void
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
function LevelReferences({
|
|
390
|
+
const LevelReferences = memo(function LevelReferences({
|
|
391
391
|
levelId,
|
|
392
392
|
isLastLevel,
|
|
393
393
|
projectId,
|
|
@@ -554,9 +554,9 @@ function LevelReferences({
|
|
|
554
554
|
)}
|
|
555
555
|
</div>
|
|
556
556
|
)
|
|
557
|
-
}
|
|
557
|
+
})
|
|
558
558
|
|
|
559
|
-
function LevelItem({
|
|
559
|
+
const LevelItem = memo(function LevelItem({
|
|
560
560
|
level,
|
|
561
561
|
selectedLevelId,
|
|
562
562
|
setSelection,
|
|
@@ -784,9 +784,9 @@ function LevelItem({
|
|
|
784
784
|
</AnimatePresence>
|
|
785
785
|
</div>
|
|
786
786
|
)
|
|
787
|
-
}
|
|
787
|
+
})
|
|
788
788
|
|
|
789
|
-
function LevelsSection({
|
|
789
|
+
const LevelsSection = memo(function LevelsSection({
|
|
790
790
|
projectId,
|
|
791
791
|
onUploadAsset,
|
|
792
792
|
onDeleteAsset,
|
|
@@ -870,9 +870,9 @@ function LevelsSection({
|
|
|
870
870
|
</div>
|
|
871
871
|
</div>
|
|
872
872
|
)
|
|
873
|
-
}
|
|
873
|
+
})
|
|
874
874
|
|
|
875
|
-
function LayerToggle() {
|
|
875
|
+
const LayerToggle = memo(function LayerToggle() {
|
|
876
876
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
877
877
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
878
878
|
const phase = useEditor((state) => state.phase)
|
|
@@ -1000,9 +1000,9 @@ function LayerToggle() {
|
|
|
1000
1000
|
</button>
|
|
1001
1001
|
</div>
|
|
1002
1002
|
)
|
|
1003
|
-
}
|
|
1003
|
+
})
|
|
1004
1004
|
|
|
1005
|
-
function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
1005
|
+
const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
1006
1006
|
const [isEditing, setIsEditing] = useState(false)
|
|
1007
1007
|
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
|
|
1008
1008
|
const deleteNode = useScene((state) => state.deleteNode)
|
|
@@ -1163,9 +1163,9 @@ function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
|
1163
1163
|
</div>
|
|
1164
1164
|
</div>
|
|
1165
1165
|
)
|
|
1166
|
-
}
|
|
1166
|
+
})
|
|
1167
1167
|
|
|
1168
|
-
function MultiSelectionBadge() {
|
|
1168
|
+
const MultiSelectionBadge = memo(function MultiSelectionBadge() {
|
|
1169
1169
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
1170
1170
|
const setSelection = useViewer((state) => state.setSelection)
|
|
1171
1171
|
|
|
@@ -1185,9 +1185,9 @@ function MultiSelectionBadge() {
|
|
|
1185
1185
|
</div>
|
|
1186
1186
|
</div>
|
|
1187
1187
|
)
|
|
1188
|
-
}
|
|
1188
|
+
})
|
|
1189
1189
|
|
|
1190
|
-
function ContentSection() {
|
|
1190
|
+
const ContentSection = memo(function ContentSection() {
|
|
1191
1191
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
1192
1192
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
1193
1193
|
const phase = useEditor((state) => state.phase)
|
|
@@ -1265,9 +1265,9 @@ function ContentSection() {
|
|
|
1265
1265
|
</div>
|
|
1266
1266
|
</TreeNodeDragProvider>
|
|
1267
1267
|
)
|
|
1268
|
-
}
|
|
1268
|
+
})
|
|
1269
1269
|
|
|
1270
|
-
function BuildingItem({
|
|
1270
|
+
const BuildingItem = memo(function BuildingItem({
|
|
1271
1271
|
building,
|
|
1272
1272
|
isBuildingActive,
|
|
1273
1273
|
buildingCameraOpen,
|
|
@@ -1308,19 +1308,16 @@ function BuildingItem({
|
|
|
1308
1308
|
}
|
|
1309
1309
|
|
|
1310
1310
|
return (
|
|
1311
|
-
<
|
|
1311
|
+
<div
|
|
1312
1312
|
className={cn('flex shrink-0 flex-col overflow-hidden', isBuildingActive && 'min-h-0 flex-1')}
|
|
1313
|
-
layout
|
|
1314
|
-
transition={{ type: 'spring', bounce: 0, duration: 0.4 }}
|
|
1315
1313
|
>
|
|
1316
|
-
<
|
|
1314
|
+
<div
|
|
1317
1315
|
className={cn(
|
|
1318
1316
|
'group/building flex h-10 shrink-0 cursor-pointer items-center border-border/50 border-b pr-2 transition-all duration-200',
|
|
1319
1317
|
isBuildingActive
|
|
1320
1318
|
? 'bg-accent/50 text-foreground'
|
|
1321
1319
|
: 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
|
|
1322
1320
|
)}
|
|
1323
|
-
layout="position"
|
|
1324
1321
|
onClick={handleSelect}
|
|
1325
1322
|
onDoubleClick={handleDoubleClick}
|
|
1326
1323
|
ref={itemRef}
|
|
@@ -1404,7 +1401,7 @@ function BuildingItem({
|
|
|
1404
1401
|
</div>
|
|
1405
1402
|
</PopoverContent>
|
|
1406
1403
|
</Popover>
|
|
1407
|
-
</
|
|
1404
|
+
</div>
|
|
1408
1405
|
|
|
1409
1406
|
{/* Tools and content for the active building */}
|
|
1410
1407
|
<AnimatePresence initial={false}>
|
|
@@ -1433,9 +1430,9 @@ function BuildingItem({
|
|
|
1433
1430
|
</motion.div>
|
|
1434
1431
|
)}
|
|
1435
1432
|
</AnimatePresence>
|
|
1436
|
-
</
|
|
1433
|
+
</div>
|
|
1437
1434
|
)
|
|
1438
|
-
}
|
|
1435
|
+
})
|
|
1439
1436
|
|
|
1440
1437
|
export interface SitePanelProps {
|
|
1441
1438
|
projectId?: string
|
|
@@ -1534,7 +1531,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1534
1531
|
No buildings yet
|
|
1535
1532
|
</motion.div>
|
|
1536
1533
|
) : (
|
|
1537
|
-
<
|
|
1534
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
1538
1535
|
{buildings.map((building) => {
|
|
1539
1536
|
const isBuildingActive =
|
|
1540
1537
|
(phase === 'structure' || phase === 'furnish') &&
|
|
@@ -1553,7 +1550,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1553
1550
|
/>
|
|
1554
1551
|
)
|
|
1555
1552
|
})}
|
|
1556
|
-
</
|
|
1553
|
+
</div>
|
|
1557
1554
|
)}
|
|
1558
1555
|
</motion.div>
|
|
1559
1556
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
6
|
import useEditor from './../../../../../store/use-editor'
|
|
7
7
|
import { InlineRenameInput } from './inline-rename-input'
|
|
@@ -24,7 +24,11 @@ interface ItemTreeNodeProps {
|
|
|
24
24
|
isLast?: boolean
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export
|
|
27
|
+
export const ItemTreeNode = memo(function ItemTreeNode({
|
|
28
|
+
nodeId,
|
|
29
|
+
depth,
|
|
30
|
+
isLast,
|
|
31
|
+
}: ItemTreeNodeProps) {
|
|
28
32
|
const [isEditing, setIsEditing] = useState(false)
|
|
29
33
|
const [expanded, setExpanded] = useState(true)
|
|
30
34
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
@@ -121,4 +125,4 @@ export function ItemTreeNode({ nodeId, depth, isLast }: ItemTreeNodeProps) {
|
|
|
121
125
|
))}
|
|
122
126
|
</TreeNodeWrapper>
|
|
123
127
|
)
|
|
124
|
-
}
|
|
128
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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 { useCallback, useState } from 'react'
|
|
4
|
+
import { memo, useCallback, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
6
|
import { InlineRenameInput } from './inline-rename-input'
|
|
7
7
|
import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
@@ -13,7 +13,11 @@ interface LevelTreeNodeProps {
|
|
|
13
13
|
isLast?: boolean
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export
|
|
16
|
+
export const LevelTreeNode = memo(function LevelTreeNode({
|
|
17
|
+
nodeId,
|
|
18
|
+
depth,
|
|
19
|
+
isLast,
|
|
20
|
+
}: LevelTreeNodeProps) {
|
|
17
21
|
const [expanded, setExpanded] = useState(true)
|
|
18
22
|
const [isEditing, setIsEditing] = useState(false)
|
|
19
23
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
@@ -67,4 +71,4 @@ export function LevelTreeNode({ nodeId, depth, isLast }: LevelTreeNodeProps) {
|
|
|
67
71
|
))}
|
|
68
72
|
</TreeNodeWrapper>
|
|
69
73
|
)
|
|
70
|
-
}
|
|
74
|
+
})
|
|
@@ -2,7 +2,7 @@ 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
6
|
import { useShallow } from 'zustand/react/shallow'
|
|
7
7
|
import useEditor from '../../../../../store/use-editor'
|
|
8
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
@@ -16,7 +16,11 @@ interface RoofTreeNodeProps {
|
|
|
16
16
|
isLast?: boolean
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export
|
|
19
|
+
export const RoofTreeNode = memo(function RoofTreeNode({
|
|
20
|
+
nodeId,
|
|
21
|
+
depth,
|
|
22
|
+
isLast,
|
|
23
|
+
}: RoofTreeNodeProps) {
|
|
20
24
|
const [isEditing, setIsEditing] = useState(false)
|
|
21
25
|
const [expanded, setExpanded] = useState(false)
|
|
22
26
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
@@ -147,7 +151,7 @@ export function RoofTreeNode({ nodeId, depth, isLast }: RoofTreeNodeProps) {
|
|
|
147
151
|
</TreeNodeWrapper>
|
|
148
152
|
</div>
|
|
149
153
|
)
|
|
150
|
-
}
|
|
154
|
+
})
|
|
151
155
|
|
|
152
156
|
function RoofSegmentTreeNode({
|
|
153
157
|
node,
|