fix: capture-mode polish for studio & presets + pausable render loop (#469)
* fix(editor): keep snapshot pitch out of preset thumbnail capture The capture overlay's snapshot hint and 'Take snapshot' shutter label rendered in every capture variant, clashing with the save-as-preset flow's own 'Frame your item — click Capture' banner. Gate them on the existing isPreset discriminator and label the shutter 'Capture' there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer): renderPaused flag suspends the frame loop Hosts that fully cover the canvas (e.g. the studio gallery overlay) can set renderPaused to stop FrameLimiter's RAF loop — a heavy animated scene otherwise keeps starving the GPU behind the overlay and makes it stutter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): drop the crop/format HUD in preset capture The pills crowded the save-as-preset banner and repeat what the fixed square frame already shows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): no contextual shortcut hints in the studio workspace Holding Shift surfaced select-mode/snapping hints over the studio compose panel and gallery, where there is no scene selection or tool to act on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f6a58489b8
commit
51fddc2d9c
@@ -497,8 +497,9 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Top-center HUD — what the shot will be */}
|
{/* Top-center HUD — what the shot will be. Preset captures are a fixed
|
||||||
{!isMobile && (
|
square and carry their own "Frame your item" banner up there. */}
|
||||||
|
{!isMobile && !isPreset && (
|
||||||
<div className="pointer-events-none absolute top-4 left-1/2 flex -translate-x-1/2 gap-2">
|
<div className="pointer-events-none absolute top-4 left-1/2 flex -translate-x-1/2 gap-2">
|
||||||
<div className={HUD_CHIP_CLASS}>
|
<div className={HUD_CHIP_CLASS}>
|
||||||
<span className="font-mono text-[8.5px] text-white/50 uppercase tracking-[0.14em]">
|
<span className="font-mono text-[8.5px] text-white/50 uppercase tracking-[0.14em]">
|
||||||
@@ -600,7 +601,9 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isMobile && (
|
{/* Preset captures carry their own "Frame your item" banner — the
|
||||||
|
snapshot pitch only applies to the studio/reference flow. */}
|
||||||
|
{!isMobile && !isPreset && (
|
||||||
<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 backdrop-blur-md">
|
<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 backdrop-blur-md">
|
||||||
A <b className="font-semibold text-white">snapshot</b>
|
A <b className="font-semibold text-white">snapshot</b>
|
||||||
{' freezes this exact camera angle as a reusable reference for renders & videos.'}
|
{' freezes this exact camera angle as a reusable reference for renders & videos.'}
|
||||||
@@ -608,7 +611,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label="Take snapshot"
|
aria-label={isPreset ? 'Capture' : 'Take snapshot'}
|
||||||
className="group pointer-events-auto relative grid h-14 w-14 place-items-center rounded-full disabled:opacity-50"
|
className="group pointer-events-auto relative grid h-14 w-14 place-items-center rounded-full disabled:opacity-50"
|
||||||
disabled={captureDisabled}
|
disabled={captureDisabled}
|
||||||
onClick={handleCapture}
|
onClick={handleCapture}
|
||||||
@@ -634,7 +637,9 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|||||||
? 'Capturing…'
|
? 'Capturing…'
|
||||||
: captureState === 'saved'
|
: captureState === 'saved'
|
||||||
? 'Saved'
|
? 'Saved'
|
||||||
: 'Take snapshot'}
|
: isPreset
|
||||||
|
? 'Capture'
|
||||||
|
: 'Take snapshot'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ function useActiveModifierKeys(): ActiveModifierKeys {
|
|||||||
export function HelperManager() {
|
export function HelperManager() {
|
||||||
const mode = useEditor((s) => s.mode)
|
const mode = useEditor((s) => s.mode)
|
||||||
const tool = useEditor((s) => s.tool)
|
const tool = useEditor((s) => s.tool)
|
||||||
|
const workspaceMode = useEditor((s) => s.workspaceMode)
|
||||||
const scope = useInteractionScope((s) => s.scope)
|
const scope = useInteractionScope((s) => s.scope)
|
||||||
const movingNode = useMovingNode()
|
const movingNode = useMovingNode()
|
||||||
const activeHandleDrag = useActiveHandleDrag()
|
const activeHandleDrag = useActiveHandleDrag()
|
||||||
@@ -146,6 +147,10 @@ export function HelperManager() {
|
|||||||
// Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
|
// Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
|
||||||
if (isMobile) return null
|
if (isMobile) return null
|
||||||
|
|
||||||
|
// The studio workspace (compose panel / gallery) has no scene selection or
|
||||||
|
// tools — editor shortcut hints would only mislead there.
|
||||||
|
if (workspaceMode === 'studio') return null
|
||||||
|
|
||||||
// Rotating a node (or a multi-selection group) via its in-world gizmo:
|
// Rotating a node (or a multi-selection group) via its in-world gizmo:
|
||||||
// advertise Shift = free rotation, the same angle-step bypass wall drafting
|
// advertise Shift = free rotation, the same angle-step bypass wall drafting
|
||||||
// exposes. Takes priority over the idle select-mode hints since a handle
|
// exposes. Takes priority over the idle select-mode hints since a handle
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useThree } from '@react-three/fiber'
|
import { useThree } from '@react-three/fiber'
|
||||||
import { useLayoutEffect } from 'react'
|
import { useLayoutEffect } from 'react'
|
||||||
|
import useViewer from '../../store/use-viewer'
|
||||||
|
|
||||||
type FrameLimiterProps = {
|
type FrameLimiterProps = {
|
||||||
fps?: number
|
fps?: number
|
||||||
@@ -8,8 +9,11 @@ type FrameLimiterProps = {
|
|||||||
const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
|
const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
|
||||||
const { advance, set, frameloop: initFrameloop, scene, clock } = useThree()
|
const { advance, set, frameloop: initFrameloop, scene, clock } = useThree()
|
||||||
const renderer = useThree((state) => state.gl)
|
const renderer = useThree((state) => state.gl)
|
||||||
|
// Fully covered canvas (e.g. studio gallery) → stop advancing frames
|
||||||
|
const renderPaused = useViewer((s) => s.renderPaused)
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
if (renderPaused) return
|
||||||
let elapsed = 0
|
let elapsed = 0
|
||||||
let then = 0
|
let then = 0
|
||||||
let i = 0
|
let i = 0
|
||||||
@@ -35,7 +39,7 @@ const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
|
|||||||
}
|
}
|
||||||
set({ frameloop: initFrameloop })
|
set({ frameloop: initFrameloop })
|
||||||
}
|
}
|
||||||
}, [fps, advance, set, initFrameloop])
|
}, [fps, advance, set, initFrameloop, renderPaused])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ type ViewerState = {
|
|||||||
isExporting: boolean
|
isExporting: boolean
|
||||||
setExporting: (value: boolean) => void
|
setExporting: (value: boolean) => void
|
||||||
|
|
||||||
|
/** Suspend the render loop while the canvas is fully covered (e.g. studio gallery). */
|
||||||
|
renderPaused: boolean
|
||||||
|
setRenderPaused: (value: boolean) => void
|
||||||
|
|
||||||
shading: RenderShading
|
shading: RenderShading
|
||||||
shadingByContext: Partial<Record<RenderContext, RenderShading>>
|
shadingByContext: Partial<Record<RenderContext, RenderShading>>
|
||||||
setShading: (shading: RenderShading) => void
|
setShading: (shading: RenderShading) => void
|
||||||
@@ -233,6 +237,9 @@ const useViewer = create<ViewerState>()(
|
|||||||
isExporting: false,
|
isExporting: false,
|
||||||
setExporting: (value) => set({ isExporting: value }),
|
setExporting: (value) => set({ isExporting: value }),
|
||||||
|
|
||||||
|
renderPaused: false,
|
||||||
|
setRenderPaused: (value) => set({ renderPaused: value }),
|
||||||
|
|
||||||
shading: 'rendered',
|
shading: 'rendered',
|
||||||
shadingByContext: {},
|
shadingByContext: {},
|
||||||
setShading: (shading) =>
|
setShading: (shading) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user