
Thatopen Impl Measurements
- 5 installs
- 17 repo stars
- Updated July 8, 2026
- openaec-foundation/thatopen-claude-skill-package
Helps with ai & agent building tasks.
About
thatopen-impl-measurements is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- thatopen-impl-measurements
- AI & Agent Building
- AI-coding skill
Thatopen Impl Measurements by the numbers
- 5 all-time installs (skills.sh)
- Ranked #13,065 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 21, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/thatopen-claude-skill-package --skill thatopen-impl-measurementsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 17 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/thatopen-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
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.DisposableShared 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
// Custom value formatter — applies to ALL measurement instances
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 interactioncreate()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"; // or "edge"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
1. Set enabled = true and mode 2. User clicks first point → create() initializes preview line 3. Preview line follows cursor with snapping 4. User clicks second point → create() → endCreation() finalizes 5. 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"; // or "square" or "face"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
1. Set enabled = true and mode 2. User clicks points → create() adds each point 3. Preview fill updates showing the polygon 4. Call endCreation() when polygon is complete (min 3 points) 5. 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
1. Set enabled = true 2. initPreview() creates a MeasureVolume preview (fires onPreviewInitialized) 3. User clicks faces → create() adds selected items to preview 4. endCreation() clones preview volume into list 5. 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
1. Set enabled = true 2. User clicks first point → create() (click 1) 3. User clicks second point (vertex) → create() (click 2) 4. User clicks third point → create() (click 3) → endCreation() 5. 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";
// 1. Get the measurement component
const lengths = components.get(OBCF.LengthMeasurement);
// 2. Assign world (REQUIRED before enabling)
lengths.world = world;
// 3. Configure snapping (optional — defaults include LINE, POINT, FACE)
lengths.snapDistance = 0.5;
lengths.pickerSize = 8;
// 4. Configure units and rounding (optional)
lengths.units = "m";
lengths.rounding = 2;
// 5. Configure color (optional)
lengths.color = new THREE.Color(0xff0000);
// 6. Set mode
lengths.mode = "free";
// 7. Enable (ALWAYS last — this activates pointer events)
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:
// Switch from length to area
lengths.enabled = false;
areas.enabled = true;NEVER have multiple measurement types enabled simultaneously. They share pointer events and will conflict.
Deleting Measurements
// Delete measurement under cursor (interactive)
lengths.delete();
// Delete all measurements of a type
lengths.list.clear(); // Clears all length measurementsDisposal
ALWAYS dispose measurement components when no longer needed:
// Dispose a specific measurement type
lengths.dispose();
// Or let components.dispose() handle all cleanup
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
1. ALWAYS assign world before setting enabled = true. 2. ALWAYS disable one measurement type before enabling another. 3. ALWAYS call dispose() or components.dispose() on cleanup. 4. ALWAYS call endCreation() to finalize a measurement. Without it, the measurement stays in preview state. 5. ALWAYS call cancelCreation() to discard an in-progress measurement cleanly. Do not just disable — this leaks preview elements. 6. NEVER enable multiple measurement types simultaneously. 7. NEVER use measurement instances after dispose(). 8. NEVER forget to set mode before enabling — the default mode may not match the desired interaction. 9. NEVER set Measurement.valueFormatter after measurements are already created — existing labels will not update retroactively. 10. NEVER skip snapping configuration when precision matters — default snap distance may be too large or too small for your model scale.
Reference Files
- references/methods.md — Measurement base class
API, LengthMeasurement, AreaMeasurement, VolumeMeasurement, AngleMeasurement full method reference
- references/examples.md — Setup and usage
examples for each measurement type
- references/anti-patterns.md — Missing
dispose, snapping issues, lifecycle errors
Source Verification
All API signatures verified against:
- GitHub:
ThatOpen/engine_componentsmain branch (packages/front/src/measurement/) - npm:
@thatopen/components-front@3.3.3 - Research:
docs/research/vooronderzoek-thatopen.md(Section 5)
Measurement Anti-Patterns
AP-1: Missing World Assignment
Wrong:
const lengths = components.get(OBCF.LengthMeasurement);
lengths.enabled = true; // No world assigned!
// Result: vertex picker has no world reference, raycasting fails silently.Correct:
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world; // ALWAYS assign world first
lengths.enabled = true;ALWAYS assign world before setting enabled = true. The vertex picker requires a valid world reference to perform raycasting.
---
AP-2: Multiple Measurement Types Enabled Simultaneously
Wrong:
lengths.enabled = true;
areas.enabled = true; // Both enabled — pointer events conflict!
// Result: unpredictable behavior, clicks register on both measurement types.Correct:
lengths.enabled = true;
// When switching to areas:
lengths.enabled = false; // ALWAYS disable first
areas.enabled = true;NEVER have multiple measurement types enabled at the same time. They share pointer events and will interfere with each other.
---
AP-3: Missing Dispose on Cleanup
Wrong:
// Component unmounts, viewer destroyed
// Measurement instances are just abandoned — no dispose called.
// Result: memory leak, GPU resources not freed, event listeners remain active.Correct:
// Option 1: Dispose individually
lengths.dispose();
areas.dispose();
// Option 2: Let components.dispose() handle all (preferred)
components.dispose();ALWAYS call dispose() when measurements are no longer needed. Each measurement instance holds DimensionLine elements with Three.js geometries, materials, and HTML overlays that MUST be cleaned up.
---
AP-4: Missing endCreation() Call
Wrong:
lengths.enabled = true;
// User clicks two points, measurement preview appears
lengths.enabled = false; // Disabled without finalizing!
// Result: preview elements leak, measurement not saved to list.Correct:
lengths.enabled = true;
// User clicks two points
lengths.endCreation(); // ALWAYS finalize before disabling
lengths.enabled = false;For programmatic workflows, ALWAYS call endCreation() to save the measurement or cancelCreation() to discard it before disabling.
---
AP-5: Missing cancelCreation() on Abort
Wrong:
// User started measuring but wants to cancel
lengths.enabled = false; // Just disabling — preview elements leak!
lengths.enabled = true; // Re-enable — orphaned preview still exists.Correct:
// User wants to cancel
lengths.cancelCreation(); // ALWAYS cancel before disabling
lengths.enabled = false;ALWAYS call cancelCreation() when discarding an in-progress measurement. Simply disabling the measurement type does not clean up preview elements.
---
AP-6: Setting valueFormatter After Measurements Exist
Wrong:
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.enabled = true;
// ... user creates several measurements ...
// Now setting formatter — existing labels do NOT update
Measurement.valueFormatter = (v) => `${v.toFixed(1)} m`;Correct:
// Set formatter BEFORE enabling measurements
Measurement.valueFormatter = (v) => `${v.toFixed(1)} m`;
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.enabled = true;ALWAYS set Measurement.valueFormatter before enabling measurements and creating the first measurement. Existing labels do not retroactively update when the formatter changes.
---
AP-7: Wrong Snap Distance for Model Scale
Wrong:
// Model is in millimeters (building is ~50000 mm wide)
lengths.snapDistance = 0.25; // Default — way too small for mm-scale models
// Result: snapping never activates, user cannot snap to vertices/edges.Correct:
// For mm-scale models, increase snap distance
lengths.snapDistance = 50; // Appropriate for millimeter-scale geometry
// For meter-scale models, default is usually fine
lengths.snapDistance = 0.25;ALWAYS configure snapDistance relative to your model's coordinate scale. The default value assumes meter-scale geometry. For models in millimeters or other units, adjust proportionally.
---
AP-8: Using Measurement After Dispose
Wrong:
lengths.dispose();
lengths.enabled = true; // Using after dispose!
// Result: errors, undefined behavior, potential crashes.Correct:
lengths.dispose();
// NEVER access the instance after dispose.
// If you need measurements again, get a new instance from components.NEVER use a measurement instance after calling dispose(). The DataSets, materials, and vertex picker are all destroyed.
---
AP-9: Not Handling Mode Before Enabling
Wrong:
const areas = components.get(OBCF.AreaMeasurement);
areas.world = world;
areas.enabled = true; // Using default mode — might not be what you wantCorrect:
const areas = components.get(OBCF.AreaMeasurement);
areas.world = world;
areas.mode = "face"; // Explicitly set desired mode
areas.enabled = true;ALWAYS set the mode property explicitly before enabling. The default mode may not match the desired interaction behavior.
---
AP-10: Ignoring Minimum Points for Area Measurement
Wrong:
areas.mode = "free";
areas.enabled = true;
// User clicks only 2 points
areas.endCreation(); // Not enough points!
// Result: endCreation() does nothing — minimum 3 points required.Correct:
areas.mode = "free";
areas.enabled = true;
// User clicks 3+ points to define a closed polygon
areas.endCreation(); // Finalizes with valid polygonIn "free" mode, AreaMeasurement requires a minimum of 3 points to create a valid polygon. ALWAYS ensure enough points are placed before calling endCreation().
---
AP-11: Forgetting to Exclude Grid from PostproductionRenderer
Wrong:
// PostproductionRenderer is active
lengths.enabled = true;
// Measurement lines render with post-processing artifacts.This is not a measurement-specific issue but affects measurement visibility. See thatopen-impl-viewer for the grid exclusion pattern. Measurement visual elements may also need consideration with post-processing.
---
AP-12: Not Listening for State Changes in UI
Wrong:
// UI shows "Length Mode: free" but user changed mode programmatically
lengths.mode = "edge";
// UI is now out of sync — no listener registered.Correct:
lengths.onStateChanged.add((changes) => {
if (changes.includes("mode")) {
updateModeIndicator(lengths.mode);
}
if (changes.includes("units")) {
updateUnitsDisplay(lengths.units);
}
});ALWAYS subscribe to onStateChanged when building UI that reflects measurement state. This ensures the UI stays synchronized with programmatic or user-driven state changes.
---
Summary Table
| # | Anti-Pattern | Consequence | Fix |
|---|---|---|---|
| AP-1 | Missing world assignment | Silent raycasting failure | Assign world before enable |
| AP-2 | Multiple types enabled | Pointer event conflicts | Disable before switching |
| AP-3 | Missing dispose | Memory/GPU leak | Call dispose on cleanup |
| AP-4 | Missing endCreation | Preview elements leak | Finalize or cancel |
| AP-5 | Missing cancelCreation | Orphaned preview elements | Cancel before disable |
| AP-6 | Late valueFormatter | Existing labels unchanged | Set formatter before enable |
| AP-7 | Wrong snap distance | Snapping fails | Scale to model units |
| AP-8 | Use after dispose | Errors/crashes | Never use after dispose |
| AP-9 | Default mode assumed | Wrong interaction | Set mode explicitly |
| AP-10 | Too few area points | endCreation no-op | Ensure 3+ points |
| AP-11 | Grid not excluded | Visual artifacts | Exclude grid from PP |
| AP-12 | No state listeners | UI out of sync | Subscribe to onStateChanged |
Measurement Examples
Prerequisites
All examples assume a working ThatOpen viewer (see thatopen-impl-viewer). The world, components, and container are already set up.
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
import * as THREE from "three";
// Viewer already initialized (see thatopen-impl-viewer)
const components = new OBC.Components();
// ... world setup ...
components.init();---
LengthMeasurement — Free Mode
Measure the distance between two arbitrary points.
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.units = "m";
lengths.rounding = 2;
lengths.snapDistance = 0.25;
lengths.mode = "free";
lengths.enabled = true;
// User clicks two points in the 3D viewport.
// A DimensionLine with distance label appears automatically.
// To programmatically end or cancel:
// lengths.endCreation(); // finalize current measurement
// lengths.cancelCreation(); // discard current measurementLengthMeasurement — Edge Mode
Snap to geometry edges and measure their length.
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.mode = "edge";
lengths.enabled = true;
// User hovers over edges — snap marker appears on edge.
// Click to measure the full edge length.LengthMeasurement — Custom Color and Endpoint
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.color = new THREE.Color(0xff0000); // red measurements
// Custom endpoint element
const endpoint = document.createElement("div");
endpoint.style.backgroundColor = "red";
endpoint.style.width = "10px";
endpoint.style.height = "10px";
endpoint.style.borderRadius = "50%";
lengths.linesEndpointElement = endpoint;
lengths.mode = "free";
lengths.enabled = true;---
AreaMeasurement — Free Mode
Define a polygon area by clicking 3+ points.
const areas = components.get(OBCF.AreaMeasurement);
areas.world = world;
areas.units = "m2";
areas.rounding = 2;
areas.mode = "free";
areas.enabled = true;
// User clicks points to define polygon vertices.
// After 3+ points, call endCreation() to finalize:
// areas.endCreation();
// A MeasureFill with perimeter lines and area label appears.AreaMeasurement — Square Mode
Define a rectangular area with two clicks.
const areas = components.get(OBCF.AreaMeasurement);
areas.world = world;
areas.mode = "square";
areas.enabled = true;
// User clicks two opposite corners of the rectangle.
// The rectangular area is computed automatically.AreaMeasurement — Face Mode
Click a geometry face to measure its area directly.
const areas = components.get(OBCF.AreaMeasurement);
areas.world = world;
areas.mode = "face";
areas.pickTolerance = 0.1;
areas.tolerance = 0.005;
areas.enabled = true;
// User clicks a face in the model.
// The face area is calculated and displayed.---
VolumeMeasurement — Free Mode
Define a volume by selecting geometry faces.
const volumes = components.get(OBCF.VolumeMeasurement);
volumes.world = world;
volumes.units = "m3";
volumes.rounding = 3;
volumes.mode = "free";
// Listen for preview initialization
volumes.onPreviewInitialized.add((preview) => {
console.log("Volume preview ready:", preview);
});
volumes.enabled = true;
// User clicks faces to define volume boundary.
// Call endCreation() to finalize:
// volumes.endCreation();---
AngleMeasurement — Free Mode
Measure the angle between three points. The angle is measured at the second (middle) point.
const angles = components.get(OBCF.AngleMeasurement);
angles.world = world;
angles.units = "deg";
angles.rounding = 1;
angles.mode = "free";
angles.enabled = true;
// User clicks three points:
// 1. First arm endpoint
// 2. Vertex (angle center)
// 3. Second arm endpoint
// An arc with angle label appears at the vertex.---
Custom Value Formatter
Apply a custom format to ALL measurement labels globally.
import { Measurement } from "@thatopen/components-front";
// Set BEFORE enabling measurements
Measurement.valueFormatter = (value: number) => {
if (value < 1) {
return `${(value * 100).toFixed(0)} cm`;
}
return `${value.toFixed(2)} m`;
};
// Now enable measurements — all labels use the custom formatter
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.enabled = true;---
Switching Between Measurement Types
ALWAYS disable the current type before enabling another.
const lengths = components.get(OBCF.LengthMeasurement);
const areas = components.get(OBCF.AreaMeasurement);
const angles = components.get(OBCF.AngleMeasurement);
// Configure all types with world reference
lengths.world = world;
areas.world = world;
angles.world = world;
// Start with length measurement
lengths.enabled = true;
function switchToArea() {
lengths.enabled = false;
areas.mode = "free";
areas.enabled = true;
}
function switchToAngle() {
areas.enabled = false;
angles.enabled = true;
}
function disableAll() {
lengths.enabled = false;
areas.enabled = false;
angles.enabled = false;
}---
Deleting Measurements
Interactive Deletion (Under Cursor)
// Delete the length measurement under the cursor
lengths.delete();
// Delete the area measurement under the cursor
areas.delete();Clear All Measurements of a Type
// Clear all length measurements
lengths.list.clear();
// Clear all area measurements
areas.list.clear();---
Listening to State Changes
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.onStateChanged.add((changes) => {
console.log("State changed:", changes);
// changes is an array like ["mode"], ["color"], ["units", "rounding"]
});
lengths.onEnabledChange.add((enabled) => {
console.log("Measurements enabled:", enabled);
});
lengths.onVisibilityChange.add((visible) => {
console.log("Measurements visible:", visible);
});---
Snapping Configuration
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
// Increase snap distance for large models
lengths.snapDistance = 1.0;
// Increase picker marker size for visibility
lengths.pickerSize = 10;
// The snappings array controls which snap types are active.
// Default includes LINE, POINT, and FACE.
// Access via the snappings property on the measurement instance.
lengths.enabled = true;---
Unit Conversion
Change units after measurements are created — values update automatically.
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.units = "m";
lengths.enabled = true;
// ... user creates measurements ...
// Switch to centimeters — all existing labels update
lengths.units = "cm";
// Switch to millimeters
lengths.units = "mm";---
Clipping Plane Integration
Make measurements respect clipping planes.
const clipper = components.get(OBC.Clipper);
// ... create clipping planes ...
const lengths = components.get(OBCF.LengthMeasurement);
lengths.world = world;
lengths.enabled = true;
// Apply clipping plane visibility to measurements
const planes = [new THREE.Plane(new THREE.Vector3(0, -1, 0), 5)];
lengths.applyPlanesVisibility(planes);---
Full Measurement Toolbar Example
Complete setup with all measurement types and a toolbar for switching.
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
import * as THREE from "three";
// Assume viewer is set up (world, components, container)
// Initialize all measurement types
const lengths = components.get(OBCF.LengthMeasurement);
const areas = components.get(OBCF.AreaMeasurement);
const volumes = components.get(OBCF.VolumeMeasurement);
const angles = components.get(OBCF.AngleMeasurement);
// Configure all with world reference
[lengths, areas, volumes, angles].forEach((m) => {
m.world = world;
m.rounding = 2;
m.snapDistance = 0.25;
});
// Custom formatter for all types
Measurement.valueFormatter = (value: number) => value.toFixed(2);
// Active measurement tracker
let activeMeasurement: typeof lengths | typeof areas | typeof volumes | typeof angles | null = null;
function activate(measurement: typeof activeMeasurement) {
if (activeMeasurement) {
activeMeasurement.enabled = false;
}
activeMeasurement = measurement;
if (measurement) {
measurement.enabled = true;
}
}
// Toolbar handlers
document.getElementById("btn-length")?.addEventListener("click", () => {
lengths.mode = "free";
activate(lengths);
});
document.getElementById("btn-area")?.addEventListener("click", () => {
areas.mode = "free";
activate(areas);
});
document.getElementById("btn-volume")?.addEventListener("click", () => {
volumes.mode = "free";
activate(volumes);
});
document.getElementById("btn-angle")?.addEventListener("click", () => {
angles.mode = "free";
activate(angles);
});
document.getElementById("btn-clear")?.addEventListener("click", () => {
activate(null);
});
// Cleanup
window.addEventListener("beforeunload", () => {
components.dispose();
});---
Disposal Pattern
ALWAYS dispose measurements when the viewer is destroyed.
// Option 1: Dispose individual measurement types
lengths.dispose();
areas.dispose();
volumes.dispose();
angles.dispose();
// Option 2: Let components.dispose() handle everything (preferred)
components.dispose();After disposal, NEVER access any measurement properties or methods.
Measurement Methods Reference
Measurement Base Class
Constructor
constructor(components: OBC.Components, measureType: U)Initializes DataSets (list, lines, fills, labels, volumes) with disposal callbacks. Registers the component via its static uuid.
Abstract Members
| Member | Type | Description |
|---|---|---|
modes | string[] | Available modes for this measurement type |
mode | string (get/set) | Current active mode |
Lifecycle Methods (Createable)
create(_input?: any): voidOverride in subclasses. Called on each user click during measurement creation.
endCreation(_data?: T): voidOverride in subclasses. Finalizes the current in-progress measurement and adds it to the list DataSet.
cancelCreation(): voidOverride in subclasses. Discards the current in-progress measurement and cleans up preview elements.
delete(_data?: any): voidOverride in subclasses. Removes a measurement element, typically by raycasting against bounding boxes to find the element under the cursor.
Disposal
dispose(): voidClears vertex picker, all DataSets (list, lines, fills, labels, volumes), and disposes all materials (linesMaterial, fillsMaterial, volumesMaterial).
Visual Element Factory Methods
protected createLineElement(line: Line, startNormal?: THREE.Vector3): DimensionLineCreates a DimensionLine with the current linesMaterial, linesEndpointElement, units, rounding, and color settings.
protected createFillElement(area: Area): MeasureFillCreates a MeasureFill with the current fillsMaterial, units, and rounding.
protected createVolumeElement(volume: Volume): MeasureVolumeCreates a MeasureVolume with the current volumesMaterial, units, and rounding.
protected addLineElementsFromPoints(points: THREE.Vector3[]): DimensionLine[]Generates dimension lines connecting sequential points. Returns array of created DimensionLine instances.
Bounding Box Retrieval
protected getLineBoxes(): THREE.Mesh[]Returns bounding box meshes from all DimensionLine elements. Used by delete() for raycasting against measurements.
protected getFillBoxes(): THREE.Mesh[]Returns Three.js meshes from all MeasureFill elements.
protected async getVolumeBoxes(): Promise<THREE.Mesh[]>Async. Returns mesh arrays from all MeasureVolume elements.
Clipping Integration
applyPlanesVisibility(planes: THREE.Plane[]): voidUpdates clipping planes for all visual elements (lines, fills, volumes).
Enabled State
get enabled(): boolean
set enabled(value: boolean)When set to true: activates the vertex picker, registers pointer and keyboard event listeners, fires onEnabledChange. When set to false: deactivates the vertex picker, removes event listeners.
Visibility
get visible(): boolean
set visible(value: boolean)When set: updates visibility for all lines, fills, and volumes. Fires onVisibilityChange and onStateChanged with "visibility".
Units
get units(): MeasureToUnitMap[U]
set units(value: MeasureToUnitMap[U])When set: propagates units to all existing measurement items and visual elements, converts values to the new unit. Fires onStateChanged with "units".
Rounding
get rounding(): number
set rounding(value: number)When set: applies rounding to all existing measurement items and visual elements. Fires onStateChanged with "rounding".
Color
get color(): THREE.Color
set color(value: THREE.Color)When set: updates all material colors (linesMaterial, fillsMaterial, volumesMaterial) and all individual element colors.
Material Properties
get linesMaterial(): THREE.LineBasicMaterial
set linesMaterial(value: THREE.LineBasicMaterial)
get fillsMaterial(): THREE.MeshLambertMaterial
set fillsMaterial(value: THREE.MeshLambertMaterial)
get volumesMaterial(): THREE.MeshLambertMaterial
set volumesMaterial(value: THREE.MeshLambertMaterial)Each setter disposes the previous material before assigning the new one.
Endpoint Element
get linesEndpointElement(): HTMLElement
set linesEndpointElement(value: HTMLElement)Custom HTML element used as the endpoint marker on DimensionLines. Default: blue rounded div created by newDimensionMark().
Vertex Picker Properties
get pickerMode(): GraphicVertexPickerMode
set pickerMode(value: GraphicVertexPickerMode)
get snapDistance(): number
set snapDistance(value: number)
get pickerSize(): number
set pickerSize(value: number)Delegates to the internal GraphicVertexPicker instance.
Static Members
static valueFormatter: ((value: number) => string) | nullWhen set, all measurement labels use this function to format their numeric values. Applies to ALL measurement instances globally.
unitsList Getter
get unitsList(): string[]Returns the available units for the measurement type:
- Length:
["mm", "cm", "m", "km"] - Area:
["mm2", "cm2", "m2", "km2"] - Volume:
["mm3", "cm3", "m3", "km3"] - Angle:
["deg", "rad"]
---
LengthMeasurement
Extends: Measurement<Line, "length"> UUID: "2f9bcacf-18a9-4be6-a293-e898eae64ea1"
Properties
| Property | Type | Description |
|---|---|---|
modes | ["free", "edge"] | Available measurement modes |
mode | string (get/set) | Current mode. Setter triggers preview reinit for "edge" |
isDragging | boolean (getter) | Whether measurement creation is in progress |
Methods
create(): voidFirst call: initializes preview at the first picked point (initPreview()). Second call: places endpoint and calls endCreation().
endCreation(): voidFinalizes the DimensionLine and adds it to the list DataSet.
cancelCreation(): voidDiscards the in-progress preview line.
delete(): voidRemoves the DimensionLine under the cursor by raycasting against getLineBoxes().
Private Methods
initPreview(): Establishes the first measurement point with snapping.updatePreviewLine(): Updates the preview endpoint as cursor moves.
---
AreaMeasurement
Extends: Measurement<Area, "area"> UUID: "09b78c1f-0ff1-4630-a818-ceda3d878c75"
Properties
| Property | Type | Default | Description |
|---|---|---|---|
modes | ["free", "square", "face"] | — | Available measurement modes |
mode | string (get/set) | "free" | Current mode |
pickTolerance | number | 0.1 | Precision for selecting measurement areas |
tolerance | number | 0.005 | Margin for point inclusion in area elements |
Methods
async create(): Promise<void>Adds a point to the current area polygon via vertex picker. In "face" mode, selects an entire face. In "square" mode, two clicks define a rectangle.
endCreation(): voidFinalizes the area measurement. Requires minimum 3 points in "free" mode. Creates MeasureFill and perimeter DimensionLines.
cancelCreation(): voidDiscards in-progress area measurement and preview elements.
delete(): voidRemoves the area measurement under the cursor by raycasting against getFillBoxes().
Private Methods
computeLineElements(): Generates DimensionLines between polygon points.updatePreview(): Updates the temporary fill preview during creation.
---
VolumeMeasurement
Extends: Measurement<Volume, "volume"> UUID: "01f885ab-ec4e-4e6c-a853-9dfc0d6766ed"
Properties
| Property | Type | Description |
|---|---|---|
modes | ["free"] | Available measurement modes |
mode | string (get/set) | Current mode. Setter cancels creation and fires state change |
Events
| Event | Payload | Description |
|---|---|---|
onPreviewInitialized | MeasureVolume | Fired when preview volume object is created |
Methods
async initPreview(): Promise<void>Creates a MeasureVolume preview instance. Fires onPreviewInitialized.
async create(): Promise<void>Retrieves vertex pick results and adds selected geometry items to the preview volume.
endCreation(): voidClones the preview volume into the list DataSet.
cancelCreation(): voidDisposes the preview volume without saving.
delete(): voidRemoves volume measurements under the cursor by raycasting against getVolumeBoxes().
---
AngleMeasurement
Extends: Measurement<Angle, "angle"> UUID: "2c88a142-2378-422e-b26a-bb2710841813"
Properties
| Property | Type | Description |
|---|---|---|
modes | ["free"] | Available measurement modes |
mode | string (get/set) | Current mode |
Static Constants
| Constant | Value | Description |
|---|---|---|
ARC_SEGMENTS | 32 | Number of segments in the arc geometry |
ARC_RADIUS_FACTOR | 0.3 | Arc radius relative to shortest arm length |
LABEL_OFFSET_FACTOR | 1.4 | Label position offset from arc center |
Methods
async create(): Promise<void>Requires three successive calls: 1. First click: sets the first point 2. Second click: sets the vertex (angle center) 3. Third click: sets the third point and calls endCreation()
endCreation(): voidFinalizes angle after the third point. Creates arc visualization with label showing the angle value.
cancelCreation(): voidCancels in-progress angle and disposes preview visuals.
delete(): voidRemoves angle measurement under the cursor via raycasting.
dispose(): voidCleans up all angle visuals (arcs, lines, labels) and base class resources.
Private Methods
createAngleVisual(): Generates complete visual with lines, arc,
endpoints, and label.
updateAngleVisual(): Updates existing visual geometry and positions.createArcGeometry()(static): Generates arc BufferGeometry between
three points.
getArcMidpoint()(static): Calculates label position on the arc.
---
GraphicVertexPicker
The internal vertex picker used by all measurement classes.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
marker | `Mark \ | null` | — |
world | `OBC.World \ | null` | — |
mode | GraphicVertexPickerMode | DEFAULT | Picking behavior mode |
maxDistance | number | — | Snap threshold distance (world units) |
pickerSize | number | 6 | Marker size in pixels |
enabled | boolean | false | Activates/deactivates the picker |
GraphicVertexPickerMode
| Mode | Behavior |
|---|---|
DEFAULT | Uses castRay with snapping classes (LINE, POINT, FACE) |
SYNCHRONOUS | Uses castRayToObjects without snapping |
Methods
async get(config?): Promise<Result | undefined>Performs vertex picking. Dispatches to default or synchronous mode.
applySnapping(intersects, snappingClass): voidDetermines closest snap point for the given class within maxDistance.
updatePointer(): voidUpdates the preview marker position during mouse movement.
dispose(): voidCleans up marker and event listeners.