# VIEWPORT_CORE_LOCK — SHiRE LimbForge

> **STABLE VIEWPORT CORE — DO NOT REPLACE OR ADD SHADER HELPERS WITHOUT RUNNING THE VIEWPORT REGRESSION TESTS.**

This document freezes the 3D viewport architecture so that future edits to
non-3D pages, customer/project/component changes, and route changes can never
produce the "3D viewport could not start" fatal screen.

## 1. Verified dependency versions (pinned, no caret/tilde)

| Package | Exact version | Role |
|---|---|---|
| react | 18.3.1 | UI |
| react-dom | 18.3.1 | UI |
| three | 0.165.0 | WebGL |
| @react-three/fiber | 8.18.0 | React renderer (React 18 → Fiber 8) |
| @react-three/drei | 9.122.0 | retained but NOT imported by the core |
| vite | 6.4.3 | bundler |

React major is 18, so the matching React Three Fiber major is **8** (not 9).
No duplicate React, Three, Fiber or Drei installations exist. Peer
dependencies all resolve. Do not upgrade automatically.

## 2. Why the core is locked

The previous viewport failed because the **core Canvas depended on Drei**
(`OrbitControls`, `PerspectiveCamera`, `OrthographicCamera`) and Drei `Html`
was used inside the render loop across 8 feature files. Drei `Html` portals into
the DOM and can throw across route/context/customer changes; a throw inside the
Canvas tree bubbled to the `ViewportErrorBoundary` and replaced the whole
viewport with a fatal error screen.

The permanent fix removes every Drei import from the viewport core and from all
scene feature modules:

- OrbitControls → native `three/addons/controls/OrbitControls.js` (one
  instance, disposed once).
- Perspective/Orthographic camera → native `THREE` cameras set via
  `useThree().set({ camera })`.
- All `Html` labels → ordinary DOM overlay (`ViewportLabelLayer`) positioned by
  projecting world points through the live camera.

## 3. Allowed materials

- `MeshStandardMaterial`
- `MeshPhysicalMaterial`
- `MeshBasicMaterial` (emergency fallback — visible without lighting)
- `LineBasicMaterial`

## 4. Allowed geometry

- `cylinderGeometry`, `sphereGeometry`, `boxGeometry`, `torusGeometry`,
  `latheGeometry`, `extrudeGeometry`
- `BufferGeometry` + `Line` for dimension lines
- `THREE.GridHelper`, `axesHelper`

## 5. Forbidden shader / Drei helpers (in the core)

`@react-three/drei/Grid`, `@react-three/drei/Line`, `@react-three/drei/Html`,
`@react-three/drei/Text`, `Environment`, `ContactShadows`,
`AccumulativeShadows`, `PresentationControls`, `Stage`, `Bounds`,
`shaderMaterial`, `LineDashedMaterial`, `dashSize`, `gapSize`,
`extensions-derivatives`, `postprocessing`, `EffectComposer`, any custom
shader, dashed lines, post-processing, dynamically generated shader
extensions.

## 6. One-Canvas rule

`StableViewportCore` renders exactly one `<Canvas key="stable-viewport-core">`.
The key is a constant — never keyed by route, project UUID, customer UUID,
selected object, station or viewport mode. Changing workshop pages updates the
scene contents inside the existing Canvas; the WebGL context is never
destroyed and recreated. There must never be multiple hidden or overlapping
Canvas elements.

## 7. Optional-layer architecture

Each optional visual feature is an isolated module wrapped in its own
`SceneFeatureBoundary` (a React error boundary returning a tiny amber wireframe
marker on failure):

- MeasurementOverlayLayer (StationRings, DimensionLabels)
- SensitiveZoneLayer / KeepOutZoneLayer / AccessZoneLayer (ZoneMeshes)
- ConceptualCoverLayer (CoverShell)
- BrandingLayer (ShireLogo)
- MovementEnvelopeLayer (MovementPath)
- ImportedModelLayer (ComponentModel)

When an optional layer fails, the procedural prosthetic stays visible, controls
keep working, the failed layer shows a small warning marker, and the layer
error is recorded in the diagnostics drawer. A failed layer may NEVER replace
the core Canvas.

## 8. Data-loading order

1. Mount the stable Canvas (lights, grid, procedural fallback, controls).
2. Show viewport-ready status.
3. Load customer / limb / component / measurement data (outside the core).
4. Apply measurement overlays and optional visual layers.

Canvas mounting is never blocked behind a database Promise. Database failures
never stop Canvas creation.

## 9. Cache-reset procedure

If a stale Vite chunk is suspected:

1. Stop the dev server.
2. Delete `node_modules/.vite`.
3. Restart Vite (it re-optimises dependencies).
4. Hard-refresh the preview.
5. Confirm the old failing chunk hash is no longer requested.

A build ID (`src/components/workshop3d/viewportBuild.js`) is shown in the
diagnostics drawer so the loaded build can be confirmed against the source.

## 10. Acceptance tests required before future viewport changes

- Open `/measurements` — model appears.
- Normal refresh ×10 and hard-refresh ×5 — viewport loads each time.
- Navigate between all 3D workshop routes ×20 — one Canvas, one OrbitControls.
- Navigate to Customers and back ×10 — viewport survives.
- Change customers ×10, projects ×10, select 20 tree items — no fatal screen.
- Toggle every optional layer repeatedly — only the toggled layer changes.
- Resize repeatedly; enter/exit fullscreen — viewport survives.
- Change perspective, orthographic, Front/Rear/Left/Right/Top views.
- Background the tab and return — viewport intact.
- Simulate imported-model failure, missing SHiRE logo, empty database —
  procedural fallback remains.
- Run `node scripts/viewport-safety.mjs` — must pass with zero violations.
- Diagnostics drawer shows Canvas count 1, Controls instances 1, WebGL Active,
  Procedural fallback Loaded, Core scene errors 0.

Future Design Forge, Sensitive Zone and Cover Build work must consume the
stable viewport API (props into `SceneComposer`) without replacing the Canvas,
renderer or controls.