@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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type AnyNodeId, type FenceNode, useScene } from '@pascal-app/core'
|
|
2
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { memo, 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 FenceTreeNodeProps {
|
|
11
|
+
nodeId: AnyNodeId
|
|
12
|
+
depth: number
|
|
13
|
+
isLast?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const FenceTreeNode = memo(function FenceTreeNode({
|
|
17
|
+
nodeId,
|
|
18
|
+
depth,
|
|
19
|
+
isLast,
|
|
20
|
+
}: FenceTreeNodeProps) {
|
|
21
|
+
const node = useScene((state) => state.nodes[nodeId]) as FenceNode | undefined
|
|
22
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
23
|
+
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
24
|
+
const isSelected = 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
|
+
if (!node) return null
|
|
30
|
+
|
|
31
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
32
|
+
e.stopPropagation()
|
|
33
|
+
const handled = handleTreeSelection(e, nodeId, selectedIds, setSelection)
|
|
34
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
35
|
+
useEditor.getState().setPhase('structure')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<TreeNodeWrapper
|
|
41
|
+
actions={<TreeNodeActions node={node} />}
|
|
42
|
+
depth={depth}
|
|
43
|
+
expanded={false}
|
|
44
|
+
hasChildren={false}
|
|
45
|
+
icon={
|
|
46
|
+
<Image alt="" className="object-contain" height={14} src="/icons/fence.png" width={14} />
|
|
47
|
+
}
|
|
48
|
+
isHovered={isHovered}
|
|
49
|
+
isLast={isLast}
|
|
50
|
+
isSelected={isSelected}
|
|
51
|
+
isVisible={node.visible !== false}
|
|
52
|
+
label={
|
|
53
|
+
<InlineRenameInput
|
|
54
|
+
defaultName="Fence"
|
|
55
|
+
isEditing={isEditing}
|
|
56
|
+
node={node}
|
|
57
|
+
onStartEditing={() => setIsEditing(true)}
|
|
58
|
+
onStopEditing={() => setIsEditing(false)}
|
|
59
|
+
/>
|
|
60
|
+
}
|
|
61
|
+
nodeId={nodeId}
|
|
62
|
+
onClick={handleClick}
|
|
63
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
64
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
65
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
66
|
+
onToggle={() => {}}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
})
|
|
@@ -23,7 +23,8 @@ 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
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
27
28
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
28
29
|
import {
|
|
29
30
|
Popover,
|
|
@@ -79,7 +80,7 @@ function useSiteNode(): SiteNode | null {
|
|
|
79
80
|
)
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
function PropertyLineSection() {
|
|
83
|
+
const PropertyLineSection = memo(function PropertyLineSection() {
|
|
83
84
|
const siteNode = useSiteNode()
|
|
84
85
|
const updateNode = useScene((state) => state.updateNode)
|
|
85
86
|
const mode = useEditor((state) => state.mode)
|
|
@@ -217,13 +218,13 @@ function PropertyLineSection() {
|
|
|
217
218
|
)}
|
|
218
219
|
</div>
|
|
219
220
|
)
|
|
220
|
-
}
|
|
221
|
+
})
|
|
221
222
|
|
|
222
223
|
// ============================================================================
|
|
223
224
|
// SITE PHASE VIEW - Property line + building buttons
|
|
224
225
|
// ============================================================================
|
|
225
226
|
|
|
226
|
-
function CameraPopover({
|
|
227
|
+
const CameraPopover = memo(function CameraPopover({
|
|
227
228
|
nodeId,
|
|
228
229
|
hasCamera,
|
|
229
230
|
open,
|
|
@@ -302,9 +303,9 @@ function CameraPopover({
|
|
|
302
303
|
</PopoverContent>
|
|
303
304
|
</Popover>
|
|
304
305
|
)
|
|
305
|
-
}
|
|
306
|
+
})
|
|
306
307
|
|
|
307
|
-
function ReferenceItem({
|
|
308
|
+
const ReferenceItem = memo(function ReferenceItem({
|
|
308
309
|
refNode,
|
|
309
310
|
isLastRow,
|
|
310
311
|
setSelectedReferenceId,
|
|
@@ -374,7 +375,7 @@ function ReferenceItem({
|
|
|
374
375
|
</button>
|
|
375
376
|
</div>
|
|
376
377
|
)
|
|
377
|
-
}
|
|
378
|
+
})
|
|
378
379
|
|
|
379
380
|
const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
|
|
380
381
|
|
|
@@ -386,15 +387,22 @@ interface LevelReferencesProps {
|
|
|
386
387
|
onDeleteAsset?: (projectId: string, url: string) => void
|
|
387
388
|
}
|
|
388
389
|
|
|
389
|
-
function LevelReferences({
|
|
390
|
+
const LevelReferences = memo(function LevelReferences({
|
|
390
391
|
levelId,
|
|
391
392
|
isLastLevel,
|
|
392
393
|
projectId,
|
|
393
394
|
onUploadAsset,
|
|
394
395
|
onDeleteAsset,
|
|
395
396
|
}: LevelReferencesProps) {
|
|
396
|
-
const nodes = useScene((s) => s.nodes)
|
|
397
397
|
const deleteNode = useScene((s) => s.deleteNode)
|
|
398
|
+
const references = useScene(
|
|
399
|
+
useShallow((s) =>
|
|
400
|
+
Object.values(s.nodes).filter(
|
|
401
|
+
(node): node is ScanNode | GuideNode =>
|
|
402
|
+
(node.type === 'scan' || node.type === 'guide') && node.parentId === levelId,
|
|
403
|
+
),
|
|
404
|
+
),
|
|
405
|
+
)
|
|
398
406
|
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
|
|
399
407
|
const uploadState = useUploadStore((s) => s.uploads[levelId])
|
|
400
408
|
const clearUpload = useUploadStore((s) => s.clearUpload)
|
|
@@ -409,11 +417,6 @@ function LevelReferences({
|
|
|
409
417
|
|
|
410
418
|
const scanInputRef = useRef<HTMLInputElement>(null)
|
|
411
419
|
|
|
412
|
-
const references = Object.values(nodes).filter(
|
|
413
|
-
(node): node is ScanNode | GuideNode =>
|
|
414
|
-
(node.type === 'scan' || node.type === 'guide') && node.parentId === levelId,
|
|
415
|
-
)
|
|
416
|
-
|
|
417
420
|
const handleAddAsset = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
418
421
|
const file = e.target.files?.[0]
|
|
419
422
|
if (!file) return
|
|
@@ -457,7 +460,10 @@ function LevelReferences({
|
|
|
457
460
|
|
|
458
461
|
const handleDelete = async (nodeId: string, e: React.MouseEvent) => {
|
|
459
462
|
e.stopPropagation()
|
|
460
|
-
const refNode = nodes[nodeId as AnyNodeId] as
|
|
463
|
+
const refNode = useScene.getState().nodes[nodeId as AnyNodeId] as
|
|
464
|
+
| ScanNode
|
|
465
|
+
| GuideNode
|
|
466
|
+
| undefined
|
|
461
467
|
|
|
462
468
|
if (
|
|
463
469
|
projectId &&
|
|
@@ -548,9 +554,9 @@ function LevelReferences({
|
|
|
548
554
|
)}
|
|
549
555
|
</div>
|
|
550
556
|
)
|
|
551
|
-
}
|
|
557
|
+
})
|
|
552
558
|
|
|
553
|
-
function LevelItem({
|
|
559
|
+
const LevelItem = memo(function LevelItem({
|
|
554
560
|
level,
|
|
555
561
|
selectedLevelId,
|
|
556
562
|
setSelection,
|
|
@@ -778,9 +784,9 @@ function LevelItem({
|
|
|
778
784
|
</AnimatePresence>
|
|
779
785
|
</div>
|
|
780
786
|
)
|
|
781
|
-
}
|
|
787
|
+
})
|
|
782
788
|
|
|
783
|
-
function LevelsSection({
|
|
789
|
+
const LevelsSection = memo(function LevelsSection({
|
|
784
790
|
projectId,
|
|
785
791
|
onUploadAsset,
|
|
786
792
|
onDeleteAsset,
|
|
@@ -789,21 +795,28 @@ function LevelsSection({
|
|
|
789
795
|
onUploadAsset?: (projectId: string, levelId: string, file: File, type: 'scan' | 'guide') => void
|
|
790
796
|
onDeleteAsset?: (projectId: string, url: string) => void
|
|
791
797
|
} = {}) {
|
|
792
|
-
const nodes = useScene((state) => state.nodes)
|
|
793
798
|
const createNode = useScene((state) => state.createNode)
|
|
794
799
|
const updateNode = useScene((state) => state.updateNode)
|
|
795
800
|
const selectedBuildingId = useViewer((state) => state.selection.buildingId)
|
|
796
801
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
797
802
|
const setSelection = useViewer((state) => state.setSelection)
|
|
798
803
|
|
|
799
|
-
const building =
|
|
804
|
+
const building = useScene((s) =>
|
|
805
|
+
selectedBuildingId ? ((s.nodes[selectedBuildingId] as BuildingNode | undefined) ?? null) : null,
|
|
806
|
+
)
|
|
807
|
+
const levels = useScene(
|
|
808
|
+
useShallow((s) => {
|
|
809
|
+
if (!selectedBuildingId) return []
|
|
810
|
+
const bldg = s.nodes[selectedBuildingId] as BuildingNode | undefined
|
|
811
|
+
if (!bldg) return []
|
|
812
|
+
return bldg.children
|
|
813
|
+
.map((id) => s.nodes[id])
|
|
814
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
815
|
+
}),
|
|
816
|
+
)
|
|
800
817
|
|
|
801
818
|
if (!building) return null
|
|
802
819
|
|
|
803
|
-
const levels = building.children
|
|
804
|
-
.map((id) => nodes[id])
|
|
805
|
-
.filter((node): node is LevelNode => node?.type === 'level')
|
|
806
|
-
|
|
807
820
|
const handleAddLevel = () => {
|
|
808
821
|
const newLevel = LevelNode.parse({
|
|
809
822
|
level: levels.length,
|
|
@@ -857,9 +870,9 @@ function LevelsSection({
|
|
|
857
870
|
</div>
|
|
858
871
|
</div>
|
|
859
872
|
)
|
|
860
|
-
}
|
|
873
|
+
})
|
|
861
874
|
|
|
862
|
-
function LayerToggle() {
|
|
875
|
+
const LayerToggle = memo(function LayerToggle() {
|
|
863
876
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
864
877
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
865
878
|
const phase = useEditor((state) => state.phase)
|
|
@@ -987,9 +1000,9 @@ function LayerToggle() {
|
|
|
987
1000
|
</button>
|
|
988
1001
|
</div>
|
|
989
1002
|
)
|
|
990
|
-
}
|
|
1003
|
+
})
|
|
991
1004
|
|
|
992
|
-
function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
1005
|
+
const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
993
1006
|
const [isEditing, setIsEditing] = useState(false)
|
|
994
1007
|
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
|
|
995
1008
|
const deleteNode = useScene((state) => state.deleteNode)
|
|
@@ -1150,9 +1163,9 @@ function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
|
1150
1163
|
</div>
|
|
1151
1164
|
</div>
|
|
1152
1165
|
)
|
|
1153
|
-
}
|
|
1166
|
+
})
|
|
1154
1167
|
|
|
1155
|
-
function MultiSelectionBadge() {
|
|
1168
|
+
const MultiSelectionBadge = memo(function MultiSelectionBadge() {
|
|
1156
1169
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
1157
1170
|
const setSelection = useViewer((state) => state.setSelection)
|
|
1158
1171
|
|
|
@@ -1172,10 +1185,9 @@ function MultiSelectionBadge() {
|
|
|
1172
1185
|
</div>
|
|
1173
1186
|
</div>
|
|
1174
1187
|
)
|
|
1175
|
-
}
|
|
1188
|
+
})
|
|
1176
1189
|
|
|
1177
|
-
function ContentSection() {
|
|
1178
|
-
const nodes = useScene((state) => state.nodes)
|
|
1190
|
+
const ContentSection = memo(function ContentSection() {
|
|
1179
1191
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
1180
1192
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
1181
1193
|
const phase = useEditor((state) => state.phase)
|
|
@@ -1183,7 +1195,25 @@ function ContentSection() {
|
|
|
1183
1195
|
const setMode = useEditor((state) => state.setMode)
|
|
1184
1196
|
const setTool = useEditor((state) => state.setTool)
|
|
1185
1197
|
|
|
1186
|
-
const level =
|
|
1198
|
+
const level = useScene((s) =>
|
|
1199
|
+
selectedLevelId ? ((s.nodes[selectedLevelId] as LevelNode | undefined) ?? null) : null,
|
|
1200
|
+
)
|
|
1201
|
+
const levelZones = useScene(
|
|
1202
|
+
useShallow((s) => {
|
|
1203
|
+
if (!selectedLevelId) return []
|
|
1204
|
+
return Object.values(s.nodes).filter(
|
|
1205
|
+
(node): node is ZoneNode => node.type === 'zone' && node.parentId === selectedLevelId,
|
|
1206
|
+
)
|
|
1207
|
+
}),
|
|
1208
|
+
)
|
|
1209
|
+
const elementChildren = useScene(
|
|
1210
|
+
useShallow((s) => {
|
|
1211
|
+
if (!selectedLevelId) return []
|
|
1212
|
+
const lvl = s.nodes[selectedLevelId] as LevelNode | undefined
|
|
1213
|
+
if (!lvl) return []
|
|
1214
|
+
return lvl.children.filter((childId) => s.nodes[childId]?.type !== 'zone')
|
|
1215
|
+
}),
|
|
1216
|
+
)
|
|
1187
1217
|
|
|
1188
1218
|
if (!level) {
|
|
1189
1219
|
return (
|
|
@@ -1192,11 +1222,6 @@ function ContentSection() {
|
|
|
1192
1222
|
}
|
|
1193
1223
|
|
|
1194
1224
|
if (structureLayer === 'zones') {
|
|
1195
|
-
// Show zones for this level
|
|
1196
|
-
const levelZones = Object.values(nodes).filter(
|
|
1197
|
-
(node): node is ZoneNode => node.type === 'zone' && node.parentId === selectedLevelId,
|
|
1198
|
-
)
|
|
1199
|
-
|
|
1200
1225
|
const handleAddZone = () => {
|
|
1201
1226
|
setPhase('structure')
|
|
1202
1227
|
setMode('build')
|
|
@@ -1223,21 +1248,9 @@ function ContentSection() {
|
|
|
1223
1248
|
)
|
|
1224
1249
|
}
|
|
1225
1250
|
|
|
1226
|
-
// Filter elements based on phase
|
|
1227
|
-
const elementChildren = level.children.filter((childId) => {
|
|
1228
|
-
const childNode = nodes[childId]
|
|
1229
|
-
if (!childNode || childNode.type === 'zone') return false
|
|
1230
|
-
|
|
1231
|
-
// We no longer filter out structural nodes in furnish mode or furnish nodes in structure mode
|
|
1232
|
-
// This allows nested items (like lights in a ceiling or cabinetry on a wall) to remain visible
|
|
1233
|
-
// and selectable in both modes, ensuring seamless transition in the tree view.
|
|
1234
|
-
return true
|
|
1235
|
-
})
|
|
1236
|
-
|
|
1237
1251
|
if (elementChildren.length === 0) {
|
|
1238
1252
|
return <div className="px-3 py-4 text-muted-foreground text-sm">No elements on this level</div>
|
|
1239
1253
|
}
|
|
1240
|
-
|
|
1241
1254
|
return (
|
|
1242
1255
|
<TreeNodeDragProvider>
|
|
1243
1256
|
<div className="flex flex-col">
|
|
@@ -1252,9 +1265,9 @@ function ContentSection() {
|
|
|
1252
1265
|
</div>
|
|
1253
1266
|
</TreeNodeDragProvider>
|
|
1254
1267
|
)
|
|
1255
|
-
}
|
|
1268
|
+
})
|
|
1256
1269
|
|
|
1257
|
-
function BuildingItem({
|
|
1270
|
+
const BuildingItem = memo(function BuildingItem({
|
|
1258
1271
|
building,
|
|
1259
1272
|
isBuildingActive,
|
|
1260
1273
|
buildingCameraOpen,
|
|
@@ -1295,19 +1308,16 @@ function BuildingItem({
|
|
|
1295
1308
|
}
|
|
1296
1309
|
|
|
1297
1310
|
return (
|
|
1298
|
-
<
|
|
1311
|
+
<div
|
|
1299
1312
|
className={cn('flex shrink-0 flex-col overflow-hidden', isBuildingActive && 'min-h-0 flex-1')}
|
|
1300
|
-
layout
|
|
1301
|
-
transition={{ type: 'spring', bounce: 0, duration: 0.4 }}
|
|
1302
1313
|
>
|
|
1303
|
-
<
|
|
1314
|
+
<div
|
|
1304
1315
|
className={cn(
|
|
1305
1316
|
'group/building flex h-10 shrink-0 cursor-pointer items-center border-border/50 border-b pr-2 transition-all duration-200',
|
|
1306
1317
|
isBuildingActive
|
|
1307
1318
|
? 'bg-accent/50 text-foreground'
|
|
1308
1319
|
: 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
|
|
1309
1320
|
)}
|
|
1310
|
-
layout="position"
|
|
1311
1321
|
onClick={handleSelect}
|
|
1312
1322
|
onDoubleClick={handleDoubleClick}
|
|
1313
1323
|
ref={itemRef}
|
|
@@ -1391,7 +1401,7 @@ function BuildingItem({
|
|
|
1391
1401
|
</div>
|
|
1392
1402
|
</PopoverContent>
|
|
1393
1403
|
</Popover>
|
|
1394
|
-
</
|
|
1404
|
+
</div>
|
|
1395
1405
|
|
|
1396
1406
|
{/* Tools and content for the active building */}
|
|
1397
1407
|
<AnimatePresence initial={false}>
|
|
@@ -1420,9 +1430,9 @@ function BuildingItem({
|
|
|
1420
1430
|
</motion.div>
|
|
1421
1431
|
)}
|
|
1422
1432
|
</AnimatePresence>
|
|
1423
|
-
</
|
|
1433
|
+
</div>
|
|
1424
1434
|
)
|
|
1425
|
-
}
|
|
1435
|
+
})
|
|
1426
1436
|
|
|
1427
1437
|
export interface SitePanelProps {
|
|
1428
1438
|
projectId?: string
|
|
@@ -1431,7 +1441,6 @@ export interface SitePanelProps {
|
|
|
1431
1441
|
}
|
|
1432
1442
|
|
|
1433
1443
|
export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanelProps = {}) {
|
|
1434
|
-
const nodes = useScene((state) => state.nodes)
|
|
1435
1444
|
const rootNodeIds = useScene((state) => state.rootNodeIds)
|
|
1436
1445
|
const updateNode = useScene((state) => state.updateNode)
|
|
1437
1446
|
const selectedBuildingId = useViewer((state) => state.selection.buildingId)
|
|
@@ -1442,13 +1451,20 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1442
1451
|
const [siteCameraOpen, setSiteCameraOpen] = useState(false)
|
|
1443
1452
|
const [buildingCameraOpen, setBuildingCameraOpen] = useState<string | null>(null)
|
|
1444
1453
|
|
|
1445
|
-
const siteNode =
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1454
|
+
const siteNode = useScene((s) =>
|
|
1455
|
+
rootNodeIds[0] ? ((s.nodes[rootNodeIds[0]] as SiteNode | undefined) ?? null) : null,
|
|
1456
|
+
)
|
|
1457
|
+
const buildings = useScene(
|
|
1458
|
+
useShallow((s) => {
|
|
1459
|
+
if (!siteNode) return []
|
|
1460
|
+
return siteNode.children
|
|
1461
|
+
.map((child) => {
|
|
1462
|
+
const id = typeof child === 'string' ? child : child.id
|
|
1463
|
+
return s.nodes[id] as BuildingNode | undefined
|
|
1464
|
+
})
|
|
1465
|
+
.filter((node): node is BuildingNode => node?.type === 'building')
|
|
1466
|
+
}),
|
|
1467
|
+
)
|
|
1452
1468
|
|
|
1453
1469
|
return (
|
|
1454
1470
|
<LayoutGroup>
|
|
@@ -1515,7 +1531,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1515
1531
|
No buildings yet
|
|
1516
1532
|
</motion.div>
|
|
1517
1533
|
) : (
|
|
1518
|
-
<
|
|
1534
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
1519
1535
|
{buildings.map((building) => {
|
|
1520
1536
|
const isBuildingActive =
|
|
1521
1537
|
(phase === 'structure' || phase === 'furnish') &&
|
|
@@ -1534,7 +1550,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1534
1550
|
/>
|
|
1535
1551
|
)
|
|
1536
1552
|
})}
|
|
1537
|
-
</
|
|
1553
|
+
</div>
|
|
1538
1554
|
)}
|
|
1539
1555
|
</motion.div>
|
|
1540
1556
|
</div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
2
2
|
import { Pencil } from 'lucide-react'
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
|
4
4
|
import { cn } from './../../../../../lib/utils'
|
|
5
5
|
|
|
6
6
|
interface InlineRenameInputProps {
|
|
7
|
-
|
|
7
|
+
nodeId: AnyNodeId
|
|
8
8
|
isEditing: boolean
|
|
9
9
|
onStopEditing: () => void
|
|
10
10
|
defaultName: string
|
|
@@ -12,8 +12,8 @@ interface InlineRenameInputProps {
|
|
|
12
12
|
onStartEditing?: () => void
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function InlineRenameInput({
|
|
16
|
-
|
|
15
|
+
export const InlineRenameInput = memo(function InlineRenameInput({
|
|
16
|
+
nodeId,
|
|
17
17
|
isEditing,
|
|
18
18
|
onStopEditing,
|
|
19
19
|
defaultName,
|
|
@@ -21,13 +21,14 @@ export function InlineRenameInput({
|
|
|
21
21
|
onStartEditing,
|
|
22
22
|
}: InlineRenameInputProps) {
|
|
23
23
|
const updateNode = useScene((s) => s.updateNode)
|
|
24
|
-
const
|
|
24
|
+
const name = useScene((s) => s.nodes[nodeId]?.name)
|
|
25
|
+
const [value, setValue] = useState(name || '')
|
|
25
26
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
26
27
|
const inputSize = Math.max((value || defaultName).length, 1)
|
|
27
28
|
|
|
28
29
|
useEffect(() => {
|
|
29
30
|
if (isEditing) {
|
|
30
|
-
setValue(
|
|
31
|
+
setValue(name || '')
|
|
31
32
|
// Focus and select all text after a short delay
|
|
32
33
|
setTimeout(() => {
|
|
33
34
|
if (inputRef.current) {
|
|
@@ -36,15 +37,15 @@ export function InlineRenameInput({
|
|
|
36
37
|
}
|
|
37
38
|
}, 0)
|
|
38
39
|
}
|
|
39
|
-
}, [isEditing,
|
|
40
|
+
}, [isEditing, name])
|
|
40
41
|
|
|
41
42
|
const handleSave = useCallback(() => {
|
|
42
43
|
const trimmed = value.trim()
|
|
43
|
-
if (trimmed !==
|
|
44
|
-
updateNode(
|
|
44
|
+
if (trimmed !== name) {
|
|
45
|
+
updateNode(nodeId, { name: trimmed || undefined })
|
|
45
46
|
}
|
|
46
47
|
onStopEditing()
|
|
47
|
-
}, [value,
|
|
48
|
+
}, [value, nodeId, name, updateNode, onStopEditing])
|
|
48
49
|
|
|
49
50
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
50
51
|
if (e.key === 'Enter') {
|
|
@@ -60,7 +61,7 @@ export function InlineRenameInput({
|
|
|
60
61
|
return (
|
|
61
62
|
<div className="group/rename flex h-5 min-w-0 items-center gap-1">
|
|
62
63
|
<span className={cn('truncate border-transparent border-b', className)}>
|
|
63
|
-
{
|
|
64
|
+
{name || defaultName}
|
|
64
65
|
</span>
|
|
65
66
|
{onStartEditing && (
|
|
66
67
|
<button
|
|
@@ -95,4 +96,4 @@ export function InlineRenameInput({
|
|
|
95
96
|
value={value}
|
|
96
97
|
/>
|
|
97
98
|
)
|
|
98
|
-
}
|
|
99
|
+
})
|