Angle Measurements


SVG + DOM overlay that paints one or more 3D angle measurements on a View's canvas. Each measurement is a corner vertex and two arms running to an origin and target anchor, labelled with the interior angle in degrees.


Mirrors the shape of V2's AngleMeasurementsPlugin, but rendered with SVG + DOM (no second renderer or off-screen canvas). One AngleMeasurementsTool per View owns the overlay and a keyed collection of AngleMeasurements; an optional MouseAngleMeasurementsControl turns canvas clicks into measurements with three-click creation (origin → corner → target).


%%{init:{"theme":"dark"}}%% classDiagram direction LR class AngleMeasurementsTool { +view : View +picker : PickStrategy +defaultColor : string +measurements : Map +onMeasurementsChanged : EventEmitter +mouseControl : MouseAngleMeasurementsControl +createMeasurement(params) AngleMeasurement +destroyMeasurement(id) +clear() +show() / hide() / toggle() +destroy() } class AngleMeasurement { +origin / corner / target : Vec3 +angle : number degrees +visible / wireVisible / labelsVisible +color : string } class MouseAngleMeasurementsControl { +tool : AngleMeasurementsTool +active : boolean +activate() +deactivate() +destroy() } class AngleMeasurementsToolParams { +view : View +container? : HTMLElement +picker? : PickStrategy +visible? : boolean +defaultColor? : string } class AngleMeasurementParams { +id? : string +origin : Vec3 +corner : Vec3 +target : Vec3 +visible? : boolean +wireVisible? : boolean +labelsVisible? : boolean +color? : string } class View { <<viewer>> } class PickStrategy { <<picking>> } AngleMeasurementsTool ..> AngleMeasurementsToolParams : openFor AngleMeasurementsTool "1" *-- "*" AngleMeasurement : measurements AngleMeasurementsTool ..> AngleMeasurementParams : createMeasurement AngleMeasurementsTool "1" o-- "0..1" MouseAngleMeasurementsControl : lazy AngleMeasurementsTool o-- View : paints over AngleMeasurementsTool o-- PickStrategy : routes clicks
%%{init:{"theme":"default"}}%% classDiagram direction LR class AngleMeasurementsTool { +view : View +picker : PickStrategy +defaultColor : string +measurements : Map +onMeasurementsChanged : EventEmitter +mouseControl : MouseAngleMeasurementsControl +createMeasurement(params) AngleMeasurement +destroyMeasurement(id) +clear() +show() / hide() / toggle() +destroy() } class AngleMeasurement { +origin / corner / target : Vec3 +angle : number degrees +visible / wireVisible / labelsVisible +color : string } class MouseAngleMeasurementsControl { +tool : AngleMeasurementsTool +active : boolean +activate() +deactivate() +destroy() } class AngleMeasurementsToolParams { +view : View +container? : HTMLElement +picker? : PickStrategy +visible? : boolean +defaultColor? : string } class AngleMeasurementParams { +id? : string +origin : Vec3 +corner : Vec3 +target : Vec3 +visible? : boolean +wireVisible? : boolean +labelsVisible? : boolean +color? : string } class View { <<viewer>> } class PickStrategy { <<picking>> } AngleMeasurementsTool ..> AngleMeasurementsToolParams : openFor AngleMeasurementsTool "1" *-- "*" AngleMeasurement : measurements AngleMeasurementsTool ..> AngleMeasurementParams : createMeasurement AngleMeasurementsTool "1" o-- "0..1" MouseAngleMeasurementsControl : lazy AngleMeasurementsTool o-- View : paints over AngleMeasurementsTool o-- PickStrategy : routes clicks
classDiagram
    direction LR
    class AngleMeasurementsTool {
      +view                  : View
      +picker                : PickStrategy
      +defaultColor          : string
      +measurements          : Map
      +onMeasurementsChanged : EventEmitter
      +mouseControl          : MouseAngleMeasurementsControl
      +createMeasurement(params) AngleMeasurement
      +destroyMeasurement(id)
      +clear()
      +show() / hide() / toggle()
      +destroy()
    }
    class AngleMeasurement {
      +origin / corner / target : Vec3
      +angle  : number degrees
      +visible / wireVisible / labelsVisible
      +color  : string
    }
    class MouseAngleMeasurementsControl {
      +tool   : AngleMeasurementsTool
      +active : boolean
      +activate()
      +deactivate()
      +destroy()
    }
    class AngleMeasurementsToolParams {
      +view          : View
      +container?    : HTMLElement
      +picker?       : PickStrategy
      +visible?      : boolean
      +defaultColor? : string
    }
    class AngleMeasurementParams {
      +id?            : string
      +origin         : Vec3
      +corner         : Vec3
      +target         : Vec3
      +visible?       : boolean
      +wireVisible?   : boolean
      +labelsVisible? : boolean
      +color?         : string
    }
    class View {
      <<viewer>>
    }
    class PickStrategy {
      <<picking>>
    }
    AngleMeasurementsTool ..> AngleMeasurementsToolParams : openFor
    AngleMeasurementsTool "1" *-- "*" AngleMeasurement : measurements
    AngleMeasurementsTool ..> AngleMeasurementParams : createMeasurement
    AngleMeasurementsTool "1" o-- "0..1" MouseAngleMeasurementsControl : lazy
    AngleMeasurementsTool o-- View : paints over
    AngleMeasurementsTool o-- PickStrategy : routes clicks

  • Per-View singletonopenFor is idempotent; re-calling for the same View brings the existing tool back to the foreground.
  • SVG + DOM overlay — no second renderer or off-screen canvas; the overlay shares the View canvas's stacking context so letterboxing and CSS transforms apply uniformly.
  • Three-anchor angle — each measurement renders the two arm wires (origin → corner, corner → target) plus an angle label in degrees at the corner. Toggle wires and labels independently via AngleMeasurementParams. Default colour is purple to distinguish from DistanceMeasurement's orange.
  • Snap-aware picking — the tool routes mouse clicks through a caller-supplied PickStrategy. Supply a RoutingPickStrategy for snap-to-vertex / snap-to-edge; omit picker to get a default BVH-only picker built from the View's Scene.
  • Lazy mouse controltool.mouseControl lazily constructs the MouseAngleMeasurementsControl on first access, so a tool used purely programmatically never pays its setup cost.
  • Coarse change eventonMeasurementsChanged fires after every create / destroy / clear; listeners that need the current contents re-read the measurements map.

npm install @xeokit/sdk

import {
AngleMeasurementsTool
} from "@xeokit/sdk/tools/measurements/angle";

openFor is the canonical entry — idempotent, View-keyed, and shows the existing tool again if it was hidden.

const tool = AngleMeasurementsTool.openFor({ view });

Pass three world-space anchors. The tool computes the angle between the two arms at corner; the label updates every frame as the camera moves.

const m = tool.createMeasurement({
id: "ridgeAngle",
origin: [10.0, 0.0, 0.0],
corner: [ 0.0, 0.0, 0.0],
target: [ 7.07, 7.07, 0.0]
});

console.log(m.angle); // degrees at corner

tool.mouseControl is lazy — first access constructs the helper. activate() starts listening for clicks on the View canvas; Esc cancels an in-progress measurement; deactivate() stops listening.

tool.mouseControl.activate();

// ...later, when the user picks a different tool:
tool.mouseControl.deactivate();

Pass a RoutingPickStrategy as picker to get snap-to-vertex and snap-to-edge in mouse-driven creation. Omit picker for a BVH-only fallback.

import { RoutingPickStrategy } from "@xeokit/sdk/spatial/picking";

const picker = new RoutingPickStrategy({ scene: view.scene, renderer });

const tool = AngleMeasurementsTool.openFor({
view,
picker
});

onMeasurementsChanged is coarse — it fires once per create / destroy / clear without the changed id. Listeners that need the current state re-read measurements.

const unsub = tool.onMeasurementsChanged.subscribe((t) => {
const ids = Object.keys(t.measurements);
console.log(`${ids.length} angle measurements`);
});

Visibility on the tool hides the entire overlay; per-measurement visibility flags hide individual measurements while the overlay stays mounted.

tool.hide();
tool.show();
const visible = tool.toggle(); // returns the new state

m.visible = false; // hide one
m.wireVisible = false; // hide its arm wires only
m.labelsVisible = false; // hide its angle label

destroy tears down the overlay, every child measurement, and the camera-update subscription. Idempotent.

tool.destroyMeasurement("ridgeAngle"); // drop one
tool.clear(); // drop all
tool.destroy(); // drop the tool itself

Classes

AngleMeasurement
AngleMeasurementsTool
MouseAngleMeasurementsControl

Interfaces

AngleMeasurementParams
AngleMeasurementsToolParams