# Canvas API — Developer Reference

Reference documentation for all `updateData` payloads accepted by `<Canvas />` and the internal helpers that process them.

---

## Table of Contents

- [How Updates Work](#how-updates-work)
- [Update Types Quick Reference](#update-types-quick-reference)
- [Payloads](#payloads)
  - [Scene](#scene)
  - [Camera](#camera)
  - [Environment](#environment)
  - [Mesh Properties](#mesh-properties)
  - [Animations](#animations)
  - [Materials](#materials)
  - [Viewport Visibility](#viewport-visibility)
  - [Publishing](#publishing)
- [Routing Reference](#routing-reference)

---

## How Updates Work

Every update is dispatched via `updateCanvas` with a shape of `{ type, method, ...payload }`.

```jsx
import { useDispatch } from 'react-redux';
import constants from 'constants/index';

const { CANVAS_UPDATE_TYPES, CANVAS_UPDATE_METHODS } = constants;
const dispatch = useDispatch();

// Preset args for each method
const updateArgs = { dispatch, type: CANVAS_UPDATE_TYPES.MESH,   method: CANVAS_UPDATE_METHODS.UPDATE };
const addArgs    = { dispatch, type: CANVAS_UPDATE_TYPES.MESH,   method: CANVAS_UPDATE_METHODS.ADD    };
const removeArgs = { dispatch, type: CANVAS_UPDATE_TYPES.MESH,   method: CANVAS_UPDATE_METHODS.REMOVE };

updateCanvas(updateArgs, inboundData);
```

After every update, `canvasCommunicationHelpers` automatically serializes the scene and returns the new state to the host via `setSerializedData`. **Data must always flow `event → Canvas → Redux → component`.** Never hold canvas data in local component state.

All type and method constants are defined in `/src/package/constants/constants`.

---

## Update Types Quick Reference

| Type | Constant | Handles |
|---|---|---|
| `mesh` | `CANVAS_UPDATE_TYPES.MESH` | Transforms, visibility, variants, animations, bounding box |
| `material` | `CANVAS_UPDATE_TYPES.MATERIAL` | PBR properties, textures, UV mapping, clear-coat, transparency |
| `camera` | `CANVAS_UPDATE_TYPES.CAMERA` | Orthographic mode, safe frame, custom views, screenshots |
| `environment` | `CANVAS_UPDATE_TYPES.ENVIRONMENT` | HDR environments, rotation, intensity, blur |
| `viewport` | `CANVAS_UPDATE_TYPES.VIEWPORT` | Grid, axes, shadows, environment background |
| `publish` | `CANVAS_UPDATE_TYPES.PUBLISH` | Metadata, proxy, mesh simplification, texture downscaling |

---

## Payloads

### Scene

#### Units

Sets the unit system used for source data or the display UI. All values go through `type: mesh, method: update`.

| Field | Type | Values |
|---|---|---|
| `sourceUnit` | `string` | `'unitless'` `'mm'` `'cm'` `'m'` `'in'` `'ft'` `'yd'` |
| `displayUnit` | `string` | same as above |

```jsx
updateCanvas(updateArgs, { sourceUnit: 'cm' });
updateCanvas(updateArgs, { displayUnit: 'in' });
```

#### Axis Compensation

Corrects the model's up-axis orientation. Pass a predefined `axisCompensation` value and the corresponding rotation offset.

```jsx
// Rotate based on selected axis
const transforms = newVal === zAxisUp
  ? { rx: -90, ry: 0, rz: 0 }
  : { rx:  90, ry: 0, rz: 0 };

updateCanvas(updateArgs, { axisCompensation: newVal, transforms });

// Mirror model along the horizontal axis
updateCanvas(updateArgs, { flipHorizontally: true });
```

#### File Name

Updates the title shown in the app header.

```jsx
updateCanvas(updateArgs, { title: inputVal });
```

#### Mesh Variants

Switches the active glTF material variant or resets to the default.

```jsx
// Activate a variant
updateCanvas(updateArgs, { variantName: newVal });

// Revert to default variant
updateCanvas(updateArgs, { resetVariant: true });
```

---

### Camera

All camera payloads use `type: camera`.

```jsx
// Switch to orthographic projection
updateCanvas(updateArgs, { orthoCamera: button.value });

// Toggle safe frame overlay
const enableSafeFrame = selectedRatio.value !== ratio.value || !safeFrameEnabled;
updateCanvas(updateArgs, { enableSafeFrame, aspectRatio: ratio.value });

// Save the current view as a named camera
updateCanvas(addArgs, { id: nameInputValue });

// Restore a saved view (or reset to default if not found)
const updateView = userCameraViews.find(view => view.id === newVal);
let inboundData = { resetCamera: true };

if (updateView) {
  const { id, ...rest } = updateView;
  inboundData = { id, transforms: rest };
}
updateCanvas(updateArgs, inboundData);

// Reset camera to default position
updateCanvas(updateArgs, { resetCamera: true });

// Take a screenshot
updateCanvas(updateArgs, {
  takeScreenshot: true,
  aspectRatio: selectedRatio.value || ratios.broadcast.value,
  // imageDataOnly: true  — returns image data only, skips download
});

// Delete a saved camera view
updateCanvas(removeArgs, { id: selectedUserCameraView.id });
```

---

### Environment

All environment payloads use `type: environment`, except where noted.

```jsx
// Load a preset environment
updateCanvas(updateArgs, { url: envURL, transforms: defaultRotation });

// Load a custom environment (only if not already active)
if (!defaultEnvActive || newVal !== selectedUserEnvironment.url) {
  updateCanvas(updateArgs, { url: newVal, transforms: defaultRotation });
}

// Upload and register a new custom environment
const file = e.target.files[0];
const envURL = URL.createObjectURL(file);
const newEnvironment = { id: file.name, url: envURL };
const exists = userEnvironments.find(env => env.id === file.name);

if (!exists) updateCanvas(addArgs, newEnvironment);

// Rotate the environment
updateCanvas(updateArgs, { transforms: { rotation: { ry: newVal } } });

// Adjust intensity (0–1)
updateCanvas(updateArgs, { envIntensity: newVal });

// Adjust background blur (0–1)
updateCanvas(updateArgs, { envBlur: newVal });

// Toggle environment background visibility  (type: VIEWPORT)
updateCanvas(updateViewportArgs, { envVisible: newVal });

// Remove a custom environment
updateCanvas(removeArgs, { id: selectedUserEnvironment?.id });
```

---

### Mesh Properties

All mesh payloads use `type: mesh`.

#### Transforms

| Field | Type | Description |
|---|---|---|
| `id` | `string` | Target mesh ID |
| `globalTransform` | `boolean` | Apply in world space instead of local space |
| `transforms.tx/ty/tz` | `number` | Position |
| `transforms.rx/ry/rz` | `number` | Rotation in degrees |
| `transforms.sx/sy/sz` | `number` | Scale |

```jsx
// Move
updateCanvas(updateArgs, {
  id: selectedMeshId,
  globalTransform: isGlobalTransform,
  transforms: { tx: xPosition, ty: yPosition, tz: zPosition, [key]: +(value) },
});

// Rotate
updateCanvas(updateArgs, {
  id: selectedMeshId,
  globalTransform: isGlobalTransform,
  transforms: { rx: xRotation, ry: yRotation, rz: zRotation, [key]: +(value) },
});

// Scale
updateCanvas(updateArgs, {
  id: selectedMeshId,
  globalTransform: isGlobalTransform,
  transforms: {
    sx: scaleLinking ? +(value) : xScale,
    sy: scaleLinking ? +(value) : yScale,
    sz: scaleLinking ? +(value) : zScale,
  },
});

// Save current position as a named snapshot
updateCanvas(addArgs, { id: nameInputValue });

// Restore a saved position
updateCanvas(updateArgs, { id: newVal, useSavedPosition: true });

// Delete a saved position
updateCanvas(removeArgs, { id: selectedMeshPositionName });
```

#### Bounding Box

```jsx
updateCanvas(updateArgs, { boundingBoxEnabled: newVal });
```

#### Visibility

```jsx
updateCanvas(updateArgs, { hideSelected: true });
updateCanvas(updateArgs, { unhideLast: true });
updateCanvas(updateArgs, { unhideAll: true });
updateCanvas(updateArgs, { deselectAll: true });
updateCanvas(removeArgs, { deleteSelectedMesh: true });
```

---

### Animations

All animation payloads use `type: mesh, method: update`.

#### Playback Controls

| Field | Type | Default | Description |
|---|---|---|---|
| `animationId` | `string` | — | Index of the animation group to activate |
| `loopAnimation` | `boolean` | `true` | Whether the animation loops |
| `playAnimation` | `boolean` | — | Resume the active animation |
| `pauseAnimation` | `boolean` | — | Pause the active animation |
| `seekAnimation` | `boolean` | — | Seek to a specific position (requires `animationProgress`) |
| `animationProgress` | `number` (0–1) | — | Target position as a fraction of total duration |
| `resetAnimation` | `boolean` | — | Stop and deselect the active animation |

```jsx
// Activate an animation by index
updateCanvas(updateArgs, { animationId: '0', loopAnimation: true });

// Play the active animation
updateCanvas(updateArgs, { playAnimation: true, loopAnimation: true });

// Pause the active animation
updateCanvas(updateArgs, { pauseAnimation: true });

// Seek to 50% through the animation
updateCanvas(updateArgs, { seekAnimation: true, animationProgress: 0.5 });

// Stop and deselect the current animation
updateCanvas(updateArgs, { resetAnimation: true });
```

#### Available Animations

The list of animation groups in the loaded model is returned via `setSerializedData` as `scene.metadata.animations`. Each entry has the following shape:

| Field | Type | Description |
|---|---|---|
| `id` | `string` | Index stringified (e.g. `'0'`, `'1'`) |
| `index` | `number` | Numeric index in `scene.animationGroups` |
| `label` | `string` | Display name — falls back to `'Animation N'` if unnamed |
| `name` | `string` | Raw `group.name` from Babylon |

#### Serialized Playback State

After every animation operation — and continuously during playback (throttled to 50 ms) — `setSerializedData` is called with the following fields in `scene.metadata`:

| Field | Type | Description |
|---|---|---|
| `selectedAnimation` | `string \| null` | Index of the active animation group, or `null` |
| `selectedAnimationIsPlaying` | `boolean` | Whether the animation is currently playing |
| `selectedAnimationProgress` | `number` (0–1) | Current position as a fraction of total duration |
| `selectedAnimationTime` | `number` | Current playback time in seconds |
| `selectedAnimationDuration` | `number` | Total duration in seconds |

> **Note:** During active playback, state is pushed via a `onBeforeRenderObservable` observer and throttled to at most one update every **50 ms** to avoid flooding the host with Redux dispatches.

---

### Materials

All material payloads use `type: material` and require `id: selectedMaterialId` unless noted.

#### General

```jsx
// Rename a material
updateCanvas(updateArgs, { id: selectedMaterialId, newId: nameInputValue });

// Set the active material in the editor
updateCanvas(updateArgs, { id: newVal, newSelectedMaterial: true });

// Select all meshes using this material
updateCanvas(updateArgs, { id: selectedMaterialId, selectMaterial: true });

// Clear all texture channels at once
updateCanvas(updateArgs, { id: selectedMaterialId, clearTextureChannels: true });
```

#### Color

| Field | Type | Description |
|---|---|---|
| `environmentIntensity` | `number` | Environment light contribution |
| `albedoColor` | `Color` | Base colour |
| `albedoTexture` | `File` | Base colour texture |
| `metallic` | `number` (0–1) | Metallic factor |
| `metallicTexture` | `File` | Metallic texture |

```jsx
updateCanvas(updateArgs, { id: selectedMaterialId, environmentIntensity: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, albedoColor: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, albedoTexture: newVal });
updateCanvas(removeArgs, { id: selectedMaterialId, removeAlbedoTexture: true });
updateCanvas(updateArgs, { id: selectedMaterialId, metallic: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, metallicTexture: newVal });
updateCanvas(removeArgs, { id: selectedMaterialId, removeMetallicTexture: true });
```

#### Roughness

```jsx
updateCanvas(updateArgs, { id: selectedMaterialId, roughness: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, microSurfaceTexture: newVal });
updateCanvas(removeArgs, { id: selectedMaterialId, removeMicroSurfaceTexture: true });
```

#### Normal / Bump

| Field | Type | Description |
|---|---|---|
| `bumpIntensity` | `number` | Normal map strength |
| `bumpTexture` | `File` | Normal map texture |

```jsx
updateCanvas(updateArgs, { id: selectedMaterialId, bumpIntensity: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, bumpTexture: newVal });
updateCanvas(removeArgs, { id: selectedMaterialId, removeBumpTexture: true });
```

#### Emissive

```jsx
updateCanvas(updateArgs, { id: selectedMaterialId, emissiveIntensity: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, emissiveColor: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, emissiveTexture: newVal });
updateCanvas(removeArgs, { id: selectedMaterialId, removeEmissiveTexture: true });
```

#### Transparency

| Field | Type | Description |
|---|---|---|
| `transparencyEnabled` | `boolean` | Enable/disable transparency |
| `transparencyType` | `string` | Blend mode |
| `alpha` | `number` (0–1) | Opacity |
| `opacityTexture` | `File` | Opacity map |
| `sssRefractionIntensity` | `number` | Sub-surface scattering refraction strength |
| `sssIOR` | `number` | Index of refraction |

```jsx
updateCanvas(updateArgs, { id: selectedMaterialId, transparencyEnabled: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, transparencyType: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, alpha: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, opacityTexture: newVal });
updateCanvas(removeArgs, { id: selectedMaterialId, removeOpacityTexture: true });
updateCanvas(updateArgs, { id: selectedMaterialId, sssRefractionIntensity: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, sssIOR: newVal });
```

#### Clear-Coat

| Field | Type | Description |
|---|---|---|
| `clearCoatEnabled` | `boolean` | Enable/disable clear-coat layer |
| `clearCoatIntensity` | `number` | Clear-coat strength |
| `clearCoatRoughness` | `number` | Clear-coat roughness |
| `clearCoatIOR` | `number` | Clear-coat index of refraction |

```jsx
updateCanvas(updateArgs, { id: selectedMaterialId, clearCoatEnabled: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, clearCoatIntensity: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, clearCoatRoughness: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, clearCoatIOR: newVal });
```

#### UV Mapping

| Field | Type | Description |
|---|---|---|
| `uvXScale` / `uvYScale` | `number` | Tiling scale per axis |
| `uvScaleLock` | `boolean` | Lock Y scale to X (uniform tiling) |
| `uvXOffset` / `uvYOffset` | `number` | UV offset per axis |
| `uvXRotation` / `uvYRotation` / `uvZRotation` | `number` | UV rotation per axis |

```jsx
updateCanvas(updateArgs, { id: selectedMaterialId, uvXScale: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, uvYScale: newVal });

// Uniform scale — locks Y to X
updateCanvas(updateArgs, { id: selectedMaterialId, uvXScale: newVal, uvScaleLock: true });

updateCanvas(updateArgs, { id: selectedMaterialId, uvXOffset: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, uvYOffset: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, uvXRotation: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, uvYRotation: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, uvZRotation: newVal });
```

#### Texture Downscaling

```jsx
updateCanvas(updateArgs, { id: selectedMaterialId, textureDownscaling: { enabled: newVal } });
updateCanvas(updateArgs, { id: selectedMaterialId, textureDownscaling: { size: +(newVal) } });
```

#### Render Flags

```jsx
updateCanvas(updateArgs, { id: selectedMaterialId, backfaceCullingEnabled: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, wireFrameEnabled: newVal });
updateCanvas(updateArgs, { id: selectedMaterialId, pointsCloudEnabled: newVal });
```

#### Mesh Simplification (per material)

```jsx
// Toggle simplification
updateCanvas(updateArgs, { id: selectedMaterialId, meshSimplification: { enabled: newVal } });

// Set optimization type and estimated triangles
updateCanvas(updateArgs, {
  id: selectedMaterialId,
  meshSimplification: { type: option, estimatedTriangles: getMultiplier(option) },
});

// Set optimization algorithm
updateCanvas(updateArgs, {
  id: selectedMaterialId,
  meshSimplification: { optimizationType: newVal },
});

// Set target triangle count
const key = basedOnEstimate ? 'estimatedTriangles' : 'userTargetTriangles';
updateCanvas(updateArgs, {
  id: selectedMaterialId,
  meshSimplification: { [key]: +(e.target.value) },
});
```

---

### Viewport Visibility

Controls scene overlay elements. Uses `type: viewport`.

| Field | Type | Description |
|---|---|---|
| `envVisible` | `boolean` | Show/hide environment background |
| `grid` | `boolean` | Show/hide grid |
| `axes` | `boolean` | Show/hide axis helper |
| `shadows` | `boolean` | Enable/disable shadow rendering |

```jsx
updateCanvas(updateArgs, { envVisible: newVal });
updateCanvas(updateArgs, { grid: newVal });
updateCanvas(updateArgs, { axes: newVal });
updateCanvas(updateArgs, { shadows: newVal });
```

---

### Publishing

Uses `type: publish`.

#### Metadata

```jsx
updateCanvas(updateArgs, { title: e.target.value });
updateCanvas(updateArgs, { category: newVal });
updateCanvas(updateArgs, { tags: !_.isEmpty(tags) ? tags.split(',') : [] });
```

#### Texture Downscaling (global)

```jsx
updateCanvas(updateArgs, { textureDownscaling: { enabled: newVal } });
updateCanvas(updateArgs, { textureDownscaling: { size: +(newVal) } });
```

#### Mesh Simplification (global)

```jsx
updateCanvas(updateArgs, { meshSimplification: { enabled: newVal } });
```

#### Proxy

```jsx
updateCanvas(updateArgs, { proxy: newVal });
updateCanvas(updateArgs, { meshSimplification: { decimateAmount: newVal } });
updateCanvas(updateArgs, { meshSimplification: { isControl: newVal } });
```

---

## Routing Reference

`canvasCommunicationHelpers` routes each payload to a helper function based on `type` and `method`. All helpers are located in `/src/package/helpers/`.

| Type | `update` | `add` | `remove` |
|---|---|---|---|
| `material` | `updateMaterial` | `addToMaterial` | `removeFromMaterial` |
| `mesh` | `updateMesh` | `addToMesh` | `removeFromMesh` |
| `camera` | `updateCamera` | `addToCamera` | `removeFromCamera` |
| `environment` | `updateEnvironment` | `addToEnvironment` | `removeFromEnvironment` |
| `viewport` | `updateViewport` | `addToViewport` | `removeFromViewport` |
| `publish` | `updatePublish` | — | — |

---

## Navigation Baseline Harness

The local dev app renders the generated canonical navigation scene only at
`http://localhost:3000?navigationBaseline=true`. Playwright loads that scene in
Chromium, replays deterministic browser interactions, and compares rendered canvas
PNGs against `goldens/navigation/canonical-scene/`.

```text
npm run test:e2e
npm run test:e2e:update
```
