# Viewer URL State Contract

Mechanica share links are part of the viewer’s public API: they need to survive refreshes, copy/paste, documentation links, and older URLs created before the viewer store grows new fields. The canonical query-string format is intentionally human-readable, while the optional `vs` payload provides a compact versioned form for very large states.

## Canonical parameters

| Parameter | Meaning | Example |
| --- | --- | --- |
| `machine` / legacy `m` | Active machine or procedural asset id. | `machine=four-stroke-engine` |
| `mode` | Render mode. Known aliases are normalised, e.g. `x-ray` becomes `xray`. | `mode=wireframe` |
| `cam` | Camera position tuple as `x,y,z`. | `cam=8,3,-6` |
| `target` | Orbit target / look-at tuple as `x,y,z`. | `target=0,1,0` |
| `zoom` | Orthographic zoom or camera zoom-like scalar. Clamped to `0.05..100`. | `zoom=1.4` |
| `fov` | Perspective camera field of view. Clamped to `10..90`. | `fov=42` |
| `preset` | Active saved camera preset id. | `preset=iso` |
| `selected` | Selected component id for drawer/tooltip restoration. | `selected=crankshaft` |
| `hidden` | Comma-separated component ids hidden by the parts panel. | `hidden=piston,valves` |
| `isolated` | Comma-separated component ids isolated by the parts panel. | `isolated=input-shaft,output-shaft` |
| `opacity` | Comma-separated `partId:opacity` pairs. Opacity is clamped to `0..1`. | `opacity=block:0.25,crankshaft:0.8` |
| `exploded` | Exploded-view toggle as `1` or `0`. | `exploded=1` |
| `explode` | Exploded-view separation amount, clamped to `0..3`. | `explode=1.25` |
| `annotations` | Annotation visibility override. Defaults are omitted. | `annotations=0` |
| `labels` | Part-label visibility override. Defaults are omitted. | `labels=0` |
| `section` | Cross-section state, usually `axis:offset` or a boolean. | `section=x:0.2` |

## Compatibility and safety rules

- The parser accepts legacy aliases such as `px/py/pz`, `tx/ty/tz`, `wireframe=1`, `cameraPosition`, `selectedPartId`, and `hiddenPartIds`.
- Malformed numeric tuples are ignored rather than propagated as `NaN`.
- Camera coordinates are capped to a large finite range to avoid accidental or malicious URLs creating unusable camera positions.
- Lists are de-duplicated and sorted so repeated serialisation does not create query-string churn.
- Opacity values are clamped, and default opacity `1` is omitted from newly generated public URLs unless `includeDefaults` is requested.

## Compact payloads

Passing `preferCompact: true` to `serializeViewStateToSearchParams` emits a single `vs` parameter. The payload is base64url-encoded JSON with a schema version and short keys, for example:

```ts
const params = serializeViewStateToSearchParams(viewState, { preferCompact: true });
const decoded = deserializeViewStateFromSearchParams(params);
```

The parser treats explicit query parameters as the stable public format and the compact payload as an implementation detail. Keep the readable parameters in documentation and tutorials; use `vs` only where URL length is a concern.

## Automated coverage

`src/utils/viewStateUrl.test.ts` covers canonical round-tripping, corrupt parameter handling, legacy camera formats, URL preservation, and compact payload decoding. These tests are intentionally pure unit tests so the share-link contract can be validated quickly without booting WebGL or loading Three.js assets.
