# Mechanica Viewer Architecture

Milestone 3 delivers the reusable React Three Fiber viewer engine used by every machine detail page. The viewer is intentionally data-driven: machine-specific geometry and metadata live in asset definitions, while the core renderer, interaction system, camera system, URL sharing, and controls are shared.

## Runtime composition

```txt
MechanicalViewer
├─ PartListPanel                 DOM controls for part visibility/opacity/selection
├─ ViewerCanvas                  React Three Fiber Canvas + mobile-safe renderer profile
│  └─ ViewerScene
│     ├─ ViewerEnvironment       PBR lighting, HDR/environment hook, grid, contact shadows
│     ├─ CameraController        Perspective camera + damped OrbitControls + presets
│     ├─ MachineAssetInstance    Procedural or GLB/GLTF asset resolution
│     │  └─ PreparedMachineRoot  Material cloning, highlighting, clipping, exploding
│     ├─ PartAnnotations         HTML labels anchored in 3D space
│     └─ FpsSampler              Frame-rate reporting to Zustand
├─ ViewerToolbar                 Camera presets, reset, copy share link
├─ LoadingProgressOverlay        Drei loader progress + store loading states
├─ PartTooltip                   Pointer-following hover tooltip
├─ DetailDrawer                  Selected-part engineering facts and controls
├─ ViewerStatusBar               FPS, part count, selected/hovered state, render modes
└─ ViewerControlsPanel           Exploded view, wireframe, labels, shadows, cross-section
```

## State model

`src/store/viewerStore.ts` is the single runtime source of truth. It stores:

- active machine ID and loaded machine metadata
- part metadata and per-part runtime settings
- selected/hovered part
- display settings: wireframe, labels, shadows, grid, exposure, exploded view, clipping
- current camera snapshot and camera command requests
- scene bounds, loading status, and sampled FPS
- pending URL view-state overrides applied when parts register

This design lets URL state hydrate before the model is loaded. If `hide`, `opacity`, or `sel` URL parameters arrive before part metadata exists, they are stored as a pending snapshot and applied during `registerParts()` / `setActiveMachine()`.

## Camera controls

`CameraController` mounts a Drei `PerspectiveCamera` and `OrbitControls` with damping, pan, and zoom enabled. Camera presets are centralized in `src/three/cameraPresets.ts`:

- front
- back
- left
- right
- top
- isometric

Presets are resolved against the current machine bounds, so a large GLB asset and a small procedural mechanism receive the same view semantics. Camera movements use GSAP easing unless the user has `prefers-reduced-motion: reduce`, in which case the camera snaps immediately.

The camera snapshot written to state includes position, target, up vector, and FOV. It is sampled at a throttled interval to keep URL sharing responsive without over-updating the browser history.

## URL view-state sharing

`src/utils/viewStateUrl.ts` encodes a compact state format:

| Query param | Meaning |
| --- | --- |
| `m` | machine ID |
| `cam` | camera position, target, optional up vector, FOV |
| `preset` | named camera preset |
| `sel` | selected part ID |
| `explode` | exploded-view enabled flag and distance |
| `wire` | wireframe enabled |
| `labels` | annotation-label visibility |
| `cs` | cross-section axis, offset, inversion, helper flag |
| `hide` | comma-separated hidden part IDs |
| `opacity` | per-part opacity overrides |

`useViewerUrlState()` hydrates once from the current URL, then debounces `history.replaceState()` updates. The toolbar's copy-link action uses the latest store state to create a shareable deep link.

## Lighting and rendering defaults

`ViewerEnvironment` configures a dark engineering scene with physically based defaults:

- ACES filmic tone mapping
- sRGB output color space
- configurable exposure
- hemisphere fill, high-intensity key light, blue rim light, warm point accent
- optional HDR/environment map via `VITE_MECHANICA_HDR_URL`
- Drei built-in environment fallback (`preset="city"`)
- PCF soft shadows and contact shadows
- renderer-local clipping enabled for cross-sections

`ViewerCanvas` uses adaptive mobile-safe renderer settings:

- DPR capped to 1.5 on coarse/mobile pointers and 2 on desktop
- antialias disabled on low-core mobile devices
- `powerPreference` set to `default` on mobile and `high-performance` on desktop
- stencil disabled and preserveDrawingBuffer disabled to reduce memory pressure

## Asset loading layer

Asset definitions live in `src/three/assets/machineAssets.ts`.

A definition may be:

1. `procedural` — supplies a `create()` factory returning a `LoadedMachineAsset`.
2. `gltf` — supplies a GLB/GLTF URL plus metadata and optional part-name mapping.
3. `hybrid` — reserved for future GLTF models with procedural overlays.

The initial registered machine is `four-stroke-petrol-engine`, implemented as a detailed procedural cutaway in `src/three/procedural/fourStrokeEngine.ts`. It includes real component metadata, annotation anchors, exploded directions, material assignments, and approximate bounds.

GLTF definitions created with `createGltfMachineAssetDefinition()` can specify:

- `dracoPath` for Draco-compressed meshes
- `ktx2TranscoderPath` for Basis/KTX2 textures
- `meshopt` for Meshopt-compressed geometry
- `partNameMap` for mapping node names to Mechanica part IDs

`MachineAssetInstance` clones loaded GLTF scenes, applies part metadata from explicit `userData`, node-name maps, or part `nodeNames`, then passes the prepared root into the shared interaction system.

## Interaction system

All selectable parts are identified using `object.userData.mechanicaPartId`. Procedural assets tag groups and meshes at creation time. GLTF assets are tagged during scene preparation.

`PreparedMachineRoot` performs the heavy lifting:

- traverses the scene once after load
- clones materials to avoid mutating cached GLTF materials
- enables cast/receive shadows
- collects part roots for exploded-view transforms
- applies visibility and opacity settings
- applies selected and hovered emissive highlights
- applies wireframe mode
- applies local clipping planes
- maps pointer events back to part IDs
- writes hover tooltip data and selected part IDs into Zustand

The exploded view is animated in `useFrame()` with exponential damping. It respects reduced-motion preferences by snapping when reduced motion is requested.

## Cross-section mode

Cross-section clipping is renderer-local and material-level. The store tracks axis, offset, and inversion. `PreparedMachineRoot` converts this into a Three.js `Plane` and assigns it to cloned materials.

The UI exposes X/Y/Z axis selection, offset, inversion, and enable/disable. Hidden or transparent parts still receive clipping state so switching modes remains consistent.

## Accessibility

The viewer UI uses semantic buttons, labelled switches, `aria-label` text for controls, and focus-visible outlines. The loading overlay announces progress with `role="status"` and `aria-live="polite"`. Reduced-motion preference is honored for camera and exploded-view transitions.

The Three.js canvas remains pointer-centric by nature, but every major viewer action is mirrored in keyboard-focusable DOM controls: component list, camera presets, reset, display toggles, cross-section controls, detail drawer controls, and share link.

## Performance notes

- Procedural geometry is grouped per part to keep raycast lookup and exploded transforms inexpensive.
- GLTF material cloning happens once per model instance.
- FPS sampling updates at 2 Hz rather than every frame.
- URL camera sampling is throttled.
- Device pixel ratio is capped to prevent mobile overdraw.
- Shadow map resolution is high enough for desktop quality while contact shadows avoid excessive scene complexity.
- Future final assets should be Draco-compressed for geometry and KTX2-compressed for textures, with mesh/material names aligned to registry part IDs.

## Adding a new GLB machine

```ts
import { registerMachineAsset, createGltfMachineAssetDefinition } from '../three/assets/machineAssets';

registerMachineAsset(
  createGltfMachineAssetDefinition(
    {
      id: 'planetary-gearbox',
      title: 'Planetary Gearbox',
      category: 'Gearboxes & Drives',
      difficulty: 'Intermediate',
      description: 'Sun, planet, carrier, and ring gear assembly.',
      parts: [
        {
          id: 'sun-gear',
          name: 'Sun Gear',
          description: 'Central input/output gear.',
          nodeNames: ['SunGear', 'sun_gear_mesh'],
          explodeDirection: [0, 0.2, 0]
        }
      ]
    },
    {
      fileName: 'model.glb',
      dracoPath: '/draco/',
      ktx2TranscoderPath: '/basis/',
      partNameMap: {
        SunGear_LOD0: 'sun-gear'
      }
    }
  )
);
```

Place the model at `/public/assets/machines/planetary-gearbox/model.glb` or configure `VITE_MECHANICA_ASSET_BASE_URL` to point at a CDN.
