Skip to content

Core Concepts

osdlabel is built as a layered monorepo of 7 separate packages, ensuring that the core data model and generic utilities have zero framework dependencies. The architecture is composed of three main layers:

  1. OpenSeaDragon — Manages the DZI/tiled image viewer, handling pan, zoom, and tile loading
  2. Fabric.js overlay — A transparent Fabric.js canvas positioned on top of each OSD viewer, synchronized on every animation frame (via @osdlabel/fabric-osd)
  3. SolidJS state & UI — Reactive stores that drive the UI, tool selection, constraints, and annotation data (the main osdlabel package)

For full details on how the packages are split, see the Packages & Architecture guide.

The overlay’s job is to compute a viewportTransform matrix that maps image-space coordinates to screen-space, so annotations stored in image pixels render correctly at any zoom level.

osdlabel uses four coordinate systems:

SystemOriginUnitsUsage
Image-spaceTop-left of full-res imagePixelsAnnotation geometry storage
OSD ViewportTop-left of viewportImage width = 1.0OSD internal calculations
Screen-spaceTop-left of browser viewportCSS pixelsMouse events, element positioning
Fabric canvasSame as screen-spaceCSS pixels (transformed)Rendering via viewportTransform

All annotation geometry (Geometry type) is stored in image-space. You never need to convert coordinates manually — the overlay handles the transform between image-space and screen-space automatically.

osdlabel uses TypeScript branded types for IDs to prevent accidental mixing:

type AnnotationId = string & { readonly __brand: unique symbol };
type ImageId = string & { readonly __brand: unique symbol };
type AnnotationContextId = string & { readonly __brand: unique symbol };

You cannot pass a plain string where a branded ID is expected. Use the factory functions:

import { createImageId, createAnnotationId } from '@osdlabel/annotation';
import { createAnnotationContextId } from '@osdlabel/annotation-context';
const imageId = createImageId('my-image');
const annotationId = createAnnotationId('ann-1');
const contextId = createAnnotationContextId('ctx-1');

A context represents a single annotation task — for example, “mark fractures” or “outline pneumothorax.” Each context defines:

  • Label — Human-readable name
  • Tools — Which drawing tools are available, optionally with count limits
  • Image scoping — Optionally restrict the context to specific images
  • Count scope — Whether limits apply per-image or globally

Only one context is active at a time. Switching contexts changes which tools are available in the toolbar.

Each grid cell contains an OpenSeaDragon viewer with a FabricOverlay on top. The overlay operates in two modes:

  • Navigation mode — OSD handles all mouse input (pan/zoom). Fabric is display-only.
  • Annotation mode — Fabric handles mouse input (draw/select/edit). OSD navigation is disabled, except for Ctrl/Cmd+drag (pan) and Ctrl/Cmd+scroll (zoom).

The active cell is in annotation mode; all other cells are in navigation mode.

osdlabel uses three SolidJS stores:

StoreContents
AnnotationStateAll annotations organized by image ID
UIStateActive tool, active cell, grid dimensions, grid assignments, selected annotation
ContextStateAvailable contexts and the active context ID

State is accessed via the useAnnotator() hook and mutated through named action functions. Components never modify stores directly.

SolidJS components run once to set up the view. Updates happen through signals and effects, not re-renders. This is critical for 60fps overlay synchronization:

  • createEffect synchronizes imperative libraries (OSD, Fabric) with reactive state
  • createMemo derives constraint status from annotation counts and context limits
  • The toolbar reads derived constraint state reactively — no imperative enable/disable logic