editor: refine preset capture framing + zoom limits (#344)
Tunes the auto-framing and camera clamps used by `setCaptureMode({ mode:
'preset', isolated })`:
- Frame from a 3/4 view of the node's *front* face. Reads the isolated
root's yaw, picks `+Z` (local forward, the face authored for catalog
shots), offsets the camera by 35° to the side so both the front and
one adjacent face are visible. 25° elevation keeps the top visible
without going isometric. Multi-isolate falls back to world `+Z`.
- Bump the bounds-fit multiplier from 1.6 → 2.4 (floor 3 → 4) so the
subject lands with ~25-30% padding inside the locked square crop —
the user can recompose without immediately needing to zoom out.
- Relax `minDistance` from 10 → 0.5 while in preset capture mode (a
0.3–2m preset can't be framed close-in against the editor's general
10m floor). Reverts to 10 on exit so general editing keeps the
looser navigation guardrails.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0ad0ac1591
commit
8a5f08b842
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type CameraControlEvent,
|
||||
type CameraControlFitSceneEvent,
|
||||
emitter,
|
||||
@@ -357,15 +358,44 @@ export const CustomCameraControls = () => {
|
||||
tempBox.getSize(tempSize)
|
||||
|
||||
// Distance heuristic: fit the subject inside the 75%-of-shorter-
|
||||
// side square crop. Multiplier 1.6 keeps a small breathing margin
|
||||
// around the bounds so the dashed frame doesn't kiss the geometry.
|
||||
// side square crop with comfortable padding. Multiplier 2.4 leaves
|
||||
// ~25-30% margin around the bounds so the user can frame without
|
||||
// immediately needing to zoom out, but isn't so far away that the
|
||||
// subject reads as small in the thumbnail.
|
||||
const maxDim = Math.max(tempSize.x, tempSize.y, tempSize.z)
|
||||
const distance = Math.max(maxDim * 1.6, 3)
|
||||
const distance = Math.max(maxDim * 2.4, 4)
|
||||
|
||||
// Frame the subject from a 3/4 view of its front face. The node's
|
||||
// local +Z is its forward axis in this scene's authoring convention
|
||||
// (the face the user sets up to be photographed). When a single
|
||||
// subtree is isolated we read its yaw and rotate the camera around
|
||||
// the bounds center so the framing follows the user's authored
|
||||
// orientation; for multi-isolate sets we fall back to world +Z.
|
||||
// The 3/4 view offsets the camera by 35° to the right of dead-on
|
||||
// so both the front face and a side are visible — the "nice angle"
|
||||
// that reads as a product shot rather than a flat elevation. ~25°
|
||||
// elevation keeps the top visible without going isometric.
|
||||
const SIDE_OFFSET_RAD = (35 * Math.PI) / 180
|
||||
const ELEVATION_RAD = (25 * Math.PI) / 180
|
||||
let yaw = 0
|
||||
if (ids.length === 1) {
|
||||
const node = useScene.getState().nodes[ids[0] as AnyNodeId]
|
||||
if (node && 'rotation' in node) {
|
||||
const r = (node as { rotation?: unknown }).rotation
|
||||
if (typeof r === 'number') yaw = r
|
||||
else if (Array.isArray(r)) yaw = (r as [number, number, number])[1] ?? 0
|
||||
}
|
||||
}
|
||||
// World-space direction the camera should sit *along* relative to
|
||||
// bounds center: in front (object's local +Z under yaw) + a right
|
||||
// offset around Y for the 3/4 read.
|
||||
const viewAngle = yaw + SIDE_OFFSET_RAD
|
||||
const horizontal = distance * Math.cos(ELEVATION_RAD)
|
||||
const elevation = distance * Math.sin(ELEVATION_RAD)
|
||||
controls.current.setLookAt(
|
||||
tempCenter.x + distance * 0.7,
|
||||
tempCenter.y + Math.max(tempSize.y * 0.4, distance * 0.4),
|
||||
tempCenter.z + distance * 0.7,
|
||||
tempCenter.x + Math.sin(viewAngle) * horizontal,
|
||||
tempCenter.y + elevation,
|
||||
tempCenter.z + Math.cos(viewAngle) * horizontal,
|
||||
tempCenter.x,
|
||||
tempCenter.y,
|
||||
tempCenter.z,
|
||||
@@ -514,12 +544,20 @@ export const CustomCameraControls = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
// Preset capture mode frames a single subtree (often a 0.3–2m preset),
|
||||
// so the default 10m minDistance prevents the user from getting close
|
||||
// enough to compose a good thumbnail. Relax the clamp to 0.5m while
|
||||
// capturing presets; reset on exit so general editing keeps the looser
|
||||
// navigation guardrails.
|
||||
const isPresetCapture = captureMode.mode === 'preset'
|
||||
const minDistance = isPresetCapture ? 0.5 : 10
|
||||
|
||||
return (
|
||||
<CameraControls
|
||||
makeDefault
|
||||
maxDistance={100}
|
||||
maxPolarAngle={maxPolarAngle}
|
||||
minDistance={10}
|
||||
minDistance={minDistance}
|
||||
minPolarAngle={0}
|
||||
mouseButtons={mouseButtons}
|
||||
onRest={onRest}
|
||||
|
||||
Reference in New Issue
Block a user