Message API/docs/components/measurement/scale

Scale Settings

Set document scale to ensure accurate measurements on the canvas.

The Mental Model

To ensure scaling and measurements work reliably, always follow this integration flow:

  • 1. Load Viewer: Initialize the viewer iframe.
  • 2. Load File: Send the `view` message to load a document.
  • 3. Apply Scale: Use `addScale` only AFTER the file is loaded.
  • 4. Activate Tools: Finally, enable measurement tools.

Lifecycle & Order

Scale logic is context-aware. You cannot apply a scale unless a document is active in the viewer. If you call `addScale` before `view`, the settings will be ignored.

Overview

Scale defines the relationship between canvas pixels and real-world dimensions. Without it, measurements like length and area cannot be calculated accurately.

  • Two-way communication: Request current scales or push new scales.
  • Metric and Imperial: Native support for both unit systems.
  • Flexible scoping: Apply scales globally, per-page, or by page range.

Quick Start

Send a scale to the viewer and listen for the confirmation.

javascript
// 1. Send a scale to the viewer
iframe.contentWindow.postMessage({
  type: 'addScale',
  payload: {
    fileIndex: 0,
    scale: {
      label: "1 m : 10 m",
      value: "1:10",
      metric: "0",           // "0" = Metric, "1" = Imperial
      metricUnit: "Meter",
      dimPrecision: 2,
      isSelected: true
    }
  }
}, '*');

// 2. Listen for the snapshot confirmation
window.addEventListener('message', (event) => {
  if (event.data.type === 'scalesSnapshot') {
    console.log('Active scale:', event.data.payload.selectedLabel);
  }
});
Quick Start

Scale Properties

Most integration issues stem from understanding the difference between ratio values and multipliers.

  • label: Display name shown to users (e.g., `"1 m : 10 m"`)
  • value: The mathematical ratio as a string (e.g., `"1:10"`).
  • metric: Unit system - `"0"` for Metric, `"1"` for Imperial
  • metricUnit: The exact unit name (e.g., `"Meter"`, `"Feet"`). Strict naming required.
  • dimPrecision: Decimal places for rounding (0-4)
  • isSelected: Set to `true` to activate this scale immediately

Supported Units

IMPORTANT: You must use these exact unit names. Using shortcodes like "m" or "ft" will result in a silent failure.

  • Metric: `Millimeter`, `Centimeter`, `Decimeter`, `Meter`, `Kilometer`
  • Imperial: `Inch`, `Feet`, `Yard`, `Mile`, `Nautical Miles`

Communication Flow

The scale system uses a request-response pattern:

Host Sends

1. Request Current Scales

Ask the viewer for the current scale state.

javascript
iframe.contentWindow.postMessage({
  type: 'getScales',
  payload: { fileIndex: 0 }
}, '*');
Canvas Emits

2. Receive Snapshot

The viewer sends back the complete scale state.

javascript
// event.data.payload
{
  "fileIndex": 1,
  "fileName": "sample.pdf",
  "selectedLabel": "1 mm : 12 mm",
  "scales": [
    {
      "value": "1:12",
      "label": "1 mm : 12 mm",
      "metric": "0",
      "metricUnit": "Millimeter",
      "dimPrecision": 2,
      "isSelected": true,
      "source": "calibrate"
    }
  ]
}
Host Sends

3. Update Scale

Apply the scale only AFTER receiving a file load event.

javascript
iframe.contentWindow.postMessage({
  type: 'addScale',
  payload: {
    fileIndex: 0,
    scale: {
      label: "1 m : 10 m",
      value: "1:10",
      metric: "0",
      metricUnit: "Meter",
      dimPrecision: 2,
      isSelected: true
    }
  }
}, '*');

Advanced Scaling

For complex layouts, you can apply scales to specific page ranges or handle architectural fractions.

javascript
// Page-Specific Scale (applies only to pages 1-5 and page 9)
iframe.contentWindow.postMessage({
  type: 'addScale',
  payload: {
    scale: {
      label: "Detail Scale",
      value: "1:5",
      metric: "0",
      metricUnit: "Meter",
      dimPrecision: 2,
      isSelected: true,
      pageRanges: [[1, 5], [9, 9]]  // Pages 1-5 and 9
    }
  }
}, '*');

// Imperial Architectural Scale (1/8" = 1')
iframe.contentWindow.postMessage({
  type: 'addScale',
  payload: {
    scale: {
      label: "1/8\" = 1'",
      value: "1:96",
      metric: "1",                  // Imperial
      metricUnit: "Feet",
      dimPrecision: 2,
      isSelected: true,
      imperialNumerator: 1,        // 1/8 inch
      imperialDenominator: 8
    }
  }
}, '*');
Advanced Scaling

Calibration Workflow

The current calibration flow is form-driven from the host application. The user opens a file, switches the scale panel to `Calibrate`, enters the known real-world length, starts calibration, picks the reference line in the Viewer, waits for `calibrationFinished`, and then sends `completeCalibration`.

This is not a passive scale label change. Calibration is a separate request-driven workflow with start, finish, and complete steps.

  • Blocked until file active: calibration does not start before the file is loaded.
  • Viewer pick required: after `startCalibration`, the user must pick a reference line in the Viewer.
  • Completion is gated: `completeCalibration` is blocked until `calibrationFinished` has been received.
  • Scale refresh follows: the host uses the next `scalesSnapshot` to refresh the saved scales list.

Start Calibration

The host starts calibration by sending `startCalibration` with a generated request id and the active file index.

const requestId = `cal-${Date.now()}`;

iframe.contentWindow?.postMessage({
  type: 'startCalibration',
  payload: {
    requestId,
    fileIndex: 0
  }
}, '*');
Start Calibration

Viewer Completion Event

After the user picks the calibration reference in the Viewer, the host expects `calibrationFinished`.

The current host accepts the event only when `isFinished` is truthy and the incoming request id matches the active calibration request when one is already tracked.

  • Measured length is reused: the current host stores the measured Viewer length back into the calibration draft field.
  • Session stays active: calibration remains active until the host completes or cancels it.
{
  type: 'calibrationFinished',
  payload: {
    requestId: 'cal-123',
    fileIndex: 0,
    fileName: 'drawing.pdf',
    isFinished: true,
    measuredLength: 137.96
  }
}
Viewer Completion Event

Complete Calibration

The host finishes calibration by sending `completeCalibration` after `calibrationFinished` has been received and the draft values validate.

Host Sends

1. Host sends completeCalibration

The host builds the payload from the calibration draft and the active file index.

javascript
iframe.contentWindow?.postMessage({
  type: 'completeCalibration',
  payload
}, '*');
Canvas Emits

2. Viewer returns scalesSnapshot

The current app relies on the next scales snapshot to refresh the saved scale state after calibration.

javascript
window.addEventListener('message', (event) => {
  if (event.data?.type !== 'scalesSnapshot') return;

  console.log('Calibrated scale snapshot:', event.data.payload);
});

Metric And Imperial Calibration Payloads

The current calibration implementation builds different completion payloads for Metric and Imperial systems.

  • Metric validation: `calibrateLength` must parse as a positive number.
  • Imperial validation: `calibrateLength` must parse as a positive number, and `calibrateLengthFraction` must parse when provided.
  • Current page metadata is fixed: the current host sends `pageRanges: [[1, 1]]` and `totalPages: 1` during completion.

Metric Complete Payload

{
  requestId,
  fileIndex,
  metric: '0',
  metricUnit: draft.unit,
  precision: draft.precision,
  calibrateLength: '<positive number>',
  currentPageMetricUnitLabel: draft.unit,
  pageRanges: [[1, 1]],
  totalPages: 1
}

Imperial Complete Payload

{
  requestId,
  fileIndex,
  metric: '1',
  metricUnit: draft.unit,
  metricUnitFraction: draft.imperialFractionUnit,
  precision: draft.precision,
  calibrateLength: '<positive number>',
  calibrateLengthFraction: '<number>',
  currentPageMetricUnitLabel: draft.imperialFractionUnit,
  pageRanges: [[1, 1]],
  totalPages: 1
}

Calibration UI

The current scale panel exposes calibration through explicit host controls rather than an automatic canvas wizard.

The panel keeps `Complete Calibration` disabled until a measured length is available, and shows a waiting state until the Viewer returns `calibrationFinished`.

  • Mode switch: `Manual Scale` and `Calibrated Scale`
  • System switch: `Metric` and `Imperial`
  • Metric inputs: calibration length plus display unit
  • Imperial inputs: calibration length, fractional page value, and fraction unit
  • Actions: `Start Calibration`, `Complete Calibration`, and `Cancel`
  • Status: measured-length readout while calibration is active

Current Limitations

These notes reflect the behavior currently documented for calibration in the app.

  • File required: calibration depends on an active file.
  • Completion depends on finish event: the host does not complete calibration before `calibrationFinished` is received.
  • Fixed page metadata: the current host hardcodes `pageRanges` and `totalPages` during completion.
  • Measured length overwrite: the measured Viewer length is written back into the draft calibration length field.
  • Snapshot is the confirmation path: the docs do not assume a separate save-success event beyond the later `scalesSnapshot` refresh.

Complete Example

A production-ready implementation with initialization and event handling.

const iframe = document.querySelector('[data-viewer-iframe]');

// 1. Request scales when iframe loads
iframe.addEventListener('load', () => {
  iframe.contentWindow.postMessage({ 
    type: 'getScales', 
    payload: { fileIndex: 0 } 
  }, '*');
});

// 2. Listen for scale updates
window.addEventListener('message', (event) => {
  if (event.data.type === 'scalesSnapshot') {
    const { scales, selectedLabel } = event.data.payload;
    console.log('Document calibrated with:', selectedLabel);
    updateScaleUI(scales, selectedLabel);
  }
});

// 3. Set a new scale
// 1. Load a file first
function loadFile() {
  iframe.contentWindow.postMessage({
    type: 'view',
    payload: { fileUrl: 'https://example.com/document.pdf' }
  }, '*');
}

// 2. Set scale AFTER the file is active (usually in a 'fileInfo' listener)
function setScale() {
  iframe.contentWindow.postMessage({
    type: 'addScale',
    payload: {
      fileIndex: 0,
      scale: {
        label: "1 m : 10 m",
        value: "1:10",
        metric: "0",
        metricUnit: "Meter",
        dimPrecision: 2,
        isSelected: true
      }
    }
  }, '*');
}

// 3. Listen for confirmations
window.addEventListener('message', (event) => {
  if (event.data.type === 'fileInfo') {
    console.log('File ready, applying scale...');
    setScale();
  }
  if (event.data.type === 'scalesSnapshot') {
    console.log('Scale active:', event.data.payload.selectedLabel);
  }
});
Complete Example

Full Type Definition

The complete ViewerScale interface with all available properties.

typescript
interface ViewerScale {
  label: string;             // Display name (e.g., "1 m : 10 m")
  value: string;             // Ratio string (e.g., "1:10")
  metric: "0" | "1";         // "0" = Metric, "1" = Imperial
  metricUnit: string;        // Unit name (e.g., "Meter", "Feet")
  dimPrecision: number;      // Decimal places (0-4)
  isSelected: boolean;       // Active state
  
  // Optional properties
  isGlobal?: boolean;        // Apply to all pages
  pageRanges?: number[][];   // Page ranges [[start, end], ...]
  source?: string;           // "manual", "auto", or "calibrate"
  imperialNumerator?: number;    // For fractional imperial scales
  imperialDenominator?: number;  // For fractional imperial scales
}
Full Type Definition