@pascal-app/editor 0.5.1 → 0.7.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 +12 -7
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +29 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +281 -83
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +10442 -3275
- package/src/components/editor/index.tsx +270 -20
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +766 -12
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +350 -157
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +377 -58
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- 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-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +19 -0
- package/src/components/tools/door/move-door-tool.tsx +38 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +27 -8
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
- package/src/components/tools/fence/move-fence-tool.tsx +102 -27
- package/src/components/tools/item/move-tool.tsx +19 -1
- package/src/components/tools/item/placement-math.ts +44 -7
- package/src/components/tools/item/placement-strategies.ts +111 -33
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
- package/src/components/tools/roof/move-roof-tool.tsx +111 -43
- package/src/components/tools/shared/polygon-editor.tsx +244 -29
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +30 -3
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +348 -17
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +28 -0
- package/src/components/tools/window/window-tool.tsx +17 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +37 -5
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +27 -5
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +189 -169
- package/src/components/ui/controls/slider-control.tsx +88 -26
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +47 -27
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +986 -294
- package/src/components/ui/panels/fence-panel.tsx +55 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +241 -30
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +243 -9
- package/src/components/ui/panels/roof-panel.tsx +30 -62
- package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
- package/src/components/ui/panels/slab-panel.tsx +46 -24
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +117 -69
- package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
- package/src/components/ui/panels/wall-panel.tsx +71 -17
- package/src/components/ui/panels/window-panel.tsx +665 -146
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- 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 +138 -56
- 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 +9 -5
- 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/spawn-tree-node.tsx +82 -0
- 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 +12 -6
- 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 +15 -8
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +96 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +67 -9
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +2 -1
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/history.ts +20 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +279 -15
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
type AnyNodeId,
|
|
4
4
|
type BuildingNode,
|
|
5
5
|
emitter,
|
|
6
|
-
|
|
6
|
+
GuideNode,
|
|
7
7
|
LevelNode,
|
|
8
|
-
|
|
8
|
+
ScanNode,
|
|
9
9
|
type SiteNode,
|
|
10
10
|
useScene,
|
|
11
11
|
type ZoneNode,
|
|
@@ -14,6 +14,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
14
14
|
import {
|
|
15
15
|
Camera,
|
|
16
16
|
ChevronDown,
|
|
17
|
+
Copy,
|
|
17
18
|
Loader2,
|
|
18
19
|
MoreHorizontal,
|
|
19
20
|
Pencil,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
X,
|
|
24
25
|
} from 'lucide-react'
|
|
25
26
|
import { AnimatePresence, LayoutGroup, motion } from 'motion/react'
|
|
26
|
-
import { useEffect, useRef, useState } from 'react'
|
|
27
|
+
import { memo, useEffect, useRef, useState } from 'react'
|
|
27
28
|
import { useShallow } from 'zustand/react/shallow'
|
|
28
29
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
29
30
|
import {
|
|
@@ -32,9 +33,17 @@ import {
|
|
|
32
33
|
PopoverTrigger,
|
|
33
34
|
} from './../../../../../components/ui/primitives/popover'
|
|
34
35
|
import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
|
|
36
|
+
import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
buildLevelDuplicateCreateOps,
|
|
40
|
+
type LevelDuplicatePreset,
|
|
41
|
+
} from './../../../../../lib/level-duplication'
|
|
42
|
+
|
|
35
43
|
import { cn } from './../../../../../lib/utils'
|
|
36
44
|
import useEditor from './../../../../../store/use-editor'
|
|
37
45
|
import { useUploadStore } from '../../../../../store/use-upload'
|
|
46
|
+
import { LevelDuplicateDialog } from '../../../level-duplicate-dialog'
|
|
38
47
|
import { InlineRenameInput } from './inline-rename-input'
|
|
39
48
|
import { focusTreeNode, TreeNode } from './tree-node'
|
|
40
49
|
import { TreeNodeDragProvider } from './tree-node-drag'
|
|
@@ -80,7 +89,7 @@ function useSiteNode(): SiteNode | null {
|
|
|
80
89
|
)
|
|
81
90
|
}
|
|
82
91
|
|
|
83
|
-
function PropertyLineSection() {
|
|
92
|
+
const PropertyLineSection = memo(function PropertyLineSection() {
|
|
84
93
|
const siteNode = useSiteNode()
|
|
85
94
|
const updateNode = useScene((state) => state.updateNode)
|
|
86
95
|
const mode = useEditor((state) => state.mode)
|
|
@@ -218,13 +227,13 @@ function PropertyLineSection() {
|
|
|
218
227
|
)}
|
|
219
228
|
</div>
|
|
220
229
|
)
|
|
221
|
-
}
|
|
230
|
+
})
|
|
222
231
|
|
|
223
232
|
// ============================================================================
|
|
224
233
|
// SITE PHASE VIEW - Property line + building buttons
|
|
225
234
|
// ============================================================================
|
|
226
235
|
|
|
227
|
-
function CameraPopover({
|
|
236
|
+
const CameraPopover = memo(function CameraPopover({
|
|
228
237
|
nodeId,
|
|
229
238
|
hasCamera,
|
|
230
239
|
open,
|
|
@@ -303,9 +312,9 @@ function CameraPopover({
|
|
|
303
312
|
</PopoverContent>
|
|
304
313
|
</Popover>
|
|
305
314
|
)
|
|
306
|
-
}
|
|
315
|
+
})
|
|
307
316
|
|
|
308
|
-
function ReferenceItem({
|
|
317
|
+
const ReferenceItem = memo(function ReferenceItem({
|
|
309
318
|
refNode,
|
|
310
319
|
isLastRow,
|
|
311
320
|
setSelectedReferenceId,
|
|
@@ -360,7 +369,7 @@ function ReferenceItem({
|
|
|
360
369
|
<InlineRenameInput
|
|
361
370
|
defaultName={refNode.type === 'scan' ? '3D Scan' : 'Guide Image'}
|
|
362
371
|
isEditing={isEditing}
|
|
363
|
-
|
|
372
|
+
nodeId={refNode.id}
|
|
364
373
|
onStartEditing={() => setIsEditing(true)}
|
|
365
374
|
onStopEditing={() => setIsEditing(false)}
|
|
366
375
|
/>
|
|
@@ -375,7 +384,7 @@ function ReferenceItem({
|
|
|
375
384
|
</button>
|
|
376
385
|
</div>
|
|
377
386
|
)
|
|
378
|
-
}
|
|
387
|
+
})
|
|
379
388
|
|
|
380
389
|
const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
|
|
381
390
|
|
|
@@ -387,14 +396,17 @@ interface LevelReferencesProps {
|
|
|
387
396
|
onDeleteAsset?: (projectId: string, url: string) => void
|
|
388
397
|
}
|
|
389
398
|
|
|
390
|
-
function LevelReferences({
|
|
399
|
+
const LevelReferences = memo(function LevelReferences({
|
|
391
400
|
levelId,
|
|
392
401
|
isLastLevel,
|
|
393
402
|
projectId,
|
|
394
403
|
onUploadAsset,
|
|
395
404
|
onDeleteAsset,
|
|
396
405
|
}: LevelReferencesProps) {
|
|
406
|
+
const createNode = useScene((s) => s.createNode)
|
|
397
407
|
const deleteNode = useScene((s) => s.deleteNode)
|
|
408
|
+
const setSelection = useViewer((s) => s.setSelection)
|
|
409
|
+
const setShowGuides = useViewer((s) => s.setShowGuides)
|
|
398
410
|
const references = useScene(
|
|
399
411
|
useShallow((s) =>
|
|
400
412
|
Object.values(s.nodes).filter(
|
|
@@ -417,19 +429,27 @@ function LevelReferences({
|
|
|
417
429
|
|
|
418
430
|
const scanInputRef = useRef<HTMLInputElement>(null)
|
|
419
431
|
|
|
420
|
-
const handleAddAsset = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
432
|
+
const handleAddAsset = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
421
433
|
const file = e.target.files?.[0]
|
|
422
434
|
if (!file) return
|
|
423
435
|
e.target.value = ''
|
|
424
436
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
437
|
+
// Auto-detect type based on file extension/mime type
|
|
438
|
+
const isScan =
|
|
439
|
+
file.name.toLowerCase().endsWith('.glb') || file.name.toLowerCase().endsWith('.gltf')
|
|
440
|
+
const isImage = file.type.startsWith('image/')
|
|
441
|
+
const type = isScan ? 'scan' : 'guide'
|
|
442
|
+
|
|
443
|
+
if (!(isScan || isImage)) {
|
|
444
|
+
useUploadStore.getState().startUpload(levelId, type, file.name)
|
|
445
|
+
useUploadStore
|
|
446
|
+
.getState()
|
|
447
|
+
.setError(levelId, 'Invalid file type. Please upload a .glb/.gltf scan or an image.')
|
|
428
448
|
return
|
|
429
449
|
}
|
|
430
450
|
|
|
431
451
|
if (file.size > MAX_FILE_SIZE) {
|
|
432
|
-
useUploadStore.getState().startUpload(levelId,
|
|
452
|
+
useUploadStore.getState().startUpload(levelId, type, file.name)
|
|
433
453
|
useUploadStore
|
|
434
454
|
.getState()
|
|
435
455
|
.setError(
|
|
@@ -439,21 +459,29 @@ function LevelReferences({
|
|
|
439
459
|
return
|
|
440
460
|
}
|
|
441
461
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
462
|
+
if (isImage) {
|
|
463
|
+
useUploadStore.getState().startUpload(levelId, 'guide', file.name)
|
|
464
|
+
useUploadStore.getState().setStatus(levelId, 'uploading')
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const guide = await createLocalGuideImage({ createNode, file, levelId })
|
|
468
|
+
setShowGuides(true)
|
|
469
|
+
setSelectedReferenceId(guide.id)
|
|
470
|
+
setSelection({ selectedIds: [], zoneId: null })
|
|
471
|
+
useUploadStore.getState().setResult(levelId, guide.url)
|
|
472
|
+
window.setTimeout(() => useUploadStore.getState().clearUpload(levelId), 600)
|
|
473
|
+
} catch {
|
|
474
|
+
useUploadStore.getState().setError(levelId, 'Could not add that guide image.')
|
|
475
|
+
}
|
|
476
|
+
return
|
|
477
|
+
}
|
|
446
478
|
|
|
447
|
-
if (!
|
|
479
|
+
if (!projectId) {
|
|
448
480
|
useUploadStore.getState().startUpload(levelId, 'scan', file.name)
|
|
449
|
-
useUploadStore
|
|
450
|
-
.getState()
|
|
451
|
-
.setError(levelId, 'Invalid file type. Please upload a .glb/.gltf scan or an image.')
|
|
481
|
+
useUploadStore.getState().setError(levelId, 'No active project. Please open a project first.')
|
|
452
482
|
return
|
|
453
483
|
}
|
|
454
484
|
|
|
455
|
-
const type = isScan ? 'scan' : 'guide'
|
|
456
|
-
|
|
457
485
|
clearUpload(levelId)
|
|
458
486
|
onUploadAsset?.(projectId, levelId, file, type)
|
|
459
487
|
}
|
|
@@ -554,10 +582,11 @@ function LevelReferences({
|
|
|
554
582
|
)}
|
|
555
583
|
</div>
|
|
556
584
|
)
|
|
557
|
-
}
|
|
585
|
+
})
|
|
558
586
|
|
|
559
|
-
function LevelItem({
|
|
587
|
+
const LevelItem = memo(function LevelItem({
|
|
560
588
|
level,
|
|
589
|
+
levels,
|
|
561
590
|
selectedLevelId,
|
|
562
591
|
setSelection,
|
|
563
592
|
updateNode,
|
|
@@ -567,6 +596,7 @@ function LevelItem({
|
|
|
567
596
|
onDeleteAsset,
|
|
568
597
|
}: {
|
|
569
598
|
level: LevelNode
|
|
599
|
+
levels: LevelNode[]
|
|
570
600
|
selectedLevelId: string | null
|
|
571
601
|
setSelection: (selection: any) => void
|
|
572
602
|
updateNode: (id: AnyNodeId, updates: Partial<AnyNode>) => void
|
|
@@ -576,11 +606,22 @@ function LevelItem({
|
|
|
576
606
|
onDeleteAsset?: (projectId: string, url: string) => void
|
|
577
607
|
}) {
|
|
578
608
|
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
|
|
609
|
+
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
579
610
|
const [isEditing, setIsEditing] = useState(false)
|
|
611
|
+
const createNodes = useScene((s) => s.createNodes)
|
|
612
|
+
const updateNodes = useScene((s) => s.updateNodes)
|
|
580
613
|
const itemRef = useRef<HTMLDivElement>(null)
|
|
581
614
|
const isSelected = selectedLevelId === level.id
|
|
582
615
|
const canDeleteLevel = level.level !== 0
|
|
583
616
|
const [isExpanded, setIsExpanded] = useState(isSelected)
|
|
617
|
+
const buildingId =
|
|
618
|
+
typeof level.parentId === 'string' && level.parentId.startsWith('building_')
|
|
619
|
+
? (level.parentId as BuildingNode['id'])
|
|
620
|
+
: undefined
|
|
621
|
+
|
|
622
|
+
const selectLevel = (levelId: LevelNode['id']) => {
|
|
623
|
+
setSelection(buildingId ? { buildingId, levelId } : { levelId })
|
|
624
|
+
}
|
|
584
625
|
|
|
585
626
|
useEffect(() => {
|
|
586
627
|
setIsExpanded(isSelected)
|
|
@@ -593,13 +634,34 @@ function LevelItem({
|
|
|
593
634
|
}, [isSelected])
|
|
594
635
|
|
|
595
636
|
const handleSelect = () => {
|
|
596
|
-
|
|
637
|
+
selectLevel(level.id)
|
|
597
638
|
}
|
|
598
639
|
|
|
599
640
|
const handleDoubleClick = () => {
|
|
600
641
|
focusTreeNode(level.id)
|
|
601
642
|
}
|
|
602
643
|
|
|
644
|
+
const handleDuplicateLevel = (preset: LevelDuplicatePreset = 'everything') => {
|
|
645
|
+
const { createOps, newLevelId, shiftedLevels } = buildLevelDuplicateCreateOps({
|
|
646
|
+
nodes: useScene.getState().nodes,
|
|
647
|
+
level,
|
|
648
|
+
levels,
|
|
649
|
+
preset,
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
if (shiftedLevels.length > 0) {
|
|
653
|
+
updateNodes(
|
|
654
|
+
shiftedLevels.map((shiftedLevel) => ({
|
|
655
|
+
id: shiftedLevel.id as AnyNodeId,
|
|
656
|
+
data: { level: shiftedLevel.level } as Partial<AnyNode>,
|
|
657
|
+
})),
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
createNodes(createOps)
|
|
661
|
+
selectLevel(newLevelId as LevelNode['id'])
|
|
662
|
+
setDuplicateDialogOpen(false)
|
|
663
|
+
}
|
|
664
|
+
|
|
603
665
|
return (
|
|
604
666
|
<div className="relative flex flex-col">
|
|
605
667
|
<div
|
|
@@ -641,7 +703,7 @@ function LevelItem({
|
|
|
641
703
|
if (isSelected) {
|
|
642
704
|
setIsExpanded(!isExpanded)
|
|
643
705
|
} else {
|
|
644
|
-
|
|
706
|
+
selectLevel(level.id)
|
|
645
707
|
}
|
|
646
708
|
}}
|
|
647
709
|
>
|
|
@@ -665,7 +727,7 @@ function LevelItem({
|
|
|
665
727
|
<InlineRenameInput
|
|
666
728
|
defaultName={`Level ${level.level}`}
|
|
667
729
|
isEditing={isEditing}
|
|
668
|
-
|
|
730
|
+
nodeId={level.id}
|
|
669
731
|
onStartEditing={() => setIsEditing(true)}
|
|
670
732
|
onStopEditing={() => setIsEditing(false)}
|
|
671
733
|
/>
|
|
@@ -750,7 +812,23 @@ function LevelItem({
|
|
|
750
812
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
|
751
813
|
</button>
|
|
752
814
|
</PopoverTrigger>
|
|
753
|
-
<PopoverContent align="start" className="w-
|
|
815
|
+
<PopoverContent align="start" className="w-48 p-1" side="right">
|
|
816
|
+
<button
|
|
817
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-left text-sm transition-colors hover:bg-accent"
|
|
818
|
+
onClick={() => handleDuplicateLevel()}
|
|
819
|
+
title="Duplicate level"
|
|
820
|
+
>
|
|
821
|
+
<Copy className="h-3.5 w-3.5" />
|
|
822
|
+
Duplicate
|
|
823
|
+
</button>
|
|
824
|
+
<button
|
|
825
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-left text-sm transition-colors hover:bg-accent"
|
|
826
|
+
onClick={() => setDuplicateDialogOpen(true)}
|
|
827
|
+
title="Duplicate level with options"
|
|
828
|
+
>
|
|
829
|
+
<Copy className="h-3.5 w-3.5" />
|
|
830
|
+
Duplicate with options...
|
|
831
|
+
</button>
|
|
754
832
|
<button
|
|
755
833
|
className="flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-sm transition-colors enabled:cursor-pointer enabled:hover:bg-accent enabled:hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
|
|
756
834
|
disabled={!canDeleteLevel}
|
|
@@ -782,11 +860,17 @@ function LevelItem({
|
|
|
782
860
|
</motion.div>
|
|
783
861
|
)}
|
|
784
862
|
</AnimatePresence>
|
|
863
|
+
<LevelDuplicateDialog
|
|
864
|
+
level={level}
|
|
865
|
+
onConfirm={handleDuplicateLevel}
|
|
866
|
+
onOpenChange={setDuplicateDialogOpen}
|
|
867
|
+
open={duplicateDialogOpen}
|
|
868
|
+
/>
|
|
785
869
|
</div>
|
|
786
870
|
)
|
|
787
|
-
}
|
|
871
|
+
})
|
|
788
872
|
|
|
789
|
-
function LevelsSection({
|
|
873
|
+
const LevelsSection = memo(function LevelsSection({
|
|
790
874
|
projectId,
|
|
791
875
|
onUploadAsset,
|
|
792
876
|
onDeleteAsset,
|
|
@@ -824,7 +908,7 @@ function LevelsSection({
|
|
|
824
908
|
parentId: building.id,
|
|
825
909
|
})
|
|
826
910
|
createNode(newLevel, building.id)
|
|
827
|
-
setSelection({ levelId: newLevel.id })
|
|
911
|
+
setSelection({ buildingId: building.id, levelId: newLevel.id })
|
|
828
912
|
}
|
|
829
913
|
|
|
830
914
|
return (
|
|
@@ -859,6 +943,7 @@ function LevelsSection({
|
|
|
859
943
|
isLast={index === levels.length - 1}
|
|
860
944
|
key={level.id}
|
|
861
945
|
level={level}
|
|
946
|
+
levels={levels}
|
|
862
947
|
onDeleteAsset={onDeleteAsset}
|
|
863
948
|
onUploadAsset={onUploadAsset}
|
|
864
949
|
projectId={projectId}
|
|
@@ -870,9 +955,9 @@ function LevelsSection({
|
|
|
870
955
|
</div>
|
|
871
956
|
</div>
|
|
872
957
|
)
|
|
873
|
-
}
|
|
958
|
+
})
|
|
874
959
|
|
|
875
|
-
function LayerToggle() {
|
|
960
|
+
const LayerToggle = memo(function LayerToggle() {
|
|
876
961
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
877
962
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
878
963
|
const phase = useEditor((state) => state.phase)
|
|
@@ -1000,9 +1085,9 @@ function LayerToggle() {
|
|
|
1000
1085
|
</button>
|
|
1001
1086
|
</div>
|
|
1002
1087
|
)
|
|
1003
|
-
}
|
|
1088
|
+
})
|
|
1004
1089
|
|
|
1005
|
-
function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
1090
|
+
const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
1006
1091
|
const [isEditing, setIsEditing] = useState(false)
|
|
1007
1092
|
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
|
|
1008
1093
|
const deleteNode = useScene((state) => state.deleteNode)
|
|
@@ -1087,7 +1172,7 @@ function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
|
1087
1172
|
<InlineRenameInput
|
|
1088
1173
|
defaultName={defaultName}
|
|
1089
1174
|
isEditing={isEditing}
|
|
1090
|
-
|
|
1175
|
+
nodeId={zone.id}
|
|
1091
1176
|
onStartEditing={() => setIsEditing(true)}
|
|
1092
1177
|
onStopEditing={() => setIsEditing(false)}
|
|
1093
1178
|
/>
|
|
@@ -1163,9 +1248,9 @@ function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
|
|
|
1163
1248
|
</div>
|
|
1164
1249
|
</div>
|
|
1165
1250
|
)
|
|
1166
|
-
}
|
|
1251
|
+
})
|
|
1167
1252
|
|
|
1168
|
-
function MultiSelectionBadge() {
|
|
1253
|
+
const MultiSelectionBadge = memo(function MultiSelectionBadge() {
|
|
1169
1254
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
1170
1255
|
const setSelection = useViewer((state) => state.setSelection)
|
|
1171
1256
|
|
|
@@ -1185,9 +1270,9 @@ function MultiSelectionBadge() {
|
|
|
1185
1270
|
</div>
|
|
1186
1271
|
</div>
|
|
1187
1272
|
)
|
|
1188
|
-
}
|
|
1273
|
+
})
|
|
1189
1274
|
|
|
1190
|
-
function ContentSection() {
|
|
1275
|
+
const ContentSection = memo(function ContentSection() {
|
|
1191
1276
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
1192
1277
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
1193
1278
|
const phase = useEditor((state) => state.phase)
|
|
@@ -1265,9 +1350,9 @@ function ContentSection() {
|
|
|
1265
1350
|
</div>
|
|
1266
1351
|
</TreeNodeDragProvider>
|
|
1267
1352
|
)
|
|
1268
|
-
}
|
|
1353
|
+
})
|
|
1269
1354
|
|
|
1270
|
-
function BuildingItem({
|
|
1355
|
+
const BuildingItem = memo(function BuildingItem({
|
|
1271
1356
|
building,
|
|
1272
1357
|
isBuildingActive,
|
|
1273
1358
|
buildingCameraOpen,
|
|
@@ -1308,19 +1393,16 @@ function BuildingItem({
|
|
|
1308
1393
|
}
|
|
1309
1394
|
|
|
1310
1395
|
return (
|
|
1311
|
-
<
|
|
1396
|
+
<div
|
|
1312
1397
|
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
1398
|
>
|
|
1316
|
-
<
|
|
1399
|
+
<div
|
|
1317
1400
|
className={cn(
|
|
1318
1401
|
'group/building flex h-10 shrink-0 cursor-pointer items-center border-border/50 border-b pr-2 transition-all duration-200',
|
|
1319
1402
|
isBuildingActive
|
|
1320
1403
|
? 'bg-accent/50 text-foreground'
|
|
1321
1404
|
: 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
|
|
1322
1405
|
)}
|
|
1323
|
-
layout="position"
|
|
1324
1406
|
onClick={handleSelect}
|
|
1325
1407
|
onDoubleClick={handleDoubleClick}
|
|
1326
1408
|
ref={itemRef}
|
|
@@ -1404,7 +1486,7 @@ function BuildingItem({
|
|
|
1404
1486
|
</div>
|
|
1405
1487
|
</PopoverContent>
|
|
1406
1488
|
</Popover>
|
|
1407
|
-
</
|
|
1489
|
+
</div>
|
|
1408
1490
|
|
|
1409
1491
|
{/* Tools and content for the active building */}
|
|
1410
1492
|
<AnimatePresence initial={false}>
|
|
@@ -1433,9 +1515,9 @@ function BuildingItem({
|
|
|
1433
1515
|
</motion.div>
|
|
1434
1516
|
)}
|
|
1435
1517
|
</AnimatePresence>
|
|
1436
|
-
</
|
|
1518
|
+
</div>
|
|
1437
1519
|
)
|
|
1438
|
-
}
|
|
1520
|
+
})
|
|
1439
1521
|
|
|
1440
1522
|
export interface SitePanelProps {
|
|
1441
1523
|
projectId?: string
|
|
@@ -1534,7 +1616,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1534
1616
|
No buildings yet
|
|
1535
1617
|
</motion.div>
|
|
1536
1618
|
) : (
|
|
1537
|
-
<
|
|
1619
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
1538
1620
|
{buildings.map((building) => {
|
|
1539
1621
|
const isBuildingActive =
|
|
1540
1622
|
(phase === 'structure' || phase === 'furnish') &&
|
|
@@ -1553,7 +1635,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
|
|
|
1553
1635
|
/>
|
|
1554
1636
|
)
|
|
1555
1637
|
})}
|
|
1556
|
-
</
|
|
1638
|
+
</div>
|
|
1557
1639
|
)}
|
|
1558
1640
|
</motion.div>
|
|
1559
1641
|
</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,19 +1,23 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { 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'
|
|
8
8
|
import { TreeNodeActions } from './tree-node-actions'
|
|
9
9
|
|
|
10
10
|
interface LevelTreeNodeProps {
|
|
11
|
-
nodeId:
|
|
11
|
+
nodeId: LevelNode['id']
|
|
12
12
|
depth: number
|
|
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,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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 { useCallback, 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'
|
|
@@ -13,7 +13,11 @@ interface SlabTreeNodeProps {
|
|
|
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
22
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
19
23
|
const polygon = useScene((s) => (s.nodes[nodeId] as SlabNode | undefined)?.polygon ?? [])
|
|
@@ -74,7 +78,7 @@ export function SlabTreeNode({ nodeId, depth, isLast }: SlabTreeNodeProps) {
|
|
|
74
78
|
onToggle={() => {}}
|
|
75
79
|
/>
|
|
76
80
|
)
|
|
77
|
-
}
|
|
81
|
+
})
|
|
78
82
|
|
|
79
83
|
/**
|
|
80
84
|
* Calculate the area of a polygon using the shoelace formula
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type SpawnNode, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import Image from 'next/image'
|
|
6
|
+
import { memo, useCallback, useState } from 'react'
|
|
7
|
+
import useEditor from './../../../../../store/use-editor'
|
|
8
|
+
import { InlineRenameInput } from './inline-rename-input'
|
|
9
|
+
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
10
|
+
import { TreeNodeActions } from './tree-node-actions'
|
|
11
|
+
|
|
12
|
+
interface SpawnTreeNodeProps {
|
|
13
|
+
nodeId: SpawnNode['id']
|
|
14
|
+
depth: number
|
|
15
|
+
isLast?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SpawnTreeNode = memo(function SpawnTreeNode({
|
|
19
|
+
nodeId,
|
|
20
|
+
depth,
|
|
21
|
+
isLast,
|
|
22
|
+
}: SpawnTreeNodeProps) {
|
|
23
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
24
|
+
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
25
|
+
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
26
|
+
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
27
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
28
|
+
const setHoveredId = useViewer((state) => state.setHoveredId)
|
|
29
|
+
|
|
30
|
+
const handleClick = useCallback(
|
|
31
|
+
(e: React.MouseEvent) => {
|
|
32
|
+
e.stopPropagation()
|
|
33
|
+
const handled = handleTreeSelection(
|
|
34
|
+
e,
|
|
35
|
+
nodeId,
|
|
36
|
+
useViewer.getState().selection.selectedIds,
|
|
37
|
+
setSelection,
|
|
38
|
+
)
|
|
39
|
+
if (!handled && useEditor.getState().phase === 'furnish') {
|
|
40
|
+
useEditor.getState().setPhase('structure')
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[nodeId, setSelection],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<TreeNodeWrapper
|
|
48
|
+
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
49
|
+
depth={depth}
|
|
50
|
+
expanded={false}
|
|
51
|
+
hasChildren={false}
|
|
52
|
+
icon={
|
|
53
|
+
<Image
|
|
54
|
+
alt=""
|
|
55
|
+
className="object-contain"
|
|
56
|
+
height={14}
|
|
57
|
+
src="/icons/site.png"
|
|
58
|
+
width={14}
|
|
59
|
+
/>
|
|
60
|
+
}
|
|
61
|
+
isHovered={isHovered}
|
|
62
|
+
isLast={isLast}
|
|
63
|
+
isSelected={isSelected}
|
|
64
|
+
isVisible={isVisible}
|
|
65
|
+
label={
|
|
66
|
+
<InlineRenameInput
|
|
67
|
+
defaultName="Spawn Point"
|
|
68
|
+
isEditing={isEditing}
|
|
69
|
+
nodeId={nodeId}
|
|
70
|
+
onStartEditing={() => setIsEditing(true)}
|
|
71
|
+
onStopEditing={() => setIsEditing(false)}
|
|
72
|
+
/>
|
|
73
|
+
}
|
|
74
|
+
nodeId={nodeId}
|
|
75
|
+
onClick={handleClick}
|
|
76
|
+
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
77
|
+
onMouseEnter={() => setHoveredId(nodeId)}
|
|
78
|
+
onMouseLeave={() => setHoveredId(null)}
|
|
79
|
+
onToggle={() => {}}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
})
|