@pascal-app/editor 1.0.0-beta.4 → 1.0.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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +63 -8
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +23 -6
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/panel-wrapper.tsx +228 -12
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { emitter } from '@pascal-app/core'
|
|
4
|
-
import {
|
|
3
|
+
import { emitter, type SnapshotSavedEvent } from '@pascal-app/core'
|
|
4
|
+
import { SNAPSHOT_MAX_EDGE } from '@pascal-app/viewer'
|
|
5
|
+
import {
|
|
6
|
+
Check,
|
|
7
|
+
Crop,
|
|
8
|
+
Drone,
|
|
9
|
+
Footprints,
|
|
10
|
+
Loader2,
|
|
11
|
+
Maximize2,
|
|
12
|
+
Monitor,
|
|
13
|
+
Orbit,
|
|
14
|
+
RotateCcw,
|
|
15
|
+
X,
|
|
16
|
+
} from 'lucide-react'
|
|
5
17
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
18
|
+
import { flushSync } from 'react-dom'
|
|
6
19
|
import { useIsMobile } from '../../hooks/use-mobile'
|
|
7
20
|
import { triggerSFX } from '../../lib/sfx-bus'
|
|
21
|
+
import { requestWalkthroughPointerLock } from '../../lib/walkthrough-pointer-lock'
|
|
8
22
|
import useEditor, {
|
|
23
|
+
CAPTURE_FOV_MAX,
|
|
24
|
+
CAPTURE_FOV_MIN,
|
|
25
|
+
type FirstPersonMovementMode,
|
|
9
26
|
type SnapshotCropMode,
|
|
10
27
|
type SnapshotStandardAspect,
|
|
11
28
|
} from '../../store/use-editor'
|
|
29
|
+
import { useFirstPersonHud } from '../../store/use-first-person-hud'
|
|
30
|
+
import { Slider } from '../ui/slider'
|
|
31
|
+
import { WalkthroughCrosshair } from '../walkthrough-hud'
|
|
32
|
+
import { isOverlaySnapshotSave } from './snapshot-capture'
|
|
12
33
|
|
|
13
34
|
// Local alias — distinct from `useEditor.captureMode` (which describes *why*
|
|
14
35
|
// a capture is happening, e.g. `preset`). This one says HOW the captured
|
|
@@ -16,6 +37,9 @@ import useEditor, {
|
|
|
16
37
|
// user-dragged area. Hosts can preselect it via `captureMode.crop`.
|
|
17
38
|
type CropMode = SnapshotCropMode
|
|
18
39
|
type CaptureState = 'idle' | 'capturing' | 'saved'
|
|
40
|
+
/** Which camera the shot is framed with: the editor's orbit camera, or one of
|
|
41
|
+
* the two first-person controllers. */
|
|
42
|
+
type CaptureCameraNav = 'orbit' | FirstPersonMovementMode
|
|
19
43
|
|
|
20
44
|
interface DragPoint {
|
|
21
45
|
x: number
|
|
@@ -37,6 +61,14 @@ const STANDARD_SIZES: Record<SnapshotStandardAspect, { w: number; h: number }> =
|
|
|
37
61
|
}
|
|
38
62
|
type StandardAspect = SnapshotStandardAspect
|
|
39
63
|
|
|
64
|
+
function clampSnapshotSize(width: number, height: number): { w: number; h: number } {
|
|
65
|
+
const maxEdge = Math.max(width, height)
|
|
66
|
+
if (maxEdge <= SNAPSHOT_MAX_EDGE) return { w: width, h: height }
|
|
67
|
+
|
|
68
|
+
const scale = SNAPSHOT_MAX_EDGE / maxEdge
|
|
69
|
+
return { w: Math.round(width * scale), h: Math.round(height * scale) }
|
|
70
|
+
}
|
|
71
|
+
|
|
40
72
|
function getResolution(
|
|
41
73
|
mode: CropMode,
|
|
42
74
|
overlayEl: HTMLDivElement | null,
|
|
@@ -50,14 +82,14 @@ function getResolution(
|
|
|
50
82
|
const dpr = Math.min(window.devicePixelRatio, 1.5)
|
|
51
83
|
|
|
52
84
|
if (mode === 'viewport') {
|
|
53
|
-
return
|
|
85
|
+
return clampSnapshotSize(Math.round(rect.width * dpr), Math.round(rect.height * dpr))
|
|
54
86
|
}
|
|
55
87
|
|
|
56
88
|
if (mode === 'area' && drag) {
|
|
57
89
|
const w = Math.abs(drag.end.x - drag.start.x)
|
|
58
90
|
const h = Math.abs(drag.end.y - drag.start.y)
|
|
59
91
|
if (w < 4 || h < 4) return null
|
|
60
|
-
return
|
|
92
|
+
return clampSnapshotSize(Math.round(w * dpr), Math.round(h * dpr))
|
|
61
93
|
}
|
|
62
94
|
|
|
63
95
|
return null
|
|
@@ -90,7 +122,7 @@ function CornerAccents() {
|
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
const HUD_CHIP_CLASS =
|
|
93
|
-
'flex flex-col gap-px rounded-lg border border-white/10 bg-neutral-950/85 px-3 py-1.5
|
|
125
|
+
'flex flex-col gap-px rounded-lg border border-white/10 bg-neutral-950/85 px-3 py-1.5'
|
|
94
126
|
|
|
95
127
|
const CROP_LABELS: Record<CropMode, string> = {
|
|
96
128
|
standard: 'Standard',
|
|
@@ -98,6 +130,42 @@ const CROP_LABELS: Record<CropMode, string> = {
|
|
|
98
130
|
area: 'Area',
|
|
99
131
|
}
|
|
100
132
|
|
|
133
|
+
// Dark-HUD skin for the shared slider, which is themed for the light editor chrome.
|
|
134
|
+
const FOV_SLIDER_CLASS =
|
|
135
|
+
'w-24 [&_[data-slot=slider-track]]:h-1 [&_[data-slot=slider-track]]:bg-white/20 [&_[data-slot=slider-range]]:bg-white [&_[data-slot=slider-thumb]]:size-3 [&_[data-slot=slider-thumb]]:border-white/50 [&_[data-slot=slider-thumb]]:bg-white [&_[data-slot=slider-thumb]]:ring-white/30'
|
|
136
|
+
|
|
137
|
+
type CameraNavHint = {
|
|
138
|
+
action: string
|
|
139
|
+
keys: readonly string[]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function CaptureWalkthroughCrosshair() {
|
|
143
|
+
const interact = useFirstPersonHud((state) => state.interact)
|
|
144
|
+
return <WalkthroughCrosshair interact={interact} />
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const CAMERA_NAV_HINTS: Record<CaptureCameraNav, readonly CameraNavHint[] | null> = {
|
|
148
|
+
orbit: null,
|
|
149
|
+
walk: [
|
|
150
|
+
{ keys: ['WASD'], action: 'move' },
|
|
151
|
+
{ keys: ['Space'], action: 'jump' },
|
|
152
|
+
{ keys: ['E'], action: 'open' },
|
|
153
|
+
{ keys: ['Wheel'], action: 'lens' },
|
|
154
|
+
{ keys: ['P', 'Esc'], action: 'free cursor' },
|
|
155
|
+
{ keys: ['Click', 'Enter'], action: 'shoot' },
|
|
156
|
+
],
|
|
157
|
+
drone: [
|
|
158
|
+
{ keys: ['WASD'], action: 'move' },
|
|
159
|
+
{ keys: ['Space', 'E'], action: 'up' },
|
|
160
|
+
{ keys: ['Q'], action: 'down' },
|
|
161
|
+
{ keys: ['Shift'], action: 'boost' },
|
|
162
|
+
{ keys: ['Alt'], action: 'slow' },
|
|
163
|
+
{ keys: ['Wheel'], action: 'lens' },
|
|
164
|
+
{ keys: ['P', 'Esc'], action: 'free cursor' },
|
|
165
|
+
{ keys: ['Click', 'Enter'], action: 'shoot' },
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
|
|
101
169
|
export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
102
170
|
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
103
171
|
const captureMode = useEditor((s) => s.captureMode)
|
|
@@ -109,9 +177,44 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
109
177
|
const isPreset = captureMode.mode === 'preset'
|
|
110
178
|
const requestedCrop = captureMode.mode === 'standard' ? captureMode.crop : undefined
|
|
111
179
|
const requestedAspect = captureMode.mode === 'standard' ? captureMode.standardAspect : undefined
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
180
|
+
// Only an explicit host lock hides the crop/aspect switcher (the publish
|
|
181
|
+
// cover needs its exact output shape). A plain preselected crop — the
|
|
182
|
+
// Studio capbar's choice — just seeds the pill and stays user-changeable.
|
|
183
|
+
const isCropLocked =
|
|
184
|
+
isPreset || (captureMode.mode === 'standard' && captureMode.lockCrop === true)
|
|
185
|
+
|
|
186
|
+
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
187
|
+
const firstPersonMovementMode = useEditor((s) => s.firstPersonMovementMode)
|
|
188
|
+
const captureFov = useEditor((s) => s.captureFov)
|
|
189
|
+
const captureFovBaseline = useEditor((s) => s.captureFovBaseline)
|
|
190
|
+
const setCaptureFov = useEditor((s) => s.setCaptureFov)
|
|
191
|
+
// Orbit is just "first person off", so deriving the segmented control's value
|
|
192
|
+
// from the walkthrough flag keeps the two from drifting when walkthrough ends
|
|
193
|
+
// on its own (Esc, or a lost pointer lock).
|
|
194
|
+
const cameraNav: CaptureCameraNav = isFirstPersonMode ? firstPersonMovementMode : 'orbit'
|
|
195
|
+
const setCameraNav = useCallback((next: CaptureCameraNav) => {
|
|
196
|
+
const editor = useEditor.getState()
|
|
197
|
+
if (next === 'orbit') {
|
|
198
|
+
editor.setFirstPersonMode(false)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
// Lock the pointer in the same click task (the gesture requirement):
|
|
202
|
+
// flush the mode flip so FirstPersonControls is mounted when the lock
|
|
203
|
+
// lands, instead of making the user click the canvas a second time.
|
|
204
|
+
flushSync(() => {
|
|
205
|
+
editor.setFirstPersonMovementMode(next)
|
|
206
|
+
if (!editor.isFirstPersonMode) editor.setFirstPersonMode(true)
|
|
207
|
+
})
|
|
208
|
+
requestWalkthroughPointerLock({
|
|
209
|
+
// Freeing the cursor in one camera and immediately picking the other
|
|
210
|
+
// hits the browser's re-lock cooldown; retry once it passes, as long as
|
|
211
|
+
// the user is still framing in a first-person camera.
|
|
212
|
+
retryWhile: () => {
|
|
213
|
+
const state = useEditor.getState()
|
|
214
|
+
return state.isCaptureMode && state.isFirstPersonMode
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
}, [])
|
|
115
218
|
|
|
116
219
|
const [mode, setMode] = useState<CropMode>('standard')
|
|
117
220
|
const [standardAspect, setStandardAspect] = useState<StandardAspect>('16:9')
|
|
@@ -134,16 +237,6 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
134
237
|
return () => observer.disconnect()
|
|
135
238
|
}, [isCaptureMode])
|
|
136
239
|
|
|
137
|
-
// Dismiss on Esc
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
if (!isCaptureMode) return
|
|
140
|
-
const onKey = (e: KeyboardEvent) => {
|
|
141
|
-
if (e.key === 'Escape') setCaptureMode(false)
|
|
142
|
-
}
|
|
143
|
-
window.addEventListener('keydown', onKey)
|
|
144
|
-
return () => window.removeEventListener('keydown', onKey)
|
|
145
|
-
}, [isCaptureMode, setCaptureMode])
|
|
146
|
-
|
|
147
240
|
// Reset local state when entering capture mode. Preset mode also
|
|
148
241
|
// auto-stages a centered square crop sized to ~75% of the shorter
|
|
149
242
|
// viewport dimension so the user can capture immediately — the
|
|
@@ -172,16 +265,31 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
172
265
|
|
|
173
266
|
// Listen for snapshot saved to show feedback then exit
|
|
174
267
|
useEffect(() => {
|
|
175
|
-
|
|
268
|
+
if (!isCaptureMode) return
|
|
269
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
270
|
+
const handler = (event: SnapshotSavedEvent | undefined) => {
|
|
271
|
+
if (!isOverlaySnapshotSave(event, projectId)) return
|
|
176
272
|
setCaptureState('saved')
|
|
177
|
-
|
|
273
|
+
clearTimeout(timer)
|
|
274
|
+
timer = setTimeout(() => {
|
|
178
275
|
setCaptureMode(false)
|
|
179
276
|
setCaptureState('idle')
|
|
180
277
|
}, 1500)
|
|
181
278
|
}
|
|
182
279
|
emitter.on('snapshot:saved', handler)
|
|
183
|
-
return () =>
|
|
184
|
-
|
|
280
|
+
return () => {
|
|
281
|
+
emitter.off('snapshot:saved', handler)
|
|
282
|
+
clearTimeout(timer)
|
|
283
|
+
}
|
|
284
|
+
}, [isCaptureMode, projectId, setCaptureMode])
|
|
285
|
+
|
|
286
|
+
// From the shutter firing until the saved toast clears, walk / drone hold
|
|
287
|
+
// still: a late WASD tap or mouse twitch must not shift the frame out from
|
|
288
|
+
// under the shot the user just took.
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
useEditor.getState().setCaptureShutterHold(isCaptureMode && captureState !== 'idle')
|
|
291
|
+
return () => useEditor.getState().setCaptureShutterHold(false)
|
|
292
|
+
}, [captureState, isCaptureMode])
|
|
185
293
|
|
|
186
294
|
const dismiss = useCallback(() => setCaptureMode(false), [setCaptureMode])
|
|
187
295
|
|
|
@@ -344,9 +452,82 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
344
452
|
})
|
|
345
453
|
}, [captureState, mode, drag, projectId, isPreset, standardAspect])
|
|
346
454
|
|
|
455
|
+
// Esc dismisses — in ORBIT only. In walk / drone, Esc means "free the
|
|
456
|
+
// cursor" (the browser's own pointer-lock exit; FirstPersonControls pauses
|
|
457
|
+
// instead of bailing) — reflexively dropping the whole capture with the
|
|
458
|
+
// framed pose would punish anyone who never noticed P. Enter fires the
|
|
459
|
+
// shutter: walk and drone hold a pointer lock, so a keyboard shutter works
|
|
460
|
+
// without leaving the camera.
|
|
461
|
+
useEffect(() => {
|
|
462
|
+
if (!isCaptureMode) return
|
|
463
|
+
const onKey = (e: KeyboardEvent) => {
|
|
464
|
+
if (e.key === 'Escape') {
|
|
465
|
+
if (cameraNav === 'orbit') setCaptureMode(false)
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
if (e.key !== 'Enter') return
|
|
469
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
|
|
470
|
+
e.preventDefault()
|
|
471
|
+
handleCapture()
|
|
472
|
+
}
|
|
473
|
+
window.addEventListener('keydown', onKey)
|
|
474
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
475
|
+
}, [cameraNav, handleCapture, isCaptureMode, setCaptureMode])
|
|
476
|
+
|
|
477
|
+
// While walk / drone hold the pointer lock, the wheel drives the lens and a
|
|
478
|
+
// click fires the shutter. Both gate on the lock being HELD: the click that
|
|
479
|
+
// acquires it happens unlocked, so entering the camera never also shoots,
|
|
480
|
+
// and an unlocked wheel keeps scrolling whatever pane it's over.
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
if (!isCaptureMode || cameraNav === 'orbit') return
|
|
483
|
+
const canvas = document.querySelector<HTMLCanvasElement>('[data-pascal-viewer-3d] canvas')
|
|
484
|
+
if (!canvas) return
|
|
485
|
+
// `setCaptureFov` rounds to whole degrees; accumulate sub-degree trackpad
|
|
486
|
+
// deltas so slow scrolls still move the lens.
|
|
487
|
+
let pendingFovDelta = 0
|
|
488
|
+
const onWheel = (e: WheelEvent) => {
|
|
489
|
+
if (document.pointerLockElement !== canvas) return
|
|
490
|
+
e.preventDefault()
|
|
491
|
+
const pixels = e.deltaMode === 1 ? e.deltaY * 16 : e.deltaY
|
|
492
|
+
// Wheel-up narrows the lens (zoom in), matching the orbit dolly.
|
|
493
|
+
pendingFovDelta += pixels * 0.05
|
|
494
|
+
const whole = Math.trunc(pendingFovDelta)
|
|
495
|
+
if (whole === 0) return
|
|
496
|
+
pendingFovDelta -= whole
|
|
497
|
+
const editor = useEditor.getState()
|
|
498
|
+
if (editor.captureFov === null) return
|
|
499
|
+
editor.setCaptureFov(editor.captureFov + whole)
|
|
500
|
+
}
|
|
501
|
+
const onMouseDown = (e: MouseEvent) => {
|
|
502
|
+
if (e.button !== 0 || document.pointerLockElement !== canvas) return
|
|
503
|
+
handleCapture()
|
|
504
|
+
}
|
|
505
|
+
window.addEventListener('wheel', onWheel, { passive: false })
|
|
506
|
+
// Capture phase: FirstPersonControls' own document-capture mousedown
|
|
507
|
+
// handler stops propagation while locked, which a bubble listener never
|
|
508
|
+
// survives — window-capture runs first.
|
|
509
|
+
window.addEventListener('mousedown', onMouseDown, true)
|
|
510
|
+
return () => {
|
|
511
|
+
window.removeEventListener('wheel', onWheel)
|
|
512
|
+
window.removeEventListener('mousedown', onMouseDown, true)
|
|
513
|
+
}
|
|
514
|
+
}, [cameraNav, handleCapture, isCaptureMode])
|
|
515
|
+
|
|
347
516
|
if (!isCaptureMode) return null
|
|
348
517
|
|
|
349
518
|
const resolution = getResolution(mode, overlayRef.current, drag, standardAspect)
|
|
519
|
+
// Walk and drone need the canvas to receive the click that grants pointer lock,
|
|
520
|
+
// so the area-drag surface steps aside — same treatment as preset mode, whose
|
|
521
|
+
// frame is fixed and camera-driven.
|
|
522
|
+
const cameraOwnsPointer = cameraNav !== 'orbit'
|
|
523
|
+
const frameLocked = isPreset || cameraOwnsPointer
|
|
524
|
+
// Preset captures are a constrained flow (fixed square, host-owned banner);
|
|
525
|
+
// they keep the plain orbit camera. Walk / drone need a keyboard, so they stay
|
|
526
|
+
// off touch. The fov control is armed by the capture rig only on a
|
|
527
|
+
// perspective camera — orthographic captures have no lens to drive.
|
|
528
|
+
const showCameraNav = !(isPreset || isMobile)
|
|
529
|
+
const fovValue = isPreset ? null : captureFov
|
|
530
|
+
const cameraHint = CAMERA_NAV_HINTS[cameraNav]
|
|
350
531
|
|
|
351
532
|
// Standard mode framing: the output is a center-crop of the canvas to the
|
|
352
533
|
// chosen aspect (see ThumbnailGenerator) — show exactly that region as a
|
|
@@ -385,6 +566,10 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
385
566
|
|
|
386
567
|
return (
|
|
387
568
|
<div className="pointer-events-none absolute inset-0 z-40" ref={overlayRef}>
|
|
569
|
+
{/* Walk / drone keep the walkthrough's centered pointer (the ring means
|
|
570
|
+
E opens the door / window under it) — the capture overlay replaces
|
|
571
|
+
the walkthrough HUD, so the crosshair rides along here. */}
|
|
572
|
+
{cameraOwnsPointer && <CaptureWalkthroughCrosshair />}
|
|
388
573
|
{/* Standard mode: letterboxed 16:9 frame with thirds + corner accents */}
|
|
389
574
|
{standardFrame && (
|
|
390
575
|
<div
|
|
@@ -412,13 +597,13 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
412
597
|
{mode === 'area' && (
|
|
413
598
|
<div
|
|
414
599
|
className={
|
|
415
|
-
|
|
600
|
+
frameLocked
|
|
416
601
|
? 'pointer-events-none absolute inset-0'
|
|
417
602
|
: 'pointer-events-auto absolute inset-0 bg-black/30'
|
|
418
603
|
}
|
|
419
|
-
onPointerDown={
|
|
604
|
+
onPointerDown={frameLocked ? undefined : onPointerDown}
|
|
420
605
|
onPointerMove={
|
|
421
|
-
|
|
606
|
+
frameLocked
|
|
422
607
|
? undefined
|
|
423
608
|
: (e) => {
|
|
424
609
|
onPointerMove(e)
|
|
@@ -436,16 +621,18 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
436
621
|
}
|
|
437
622
|
}
|
|
438
623
|
}
|
|
439
|
-
onPointerUp={
|
|
440
|
-
style={
|
|
624
|
+
onPointerUp={frameLocked ? undefined : onPointerUp}
|
|
625
|
+
style={frameLocked ? undefined : { cursor: 'crosshair' }}
|
|
441
626
|
>
|
|
442
627
|
{/* "No selection" hint — only when the user has to draw the
|
|
443
628
|
area themselves (`standard` capture). Preset mode always
|
|
444
629
|
has a pre-staged square, so we never show it there. */}
|
|
445
630
|
{!selectionStyle && !isPreset && (
|
|
446
631
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
447
|
-
<span className="rounded-full border border-white/10 bg-neutral-950/80 px-4 py-2 text-sm text-white
|
|
448
|
-
|
|
632
|
+
<span className="rounded-full border border-white/10 bg-neutral-950/80 px-4 py-2 text-sm text-white">
|
|
633
|
+
{cameraOwnsPointer
|
|
634
|
+
? 'Switch back to orbit to drag a capture area'
|
|
635
|
+
: 'Drag the area you want to capture'}
|
|
449
636
|
</span>
|
|
450
637
|
</div>
|
|
451
638
|
)}
|
|
@@ -469,8 +656,8 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
469
656
|
<CornerAccents />
|
|
470
657
|
{/* Corner handles — preset mode locks the frame to the
|
|
471
658
|
auto-staged centered square; the user adjusts the
|
|
472
|
-
camera instead. */}
|
|
473
|
-
{!
|
|
659
|
+
camera instead. Walk / drone lock it for the same reason. */}
|
|
660
|
+
{!frameLocked &&
|
|
474
661
|
(
|
|
475
662
|
[
|
|
476
663
|
{ pos: { top: -5, left: -5 }, cursor: 'nwse-resize' },
|
|
@@ -527,7 +714,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
527
714
|
<div className="pointer-events-auto absolute top-4 right-4">
|
|
528
715
|
<button
|
|
529
716
|
aria-label="Close capture mode"
|
|
530
|
-
className="flex items-center gap-1.5 rounded-lg border border-white/10 bg-neutral-950/85 px-3 py-1.5 text-white/80 text-xs
|
|
717
|
+
className="flex items-center gap-1.5 rounded-lg border border-white/10 bg-neutral-950/85 px-3 py-1.5 text-white/80 text-xs transition-colors hover:bg-neutral-950 hover:text-white"
|
|
531
718
|
onClick={dismiss}
|
|
532
719
|
type="button"
|
|
533
720
|
>
|
|
@@ -539,10 +726,71 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
539
726
|
{/* Subtle scrim so the bottom controls stay readable on bright scenes */}
|
|
540
727
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-36 bg-gradient-to-t from-black/45 via-black/15 to-transparent" />
|
|
541
728
|
|
|
542
|
-
{/* Bottom-center: crop switcher, caption + shutter */}
|
|
729
|
+
{/* Bottom-center: camera row, crop switcher, caption + shutter */}
|
|
543
730
|
<div className="pointer-events-none absolute right-0 bottom-5 left-0 flex flex-col items-center gap-2.5">
|
|
731
|
+
{/* How you move while composing, and the lens. Both are capture-scoped:
|
|
732
|
+
leaving capture puts the camera back on orbit at its entry fov. */}
|
|
733
|
+
{(showCameraNav || fovValue !== null) && (
|
|
734
|
+
<div className="pointer-events-auto flex items-center gap-2">
|
|
735
|
+
{showCameraNav && (
|
|
736
|
+
<div className="flex items-center gap-1 rounded-full border border-white/10 bg-neutral-950/85 px-1.5 py-1.5 shadow-xl">
|
|
737
|
+
<ModeButton
|
|
738
|
+
active={cameraNav === 'orbit'}
|
|
739
|
+
icon={<Orbit className="h-3.5 w-3.5" />}
|
|
740
|
+
label="Orbit"
|
|
741
|
+
onClick={() => setCameraNav('orbit')}
|
|
742
|
+
/>
|
|
743
|
+
<ModeButton
|
|
744
|
+
active={cameraNav === 'walk'}
|
|
745
|
+
icon={<Footprints className="h-3.5 w-3.5" />}
|
|
746
|
+
label="Walk"
|
|
747
|
+
onClick={() => setCameraNav('walk')}
|
|
748
|
+
/>
|
|
749
|
+
<ModeButton
|
|
750
|
+
active={cameraNav === 'drone'}
|
|
751
|
+
icon={<Drone className="h-3.5 w-3.5" />}
|
|
752
|
+
label="Drone"
|
|
753
|
+
onClick={() => setCameraNav('drone')}
|
|
754
|
+
/>
|
|
755
|
+
</div>
|
|
756
|
+
)}
|
|
757
|
+
{fovValue !== null && (
|
|
758
|
+
<div className="flex items-center gap-2.5 rounded-full border border-white/10 bg-neutral-950/85 py-1.5 pr-1.5 pl-3 shadow-xl">
|
|
759
|
+
<span className="font-mono text-[8.5px] text-white/50 uppercase tracking-[0.14em]">
|
|
760
|
+
Lens
|
|
761
|
+
</span>
|
|
762
|
+
<Slider
|
|
763
|
+
aria-label="Field of view"
|
|
764
|
+
className={FOV_SLIDER_CLASS}
|
|
765
|
+
max={CAPTURE_FOV_MAX}
|
|
766
|
+
min={CAPTURE_FOV_MIN}
|
|
767
|
+
onValueChange={([next]) => {
|
|
768
|
+
if (next !== undefined) setCaptureFov(next)
|
|
769
|
+
}}
|
|
770
|
+
step={1}
|
|
771
|
+
value={[fovValue]}
|
|
772
|
+
/>
|
|
773
|
+
<span className="w-8 text-right font-semibold text-white text-xs tabular-nums">
|
|
774
|
+
{fovValue}°
|
|
775
|
+
</span>
|
|
776
|
+
<button
|
|
777
|
+
aria-label="Reset field of view"
|
|
778
|
+
className="grid h-6 w-6 place-items-center rounded-full text-white/50 transition-colors hover:bg-white/10 hover:text-white disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-white/50"
|
|
779
|
+
disabled={captureFovBaseline === null || fovValue === captureFovBaseline}
|
|
780
|
+
onClick={() => {
|
|
781
|
+
if (captureFovBaseline !== null) setCaptureFov(captureFovBaseline)
|
|
782
|
+
}}
|
|
783
|
+
type="button"
|
|
784
|
+
>
|
|
785
|
+
<RotateCcw className="h-3 w-3" />
|
|
786
|
+
</button>
|
|
787
|
+
</div>
|
|
788
|
+
)}
|
|
789
|
+
</div>
|
|
790
|
+
)}
|
|
791
|
+
|
|
544
792
|
{!isCropLocked && (
|
|
545
|
-
<div className="pointer-events-auto relative flex items-center gap-1 rounded-full border border-white/10 bg-neutral-950/85 px-1.5 py-1.5 shadow-xl
|
|
793
|
+
<div className="pointer-events-auto relative flex items-center gap-1 rounded-full border border-white/10 bg-neutral-950/85 px-1.5 py-1.5 shadow-xl">
|
|
546
794
|
{/* Clicking Standard while it's active opens the aspect picker */}
|
|
547
795
|
<ModeButton
|
|
548
796
|
active={mode === 'standard'}
|
|
@@ -557,7 +805,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
557
805
|
}}
|
|
558
806
|
/>
|
|
559
807
|
{aspectMenuOpen && mode === 'standard' && (
|
|
560
|
-
<div className="absolute bottom-[calc(100%+8px)] left-0 flex gap-1 rounded-full border border-white/10 bg-neutral-950/90 p-1.5 shadow-xl
|
|
808
|
+
<div className="absolute bottom-[calc(100%+8px)] left-0 flex gap-1 rounded-full border border-white/10 bg-neutral-950/90 p-1.5 shadow-xl">
|
|
561
809
|
{(Object.keys(STANDARD_SIZES) as StandardAspect[]).map((aspect) => (
|
|
562
810
|
<button
|
|
563
811
|
className={`rounded-full px-2.5 py-1 font-mono text-[11px] transition-colors ${
|
|
@@ -606,12 +854,32 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
606
854
|
|
|
607
855
|
{/* Preset captures carry their own "Frame your item" banner — the
|
|
608
856
|
snapshot pitch only applies to the studio/reference flow. */}
|
|
609
|
-
{!isMobile &&
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
857
|
+
{!isMobile &&
|
|
858
|
+
!isPreset &&
|
|
859
|
+
(cameraHint ? (
|
|
860
|
+
<div className="pointer-events-none flex max-w-lg flex-wrap items-center justify-center gap-x-2.5 gap-y-1 rounded-lg border border-white/10 bg-neutral-950/85 px-3 py-1.5 text-[10px]">
|
|
861
|
+
{cameraHint.map(({ keys, action }) => (
|
|
862
|
+
<span className="inline-flex items-center gap-1 whitespace-nowrap" key={action}>
|
|
863
|
+
<span className="inline-flex items-center gap-0.5">
|
|
864
|
+
{keys.map((key, index) => (
|
|
865
|
+
<span className="inline-flex items-center gap-0.5" key={key}>
|
|
866
|
+
{index > 0 && <span className="text-white/25">/</span>}
|
|
867
|
+
<kbd className="rounded border border-white/15 bg-white/5 px-1.5 py-0.5 font-mono text-[9px] text-white/80 leading-none shadow-sm">
|
|
868
|
+
{key}
|
|
869
|
+
</kbd>
|
|
870
|
+
</span>
|
|
871
|
+
))}
|
|
872
|
+
</span>
|
|
873
|
+
<span className="text-white/55">{action}</span>
|
|
874
|
+
</span>
|
|
875
|
+
))}
|
|
876
|
+
</div>
|
|
877
|
+
) : (
|
|
878
|
+
<span className="pointer-events-none max-w-90 rounded-lg border border-white/10 bg-neutral-950/85 px-3.5 py-1.5 text-center text-[11.5px] text-white/85 leading-relaxed">
|
|
879
|
+
A <b className="font-semibold text-white">snapshot</b>
|
|
880
|
+
{' freezes this exact camera angle as a reusable reference for renders & videos.'}
|
|
881
|
+
</span>
|
|
882
|
+
))}
|
|
615
883
|
|
|
616
884
|
<button
|
|
617
885
|
aria-label={isPreset ? 'Capture' : 'Take snapshot'}
|