# How the OpenMAIC Whiteboard Runtime Handles Viewport Rendering and Browser Projection

> Discover how the OpenMAIC whiteboard runtime synchronizes on-screen canvas with data using viewport rendering and browser projection. Learn about its three-stage pipeline for seamless updates.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: internals
- Published: 2026-09-13

---

**The OpenMAIC whiteboard runtime employs a three-stage pipeline—encompassing viewport geometry normalization, reactive canvas store state management, and generation-guarded browser projection—to ensure the on-screen canvas remains synchronized with persisted whiteboard data across sessions.**

The THU-MAIC/OpenMAIC platform manages complex whiteboard synchronization challenges by decoupling viewport rendering logic from runtime data persistence. Understanding how this architecture handles viewport rendering and browser projection is essential for developers extending whiteboard functionality or debugging geometry inconsistencies.

## Normalizing Viewport Geometry for Consistent Rendering

Every whiteboard payload carries two critical dimension fields: `viewportSize` (the base width in pixels) and `viewportRatio` (height divided by width). Historical persistence layers occasionally stored the inverse ratio or values outside plausible bounds, necessitating robust normalization before rendering.

### Correcting Historical Data Anomalies

The helper function `normalizeWhiteboardViewportRatio` in **[[`lib/whiteboard/viewport.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/whiteboard/viewport.ts)](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/whiteboard/viewport.ts)** corrects legacy data through three deterministic steps:

- **Reciprocation**: Any ratio greater than 1 is inverted to enforce the height/width convention.
- **Clamping**: The result is constrained to the accepted band of 0.4–1.0 to prevent extreme aspect ratios.
- **Fallback**: Non-finite values default to the canonical 16:9 ratio (9/16 ≈ 0.5625).

```ts
import { normalizeWhiteboardViewportRatio } from '@/lib/whiteboard/viewport';

const rawRatio = 1.777777;   // stored as width/height (16:9)
const saneRatio = normalizeWhiteboardViewportRatio(rawRatio);
// saneRatio === 0.5625 (height/width)

```

## Managing Canvas State with the Viewport Store

The UI layer maintains reactive viewport dimensions within the **canvas store** (`useCanvasStore`), defined in **[[`lib/store/canvas.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/store/canvas.ts)](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/store/canvas.ts)**. This centralized state enables consistent coordinate calculations across components.

### State Structure and Reactive Updates

The store shape (lines 28–33) exposes `viewportSize` (defaulting to 1000px) and `viewportRatio`, while the actions slice (lines 84–93) provides `setViewportSize` and `setViewportRatio` for mutation. When a slide or whiteboard loads, these setters initialize the rendering context:

```ts
import { useCanvasStore } from '@/lib/store/canvas';

function applySlideViewport(slide) {
  const { viewportSize, viewportRatio } = slide;
  useCanvasStore.getState().setViewportSize(viewportSize);
  useCanvasStore.getState().setViewportRatio(viewportRatio);
}

```

### Generation Tokens for Projection Safety

To prevent race conditions, the store exposes `beginRuntimeWhiteboardProjection`, which creates a unique **generation token**. This token guards against stale updates by validating that incoming projection data matches the current rendering lifecycle.

## Synchronizing Browser Projections at Runtime

The runtime service publishes **whiteboard projections** containing the latest `whiteboard` object and a monotonic `lastSeq` marker. The client-side function `refreshWhiteboardRuntimeProjection` in **[[`lib/whiteboard/runtime/browser-projection.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/whiteboard/runtime/browser-projection.ts)](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/whiteboard/runtime/browser-projection.ts)** orchestrates the synchronization handshake.

### The Projection Refresh Pipeline

The function executes a five-stage validation sequence:

1. **Early Exit**: If browser persistence is disabled, the store clears cached projection data immediately.
2. **Generation Initialization**: Invokes `useCanvasStore.getState().beginRuntimeWhiteboardProjection(stageId)` to obtain a fresh generation token.
3. **Fetch**: Retrieves the projection via `getWhiteboardRuntimeService().read(stageId)`.
4. **Validation**: Confirms the response matches the current `stageId` and generation, optionally enforcing a `minimumLastSeq` constraint.
5. **Atomic Commit**: Compares the incoming `lastSeq` with the cached projection; if newer, commits via `canvas.setRuntimeWhiteboardProjection({stageId, lastSeq, whiteboard})`.

### Conflict Resolution and Error Boundaries

The function returns a boolean indicating whether the store was updated (`true`) or ignored due to stale data (`false`). All network and parsing errors are swallowed to prevent UI crashes, ensuring that temporary runtime service failures do not corrupt the whiteboard state.

```ts
import { refreshWhiteboardRuntimeProjection } from '@/lib/whiteboard/runtime/browser-projection';

// Pull the latest version for stage 'stage-1', only accept sequences ≥ 5
await refreshWhiteboardRuntimeProjection('stage-1', 5);
// → updates `runtimeWhiteboardProjection` in the canvas store if newer data is available

```

## Integrating with the Rendering Pipeline

The actual pixel calculation occurs in the preview renderer. As implemented in **[[`render-service/src/preview-renderer.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/render-service/src/preview-renderer.ts)](https://github.com/THU-MAIC/OpenMAIC/blob/main/render-service/src/preview-renderer.ts)** (line 258), the renderer reads `canvas.viewportSize` and `canvas.viewportRatio` from the store to compute element positions in screen space, ensuring that whiteboard annotations render at correct scale regardless of client display density.

## Summary

- **Viewport Normalization**: The `normalizeWhiteboardViewportRatio` function in [`lib/whiteboard/viewport.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/whiteboard/viewport.ts) sanitizes legacy aspect ratios to the 0.4–1.0 range with a 16:9 fallback.
- **State Management**: The `useCanvasStore` in [`lib/store/canvas.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/store/canvas.ts) holds the canonical `viewportSize` and `viewportRatio`, protecting updates with generation tokens.
- **Browser Synchronization**: The `refreshWhiteboardRuntimeProjection` function sequences network fetches and validates `lastSeq` markers to prevent out-of-order updates.
- **Render Integration**: The preview renderer consumes store state to map logical whiteboard coordinates to physical pixels.

## Frequently Asked Questions

### How does OpenMAIC handle invalid viewport ratios in legacy data?

The `normalizeWhiteboardViewportRatio` function reciprocates values greater than 1 (converting width/height to height/width), clamps the result between 0.4 and 1.0, and substitutes the standard 16:9 ratio (0.5625) when encountering non-finite numbers. This ensures all downstream rendering calculations operate on consistent geometric assumptions.

### What prevents stale whiteboard data from overwriting newer projections?

The runtime employs **generation tokens** created by `beginRuntimeWhiteboardProjection` combined with sequence number validation (`lastSeq`). Each fetch validates the response against the current generation and discards any payload with a sequence number older than the cached projection, effectively silencing out-of-order network responses.

### Where is the viewport size configured in the OpenMAIC architecture?

Viewport dimensions reside in the **canvas store** (`useCanvasStore`) defined in **[`lib/store/canvas.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/store/canvas.ts)**, specifically within lines 28–33 for state shape and lines 84–93 for setter actions. The default `viewportSize` initializes to 1000px unless overridden by slide-specific metadata.

### How does the browser client know when to refresh the whiteboard projection?

The `refreshWhiteboardRuntimeProjection` function in **[`lib/whiteboard/runtime/browser-projection.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/whiteboard/runtime/browser-projection.ts)** polls the whiteboard runtime service and performs an atomic comparison between the incoming `lastSeq` and the cached sequence number. The store updates only when the fetched projection represents newer authoritative state, ensuring the browser reflects the most recent server-side changes.