# Viewer Camera Framing

The viewer now has a reusable fit-to-bounds utility for reset views, first-load framing, saved camera presets, and future “focus selected part” actions. It is intentionally independent from React Three Fiber so the same code can be used by the procedural catalogue, GLTF asset loader, tests, and any non-React tooling that prepares thumbnails.

## Why this exists

A mechanical viewer should not rely on hard-coded camera distances. Engines, gear trains, pumps, and linkages have very different aspect ratios, and mobile portrait screens can turn a good desktop reset view into a clipped model. `computePerspectiveCameraFit` projects all eight bounding-box corners into the intended camera basis and solves the minimum camera distance needed for both vertical and horizontal field-of-view constraints.

This is more predictable than using only a bounding sphere:

- long gearboxes fit tightly on wide desktop screens;
- tall or wide machines back the camera out correctly on narrow screens;
- oblique engineering/isometric directions account for the object depth nearest the camera;
- empty or point-sized bounds still produce a finite, safe pose.

## Primary API

```ts
import { Box3 } from 'three';
import { applyPerspectiveCameraFit } from '../src/three/camera/fitCameraToBounds';

const bounds = new Box3().setFromObject(machineRoot);

const fit = applyPerspectiveCameraFit(camera, bounds, {
  direction: [1, 0.65, 1],
  margin: isMobile ? 1.3 : 1.15,
});

// OrbitControls/Drei controls keep their own target.
controls.target.copy(fit.target);
controls.update();
```

For non-mutating use, call `computePerspectiveCameraFit(bounds, camera.fov, camera.aspect, options)` and apply the returned `position`, `target`, `near`, and `far` through the viewer store or camera controller.

## Integration points

Recommended viewer integration:

1. After a machine root is prepared, compute a world-space `Box3` from the loaded `Object3D`.
2. On first load, camera reset, or machine change, compute a fit using the active preset direction.
3. Apply the camera pose and update the OrbitControls target from `fit.target`.
4. Persist the resulting camera position/target through the existing view-state URL contract.
5. Do not persist the raw bounds in the URL; bounds are derived from the asset and may change as assets improve.

For selected-part focusing, compute bounds for the selected part subtree and use a slightly larger margin, for example `1.35`, so annotations and outlines remain visible.

## Edge cases handled

- Empty `Box3` instances use a configurable fallback cube.
- Zero-size boxes still produce a useful non-zero camera distance.
- Invalid FOV/aspect inputs are sanitized to safe defaults.
- A preferred up vector parallel to the view direction is replaced with a stable orthogonal up vector.
- Near/far planes are recomputed from projected depth so reset views do not clip the model.
- `maxDistance` is supported for constrained experiences; the result reports `constrainedByMaxDistance` when the clamp may prevent a full fit.

## Test coverage

`src/three/camera/fitCameraToBounds.test.ts` covers finite/invalid bounds detection, parallel-up recovery, corner fitting, mobile/narrow aspect behavior, fallback bounds, max-distance constraint reporting, and mutating application to `PerspectiveCamera`.
