# Shareable Animation State

Milestone 4 now includes a deterministic URL-state codec for animation and procedural-machine viewer state. The goal is to make links safe to open, stable to diff in tests, and compact enough for “copy link” UI without trusting arbitrary query-string values.

## Import

```ts
import {
  createShareableAnimationSearchParams,
  decodeShareableAnimationState,
  encodeShareableAnimationState,
} from '../src/animations/shareableAnimationState';
```

## Encoded fields

| Field | URL key | Notes |
| --- | --- | --- |
| Schema version | `v` | Current version is `1`. Future versions are decoded conservatively with a warning. |
| Machine | `m` | Procedural machine id such as `wankel-rotary-engine`. |
| Animation | `a` | Optional animation module id when a machine exposes multiple animations. |
| Time | `t` | Animation time in seconds, clamped to a safe non-negative range. |
| RPM | `rpm` | Clamped by configurable limits; default max is `12000`. |
| Time scale | `s` | Default safe range is `0.05–4`. |
| Playback | `play` | `1` for playing, `0` for paused. |
| Loop | `loop` | Boolean. |
| Step-through mode | `step` | Boolean. |
| Reduced motion | `rm` | Boolean user preference/state override. |
| Exploded view | `exp` | Default normalized range is `0–1`. |
| Render mode | `mode` | `solid`, `wireframe`, `xray`, or `cross-section`. |
| Selected component | `part` | Component id for details/highlight panels. |
| Guided tour | `tour`, `tourStep` | Tour id and zero-based step index. |
| Camera preset/vector | `cam`, `pos`, `target`, `zoom` | `pos` and `target` are comma-separated xyz triples. |
| Hidden components | `hide` | URI-escaped list. Duplicates are removed and ids are sorted. |
| Isolated components | `iso` | URI-escaped list. Isolation wins over hidden-state conflicts. |
| Component opacity | `op` | URI-escaped `componentId:opacity` entries. Opacity is clamped to `0–1`. |
| Labels | `labels` | Boolean label visibility. |
| Cross-section cut | `cut` | Encoded as `axis:offset:inverted`, for example `z:0.15:1`. |

## Basic usage

```ts
const query = encodeShareableAnimationState({
  machineId: 'planetary-gearbox',
  rpm: 120,
  playing: false,
  exploded: 0.35,
  renderMode: 'xray',
  camera: {
    preset: 'cutaway',
  },
  hiddenComponents: ['carrier', 'ring-gear'],
});

// Example output:
// v=1&m=planetary-gearbox&rpm=120&play=0&exp=0.35&mode=xray&cam=cutaway&hide=carrier%2Cring-gear
```

To read a link:

```ts
const decoded = decodeShareableAnimationState(window.location.href, {
  knownMachineIds: ['four-stroke-petrol-engine', 'planetary-gearbox'],
});

if (decoded.warnings.length > 0) {
  console.info('Some shared viewer-state fields were ignored or clamped', decoded.warnings);
}

const state = decoded.state;
```

The decoder accepts plain query strings, full URLs, `URLSearchParams`, record objects, and hash-routed SPA links such as:

```txt
/#/procedural-demo?m=geneva-drive&rpm=60&step=1
```

## Updating an existing URL

Use `createShareableAnimationSearchParams` when a route already has unrelated campaign, lesson, or navigation parameters. It removes previous animation-state keys and preserves everything else.

```ts
const params = createShareableAnimationSearchParams(window.location.search, {
  machineId: 'centrifugal-pump',
  rpm: 850,
  playing: true,
});

window.history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`);
```

## Safety behavior

The codec is intentionally forgiving:

- non-finite numbers are ignored;
- extreme values are clamped to safe limits;
- unknown ids can be rejected with `knownMachineIds`, `knownComponentIds`, `knownTourIds`, and related allow-lists;
- duplicate component ids are deduplicated;
- a component listed as both hidden and isolated is kept isolated and removed from the hidden list;
- reserved object keys such as `__proto__` are rejected before opacity maps are materialized.

Warnings are returned as structured objects so UI can show a non-blocking “link partially restored” message without crashing the viewer.
