# State management documentation

Mechanica uses Zustand for client state. The state model is intentionally split into small slices so catalogue UI, viewer settings, animation state, guided tours, and persisted user preferences can evolve independently.

## State principles

1. **Registry data is not mutable UI state.** Machine definitions, part metadata, facts, and tour steps live in the machine registry and are treated as immutable source data.
2. **Viewer state is serializable.** Anything needed for share links must be represented as plain JSON-compatible values.
3. **Three.js objects stay outside persisted stores.** Meshes, materials, cameras, and animation mixers should be referenced through React refs or ephemeral runtime maps, not localStorage.
4. **Persistence is versioned.** Any localStorage payload has a schema version and migration path.
5. **Selectors should be narrow.** Components subscribe only to the values they render or the actions they call.

## Recommended slice map

| Slice | Responsibility | Persisted |
| --- | --- | --- |
| `catalogue` | Search, category filters, difficulty filters, sorting. | Optional session-level only. |
| `favourites` | Bookmarked machine slugs. | Yes. |
| `viewer` | Active machine, selected part, camera preset, display mode, annotations, cross-section, exploded distance. | Partly via URL, partly local preference. |
| `parts` | Per-part visibility, opacity, hover/selection status. | Visibility/opacity can be encoded in URL for sharing. Hover is ephemeral. |
| `animation` | Playback status, RPM, time scale, cycle phase, step mode. | No, except optional user defaults. |
| `tour` | Active guided tour step, highlighted part IDs, caption state. | No. |
| `preferences` | Theme, reduced-motion override, panel density, diagnostics flags. | Yes. |

## Persisted localStorage keys

Use stable, namespaced keys:

```text
mechanica:favourites:v1
mechanica:preferences:v1
mechanica:viewer-defaults:v1
```

Persisted values should include a version field:

```ts
type PersistedPreferencesV1 = {
  version: 1;
  theme: 'dark' | 'system';
  reduceMotion: 'system' | 'always' | 'never';
  showAnnotationsByDefault: boolean;
  panelDensity: 'comfortable' | 'compact';
};
```

## URL-synchronized state

Shareable state should be compact and resilient to unknown values. Recommended query parameters:

| Param | Example | Meaning |
| --- | --- | --- |
| `machine` | `v8-engine` | Machine slug when the route itself does not encode it. |
| `cam` | `4.2,2.8,5.1,0,0.4,0` | Camera position xyz and target xyz. |
| `preset` | `iso` | Camera preset identifier. |
| `explode` | `0.65` | Normalized exploded-view amount from `0` to `1`. |
| `mode` | `solid` | `solid`, `wireframe`, or `xray`. |
| `clip` | `x:0.25` | Cross-section axis and normalized offset. |
| `labels` | `1` | Annotation labels enabled. |
| `hidden` | `piston,camshaft` | Comma-separated part IDs hidden in the shared view. |
| `opacity` | `housing:0.35,rotor:0.75` | Per-part opacity overrides. |
| `rpm` | `1200` | Animation RPM. |
| `speed` | `1.25` | Time scale. |
| `part` | `injector` | Selected part ID. |

The parser should ignore unsupported params, clamp numeric values, and drop part IDs not found in the active machine. This prevents old links from breaking after registry updates.

## Store action conventions

Use verbs for actions and keep all data validation at the action boundary.

Examples:

```ts
selectMachine(slug: string): void;
selectPart(partId: string | null): void;
setExplodedAmount(amount: number): void;
setPartVisibility(partId: string, visible: boolean): void;
setPartOpacity(partId: string, opacity: number): void;
setPlaybackState(state: 'playing' | 'paused' | 'stopped'): void;
setRpm(rpm: number): void;
resetViewerState(): void;
applySharedViewState(payload: SharedViewState): void;
```

Actions that accept user-provided values must clamp and normalize input before storing it.

## Animation state

The animation engine should own the simulation clock. Zustand stores only user intent:

```ts
type AnimationControlsState = {
  playback: 'playing' | 'paused' | 'stopped';
  rpm: number;
  timeScale: number;
  stepMode: boolean;
  requestedStep: number;
};
```

Machine animation modules read this state and update object transforms in the render loop. They should not write to the store every frame because that would cause avoidable React re-renders.

## Three.js runtime maps

For part interaction, keep a runtime map from `partId` to Three.js object refs:

```ts
type PartRuntimeRegistry = Map<string, THREE.Object3D>;
```

This map can live in a React context created by the viewer scene. Store only the selected and hovered `partId`, not the `Object3D`.

## Persistence migrations

When persisted state changes:

1. Increment the key suffix or internal `version`.
2. Provide a migration function for the previous version.
3. Validate migrated values before writing them back.
4. If migration fails, clear only that key and fall back to defaults.

Never silently reuse incompatible persisted state because stale viewer settings can make the scene appear broken.

## Reset behavior

Provide distinct reset actions:

- **Reset camera**: camera position and target only.
- **Reset display**: wireframe, cross-section, annotations, exploded amount, part visibility/opacity.
- **Reset machine session**: selected part, animation phase, active tour.
- **Reset all local preferences**: persisted favourites and preferences after user confirmation.

## Testing requirements

State utilities should have unit tests for:

- default values
- action clamping
- persistence migration
- URL parse/serialize round trips
- unknown machine and part IDs
- reset behavior
- reduced-motion preference precedence

Component tests should assert that controls call the correct store actions without depending on Three.js internals.
