beckhoff-xts-viewer-3d 5.2.0 → 5.2.2

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.
Files changed (38) hide show
  1. package/README.md +35 -8
  2. package/dist/index.cjs +2 -2
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +880 -753
  5. package/dist/index.d.ts +880 -753
  6. package/dist/index.js +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/docs/screenshots/01-packaging-line.webp +0 -0
  9. package/docs/screenshots/02-standard-at.webp +0 -0
  10. package/docs/screenshots/03-eco-at2200.webp +0 -0
  11. package/docs/screenshots/04-nct-tools.webp +0 -0
  12. package/docs/screenshots/05-hygienic-ath.webp +0 -0
  13. package/docs/screenshots/06-hepco-gfx.webp +0 -0
  14. package/docs/screenshots/07-materials-closeup.webp +0 -0
  15. package/docs/screenshots/08-multi-track.webp +0 -0
  16. package/docs/screenshots/09-stations-areas.webp +0 -0
  17. package/docs/screenshots/10-dimensions.webp +0 -0
  18. package/docs/screenshots/11-stator-heatmap.webp +0 -0
  19. package/docs/screenshots/12-collision.webp +0 -0
  20. package/docs/screenshots/13-drive-status.webp +0 -0
  21. package/docs/screenshots/14-feed-segments.webp +0 -0
  22. package/docs/screenshots/15-measurements.webp +0 -0
  23. package/docs/screenshots/16-selection.webp +0 -0
  24. package/docs/screenshots/17-plan-view.webp +0 -0
  25. package/docs/screenshots/18-shadows.webp +0 -0
  26. package/docs/screenshots/19-perf-stress.webp +0 -0
  27. package/docs/screenshots/README.md +106 -46
  28. package/package.json +6 -3
  29. package/docs/screenshots/01-oval-loop.png +0 -0
  30. package/docs/screenshots/02-multi-track.png +0 -0
  31. package/docs/screenshots/03-stations-areas.png +0 -0
  32. package/docs/screenshots/04-stator-heatmap.png +0 -0
  33. package/docs/screenshots/05-collision.png +0 -0
  34. package/docs/screenshots/06-drive-status.png +0 -0
  35. package/docs/screenshots/07-perf-stress.png +0 -0
  36. package/docs/screenshots/08-shadows.png +0 -0
  37. package/docs/screenshots/09-screenshot-export.png +0 -0
  38. package/docs/screenshots/10-feed-segments.png +0 -0
package/dist/index.d.cts CHANGED
@@ -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.2.0";
15
+ declare const VERSION: "5.2.2";
16
16
 
17
17
  /**
18
18
  * Default CDN URL for the GLB asset bundle (`beckhoff-xts-viewer-3d-assets`).
@@ -35,273 +35,14 @@ declare const VERSION: "5.2.0";
35
35
  declare const JSDELIVR_ASSETS_BASE_URL: "https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d-assets@2.0.1/models";
36
36
 
37
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
- }
293
-
294
- /**
295
- * Public type surface for beckhoff-xts-viewer-3d.
38
+ * Catalogue taxonomy — the string unions naming every module, mover, tool and
39
+ * guiding-rail the viewer can render.
296
40
  *
297
- * Anything that crosses the Component boundary — props, refs, callbacks —
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`.
41
+ * These are the identifiers the asset pipeline, the geometry catalogues
42
+ * (`geometry/ModuleCatalog`, `geometry/MoverCatalog`) and the sidecar files
43
+ * all key off, so they are the narrowest and most-depended-on part of the
44
+ * public type surface. Nothing here imports anything.
303
45
  */
304
-
305
46
  /** Beckhoff XTS module types supported by the 3D viewer. */
306
47
  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';
307
48
  /**
@@ -343,313 +84,28 @@ type MoverToolType3D = 'AT8200_1000_0100' | 'AT8200_2000_0100' | 'Custom';
343
84
  * GLB and therefore do NOT receive a separate guiding-rail mesh.
344
85
  */
345
86
  type RailType3D = 'AT9000_0249' | 'AT9000_0250' | 'AT9000_0500' | 'AT9020_1250' | 'AT9025_1466' | 'AT9040_0750' | 'AT9050_0500';
346
- type Vec3 = [number, number, number];
347
- type Vec2 = [number, number];
348
- /** Pose of the entire XTS group in the world frame. */
349
- interface Orientation {
350
- positionMm?: Vec3;
351
- /** Intrinsic XYZ Euler angles in degrees. */
352
- rotationDegEuler?: Vec3;
353
- }
354
- /**
355
- * Per-part heatmap samples plus colour range. Rendered as a coloured
356
- * tube running along each part's centerline (with optional lateral /
357
- * vertical offset), with the tube's vertex colours interpolated between
358
- * `minColor` and `maxColor` according to the consumer-supplied
359
- * `(positionMm, value)` samples.
360
- *
361
- * Typical use: per-stator drive current, motor temperature, or fault
362
- * counters streamed from the controller and overlaid on the layout in
363
- * real time. Sample positions are interpreted in the XPU's user-frame
364
- * (`ProcessingUnitConfig.positionFrame`), same convention as mover /
365
- * station / area positions.
366
- */
367
- interface StatorHeatmap {
368
- /**
369
- * One bucket per `PartConfig.globalNumber`. Parts not listed render
370
- * no heatmap. Samples within a part are sorted by `positionMm`
371
- * before interpolation; values between consecutive samples are
372
- * linearly interpolated, values outside the sample range are
373
- * held flat at the nearest sample.
374
- */
375
- parts: Array<{
376
- partOid: number;
377
- samples: Array<{
378
- positionMm: number;
379
- value: number;
380
- }>;
381
- }>;
382
- /** Value mapped to `minColor`. Values ≤ min clamp to `minColor`. */
383
- min: number;
384
- /** Value mapped to `maxColor`. Values ≥ max clamp to `maxColor`. */
385
- max: number;
386
- /** Colour at `min`. Default `'#22c55e'` (green). */
387
- minColor?: string;
388
- /** Colour at `max`. Default `'#ef4444'` (red). */
389
- maxColor?: string;
390
- /** Tube thickness in mm. Default 6. */
391
- thicknessMm?: number;
392
- /** Vertical Z lift (mm). Default -15 — sits just below the rail. */
393
- displacementMm?: number;
394
- /** Lateral Y displacement (mm). Default 0. */
395
- lateralDisplacementMm?: number;
396
- /**
397
- * Path segments per module along the heatmap tube. More = smoother
398
- * gradient, more triangles. Default 8 (= 32 segments on a 4-module
399
- * straight, ~1 GPU vertex per 8 mm of track).
400
- */
401
- segmentsPerModule?: number;
402
- /** Tube opacity (0..1). Default 0.85. */
403
- opacity?: number;
404
- }
405
- interface XtsConfig {
406
- orientation?: Orientation;
407
- processingUnits: ProcessingUnitConfig[];
408
- stations?: StationConfig[];
409
- /**
410
- * Labelled regions along the track. Like a StationConfig, but
411
- * without stop positions — only a coloured tube segment plus an
412
- * optional description billboard. For zone markings (e.g. "Manual
413
- * Access", "Safety Area", "Cleanroom") that have no
414
- * machine stops.
415
- */
416
- areas?: AreaConfig[];
417
- infoBars?: InfoBarConfig[];
418
- customAssets?: CustomAssetConfig[];
419
- }
420
- interface ProcessingUnitConfig {
421
- /**
422
- * Stable host-supplied 32-bit unique identifier for this XPU. Required
423
- * at runtime — must be a non-negative integer that fits in a uint32
424
- * and is unique across all XPUs in the config.
425
- */
426
- objectId: number;
427
- moverType: MoverType3D;
428
- customMoverLayout?: CustomMoverLayout;
429
- /** Default: 'Beckhoff'. */
430
- railSystem?: RailSystem;
431
- parts: PartConfig[];
432
- movers: MoverConfig[];
433
- /**
434
- * Track-level pose, applied to the entire XPU subtree (modules, movers,
435
- * tools, stations, info bars, mover-bound custom assets). Composes
436
- * MULTIPLICATIVELY with the root `XtsConfig.orientation`:
437
- *
438
- * world = orientation ⊗ trackTransform ⊗ partTransformation ⊗ chain
439
- *
440
- * `scaleUniform` is applied as a uniform scale factor (default 1). Non-
441
- * uniform scaling is intentionally not supported — it would distort path
442
- * radii and break GLB lighting.
443
- */
444
- trackTransform?: TrackTransform;
445
- /**
446
- * User-frame remap applied to every `partPositionMm`-style value inside
447
- * this XPU (mover positions, station start/end/stops, area start/end,
448
- * stop-position ghost movers). Lets a host that uses a different "zero"
449
- * or sign convention than the GLB chain enter positions in their own
450
- * frame; the renderer translates to the chain's intrinsic frame:
451
- *
452
- * chainPos = originMm + (direction === 'negative' ? -userPos : userPos)
453
- *
454
- * Defaults: `direction: 'positive'`, `originMm: 0` — i.e. user-frame
455
- * equals chain-frame, no transform.
456
- */
457
- positionFrame?: PositionFrame;
458
- }
459
- /**
460
- * Sign + zero-offset convention for partPosition values within an XPU.
461
- * See `ProcessingUnitConfig.positionFrame` for semantics.
462
- */
463
- interface PositionFrame {
464
- /** Travel direction. Default: `'positive'`. */
465
- direction?: 'positive' | 'negative';
466
- /** Zero-point shift (in chain-coordinates mm). Default: 0. */
467
- originMm?: number;
468
- }
469
- interface TrackTransform {
470
- positionMm?: Vec3;
471
- /** Intrinsic XYZ Euler angles in degrees. */
472
- rotationDegEuler?: Vec3;
473
- /** Uniform scale factor. Default 1. */
474
- scaleUniform?: number;
475
- /** Hide the entire XPU when false. Default true. */
476
- visible?: boolean;
477
- }
478
- interface PartConfig {
479
- /**
480
- * Stable host-supplied 32-bit unique identifier for this part. Distinct
481
- * from `globalNumber` (server-side ObjectId): callers reference parts
482
- * by `objectId` at runtime — e.g. in the array form of
483
- * `XtsViewer3DRef.setMoverPositions` (`{partObjectId, partPosition}`).
484
- * Required at runtime — must be a non-negative integer that fits in a
485
- * uint32 and is unique across all parts in the system.
486
- * `normalizeXtsConfig` emits a `missing-part-object-id` /
487
- * `invalid-part-object-id` / `duplicate-part-object-id` warning when
488
- * the contract is violated.
489
- */
490
- objectId: number;
491
- /** = ServerTypes.PartModel.ObjectId — unique system-wide. */
492
- globalNumber: number;
493
- modules: ModuleEntry[];
494
- partTransformation?: PartTransformation;
495
- }
496
- interface ModuleEntry {
497
- moduleType: ModuleType3D;
498
- /**
499
- * Stable host-supplied number for this module. Required at runtime —
500
- * simply counted up per module (1, 2, 3, …) across the whole system
501
- * and used as the index in `XtsViewer3DRef.setModuleStatuses(array)` —
502
- * i.e. `statuses[m.globalNumber] === { warning?: …, error?: … }`.
503
- * `normalizeXtsConfig` emits a `missing-module-global-number` /
504
- * `invalid-module-global-number` / `duplicate-module-global-number`
505
- * warning when the contract is violated. Typed optional only so
506
- * legacy configs and tests keep compiling; new code MUST set it.
507
- */
508
- globalNumber?: number;
509
- /**
510
- * Optional drive-status overlay for this module. Mirrors the
511
- * `MoverConfig.status` shape; the same per-mesh emissive blink +
512
- * billboard <DriveStatusIcon> surfaces both. Filtered by
513
- * `display.showDriveWarnings` / `display.showDriveErrors`.
514
- *
515
- * For live updates (e.g. drive warnings/errors streamed from PLC),
516
- * prefer the imperative
517
- * `XtsViewer3DRef.setModuleStatuses(array)` channel — index = this
518
- * module's `globalNumber`. The store wins over this config-time value.
519
- */
520
- status?: {
521
- warning?: boolean;
522
- error?: boolean;
523
- };
524
- }
87
+
525
88
  /**
526
- * Per-part 3D pose, applied to EVERYTHING anchored to the part:
527
- * modules, guiding rails, movers, mover tools, mover-bound custom assets,
528
- * and the path-derived overlays (stations, areas, dimensions, info bars,
529
- * stop-position ghost movers, stator heatmap). Composes inside the
530
- * per-XPU `trackTransform`:
531
- *
532
- * world = orientation ⊗ trackTransform ⊗ partTransformation ⊗ chain
533
- *
534
- * All fields are optional and default to identity (offset = [0,0,0],
535
- * rotation = [0,0,0]). Whole-track placement is done via
536
- * `ProcessingUnitConfig.trackTransform`; this transform is for moving a
537
- * part WITHIN its XPU — e.g. lifts that translate along Z, parts
538
- * mounted at an angle, or live-editable kinematics in the playground.
89
+ * Scalar tuples shared by every other type module.
539
90
  *
540
- * Multi-part overlays (stations / areas spanning multiple `partOids`)
541
- * follow the transform of their FIRST partOid — the chain math
542
- * upstream is part-local, and points from differently-transformed
543
- * parts can't be stitched into a single tube. Anchor multi-part
544
- * overlays to the part you want them to track.
91
+ * The leaf of the type graph: `Vec3` is a position or an Euler triple in
92
+ * millimetres / degrees, `Vec2` a viewport or margin pair in pixels. Kept
93
+ * apart from the layout types so `display`, `annotations` and `config` can
94
+ * all reach them without importing each other.
545
95
  */
546
- interface PartTransformation {
547
- /** Translation in mm, in the XPU's track-transform frame. */
548
- offsetMm?: Vec3;
549
- /** Intrinsic XYZ Euler angles in degrees. */
550
- rotationDegEuler?: Vec3;
551
- partSide?: 'Default' | 'Driver' | 'Encoder';
552
- /** Hide everything on this part when false. Default true. */
553
- visible?: boolean;
554
- }
555
- interface MoverConfig {
556
- /** Position-on-track index within the XPU. */
557
- index: number;
558
- /**
559
- * Stable host-supplied 32-bit unique identifier for this mover.
560
- * Required, must be a non-negative integer that fits in a uint32 and
561
- * is unique across all movers in the config.
562
- */
563
- id: number;
564
- /** References PartConfig.globalNumber. */
565
- partOid: number;
566
- /** [0, part.trackLengthMm] — clamped when out-of-bounds. */
567
- partPositionMm: number;
568
- status?: {
569
- warning?: boolean;
570
- error?: boolean;
571
- };
572
- selected?: boolean;
573
- /** Predefined tool carriers or custom GLBs attached to this mover. */
574
- tools?: MoverToolConfig[];
575
- }
576
- /**
577
- * Runtime mover-position update entry — pushed through
578
- * `XtsViewer3DRef.setMoverPositions(array)`. The array is indexed by
579
- * `MoverConfig.index` (i.e. entry `i` targets the mover with
580
- * `index === i`); use `null` / `undefined` to skip a slot. The
581
- * `partObjectId` field references `PartConfig.objectId`.
582
- */
583
- interface MoverPositionEntry {
584
- /** = PartConfig.objectId (the host-side 32-bit unique part ID). */
585
- partObjectId: number;
586
- /** Position along the referenced part, in user-frame mm. */
587
- partPosition: number;
588
- }
589
- /**
590
- * Runtime drive-status entry for a single module — pushed through
591
- * `XtsViewer3DRef.setModuleStatuses(array)`. The array is indexed by
592
- * `ModuleEntry.globalNumber`; use `null` / `undefined` to clear a slot.
96
+ type Vec3 = [number, number, number];
97
+ type Vec2 = [number, number];
98
+
99
+ /**
100
+ * Display options — how a layout is drawn, as opposed to what it is.
101
+ *
102
+ * Everything reachable from the `display` prop: visibility toggles, lighting
103
+ * and post-processing, and the per-overlay styling blocks (`StationOptions`,
104
+ * `AreaOptions`, `DimensionOptions`, …). The overlays these style are declared
105
+ * as configuration in `annotations.ts`; nothing here changes the layout, only
106
+ * its appearance.
593
107
  */
594
- interface ModuleStatusEntry {
595
- warning?: boolean;
596
- error?: boolean;
597
- }
598
- interface MoverToolConfig {
599
- toolType: MoverToolType3D;
600
- /** Required when toolType === 'Custom'. */
601
- customGlbUrl?: string;
602
- customOriginCorrection?: {
603
- translateMm: Vec3;
604
- rotationDegEuler: Vec3;
605
- };
606
- /** Local placement on the mover, relative to the magnet-plate center. */
607
- offsetMm?: Vec3;
608
- rotationDegEuler?: Vec3;
609
- opacity?: number;
610
- visible?: boolean;
611
- id?: string;
612
- }
613
- interface CustomMoverLayout {
614
- glbUrl: string;
615
- originCorrection?: {
616
- translateMm: Vec3;
617
- rotationDegEuler: Vec3;
618
- };
619
- magnetPlateCenterMm?: Vec3;
620
- pathLengthMm?: number;
621
- imageFrontUrl?: string;
622
- imageBackUrl?: string;
623
- }
624
- type CustomAssetBinding = {
625
- type: 'static';
626
- positionMm: Vec3;
627
- rotationDegEuler: Vec3;
628
- scale?: number;
629
- } | {
630
- type: 'mover';
631
- moverRef: {
632
- processingUnitObjectId: number;
633
- moverIndex: number;
634
- };
635
- offsetMm?: Vec3;
636
- rotationDegEuler?: Vec3;
637
- /** Uniform scale (default 1). */
638
- scale?: number;
639
- } | {
640
- type: 'all-movers';
641
- offsetMm?: Vec3;
642
- rotationDegEuler?: Vec3;
643
- /** Uniform scale (default 1). Applied per mover. */
644
- scale?: number;
645
- };
646
- interface CustomAssetConfig {
647
- id: string;
648
- glbUrl: string;
649
- binding: CustomAssetBinding;
650
- opacity?: number;
651
- visible?: boolean;
652
- }
108
+
653
109
  /**
654
110
  * Y-axis / rotation convention for incoming position data.
655
111
  *
@@ -1024,101 +480,436 @@ interface DimensionOptions {
1024
480
  * Whether intermediate ticks also get value labels. Only applies when
1025
481
  * `showValues` is true. Default: true.
1026
482
  */
1027
- showIntermediateValues?: boolean;
483
+ showIntermediateValues?: boolean;
484
+ /**
485
+ * What number to display next to each tick:
486
+ * - 'partPosition' (default): cumulative mm from the part's start.
487
+ * - 'fromModule' : mm from the start of the containing module.
488
+ * - 'both' : "<part> mm (<module> mm)".
489
+ */
490
+ valueMode?: 'partPosition' | 'fromModule' | 'both';
491
+ }
492
+ interface InfoBarOptions {
493
+ defaultThicknessMm?: number;
494
+ textOptions?: TextOptions;
495
+ }
496
+ interface TextOptions {
497
+ sizeMm?: number;
498
+ color?: string;
499
+ fontFamily?: string;
500
+ }
501
+
502
+ /**
503
+ * Track annotations — stations, areas and info bars.
504
+ *
505
+ * Regions and labels a host places along the track. They are configuration,
506
+ * not geometry: each is anchored to part object ids and positions on those
507
+ * parts, and the renderer resolves them against the built chain. How they are
508
+ * styled lives in `display.ts`.
509
+ */
510
+
511
+ interface StationConfig {
512
+ stationId: number;
513
+ description: string;
514
+ isEnabled: boolean;
515
+ partOids: number[];
516
+ startPositionOnPart: number;
517
+ endPositionOnPart: number;
518
+ /** ARGB int (same as 2D). */
519
+ stationColor: number;
520
+ stopPositions?: number[];
521
+ /**
522
+ * Reference frame for `stopPositions` values:
523
+ * - `'track'` (default) — values are absolute partPositionMm from
524
+ * track start. Same convention as
525
+ * `startPositionOnPart` / `endPositionOnPart`.
526
+ * - `'station'` — values are RELATIVE to `startPositionOnPart`. So
527
+ * `0` puts a stop right at the station's start;
528
+ * `100` puts one 100 mm into the station regardless
529
+ * of where the station lives on the part.
530
+ *
531
+ * Useful when stops are conceptually tied to the station's geometry
532
+ * (entry, mid, exit) rather than absolute mm marks on the track.
533
+ */
534
+ stopPositionsRelativeTo?: 'track' | 'station';
535
+ /**
536
+ * Per-station override of the stop-marker geometry. Falls back to
537
+ * `display.stationMarkerOptions.shape` when omitted. Lets one station
538
+ * render Diamonds while a neighbour uses Cones, etc.
539
+ */
540
+ stopMarkerShape?: MarkerShape;
541
+ /** Per-station override of the stop-marker size (mm). */
542
+ stopMarkerSizeMm?: number;
543
+ }
544
+ /**
545
+ * Labelled region along the track. Identical to
546
+ * `StationConfig`, but without stop positions — areas only carry text +
547
+ * colour and describe a zone (e.g. "Cleanroom", "Manual Access",
548
+ * "Safety Loop") that does not trigger machine stops.
549
+ *
550
+ * Multi-part: same semantics as `StationConfig.partOids`. With a single
551
+ * partOid and `endPositionOnPart < startPositionOnPart`, a closed loop
552
+ * is rendered (to the end of the track, then from 0 to End).
553
+ */
554
+ interface AreaConfig {
555
+ areaId: number;
556
+ description: string;
557
+ isEnabled: boolean;
558
+ partOids: number[];
559
+ startPositionOnPart: number;
560
+ endPositionOnPart: number;
561
+ /** ARGB int (same as StationConfig). Used for tube + default label. */
562
+ color: number;
563
+ }
564
+ interface InfoBarConfig {
565
+ processingUnitObjectId: number;
566
+ partObjectId: number;
567
+ partStartPositionMm: number;
568
+ partEndPositionMm: number;
569
+ thickness: number;
570
+ displacement: number;
571
+ color: {
572
+ color: string;
573
+ };
574
+ visible: boolean;
575
+ text?: string;
576
+ textPlacement?: 'Left' | 'Center' | 'Right';
577
+ textDisplacement?: number;
578
+ textOptions?: TextOptions;
579
+ markers?: InfoBarMarkerConfig[];
580
+ zIndex: number;
581
+ }
582
+ interface InfoBarMarkerConfig {
583
+ positionMm: number;
584
+ shape: 'Diamond' | 'Tick' | 'None';
585
+ sizeMm: number;
586
+ color?: string;
587
+ }
588
+
589
+ /**
590
+ * The configuration tree — what a host hands `<XtsViewer3D config={...}>`.
591
+ *
592
+ * `XtsConfig` down through processing units, parts, modules and movers: the
593
+ * description of a physical layout, independent of how it is drawn. Rendering
594
+ * preferences live in `display.ts`, and the overlays placed on top of a layout
595
+ * (stations, areas, info bars) in `annotations.ts`.
596
+ */
597
+
598
+ /** Pose of the entire XTS group in the world frame. */
599
+ interface Orientation {
600
+ positionMm?: Vec3;
601
+ /** Intrinsic XYZ Euler angles in degrees. */
602
+ rotationDegEuler?: Vec3;
603
+ }
604
+ /**
605
+ * Per-part heatmap samples plus colour range. Rendered as a coloured
606
+ * tube running along each part's centerline (with optional lateral /
607
+ * vertical offset), with the tube's vertex colours interpolated between
608
+ * `minColor` and `maxColor` according to the consumer-supplied
609
+ * `(positionMm, value)` samples.
610
+ *
611
+ * Typical use: per-stator drive current, motor temperature, or fault
612
+ * counters streamed from the controller and overlaid on the layout in
613
+ * real time. Sample positions are interpreted in the XPU's user-frame
614
+ * (`ProcessingUnitConfig.positionFrame`), same convention as mover /
615
+ * station / area positions.
616
+ */
617
+ interface StatorHeatmap {
618
+ /**
619
+ * One bucket per `PartConfig.globalNumber`. Parts not listed render
620
+ * no heatmap. Samples within a part are sorted by `positionMm`
621
+ * before interpolation; values between consecutive samples are
622
+ * linearly interpolated, values outside the sample range are
623
+ * held flat at the nearest sample.
624
+ */
625
+ parts: Array<{
626
+ partOid: number;
627
+ samples: Array<{
628
+ positionMm: number;
629
+ value: number;
630
+ }>;
631
+ }>;
632
+ /** Value mapped to `minColor`. Values ≤ min clamp to `minColor`. */
633
+ min: number;
634
+ /** Value mapped to `maxColor`. Values ≥ max clamp to `maxColor`. */
635
+ max: number;
636
+ /** Colour at `min`. Default `'#22c55e'` (green). */
637
+ minColor?: string;
638
+ /** Colour at `max`. Default `'#ef4444'` (red). */
639
+ maxColor?: string;
640
+ /** Tube thickness in mm. Default 6. */
641
+ thicknessMm?: number;
642
+ /** Vertical Z lift (mm). Default -15 — sits just below the rail. */
643
+ displacementMm?: number;
644
+ /** Lateral Y displacement (mm). Default 0. */
645
+ lateralDisplacementMm?: number;
646
+ /**
647
+ * Path segments per module along the heatmap tube. More = smoother
648
+ * gradient, more triangles. Default 8 (= 32 segments on a 4-module
649
+ * straight, ~1 GPU vertex per 8 mm of track).
650
+ */
651
+ segmentsPerModule?: number;
652
+ /** Tube opacity (0..1). Default 0.85. */
653
+ opacity?: number;
654
+ }
655
+ interface XtsConfig {
656
+ orientation?: Orientation;
657
+ processingUnits: ProcessingUnitConfig[];
658
+ stations?: StationConfig[];
659
+ /**
660
+ * Labelled regions along the track. Like a StationConfig, but
661
+ * without stop positions — only a coloured tube segment plus an
662
+ * optional description billboard. For zone markings (e.g. "Manual
663
+ * Access", "Safety Area", "Cleanroom") that have no
664
+ * machine stops.
665
+ */
666
+ areas?: AreaConfig[];
667
+ infoBars?: InfoBarConfig[];
668
+ customAssets?: CustomAssetConfig[];
669
+ }
670
+ interface ProcessingUnitConfig {
671
+ /**
672
+ * Stable host-supplied 32-bit unique identifier for this XPU. Required
673
+ * at runtime — must be a non-negative integer that fits in a uint32
674
+ * and is unique across all XPUs in the config.
675
+ */
676
+ objectId: number;
677
+ moverType: MoverType3D;
678
+ customMoverLayout?: CustomMoverLayout;
679
+ /** Default: 'Beckhoff'. */
680
+ railSystem?: RailSystem;
681
+ parts: PartConfig[];
682
+ movers: MoverConfig[];
683
+ /**
684
+ * Track-level pose, applied to the entire XPU subtree (modules, movers,
685
+ * tools, stations, info bars, mover-bound custom assets). Composes
686
+ * MULTIPLICATIVELY with the root `XtsConfig.orientation`:
687
+ *
688
+ * world = orientation ⊗ trackTransform ⊗ partTransformation ⊗ chain
689
+ *
690
+ * `scaleUniform` is applied as a uniform scale factor (default 1). Non-
691
+ * uniform scaling is intentionally not supported — it would distort path
692
+ * radii and break GLB lighting.
693
+ */
694
+ trackTransform?: TrackTransform;
1028
695
  /**
1029
- * What number to display next to each tick:
1030
- * - 'partPosition' (default): cumulative mm from the part's start.
1031
- * - 'fromModule' : mm from the start of the containing module.
1032
- * - 'both' : "<part> mm (<module> mm)".
696
+ * User-frame remap applied to every `partPositionMm`-style value inside
697
+ * this XPU (mover positions, station start/end/stops, area start/end,
698
+ * stop-position ghost movers). Lets a host that uses a different "zero"
699
+ * or sign convention than the GLB chain enter positions in their own
700
+ * frame; the renderer translates to the chain's intrinsic frame:
701
+ *
702
+ * chainPos = originMm + (direction === 'negative' ? -userPos : userPos)
703
+ *
704
+ * Defaults: `direction: 'positive'`, `originMm: 0` — i.e. user-frame
705
+ * equals chain-frame, no transform.
1033
706
  */
1034
- valueMode?: 'partPosition' | 'fromModule' | 'both';
707
+ positionFrame?: PositionFrame;
1035
708
  }
1036
- interface InfoBarOptions {
1037
- defaultThicknessMm?: number;
1038
- textOptions?: TextOptions;
709
+ /**
710
+ * Sign + zero-offset convention for partPosition values within an XPU.
711
+ * See `ProcessingUnitConfig.positionFrame` for semantics.
712
+ */
713
+ interface PositionFrame {
714
+ /** Travel direction. Default: `'positive'`. */
715
+ direction?: 'positive' | 'negative';
716
+ /** Zero-point shift (in chain-coordinates mm). Default: 0. */
717
+ originMm?: number;
1039
718
  }
1040
- interface TextOptions {
1041
- sizeMm?: number;
1042
- color?: string;
1043
- fontFamily?: string;
719
+ interface TrackTransform {
720
+ positionMm?: Vec3;
721
+ /** Intrinsic XYZ Euler angles in degrees. */
722
+ rotationDegEuler?: Vec3;
723
+ /** Uniform scale factor. Default 1. */
724
+ scaleUniform?: number;
725
+ /** Hide the entire XPU when false. Default true. */
726
+ visible?: boolean;
1044
727
  }
1045
- interface StationConfig {
1046
- stationId: number;
1047
- description: string;
1048
- isEnabled: boolean;
1049
- partOids: number[];
1050
- startPositionOnPart: number;
1051
- endPositionOnPart: number;
1052
- /** ARGB int (same as 2D). */
1053
- stationColor: number;
1054
- stopPositions?: number[];
728
+ interface PartConfig {
1055
729
  /**
1056
- * Reference frame for `stopPositions` values:
1057
- * - `'track'` (default) — values are absolute partPositionMm from
1058
- * track start. Same convention as
1059
- * `startPositionOnPart` / `endPositionOnPart`.
1060
- * - `'station'` — values are RELATIVE to `startPositionOnPart`. So
1061
- * `0` puts a stop right at the station's start;
1062
- * `100` puts one 100 mm into the station regardless
1063
- * of where the station lives on the part.
1064
- *
1065
- * Useful when stops are conceptually tied to the station's geometry
1066
- * (entry, mid, exit) rather than absolute mm marks on the track.
730
+ * Stable host-supplied 32-bit unique identifier for this part. Distinct
731
+ * from `globalNumber` (server-side ObjectId): callers reference parts
732
+ * by `objectId` at runtime — e.g. in the array form of
733
+ * `XtsViewer3DRef.setMoverPositions` (`{partObjectId, partPosition}`).
734
+ * Required at runtime — must be a non-negative integer that fits in a
735
+ * uint32 and is unique across all parts in the system.
736
+ * `normalizeXtsConfig` emits a `missing-part-object-id` /
737
+ * `invalid-part-object-id` / `duplicate-part-object-id` warning when
738
+ * the contract is violated.
1067
739
  */
1068
- stopPositionsRelativeTo?: 'track' | 'station';
740
+ objectId: number;
741
+ /** = ServerTypes.PartModel.ObjectId — unique system-wide. */
742
+ globalNumber: number;
743
+ modules: ModuleEntry[];
744
+ partTransformation?: PartTransformation;
745
+ }
746
+ interface ModuleEntry {
747
+ moduleType: ModuleType3D;
1069
748
  /**
1070
- * Per-station override of the stop-marker geometry. Falls back to
1071
- * `display.stationMarkerOptions.shape` when omitted. Lets one station
1072
- * render Diamonds while a neighbour uses Cones, etc.
749
+ * Stable host-supplied number for this module. Required at runtime —
750
+ * simply counted up per module (1, 2, 3, …) across the whole system
751
+ * and used as the index in `XtsViewer3DRef.setModuleStatuses(array)` —
752
+ * i.e. `statuses[m.globalNumber] === { warning?: …, error?: … }`.
753
+ * `normalizeXtsConfig` emits a `missing-module-global-number` /
754
+ * `invalid-module-global-number` / `duplicate-module-global-number`
755
+ * warning when the contract is violated. Typed optional only so
756
+ * legacy configs and tests keep compiling; new code MUST set it.
1073
757
  */
1074
- stopMarkerShape?: MarkerShape;
1075
- /** Per-station override of the stop-marker size (mm). */
1076
- stopMarkerSizeMm?: number;
758
+ globalNumber?: number;
759
+ /**
760
+ * Optional drive-status overlay for this module. Mirrors the
761
+ * `MoverConfig.status` shape; the same per-mesh emissive blink +
762
+ * billboard <DriveStatusIcon> surfaces both. Filtered by
763
+ * `display.showDriveWarnings` / `display.showDriveErrors`.
764
+ *
765
+ * For live updates (e.g. drive warnings/errors streamed from PLC),
766
+ * prefer the imperative
767
+ * `XtsViewer3DRef.setModuleStatuses(array)` channel — index = this
768
+ * module's `globalNumber`. The store wins over this config-time value.
769
+ */
770
+ status?: {
771
+ warning?: boolean;
772
+ error?: boolean;
773
+ };
1077
774
  }
1078
775
  /**
1079
- * Labelled region along the track. Identical to
1080
- * `StationConfig`, but without stop positions — areas only carry text +
1081
- * colour and describe a zone (e.g. "Cleanroom", "Manual Access",
1082
- * "Safety Loop") that does not trigger machine stops.
776
+ * Per-part 3D pose, applied to EVERYTHING anchored to the part:
777
+ * modules, guiding rails, movers, mover tools, mover-bound custom assets,
778
+ * and the path-derived overlays (stations, areas, dimensions, info bars,
779
+ * stop-position ghost movers, stator heatmap). Composes inside the
780
+ * per-XPU `trackTransform`:
1083
781
  *
1084
- * Multi-part: same semantics as `StationConfig.partOids`. With a single
1085
- * partOid and `endPositionOnPart < startPositionOnPart`, a closed loop
1086
- * is rendered (to the end of the track, then from 0 to End).
782
+ * world = orientation ⊗ trackTransform ⊗ partTransformation ⊗ chain
783
+ *
784
+ * All fields are optional and default to identity (offset = [0,0,0],
785
+ * rotation = [0,0,0]). Whole-track placement is done via
786
+ * `ProcessingUnitConfig.trackTransform`; this transform is for moving a
787
+ * part WITHIN its XPU — e.g. lifts that translate along Z, parts
788
+ * mounted at an angle, or live-editable kinematics in the playground.
789
+ *
790
+ * Multi-part overlays (stations / areas spanning multiple `partOids`)
791
+ * follow the transform of their FIRST partOid — the chain math
792
+ * upstream is part-local, and points from differently-transformed
793
+ * parts can't be stitched into a single tube. Anchor multi-part
794
+ * overlays to the part you want them to track.
1087
795
  */
1088
- interface AreaConfig {
1089
- areaId: number;
1090
- description: string;
1091
- isEnabled: boolean;
1092
- partOids: number[];
1093
- startPositionOnPart: number;
1094
- endPositionOnPart: number;
1095
- /** ARGB int (same as StationConfig). Used for tube + default label. */
1096
- color: number;
796
+ interface PartTransformation {
797
+ /** Translation in mm, in the XPU's track-transform frame. */
798
+ offsetMm?: Vec3;
799
+ /** Intrinsic XYZ Euler angles in degrees. */
800
+ rotationDegEuler?: Vec3;
801
+ partSide?: 'Default' | 'Driver' | 'Encoder';
802
+ /** Hide everything on this part when false. Default true. */
803
+ visible?: boolean;
1097
804
  }
1098
- interface InfoBarConfig {
1099
- processingUnitObjectId: number;
805
+ interface MoverConfig {
806
+ /** Position-on-track index within the XPU. */
807
+ index: number;
808
+ /**
809
+ * Stable host-supplied 32-bit unique identifier for this mover.
810
+ * Required, must be a non-negative integer that fits in a uint32 and
811
+ * is unique across all movers in the config.
812
+ */
813
+ id: number;
814
+ /** References PartConfig.globalNumber. */
815
+ partOid: number;
816
+ /** [0, part.trackLengthMm] — clamped when out-of-bounds. */
817
+ partPositionMm: number;
818
+ status?: {
819
+ warning?: boolean;
820
+ error?: boolean;
821
+ };
822
+ selected?: boolean;
823
+ /** Predefined tool carriers or custom GLBs attached to this mover. */
824
+ tools?: MoverToolConfig[];
825
+ }
826
+ /**
827
+ * Runtime mover-position update entry — pushed through
828
+ * `XtsViewer3DRef.setMoverPositions(array)`. The array is indexed by
829
+ * `MoverConfig.index` (i.e. entry `i` targets the mover with
830
+ * `index === i`); use `null` / `undefined` to skip a slot. The
831
+ * `partObjectId` field references `PartConfig.objectId`.
832
+ */
833
+ interface MoverPositionEntry {
834
+ /** = PartConfig.objectId (the host-side 32-bit unique part ID). */
1100
835
  partObjectId: number;
1101
- partStartPositionMm: number;
1102
- partEndPositionMm: number;
1103
- thickness: number;
1104
- displacement: number;
1105
- color: {
1106
- color: string;
836
+ /** Position along the referenced part, in user-frame mm. */
837
+ partPosition: number;
838
+ }
839
+ /**
840
+ * Runtime drive-status entry for a single module — pushed through
841
+ * `XtsViewer3DRef.setModuleStatuses(array)`. The array is indexed by
842
+ * `ModuleEntry.globalNumber`; use `null` / `undefined` to clear a slot.
843
+ */
844
+ interface ModuleStatusEntry {
845
+ warning?: boolean;
846
+ error?: boolean;
847
+ }
848
+ interface MoverToolConfig {
849
+ toolType: MoverToolType3D;
850
+ /** Required when toolType === 'Custom'. */
851
+ customGlbUrl?: string;
852
+ customOriginCorrection?: {
853
+ translateMm: Vec3;
854
+ rotationDegEuler: Vec3;
1107
855
  };
1108
- visible: boolean;
1109
- text?: string;
1110
- textPlacement?: 'Left' | 'Center' | 'Right';
1111
- textDisplacement?: number;
1112
- textOptions?: TextOptions;
1113
- markers?: InfoBarMarkerConfig[];
1114
- zIndex: number;
856
+ /** Local placement on the mover, relative to the magnet-plate center. */
857
+ offsetMm?: Vec3;
858
+ rotationDegEuler?: Vec3;
859
+ opacity?: number;
860
+ visible?: boolean;
861
+ id?: string;
1115
862
  }
1116
- interface InfoBarMarkerConfig {
1117
- positionMm: number;
1118
- shape: 'Diamond' | 'Tick' | 'None';
1119
- sizeMm: number;
1120
- color?: string;
863
+ interface CustomMoverLayout {
864
+ glbUrl: string;
865
+ originCorrection?: {
866
+ translateMm: Vec3;
867
+ rotationDegEuler: Vec3;
868
+ };
869
+ magnetPlateCenterMm?: Vec3;
870
+ pathLengthMm?: number;
871
+ imageFrontUrl?: string;
872
+ imageBackUrl?: string;
873
+ }
874
+ type CustomAssetBinding = {
875
+ type: 'static';
876
+ positionMm: Vec3;
877
+ rotationDegEuler: Vec3;
878
+ scale?: number;
879
+ } | {
880
+ type: 'mover';
881
+ moverRef: {
882
+ processingUnitObjectId: number;
883
+ moverIndex: number;
884
+ };
885
+ offsetMm?: Vec3;
886
+ rotationDegEuler?: Vec3;
887
+ /** Uniform scale (default 1). */
888
+ scale?: number;
889
+ } | {
890
+ type: 'all-movers';
891
+ offsetMm?: Vec3;
892
+ rotationDegEuler?: Vec3;
893
+ /** Uniform scale (default 1). Applied per mover. */
894
+ scale?: number;
895
+ };
896
+ interface CustomAssetConfig {
897
+ id: string;
898
+ glbUrl: string;
899
+ binding: CustomAssetBinding;
900
+ opacity?: number;
901
+ visible?: boolean;
1121
902
  }
903
+
904
+ /**
905
+ * Interaction surface — selection, highlighting and the camera.
906
+ *
907
+ * The types a host exchanges with the viewer at runtime rather than at
908
+ * configuration time: what is addressed (`ModuleRef`, `MoverRef`), what is
909
+ * picked out (`SelectionState`, `ModuleHighlight`, `FeedSegmentHighlight`),
910
+ * and where the camera looks (`CameraState`, `FocusTarget`).
911
+ */
912
+
1122
913
  interface ModuleRef {
1123
914
  processingUnitObjectId: number;
1124
915
  partObjectId: number;
@@ -1183,125 +974,412 @@ interface FeedSegmentHighlight {
1183
974
  color: string;
1184
975
  }
1185
976
  /**
1186
- * One feed segment (Einspeisestrang) as detected in the configuration: the
1187
- * contiguous run of modules starting at an infeed module (Einspeisemodul,
1188
- * `ModuleCatalogEntry.isInfeed`) and ending right before the next infeed
1189
- * module of the same part.
1190
- *
1191
- * On a closed chain (oval, U-turn pair — see `isChainClosed`) the modules
1192
- * ahead of the part's first infeed module belong to the LAST segment, which
1193
- * then carries `wrapsSeam: true`. On an open chain those leading modules
1194
- * belong to no segment at all and cannot be tinted.
1195
- *
1196
- * Returned by `XtsViewer3DRef.getFeedSegments()`.
977
+ * One feed segment (Einspeisestrang) as detected in the configuration: the
978
+ * contiguous run of modules starting at an infeed module (Einspeisemodul,
979
+ * `ModuleCatalogEntry.isInfeed`) and ending right before the next infeed
980
+ * module of the same part.
981
+ *
982
+ * On a closed chain (oval, U-turn pair — see `isChainClosed`) the modules
983
+ * ahead of the part's first infeed module belong to the LAST segment, which
984
+ * then carries `wrapsSeam: true`. On an open chain those leading modules
985
+ * belong to no segment at all and cannot be tinted.
986
+ *
987
+ * Returned by `XtsViewer3DRef.getFeedSegments()`.
988
+ */
989
+ interface FeedSegment {
990
+ processingUnitObjectId: number;
991
+ partObjectId: number;
992
+ /** 0-based, in chain order, counted from the part's first infeed module. */
993
+ segmentIndex: number;
994
+ /**
995
+ * `ModuleEntry.globalNumber` of the leading infeed module. `undefined` when
996
+ * the config does not set it — such a segment can only be addressed via
997
+ * `{ partObjectId, segmentIndex }`.
998
+ */
999
+ infeedModuleGlobalNumber?: number;
1000
+ /** Module type of the leading infeed module. */
1001
+ infeedModuleType: ModuleType3D;
1002
+ /** Chain indices of every module in this segment, in chain order. */
1003
+ moduleIndices: number[];
1004
+ /** Σ of the segment's module lengths, in mm. */
1005
+ lengthMm: number;
1006
+ /** True when the segment wraps the closed-loop seam. */
1007
+ wrapsSeam: boolean;
1008
+ }
1009
+ interface CameraState {
1010
+ positionMm: Vec3;
1011
+ targetMm: Vec3;
1012
+ /** Always 'Z' in the current spec. */
1013
+ upAxis: 'Z' | 'Y';
1014
+ zoom?: number;
1015
+ }
1016
+ /**
1017
+ * Live camera projection.
1018
+ * - `'perspective'` (default) — the regular 3D view.
1019
+ * - `'orthographic'` — paired with `topDown`, gives the flat 2D plan view
1020
+ * that matches `exportScreenshot({ mode: 'top-down' })`.
1021
+ */
1022
+ type CameraProjection = 'perspective' | 'orthographic';
1023
+ /**
1024
+ * Target for the imperative `viewerRef.current.focusOn(target, opts)` camera
1025
+ * animation. The viewer computes the target's world-space bounding box and
1026
+ * flies the camera so the whole object fits the frame:
1027
+ * - In `'perspective'` projection the current view angle is preserved
1028
+ * (the camera only dollies / re-centres).
1029
+ * - In `'orthographic'` top-down the frustum is re-framed and the camera
1030
+ * pans straight over the object.
1031
+ */
1032
+ type FocusTarget = {
1033
+ kind: 'scene';
1034
+ } | {
1035
+ kind: 'station';
1036
+ stationId: number;
1037
+ } | {
1038
+ kind: 'area';
1039
+ areaId: number;
1040
+ } | {
1041
+ kind: 'mover';
1042
+ ref: MoverRef;
1043
+ } | {
1044
+ kind: 'module';
1045
+ ref: ModuleRef;
1046
+ };
1047
+ interface FocusOptions {
1048
+ /** Animation duration in ms. Default 700. `0` jumps immediately. */
1049
+ durationMs?: number;
1050
+ /**
1051
+ * Padding around the target's bounding box (1 = tight fit). Default 1.2 in
1052
+ * perspective, 1.1 in orthographic top-down.
1053
+ */
1054
+ paddingFactor?: number;
1055
+ /** Easing curve. Default `'easeInOutCubic'`. */
1056
+ easing?: 'linear' | 'easeInOutCubic';
1057
+ }
1058
+
1059
+ /**
1060
+ * Viewer errors.
1061
+ *
1062
+ * `XtsViewerErrorException` is the one runtime value in the public type
1063
+ * surface — everything else the viewer exports as `types` is erased at
1064
+ * compile time. It is thrown by the normalization pass for configurations
1065
+ * that cannot be rendered at all; recoverable problems are reported through
1066
+ * the `onError` callback as plain `XtsViewerError` records instead.
1067
+ */
1068
+ type XtsViewerErrorCode = 'asset-load-failed' | 'unknown-module-type' | 'unknown-mover-type' | 'unknown-tool-type' | 'invalid-config' | 'unmatched-clothoid-half' | 'webgl-context-lost';
1069
+ interface XtsViewerError {
1070
+ code: XtsViewerErrorCode;
1071
+ message: string;
1072
+ details?: unknown;
1073
+ }
1074
+ declare class XtsViewerErrorException extends Error {
1075
+ readonly code: XtsViewerErrorCode;
1076
+ readonly details?: unknown;
1077
+ constructor(code: XtsViewerErrorCode, message: string, details?: unknown);
1078
+ }
1079
+
1080
+ /**
1081
+ * Asset manifest — per-type overrides for the GLB URLs the viewer loads.
1082
+ *
1083
+ * A host registers custom or relocated assets here instead of replacing the
1084
+ * whole `assetsBaseUrl`. Entries fall back to the default CDN mapping for any
1085
+ * type they do not name.
1086
+ */
1087
+
1088
+ interface AssetManifest {
1089
+ /** Override per moduleType. Falls back to default mapping otherwise. */
1090
+ modules?: Partial<Record<ModuleType3D, ModuleAssetEntry>>;
1091
+ movers?: Partial<Record<MoverType3D, MoverAssetEntry>>;
1092
+ tools?: Partial<Record<MoverToolType3D, MoverToolAssetEntry>>;
1093
+ guidingRails?: Partial<Record<RailType3D, RailAssetEntry>>;
1094
+ }
1095
+ interface RailAssetEntry {
1096
+ glbUrl: string;
1097
+ sidecarUrl?: string;
1098
+ }
1099
+ interface ModuleAssetEntry {
1100
+ /** Per-RailSystem GLB URL (relative to assetsBaseUrl). */
1101
+ glbByRailSystem: {
1102
+ Beckhoff: string | null;
1103
+ HepcoGfx?: string | null;
1104
+ };
1105
+ sidecarUrl?: string;
1106
+ }
1107
+ interface MoverAssetEntry {
1108
+ glbUrl: string;
1109
+ sidecarUrl?: string;
1110
+ }
1111
+ interface MoverToolAssetEntry {
1112
+ glbUrl: string;
1113
+ sidecarUrl?: string;
1114
+ }
1115
+
1116
+ /**
1117
+ * Public types for the measurement + annotation feature.
1118
+ *
1119
+ * Everything here is expressed in the viewer's WORLD frame — millimetres,
1120
+ * Z-up, identical to `XtsViewer3DRef.getMoverWorldTransform()` and
1121
+ * `CameraState.positionMm`. That is what makes one implementation serve both
1122
+ * the perspective 3D view and the orthographic 2D plan view
1123
+ * (`projection='orthographic'`): only the camera differs, the geometry does
1124
+ * not.
1125
+ */
1126
+
1127
+ /**
1128
+ * How a measurement point was derived. `'free'` is the raw ray hit (surface
1129
+ * or ground plane); everything else is a snap the picker resolved to.
1130
+ */
1131
+ type SnapKind = 'free' | 'vertex' | 'edge-midpoint' | 'face-center' | 'grid' | 'mover-center' | 'module-boundary' | 'module-center' | 'station-stop' | 'track-path';
1132
+ /** Scene entity a picked point belongs to, when the picker could resolve one. */
1133
+ type MeasuredEntityRef = {
1134
+ kind: 'module';
1135
+ ref: ModuleRef;
1136
+ } | {
1137
+ kind: 'mover';
1138
+ ref: MoverRef;
1139
+ } | {
1140
+ kind: 'station';
1141
+ stationId: number;
1142
+ };
1143
+ /** A position on a part's track path, in the XPU's user position frame. */
1144
+ interface TrackAnchor {
1145
+ processingUnitObjectId: number;
1146
+ /** `PartConfig.objectId`. */
1147
+ partObjectId: number;
1148
+ /** Position along the part, user frame (same convention as movers). */
1149
+ partPositionMm: number;
1150
+ }
1151
+ /** One measurement vertex. */
1152
+ interface MeasurementPoint {
1153
+ /** World position (mm). */
1154
+ positionMm: Vec3;
1155
+ /** How the point was obtained. Default `'free'` when omitted. */
1156
+ snap?: SnapKind;
1157
+ /** Entity the point sits on, when known. */
1158
+ entity?: MeasuredEntityRef;
1159
+ /**
1160
+ * Track position, set whenever the point resolved onto a part's path.
1161
+ * Required on both endpoints of a `'trackDistance'` measurement.
1162
+ */
1163
+ track?: TrackAnchor;
1164
+ }
1165
+ /** Snapping configuration. */
1166
+ interface SnapOptions {
1167
+ /** Master switch. Default `true`. */
1168
+ enabled?: boolean;
1169
+ /**
1170
+ * Snap kinds to consider, highest priority first. Default:
1171
+ * `['vertex', 'edge-midpoint', 'mover-center', 'module-boundary',
1172
+ * 'station-stop', 'track-path', 'face-center']`.
1173
+ */
1174
+ kinds?: SnapKind[];
1175
+ /** Screen-space radius (px) a candidate must fall within. Default `14`. */
1176
+ radiusPx?: number;
1177
+ /**
1178
+ * Grid step (mm) for the `'grid'` snap, applied to the free hit point when
1179
+ * no other candidate matched and `'grid'` is in `kinds`. Default `0` (off).
1180
+ */
1181
+ gridMm?: number;
1182
+ }
1183
+ type MeasurementId = string;
1184
+ type MeasurementKind = 'distance' | 'angle' | 'path' | 'trackDistance';
1185
+ /** Axis constraint for a distance measurement. */
1186
+ type MeasurementAxis = 'free' | 'x' | 'y' | 'z';
1187
+ interface MeasurementBase {
1188
+ id: MeasurementId;
1189
+ /** Optional caption rendered above the value. */
1190
+ label?: string;
1191
+ /** Overrides `MeasurementStyle.color` for this one measurement. */
1192
+ color?: string;
1193
+ /** Default `true`. */
1194
+ visible?: boolean;
1195
+ /** Free-form payload the host can round-trip (never read by the viewer). */
1196
+ meta?: Record<string, unknown>;
1197
+ }
1198
+ /** Straight-line distance between two points. */
1199
+ interface DistanceMeasurement extends MeasurementBase {
1200
+ kind: 'distance';
1201
+ points: [MeasurementPoint, MeasurementPoint];
1202
+ /** Constrain the reported value to one axis. Default `'free'`. */
1203
+ axis?: MeasurementAxis;
1204
+ /** Render dashed ΔX / ΔY / ΔZ guides. Default `false`. */
1205
+ showComponents?: boolean;
1206
+ }
1207
+ /** Angle at `points[1]` between the legs to `points[0]` and `points[2]`. */
1208
+ interface AngleMeasurement extends MeasurementBase {
1209
+ kind: 'angle';
1210
+ /** `[legA, vertex, legB]` — the middle point is the vertex. */
1211
+ points: [MeasurementPoint, MeasurementPoint, MeasurementPoint];
1212
+ }
1213
+ /** Polyline through N points; reports per-segment and total length. */
1214
+ interface PathMeasurement extends MeasurementBase {
1215
+ kind: 'path';
1216
+ points: MeasurementPoint[];
1217
+ /** Close the polyline back to the first point. Default `false`. */
1218
+ closed?: boolean;
1219
+ /** Label every segment, not just the total. Default `true`. */
1220
+ showSegmentLengths?: boolean;
1221
+ }
1222
+ /**
1223
+ * Distance ALONG the XTS track between two points on the same part — the
1224
+ * distance a mover actually travels, not the straight line.
1197
1225
  */
1198
- interface FeedSegment {
1199
- processingUnitObjectId: number;
1200
- partObjectId: number;
1201
- /** 0-based, in chain order, counted from the part's first infeed module. */
1202
- segmentIndex: number;
1226
+ interface TrackDistanceMeasurement extends MeasurementBase {
1227
+ kind: 'trackDistance';
1228
+ points: [MeasurementPoint, MeasurementPoint];
1229
+ }
1230
+ type Measurement = DistanceMeasurement | AngleMeasurement | PathMeasurement | TrackDistanceMeasurement;
1231
+ /** Same as `Measurement`, but the store generates the `id` when omitted. */
1232
+ type MeasurementInput = (Omit<DistanceMeasurement, 'id'> & {
1233
+ id?: MeasurementId;
1234
+ }) | (Omit<AngleMeasurement, 'id'> & {
1235
+ id?: MeasurementId;
1236
+ }) | (Omit<PathMeasurement, 'id'> & {
1237
+ id?: MeasurementId;
1238
+ }) | (Omit<TrackDistanceMeasurement, 'id'> & {
1239
+ id?: MeasurementId;
1240
+ });
1241
+ /** Computed value of a measurement. */
1242
+ interface MeasurementResult {
1243
+ id: MeasurementId;
1244
+ kind: MeasurementKind;
1245
+ /** Millimetres for lengths, degrees for angles. */
1246
+ value: number;
1247
+ unit: 'mm' | 'deg';
1248
+ /** Formatted value as rendered in the scene (e.g. `'1 234.5 mm'`). */
1249
+ text: string;
1250
+ /** Signed component deltas — `'distance'` only. */
1251
+ deltaMm?: Vec3;
1252
+ /** Per-segment lengths — `'path'` only. */
1253
+ segmentLengthsMm?: number[];
1203
1254
  /**
1204
- * `ModuleEntry.globalNumber` of the leading infeed module. `undefined` when
1205
- * the config does not set it — such a segment can only be addressed via
1206
- * `{ partObjectId, segmentIndex }`.
1255
+ * `true` when the measurement could not be evaluated (e.g. a
1256
+ * `trackDistance` whose endpoints sit on different parts). `value` is then
1257
+ * the straight-line fallback.
1207
1258
  */
1208
- infeedModuleGlobalNumber?: number;
1209
- /** Module type of the leading infeed module. */
1210
- infeedModuleType: ModuleType3D;
1211
- /** Chain indices of every module in this segment, in chain order. */
1212
- moduleIndices: number[];
1213
- /** Σ of the segment's module lengths, in mm. */
1214
- lengthMm: number;
1215
- /** True when the segment wraps the closed-loop seam. */
1216
- wrapsSeam: boolean;
1259
+ incomplete?: boolean;
1217
1260
  }
1218
- interface CameraState {
1261
+ /** Where an annotation hangs. Mover anchors follow the live mover position. */
1262
+ type AnnotationAnchor = {
1263
+ kind: 'world';
1219
1264
  positionMm: Vec3;
1220
- targetMm: Vec3;
1221
- /** Always 'Z' in the current spec. */
1222
- upAxis: 'Z' | 'Y';
1223
- zoom?: number;
1224
- }
1225
- /**
1226
- * Live camera projection.
1227
- * - `'perspective'` (default) — the regular 3D view.
1228
- * - `'orthographic'` — paired with `topDown`, gives the flat 2D plan view
1229
- * that matches `exportScreenshot({ mode: 'top-down' })`.
1230
- */
1231
- type CameraProjection = 'perspective' | 'orthographic';
1232
- /**
1233
- * Target for the imperative `viewerRef.current.focusOn(target, opts)` camera
1234
- * animation. The viewer computes the target's world-space bounding box and
1235
- * flies the camera so the whole object fits the frame:
1236
- * - In `'perspective'` projection the current view angle is preserved
1237
- * (the camera only dollies / re-centres).
1238
- * - In `'orthographic'` top-down the frustum is re-framed and the camera
1239
- * pans straight over the object.
1240
- */
1241
- type FocusTarget = {
1242
- kind: 'scene';
1243
- } | {
1244
- kind: 'station';
1245
- stationId: number;
1246
- } | {
1247
- kind: 'area';
1248
- areaId: number;
1249
1265
  } | {
1250
1266
  kind: 'mover';
1251
1267
  ref: MoverRef;
1252
1268
  } | {
1253
1269
  kind: 'module';
1254
1270
  ref: ModuleRef;
1255
- };
1256
- interface FocusOptions {
1257
- /** Animation duration in ms. Default 700. `0` jumps immediately. */
1258
- durationMs?: number;
1271
+ } | ({
1272
+ kind: 'trackPosition';
1273
+ } & TrackAnchor);
1274
+ interface Annotation {
1275
+ id: string;
1276
+ text: string;
1277
+ anchor: AnnotationAnchor;
1259
1278
  /**
1260
- * Padding around the target's bounding box (1 = tight fit). Default 1.2 in
1261
- * perspective, 1.1 in orthographic top-down.
1279
+ * Leader-line offset from the anchor to the label (mm, world frame).
1280
+ * Default `[0, 0, 250]`.
1262
1281
  */
1263
- paddingFactor?: number;
1264
- /** Easing curve. Default `'easeInOutCubic'`. */
1265
- easing?: 'linear' | 'easeInOutCubic';
1266
- }
1267
- type XtsViewerErrorCode = 'asset-load-failed' | 'unknown-module-type' | 'unknown-mover-type' | 'unknown-tool-type' | 'invalid-config' | 'unmatched-clothoid-half' | 'webgl-context-lost';
1268
- interface XtsViewerError {
1269
- code: XtsViewerErrorCode;
1270
- message: string;
1271
- details?: unknown;
1272
- }
1273
- declare class XtsViewerErrorException extends Error {
1274
- readonly code: XtsViewerErrorCode;
1275
- readonly details?: unknown;
1276
- constructor(code: XtsViewerErrorCode, message: string, details?: unknown);
1277
- }
1278
- interface AssetManifest {
1279
- /** Override per moduleType. Falls back to default mapping otherwise. */
1280
- modules?: Partial<Record<ModuleType3D, ModuleAssetEntry>>;
1281
- movers?: Partial<Record<MoverType3D, MoverAssetEntry>>;
1282
- tools?: Partial<Record<MoverToolType3D, MoverToolAssetEntry>>;
1283
- guidingRails?: Partial<Record<RailType3D, RailAssetEntry>>;
1282
+ offsetMm?: Vec3;
1283
+ color?: string;
1284
+ /** Text height (mm). Default `MeasurementStyle.labelSizeMm`. */
1285
+ sizeMm?: number;
1286
+ /** Marker drawn at the anchor. Default `'Sphere'`. */
1287
+ markerShape?: MarkerShape;
1288
+ markerSizeMm?: number;
1289
+ /** Default `true`. */
1290
+ visible?: boolean;
1291
+ meta?: Record<string, unknown>;
1284
1292
  }
1285
- interface RailAssetEntry {
1286
- glbUrl: string;
1287
- sidecarUrl?: string;
1293
+ type AnnotationInput = Omit<Annotation, 'id'> & {
1294
+ id?: string;
1295
+ };
1296
+ type MeasurementTool = 'none' | 'distance' | 'angle' | 'path' | 'trackDistance' | 'annotation';
1297
+ /** In-progress pick. `null` while no tool is armed. */
1298
+ interface MeasurementDraft {
1299
+ tool: Exclude<MeasurementTool, 'none'>;
1300
+ /** Points committed so far. */
1301
+ points: MeasurementPoint[];
1302
+ /** Point currently under the cursor, or `null` when off the scene. */
1303
+ hover: MeasurementPoint | null;
1288
1304
  }
1289
- interface ModuleAssetEntry {
1290
- /** Per-RailSystem GLB URL (relative to assetsBaseUrl). */
1291
- glbByRailSystem: {
1292
- Beckhoff: string | null;
1293
- HepcoGfx?: string | null;
1294
- };
1295
- sidecarUrl?: string;
1305
+ interface MeasurementStyle {
1306
+ /** Line + label colour of committed measurements. Default `'#00E5FF'`. */
1307
+ color?: string;
1308
+ /** Colour of the in-progress draft. Default `'#FFC400'`. */
1309
+ draftColor?: string;
1310
+ /** Annotation colour. Default `'#FFFFFF'`. */
1311
+ annotationColor?: string;
1312
+ /** Line width in pixels. Default `2`. */
1313
+ lineWidthPx?: number;
1314
+ /** Text height (mm). Default `40`. */
1315
+ labelSizeMm?: number;
1316
+ /** Endpoint marker diameter (mm). Default `18`. */
1317
+ pointSizeMm?: number;
1318
+ /** Decimal places on values. Default `1`. */
1319
+ decimals?: number;
1320
+ /** Unit suffix appended to lengths. Default `' mm'`. */
1321
+ unitSuffix?: string;
1322
+ /** Font URL passed to troika (see `DEFAULT_LABEL_FONT_URL`). */
1323
+ fontFamily?: string;
1324
+ /**
1325
+ * Draw measurement lines and markers through solid geometry
1326
+ * (`depthTest: false`). Default `true` — CAD viewers keep dimensions
1327
+ * readable regardless of what is in front of them.
1328
+ */
1329
+ alwaysOnTop?: boolean;
1296
1330
  }
1297
- interface MoverAssetEntry {
1298
- glbUrl: string;
1299
- sidecarUrl?: string;
1331
+ /** Serializable snapshot of everything the measurement feature holds. */
1332
+ interface MeasurementDocument {
1333
+ schemaVersion: 1;
1334
+ measurements: Measurement[];
1335
+ annotations: Annotation[];
1300
1336
  }
1301
- interface MoverToolAssetEntry {
1302
- glbUrl: string;
1303
- sidecarUrl?: string;
1337
+ /**
1338
+ * `measurement` prop of `<XtsViewer3D>`. Data can be driven either
1339
+ * declaratively (`measurements` / `annotations` — controlled arrays) or
1340
+ * imperatively via `viewerRef.current.measurements`. When the controlled
1341
+ * arrays are supplied they win on every render, so a host that uses them must
1342
+ * persist what the change callbacks report.
1343
+ */
1344
+ interface MeasurementOptions {
1345
+ /** Armed tool. Default `'none'` — no picking, no cursor change. */
1346
+ tool?: MeasurementTool;
1347
+ /** Controlled measurement list. Omit to let the viewer keep its own. */
1348
+ measurements?: MeasurementInput[];
1349
+ /** Controlled annotation list. Omit to let the viewer keep its own. */
1350
+ annotations?: AnnotationInput[];
1351
+ snap?: SnapOptions;
1352
+ style?: MeasurementStyle;
1353
+ /**
1354
+ * Z of the fallback pick plane (mm) used when the ray misses all geometry,
1355
+ * so free space can be measured. Default `0`; `null` disables the fallback.
1356
+ */
1357
+ groundPlaneZMm?: number | null;
1358
+ /** Fired after every change to the measurement list. */
1359
+ onMeasurementsChange?: (measurements: Measurement[], results: MeasurementResult[]) => void;
1360
+ /** Fired once when a tool completes a measurement. */
1361
+ onMeasurementCreate?: (measurement: Measurement, result: MeasurementResult) => void;
1362
+ onAnnotationsChange?: (annotations: Annotation[]) => void;
1363
+ onAnnotationCreate?: (annotation: Annotation) => void;
1364
+ /** Fired on every draft change (point added, hover moved, cancelled). */
1365
+ onDraftChange?: (draft: MeasurementDraft | null) => void;
1366
+ /**
1367
+ * Text for an annotation the `'annotation'` tool just placed. Return
1368
+ * `null` to abort. Default: `'Annotation'`.
1369
+ */
1370
+ annotationTextFactory?: (point: MeasurementPoint) => string | null;
1304
1371
  }
1372
+
1373
+ /**
1374
+ * `XtsModelDocument` — the canonical serialized form of a viewer scene.
1375
+ *
1376
+ * What `viewerRef.current.exportModel()` produces and what a host persists:
1377
+ * the configuration, optionally the runtime mover state, and optionally the
1378
+ * measurements. The measurement half keeps its own types next to its
1379
+ * implementation (`measurement/types.ts`); only the serialized document
1380
+ * surfaces here.
1381
+ */
1382
+
1305
1383
  interface XtsModelDocument {
1306
1384
  schemaVersion: 1;
1307
1385
  meta: {
@@ -2524,12 +2602,13 @@ declare class SidecarLoader {
2524
2602
  private readonly timeoutMs;
2525
2603
  private readonly cache;
2526
2604
  constructor(timeoutMs?: number);
2527
- fetchModule(url: string): Promise<ModuleSidecar | undefined>;
2528
- fetchMover(url: string): Promise<MoverSidecar | undefined>;
2529
- fetchTool(url: string): Promise<MoverToolSidecar | undefined>;
2530
- fetchRail(url: string): Promise<RailSidecar | undefined>;
2531
2605
  clear(): void;
2532
- private fetchJson;
2606
+ /**
2607
+ * Fetch and cache the sidecar at `url`. The loader validates the one field
2608
+ * every sidecar shape shares (`originCorrection`) and hands the rest to the
2609
+ * caller as `T` — the kind is a property of the URL, not of the loader.
2610
+ */
2611
+ fetch<T>(url: string): Promise<T | undefined>;
2533
2612
  }
2534
2613
  /**
2535
2614
  * Resolve the effective origin correction for a module sidecar — falls back
@@ -3615,6 +3694,54 @@ declare function getSharedAssetLoader(): AssetLoader;
3615
3694
  /** Compose a URL from a base + a filename, normalising trailing slashes. */
3616
3695
  declare function composeAssetUrl(baseUrl: string, filename: string): string;
3617
3696
 
3697
+ /**
3698
+ * The four sidecar kinds as data.
3699
+ *
3700
+ * Modules, movers, tools and rails all resolve their calibration the same
3701
+ * way — compiled-in table, else `<assetsBaseUrl>/<filename>`, else identity.
3702
+ * Only two things differ per kind: which built-in table to read and how the
3703
+ * filename is spelled. Keeping those two as a table (rather than four copies
3704
+ * of the resolution logic) means `useSidecar` and the calibration tool share
3705
+ * one implementation and a new kind is one entry, not one more branch.
3706
+ *
3707
+ * `filename` returning `null` marks an id that has no sidecar at all — the
3708
+ * `'Custom'` mover / tool, whose layout comes from the config instead. That
3709
+ * keeps "no lookup for Custom" a data fact rather than a special case each
3710
+ * consumer has to remember.
3711
+ */
3712
+
3713
+ /** Id and sidecar shape carried by each kind. */
3714
+ interface SidecarKinds {
3715
+ module: {
3716
+ id: ModuleType3D;
3717
+ sidecar: ModuleSidecar;
3718
+ };
3719
+ mover: {
3720
+ id: MoverType3D;
3721
+ sidecar: MoverSidecar;
3722
+ };
3723
+ tool: {
3724
+ id: MoverToolType3D;
3725
+ sidecar: MoverToolSidecar;
3726
+ };
3727
+ rail: {
3728
+ id: RailType3D;
3729
+ sidecar: RailSidecar;
3730
+ };
3731
+ }
3732
+ type SidecarKind = keyof SidecarKinds;
3733
+ type SidecarIdOf<K extends SidecarKind> = SidecarKinds[K]['id'];
3734
+ type SidecarOf<K extends SidecarKind> = SidecarKinds[K]['sidecar'];
3735
+ interface SidecarKindSpec<K extends SidecarKind> {
3736
+ /** Sidecar filename for `id`, or `null` when the id carries no sidecar. */
3737
+ filename(id: SidecarIdOf<K>): string | null;
3738
+ /** Compiled-in sidecar for `id`, if the bundle carries one. */
3739
+ builtin(id: SidecarIdOf<K>): SidecarOf<K> | undefined;
3740
+ }
3741
+ declare const SIDECAR_KINDS: {
3742
+ [K in SidecarKind]: SidecarKindSpec<K>;
3743
+ };
3744
+
3618
3745
  /**
3619
3746
  * Style resolution for the measurement overlay — one place where every
3620
3747
  * default lives, so the scene, the draft preview and the value formatting
@@ -3716,4 +3843,4 @@ declare function snapToGrid(positionMm: Vec3, gridMm: number): Vec3;
3716
3843
  /** Convenience: `resolveSceneContext` + `toEntityRef`. */
3717
3844
  declare function resolveEntityFromObject(object: Object3D | null, instanceId?: number): MeasuredEntityRef | null;
3718
3845
 
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 };
3846
+ 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, SIDECAR_KINDS, type ScreenshotFormat, type ScreenshotMode, type ScreenshotOptions, type ScreenshotResult, type SelectionMode, type SelectionState, type SidecarIdOf, type SidecarKind, type SidecarKindSpec, type SidecarKinds, SidecarLoader, type SidecarOf, 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 };