@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
|
@@ -7,8 +7,16 @@ import {
|
|
|
7
7
|
spatialGridManager,
|
|
8
8
|
useScene,
|
|
9
9
|
} from '@pascal-app/core'
|
|
10
|
-
import { InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
|
|
11
|
-
import {
|
|
10
|
+
import { type HoverStyles, InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
|
|
11
|
+
import {
|
|
12
|
+
memo,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
useCallback,
|
|
15
|
+
useEffect,
|
|
16
|
+
useLayoutEffect,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
} from 'react'
|
|
12
20
|
import { ViewerOverlay } from '../../components/viewer-overlay'
|
|
13
21
|
import { ViewerZoneSystem } from '../../components/viewer-zone-system'
|
|
14
22
|
import { type PresetsAdapter, PresetsProvider } from '../../contexts/presets-context'
|
|
@@ -22,6 +30,7 @@ import {
|
|
|
22
30
|
} from '../../lib/scene'
|
|
23
31
|
import { initSFXBus } from '../../lib/sfx-bus'
|
|
24
32
|
import useEditor from '../../store/use-editor'
|
|
33
|
+
import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
|
|
25
34
|
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
26
35
|
import { RoofEditSystem } from '../systems/roof/roof-edit-system'
|
|
27
36
|
import { StairEditSystem } from '../systems/stair/stair-edit-system'
|
|
@@ -62,6 +71,21 @@ const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-
|
|
|
62
71
|
const DELETE_CURSOR_BADGE_COLOR = '#ef4444'
|
|
63
72
|
const DELETE_CURSOR_BADGE_OFFSET_X = 14
|
|
64
73
|
const DELETE_CURSOR_BADGE_OFFSET_Y = 14
|
|
74
|
+
const PAINT_CURSOR_BADGE_COLOR = '#f59e0b'
|
|
75
|
+
const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
|
76
|
+
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
|
77
|
+
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
|
78
|
+
const EDITOR_HOVER_STYLES: HoverStyles = {
|
|
79
|
+
default: { visibleColor: 0x00_aaff, hiddenColor: 0xf3_ff47, strength: 5, pulse: true },
|
|
80
|
+
delete: { visibleColor: 0xef_4444, hiddenColor: 0x99_1b1b, strength: 6, pulse: false },
|
|
81
|
+
'paint-ready': { visibleColor: 0xf5_9e0b, hiddenColor: 0xfd_e068, strength: 5, pulse: true },
|
|
82
|
+
'paint-disabled': {
|
|
83
|
+
visibleColor: 0x94_a3b8,
|
|
84
|
+
hiddenColor: 0x47_5569,
|
|
85
|
+
strength: 4,
|
|
86
|
+
pulse: false,
|
|
87
|
+
},
|
|
88
|
+
}
|
|
65
89
|
|
|
66
90
|
/**
|
|
67
91
|
* Wire up module-level singletons (spatial grid, space detection, SFX) for
|
|
@@ -500,6 +524,50 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } })
|
|
|
500
524
|
)
|
|
501
525
|
}
|
|
502
526
|
|
|
527
|
+
function PaintCursorBadge({
|
|
528
|
+
position,
|
|
529
|
+
label,
|
|
530
|
+
disabled,
|
|
531
|
+
icon,
|
|
532
|
+
}: {
|
|
533
|
+
position: { x: number; y: number }
|
|
534
|
+
label: string
|
|
535
|
+
disabled: boolean
|
|
536
|
+
icon: string
|
|
537
|
+
}) {
|
|
538
|
+
const accentColor = disabled ? PAINT_CURSOR_BADGE_DISABLED_COLOR : PAINT_CURSOR_BADGE_COLOR
|
|
539
|
+
|
|
540
|
+
return (
|
|
541
|
+
<div
|
|
542
|
+
aria-hidden="true"
|
|
543
|
+
className="pointer-events-none absolute z-40"
|
|
544
|
+
style={{
|
|
545
|
+
left: position.x + PAINT_CURSOR_BADGE_OFFSET_X,
|
|
546
|
+
top: position.y + PAINT_CURSOR_BADGE_OFFSET_Y,
|
|
547
|
+
}}
|
|
548
|
+
>
|
|
549
|
+
<div
|
|
550
|
+
className="flex items-center gap-2 rounded-xl border border-white/5 bg-zinc-900/95 px-3 py-2 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
|
|
551
|
+
style={{
|
|
552
|
+
boxShadow: `0 8px 16px -4px rgba(0,0,0,0.3), 0 4px 8px -4px rgba(0,0,0,0.2), 0 0 18px ${accentColor}22`,
|
|
553
|
+
}}
|
|
554
|
+
>
|
|
555
|
+
<Icon
|
|
556
|
+
aria-hidden="true"
|
|
557
|
+
className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
558
|
+
color={accentColor}
|
|
559
|
+
height={16}
|
|
560
|
+
icon={icon}
|
|
561
|
+
width={16}
|
|
562
|
+
/>
|
|
563
|
+
<span className="font-medium text-[11px]" style={{ color: accentColor }}>
|
|
564
|
+
{label}
|
|
565
|
+
</span>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
503
571
|
// ── Viewer scene content: memoized so <Viewer> doesn't re-render on mode/viewMode changes ──
|
|
504
572
|
|
|
505
573
|
const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
@@ -516,19 +584,20 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
516
584
|
return (
|
|
517
585
|
<>
|
|
518
586
|
{!isFirstPersonMode && <SelectionManager />}
|
|
519
|
-
{!isVersionPreviewMode
|
|
520
|
-
{!isVersionPreviewMode
|
|
521
|
-
{!isVersionPreviewMode
|
|
587
|
+
{!(isVersionPreviewMode || isFirstPersonMode) && <BoxSelectTool />}
|
|
588
|
+
{!(isVersionPreviewMode || isFirstPersonMode) && <FloatingActionMenu />}
|
|
589
|
+
{!(isVersionPreviewMode || isFirstPersonMode) && <FloatingBuildingActionMenu />}
|
|
522
590
|
{!isFirstPersonMode && <WallMeasurementLabel />}
|
|
523
591
|
<ExportManager />
|
|
524
592
|
{isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
|
|
525
593
|
<CeilingSystem />
|
|
594
|
+
<CeilingSelectionAffordanceSystem />
|
|
526
595
|
<RoofEditSystem />
|
|
527
596
|
<StairEditSystem />
|
|
528
|
-
{!isLoading
|
|
597
|
+
{!(isLoading || isFirstPersonMode) && (
|
|
529
598
|
<Grid cellColor="#aaa" fadeDistance={500} sectionColor="#ccc" />
|
|
530
599
|
)}
|
|
531
|
-
{!(isLoading || isVersionPreviewMode
|
|
600
|
+
{!(isLoading || isVersionPreviewMode || isFirstPersonMode) && <ToolManager />}
|
|
532
601
|
{isFirstPersonMode && <FirstPersonControls />}
|
|
533
602
|
<CustomCameraControls />
|
|
534
603
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
@@ -550,31 +619,165 @@ function DeleteCursorLayer({
|
|
|
550
619
|
isVersionPreviewMode: boolean
|
|
551
620
|
}) {
|
|
552
621
|
const mode = useEditor((s) => s.mode)
|
|
553
|
-
const
|
|
622
|
+
const badgeRef = useRef<HTMLDivElement>(null)
|
|
554
623
|
const active = mode === 'delete' && !isVersionPreviewMode
|
|
555
624
|
|
|
556
625
|
useEffect(() => {
|
|
557
626
|
if (!active) {
|
|
558
|
-
|
|
627
|
+
if (badgeRef.current) {
|
|
628
|
+
badgeRef.current.style.display = 'none'
|
|
629
|
+
}
|
|
559
630
|
return
|
|
560
631
|
}
|
|
561
632
|
const el = containerRef.current
|
|
562
633
|
if (!el) return
|
|
634
|
+
let frame = 0
|
|
635
|
+
let nextX = 0
|
|
636
|
+
let nextY = 0
|
|
637
|
+
const badge = badgeRef.current
|
|
638
|
+
|
|
639
|
+
const flushPosition = () => {
|
|
640
|
+
frame = 0
|
|
641
|
+
if (!badge) return
|
|
642
|
+
badge.style.display = 'block'
|
|
643
|
+
badge.style.transform = `translate(${nextX + DELETE_CURSOR_BADGE_OFFSET_X}px, ${nextY + DELETE_CURSOR_BADGE_OFFSET_Y}px)`
|
|
644
|
+
}
|
|
645
|
+
|
|
563
646
|
const onMove = (e: PointerEvent) => {
|
|
564
647
|
const rect = el.getBoundingClientRect()
|
|
565
|
-
|
|
648
|
+
nextX = e.clientX - rect.left
|
|
649
|
+
nextY = e.clientY - rect.top
|
|
650
|
+
|
|
651
|
+
if (frame === 0) {
|
|
652
|
+
frame = window.requestAnimationFrame(flushPosition)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const onLeave = () => {
|
|
656
|
+
if (frame !== 0) {
|
|
657
|
+
window.cancelAnimationFrame(frame)
|
|
658
|
+
frame = 0
|
|
659
|
+
}
|
|
660
|
+
if (badge) {
|
|
661
|
+
badge.style.display = 'none'
|
|
662
|
+
}
|
|
566
663
|
}
|
|
567
|
-
const onLeave = () => setPosition(null)
|
|
568
664
|
el.addEventListener('pointermove', onMove)
|
|
569
665
|
el.addEventListener('pointerleave', onLeave)
|
|
570
666
|
return () => {
|
|
667
|
+
if (frame !== 0) {
|
|
668
|
+
window.cancelAnimationFrame(frame)
|
|
669
|
+
}
|
|
571
670
|
el.removeEventListener('pointermove', onMove)
|
|
572
671
|
el.removeEventListener('pointerleave', onLeave)
|
|
573
672
|
}
|
|
574
673
|
}, [active, containerRef])
|
|
575
674
|
|
|
576
|
-
if (!
|
|
577
|
-
|
|
675
|
+
if (!active) return null
|
|
676
|
+
|
|
677
|
+
return (
|
|
678
|
+
<div
|
|
679
|
+
className="pointer-events-none"
|
|
680
|
+
ref={badgeRef}
|
|
681
|
+
style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
|
|
682
|
+
>
|
|
683
|
+
<DeleteCursorBadge position={{ x: 0, y: 0 }} />
|
|
684
|
+
</div>
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function PaintCursorLayer({
|
|
689
|
+
containerRef,
|
|
690
|
+
isVersionPreviewMode,
|
|
691
|
+
}: {
|
|
692
|
+
containerRef: React.RefObject<HTMLDivElement | null>
|
|
693
|
+
isVersionPreviewMode: boolean
|
|
694
|
+
}) {
|
|
695
|
+
const mode = useEditor((s) => s.mode)
|
|
696
|
+
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
|
697
|
+
const activePaintTarget = useEditor((s) => s.activePaintTarget)
|
|
698
|
+
const badgeRef = useRef<HTMLDivElement>(null)
|
|
699
|
+
const active = mode === 'material-paint' && !isVersionPreviewMode
|
|
700
|
+
|
|
701
|
+
useEffect(() => {
|
|
702
|
+
if (!active) {
|
|
703
|
+
if (badgeRef.current) {
|
|
704
|
+
badgeRef.current.style.display = 'none'
|
|
705
|
+
}
|
|
706
|
+
return
|
|
707
|
+
}
|
|
708
|
+
const el = containerRef.current
|
|
709
|
+
if (!el) return
|
|
710
|
+
let frame = 0
|
|
711
|
+
let nextX = 0
|
|
712
|
+
let nextY = 0
|
|
713
|
+
const badge = badgeRef.current
|
|
714
|
+
|
|
715
|
+
const flushPosition = () => {
|
|
716
|
+
frame = 0
|
|
717
|
+
if (!badge) return
|
|
718
|
+
badge.style.display = 'block'
|
|
719
|
+
badge.style.transform = `translate(${nextX + PAINT_CURSOR_BADGE_OFFSET_X}px, ${nextY + PAINT_CURSOR_BADGE_OFFSET_Y}px)`
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const onMove = (e: PointerEvent) => {
|
|
723
|
+
const rect = el.getBoundingClientRect()
|
|
724
|
+
nextX = e.clientX - rect.left
|
|
725
|
+
nextY = e.clientY - rect.top
|
|
726
|
+
|
|
727
|
+
if (frame === 0) {
|
|
728
|
+
frame = window.requestAnimationFrame(flushPosition)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const onLeave = () => {
|
|
732
|
+
if (frame !== 0) {
|
|
733
|
+
window.cancelAnimationFrame(frame)
|
|
734
|
+
frame = 0
|
|
735
|
+
}
|
|
736
|
+
if (badge) {
|
|
737
|
+
badge.style.display = 'none'
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
el.addEventListener('pointermove', onMove)
|
|
741
|
+
el.addEventListener('pointerleave', onLeave)
|
|
742
|
+
return () => {
|
|
743
|
+
if (frame !== 0) {
|
|
744
|
+
window.cancelAnimationFrame(frame)
|
|
745
|
+
}
|
|
746
|
+
el.removeEventListener('pointermove', onMove)
|
|
747
|
+
el.removeEventListener('pointerleave', onLeave)
|
|
748
|
+
}
|
|
749
|
+
}, [active, containerRef])
|
|
750
|
+
|
|
751
|
+
const hasMaterial = Boolean(
|
|
752
|
+
activePaintMaterial &&
|
|
753
|
+
(activePaintMaterial.material !== undefined ||
|
|
754
|
+
activePaintMaterial.materialPreset !== undefined),
|
|
755
|
+
)
|
|
756
|
+
const label = !hasMaterial ? 'Choose material' : `Paint ${activePaintTarget}`
|
|
757
|
+
const icon = 'mdi:format-color-fill'
|
|
758
|
+
|
|
759
|
+
useLayoutEffect(() => {
|
|
760
|
+
if (!active && badgeRef.current) {
|
|
761
|
+
badgeRef.current.style.display = 'none'
|
|
762
|
+
}
|
|
763
|
+
}, [active])
|
|
764
|
+
|
|
765
|
+
if (!active) return null
|
|
766
|
+
|
|
767
|
+
return (
|
|
768
|
+
<div
|
|
769
|
+
className="pointer-events-none"
|
|
770
|
+
ref={badgeRef}
|
|
771
|
+
style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
|
|
772
|
+
>
|
|
773
|
+
<PaintCursorBadge
|
|
774
|
+
disabled={!hasMaterial}
|
|
775
|
+
icon={icon}
|
|
776
|
+
label={label}
|
|
777
|
+
position={{ x: 0, y: 0 }}
|
|
778
|
+
/>
|
|
779
|
+
</div>
|
|
780
|
+
)
|
|
578
781
|
}
|
|
579
782
|
|
|
580
783
|
// ── Viewer canvas: memoized, subscribes to viewMode/floorplanPaneRatio internally ──
|
|
@@ -682,6 +885,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
682
885
|
containerRef={viewer3dRef}
|
|
683
886
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
684
887
|
/>
|
|
888
|
+
<PaintCursorLayer
|
|
889
|
+
containerRef={viewer3dRef}
|
|
890
|
+
isVersionPreviewMode={isVersionPreviewMode}
|
|
891
|
+
/>
|
|
685
892
|
{!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
|
|
686
893
|
<ViewerCanvasControlsHint
|
|
687
894
|
isPreviewMode={isPreviewMode}
|
|
@@ -689,7 +896,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
689
896
|
/>
|
|
690
897
|
) : null}
|
|
691
898
|
<SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
|
|
692
|
-
<Viewer
|
|
899
|
+
<Viewer
|
|
900
|
+
hoverStyles={EDITOR_HOVER_STYLES}
|
|
901
|
+
selectionManager={isFirstPersonMode ? 'default' : 'custom'}
|
|
902
|
+
>
|
|
693
903
|
<ViewerSceneContent
|
|
694
904
|
isFirstPersonMode={isFirstPersonMode}
|
|
695
905
|
isLoading={isLoading}
|
|
@@ -729,8 +939,8 @@ export default function Editor({
|
|
|
729
939
|
presetsAdapter,
|
|
730
940
|
commandPaletteEmptyAction,
|
|
731
941
|
}: EditorProps) {
|
|
732
|
-
|
|
733
|
-
|
|
942
|
+
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
943
|
+
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode })
|
|
734
944
|
const { isLoadingSceneRef } = useAutoSave({
|
|
735
945
|
onSave,
|
|
736
946
|
onDirty,
|
|
@@ -741,7 +951,8 @@ export default function Editor({
|
|
|
741
951
|
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
|
742
952
|
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
|
|
743
953
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
744
|
-
const
|
|
954
|
+
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
|
955
|
+
const wasFirstPersonModeRef = useRef(isFirstPersonMode)
|
|
745
956
|
|
|
746
957
|
const sidebarWidth = useSidebarStore((s) => s.width)
|
|
747
958
|
const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
|
|
@@ -759,6 +970,39 @@ export default function Editor({
|
|
|
759
970
|
}
|
|
760
971
|
}, [projectId])
|
|
761
972
|
|
|
973
|
+
useEffect(() => {
|
|
974
|
+
const wasFirstPersonMode = wasFirstPersonModeRef.current
|
|
975
|
+
wasFirstPersonModeRef.current = isFirstPersonMode
|
|
976
|
+
|
|
977
|
+
if (isFirstPersonMode && !wasFirstPersonMode) {
|
|
978
|
+
const viewer = useViewer.getState()
|
|
979
|
+
firstPersonPreviousLevelRef.current = viewer.selection.levelId
|
|
980
|
+
viewer.setCameraMode('perspective')
|
|
981
|
+
viewer.setWallMode('up')
|
|
982
|
+
viewer.setWalkthroughMode(true)
|
|
983
|
+
viewer.setSelection({ selectedIds: [], zoneId: null })
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (!(wasFirstPersonMode && !isFirstPersonMode)) return
|
|
988
|
+
|
|
989
|
+
const viewer = useViewer.getState()
|
|
990
|
+
const previousLevelId = firstPersonPreviousLevelRef.current
|
|
991
|
+
firstPersonPreviousLevelRef.current = null
|
|
992
|
+
viewer.setWalkthroughMode(false)
|
|
993
|
+
|
|
994
|
+
if (!previousLevelId) return
|
|
995
|
+
|
|
996
|
+
const previousLevelNode = useScene.getState().nodes[previousLevelId]
|
|
997
|
+
if (previousLevelNode?.type === 'level') {
|
|
998
|
+
viewer.setSelection({
|
|
999
|
+
levelId: previousLevelId,
|
|
1000
|
+
zoneId: null,
|
|
1001
|
+
selectedIds: [],
|
|
1002
|
+
})
|
|
1003
|
+
}
|
|
1004
|
+
}, [isFirstPersonMode])
|
|
1005
|
+
|
|
762
1006
|
// Load scene on mount (or when onLoad identity changes, e.g. project switch)
|
|
763
1007
|
useEffect(() => {
|
|
764
1008
|
let cancelled = false
|
|
@@ -821,7 +1065,7 @@ export default function Editor({
|
|
|
821
1065
|
const showLoader = isLoading || isSceneLoading
|
|
822
1066
|
|
|
823
1067
|
const previewViewerContent = (
|
|
824
|
-
<Viewer selectionManager="default">
|
|
1068
|
+
<Viewer hoverStyles={EDITOR_HOVER_STYLES} selectionManager="default">
|
|
825
1069
|
<ExportManager />
|
|
826
1070
|
<ViewerZoneSystem />
|
|
827
1071
|
<CeilingSystem />
|
|
@@ -864,7 +1108,13 @@ export default function Editor({
|
|
|
864
1108
|
return <Component />
|
|
865
1109
|
}
|
|
866
1110
|
|
|
867
|
-
const tabBarTabs =
|
|
1111
|
+
const tabBarTabs =
|
|
1112
|
+
sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon }) => ({
|
|
1113
|
+
id,
|
|
1114
|
+
label,
|
|
1115
|
+
mobileDefaultSnap,
|
|
1116
|
+
mobileIcon,
|
|
1117
|
+
})) ?? []
|
|
868
1118
|
|
|
869
1119
|
return (
|
|
870
1120
|
<PresetsProvider adapter={presetsAdapter}>
|
|
@@ -911,7 +1161,7 @@ export default function Editor({
|
|
|
911
1161
|
/>
|
|
912
1162
|
{/* First-person overlay — rendered on top of normal layout */}
|
|
913
1163
|
{isFirstPersonMode && (
|
|
914
|
-
<div className="fixed inset-0 z-50
|
|
1164
|
+
<div className="pointer-events-none fixed inset-0 z-50">
|
|
915
1165
|
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
916
1166
|
</div>
|
|
917
1167
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
|
-
import { Copy, Move, Trash2 } from 'lucide-react'
|
|
4
|
+
import { Copy, Move, Spline, Trash2 } from 'lucide-react'
|
|
5
5
|
import type { MouseEventHandler, PointerEventHandler } from 'react'
|
|
6
6
|
|
|
7
7
|
type NodeActionMenuProps = {
|
|
@@ -9,6 +9,7 @@ type NodeActionMenuProps = {
|
|
|
9
9
|
onDelete?: MouseEventHandler<HTMLButtonElement>
|
|
10
10
|
onDuplicate?: MouseEventHandler<HTMLButtonElement>
|
|
11
11
|
onMove?: MouseEventHandler<HTMLButtonElement>
|
|
12
|
+
onCurve?: MouseEventHandler<HTMLButtonElement>
|
|
12
13
|
onPointerDown?: PointerEventHandler<HTMLDivElement>
|
|
13
14
|
onPointerUp?: PointerEventHandler<HTMLDivElement>
|
|
14
15
|
onPointerEnter?: PointerEventHandler<HTMLDivElement>
|
|
@@ -20,6 +21,7 @@ export function NodeActionMenu({
|
|
|
20
21
|
onDelete,
|
|
21
22
|
onDuplicate,
|
|
22
23
|
onMove,
|
|
24
|
+
onCurve,
|
|
23
25
|
onPointerDown,
|
|
24
26
|
onPointerUp,
|
|
25
27
|
onPointerEnter,
|
|
@@ -44,6 +46,17 @@ export function NodeActionMenu({
|
|
|
44
46
|
<Move className="h-4 w-4" />
|
|
45
47
|
</button>
|
|
46
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
|
+
)}
|
|
47
60
|
{onDuplicate && (
|
|
48
61
|
<button
|
|
49
62
|
aria-label="Duplicate"
|