@pascal-app/editor 0.6.0 → 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 +9 -5
- 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 +20 -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 +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9855 -3298
- package/src/components/editor/index.tsx +269 -21
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/thumbnail-generator.tsx +38 -7
- 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 +267 -36
- 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/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- 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 +7 -0
- package/src/components/tools/door/move-door-tool.tsx +28 -8
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
- package/src/components/tools/fence/move-fence-tool.tsx +101 -34
- package/src/components/tools/item/move-tool.tsx +10 -1
- package/src/components/tools/item/placement-math.ts +30 -1
- package/src/components/tools/item/placement-strategies.ts +109 -31
- 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 +660 -52
- package/src/components/tools/roof/move-roof-tool.tsx +22 -15
- package/src/components/tools/shared/polygon-editor.tsx +153 -28
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -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/tool-manager.tsx +18 -3
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +28 -1
- 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 +18 -1
- package/src/components/ui/controls/material-picker.tsx +152 -165
- package/src/components/ui/controls/slider-control.tsx +66 -18
- 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 +1116 -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 +1 -25
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +981 -289
- package/src/components/ui/panels/fence-panel.tsx +3 -45
- 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 +210 -1
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +238 -5
- package/src/components/ui/panels/roof-panel.tsx +4 -105
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
- package/src/components/ui/panels/slab-panel.tsx +4 -30
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +11 -117
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +1 -95
- package/src/components/ui/panels/window-panel.tsx +660 -139
- 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 +2 -2
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +42 -1
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-keyboard.ts +64 -7
- package/src/hooks/use-mobile.ts +12 -12
- 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/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/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +164 -8
|
@@ -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,8 +30,8 @@ import {
|
|
|
22
30
|
} from '../../lib/scene'
|
|
23
31
|
import { initSFXBus } from '../../lib/sfx-bus'
|
|
24
32
|
import useEditor from '../../store/use-editor'
|
|
25
|
-
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
26
33
|
import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
|
|
34
|
+
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
27
35
|
import { RoofEditSystem } from '../systems/roof/roof-edit-system'
|
|
28
36
|
import { StairEditSystem } from '../systems/stair/stair-edit-system'
|
|
29
37
|
import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
|
|
@@ -63,6 +71,21 @@ const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-
|
|
|
63
71
|
const DELETE_CURSOR_BADGE_COLOR = '#ef4444'
|
|
64
72
|
const DELETE_CURSOR_BADGE_OFFSET_X = 14
|
|
65
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
|
+
}
|
|
66
89
|
|
|
67
90
|
/**
|
|
68
91
|
* Wire up module-level singletons (spatial grid, space detection, SFX) for
|
|
@@ -501,6 +524,50 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } })
|
|
|
501
524
|
)
|
|
502
525
|
}
|
|
503
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
|
+
|
|
504
571
|
// ── Viewer scene content: memoized so <Viewer> doesn't re-render on mode/viewMode changes ──
|
|
505
572
|
|
|
506
573
|
const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
@@ -517,9 +584,9 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
517
584
|
return (
|
|
518
585
|
<>
|
|
519
586
|
{!isFirstPersonMode && <SelectionManager />}
|
|
520
|
-
{!isVersionPreviewMode
|
|
521
|
-
{!isVersionPreviewMode
|
|
522
|
-
{!isVersionPreviewMode
|
|
587
|
+
{!(isVersionPreviewMode || isFirstPersonMode) && <BoxSelectTool />}
|
|
588
|
+
{!(isVersionPreviewMode || isFirstPersonMode) && <FloatingActionMenu />}
|
|
589
|
+
{!(isVersionPreviewMode || isFirstPersonMode) && <FloatingBuildingActionMenu />}
|
|
523
590
|
{!isFirstPersonMode && <WallMeasurementLabel />}
|
|
524
591
|
<ExportManager />
|
|
525
592
|
{isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
|
|
@@ -527,10 +594,10 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
527
594
|
<CeilingSelectionAffordanceSystem />
|
|
528
595
|
<RoofEditSystem />
|
|
529
596
|
<StairEditSystem />
|
|
530
|
-
{!isLoading
|
|
597
|
+
{!(isLoading || isFirstPersonMode) && (
|
|
531
598
|
<Grid cellColor="#aaa" fadeDistance={500} sectionColor="#ccc" />
|
|
532
599
|
)}
|
|
533
|
-
{!(isLoading || isVersionPreviewMode
|
|
600
|
+
{!(isLoading || isVersionPreviewMode || isFirstPersonMode) && <ToolManager />}
|
|
534
601
|
{isFirstPersonMode && <FirstPersonControls />}
|
|
535
602
|
<CustomCameraControls />
|
|
536
603
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
@@ -552,31 +619,165 @@ function DeleteCursorLayer({
|
|
|
552
619
|
isVersionPreviewMode: boolean
|
|
553
620
|
}) {
|
|
554
621
|
const mode = useEditor((s) => s.mode)
|
|
555
|
-
const
|
|
622
|
+
const badgeRef = useRef<HTMLDivElement>(null)
|
|
556
623
|
const active = mode === 'delete' && !isVersionPreviewMode
|
|
557
624
|
|
|
558
625
|
useEffect(() => {
|
|
559
626
|
if (!active) {
|
|
560
|
-
|
|
627
|
+
if (badgeRef.current) {
|
|
628
|
+
badgeRef.current.style.display = 'none'
|
|
629
|
+
}
|
|
561
630
|
return
|
|
562
631
|
}
|
|
563
632
|
const el = containerRef.current
|
|
564
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
|
+
|
|
565
646
|
const onMove = (e: PointerEvent) => {
|
|
566
647
|
const rect = el.getBoundingClientRect()
|
|
567
|
-
|
|
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
|
+
}
|
|
568
663
|
}
|
|
569
|
-
const onLeave = () => setPosition(null)
|
|
570
664
|
el.addEventListener('pointermove', onMove)
|
|
571
665
|
el.addEventListener('pointerleave', onLeave)
|
|
572
666
|
return () => {
|
|
667
|
+
if (frame !== 0) {
|
|
668
|
+
window.cancelAnimationFrame(frame)
|
|
669
|
+
}
|
|
573
670
|
el.removeEventListener('pointermove', onMove)
|
|
574
671
|
el.removeEventListener('pointerleave', onLeave)
|
|
575
672
|
}
|
|
576
673
|
}, [active, containerRef])
|
|
577
674
|
|
|
578
|
-
if (!
|
|
579
|
-
|
|
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
|
+
)
|
|
580
781
|
}
|
|
581
782
|
|
|
582
783
|
// ── Viewer canvas: memoized, subscribes to viewMode/floorplanPaneRatio internally ──
|
|
@@ -684,6 +885,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
684
885
|
containerRef={viewer3dRef}
|
|
685
886
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
686
887
|
/>
|
|
888
|
+
<PaintCursorLayer
|
|
889
|
+
containerRef={viewer3dRef}
|
|
890
|
+
isVersionPreviewMode={isVersionPreviewMode}
|
|
891
|
+
/>
|
|
687
892
|
{!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
|
|
688
893
|
<ViewerCanvasControlsHint
|
|
689
894
|
isPreviewMode={isPreviewMode}
|
|
@@ -691,7 +896,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
691
896
|
/>
|
|
692
897
|
) : null}
|
|
693
898
|
<SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
|
|
694
|
-
<Viewer
|
|
899
|
+
<Viewer
|
|
900
|
+
hoverStyles={EDITOR_HOVER_STYLES}
|
|
901
|
+
selectionManager={isFirstPersonMode ? 'default' : 'custom'}
|
|
902
|
+
>
|
|
695
903
|
<ViewerSceneContent
|
|
696
904
|
isFirstPersonMode={isFirstPersonMode}
|
|
697
905
|
isLoading={isLoading}
|
|
@@ -731,8 +939,8 @@ export default function Editor({
|
|
|
731
939
|
presetsAdapter,
|
|
732
940
|
commandPaletteEmptyAction,
|
|
733
941
|
}: EditorProps) {
|
|
734
|
-
|
|
735
|
-
|
|
942
|
+
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
943
|
+
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode })
|
|
736
944
|
const { isLoadingSceneRef } = useAutoSave({
|
|
737
945
|
onSave,
|
|
738
946
|
onDirty,
|
|
@@ -743,7 +951,8 @@ export default function Editor({
|
|
|
743
951
|
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
|
744
952
|
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
|
|
745
953
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
746
|
-
const
|
|
954
|
+
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
|
955
|
+
const wasFirstPersonModeRef = useRef(isFirstPersonMode)
|
|
747
956
|
|
|
748
957
|
const sidebarWidth = useSidebarStore((s) => s.width)
|
|
749
958
|
const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
|
|
@@ -761,6 +970,39 @@ export default function Editor({
|
|
|
761
970
|
}
|
|
762
971
|
}, [projectId])
|
|
763
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
|
+
|
|
764
1006
|
// Load scene on mount (or when onLoad identity changes, e.g. project switch)
|
|
765
1007
|
useEffect(() => {
|
|
766
1008
|
let cancelled = false
|
|
@@ -823,7 +1065,7 @@ export default function Editor({
|
|
|
823
1065
|
const showLoader = isLoading || isSceneLoading
|
|
824
1066
|
|
|
825
1067
|
const previewViewerContent = (
|
|
826
|
-
<Viewer selectionManager="default">
|
|
1068
|
+
<Viewer hoverStyles={EDITOR_HOVER_STYLES} selectionManager="default">
|
|
827
1069
|
<ExportManager />
|
|
828
1070
|
<ViewerZoneSystem />
|
|
829
1071
|
<CeilingSystem />
|
|
@@ -866,7 +1108,13 @@ export default function Editor({
|
|
|
866
1108
|
return <Component />
|
|
867
1109
|
}
|
|
868
1110
|
|
|
869
|
-
const tabBarTabs =
|
|
1111
|
+
const tabBarTabs =
|
|
1112
|
+
sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon }) => ({
|
|
1113
|
+
id,
|
|
1114
|
+
label,
|
|
1115
|
+
mobileDefaultSnap,
|
|
1116
|
+
mobileIcon,
|
|
1117
|
+
})) ?? []
|
|
870
1118
|
|
|
871
1119
|
return (
|
|
872
1120
|
<PresetsProvider adapter={presetsAdapter}>
|
|
@@ -913,7 +1161,7 @@ export default function Editor({
|
|
|
913
1161
|
/>
|
|
914
1162
|
{/* First-person overlay — rendered on top of normal layout */}
|
|
915
1163
|
{isFirstPersonMode && (
|
|
916
|
-
<div className="fixed inset-0 z-50
|
|
1164
|
+
<div className="pointer-events-none fixed inset-0 z-50">
|
|
917
1165
|
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
918
1166
|
</div>
|
|
919
1167
|
)}
|