beckhoff-xts-viewer-3d 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import React__default from 'react';
3
- import { Vector3, ColorRepresentation, Shape, Curve, CurvePath, Matrix4, WebGLRenderer } from 'three';
3
+ import { Vector3, ColorRepresentation, Matrix4, Shape, Curve, CurvePath, WebGLRenderer, Object3D } from 'three';
4
4
  import { GLTF } from 'three-stdlib';
5
5
 
6
6
  /**
@@ -12,7 +12,7 @@ import { GLTF } from 'three-stdlib';
12
12
  * Run `npm run sync-version` (or any script that depends on it) to refresh
13
13
  * after bumping the package version.
14
14
  */
15
- declare const VERSION: "5.0.0";
15
+ declare const VERSION: "5.2.0";
16
16
 
17
17
  /**
18
18
  * Default CDN URL for the GLB asset bundle (`beckhoff-xts-viewer-3d-assets`).
@@ -32,14 +32,276 @@ declare const VERSION: "5.0.0";
32
32
  * build time so every install streams from a known-compatible GLB
33
33
  * release.
34
34
  */
35
- declare const JSDELIVR_ASSETS_BASE_URL: "https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d-assets@1.0.0/models";
35
+ declare const JSDELIVR_ASSETS_BASE_URL: "https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d-assets@2.0.1/models";
36
+
37
+ /**
38
+ * Public types for the measurement + annotation feature.
39
+ *
40
+ * Everything here is expressed in the viewer's WORLD frame — millimetres,
41
+ * Z-up, identical to `XtsViewer3DRef.getMoverWorldTransform()` and
42
+ * `CameraState.positionMm`. That is what makes one implementation serve both
43
+ * the perspective 3D view and the orthographic 2D plan view
44
+ * (`projection='orthographic'`): only the camera differs, the geometry does
45
+ * not.
46
+ */
47
+
48
+ /**
49
+ * How a measurement point was derived. `'free'` is the raw ray hit (surface
50
+ * or ground plane); everything else is a snap the picker resolved to.
51
+ */
52
+ type SnapKind = 'free' | 'vertex' | 'edge-midpoint' | 'face-center' | 'grid' | 'mover-center' | 'module-boundary' | 'module-center' | 'station-stop' | 'track-path';
53
+ /** Scene entity a picked point belongs to, when the picker could resolve one. */
54
+ type MeasuredEntityRef = {
55
+ kind: 'module';
56
+ ref: ModuleRef;
57
+ } | {
58
+ kind: 'mover';
59
+ ref: MoverRef;
60
+ } | {
61
+ kind: 'station';
62
+ stationId: number;
63
+ };
64
+ /** A position on a part's track path, in the XPU's user position frame. */
65
+ interface TrackAnchor {
66
+ processingUnitObjectId: number;
67
+ /** `PartConfig.objectId`. */
68
+ partObjectId: number;
69
+ /** Position along the part, user frame (same convention as movers). */
70
+ partPositionMm: number;
71
+ }
72
+ /** One measurement vertex. */
73
+ interface MeasurementPoint {
74
+ /** World position (mm). */
75
+ positionMm: Vec3;
76
+ /** How the point was obtained. Default `'free'` when omitted. */
77
+ snap?: SnapKind;
78
+ /** Entity the point sits on, when known. */
79
+ entity?: MeasuredEntityRef;
80
+ /**
81
+ * Track position, set whenever the point resolved onto a part's path.
82
+ * Required on both endpoints of a `'trackDistance'` measurement.
83
+ */
84
+ track?: TrackAnchor;
85
+ }
86
+ /** Snapping configuration. */
87
+ interface SnapOptions {
88
+ /** Master switch. Default `true`. */
89
+ enabled?: boolean;
90
+ /**
91
+ * Snap kinds to consider, highest priority first. Default:
92
+ * `['vertex', 'edge-midpoint', 'mover-center', 'module-boundary',
93
+ * 'station-stop', 'track-path', 'face-center']`.
94
+ */
95
+ kinds?: SnapKind[];
96
+ /** Screen-space radius (px) a candidate must fall within. Default `14`. */
97
+ radiusPx?: number;
98
+ /**
99
+ * Grid step (mm) for the `'grid'` snap, applied to the free hit point when
100
+ * no other candidate matched and `'grid'` is in `kinds`. Default `0` (off).
101
+ */
102
+ gridMm?: number;
103
+ }
104
+ type MeasurementId = string;
105
+ type MeasurementKind = 'distance' | 'angle' | 'path' | 'trackDistance';
106
+ /** Axis constraint for a distance measurement. */
107
+ type MeasurementAxis = 'free' | 'x' | 'y' | 'z';
108
+ interface MeasurementBase {
109
+ id: MeasurementId;
110
+ /** Optional caption rendered above the value. */
111
+ label?: string;
112
+ /** Overrides `MeasurementStyle.color` for this one measurement. */
113
+ color?: string;
114
+ /** Default `true`. */
115
+ visible?: boolean;
116
+ /** Free-form payload the host can round-trip (never read by the viewer). */
117
+ meta?: Record<string, unknown>;
118
+ }
119
+ /** Straight-line distance between two points. */
120
+ interface DistanceMeasurement extends MeasurementBase {
121
+ kind: 'distance';
122
+ points: [MeasurementPoint, MeasurementPoint];
123
+ /** Constrain the reported value to one axis. Default `'free'`. */
124
+ axis?: MeasurementAxis;
125
+ /** Render dashed ΔX / ΔY / ΔZ guides. Default `false`. */
126
+ showComponents?: boolean;
127
+ }
128
+ /** Angle at `points[1]` between the legs to `points[0]` and `points[2]`. */
129
+ interface AngleMeasurement extends MeasurementBase {
130
+ kind: 'angle';
131
+ /** `[legA, vertex, legB]` — the middle point is the vertex. */
132
+ points: [MeasurementPoint, MeasurementPoint, MeasurementPoint];
133
+ }
134
+ /** Polyline through N points; reports per-segment and total length. */
135
+ interface PathMeasurement extends MeasurementBase {
136
+ kind: 'path';
137
+ points: MeasurementPoint[];
138
+ /** Close the polyline back to the first point. Default `false`. */
139
+ closed?: boolean;
140
+ /** Label every segment, not just the total. Default `true`. */
141
+ showSegmentLengths?: boolean;
142
+ }
143
+ /**
144
+ * Distance ALONG the XTS track between two points on the same part — the
145
+ * distance a mover actually travels, not the straight line.
146
+ */
147
+ interface TrackDistanceMeasurement extends MeasurementBase {
148
+ kind: 'trackDistance';
149
+ points: [MeasurementPoint, MeasurementPoint];
150
+ }
151
+ type Measurement = DistanceMeasurement | AngleMeasurement | PathMeasurement | TrackDistanceMeasurement;
152
+ /** Same as `Measurement`, but the store generates the `id` when omitted. */
153
+ type MeasurementInput = (Omit<DistanceMeasurement, 'id'> & {
154
+ id?: MeasurementId;
155
+ }) | (Omit<AngleMeasurement, 'id'> & {
156
+ id?: MeasurementId;
157
+ }) | (Omit<PathMeasurement, 'id'> & {
158
+ id?: MeasurementId;
159
+ }) | (Omit<TrackDistanceMeasurement, 'id'> & {
160
+ id?: MeasurementId;
161
+ });
162
+ /** Computed value of a measurement. */
163
+ interface MeasurementResult {
164
+ id: MeasurementId;
165
+ kind: MeasurementKind;
166
+ /** Millimetres for lengths, degrees for angles. */
167
+ value: number;
168
+ unit: 'mm' | 'deg';
169
+ /** Formatted value as rendered in the scene (e.g. `'1 234.5 mm'`). */
170
+ text: string;
171
+ /** Signed component deltas — `'distance'` only. */
172
+ deltaMm?: Vec3;
173
+ /** Per-segment lengths — `'path'` only. */
174
+ segmentLengthsMm?: number[];
175
+ /**
176
+ * `true` when the measurement could not be evaluated (e.g. a
177
+ * `trackDistance` whose endpoints sit on different parts). `value` is then
178
+ * the straight-line fallback.
179
+ */
180
+ incomplete?: boolean;
181
+ }
182
+ /** Where an annotation hangs. Mover anchors follow the live mover position. */
183
+ type AnnotationAnchor = {
184
+ kind: 'world';
185
+ positionMm: Vec3;
186
+ } | {
187
+ kind: 'mover';
188
+ ref: MoverRef;
189
+ } | {
190
+ kind: 'module';
191
+ ref: ModuleRef;
192
+ } | ({
193
+ kind: 'trackPosition';
194
+ } & TrackAnchor);
195
+ interface Annotation {
196
+ id: string;
197
+ text: string;
198
+ anchor: AnnotationAnchor;
199
+ /**
200
+ * Leader-line offset from the anchor to the label (mm, world frame).
201
+ * Default `[0, 0, 250]`.
202
+ */
203
+ offsetMm?: Vec3;
204
+ color?: string;
205
+ /** Text height (mm). Default `MeasurementStyle.labelSizeMm`. */
206
+ sizeMm?: number;
207
+ /** Marker drawn at the anchor. Default `'Sphere'`. */
208
+ markerShape?: MarkerShape;
209
+ markerSizeMm?: number;
210
+ /** Default `true`. */
211
+ visible?: boolean;
212
+ meta?: Record<string, unknown>;
213
+ }
214
+ type AnnotationInput = Omit<Annotation, 'id'> & {
215
+ id?: string;
216
+ };
217
+ type MeasurementTool = 'none' | 'distance' | 'angle' | 'path' | 'trackDistance' | 'annotation';
218
+ /** In-progress pick. `null` while no tool is armed. */
219
+ interface MeasurementDraft {
220
+ tool: Exclude<MeasurementTool, 'none'>;
221
+ /** Points committed so far. */
222
+ points: MeasurementPoint[];
223
+ /** Point currently under the cursor, or `null` when off the scene. */
224
+ hover: MeasurementPoint | null;
225
+ }
226
+ interface MeasurementStyle {
227
+ /** Line + label colour of committed measurements. Default `'#00E5FF'`. */
228
+ color?: string;
229
+ /** Colour of the in-progress draft. Default `'#FFC400'`. */
230
+ draftColor?: string;
231
+ /** Annotation colour. Default `'#FFFFFF'`. */
232
+ annotationColor?: string;
233
+ /** Line width in pixels. Default `2`. */
234
+ lineWidthPx?: number;
235
+ /** Text height (mm). Default `40`. */
236
+ labelSizeMm?: number;
237
+ /** Endpoint marker diameter (mm). Default `18`. */
238
+ pointSizeMm?: number;
239
+ /** Decimal places on values. Default `1`. */
240
+ decimals?: number;
241
+ /** Unit suffix appended to lengths. Default `' mm'`. */
242
+ unitSuffix?: string;
243
+ /** Font URL passed to troika (see `DEFAULT_LABEL_FONT_URL`). */
244
+ fontFamily?: string;
245
+ /**
246
+ * Draw measurement lines and markers through solid geometry
247
+ * (`depthTest: false`). Default `true` — CAD viewers keep dimensions
248
+ * readable regardless of what is in front of them.
249
+ */
250
+ alwaysOnTop?: boolean;
251
+ }
252
+ /** Serializable snapshot of everything the measurement feature holds. */
253
+ interface MeasurementDocument {
254
+ schemaVersion: 1;
255
+ measurements: Measurement[];
256
+ annotations: Annotation[];
257
+ }
258
+ /**
259
+ * `measurement` prop of `<XtsViewer3D>`. Data can be driven either
260
+ * declaratively (`measurements` / `annotations` — controlled arrays) or
261
+ * imperatively via `viewerRef.current.measurements`. When the controlled
262
+ * arrays are supplied they win on every render, so a host that uses them must
263
+ * persist what the change callbacks report.
264
+ */
265
+ interface MeasurementOptions {
266
+ /** Armed tool. Default `'none'` — no picking, no cursor change. */
267
+ tool?: MeasurementTool;
268
+ /** Controlled measurement list. Omit to let the viewer keep its own. */
269
+ measurements?: MeasurementInput[];
270
+ /** Controlled annotation list. Omit to let the viewer keep its own. */
271
+ annotations?: AnnotationInput[];
272
+ snap?: SnapOptions;
273
+ style?: MeasurementStyle;
274
+ /**
275
+ * Z of the fallback pick plane (mm) used when the ray misses all geometry,
276
+ * so free space can be measured. Default `0`; `null` disables the fallback.
277
+ */
278
+ groundPlaneZMm?: number | null;
279
+ /** Fired after every change to the measurement list. */
280
+ onMeasurementsChange?: (measurements: Measurement[], results: MeasurementResult[]) => void;
281
+ /** Fired once when a tool completes a measurement. */
282
+ onMeasurementCreate?: (measurement: Measurement, result: MeasurementResult) => void;
283
+ onAnnotationsChange?: (annotations: Annotation[]) => void;
284
+ onAnnotationCreate?: (annotation: Annotation) => void;
285
+ /** Fired on every draft change (point added, hover moved, cancelled). */
286
+ onDraftChange?: (draft: MeasurementDraft | null) => void;
287
+ /**
288
+ * Text for an annotation the `'annotation'` tool just placed. Return
289
+ * `null` to abort. Default: `'Annotation'`.
290
+ */
291
+ annotationTextFactory?: (point: MeasurementPoint) => string | null;
292
+ }
36
293
 
37
294
  /**
38
295
  * Public type surface for beckhoff-xts-viewer-3d.
39
296
  *
40
297
  * Anything that crosses the Component boundary — props, refs, callbacks —
41
298
  * is declared here.
299
+ *
300
+ * The measurement feature keeps its own public types next to its
301
+ * implementation (`measurement/types.ts`); only the serialized document shows
302
+ * up here, as part of `XtsModelDocument`.
42
303
  */
304
+
43
305
  /** Beckhoff XTS module types supported by the 3D viewer. */
44
306
  type ModuleType3D = 'AT2000_0250' | 'AT2001_0250' | 'AT2002_0250' | 'AT2002_0249' | 'AT2002_0249_ZX2002_0001' | 'AT2000_0233' | 'AT2000_0249' | 'AT2100_0250' | 'AT2102_0250' | 'AT2020_0250' | 'AT2021_0250' | 'AT2025_0250' | 'AT2026_0250' | 'AT2040_0250' | 'AT2041_0250' | 'AT2042_0250' | 'AT2140_0250' | 'AT2050_0500' | 'AT2050_0501' | 'AT2050_0500_180' | 'AT2200_0500' | 'AT2202_0500' | 'ATH2000_0250' | 'ATH2001_0250' | 'ATH2002_0250' | 'ATH2020_0250' | 'ATH2040_0250' | 'ATH2041_0250' | 'ATH2042_0250' | 'ATH2050_0500' | 'ATH2050_0501' | 'ATH2050_0500_180';
45
307
  /**
@@ -482,6 +744,18 @@ interface DisplayOptions {
482
744
  moverAnchorMarkerSizeMm?: number;
483
745
  /** Color of the anchor marker. Default: '#FF2030' (red). */
484
746
  moverAnchorMarkerColor?: string;
747
+ /**
748
+ * Print each module's type designation onto its type-plate panel.
749
+ *
750
+ * The hygienic ATH modules and the AT2100 / AT2102 variants carry their
751
+ * labelling as modelled geometry and are unaffected. The remaining AT2
752
+ * modules only have a blank plate panel in their CAD data, so the
753
+ * designation is drawn onto it at render time. No logo is reproduced —
754
+ * only the type designation.
755
+ *
756
+ * Default: `true`.
757
+ */
758
+ moduleTypePlates?: boolean;
485
759
  /**
486
760
  * Render shadows: enables the WebGL shadow map, makes one directional
487
761
  * light cast shadows, and renders a transparent shadow-catcher plane
@@ -1041,6 +1315,12 @@ interface XtsModelDocument {
1041
1315
  railSystem?: RailSystem | {
1042
1316
  byProcessingUnit: Record<number, RailSystem>;
1043
1317
  };
1318
+ /**
1319
+ * Measurements + annotations placed on the layout, as returned by
1320
+ * `XtsViewer3DRef.measurements.toJSON()`. Omitted when there are none, so
1321
+ * documents written before the measurement feature stay byte-identical.
1322
+ */
1323
+ measurements?: MeasurementDocument;
1044
1324
  }
1045
1325
  interface XtsRuntimeState {
1046
1326
  movers: Array<{
@@ -1808,6 +2088,221 @@ interface FrameCaptureSession {
1808
2088
  dispose(): void;
1809
2089
  }
1810
2090
 
2091
+ /**
2092
+ * Pure measurement math — no three.js scene, no React.
2093
+ *
2094
+ * Everything the measurement feature reports goes through here, so the values
2095
+ * a host reads back from `XtsViewer3DRef.measurements.results()` are produced
2096
+ * by the same code the scene labels are drawn from.
2097
+ *
2098
+ * Vectors are plain `[x, y, z]` tuples in the viewer's world frame (mm).
2099
+ */
2100
+
2101
+ interface MeasurementFormatOptions {
2102
+ decimals?: number;
2103
+ unitSuffix?: string;
2104
+ }
2105
+ /** Track metrics of the part an endpoint sits on. */
2106
+ interface TrackMetrics {
2107
+ trackLengthMm: number;
2108
+ /** Closed chains measure the shorter way around. */
2109
+ closed: boolean;
2110
+ }
2111
+ /** Resolves the metrics of the part a `TrackAnchor` addresses. */
2112
+ type TrackMetricsResolver = (anchor: TrackAnchor) => TrackMetrics | null;
2113
+ /** Straight-line distance (mm). */
2114
+ declare function distanceMm(a: Vec3, b: Vec3): number;
2115
+ /**
2116
+ * Distance under an axis constraint: `'free'` is the 3D distance, `'x' | 'y' |
2117
+ * 'z'` the absolute delta along that world axis.
2118
+ */
2119
+ declare function axisDistanceMm(a: Vec3, b: Vec3, axis: MeasurementAxis): number;
2120
+ /**
2121
+ * Angle at `vertex` between the legs to `a` and `b`, in degrees `[0, 180]`.
2122
+ * Returns `0` when a leg is degenerate (both picks landed on the vertex).
2123
+ */
2124
+ declare function angleDeg(a: Vec3, vertex: Vec3, b: Vec3): number;
2125
+ /** Per-segment and total length of a polyline. */
2126
+ declare function pathLengthsMm(points: readonly Vec3[], closed?: boolean): {
2127
+ segments: number[];
2128
+ total: number;
2129
+ };
2130
+ /**
2131
+ * Distance along a part's path between two positions on it. On a closed chain
2132
+ * the shorter of the two directions wins — that is the distance a mover
2133
+ * actually travels.
2134
+ */
2135
+ declare function trackDistanceMm(aMm: number, bMm: number, metrics: TrackMetrics): number;
2136
+ /**
2137
+ * Polyline approximating the angle arc at `vertex`, from the `a` leg to the
2138
+ * `b` leg at `radiusMm`. Returns `[]` for a degenerate angle.
2139
+ */
2140
+ declare function arcPoints(a: Vec3, vertex: Vec3, b: Vec3, radiusMm: number, segments?: number): Vec3[];
2141
+ /** Where the value label of a measurement belongs (world mm). */
2142
+ declare function labelAnchorFor(measurement: Measurement): Vec3;
2143
+ /**
2144
+ * Format a length. Plain-space thousands grouping keeps four-digit millimetre
2145
+ * values readable in the 3D label without dragging in a locale dependency —
2146
+ * and without relying on a glyph (thin space) the label font may not ship.
2147
+ */
2148
+ declare function formatLengthMm(valueMm: number, options?: MeasurementFormatOptions): string;
2149
+ /** Format an angle in degrees. */
2150
+ declare function formatAngleDeg(valueDeg: number, options?: MeasurementFormatOptions): string;
2151
+ interface ComputeResultOptions {
2152
+ format?: MeasurementFormatOptions;
2153
+ /** Required for `'trackDistance'`; without it the result falls back to 3D. */
2154
+ resolveTrack?: TrackMetricsResolver;
2155
+ }
2156
+ /** Evaluate a measurement into the value + label the scene renders. */
2157
+ declare function computeMeasurementResult(measurement: Measurement, options?: ComputeResultOptions): MeasurementResult;
2158
+
2159
+ /**
2160
+ * Track-aware snap candidates — the "XTS semantics" half of the picker.
2161
+ *
2162
+ * Geometry snaps (vertex / edge / face, see `snapping.ts`) know nothing about
2163
+ * what they hit. These helpers add the points an XTS layout is actually
2164
+ * measured against: positions on a part's path, module boundaries, station
2165
+ * stop positions and mover centres — all resolved through the same
2166
+ * `orientation ⊗ trackTransform ⊗ partTransformation ⊗ chain` composition
2167
+ * the renderer uses (`composePartWorldMatrix`), so a snap lands exactly where
2168
+ * the geometry is drawn.
2169
+ *
2170
+ * Pure: no three.js scene, no React — only `Matrix4` / `Vector3` math.
2171
+ */
2172
+
2173
+ /** One part, resolved to everything the picker needs about it. */
2174
+ interface TrackIndexEntry {
2175
+ processingUnitObjectId: number;
2176
+ /** `PartConfig.objectId`. */
2177
+ partObjectId: number;
2178
+ /** `PartConfig.globalNumber` — what `StationConfig.partOids` addresses. */
2179
+ partGlobalNumber: number;
2180
+ chain: BuiltChain;
2181
+ /** orientation ⊗ trackTransform ⊗ partTransformation. */
2182
+ matrix: Matrix4;
2183
+ positionFrame?: PositionFrame;
2184
+ closed: boolean;
2185
+ }
2186
+ type TrackIndex = TrackIndexEntry[];
2187
+ /** A point resolved onto a part's path. */
2188
+ interface TrackProjection {
2189
+ entry: TrackIndexEntry;
2190
+ /** Chain-frame position (mm from chain start). */
2191
+ chainPositionMm: number;
2192
+ /** User-frame position — what `MeasurementPoint.track` carries. */
2193
+ partPositionMm: number;
2194
+ positionMm: Vec3;
2195
+ /** Distance from the queried point to the path (mm). */
2196
+ distanceMm: number;
2197
+ }
2198
+ /**
2199
+ * Resolve every visible part of a configuration once. Cheap enough to rebuild
2200
+ * on config change, far too expensive to rebuild per pointer move — callers
2201
+ * memoize it.
2202
+ */
2203
+ declare function buildTrackIndex(config: NormalizedXtsConfig, trackTransformOverrides?: Record<number, TrackTransform>): TrackIndex;
2204
+ /**
2205
+ * World position of a chain-frame position on a part. Positions outside
2206
+ * `[0, trackLengthMm]` clamp to the chain ends (`moverWorldAt` semantics);
2207
+ * `null` only for a chain without modules.
2208
+ */
2209
+ declare function worldPointOnPart(entry: TrackIndexEntry, chainPositionMm: number): Vec3 | null;
2210
+ interface NearestTrackPointOptions {
2211
+ /** Restrict the search to one part (from the hit's scene context). */
2212
+ partGlobalNumber?: number;
2213
+ processingUnitObjectId?: number;
2214
+ /** Coarse scan step (mm). Default 50 restricted, 150 unrestricted. */
2215
+ coarseStepMm?: number;
2216
+ /** Reject results further away than this (mm). Default: no limit. */
2217
+ maxDistanceMm?: number;
2218
+ }
2219
+ /**
2220
+ * Project a world point onto the nearest part path: coarse scan, then a local
2221
+ * refinement around the best coarse sample. Deterministic and allocation-light
2222
+ * so it can run on every pointer move.
2223
+ */
2224
+ declare function nearestTrackPoint(index: TrackIndex, worldPointMm: Vec3, options?: NearestTrackPointOptions): TrackProjection | null;
2225
+ /**
2226
+ * World polyline following a part's path between two chain positions.
2227
+ *
2228
+ * On a closed chain the route across the seam is drawn whenever it is the
2229
+ * shorter one — the polyline then matches what `trackDistanceMm` reports, so
2230
+ * the drawn line and the printed value never disagree.
2231
+ */
2232
+ declare function trackPolylineWorld(entry: TrackIndexEntry, fromChainMm: number, toChainMm: number, stepMm?: number): Vec3[];
2233
+ /**
2234
+ * World position of a mover — the same math `getMoverWorldTransform` reports,
2235
+ * so an annotation anchored to a mover sits exactly on it while it drives.
2236
+ * `getLivePositionMm` supplies the imperative store value; without it the
2237
+ * config-time position is used.
2238
+ */
2239
+ declare function moverAnchorWorld(config: NormalizedXtsConfig, index: TrackIndex, ref: MoverRef, getLivePositionMm?: (moverId: number) => number | undefined): Vec3 | null;
2240
+ /** World centre of a module, for `AnnotationAnchor` of kind `'module'`. */
2241
+ declare function moduleAnchorWorld(index: TrackIndex, processingUnitObjectId: number, partObjectId: number, moduleIndex: number): Vec3 | null;
2242
+ /** World position of a `TrackAnchor` (user-frame mm on a part). */
2243
+ declare function trackAnchorWorld(index: TrackIndex, anchor: TrackAnchor): Vec3 | null;
2244
+
2245
+ /**
2246
+ * `XtsViewer3DRef.measurements` — the public imperative surface of the
2247
+ * measurement feature.
2248
+ *
2249
+ * A thin, dependency-injected facade over `MeasurementStore`: the store owns
2250
+ * the data, this module adds the evaluated results (which need the track
2251
+ * index and the host's formatting options) and the snapshot shape hosts
2252
+ * subscribe to. Keeping it out of the React handle means the whole API can be
2253
+ * exercised in a unit test without mounting a canvas.
2254
+ */
2255
+
2256
+ /** Everything the feature holds, handed to `subscribe` listeners. */
2257
+ interface MeasurementSnapshot {
2258
+ measurements: Measurement[];
2259
+ results: MeasurementResult[];
2260
+ annotations: Annotation[];
2261
+ tool: MeasurementTool;
2262
+ draft: MeasurementDraft | null;
2263
+ }
2264
+ interface MeasurementApi {
2265
+ /** Arm a tool (or `'none'`). Discards a half-finished pick. */
2266
+ setTool(tool: MeasurementTool): void;
2267
+ getTool(): MeasurementTool;
2268
+ /** Points picked so far, `null` while no tool is armed. */
2269
+ getDraft(): MeasurementDraft | null;
2270
+ /** Discard the current pick; the tool stays armed. */
2271
+ cancel(): void;
2272
+ /** Close the current pick — the polyline tool's "done". */
2273
+ finish(): Measurement | null;
2274
+ add(measurement: MeasurementInput): Measurement;
2275
+ update(id: MeasurementId, patch: Partial<Omit<Measurement, 'id' | 'kind'>>): Measurement | null;
2276
+ remove(id: MeasurementId): boolean;
2277
+ clear(): void;
2278
+ /** Replace the whole list (e.g. restoring a persisted session). */
2279
+ set(measurements: readonly MeasurementInput[]): Measurement[];
2280
+ list(): Measurement[];
2281
+ get(id: MeasurementId): Measurement | null;
2282
+ /** Evaluated values of every measurement, in list order. */
2283
+ results(): MeasurementResult[];
2284
+ result(id: MeasurementId): MeasurementResult | null;
2285
+ addAnnotation(annotation: AnnotationInput): Annotation;
2286
+ updateAnnotation(id: string, patch: Partial<Omit<Annotation, 'id'>>): Annotation | null;
2287
+ removeAnnotation(id: string): boolean;
2288
+ clearAnnotations(): void;
2289
+ setAnnotations(annotations: readonly AnnotationInput[]): Annotation[];
2290
+ listAnnotations(): Annotation[];
2291
+ /**
2292
+ * One-off computation between two world points — nothing is added to the
2293
+ * scene. Useful for readouts a host derives from its own data.
2294
+ */
2295
+ measureBetween(a: Vec3, b: Vec3, options?: {
2296
+ axis?: 'free' | 'x' | 'y' | 'z';
2297
+ }): MeasurementResult;
2298
+ /** Fires after every change; returns an unsubscribe function. */
2299
+ subscribe(listener: (snapshot: MeasurementSnapshot) => void): () => void;
2300
+ /** Current state as one object. */
2301
+ snapshot(): MeasurementSnapshot;
2302
+ toJSON(): MeasurementDocument;
2303
+ fromJSON(document: MeasurementDocument): void;
2304
+ }
2305
+
1811
2306
  /**
1812
2307
  * Imperative ref API for <XtsViewer3D>.
1813
2308
  *
@@ -1858,6 +2353,12 @@ interface XtsViewer3DRef {
1858
2353
  getRuntimeState(): XtsRuntimeState;
1859
2354
  exportModel(opts?: {
1860
2355
  includeRuntime?: boolean;
2356
+ /**
2357
+ * Include measurements + annotations (default `true` when any exist).
2358
+ * The key is omitted entirely for a layout without measurements, so
2359
+ * documents stay identical to pre-measurement exports.
2360
+ */
2361
+ includeMeasurements?: boolean;
1861
2362
  }): XtsModelDocument;
1862
2363
  exportScreenshot(opts?: ScreenshotOptions): Promise<ScreenshotResult>;
1863
2364
  /**
@@ -1928,6 +2429,25 @@ interface XtsViewer3DRef {
1928
2429
  * sorted deepest-overlap-first.
1929
2430
  */
1930
2431
  checkModuleCollisions(opts?: CheckModuleCollisionsOptions): ModuleCollision[];
2432
+ /**
2433
+ * CAD-style measurements and annotations — see `MeasurementApi`.
2434
+ *
2435
+ * The namespace is stable for the lifetime of the component, so it can be
2436
+ * captured once:
2437
+ *
2438
+ * ```ts
2439
+ * const measure = viewerRef.current.measurements;
2440
+ * measure.setTool('distance'); // user picks two points
2441
+ * measure.add({ kind: 'distance', points: [ // or place one directly
2442
+ * { positionMm: [0, 0, 200] },
2443
+ * { positionMm: [1000, 0, 200] },
2444
+ * ]});
2445
+ * const doc = measure.toJSON(); // persist
2446
+ * ```
2447
+ *
2448
+ * Works the same in the 2D plan view (`projection='orthographic'`).
2449
+ */
2450
+ measurements: MeasurementApi;
1931
2451
  }
1932
2452
 
1933
2453
  /**
@@ -2376,6 +2896,28 @@ interface XtsViewer3DProps {
2376
2896
  warningGapMm?: number;
2377
2897
  onCollisionsChange?: (collisions: ModuleCollision[]) => void;
2378
2898
  };
2899
+ /**
2900
+ * CAD-style measurement tools + annotations, in the 3D view and the 2D
2901
+ * plan view alike.
2902
+ *
2903
+ * Arm a tool (`measurement={{ tool: 'distance' }}`) and the user picks
2904
+ * points in the scene with snapping; everything already measured is
2905
+ * available — and editable — through `viewerRef.current.measurements`.
2906
+ * Selection is suppressed while a tool is armed so a measuring click never
2907
+ * also selects a mover.
2908
+ *
2909
+ * @example
2910
+ * ```tsx
2911
+ * <XtsViewer3D
2912
+ * measurement={{
2913
+ * tool,
2914
+ * snap: { enabled: true, radiusPx: 14 },
2915
+ * onMeasurementCreate: (m, result) => console.log(result.text),
2916
+ * }}
2917
+ * />
2918
+ * ```
2919
+ */
2920
+ measurement?: MeasurementOptions;
2379
2921
  /** Dev/Debug. */
2380
2922
  debug?: {
2381
2923
  showPathSplines?: boolean;
@@ -3073,4 +3615,105 @@ declare function getSharedAssetLoader(): AssetLoader;
3073
3615
  /** Compose a URL from a base + a filename, normalising trailing slashes. */
3074
3616
  declare function composeAssetUrl(baseUrl: string, filename: string): string;
3075
3617
 
3076
- export { type AreaConfig, type AreaOptions, AssetLoader, type AssetManifest, BUILTIN_MODULE_SIDECARS, BUILTIN_MOVER_SIDECARS, BUILTIN_RAIL_SIDECARS, BUILTIN_TOOL_SIDECARS, type BuiltChain, type BuiltModule, type CameraProjection, type CameraState, type CaptureMode, type CheckModuleCollisionsOptions, type CheckMoverCollisionsOptions, type ClickModifiers, type CoordinateSystem, type CustomAssetBinding, type CustomAssetConfig, type CustomMoverLayout, DEFAULT_LABEL_FONT_URL, DEFAULT_MODULE_HALF_WIDTH_MM, DEFAULT_MODULE_HEIGHT_MM, DEFAULT_MODULE_MANIFEST, DEFAULT_MOVER_MANIFEST, DEFAULT_ORIGIN_CORRECTION, DEFAULT_RAIL_MANIFEST, DEFAULT_TOOL_MANIFEST, type DimensionOptions, type DisplayOptions, EMPTY_SELECTION, type EndDelta, type FeedSegment, type FeedSegmentHighlight, type FeedSegmentSelector, type FeedSegmentSpan, type FocusOptions, type FocusTarget, type FrameCaptureOptions, type FrameCaptureSession, HEPCO_GFX_PROFILE, type HeatmapSample, type HepcoGfxRailProfileDims, IDENTITY_POSE, INFEED_MODULE_TYPES, type InfoBarConfig, type InfoBarMarkerConfig, type InfoBarOptions, JSDELIVR_ASSETS_BASE_URL, MODULE_CATALOG, MODULE_GUIDING_RAIL_MAP, MOVER_CATALOG, type MarkerOptions, type MarkerShape, type ModuleAssetEntry, type ModuleCatalogEntry, type ModuleCollision, type ModuleCollisionPartInput, type ModuleCollisionXpuInput, ModuleCornerMarkers, type ModuleEntry, type ModuleHighlight, type ModuleProbe, type ModuleRef, type ModuleSidecar, type ModuleStatusEntry, type ModuleType3D, type MoverAssetEntry, type MoverCatalogEntry, type MoverCollision, type MoverConfig, MoverIdLabel, type MoverIdLabelOptions, type MoverPositionEntry, type MoverProbe, type MoverRef, type MoverSidecar, type MoverToolAssetEntry, type MoverToolConfig, type MoverToolSidecar, type MoverToolType3D, type MoverType3D, type NormalizationWarning, type NormalizedXtsConfig, type Orientation, type OriginCorrection, type PartConfig, type PartPath, type PartTransformation, type PathSample, type PathType, type PlanarPose, type PointCloud, type PositionFrame, type ProcessingUnitConfig, type RailAssetEntry, type RailSidecar, type RailSystem, type RailType3D, type ScreenshotFormat, type ScreenshotMode, type ScreenshotOptions, type ScreenshotResult, type SelectionMode, type SelectionState, SidecarLoader, type SidecarOverrides, type SidecarSourceConfig, SidecarSourceContext, type SplitFeedSegmentsOptions, type StationConfig, type StationOptions, type StatorHeatmap, type StopPositionMoverOptions, type TextOptions, type TrackTransform, VERSION, type Vec2, type Vec3, XtsArcCurve3, type XtsConfig, XtsConfigNormalizer, type XtsModelDocument, XtsPointCloudCurve3, type XtsRuntimeState, XtsViewer3D, type XtsViewer3DProps, type XtsViewer3DRef, type XtsViewerError, type XtsViewerErrorCode, XtsViewerErrorException, applyBeckhoffXtsConvention, applyEmptyClick, applyModuleClick, applyMoverClick, buildChain, buildModuleProbes, buildPartPath, chainToUserPositionMm, checkModuleCollisions, checkSamePathCollisions, collectFeedSegments, collectFeedSegmentsForXpu, composeAssetUrl, createHepcoGfxBaseplateShape, createHepcoGfxRailShape, deselectAll, findModuleAt, getModuleEntry, getMoverEntry, getPointCloud, getSharedAssetLoader, interpolateHeatmapValue, isChainClosed, isKnownModuleType, isKnownMoverType, isModuleSelected, isMoverSelected, moduleEndDelta, moduleSidecarFilename, moverSidecarFilename, moverWorldAt, normaliseToHeatmapRange, normalizeXtsConfig, pickGlbForRail, poseToMatrix4, railSidecarFilename, registerPointCloud, resolveGuidingRailGlbUrl, resolveGuidingRailType, resolveHepcoGfxProfile, resolveModuleGlbUrl, resolveMoverGlbUrl, resolveOriginCorrection, resolveStopPositionMm, resolveToolGlbUrl, sampleChainPoint, sampleChainRange, sampleModulePath, sortHeatmapSamples, splitIntoFeedSegments, toolSidecarFilename, trackLengthOf, unregisterPointCloud, useSidecarSource, userToChainPositionMm };
3618
+ /**
3619
+ * Style resolution for the measurement overlay — one place where every
3620
+ * default lives, so the scene, the draft preview and the value formatting
3621
+ * agree without each component re-deriving fallbacks.
3622
+ */
3623
+
3624
+ interface ResolvedMeasurementStyle {
3625
+ color: string;
3626
+ draftColor: string;
3627
+ annotationColor: string;
3628
+ lineWidthPx: number;
3629
+ labelSizeMm: number;
3630
+ pointSizeMm: number;
3631
+ decimals: number;
3632
+ unitSuffix: string;
3633
+ fontFamily?: string;
3634
+ alwaysOnTop: boolean;
3635
+ }
3636
+ declare const DEFAULT_MEASUREMENT_STYLE: Omit<ResolvedMeasurementStyle, 'fontFamily'>;
3637
+ declare function resolveMeasurementStyle(style?: MeasurementStyle): ResolvedMeasurementStyle;
3638
+
3639
+ /**
3640
+ * Snap candidate collection + selection.
3641
+ *
3642
+ * A pick is resolved in two steps:
3643
+ * 1. collect candidates — corners / edge midpoints / face centre of the
3644
+ * triangle the ray hit (`collectTriangleSnapCandidates`), plus the
3645
+ * semantic XTS points from `trackSnapping.ts`;
3646
+ * 2. choose one — `chooseSnapCandidate` scores them in SCREEN space, so a
3647
+ * snap feels the same whether the user is 300 mm or 30 m from the
3648
+ * geometry, in the perspective view and in the 2D plan view alike.
3649
+ *
3650
+ * `chooseSnapCandidate` takes the projection as a function rather than a
3651
+ * camera, which keeps the selection rule testable without a renderer.
3652
+ */
3653
+
3654
+ interface SnapCandidate {
3655
+ kind: SnapKind;
3656
+ positionMm: Vec3;
3657
+ entity?: MeasuredEntityRef;
3658
+ track?: TrackAnchor;
3659
+ }
3660
+ /** Screen-space point in CSS pixels, relative to the canvas. */
3661
+ interface PointerPx {
3662
+ x: number;
3663
+ y: number;
3664
+ }
3665
+ /** Projects a world point to canvas pixels, or `null` when off-screen. */
3666
+ type PointProjector = (positionMm: Vec3) => PointerPx | null;
3667
+ /**
3668
+ * Default priority order. Corners beat everything (they are what a CAD user
3669
+ * aims at), the semantic XTS points come next, and the face centre is last —
3670
+ * it is the weakest hint and would otherwise mask a nearby vertex.
3671
+ */
3672
+ declare const DEFAULT_SNAP_KINDS: SnapKind[];
3673
+ declare const DEFAULT_SNAP_RADIUS_PX = 14;
3674
+ interface ChooseSnapOptions {
3675
+ radiusPx?: number;
3676
+ /** Allowed kinds, highest priority first. Default {@link DEFAULT_SNAP_KINDS}. */
3677
+ kinds?: readonly SnapKind[];
3678
+ }
3679
+ /**
3680
+ * Pick the best candidate within `radiusPx` of the pointer: higher-priority
3681
+ * kinds win outright, ties break on screen distance. Returns `null` when
3682
+ * nothing qualifies — the caller then keeps the raw hit point.
3683
+ */
3684
+ declare function chooseSnapCandidate(candidates: readonly SnapCandidate[], project: PointProjector, pointerPx: PointerPx, options?: ChooseSnapOptions): SnapCandidate | null;
3685
+ /** Minimal structural view of a raycast hit — keeps this testable. */
3686
+ interface TriangleHit {
3687
+ object: Object3D;
3688
+ face?: {
3689
+ a: number;
3690
+ b: number;
3691
+ c: number;
3692
+ } | null | undefined;
3693
+ instanceId?: number | undefined;
3694
+ }
3695
+ /**
3696
+ * Corners, edge midpoints and centre of the triangle the ray hit, in world
3697
+ * space. Returns `[]` for hits without face information (lines, points,
3698
+ * sprites) or geometry without a position attribute.
3699
+ */
3700
+ declare function collectTriangleSnapCandidates(hit: TriangleHit, entity?: MeasuredEntityRef): SnapCandidate[];
3701
+ /** Round a point onto a `gridMm` lattice. `gridMm <= 0` returns the input. */
3702
+ declare function snapToGrid(positionMm: Vec3, gridMm: number): Vec3;
3703
+
3704
+ /**
3705
+ * Scene identity tags — how a raycast hit becomes an XTS entity reference.
3706
+ *
3707
+ * The renderer already names its groups (`xpu-3`, `part-1`, …), but a name is
3708
+ * a debug aid, not a contract: it carries no object ids and would break the
3709
+ * moment a name is reworded. Instead every group that corresponds to a
3710
+ * configuration entity carries a structured `userData.xtsTag`, and the picker
3711
+ * walks the parent chain to assemble the reference — nearest tag of each kind
3712
+ * wins, which is exactly the nesting the scene builds
3713
+ * (xpu → part → module | mover).
3714
+ */
3715
+
3716
+ /** Convenience: `resolveSceneContext` + `toEntityRef`. */
3717
+ declare function resolveEntityFromObject(object: Object3D | null, instanceId?: number): MeasuredEntityRef | null;
3718
+
3719
+ export { type AngleMeasurement, type Annotation, type AnnotationAnchor, type AnnotationInput, type AreaConfig, type AreaOptions, AssetLoader, type AssetManifest, BUILTIN_MODULE_SIDECARS, BUILTIN_MOVER_SIDECARS, BUILTIN_RAIL_SIDECARS, BUILTIN_TOOL_SIDECARS, type BuiltChain, type BuiltModule, type CameraProjection, type CameraState, type CaptureMode, type CheckModuleCollisionsOptions, type CheckMoverCollisionsOptions, type ClickModifiers, type CoordinateSystem, type CustomAssetBinding, type CustomAssetConfig, type CustomMoverLayout, DEFAULT_LABEL_FONT_URL, DEFAULT_MEASUREMENT_STYLE, DEFAULT_MODULE_HALF_WIDTH_MM, DEFAULT_MODULE_HEIGHT_MM, DEFAULT_MODULE_MANIFEST, DEFAULT_MOVER_MANIFEST, DEFAULT_ORIGIN_CORRECTION, DEFAULT_RAIL_MANIFEST, DEFAULT_SNAP_KINDS, DEFAULT_SNAP_RADIUS_PX, DEFAULT_TOOL_MANIFEST, type DimensionOptions, type DisplayOptions, type DistanceMeasurement, EMPTY_SELECTION, type EndDelta, type FeedSegment, type FeedSegmentHighlight, type FeedSegmentSelector, type FeedSegmentSpan, type FocusOptions, type FocusTarget, type FrameCaptureOptions, type FrameCaptureSession, HEPCO_GFX_PROFILE, type HeatmapSample, type HepcoGfxRailProfileDims, IDENTITY_POSE, INFEED_MODULE_TYPES, type InfoBarConfig, type InfoBarMarkerConfig, type InfoBarOptions, JSDELIVR_ASSETS_BASE_URL, MODULE_CATALOG, MODULE_GUIDING_RAIL_MAP, MOVER_CATALOG, type MarkerOptions, type MarkerShape, type MeasuredEntityRef, type Measurement, type MeasurementApi, type MeasurementAxis, type MeasurementDocument, type MeasurementDraft, type MeasurementFormatOptions, type MeasurementId, type MeasurementInput, type MeasurementKind, type MeasurementOptions, type MeasurementPoint, type MeasurementResult, type MeasurementSnapshot, type MeasurementStyle, type MeasurementTool, type ModuleAssetEntry, type ModuleCatalogEntry, type ModuleCollision, type ModuleCollisionPartInput, type ModuleCollisionXpuInput, ModuleCornerMarkers, type ModuleEntry, type ModuleHighlight, type ModuleProbe, type ModuleRef, type ModuleSidecar, type ModuleStatusEntry, type ModuleType3D, type MoverAssetEntry, type MoverCatalogEntry, type MoverCollision, type MoverConfig, MoverIdLabel, type MoverIdLabelOptions, type MoverPositionEntry, type MoverProbe, type MoverRef, type MoverSidecar, type MoverToolAssetEntry, type MoverToolConfig, type MoverToolSidecar, type MoverToolType3D, type MoverType3D, type NormalizationWarning, type NormalizedXtsConfig, type Orientation, type OriginCorrection, type PartConfig, type PartPath, type PartTransformation, type PathMeasurement, type PathSample, type PathType, type PlanarPose, type PointCloud, type PointProjector, type PositionFrame, type ProcessingUnitConfig, type RailAssetEntry, type RailSidecar, type RailSystem, type RailType3D, type ResolvedMeasurementStyle, type ScreenshotFormat, type ScreenshotMode, type ScreenshotOptions, type ScreenshotResult, type SelectionMode, type SelectionState, SidecarLoader, type SidecarOverrides, type SidecarSourceConfig, SidecarSourceContext, type SnapCandidate, type SnapKind, type SnapOptions, type SplitFeedSegmentsOptions, type StationConfig, type StationOptions, type StatorHeatmap, type StopPositionMoverOptions, type TextOptions, type TrackAnchor, type TrackDistanceMeasurement, type TrackIndex, type TrackIndexEntry, type TrackMetrics, type TrackMetricsResolver, type TrackProjection, type TrackTransform, VERSION, type Vec2, type Vec3, XtsArcCurve3, type XtsConfig, XtsConfigNormalizer, type XtsModelDocument, XtsPointCloudCurve3, type XtsRuntimeState, XtsViewer3D, type XtsViewer3DProps, type XtsViewer3DRef, type XtsViewerError, type XtsViewerErrorCode, XtsViewerErrorException, angleDeg, applyBeckhoffXtsConvention, applyEmptyClick, applyModuleClick, applyMoverClick, arcPoints, axisDistanceMm, buildChain, buildModuleProbes, buildPartPath, buildTrackIndex, chainToUserPositionMm, checkModuleCollisions, checkSamePathCollisions, chooseSnapCandidate, collectFeedSegments, collectFeedSegmentsForXpu, collectTriangleSnapCandidates, composeAssetUrl, computeMeasurementResult, createHepcoGfxBaseplateShape, createHepcoGfxRailShape, deselectAll, distanceMm, findModuleAt, formatAngleDeg, formatLengthMm, getModuleEntry, getMoverEntry, getPointCloud, getSharedAssetLoader, interpolateHeatmapValue, isChainClosed, isKnownModuleType, isKnownMoverType, isModuleSelected, isMoverSelected, labelAnchorFor, moduleAnchorWorld, moduleEndDelta, moduleSidecarFilename, moverAnchorWorld, moverSidecarFilename, moverWorldAt, nearestTrackPoint, normaliseToHeatmapRange, normalizeXtsConfig, pathLengthsMm, pickGlbForRail, poseToMatrix4, railSidecarFilename, registerPointCloud, resolveEntityFromObject, resolveGuidingRailGlbUrl, resolveGuidingRailType, resolveHepcoGfxProfile, resolveMeasurementStyle, resolveModuleGlbUrl, resolveMoverGlbUrl, resolveOriginCorrection, resolveStopPositionMm, resolveToolGlbUrl, sampleChainPoint, sampleChainRange, sampleModulePath, snapToGrid, sortHeatmapSamples, splitIntoFeedSegments, toolSidecarFilename, trackAnchorWorld, trackDistanceMm, trackLengthOf, trackPolylineWorld, unregisterPointCloud, useSidecarSource, userToChainPositionMm, worldPointOnPart };