@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Pascal building editor component",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"check-types": "tsc --noEmit"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@pascal-app/core": "^0.
|
|
15
|
-
"@pascal-app/viewer": "^0.
|
|
14
|
+
"@pascal-app/core": "^0.7.0",
|
|
15
|
+
"@pascal-app/viewer": "^0.7.0",
|
|
16
16
|
"@react-three/drei": "^10",
|
|
17
17
|
"@react-three/fiber": "^9",
|
|
18
18
|
"next": ">=15",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"three": "^0.184"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@dnd-kit/core": "^6.3.1",
|
|
25
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
26
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
24
27
|
"@iconify/react": "^6.0.2",
|
|
25
28
|
"@number-flow/react": "^0.5.14",
|
|
26
29
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
@@ -41,23 +44,24 @@
|
|
|
41
44
|
"clsx": "^2.1.1",
|
|
42
45
|
"cmdk": "^1.1.1",
|
|
43
46
|
"howler": "^2.2.4",
|
|
44
|
-
"lucide-react": "^
|
|
47
|
+
"lucide-react": "^1.7.0",
|
|
45
48
|
"mitt": "^3.0.1",
|
|
46
49
|
"motion": "^12.34.3",
|
|
47
50
|
"nanoid": "^5.1.6",
|
|
48
51
|
"tailwind-merge": "^3.5.0",
|
|
49
52
|
"zod": "^4.3.6",
|
|
50
|
-
"zustand": "^5.0.11"
|
|
53
|
+
"zustand": "^5.0.11",
|
|
54
|
+
"three-mesh-bvh": "~0.9.8"
|
|
51
55
|
},
|
|
52
56
|
"devDependencies": {
|
|
53
|
-
"@pascal-app/core": "^0.
|
|
54
|
-
"@pascal-app/viewer": "^0.
|
|
57
|
+
"@pascal-app/core": "^0.7.0",
|
|
58
|
+
"@pascal-app/viewer": "^0.7.0",
|
|
55
59
|
"@pascal/typescript-config": "*",
|
|
60
|
+
"@types/bun": "^1.3.0",
|
|
56
61
|
"@types/howler": "^2.2.12",
|
|
57
|
-
"@types/node": "^22.19.12",
|
|
58
62
|
"@types/react": "19.2.2",
|
|
59
63
|
"@types/react-dom": "19.2.2",
|
|
60
64
|
"@types/three": "^0.184.0",
|
|
61
|
-
"typescript": "
|
|
65
|
+
"typescript": "6.0.2"
|
|
62
66
|
}
|
|
63
67
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { animate, motion, useMotionValue } from 'motion/react'
|
|
4
|
+
import {
|
|
5
|
+
forwardRef,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useImperativeHandle,
|
|
10
|
+
useRef,
|
|
11
|
+
} from 'react'
|
|
12
|
+
|
|
13
|
+
export type BottomSheetHandle = {
|
|
14
|
+
snapTo: (heightPx: number) => void
|
|
15
|
+
getHeight: () => number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface BottomSheetProps {
|
|
19
|
+
initialHeightPx: number
|
|
20
|
+
snapPointsPx: number[]
|
|
21
|
+
onCommit: (heightPx: number) => void
|
|
22
|
+
children: ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DRAG_THRESHOLD_PX = 6
|
|
26
|
+
|
|
27
|
+
export const BottomSheet = forwardRef<BottomSheetHandle, BottomSheetProps>(function BottomSheet(
|
|
28
|
+
{ initialHeightPx, snapPointsPx, onCommit, children },
|
|
29
|
+
ref,
|
|
30
|
+
) {
|
|
31
|
+
const height = useMotionValue(initialHeightPx)
|
|
32
|
+
const dragStartY = useRef<number | null>(null)
|
|
33
|
+
const dragStartHeight = useRef(0)
|
|
34
|
+
const hasDragged = useRef(false)
|
|
35
|
+
const animationRef = useRef<ReturnType<typeof animate> | null>(null)
|
|
36
|
+
|
|
37
|
+
const clamp = useCallback(
|
|
38
|
+
(px: number) => {
|
|
39
|
+
const min = Math.min(...snapPointsPx)
|
|
40
|
+
const max = Math.max(...snapPointsPx)
|
|
41
|
+
return Math.max(min, Math.min(max, px))
|
|
42
|
+
},
|
|
43
|
+
[snapPointsPx],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const nearestSnap = useCallback(
|
|
47
|
+
(px: number) => {
|
|
48
|
+
let best = snapPointsPx[0] ?? 0
|
|
49
|
+
let bestDist = Number.POSITIVE_INFINITY
|
|
50
|
+
for (const p of snapPointsPx) {
|
|
51
|
+
const d = Math.abs(p - px)
|
|
52
|
+
if (d < bestDist) {
|
|
53
|
+
bestDist = d
|
|
54
|
+
best = p
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return best
|
|
58
|
+
},
|
|
59
|
+
[snapPointsPx],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const animateTo = useCallback(
|
|
63
|
+
(targetPx: number) => {
|
|
64
|
+
animationRef.current?.stop()
|
|
65
|
+
const controls = animate(height, targetPx, {
|
|
66
|
+
type: 'spring',
|
|
67
|
+
stiffness: 320,
|
|
68
|
+
damping: 32,
|
|
69
|
+
mass: 0.8,
|
|
70
|
+
onComplete: () => {
|
|
71
|
+
onCommit(targetPx)
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
animationRef.current = controls
|
|
75
|
+
},
|
|
76
|
+
[height, onCommit],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
useImperativeHandle(
|
|
80
|
+
ref,
|
|
81
|
+
() => ({
|
|
82
|
+
snapTo: (px: number) => animateTo(clamp(px)),
|
|
83
|
+
getHeight: () => height.get(),
|
|
84
|
+
}),
|
|
85
|
+
[animateTo, clamp, height],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const handlePointerDown = useCallback(
|
|
89
|
+
(e: React.PointerEvent) => {
|
|
90
|
+
if (e.button !== 0 && e.pointerType === 'mouse') return
|
|
91
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
92
|
+
animationRef.current?.stop()
|
|
93
|
+
dragStartY.current = e.clientY
|
|
94
|
+
dragStartHeight.current = height.get()
|
|
95
|
+
hasDragged.current = false
|
|
96
|
+
},
|
|
97
|
+
[height],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const handlePointerMove = useCallback(
|
|
101
|
+
(e: React.PointerEvent) => {
|
|
102
|
+
if (dragStartY.current === null) return
|
|
103
|
+
const dy = e.clientY - dragStartY.current
|
|
104
|
+
if (!hasDragged.current && Math.abs(dy) < DRAG_THRESHOLD_PX) return
|
|
105
|
+
hasDragged.current = true
|
|
106
|
+
const next = clamp(dragStartHeight.current - dy)
|
|
107
|
+
height.set(next)
|
|
108
|
+
},
|
|
109
|
+
[clamp, height],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const endDrag = useCallback(
|
|
113
|
+
(e: React.PointerEvent) => {
|
|
114
|
+
if (dragStartY.current === null) return
|
|
115
|
+
dragStartY.current = null
|
|
116
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
117
|
+
e.currentTarget.releasePointerCapture(e.pointerId)
|
|
118
|
+
}
|
|
119
|
+
if (!hasDragged.current) return
|
|
120
|
+
const target = nearestSnap(height.get())
|
|
121
|
+
animateTo(target)
|
|
122
|
+
},
|
|
123
|
+
[animateTo, height, nearestSnap],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
return () => {
|
|
128
|
+
animationRef.current?.stop()
|
|
129
|
+
}
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<motion.div
|
|
134
|
+
className="absolute right-0 bottom-0 left-0 z-40 flex flex-col overflow-hidden rounded-t-2xl bg-sidebar text-sidebar-foreground shadow-[0_-4px_16px_rgba(0,0,0,0.12)]"
|
|
135
|
+
style={{ height }}
|
|
136
|
+
>
|
|
137
|
+
<div
|
|
138
|
+
className="flex h-6 shrink-0 cursor-grab touch-none items-center justify-center active:cursor-grabbing"
|
|
139
|
+
onPointerCancel={endDrag}
|
|
140
|
+
onPointerDown={handlePointerDown}
|
|
141
|
+
onPointerMove={handlePointerMove}
|
|
142
|
+
onPointerUp={endDrag}
|
|
143
|
+
>
|
|
144
|
+
<div className="h-1 w-10 rounded-full bg-muted-foreground/40" />
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">{children}</div>
|
|
147
|
+
</motion.div>
|
|
148
|
+
)
|
|
149
|
+
})
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type CameraControlEvent,
|
|
5
|
+
type CameraControlFitSceneEvent,
|
|
6
|
+
emitter,
|
|
7
|
+
sceneRegistry,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
4
10
|
import { useViewer, WalkthroughControls, ZONE_LAYER } from '@pascal-app/viewer'
|
|
5
11
|
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
|
|
6
12
|
import { useThree } from '@react-three/fiber'
|
|
@@ -22,7 +28,7 @@ const DEBUG_MAX_POLAR_ANGLE = Math.PI - 0.05
|
|
|
22
28
|
export const CustomCameraControls = () => {
|
|
23
29
|
const controls = useRef<CameraControlsImpl>(null!)
|
|
24
30
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
25
|
-
const
|
|
31
|
+
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
26
32
|
const allowUndergroundCamera = useEditor((s) => s.allowUndergroundCamera)
|
|
27
33
|
const selection = useViewer((s) => s.selection)
|
|
28
34
|
const currentLevelId = selection.levelId
|
|
@@ -112,6 +118,49 @@ export const CustomCameraControls = () => {
|
|
|
112
118
|
}
|
|
113
119
|
}, [cameraMode, isPreviewMode])
|
|
114
120
|
|
|
121
|
+
// Touch gestures (mobile / trackpad).
|
|
122
|
+
// - One finger drag → rotate by default (much easier on a phone), but
|
|
123
|
+
// falls back to NONE while the user is actively
|
|
124
|
+
// placing/moving something OR in box-select mode,
|
|
125
|
+
// so the editor's pointer handlers (place tool,
|
|
126
|
+
// drag-to-move endpoint, marquee selection drag)
|
|
127
|
+
// keep priority over the camera.
|
|
128
|
+
// In preview mode it's TOUCH_TRUCK (pan), matching
|
|
129
|
+
// preview's left = SCREEN_PAN.
|
|
130
|
+
// - Two finger pinch → zoom + pan together (TOUCH_DOLLY_TRUCK for
|
|
131
|
+
// perspective, TOUCH_ZOOM_TRUCK for orthographic).
|
|
132
|
+
// - Three finger drag → rotate, so the camera is always orbitable even
|
|
133
|
+
// when one-finger is suppressed by an active
|
|
134
|
+
// editor action.
|
|
135
|
+
const tool = useEditor((s) => s.tool)
|
|
136
|
+
const mode = useEditor((s) => s.mode)
|
|
137
|
+
const selectionTool = useEditor((s) => s.floorplanSelectionTool)
|
|
138
|
+
const movingNode = useEditor((s) => s.movingNode)
|
|
139
|
+
const movingWallEndpoint = useEditor((s) => s.movingWallEndpoint)
|
|
140
|
+
const movingFenceEndpoint = useEditor((s) => s.movingFenceEndpoint)
|
|
141
|
+
const isBoxSelectActive = mode === 'select' && selectionTool === 'marquee'
|
|
142
|
+
const isInteracting = Boolean(
|
|
143
|
+
tool || movingNode || movingWallEndpoint || movingFenceEndpoint || isBoxSelectActive,
|
|
144
|
+
)
|
|
145
|
+
const touches = useMemo(() => {
|
|
146
|
+
const twoFingerAction =
|
|
147
|
+
cameraMode === 'orthographic'
|
|
148
|
+
? CameraControlsImpl.ACTION.TOUCH_ZOOM_TRUCK
|
|
149
|
+
: CameraControlsImpl.ACTION.TOUCH_DOLLY_TRUCK
|
|
150
|
+
|
|
151
|
+
const oneFingerAction = isPreviewMode
|
|
152
|
+
? CameraControlsImpl.ACTION.TOUCH_TRUCK
|
|
153
|
+
: isInteracting
|
|
154
|
+
? CameraControlsImpl.ACTION.NONE
|
|
155
|
+
: CameraControlsImpl.ACTION.TOUCH_ROTATE
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
one: oneFingerAction,
|
|
159
|
+
two: twoFingerAction,
|
|
160
|
+
three: CameraControlsImpl.ACTION.TOUCH_ROTATE,
|
|
161
|
+
}
|
|
162
|
+
}, [cameraMode, isPreviewMode, isInteracting])
|
|
163
|
+
|
|
115
164
|
useEffect(() => {
|
|
116
165
|
const keyState = {
|
|
117
166
|
shiftRight: false,
|
|
@@ -340,12 +389,30 @@ export const CustomCameraControls = () => {
|
|
|
340
389
|
focusNode(nodeId)
|
|
341
390
|
}
|
|
342
391
|
|
|
392
|
+
const handleFitScene = ({ bounds }: CameraControlFitSceneEvent) => {
|
|
393
|
+
if (!controls.current || isPreviewMode) return
|
|
394
|
+
if (!bounds) {
|
|
395
|
+
// Restore default framing pose when no bounds were computed.
|
|
396
|
+
controls.current.setLookAt(20, 20, 20, 0, 0, 0, true)
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
const [cx, cz] = bounds.center
|
|
400
|
+
const [w, d] = bounds.size
|
|
401
|
+
// Use the longer horizontal extent to size the orbit radius so the whole
|
|
402
|
+
// footprint sits in view regardless of aspect ratio.
|
|
403
|
+
const maxExtent = Math.max(w, d)
|
|
404
|
+
const distance = Math.max(maxExtent * 1.4, 15)
|
|
405
|
+
const height = Math.max(maxExtent * 0.8, 10)
|
|
406
|
+
controls.current.setLookAt(cx + distance * 0.7, height, cz + distance * 0.7, cx, 0, cz, true)
|
|
407
|
+
}
|
|
408
|
+
|
|
343
409
|
emitter.on('camera-controls:capture', handleNodeCapture)
|
|
344
410
|
emitter.on('camera-controls:focus', handleNodeFocus)
|
|
345
411
|
emitter.on('camera-controls:view', handleNodeView)
|
|
346
412
|
emitter.on('camera-controls:top-view', handleTopView)
|
|
347
413
|
emitter.on('camera-controls:orbit-cw', handleOrbitCW)
|
|
348
414
|
emitter.on('camera-controls:orbit-ccw', handleOrbitCCW)
|
|
415
|
+
emitter.on('camera-controls:fit-scene', handleFitScene)
|
|
349
416
|
|
|
350
417
|
return () => {
|
|
351
418
|
emitter.off('camera-controls:capture', handleNodeCapture)
|
|
@@ -354,8 +421,9 @@ export const CustomCameraControls = () => {
|
|
|
354
421
|
emitter.off('camera-controls:top-view', handleTopView)
|
|
355
422
|
emitter.off('camera-controls:orbit-cw', handleOrbitCW)
|
|
356
423
|
emitter.off('camera-controls:orbit-ccw', handleOrbitCCW)
|
|
424
|
+
emitter.off('camera-controls:fit-scene', handleFitScene)
|
|
357
425
|
}
|
|
358
|
-
}, [focusNode])
|
|
426
|
+
}, [focusNode, isPreviewMode])
|
|
359
427
|
|
|
360
428
|
const onTransitionStart = useCallback(() => {
|
|
361
429
|
useViewer.getState().setCameraDragging(true)
|
|
@@ -365,8 +433,8 @@ export const CustomCameraControls = () => {
|
|
|
365
433
|
useViewer.getState().setCameraDragging(false)
|
|
366
434
|
}, [])
|
|
367
435
|
|
|
368
|
-
if (
|
|
369
|
-
return
|
|
436
|
+
if (isFirstPersonMode) {
|
|
437
|
+
return null
|
|
370
438
|
}
|
|
371
439
|
|
|
372
440
|
return (
|
|
@@ -382,6 +450,7 @@ export const CustomCameraControls = () => {
|
|
|
382
450
|
onTransitionStart={onTransitionStart}
|
|
383
451
|
ref={controls}
|
|
384
452
|
restThreshold={0.01}
|
|
453
|
+
touches={touches}
|
|
385
454
|
/>
|
|
386
455
|
)
|
|
387
456
|
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
5
|
+
import useEditor from '../../store/use-editor'
|
|
6
|
+
import { MobileTabBar } from '../ui/sidebar/mobile-tab-bar'
|
|
7
|
+
import type { SidebarTab } from '../ui/sidebar/tab-bar'
|
|
8
|
+
import { BottomSheet, type BottomSheetHandle } from './bottom-sheet'
|
|
9
|
+
|
|
10
|
+
const MIN_SNAP = 0
|
|
11
|
+
const MAX_SNAP = 1
|
|
12
|
+
const DEFAULT_SNAP = 0.5
|
|
13
|
+
// Viewer extends this many pixels behind the sheet's rounded top corners
|
|
14
|
+
// so the curve reveals viewer content underneath.
|
|
15
|
+
const SHEET_OVERLAP_PX = 16
|
|
16
|
+
// Sheet never collapses below the drag handle so the user can always grab it.
|
|
17
|
+
const SHEET_HANDLE_PX = 24
|
|
18
|
+
|
|
19
|
+
// Match the viewer's scene background colors (packages/viewer/src/components/viewer/index.tsx)
|
|
20
|
+
const VIEWER_BG_DARK = '#1f2433'
|
|
21
|
+
const VIEWER_BG_LIGHT = '#ffffff'
|
|
22
|
+
|
|
23
|
+
// Fixed set of intermediate snap heights (handle + middleH are added on top).
|
|
24
|
+
// Per-tab `mobileDefaultSnap` decides the OPENING height; this list bounds
|
|
25
|
+
// what the user can drag to.
|
|
26
|
+
const SNAP_RATIOS = [0.5, 0.66] as const
|
|
27
|
+
|
|
28
|
+
function getDefaultSnap(tab: SidebarTab | undefined): number {
|
|
29
|
+
const s = tab?.mobileDefaultSnap
|
|
30
|
+
if (typeof s !== 'number') return DEFAULT_SNAP
|
|
31
|
+
return Math.max(MIN_SNAP, Math.min(MAX_SNAP, s))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface EditorLayoutMobileProps {
|
|
35
|
+
navbarSlot?: ReactNode
|
|
36
|
+
sidebarTabs?: SidebarTab[]
|
|
37
|
+
renderTabContent: (tabId: string) => ReactNode
|
|
38
|
+
sidebarOverlay?: ReactNode
|
|
39
|
+
viewerToolbarLeft?: ReactNode
|
|
40
|
+
viewerToolbarRight?: ReactNode
|
|
41
|
+
viewerContent: ReactNode
|
|
42
|
+
overlays?: ReactNode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function EditorLayoutMobile({
|
|
46
|
+
navbarSlot,
|
|
47
|
+
sidebarTabs = [],
|
|
48
|
+
renderTabContent,
|
|
49
|
+
sidebarOverlay,
|
|
50
|
+
viewerToolbarLeft,
|
|
51
|
+
viewerToolbarRight,
|
|
52
|
+
viewerContent,
|
|
53
|
+
overlays,
|
|
54
|
+
}: EditorLayoutMobileProps) {
|
|
55
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
56
|
+
const activePanel = useEditor((s) => s.activeSidebarPanel)
|
|
57
|
+
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
|
|
58
|
+
const panelSheetHeight = useEditor((s) => s.mobilePanelSheetHeight)
|
|
59
|
+
const theme = useViewer((s) => s.theme)
|
|
60
|
+
const viewerBg = theme === 'light' ? VIEWER_BG_LIGHT : VIEWER_BG_DARK
|
|
61
|
+
|
|
62
|
+
const middleRef = useRef<HTMLDivElement>(null)
|
|
63
|
+
const sheetRef = useRef<BottomSheetHandle>(null)
|
|
64
|
+
const [middleH, setMiddleH] = useState(0)
|
|
65
|
+
// Distance from the middle area's bottom edge to the viewport's bottom edge
|
|
66
|
+
// (i.e. the tab bar height incl. safe area). Needed to translate the panel
|
|
67
|
+
// sheet's viewport-relative height into middle-area coordinates.
|
|
68
|
+
const [middleBottomFromViewport, setMiddleBottomFromViewport] = useState(0)
|
|
69
|
+
const [committedSheetH, setCommittedSheetH] = useState(0)
|
|
70
|
+
|
|
71
|
+
const currentTab = sidebarTabs.find((t) => t.id === activePanel)
|
|
72
|
+
|
|
73
|
+
// Keep active panel valid
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (sidebarTabs.length > 0 && !sidebarTabs.some((t) => t.id === activePanel)) {
|
|
76
|
+
setActivePanel(sidebarTabs[0]!.id)
|
|
77
|
+
}
|
|
78
|
+
}, [sidebarTabs, activePanel, setActivePanel])
|
|
79
|
+
|
|
80
|
+
// Sync editor phase / mode with the active tab:
|
|
81
|
+
// - Entering Chat always drops to Select (chat is a composing context).
|
|
82
|
+
// - Entering Items snaps the editor into furnish-build (matches the
|
|
83
|
+
// desktop "Furnish" action which itself opens the Items panel).
|
|
84
|
+
// - Leaving Items while still furnishing exits the build mode.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const { phase, mode, setMode, setPhase } = useEditor.getState()
|
|
87
|
+
if (activePanel === 'ai' && mode === 'build') {
|
|
88
|
+
setMode('select')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
if (activePanel === 'items') {
|
|
92
|
+
if (phase !== 'furnish') setPhase('furnish')
|
|
93
|
+
if (mode !== 'build') setMode('build')
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (phase === 'furnish' && mode === 'build') {
|
|
97
|
+
setMode('select')
|
|
98
|
+
}
|
|
99
|
+
}, [activePanel])
|
|
100
|
+
|
|
101
|
+
// Measure middle area height + its bottom offset from viewport bottom
|
|
102
|
+
useLayoutEffect(() => {
|
|
103
|
+
const el = middleRef.current
|
|
104
|
+
if (!el) return
|
|
105
|
+
const measure = () => {
|
|
106
|
+
const rect = el.getBoundingClientRect()
|
|
107
|
+
setMiddleH(rect.height)
|
|
108
|
+
setMiddleBottomFromViewport(Math.max(0, window.innerHeight - rect.bottom))
|
|
109
|
+
}
|
|
110
|
+
const ro = new ResizeObserver(measure)
|
|
111
|
+
ro.observe(el)
|
|
112
|
+
measure()
|
|
113
|
+
window.addEventListener('resize', measure)
|
|
114
|
+
return () => {
|
|
115
|
+
ro.disconnect()
|
|
116
|
+
window.removeEventListener('resize', measure)
|
|
117
|
+
}
|
|
118
|
+
}, [])
|
|
119
|
+
|
|
120
|
+
// Initialise sheet to current tab default once we know the middle height
|
|
121
|
+
const didInit = useRef(false)
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (didInit.current || middleH <= 0) return
|
|
124
|
+
didInit.current = true
|
|
125
|
+
const targetPx = getDefaultSnap(currentTab) * middleH
|
|
126
|
+
setCommittedSheetH(targetPx)
|
|
127
|
+
sheetRef.current?.snapTo(targetPx)
|
|
128
|
+
}, [middleH, currentTab])
|
|
129
|
+
|
|
130
|
+
// When middle height changes (rotation / resize), keep sheet in proportion
|
|
131
|
+
const prevMiddleH = useRef(0)
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (middleH <= 0) return
|
|
134
|
+
if (prevMiddleH.current === 0) {
|
|
135
|
+
prevMiddleH.current = middleH
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
if (prevMiddleH.current === middleH) return
|
|
139
|
+
const ratio = committedSheetH / prevMiddleH.current
|
|
140
|
+
const nextPx = Math.max(SHEET_HANDLE_PX, Math.min(middleH, ratio * middleH))
|
|
141
|
+
prevMiddleH.current = middleH
|
|
142
|
+
setCommittedSheetH(nextPx)
|
|
143
|
+
sheetRef.current?.snapTo(nextPx)
|
|
144
|
+
}, [middleH, committedSheetH])
|
|
145
|
+
|
|
146
|
+
const handleTabPress = useCallback(
|
|
147
|
+
(id: string) => {
|
|
148
|
+
if (middleH <= 0) return
|
|
149
|
+
const tab = sidebarTabs.find((t) => t.id === id)
|
|
150
|
+
if (!tab) return
|
|
151
|
+
const defaultPx = getDefaultSnap(tab) * middleH
|
|
152
|
+
if (id !== activePanel) {
|
|
153
|
+
setActivePanel(id)
|
|
154
|
+
sheetRef.current?.snapTo(defaultPx)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
// Same tab tapped — toggle
|
|
158
|
+
const current = sheetRef.current?.getHeight() ?? committedSheetH
|
|
159
|
+
const expandedThreshold = Math.max(SHEET_HANDLE_PX, defaultPx * 0.5)
|
|
160
|
+
if (current > expandedThreshold) {
|
|
161
|
+
sheetRef.current?.snapTo(SHEET_HANDLE_PX)
|
|
162
|
+
} else {
|
|
163
|
+
sheetRef.current?.snapTo(defaultPx)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
[sidebarTabs, activePanel, setActivePanel, middleH, committedSheetH],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const snapPointsPx = (() => {
|
|
170
|
+
if (middleH <= 0) return [SHEET_HANDLE_PX]
|
|
171
|
+
const intermediate = SNAP_RATIOS.map((r) => r * middleH)
|
|
172
|
+
return Array.from(new Set([SHEET_HANDLE_PX, ...intermediate, middleH])).sort((a, b) => a - b)
|
|
173
|
+
})()
|
|
174
|
+
|
|
175
|
+
// When the secondary panel sheet is open, it covers the tab bar + part of
|
|
176
|
+
// the middle area; translate its viewport height into middle-area units.
|
|
177
|
+
const panelPenetrationInMiddle = Math.max(0, panelSheetHeight - middleBottomFromViewport)
|
|
178
|
+
// The effective "sheet height" that the viewer sits above is the larger of
|
|
179
|
+
// the primary sidebar sheet and the secondary panel sheet's penetration.
|
|
180
|
+
const effectiveSheetH = Math.max(committedSheetH, panelPenetrationInMiddle)
|
|
181
|
+
|
|
182
|
+
// In capture mode the sheet and tab bar are hidden — the viewer should fill
|
|
183
|
+
// the entire middle area regardless of the stored sheet height.
|
|
184
|
+
// Otherwise, the viewer extends SHEET_OVERLAP_PX behind the sheet's rounded
|
|
185
|
+
// corners so the curve reveals viewer content underneath.
|
|
186
|
+
const baseViewerHeight = Math.max(0, middleH - effectiveSheetH)
|
|
187
|
+
const viewerHeight = isCaptureMode
|
|
188
|
+
? middleH
|
|
189
|
+
: baseViewerHeight === 0
|
|
190
|
+
? 0
|
|
191
|
+
: Math.min(middleH, baseViewerHeight + SHEET_OVERLAP_PX)
|
|
192
|
+
|
|
193
|
+
// While the panel sheet is open, collapse the primary sheet to its handle so
|
|
194
|
+
// it doesn't peek above. Remember the previous height and restore it on close.
|
|
195
|
+
const sheetHeightBeforePanel = useRef<number | null>(null)
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (panelSheetHeight > 0) {
|
|
198
|
+
if (sheetHeightBeforePanel.current === null && committedSheetH > SHEET_HANDLE_PX) {
|
|
199
|
+
sheetHeightBeforePanel.current = committedSheetH
|
|
200
|
+
sheetRef.current?.snapTo(SHEET_HANDLE_PX)
|
|
201
|
+
}
|
|
202
|
+
} else if (sheetHeightBeforePanel.current !== null) {
|
|
203
|
+
const target = sheetHeightBeforePanel.current
|
|
204
|
+
sheetHeightBeforePanel.current = null
|
|
205
|
+
sheetRef.current?.snapTo(target)
|
|
206
|
+
}
|
|
207
|
+
}, [panelSheetHeight, committedSheetH])
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className="dark flex h-full w-full flex-col bg-sidebar text-foreground">
|
|
211
|
+
{navbarSlot}
|
|
212
|
+
|
|
213
|
+
<div
|
|
214
|
+
className="relative flex min-h-0 flex-1"
|
|
215
|
+
ref={middleRef}
|
|
216
|
+
style={{ backgroundColor: viewerBg }}
|
|
217
|
+
>
|
|
218
|
+
{/* Viewer column: sized by committed sheet height */}
|
|
219
|
+
<div className="absolute inset-x-0 top-0 overflow-hidden" style={{ height: viewerHeight }}>
|
|
220
|
+
<div className="relative h-full w-full">
|
|
221
|
+
{(viewerToolbarLeft || viewerToolbarRight) && !isCaptureMode && (
|
|
222
|
+
<div className="pointer-events-none absolute top-3 right-3 left-3 z-20 flex items-center justify-between gap-2">
|
|
223
|
+
<div className="pointer-events-auto flex items-center gap-2">
|
|
224
|
+
{viewerToolbarLeft}
|
|
225
|
+
</div>
|
|
226
|
+
<div className="pointer-events-auto flex items-center gap-2">
|
|
227
|
+
{viewerToolbarRight}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
<div className="relative h-full w-full overflow-hidden">{viewerContent}</div>
|
|
232
|
+
{overlays && (
|
|
233
|
+
<div
|
|
234
|
+
className="pointer-events-none absolute inset-0 z-30"
|
|
235
|
+
style={{ transform: 'translateZ(0)' }}
|
|
236
|
+
>
|
|
237
|
+
{overlays}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{/* Bottom sheet: overlays the lower part of the middle area */}
|
|
244
|
+
{!isCaptureMode && sidebarTabs.length > 0 && (
|
|
245
|
+
<BottomSheet
|
|
246
|
+
initialHeightPx={SHEET_HANDLE_PX}
|
|
247
|
+
onCommit={setCommittedSheetH}
|
|
248
|
+
ref={sheetRef}
|
|
249
|
+
snapPointsPx={snapPointsPx}
|
|
250
|
+
>
|
|
251
|
+
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
252
|
+
{renderTabContent(activePanel)}
|
|
253
|
+
{sidebarOverlay && <div className="absolute inset-0 z-50">{sidebarOverlay}</div>}
|
|
254
|
+
</div>
|
|
255
|
+
</BottomSheet>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{!isCaptureMode && sidebarTabs.length > 0 && (
|
|
260
|
+
<MobileTabBar activeTab={activePanel} onTabPress={handleTabPress} tabs={sidebarTabs} />
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { type ReactNode, useCallback, useEffect, useRef } from 'react'
|
|
4
|
+
import { useIsMobile } from '../../hooks/use-mobile'
|
|
4
5
|
import useEditor from '../../store/use-editor'
|
|
6
|
+
|
|
5
7
|
import { useSidebarStore } from '../ui/primitives/sidebar'
|
|
6
8
|
import { type SidebarTab, TabBar } from '../ui/sidebar/tab-bar'
|
|
9
|
+
import { EditorLayoutMobile } from './editor-layout-mobile'
|
|
7
10
|
|
|
8
11
|
const SIDEBAR_MIN_WIDTH = 300
|
|
9
12
|
const SIDEBAR_MAX_WIDTH = 800
|
|
@@ -202,6 +205,24 @@ export function EditorLayoutV2({
|
|
|
202
205
|
viewerContent,
|
|
203
206
|
overlays,
|
|
204
207
|
}: EditorLayoutV2Props) {
|
|
208
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
209
|
+
const isMobile = useIsMobile()
|
|
210
|
+
|
|
211
|
+
if (isMobile) {
|
|
212
|
+
return (
|
|
213
|
+
<EditorLayoutMobile
|
|
214
|
+
navbarSlot={navbarSlot}
|
|
215
|
+
overlays={overlays}
|
|
216
|
+
renderTabContent={renderTabContent}
|
|
217
|
+
sidebarOverlay={sidebarOverlay}
|
|
218
|
+
sidebarTabs={sidebarTabs}
|
|
219
|
+
viewerContent={viewerContent}
|
|
220
|
+
viewerToolbarLeft={viewerToolbarLeft}
|
|
221
|
+
viewerToolbarRight={viewerToolbarRight}
|
|
222
|
+
/>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
205
226
|
return (
|
|
206
227
|
<div className="dark flex h-full w-full flex-col bg-sidebar text-foreground">
|
|
207
228
|
{/* Top navbar */}
|
|
@@ -209,7 +230,7 @@ export function EditorLayoutV2({
|
|
|
209
230
|
|
|
210
231
|
{/* Main content: left column + right column */}
|
|
211
232
|
<div className="flex min-h-0 flex-1">
|
|
212
|
-
{sidebarTabs.length > 0 && (
|
|
233
|
+
{!isCaptureMode && sidebarTabs.length > 0 && (
|
|
213
234
|
<LeftColumn
|
|
214
235
|
renderTabContent={renderTabContent}
|
|
215
236
|
sidebarOverlay={sidebarOverlay}
|
|
@@ -218,8 +239,8 @@ export function EditorLayoutV2({
|
|
|
218
239
|
)}
|
|
219
240
|
<RightColumn
|
|
220
241
|
overlays={overlays}
|
|
221
|
-
toolbarLeft={viewerToolbarLeft}
|
|
222
|
-
toolbarRight={viewerToolbarRight}
|
|
242
|
+
toolbarLeft={isCaptureMode ? undefined : viewerToolbarLeft}
|
|
243
|
+
toolbarRight={isCaptureMode ? undefined : viewerToolbarRight}
|
|
223
244
|
>
|
|
224
245
|
{viewerContent}
|
|
225
246
|
</RightColumn>
|