How the OpenMAIC Whiteboard Runtime Handles Viewport Rendering and Browser Projection

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) 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).
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). 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:

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) 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.

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) (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 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 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, 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →