# 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.