From 8a5f08b842959527bf7bfb6caf49acd281f8593e Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Thu, 28 May 2026 13:33:27 -0400 Subject: [PATCH] editor: refine preset capture framing + zoom limits (#344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../editor/custom-camera-controls.tsx | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/components/editor/custom-camera-controls.tsx b/packages/editor/src/components/editor/custom-camera-controls.tsx index 3c0d98b2..543a0c4b 100644 --- a/packages/editor/src/components/editor/custom-camera-controls.tsx +++ b/packages/editor/src/components/editor/custom-camera-controls.tsx @@ -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 (