@pascal-app/editor 0.6.0 → 0.8.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 +13 -9
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +74 -5
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +24 -3
- package/src/components/editor/first-person/build-collider-world.ts +363 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +860 -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 +9861 -3297
- package/src/components/editor/index.tsx +295 -32
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +56 -68
- 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 +124 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +202 -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 +10 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1 -1
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -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/ceiling/move-ceiling-tool.tsx +9 -2
- 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/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +160 -4
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +139 -25
- package/src/components/tools/fence/move-fence-tool.tsx +111 -40
- package/src/components/tools/item/move-tool.tsx +7 -1
- package/src/components/tools/item/placement-math.ts +32 -5
- package/src/components/tools/item/placement-strategies.ts +110 -31
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +1 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +558 -52
- package/src/components/tools/roof/move-roof-tool.tsx +29 -17
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- 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 +20 -5
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +131 -27
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +136 -4
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +34 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +98 -59
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +418 -41
- package/src/components/ui/command-palette/editor-commands.tsx +24 -5
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +154 -164
- 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 +10 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +2563 -1239
- package/src/components/ui/item-catalog/item-catalog.tsx +96 -187
- package/src/components/ui/level-duplicate-dialog.tsx +113 -0
- package/src/components/ui/panels/ceiling-panel.tsx +3 -28
- package/src/components/ui/panels/column-panel.tsx +759 -0
- package/src/components/ui/panels/door-panel.tsx +989 -290
- package/src/components/ui/panels/fence-panel.tsx +2 -49
- 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 +163 -0
- package/src/components/ui/panels/panel-manager.tsx +208 -28
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +253 -5
- package/src/components/ui/panels/roof-panel.tsx +13 -64
- 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 +161 -0
- package/src/components/ui/panels/stair-panel.tsx +20 -74
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +10 -8
- package/src/components/ui/panels/window-panel.tsx +668 -139
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -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/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +105 -22
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +76 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +11 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +10 -5
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +0 -0
- package/src/components/viewer-zone-system.tsx +0 -0
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +74 -7
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +8 -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/level-duplication.test.ts +70 -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/scene.ts +0 -0
- package/src/lib/sfx-bus.ts +2 -0
- package/src/lib/sfx-player.ts +5 -5
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +186 -62
- package/tsconfig.json +2 -1
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/ui/viewer-toolbar.tsx +0 -395
|
@@ -7,11 +7,20 @@ 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'
|
|
23
|
+
import { useAutoFrame } from '../../hooks/use-auto-frame'
|
|
15
24
|
import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
|
|
16
25
|
import { useKeyboard } from '../../hooks/use-keyboard'
|
|
17
26
|
import {
|
|
@@ -22,8 +31,8 @@ import {
|
|
|
22
31
|
} from '../../lib/scene'
|
|
23
32
|
import { initSFXBus } from '../../lib/sfx-bus'
|
|
24
33
|
import useEditor from '../../store/use-editor'
|
|
25
|
-
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
26
34
|
import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
|
|
35
|
+
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
27
36
|
import { RoofEditSystem } from '../systems/roof/roof-edit-system'
|
|
28
37
|
import { StairEditSystem } from '../systems/stair/stair-edit-system'
|
|
29
38
|
import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
|
|
@@ -56,6 +65,7 @@ import { Grid } from './grid'
|
|
|
56
65
|
import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
|
|
57
66
|
import { SelectionManager } from './selection-manager'
|
|
58
67
|
import { SiteEdgeLabels } from './site-edge-labels'
|
|
68
|
+
import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
|
|
59
69
|
import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
|
|
60
70
|
import { WallMeasurementLabel } from './wall-measurement-label'
|
|
61
71
|
|
|
@@ -63,6 +73,21 @@ const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-
|
|
|
63
73
|
const DELETE_CURSOR_BADGE_COLOR = '#ef4444'
|
|
64
74
|
const DELETE_CURSOR_BADGE_OFFSET_X = 14
|
|
65
75
|
const DELETE_CURSOR_BADGE_OFFSET_Y = 14
|
|
76
|
+
const PAINT_CURSOR_BADGE_COLOR = '#f59e0b'
|
|
77
|
+
const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
|
78
|
+
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
|
79
|
+
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
|
80
|
+
const EDITOR_HOVER_STYLES: HoverStyles = {
|
|
81
|
+
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
|
|
82
|
+
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
|
|
83
|
+
'paint-ready': { visibleColor: 0xf5_9e_0b, hiddenColor: 0xfd_e0_68, strength: 5, pulse: true },
|
|
84
|
+
'paint-disabled': {
|
|
85
|
+
visibleColor: 0x94_a3_b8,
|
|
86
|
+
hiddenColor: 0x47_55_69,
|
|
87
|
+
strength: 4,
|
|
88
|
+
pulse: false,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
66
91
|
|
|
67
92
|
/**
|
|
68
93
|
* Wire up module-level singletons (spatial grid, space detection, SFX) for
|
|
@@ -439,7 +464,7 @@ function ViewerCanvasControlsHint({
|
|
|
439
464
|
<div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
|
|
440
465
|
<section
|
|
441
466
|
aria-label="Camera controls hint"
|
|
442
|
-
className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-
|
|
467
|
+
className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-elevation-4 backdrop-blur-xl"
|
|
443
468
|
>
|
|
444
469
|
<div className="grid min-w-0 flex-1 grid-cols-3 items-start divide-x divide-border/18">
|
|
445
470
|
{hints.map((hint) => (
|
|
@@ -501,6 +526,50 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } })
|
|
|
501
526
|
)
|
|
502
527
|
}
|
|
503
528
|
|
|
529
|
+
function PaintCursorBadge({
|
|
530
|
+
position,
|
|
531
|
+
label,
|
|
532
|
+
disabled,
|
|
533
|
+
icon,
|
|
534
|
+
}: {
|
|
535
|
+
position: { x: number; y: number }
|
|
536
|
+
label: string
|
|
537
|
+
disabled: boolean
|
|
538
|
+
icon: string
|
|
539
|
+
}) {
|
|
540
|
+
const accentColor = disabled ? PAINT_CURSOR_BADGE_DISABLED_COLOR : PAINT_CURSOR_BADGE_COLOR
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<div
|
|
544
|
+
aria-hidden="true"
|
|
545
|
+
className="pointer-events-none absolute z-40"
|
|
546
|
+
style={{
|
|
547
|
+
left: position.x + PAINT_CURSOR_BADGE_OFFSET_X,
|
|
548
|
+
top: position.y + PAINT_CURSOR_BADGE_OFFSET_Y,
|
|
549
|
+
}}
|
|
550
|
+
>
|
|
551
|
+
<div
|
|
552
|
+
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)]"
|
|
553
|
+
style={{
|
|
554
|
+
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`,
|
|
555
|
+
}}
|
|
556
|
+
>
|
|
557
|
+
<Icon
|
|
558
|
+
aria-hidden="true"
|
|
559
|
+
className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
560
|
+
color={accentColor}
|
|
561
|
+
height={16}
|
|
562
|
+
icon={icon}
|
|
563
|
+
width={16}
|
|
564
|
+
/>
|
|
565
|
+
<span className="font-medium text-[11px]" style={{ color: accentColor }}>
|
|
566
|
+
{label}
|
|
567
|
+
</span>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
504
573
|
// ── Viewer scene content: memoized so <Viewer> doesn't re-render on mode/viewMode changes ──
|
|
505
574
|
|
|
506
575
|
const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
@@ -517,9 +586,9 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
517
586
|
return (
|
|
518
587
|
<>
|
|
519
588
|
{!isFirstPersonMode && <SelectionManager />}
|
|
520
|
-
{!isVersionPreviewMode
|
|
521
|
-
{!isVersionPreviewMode
|
|
522
|
-
{!isVersionPreviewMode
|
|
589
|
+
{!(isVersionPreviewMode || isFirstPersonMode) && <BoxSelectTool />}
|
|
590
|
+
{!(isVersionPreviewMode || isFirstPersonMode) && <FloatingActionMenu />}
|
|
591
|
+
{!(isVersionPreviewMode || isFirstPersonMode) && <FloatingBuildingActionMenu />}
|
|
523
592
|
{!isFirstPersonMode && <WallMeasurementLabel />}
|
|
524
593
|
<ExportManager />
|
|
525
594
|
{isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
|
|
@@ -527,10 +596,10 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
527
596
|
<CeilingSelectionAffordanceSystem />
|
|
528
597
|
<RoofEditSystem />
|
|
529
598
|
<StairEditSystem />
|
|
530
|
-
{!isLoading
|
|
599
|
+
{!(isLoading || isFirstPersonMode) && (
|
|
531
600
|
<Grid cellColor="#aaa" fadeDistance={500} sectionColor="#ccc" />
|
|
532
601
|
)}
|
|
533
|
-
{!(isLoading || isVersionPreviewMode
|
|
602
|
+
{!(isLoading || isVersionPreviewMode || isFirstPersonMode) && <ToolManager />}
|
|
534
603
|
{isFirstPersonMode && <FirstPersonControls />}
|
|
535
604
|
<CustomCameraControls />
|
|
536
605
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
@@ -552,31 +621,165 @@ function DeleteCursorLayer({
|
|
|
552
621
|
isVersionPreviewMode: boolean
|
|
553
622
|
}) {
|
|
554
623
|
const mode = useEditor((s) => s.mode)
|
|
555
|
-
const
|
|
624
|
+
const badgeRef = useRef<HTMLDivElement>(null)
|
|
556
625
|
const active = mode === 'delete' && !isVersionPreviewMode
|
|
557
626
|
|
|
558
627
|
useEffect(() => {
|
|
559
628
|
if (!active) {
|
|
560
|
-
|
|
629
|
+
if (badgeRef.current) {
|
|
630
|
+
badgeRef.current.style.display = 'none'
|
|
631
|
+
}
|
|
561
632
|
return
|
|
562
633
|
}
|
|
563
634
|
const el = containerRef.current
|
|
564
635
|
if (!el) return
|
|
636
|
+
let frame = 0
|
|
637
|
+
let nextX = 0
|
|
638
|
+
let nextY = 0
|
|
639
|
+
const badge = badgeRef.current
|
|
640
|
+
|
|
641
|
+
const flushPosition = () => {
|
|
642
|
+
frame = 0
|
|
643
|
+
if (!badge) return
|
|
644
|
+
badge.style.display = 'block'
|
|
645
|
+
badge.style.transform = `translate(${nextX + DELETE_CURSOR_BADGE_OFFSET_X}px, ${nextY + DELETE_CURSOR_BADGE_OFFSET_Y}px)`
|
|
646
|
+
}
|
|
647
|
+
|
|
565
648
|
const onMove = (e: PointerEvent) => {
|
|
566
649
|
const rect = el.getBoundingClientRect()
|
|
567
|
-
|
|
650
|
+
nextX = e.clientX - rect.left
|
|
651
|
+
nextY = e.clientY - rect.top
|
|
652
|
+
|
|
653
|
+
if (frame === 0) {
|
|
654
|
+
frame = window.requestAnimationFrame(flushPosition)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const onLeave = () => {
|
|
658
|
+
if (frame !== 0) {
|
|
659
|
+
window.cancelAnimationFrame(frame)
|
|
660
|
+
frame = 0
|
|
661
|
+
}
|
|
662
|
+
if (badge) {
|
|
663
|
+
badge.style.display = 'none'
|
|
664
|
+
}
|
|
568
665
|
}
|
|
569
|
-
const onLeave = () => setPosition(null)
|
|
570
666
|
el.addEventListener('pointermove', onMove)
|
|
571
667
|
el.addEventListener('pointerleave', onLeave)
|
|
572
668
|
return () => {
|
|
669
|
+
if (frame !== 0) {
|
|
670
|
+
window.cancelAnimationFrame(frame)
|
|
671
|
+
}
|
|
573
672
|
el.removeEventListener('pointermove', onMove)
|
|
574
673
|
el.removeEventListener('pointerleave', onLeave)
|
|
575
674
|
}
|
|
576
675
|
}, [active, containerRef])
|
|
577
676
|
|
|
578
|
-
if (!
|
|
579
|
-
|
|
677
|
+
if (!active) return null
|
|
678
|
+
|
|
679
|
+
return (
|
|
680
|
+
<div
|
|
681
|
+
className="pointer-events-none"
|
|
682
|
+
ref={badgeRef}
|
|
683
|
+
style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
|
|
684
|
+
>
|
|
685
|
+
<DeleteCursorBadge position={{ x: 0, y: 0 }} />
|
|
686
|
+
</div>
|
|
687
|
+
)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function PaintCursorLayer({
|
|
691
|
+
containerRef,
|
|
692
|
+
isVersionPreviewMode,
|
|
693
|
+
}: {
|
|
694
|
+
containerRef: React.RefObject<HTMLDivElement | null>
|
|
695
|
+
isVersionPreviewMode: boolean
|
|
696
|
+
}) {
|
|
697
|
+
const mode = useEditor((s) => s.mode)
|
|
698
|
+
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
|
699
|
+
const activePaintTarget = useEditor((s) => s.activePaintTarget)
|
|
700
|
+
const badgeRef = useRef<HTMLDivElement>(null)
|
|
701
|
+
const active = mode === 'material-paint' && !isVersionPreviewMode
|
|
702
|
+
|
|
703
|
+
useEffect(() => {
|
|
704
|
+
if (!active) {
|
|
705
|
+
if (badgeRef.current) {
|
|
706
|
+
badgeRef.current.style.display = 'none'
|
|
707
|
+
}
|
|
708
|
+
return
|
|
709
|
+
}
|
|
710
|
+
const el = containerRef.current
|
|
711
|
+
if (!el) return
|
|
712
|
+
let frame = 0
|
|
713
|
+
let nextX = 0
|
|
714
|
+
let nextY = 0
|
|
715
|
+
const badge = badgeRef.current
|
|
716
|
+
|
|
717
|
+
const flushPosition = () => {
|
|
718
|
+
frame = 0
|
|
719
|
+
if (!badge) return
|
|
720
|
+
badge.style.display = 'block'
|
|
721
|
+
badge.style.transform = `translate(${nextX + PAINT_CURSOR_BADGE_OFFSET_X}px, ${nextY + PAINT_CURSOR_BADGE_OFFSET_Y}px)`
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const onMove = (e: PointerEvent) => {
|
|
725
|
+
const rect = el.getBoundingClientRect()
|
|
726
|
+
nextX = e.clientX - rect.left
|
|
727
|
+
nextY = e.clientY - rect.top
|
|
728
|
+
|
|
729
|
+
if (frame === 0) {
|
|
730
|
+
frame = window.requestAnimationFrame(flushPosition)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const onLeave = () => {
|
|
734
|
+
if (frame !== 0) {
|
|
735
|
+
window.cancelAnimationFrame(frame)
|
|
736
|
+
frame = 0
|
|
737
|
+
}
|
|
738
|
+
if (badge) {
|
|
739
|
+
badge.style.display = 'none'
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
el.addEventListener('pointermove', onMove)
|
|
743
|
+
el.addEventListener('pointerleave', onLeave)
|
|
744
|
+
return () => {
|
|
745
|
+
if (frame !== 0) {
|
|
746
|
+
window.cancelAnimationFrame(frame)
|
|
747
|
+
}
|
|
748
|
+
el.removeEventListener('pointermove', onMove)
|
|
749
|
+
el.removeEventListener('pointerleave', onLeave)
|
|
750
|
+
}
|
|
751
|
+
}, [active, containerRef])
|
|
752
|
+
|
|
753
|
+
const hasMaterial = Boolean(
|
|
754
|
+
activePaintMaterial &&
|
|
755
|
+
(activePaintMaterial.material !== undefined ||
|
|
756
|
+
activePaintMaterial.materialPreset !== undefined),
|
|
757
|
+
)
|
|
758
|
+
const label = hasMaterial ? `Paint ${activePaintTarget}` : 'Choose material'
|
|
759
|
+
const icon = 'mdi:format-color-fill'
|
|
760
|
+
|
|
761
|
+
useLayoutEffect(() => {
|
|
762
|
+
if (!active && badgeRef.current) {
|
|
763
|
+
badgeRef.current.style.display = 'none'
|
|
764
|
+
}
|
|
765
|
+
}, [active])
|
|
766
|
+
|
|
767
|
+
if (!active) return null
|
|
768
|
+
|
|
769
|
+
return (
|
|
770
|
+
<div
|
|
771
|
+
className="pointer-events-none"
|
|
772
|
+
ref={badgeRef}
|
|
773
|
+
style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
|
|
774
|
+
>
|
|
775
|
+
<PaintCursorBadge
|
|
776
|
+
disabled={!hasMaterial}
|
|
777
|
+
icon={icon}
|
|
778
|
+
label={label}
|
|
779
|
+
position={{ x: 0, y: 0 }}
|
|
780
|
+
/>
|
|
781
|
+
</div>
|
|
782
|
+
)
|
|
580
783
|
}
|
|
581
784
|
|
|
582
785
|
// ── Viewer canvas: memoized, subscribes to viewMode/floorplanPaneRatio internally ──
|
|
@@ -585,16 +788,16 @@ function DeleteCursorLayer({
|
|
|
585
788
|
const ViewerCanvas = memo(function ViewerCanvas({
|
|
586
789
|
isVersionPreviewMode,
|
|
587
790
|
isLoading,
|
|
791
|
+
isFirstPersonMode,
|
|
588
792
|
hasLoadedInitialScene,
|
|
589
793
|
showLoader,
|
|
590
|
-
isFirstPersonMode,
|
|
591
794
|
onThumbnailCapture,
|
|
592
795
|
}: {
|
|
593
796
|
isVersionPreviewMode: boolean
|
|
594
797
|
isLoading: boolean
|
|
798
|
+
isFirstPersonMode: boolean
|
|
595
799
|
hasLoadedInitialScene: boolean
|
|
596
800
|
showLoader: boolean
|
|
597
|
-
isFirstPersonMode: boolean
|
|
598
801
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
599
802
|
}) {
|
|
600
803
|
const viewMode = useEditor((s) => s.viewMode)
|
|
@@ -636,7 +839,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
636
839
|
window.removeEventListener('pointermove', handlePointerMove)
|
|
637
840
|
window.removeEventListener('pointerup', handlePointerUp)
|
|
638
841
|
}
|
|
639
|
-
}, [
|
|
842
|
+
}, [])
|
|
640
843
|
|
|
641
844
|
useEffect(() => {
|
|
642
845
|
setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
|
|
@@ -684,6 +887,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
684
887
|
containerRef={viewer3dRef}
|
|
685
888
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
686
889
|
/>
|
|
890
|
+
<PaintCursorLayer
|
|
891
|
+
containerRef={viewer3dRef}
|
|
892
|
+
isVersionPreviewMode={isVersionPreviewMode}
|
|
893
|
+
/>
|
|
687
894
|
{!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
|
|
688
895
|
<ViewerCanvasControlsHint
|
|
689
896
|
isPreviewMode={isPreviewMode}
|
|
@@ -691,7 +898,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
691
898
|
/>
|
|
692
899
|
) : null}
|
|
693
900
|
<SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
|
|
694
|
-
<Viewer
|
|
901
|
+
<Viewer
|
|
902
|
+
hoverStyles={EDITOR_HOVER_STYLES}
|
|
903
|
+
selectionManager={isFirstPersonMode ? 'default' : 'custom'}
|
|
904
|
+
>
|
|
695
905
|
<ViewerSceneContent
|
|
696
906
|
isFirstPersonMode={isFirstPersonMode}
|
|
697
907
|
isLoading={isLoading}
|
|
@@ -731,7 +941,9 @@ export default function Editor({
|
|
|
731
941
|
presetsAdapter,
|
|
732
942
|
commandPaletteEmptyAction,
|
|
733
943
|
}: EditorProps) {
|
|
734
|
-
|
|
944
|
+
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
945
|
+
|
|
946
|
+
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode })
|
|
735
947
|
|
|
736
948
|
const { isLoadingSceneRef } = useAutoSave({
|
|
737
949
|
onSave,
|
|
@@ -743,7 +955,7 @@ export default function Editor({
|
|
|
743
955
|
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
|
744
956
|
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
|
|
745
957
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
746
|
-
const
|
|
958
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
747
959
|
|
|
748
960
|
const sidebarWidth = useSidebarStore((s) => s.width)
|
|
749
961
|
const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
|
|
@@ -822,8 +1034,44 @@ export default function Editor({
|
|
|
822
1034
|
|
|
823
1035
|
const showLoader = isLoading || isSceneLoading
|
|
824
1036
|
|
|
1037
|
+
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
|
1038
|
+
const wasFirstPersonModeRef = useRef(isFirstPersonMode)
|
|
1039
|
+
|
|
1040
|
+
useEffect(() => {
|
|
1041
|
+
const wasFirstPersonMode = wasFirstPersonModeRef.current
|
|
1042
|
+
wasFirstPersonModeRef.current = isFirstPersonMode
|
|
1043
|
+
|
|
1044
|
+
if (isFirstPersonMode && !wasFirstPersonMode) {
|
|
1045
|
+
const viewer = useViewer.getState()
|
|
1046
|
+
firstPersonPreviousLevelRef.current = viewer.selection.levelId
|
|
1047
|
+
viewer.setCameraMode('perspective')
|
|
1048
|
+
viewer.setWallMode('up')
|
|
1049
|
+
viewer.setWalkthroughMode(true)
|
|
1050
|
+
viewer.setSelection({ selectedIds: [], zoneId: null })
|
|
1051
|
+
return
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (!(wasFirstPersonMode && !isFirstPersonMode)) return
|
|
1055
|
+
|
|
1056
|
+
const viewer = useViewer.getState()
|
|
1057
|
+
const previousLevelId = firstPersonPreviousLevelRef.current
|
|
1058
|
+
firstPersonPreviousLevelRef.current = null
|
|
1059
|
+
viewer.setWalkthroughMode(false)
|
|
1060
|
+
|
|
1061
|
+
if (!previousLevelId) return
|
|
1062
|
+
|
|
1063
|
+
const previousLevelNode = useScene.getState().nodes[previousLevelId]
|
|
1064
|
+
if (previousLevelNode?.type === 'level') {
|
|
1065
|
+
viewer.setSelection({
|
|
1066
|
+
levelId: previousLevelId,
|
|
1067
|
+
zoneId: null,
|
|
1068
|
+
selectedIds: [],
|
|
1069
|
+
})
|
|
1070
|
+
}
|
|
1071
|
+
}, [isFirstPersonMode])
|
|
1072
|
+
|
|
825
1073
|
const previewViewerContent = (
|
|
826
|
-
<Viewer selectionManager="default">
|
|
1074
|
+
<Viewer hoverStyles={EDITOR_HOVER_STYLES} selectionManager="default">
|
|
827
1075
|
<ExportManager />
|
|
828
1076
|
<ViewerZoneSystem />
|
|
829
1077
|
<CeilingSystem />
|
|
@@ -866,7 +1114,13 @@ export default function Editor({
|
|
|
866
1114
|
return <Component />
|
|
867
1115
|
}
|
|
868
1116
|
|
|
869
|
-
const tabBarTabs =
|
|
1117
|
+
const tabBarTabs =
|
|
1118
|
+
sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon }) => ({
|
|
1119
|
+
id,
|
|
1120
|
+
label,
|
|
1121
|
+
mobileDefaultSnap,
|
|
1122
|
+
mobileIcon,
|
|
1123
|
+
})) ?? []
|
|
870
1124
|
|
|
871
1125
|
return (
|
|
872
1126
|
<PresetsProvider adapter={presetsAdapter}>
|
|
@@ -887,21 +1141,24 @@ export default function Editor({
|
|
|
887
1141
|
navbarSlot={navbarSlot}
|
|
888
1142
|
overlays={
|
|
889
1143
|
<>
|
|
890
|
-
<FloatingLevelSelector />
|
|
891
|
-
{!isVersionPreviewMode && (
|
|
1144
|
+
{!isCaptureMode && <FloatingLevelSelector />}
|
|
1145
|
+
{!(isVersionPreviewMode || isCaptureMode) && (
|
|
892
1146
|
<div className="pointer-events-auto">
|
|
893
1147
|
<ActionMenu />
|
|
894
1148
|
</div>
|
|
895
1149
|
)}
|
|
896
|
-
{!isVersionPreviewMode && (
|
|
1150
|
+
{!(isVersionPreviewMode || isCaptureMode) && (
|
|
897
1151
|
<div className="pointer-events-auto">
|
|
898
1152
|
<PanelManager />
|
|
899
1153
|
</div>
|
|
900
1154
|
)}
|
|
901
|
-
|
|
902
|
-
<
|
|
903
|
-
|
|
1155
|
+
{!isCaptureMode && (
|
|
1156
|
+
<div className="pointer-events-auto">
|
|
1157
|
+
<HelperManager />
|
|
1158
|
+
</div>
|
|
1159
|
+
)}
|
|
904
1160
|
{viewerBanner}
|
|
1161
|
+
{projectId ? <SnapshotCaptureOverlay projectId={projectId} /> : null}
|
|
905
1162
|
</>
|
|
906
1163
|
}
|
|
907
1164
|
renderTabContent={renderTabContent}
|
|
@@ -911,14 +1168,14 @@ export default function Editor({
|
|
|
911
1168
|
viewerToolbarLeft={viewerToolbarLeft}
|
|
912
1169
|
viewerToolbarRight={viewerToolbarRight}
|
|
913
1170
|
/>
|
|
1171
|
+
<EditorCommands />
|
|
1172
|
+
<CommandPalette emptyAction={commandPaletteEmptyAction} />
|
|
914
1173
|
{/* First-person overlay — rendered on top of normal layout */}
|
|
915
1174
|
{isFirstPersonMode && (
|
|
916
|
-
<div className="fixed inset-0 z-50
|
|
1175
|
+
<div className="pointer-events-none fixed inset-0 z-50">
|
|
917
1176
|
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
918
1177
|
</div>
|
|
919
1178
|
)}
|
|
920
|
-
<EditorCommands />
|
|
921
|
-
<CommandPalette emptyAction={commandPaletteEmptyAction} />
|
|
922
1179
|
</>
|
|
923
1180
|
)}
|
|
924
1181
|
</PresetsProvider>
|
|
@@ -974,6 +1231,12 @@ export default function Editor({
|
|
|
974
1231
|
<HelperManager />
|
|
975
1232
|
</div>
|
|
976
1233
|
</ViewerOverlays>
|
|
1234
|
+
{/* First-person overlay — rendered on top of normal layout */}
|
|
1235
|
+
{isFirstPersonMode && (
|
|
1236
|
+
<div className="pointer-events-none fixed inset-0 z-50">
|
|
1237
|
+
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
1238
|
+
</div>
|
|
1239
|
+
)}
|
|
977
1240
|
</>
|
|
978
1241
|
)}
|
|
979
1242
|
</div>
|