| name | thatopen-impl-measurements |
| description | Use when adding measurement tools to a ThatOpen BIM viewer, including distance, area, volume, and angle measurements. Prevents measurement lifecycle errors and missing snapping configuration. Covers LengthMeasurement, AreaMeasurement, VolumeMeasurement, AngleMeasurement, Measurement base class, snapping, units, modes, valueFormatter, measurement lifecycle (create/end/cancel/delete). Keywords: measurement, length, area, volume, angle, distance, snap, dimension, measure, units, picker, measure distance, how far apart, calculate area in viewer.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires @thatopen/components-front 3.3.x. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
ThatOpen Measurements
Overview
This skill covers the measurement system in @thatopen/components-front:
LengthMeasurement, AreaMeasurement, VolumeMeasurement, and AngleMeasurement.
All four extend the abstract Measurement<T, U> base class, which provides
shared visual elements, snapping, units, disposal, and event handling.
Version: @thatopen/components-front 3.3.x
Prerequisites: thatopen-impl-viewer (world setup), thatopen-core-architecture
Measurement Type Comparison
| Type | Class | Modes | Unit Type | Visual Elements |
|---|
| Distance | LengthMeasurement | "free", "edge" | mm, cm, m, km | DimensionLine + Mark labels |
| Area | AreaMeasurement | "free", "square", "face" | mm2, cm2, m2, km2 | MeasureFill + DimensionLine + Mark |
| Volume | VolumeMeasurement | "free" | mm3, cm3, m3, km3 | MeasureVolume + DimensionLine + Mark |
| Angle | AngleMeasurement | "free" | deg, rad | Arc geometry + DimensionLine + Mark |
Measurement Base Class
All measurement classes extend:
abstract class Measurement<
T extends Record<string, any>,
U extends keyof MeasureToUnitMap
> extends OBC.Component implements OBC.Createable, OBC.Hideable, OBC.Disposable
Shared Data Collections
| Collection | Type | Purpose |
|---|
list | DataSet<T> | All measurement elements of that type |
lines | DataSet<DimensionLine> | Visual dimension lines |
fills | DataSet<MeasureFill> | Visual area fill meshes |
labels | DataSet<Mark> | HTML label overlays |
volumes | DataSet<MeasureVolume> | Visual volume meshes |
Shared Configuration Properties
| Property | Type | Default | Description |
|---|
enabled | boolean | false | Activates measurement interaction |
visible | boolean | true | Shows/hides all visual elements |
units | MeasureToUnitMap[U] | per type | Unit for display |
rounding | number | 2 | Decimal precision |
color | THREE.Color | blue | Color of all visual elements |
delay | number | 300 | Pointer stop detection delay (ms) |
world | OBC.World | — | World reference (REQUIRED) |
Material Properties
| Property | Type | Default |
|---|
linesMaterial | THREE.LineBasicMaterial | Blue, depthTest false |
fillsMaterial | THREE.MeshLambertMaterial | Green, double-sided, 30% opacity |
volumesMaterial | THREE.MeshLambertMaterial | Green, double-sided, 30% opacity |
linesEndpointElement | HTMLElement | Blue rounded div (dimension mark) |
Static Members
Measurement.valueFormatter = (value: number) => `${value.toFixed(1)} m`;
ALWAYS set Measurement.valueFormatter before enabling measurements if you
need custom label formatting. It is a static property shared across all
measurement types.
Events
| Event | Payload | Trigger |
|---|
onPointerStop | — | Pointer stopped moving for delay ms |
onPointerMove | — | Pointer moved |
onStateChanged | MeasurementStateChange[] | Mode, color, units, rounding, visibility, or enabled changed |
onEnabledChange | boolean | Enabled toggled |
onVisibilityChange | boolean | Visibility toggled |
onDisposed | — | Measurement disposed |
MeasurementStateChange Values
type MeasurementStateChange =
| "mode" | "color" | "units" | "rounding" | "visibility" | "enabled";
Unit Types
type MeasureToUnitMap = {
length: "mm" | "cm" | "m" | "km";
area: "mm2" | "cm2" | "m2" | "km2";
volume: "mm3" | "cm3" | "m3" | "km3";
angle: "deg" | "rad";
};
Default units: length = "m", area = "m2", volume = "m3", angle = "deg".
Snapping System
Snapping is provided by the GraphicVertexPicker inside the base class.
Configure snapping via these properties on any measurement instance:
| Property | Type | Description |
|---|
snappings | Snapping class array | Which snap types to use: LINE, POINT, FACE |
snapDistance | number | Maximum distance for snapping (world units) |
pickerSize | number | Visual size of the snap marker (pixels, default 6) |
pickerMode | GraphicVertexPickerMode | DEFAULT (with snapping) or SYNCHRONOUS |
Snapping Classes
| Class | Visual Indicator | Snap Behavior |
|---|
LINE | Gray border, square | Snaps to nearest edge |
POINT | Red border, square | Snaps to nearest vertex |
FACE | Purple border, circle | Snaps to face surface |
ALWAYS configure snappings before enabling measurements. The default
snapping array includes LINE, POINT, and FACE.
Measurement Lifecycle
All measurement types follow the same creation lifecycle via the
Createable interface:
1. Set enabled = true → activates vertex picker and pointer events
2. User clicks → create() → places first point / adds point
3. User clicks → create() → places additional points (area/volume/angle)
4. endCreation() → finalizes measurement, adds to list
5. cancelCreation() → discards in-progress measurement
6. delete() → removes measurement under cursor via raycasting
7. Set enabled = false → deactivates interaction
create() is called on each click. For LengthMeasurement it takes two
clicks (start + end). For AreaMeasurement it takes 3+ clicks. For
AngleMeasurement it takes exactly 3 clicks. For VolumeMeasurement,
points define the volume boundary.
endCreation() finalizes the current measurement. For AreaMeasurement,
a minimum of 3 points is required.
cancelCreation() discards any in-progress measurement without saving.
delete() uses raycasting against measurement bounding boxes to find
and remove the measurement under the cursor.
LengthMeasurement
Measures distance between two points. Supports free placement and edge
snapping.
import * as OBCF from "@thatopen/components-front";
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.enabled = true;
lengths.mode = "free";
UUID: "2f9bcacf-18a9-4be6-a293-e898eae64ea1"
Modes
| Mode | Behavior |
|---|
"free" | Click any two points to measure distance between them |
"edge" | Snap to geometry edges; measures edge length |
Workflow
- Set
enabled = true and mode
- User clicks first point →
create() initializes preview line
- Preview line follows cursor with snapping
- User clicks second point →
create() → endCreation() finalizes
- DimensionLine with label appears showing the distance
AreaMeasurement
Measures area defined by a polygon of 3+ points.
const areas = components.get(OBCF.AreaMeasurement);
areas.world = world;
areas.enabled = true;
areas.mode = "free";
UUID: "09b78c1f-0ff1-4630-a818-ceda3d878c75"
Modes
| Mode | Behavior |
|---|
"free" | Click 3+ points to define a polygon area |
"square" | Click 2 points to define a rectangular area |
"face" | Click a face to measure its area directly |
Extra Properties
| Property | Type | Default | Description |
|---|
pickTolerance | number | 0.1 | Precision for point selection |
tolerance | number | 0.005 | Margin for point inclusion in area |
Workflow
- Set
enabled = true and mode
- User clicks points →
create() adds each point
- Preview fill updates showing the polygon
- Call
endCreation() when polygon is complete (min 3 points)
- MeasureFill with perimeter DimensionLines and label appears
VolumeMeasurement
Measures volume defined by a 3D boundary.
const volumes = components.get(OBCF.VolumeMeasurement);
volumes.world = world;
volumes.enabled = true;
volumes.mode = "free";
UUID: "01f885ab-ec4e-4e6c-a853-9dfc0d6766ed"
Modes
| Mode | Behavior |
|---|
"free" | Define volume boundary by selecting geometry faces |
Extra Events
| Event | Payload | Trigger |
|---|
onPreviewInitialized | MeasureVolume | Preview volume object created |
Workflow
- Set
enabled = true
initPreview() creates a MeasureVolume preview (fires onPreviewInitialized)
- User clicks faces →
create() adds selected items to preview
endCreation() clones preview volume into list
cancelCreation() disposes preview without saving
AngleMeasurement
Measures the angle between three points (vertex at second point).
const angles = components.get(OBCF.AngleMeasurement);
angles.world = world;
angles.enabled = true;
angles.mode = "free";
UUID: "2c88a142-2378-422e-b26a-bb2710841813"
Modes
| Mode | Behavior |
|---|
"free" | Click three points to measure the angle at the middle point |
Constants
| Constant | Value | Purpose |
|---|
ARC_SEGMENTS | 32 | Number of segments in the arc visualization |
ARC_RADIUS_FACTOR | 0.3 | Arc radius relative to shortest arm |
LABEL_OFFSET_FACTOR | 1.4 | Label distance from arc center |
Workflow
- Set
enabled = true
- User clicks first point →
create() (click 1)
- User clicks second point (vertex) →
create() (click 2)
- User clicks third point →
create() (click 3) → endCreation()
- Arc visualization with angle label appears
Setup Pattern (All Types)
ALWAYS follow this pattern when setting up any measurement type:
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.snapDistance = 0.5;
lengths.pickerSize = 8;
lengths.units = "m";
lengths.rounding = 2;
lengths.color = new THREE.Color(0xff0000);
lengths.mode = "free";
lengths.enabled = true;
NEVER enable a measurement before assigning world. The vertex picker
requires a valid world reference to perform raycasting.
Toggling Between Measurement Types
ALWAYS disable the current measurement before enabling another:
lengths.enabled = false;
areas.enabled = true;
NEVER have multiple measurement types enabled simultaneously. They share
pointer events and will conflict.
Deleting Measurements
lengths.delete();
lengths.list.clear();
Disposal
ALWAYS dispose measurement components when no longer needed:
lengths.dispose();
components.dispose();
dispose() clears the vertex picker, all DataSets (list, lines, fills,
labels, volumes), and disposes all materials. NEVER use a measurement
instance after calling dispose().
Clipping Plane Integration
Measurements support clipping plane visibility:
lengths.applyPlanesVisibility(planes);
This updates all visual elements (lines, fills, volumes) to respect the
provided clipping planes.
Critical Rules
- ALWAYS assign
world before setting enabled = true.
- ALWAYS disable one measurement type before enabling another.
- ALWAYS call
dispose() or components.dispose() on cleanup.
- ALWAYS call
endCreation() to finalize a measurement. Without it,
the measurement stays in preview state.
- ALWAYS call
cancelCreation() to discard an in-progress measurement
cleanly. Do not just disable — this leaks preview elements.
- NEVER enable multiple measurement types simultaneously.
- NEVER use measurement instances after
dispose().
- NEVER forget to set
mode before enabling — the default mode may
not match the desired interaction.
- NEVER set
Measurement.valueFormatter after measurements are
already created — existing labels will not update retroactively.
- NEVER skip snapping configuration when precision matters — default
snap distance may be too large or too small for your model scale.
Reference Files
Source Verification
All API signatures verified against:
- GitHub:
ThatOpen/engine_components main branch (packages/front/src/measurement/)
- npm:
@thatopen/components-front@3.3.3
- Research:
docs/research/vooronderzoek-thatopen.md (Section 5)