@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
|
@@ -8,14 +8,7 @@ import {
|
|
|
8
8
|
useScene,
|
|
9
9
|
} from '@pascal-app/core'
|
|
10
10
|
import { InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
|
|
11
|
-
import {
|
|
12
|
-
type ReactNode,
|
|
13
|
-
type PointerEvent as ReactPointerEvent,
|
|
14
|
-
useCallback,
|
|
15
|
-
useEffect,
|
|
16
|
-
useRef,
|
|
17
|
-
useState,
|
|
18
|
-
} from 'react'
|
|
11
|
+
import { memo, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
|
19
12
|
import { ViewerOverlay } from '../../components/viewer-overlay'
|
|
20
13
|
import { ViewerZoneSystem } from '../../components/viewer-zone-system'
|
|
21
14
|
import { type PresetsAdapter, PresetsProvider } from '../../contexts/presets-context'
|
|
@@ -30,6 +23,7 @@ import {
|
|
|
30
23
|
import { initSFXBus } from '../../lib/sfx-bus'
|
|
31
24
|
import useEditor from '../../store/use-editor'
|
|
32
25
|
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
26
|
+
import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
|
|
33
27
|
import { RoofEditSystem } from '../systems/roof/roof-edit-system'
|
|
34
28
|
import { StairEditSystem } from '../systems/stair/stair-edit-system'
|
|
35
29
|
import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
|
|
@@ -54,15 +48,16 @@ import type { SidebarTab } from '../ui/sidebar/tab-bar'
|
|
|
54
48
|
import { CustomCameraControls } from './custom-camera-controls'
|
|
55
49
|
import { EditorLayoutV2 } from './editor-layout-v2'
|
|
56
50
|
import { ExportManager } from './export-manager'
|
|
51
|
+
import { FirstPersonControls, FirstPersonOverlay } from './first-person-controls'
|
|
57
52
|
import { FloatingActionMenu } from './floating-action-menu'
|
|
53
|
+
import { FloatingBuildingActionMenu } from './floating-building-action-menu'
|
|
58
54
|
import { FloorplanPanel } from './floorplan-panel'
|
|
59
55
|
import { Grid } from './grid'
|
|
60
56
|
import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
|
|
61
57
|
import { SelectionManager } from './selection-manager'
|
|
62
58
|
import { SiteEdgeLabels } from './site-edge-labels'
|
|
63
|
-
import { ThumbnailGenerator } from './thumbnail-generator'
|
|
59
|
+
import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
|
|
64
60
|
import { WallMeasurementLabel } from './wall-measurement-label'
|
|
65
|
-
import { FirstPersonControls, FirstPersonOverlay } from './first-person-controls'
|
|
66
61
|
|
|
67
62
|
const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-dismissed:v1'
|
|
68
63
|
const DELETE_CURSOR_BADGE_COLOR = '#ef4444'
|
|
@@ -122,7 +117,7 @@ export interface EditorProps {
|
|
|
122
117
|
isLoading?: boolean
|
|
123
118
|
|
|
124
119
|
// Thumbnail
|
|
125
|
-
onThumbnailCapture?: (blob: Blob) => void
|
|
120
|
+
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
126
121
|
|
|
127
122
|
// Version preview overlays (rendered by host app)
|
|
128
123
|
sidebarOverlay?: ReactNode
|
|
@@ -506,6 +501,211 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } })
|
|
|
506
501
|
)
|
|
507
502
|
}
|
|
508
503
|
|
|
504
|
+
// ── Viewer scene content: memoized so <Viewer> doesn't re-render on mode/viewMode changes ──
|
|
505
|
+
|
|
506
|
+
const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
507
|
+
isVersionPreviewMode,
|
|
508
|
+
isLoading,
|
|
509
|
+
isFirstPersonMode,
|
|
510
|
+
onThumbnailCapture,
|
|
511
|
+
}: {
|
|
512
|
+
isVersionPreviewMode: boolean
|
|
513
|
+
isLoading: boolean
|
|
514
|
+
isFirstPersonMode: boolean
|
|
515
|
+
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
516
|
+
}) {
|
|
517
|
+
return (
|
|
518
|
+
<>
|
|
519
|
+
{!isFirstPersonMode && <SelectionManager />}
|
|
520
|
+
{!isVersionPreviewMode && !isFirstPersonMode && <BoxSelectTool />}
|
|
521
|
+
{!isVersionPreviewMode && !isFirstPersonMode && <FloatingActionMenu />}
|
|
522
|
+
{!isVersionPreviewMode && !isFirstPersonMode && <FloatingBuildingActionMenu />}
|
|
523
|
+
{!isFirstPersonMode && <WallMeasurementLabel />}
|
|
524
|
+
<ExportManager />
|
|
525
|
+
{isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
|
|
526
|
+
<CeilingSystem />
|
|
527
|
+
<CeilingSelectionAffordanceSystem />
|
|
528
|
+
<RoofEditSystem />
|
|
529
|
+
<StairEditSystem />
|
|
530
|
+
{!isLoading && !isFirstPersonMode && (
|
|
531
|
+
<Grid cellColor="#aaa" fadeDistance={500} sectionColor="#ccc" />
|
|
532
|
+
)}
|
|
533
|
+
{!(isLoading || isVersionPreviewMode) && !isFirstPersonMode && <ToolManager />}
|
|
534
|
+
{isFirstPersonMode && <FirstPersonControls />}
|
|
535
|
+
<CustomCameraControls />
|
|
536
|
+
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
537
|
+
<PresetThumbnailGenerator />
|
|
538
|
+
{!isFirstPersonMode && <SiteEdgeLabels />}
|
|
539
|
+
{isFirstPersonMode && <InteractiveSystem />}
|
|
540
|
+
</>
|
|
541
|
+
)
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
// ── Delete cursor badge: isolated component so cursor moves don't re-render ViewerCanvas ──
|
|
545
|
+
// Subscribes to mode itself and manages cursor position state independently.
|
|
546
|
+
|
|
547
|
+
function DeleteCursorLayer({
|
|
548
|
+
containerRef,
|
|
549
|
+
isVersionPreviewMode,
|
|
550
|
+
}: {
|
|
551
|
+
containerRef: React.RefObject<HTMLDivElement | null>
|
|
552
|
+
isVersionPreviewMode: boolean
|
|
553
|
+
}) {
|
|
554
|
+
const mode = useEditor((s) => s.mode)
|
|
555
|
+
const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
|
|
556
|
+
const active = mode === 'delete' && !isVersionPreviewMode
|
|
557
|
+
|
|
558
|
+
useEffect(() => {
|
|
559
|
+
if (!active) {
|
|
560
|
+
setPosition(null)
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
const el = containerRef.current
|
|
564
|
+
if (!el) return
|
|
565
|
+
const onMove = (e: PointerEvent) => {
|
|
566
|
+
const rect = el.getBoundingClientRect()
|
|
567
|
+
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top })
|
|
568
|
+
}
|
|
569
|
+
const onLeave = () => setPosition(null)
|
|
570
|
+
el.addEventListener('pointermove', onMove)
|
|
571
|
+
el.addEventListener('pointerleave', onLeave)
|
|
572
|
+
return () => {
|
|
573
|
+
el.removeEventListener('pointermove', onMove)
|
|
574
|
+
el.removeEventListener('pointerleave', onLeave)
|
|
575
|
+
}
|
|
576
|
+
}, [active, containerRef])
|
|
577
|
+
|
|
578
|
+
if (!(active && position)) return null
|
|
579
|
+
return <DeleteCursorBadge position={position} />
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ── Viewer canvas: memoized, subscribes to viewMode/floorplanPaneRatio internally ──
|
|
583
|
+
// This prevents Editor from re-rendering when those values change.
|
|
584
|
+
|
|
585
|
+
const ViewerCanvas = memo(function ViewerCanvas({
|
|
586
|
+
isVersionPreviewMode,
|
|
587
|
+
isLoading,
|
|
588
|
+
hasLoadedInitialScene,
|
|
589
|
+
showLoader,
|
|
590
|
+
isFirstPersonMode,
|
|
591
|
+
onThumbnailCapture,
|
|
592
|
+
}: {
|
|
593
|
+
isVersionPreviewMode: boolean
|
|
594
|
+
isLoading: boolean
|
|
595
|
+
hasLoadedInitialScene: boolean
|
|
596
|
+
showLoader: boolean
|
|
597
|
+
isFirstPersonMode: boolean
|
|
598
|
+
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
599
|
+
}) {
|
|
600
|
+
const viewMode = useEditor((s) => s.viewMode)
|
|
601
|
+
const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
|
|
602
|
+
const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio)
|
|
603
|
+
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
604
|
+
|
|
605
|
+
const [isCameraControlsHintVisible, setIsCameraControlsHintVisible] = useState<boolean | null>(
|
|
606
|
+
null,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
const viewerAreaRef = useRef<HTMLDivElement>(null)
|
|
610
|
+
const viewer3dRef = useRef<HTMLDivElement>(null)
|
|
611
|
+
const isResizingFloorplan = useRef(false)
|
|
612
|
+
|
|
613
|
+
const handleFloorplanDividerDown = useCallback((e: React.PointerEvent) => {
|
|
614
|
+
e.preventDefault()
|
|
615
|
+
isResizingFloorplan.current = true
|
|
616
|
+
document.body.style.cursor = 'col-resize'
|
|
617
|
+
document.body.style.userSelect = 'none'
|
|
618
|
+
}, [])
|
|
619
|
+
|
|
620
|
+
useEffect(() => {
|
|
621
|
+
const handlePointerMove = (e: PointerEvent) => {
|
|
622
|
+
if (!isResizingFloorplan.current) return
|
|
623
|
+
if (!viewerAreaRef.current) return
|
|
624
|
+
const rect = viewerAreaRef.current.getBoundingClientRect()
|
|
625
|
+
const newRatio = (e.clientX - rect.left) / rect.width
|
|
626
|
+
setFloorplanPaneRatio(Math.max(0.15, Math.min(0.85, newRatio)))
|
|
627
|
+
}
|
|
628
|
+
const handlePointerUp = () => {
|
|
629
|
+
isResizingFloorplan.current = false
|
|
630
|
+
document.body.style.cursor = ''
|
|
631
|
+
document.body.style.userSelect = ''
|
|
632
|
+
}
|
|
633
|
+
window.addEventListener('pointermove', handlePointerMove)
|
|
634
|
+
window.addEventListener('pointerup', handlePointerUp)
|
|
635
|
+
return () => {
|
|
636
|
+
window.removeEventListener('pointermove', handlePointerMove)
|
|
637
|
+
window.removeEventListener('pointerup', handlePointerUp)
|
|
638
|
+
}
|
|
639
|
+
}, [setFloorplanPaneRatio])
|
|
640
|
+
|
|
641
|
+
useEffect(() => {
|
|
642
|
+
setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
|
|
643
|
+
}, [])
|
|
644
|
+
|
|
645
|
+
const dismissCameraControlsHint = useCallback(() => {
|
|
646
|
+
setIsCameraControlsHintVisible(false)
|
|
647
|
+
writeCameraControlsHintDismissed(true)
|
|
648
|
+
}, [])
|
|
649
|
+
|
|
650
|
+
const show2d = viewMode === '2d' || viewMode === 'split'
|
|
651
|
+
const show3d = viewMode === '3d' || viewMode === 'split'
|
|
652
|
+
|
|
653
|
+
return (
|
|
654
|
+
<ErrorBoundary fallback={<EditorSceneCrashFallback />}>
|
|
655
|
+
<div className="flex h-full" ref={viewerAreaRef}>
|
|
656
|
+
{/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
|
|
657
|
+
<div
|
|
658
|
+
className="relative h-full flex-shrink-0"
|
|
659
|
+
style={{
|
|
660
|
+
width: viewMode === '2d' ? '100%' : `${floorplanPaneRatio * 100}%`,
|
|
661
|
+
display: show2d ? undefined : 'none',
|
|
662
|
+
}}
|
|
663
|
+
>
|
|
664
|
+
<div className="h-full w-full overflow-hidden">
|
|
665
|
+
<FloorplanPanel />
|
|
666
|
+
</div>
|
|
667
|
+
{viewMode === 'split' && (
|
|
668
|
+
<div
|
|
669
|
+
className="absolute inset-y-0 -right-3 z-10 flex w-6 cursor-col-resize items-center justify-center"
|
|
670
|
+
onPointerDown={handleFloorplanDividerDown}
|
|
671
|
+
>
|
|
672
|
+
<div className="h-8 w-1 rounded-full bg-neutral-400" />
|
|
673
|
+
</div>
|
|
674
|
+
)}
|
|
675
|
+
</div>
|
|
676
|
+
|
|
677
|
+
{/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
|
|
678
|
+
<div
|
|
679
|
+
className="relative min-w-0 flex-1 overflow-hidden"
|
|
680
|
+
ref={viewer3dRef}
|
|
681
|
+
style={{ display: show3d ? undefined : 'none' }}
|
|
682
|
+
>
|
|
683
|
+
<DeleteCursorLayer
|
|
684
|
+
containerRef={viewer3dRef}
|
|
685
|
+
isVersionPreviewMode={isVersionPreviewMode}
|
|
686
|
+
/>
|
|
687
|
+
{!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
|
|
688
|
+
<ViewerCanvasControlsHint
|
|
689
|
+
isPreviewMode={isPreviewMode}
|
|
690
|
+
onDismiss={dismissCameraControlsHint}
|
|
691
|
+
/>
|
|
692
|
+
) : null}
|
|
693
|
+
<SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
|
|
694
|
+
<Viewer selectionManager={isFirstPersonMode ? 'default' : 'custom'}>
|
|
695
|
+
<ViewerSceneContent
|
|
696
|
+
isFirstPersonMode={isFirstPersonMode}
|
|
697
|
+
isLoading={isLoading}
|
|
698
|
+
isVersionPreviewMode={isVersionPreviewMode}
|
|
699
|
+
onThumbnailCapture={onThumbnailCapture}
|
|
700
|
+
/>
|
|
701
|
+
</Viewer>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
{!(isLoading || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
|
|
705
|
+
</ErrorBoundary>
|
|
706
|
+
)
|
|
707
|
+
})
|
|
708
|
+
|
|
509
709
|
export default function Editor({
|
|
510
710
|
layoutVersion = 'v1',
|
|
511
711
|
appMenuButton,
|
|
@@ -542,51 +742,11 @@ export default function Editor({
|
|
|
542
742
|
|
|
543
743
|
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
|
544
744
|
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
|
|
545
|
-
const [isCameraControlsHintVisible, setIsCameraControlsHintVisible] = useState<boolean | null>(
|
|
546
|
-
null,
|
|
547
|
-
)
|
|
548
745
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
549
|
-
const mode = useEditor((s) => s.mode)
|
|
550
746
|
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
551
|
-
const isFloorplanOpen = useEditor((s) => s.isFloorplanOpen)
|
|
552
|
-
const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
|
|
553
|
-
const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio)
|
|
554
|
-
const [viewerCursorPosition, setViewerCursorPosition] = useState<{ x: number; y: number } | null>(
|
|
555
|
-
null,
|
|
556
|
-
)
|
|
557
747
|
|
|
558
748
|
const sidebarWidth = useSidebarStore((s) => s.width)
|
|
559
749
|
const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
|
|
560
|
-
const viewerAreaRef = useRef<HTMLDivElement>(null)
|
|
561
|
-
const isResizingFloorplan = useRef(false)
|
|
562
|
-
|
|
563
|
-
const handleFloorplanDividerDown = useCallback((e: React.PointerEvent) => {
|
|
564
|
-
e.preventDefault()
|
|
565
|
-
isResizingFloorplan.current = true
|
|
566
|
-
document.body.style.cursor = 'col-resize'
|
|
567
|
-
document.body.style.userSelect = 'none'
|
|
568
|
-
}, [])
|
|
569
|
-
|
|
570
|
-
useEffect(() => {
|
|
571
|
-
const handlePointerMove = (e: PointerEvent) => {
|
|
572
|
-
if (!isResizingFloorplan.current) return
|
|
573
|
-
if (!viewerAreaRef.current) return
|
|
574
|
-
const rect = viewerAreaRef.current.getBoundingClientRect()
|
|
575
|
-
const newRatio = (e.clientX - rect.left) / rect.width
|
|
576
|
-
setFloorplanPaneRatio(Math.max(0.15, Math.min(0.85, newRatio)))
|
|
577
|
-
}
|
|
578
|
-
const handlePointerUp = () => {
|
|
579
|
-
isResizingFloorplan.current = false
|
|
580
|
-
document.body.style.cursor = ''
|
|
581
|
-
document.body.style.userSelect = ''
|
|
582
|
-
}
|
|
583
|
-
window.addEventListener('pointermove', handlePointerMove)
|
|
584
|
-
window.addEventListener('pointerup', handlePointerUp)
|
|
585
|
-
return () => {
|
|
586
|
-
window.removeEventListener('pointermove', handlePointerMove)
|
|
587
|
-
window.removeEventListener('pointerup', handlePointerUp)
|
|
588
|
-
}
|
|
589
|
-
}, [])
|
|
590
750
|
|
|
591
751
|
useEffect(() => {
|
|
592
752
|
const teardown = initializeEditorRuntime()
|
|
@@ -660,38 +820,7 @@ export default function Editor({
|
|
|
660
820
|
}
|
|
661
821
|
}, [])
|
|
662
822
|
|
|
663
|
-
useEffect(() => {
|
|
664
|
-
setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
|
|
665
|
-
}, [])
|
|
666
|
-
|
|
667
823
|
const showLoader = isLoading || isSceneLoading
|
|
668
|
-
const dismissCameraControlsHint = useCallback(() => {
|
|
669
|
-
setIsCameraControlsHintVisible(false)
|
|
670
|
-
writeCameraControlsHintDismissed(true)
|
|
671
|
-
}, [])
|
|
672
|
-
|
|
673
|
-
// ── Shared viewer scene content ──
|
|
674
|
-
const viewerSceneContent = (
|
|
675
|
-
<>
|
|
676
|
-
{!isFirstPersonMode && <SelectionManager />}
|
|
677
|
-
{!isVersionPreviewMode && !isFirstPersonMode && <BoxSelectTool />}
|
|
678
|
-
{!isVersionPreviewMode && !isFirstPersonMode && <FloatingActionMenu />}
|
|
679
|
-
{!isFirstPersonMode && <WallMeasurementLabel />}
|
|
680
|
-
<ExportManager />
|
|
681
|
-
{isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
|
|
682
|
-
<CeilingSystem />
|
|
683
|
-
<RoofEditSystem />
|
|
684
|
-
<StairEditSystem />
|
|
685
|
-
{!isLoading && !isFirstPersonMode && <Grid cellColor="#aaa" fadeDistance={500} sectionColor="#ccc" />}
|
|
686
|
-
{!(isLoading || isVersionPreviewMode) && !isFirstPersonMode && <ToolManager />}
|
|
687
|
-
{isFirstPersonMode && <FirstPersonControls />}
|
|
688
|
-
<CustomCameraControls />
|
|
689
|
-
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
690
|
-
<PresetThumbnailGenerator />
|
|
691
|
-
{!isFirstPersonMode && <SiteEdgeLabels />}
|
|
692
|
-
{isFirstPersonMode && <InteractiveSystem />}
|
|
693
|
-
</>
|
|
694
|
-
)
|
|
695
824
|
|
|
696
825
|
const previewViewerContent = (
|
|
697
826
|
<Viewer selectionManager="default">
|
|
@@ -707,86 +836,15 @@ export default function Editor({
|
|
|
707
836
|
</Viewer>
|
|
708
837
|
)
|
|
709
838
|
|
|
710
|
-
// ── Shared viewer canvas (handles split/2d/3d) ──
|
|
711
|
-
const viewMode = useEditor((s) => s.viewMode)
|
|
712
|
-
|
|
713
|
-
const show2d = viewMode === '2d' || viewMode === 'split'
|
|
714
|
-
const show3d = viewMode === '3d' || viewMode === 'split'
|
|
715
|
-
const showDeleteCursorBadge = mode === 'delete' && !isVersionPreviewMode
|
|
716
|
-
|
|
717
|
-
useEffect(() => {
|
|
718
|
-
if (!(showDeleteCursorBadge && show3d)) {
|
|
719
|
-
setViewerCursorPosition(null)
|
|
720
|
-
}
|
|
721
|
-
}, [show3d, showDeleteCursorBadge])
|
|
722
|
-
|
|
723
|
-
const handleViewerPointerMove = useCallback(
|
|
724
|
-
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
725
|
-
if (!showDeleteCursorBadge) {
|
|
726
|
-
setViewerCursorPosition(null)
|
|
727
|
-
return
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const rect = event.currentTarget.getBoundingClientRect()
|
|
731
|
-
setViewerCursorPosition({
|
|
732
|
-
x: event.clientX - rect.left,
|
|
733
|
-
y: event.clientY - rect.top,
|
|
734
|
-
})
|
|
735
|
-
},
|
|
736
|
-
[showDeleteCursorBadge],
|
|
737
|
-
)
|
|
738
|
-
|
|
739
|
-
const handleViewerPointerLeave = useCallback(() => {
|
|
740
|
-
setViewerCursorPosition(null)
|
|
741
|
-
}, [])
|
|
742
|
-
|
|
743
839
|
const viewerCanvas = (
|
|
744
|
-
<
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}}
|
|
753
|
-
>
|
|
754
|
-
<div className="h-full w-full overflow-hidden">
|
|
755
|
-
<FloorplanPanel />
|
|
756
|
-
</div>
|
|
757
|
-
{viewMode === 'split' && (
|
|
758
|
-
<div
|
|
759
|
-
className="absolute inset-y-0 -right-3 z-10 flex w-6 cursor-col-resize items-center justify-center"
|
|
760
|
-
onPointerDown={handleFloorplanDividerDown}
|
|
761
|
-
>
|
|
762
|
-
<div className="h-8 w-1 rounded-full bg-neutral-400" />
|
|
763
|
-
</div>
|
|
764
|
-
)}
|
|
765
|
-
</div>
|
|
766
|
-
|
|
767
|
-
{/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
|
|
768
|
-
<div
|
|
769
|
-
className="relative min-w-0 flex-1 overflow-hidden"
|
|
770
|
-
onPointerEnter={handleViewerPointerMove}
|
|
771
|
-
onPointerLeave={handleViewerPointerLeave}
|
|
772
|
-
onPointerMove={handleViewerPointerMove}
|
|
773
|
-
style={{ display: show3d ? undefined : 'none' }}
|
|
774
|
-
>
|
|
775
|
-
{showDeleteCursorBadge && viewerCursorPosition ? (
|
|
776
|
-
<DeleteCursorBadge position={viewerCursorPosition} />
|
|
777
|
-
) : null}
|
|
778
|
-
{!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
|
|
779
|
-
<ViewerCanvasControlsHint
|
|
780
|
-
isPreviewMode={isPreviewMode}
|
|
781
|
-
onDismiss={dismissCameraControlsHint}
|
|
782
|
-
/>
|
|
783
|
-
) : null}
|
|
784
|
-
<SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
|
|
785
|
-
<Viewer selectionManager={isFirstPersonMode ? 'default' : 'custom'}>{viewerSceneContent}</Viewer>
|
|
786
|
-
</div>
|
|
787
|
-
</div>
|
|
788
|
-
{!(isLoading || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
|
|
789
|
-
</ErrorBoundary>
|
|
840
|
+
<ViewerCanvas
|
|
841
|
+
hasLoadedInitialScene={hasLoadedInitialScene}
|
|
842
|
+
isFirstPersonMode={isFirstPersonMode}
|
|
843
|
+
isLoading={isLoading}
|
|
844
|
+
isVersionPreviewMode={isVersionPreviewMode}
|
|
845
|
+
onThumbnailCapture={onThumbnailCapture}
|
|
846
|
+
showLoader={showLoader}
|
|
847
|
+
/>
|
|
790
848
|
)
|
|
791
849
|
|
|
792
850
|
// ── V2 layout ──
|
|
@@ -856,9 +914,7 @@ export default function Editor({
|
|
|
856
914
|
{/* First-person overlay — rendered on top of normal layout */}
|
|
857
915
|
{isFirstPersonMode && (
|
|
858
916
|
<div className="fixed inset-0 z-50 pointer-events-none">
|
|
859
|
-
<FirstPersonOverlay
|
|
860
|
-
onExit={() => useEditor.getState().setFirstPersonMode(false)}
|
|
861
|
-
/>
|
|
917
|
+
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
862
918
|
</div>
|
|
863
919
|
)}
|
|
864
920
|
<EditorCommands />
|
|
@@ -904,9 +960,7 @@ export default function Editor({
|
|
|
904
960
|
</SidebarSlot>
|
|
905
961
|
|
|
906
962
|
{/* Viewer area */}
|
|
907
|
-
<div className="relative flex-1 overflow-hidden rounded-xl"
|
|
908
|
-
{viewerCanvas}
|
|
909
|
-
</div>
|
|
963
|
+
<div className="relative flex-1 overflow-hidden rounded-xl">{viewerCanvas}</div>
|
|
910
964
|
|
|
911
965
|
{/* Fixed UI overlays scoped to the viewer area */}
|
|
912
966
|
<ViewerOverlays left={overlayLeft}>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Icon } from '@iconify/react'
|
|
4
|
+
import { Copy, Move, Spline, Trash2 } from 'lucide-react'
|
|
4
5
|
import type { MouseEventHandler, PointerEventHandler } from 'react'
|
|
5
6
|
|
|
6
7
|
type NodeActionMenuProps = {
|
|
7
|
-
|
|
8
|
+
onAddHole?: MouseEventHandler<HTMLButtonElement>
|
|
9
|
+
onDelete?: MouseEventHandler<HTMLButtonElement>
|
|
8
10
|
onDuplicate?: MouseEventHandler<HTMLButtonElement>
|
|
9
11
|
onMove?: MouseEventHandler<HTMLButtonElement>
|
|
12
|
+
onCurve?: MouseEventHandler<HTMLButtonElement>
|
|
10
13
|
onPointerDown?: PointerEventHandler<HTMLDivElement>
|
|
11
14
|
onPointerUp?: PointerEventHandler<HTMLDivElement>
|
|
12
15
|
onPointerEnter?: PointerEventHandler<HTMLDivElement>
|
|
@@ -14,9 +17,11 @@ type NodeActionMenuProps = {
|
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export function NodeActionMenu({
|
|
20
|
+
onAddHole,
|
|
17
21
|
onDelete,
|
|
18
22
|
onDuplicate,
|
|
19
23
|
onMove,
|
|
24
|
+
onCurve,
|
|
20
25
|
onPointerDown,
|
|
21
26
|
onPointerUp,
|
|
22
27
|
onPointerEnter,
|
|
@@ -41,6 +46,17 @@ export function NodeActionMenu({
|
|
|
41
46
|
<Move className="h-4 w-4" />
|
|
42
47
|
</button>
|
|
43
48
|
)}
|
|
49
|
+
{onCurve && (
|
|
50
|
+
<button
|
|
51
|
+
aria-label="Curve"
|
|
52
|
+
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
53
|
+
onClick={onCurve}
|
|
54
|
+
title="Curve"
|
|
55
|
+
type="button"
|
|
56
|
+
>
|
|
57
|
+
<Spline className="h-4 w-4" />
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
44
60
|
{onDuplicate && (
|
|
45
61
|
<button
|
|
46
62
|
aria-label="Duplicate"
|
|
@@ -52,15 +68,28 @@ export function NodeActionMenu({
|
|
|
52
68
|
<Copy className="h-4 w-4" />
|
|
53
69
|
</button>
|
|
54
70
|
)}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
{onAddHole && (
|
|
72
|
+
<button
|
|
73
|
+
aria-label="Cut Out"
|
|
74
|
+
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
75
|
+
onClick={onAddHole}
|
|
76
|
+
title="Cut Out"
|
|
77
|
+
type="button"
|
|
78
|
+
>
|
|
79
|
+
<Icon height={16} icon="carbon:cut-out" width={16} />
|
|
80
|
+
</button>
|
|
81
|
+
)}
|
|
82
|
+
{onDelete && (
|
|
83
|
+
<button
|
|
84
|
+
aria-label="Delete"
|
|
85
|
+
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
|
86
|
+
onClick={onDelete}
|
|
87
|
+
title="Delete"
|
|
88
|
+
type="button"
|
|
89
|
+
>
|
|
90
|
+
<Trash2 className="h-4 w-4" />
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
64
93
|
</div>
|
|
65
94
|
)
|
|
66
95
|
}
|