beckhoff-xts-viewer-3d 5.2.2 → 5.3.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.ts 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.2";
15
+ declare const VERSION: "5.3.0";
16
16
 
17
17
  /**
18
18
  * Default CDN URL for the GLB asset bundle (`beckhoff-xts-viewer-3d-assets`).
@@ -32,7 +32,7 @@ declare const VERSION: "5.2.2";
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@2.0.1/models";
35
+ declare const JSDELIVR_ASSETS_BASE_URL: "https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d-assets@2.1.0/models";
36
36
 
37
37
  /**
38
38
  * Catalogue taxonomy — the string unions naming every module, mover, tool and
@@ -44,7 +44,7 @@ declare const JSDELIVR_ASSETS_BASE_URL: "https://cdn.jsdelivr.net/npm/beckhoff-x
44
44
  * public type surface. Nothing here imports anything.
45
45
  */
46
46
  /** Beckhoff XTS module types supported by the 3D viewer. */
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';
47
+ type ModuleType3D = 'AT2000_0250' | 'AT2001_0250' | 'AT2002_0250' | 'AT2002_0249' | 'AT2000_0233' | 'AT2000_0249' | 'AT2100_0250' | 'AT2102_0250' | 'AT2020_0250' | 'AT2021_0250' | 'AT2025_0250' | 'AT2026_0250' | 'AT2040_0250' | 'AT2041_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';
48
48
  /**
49
49
  * Rail system beneath the module. Only affects the visual mesh
50
50
  * (Beckhoff aluminium rail vs. Hepco GFX), not the path math.
@@ -65,7 +65,20 @@ type RailSystem = 'Beckhoff' | 'HepcoGfx';
65
65
  * The `_S25` carrier ships without a magnet plate; the `_C25/_C34/_M34/_M40`
66
66
  * variants include the Beckhoff magnet plate set (`7P`/`10P` = 7/10-pole).
67
67
  */
68
- type MoverType3D = 'AT9011_0050' | 'AT9011_0070' | 'AT9012_0050' | 'AT9014_0055' | 'AT9014_0070' | 'ATH9011_0075' | 'ATH9013_0075' | 'Hepco_GFX2_1TC_S25' | 'Hepco_GFX2_1TC_C25' | 'Hepco_GFX2_1TC_C34' | 'Hepco_GFX2_1TC_M34' | 'Hepco_GFX2_1TC_M34_7P' | 'Hepco_GFX2_FCC_C25' | 'Hepco_GFX2_FCC_M34' | 'Hepco_GFX2_FCC_M34_7P' | 'Hepco_GFX2_FCC_M34_10P' | 'Hepco_GFX2_FCC_M40_10P' | 'Custom';
68
+ type MoverType3D = 'AT9014_0055' | 'AT9014_0070' | 'ATH9011_0075' | 'Hepco_GFX2_1TC_S25' | 'Hepco_GFX2_1TC_C25' | 'Hepco_GFX2_1TC_C34' | 'Hepco_GFX2_1TC_M34' | 'Hepco_GFX2_1TC_M34_7P' | 'Hepco_GFX2_FCC_C25' | 'Hepco_GFX2_FCC_M34' | 'Hepco_GFX2_FCC_M34_7P' | 'Hepco_GFX2_FCC_M34_10P' | 'Hepco_GFX2_FCC_M40_10P' | 'Custom';
69
+ /**
70
+ * Beckhoff magnet plate sets — the part carrying the mover's magnets and,
71
+ * on the AT9001 family, the encoder flag ("Geberfahne").
72
+ *
73
+ * The digit after the dash is the pole count in base 16 (`0450` → 4,
74
+ * `0550` → 5, `0775` → 7, `0AA0` → 10), verified by counting magnets in each
75
+ * converted mesh rather than read off the part number. `ATH9001` is the
76
+ * hygienic family, whose rows are encapsulated in sealed stainless.
77
+ *
78
+ * Every set is symmetric about its own travel axis, so its GLB origin already
79
+ * sits on the magnet-plate centre that the mover frame is defined around.
80
+ */
81
+ type MagnetPlateType3D = 'AT9001_0450' | 'AT9001_0550' | 'AT9001_0775' | 'AT9001_0AA0' | 'ATH9001_0550' | 'ATH9001_0550_0001' | 'ATH9001_0AA0';
69
82
  /** Predefined mover-tool / carrier-plate types. */
70
83
  type MoverToolType3D = 'AT8200_1000_0100' | 'AT8200_2000_0100' | 'Custom';
71
84
  /**
@@ -73,17 +86,23 @@ type MoverToolType3D = 'AT8200_1000_0100' | 'AT8200_2000_0100' | 'Custom';
73
86
  * when `RailSystem === 'Beckhoff'` to visualise the outer guide
74
87
  * profile that the mover wheels engage.
75
88
  *
76
- * Curve sign drives the selection (see `MODULE_GUIDING_RAIL_MAP`):
89
+ * Curve sign drives the selection, and on curves so does the mover — see
90
+ * `resolveGuidingRailType`:
77
91
  * - Straights: AT9000_0249 / AT9000_0250 / AT9000_0500
78
92
  * - +22.5° curve: AT9020_1250
79
93
  * - −22.5° curve: AT9025_1466
80
94
  * - 45° curve: AT9040_0750
81
95
  * - 180° clothoid (entire AT2050 500 mm path): AT9050_0500
82
96
  *
97
+ * Beckhoff builds the curve rails per mover: the unsuffixed types above are
98
+ * the `-0055` order variants, made for the AT9014-0055, and the `_0170` ones
99
+ * are the `-0170` variants for the wider AT9014-0070. The straights carry no
100
+ * such variant — one AT9000 profile serves every mover.
101
+ *
83
102
  * Hygienic ATH modules ship with their guide profile baked into the module
84
103
  * GLB and therefore do NOT receive a separate guiding-rail mesh.
85
104
  */
86
- type RailType3D = 'AT9000_0249' | 'AT9000_0250' | 'AT9000_0500' | 'AT9020_1250' | 'AT9025_1466' | 'AT9040_0750' | 'AT9050_0500';
105
+ type RailType3D = 'AT9000_0249' | 'AT9000_0250' | 'AT9000_0500' | 'AT9020_1250' | 'AT9020_1250_0170' | 'AT9025_1466' | 'AT9025_1466_0170' | 'AT9040_0750' | 'AT9050_0500' | 'AT9050_0500_0170';
87
106
 
88
107
  /**
89
108
  * Scalar tuples shared by every other type module.
@@ -330,8 +349,13 @@ interface MoverIdLabelOptions {
330
349
  */
331
350
  offsetMm?: Vec3;
332
351
  /**
333
- * URL of a TTF / OTF / WOFF font, passed straight to troika-three-text.
334
- * Default: `DEFAULT_LABEL_FONT_URL` (Roboto Condensed via jsDelivr).
352
+ * URL of a TTF / OTF / WOFF font for the label.
353
+ *
354
+ * Default: the viewer's built-in Roboto Condensed, compiled into the
355
+ * bundle and served from a blob URL — no request, and nothing that can
356
+ * fail. A custom URL is fetched once and shared by every label; until
357
+ * it answers, and permanently if it never does, labels render in the
358
+ * built-in font and the failure is reported once via `console.warn`.
335
359
  */
336
360
  fontUrl?: string;
337
361
  /** Outline thickness in mm. 0 disables the outline. Default: 0. */
@@ -675,6 +699,20 @@ interface ProcessingUnitConfig {
675
699
  */
676
700
  objectId: number;
677
701
  moverType: MoverType3D;
702
+ /**
703
+ * Magnet plate set attached to the carriage.
704
+ *
705
+ * Only meaningful for a mover whose GLB ships bare — today just
706
+ * `Hepco_GFX2_1TC_S25`. Every other mover models its plate already
707
+ * (see `MOVER_INTEGRATED_PLATE`) and ignores this, so setting it there
708
+ * cannot double-plate the mover. Defaults to `AT9001_0550`, the 5-pole set
709
+ * the equivalent `_C25` carriage ships with.
710
+ *
711
+ * Deliberately per processing unit rather than per mover: `moverType` is
712
+ * too, and a plate that varied between movers would force every one of them
713
+ * to claim its own scene node, disabling the instanced render path.
714
+ */
715
+ magnetPlateType?: MagnetPlateType3D;
678
716
  customMoverLayout?: CustomMoverLayout;
679
717
  /** Default: 'Beckhoff'. */
680
718
  railSystem?: RailSystem;
@@ -1090,6 +1128,7 @@ interface AssetManifest {
1090
1128
  modules?: Partial<Record<ModuleType3D, ModuleAssetEntry>>;
1091
1129
  movers?: Partial<Record<MoverType3D, MoverAssetEntry>>;
1092
1130
  tools?: Partial<Record<MoverToolType3D, MoverToolAssetEntry>>;
1131
+ magnetPlates?: Partial<Record<MagnetPlateType3D, MagnetPlateAssetEntry>>;
1093
1132
  guidingRails?: Partial<Record<RailType3D, RailAssetEntry>>;
1094
1133
  }
1095
1134
  interface RailAssetEntry {
@@ -1112,6 +1151,10 @@ interface MoverToolAssetEntry {
1112
1151
  glbUrl: string;
1113
1152
  sidecarUrl?: string;
1114
1153
  }
1154
+ interface MagnetPlateAssetEntry {
1155
+ glbUrl: string;
1156
+ sidecarUrl?: string;
1157
+ }
1115
1158
 
1116
1159
  /**
1117
1160
  * Public types for the measurement + annotation feature.
@@ -1420,6 +1463,243 @@ interface XtsRuntimeState {
1420
1463
  display?: Partial<DisplayOptions>;
1421
1464
  }
1422
1465
 
1466
+ /**
1467
+ * SidecarLoader — fetches per-asset JSON sidecars.
1468
+ *
1469
+ * Sidecars carry origin-correction (translateMm + rotationDegEuler) plus
1470
+ * path metadata. They live next to the GLB at <baseUrl>/<id>.meta.json (or
1471
+ * <id>.tool.meta.json for mover tools).
1472
+ *
1473
+ * On 404 the loader falls back to default origin-correction `(0,0,0)` /
1474
+ * `(0,0,0)`. The renderer can still place the GLB; the calibration tool is
1475
+ * the authoritative source for sidecar values.
1476
+ */
1477
+
1478
+ interface OriginCorrection {
1479
+ translateMm: Vec3;
1480
+ rotationDegEuler: Vec3;
1481
+ }
1482
+ interface ModuleSidecar {
1483
+ moduleType: ModuleType3D;
1484
+ glbByRailSystem: {
1485
+ Beckhoff: string | null;
1486
+ HepcoGfx?: string | null;
1487
+ };
1488
+ originCorrection: OriginCorrection;
1489
+ pathType: 'Straight' | 'Curve' | 'Free';
1490
+ moduleLengthMm: number;
1491
+ endAngleDeg: number;
1492
+ freePathFile?: string;
1493
+ approximateBoundsMm?: {
1494
+ min: Vec3;
1495
+ max: Vec3;
1496
+ };
1497
+ }
1498
+ interface MoverSidecar {
1499
+ moverType: MoverType3D;
1500
+ glb: string;
1501
+ originCorrection: OriginCorrection;
1502
+ magnetPlateCenterMm: Vec3;
1503
+ pathLengthMm: number;
1504
+ }
1505
+ /**
1506
+ * Calibration sidecar for a standalone magnet plate set. The plate is drawn
1507
+ * in the mover-local frame (+X travel, +Z up) with this correction applied,
1508
+ * so `originCorrection` is what seats it on the carriage.
1509
+ */
1510
+ interface MagnetPlateSidecar {
1511
+ plateType: MagnetPlateType3D;
1512
+ glb: string;
1513
+ originCorrection: OriginCorrection;
1514
+ poleCount: number;
1515
+ lengthMm: number;
1516
+ }
1517
+ interface MoverToolSidecar {
1518
+ toolType: MoverToolType3D;
1519
+ glbUrl: string;
1520
+ originCorrection: OriginCorrection;
1521
+ defaultOffsetMm: Vec3;
1522
+ approximateBoundsMm?: {
1523
+ min: Vec3;
1524
+ max: Vec3;
1525
+ };
1526
+ label: string;
1527
+ }
1528
+ /**
1529
+ * Calibration sidecar for a guiding-rail GLB. The rail is rendered in the
1530
+ * same module-local frame as the module (i.e. under the module's
1531
+ * `startWorldPose`) with this correction applied to the GLB scene.
1532
+ */
1533
+ interface RailSidecar {
1534
+ railType: RailType3D;
1535
+ glb: string;
1536
+ originCorrection: OriginCorrection;
1537
+ approximateBoundsMm?: {
1538
+ min: Vec3;
1539
+ max: Vec3;
1540
+ };
1541
+ }
1542
+ declare const DEFAULT_ORIGIN_CORRECTION: OriginCorrection;
1543
+ /**
1544
+ * Generic sidecar fetch with promise caching + graceful fallback. Returns
1545
+ * `undefined` on 404 (caller substitutes a default), and surfaces network or
1546
+ * parse errors as `Error` (caller is expected to forward to onError).
1547
+ */
1548
+ declare class SidecarLoader {
1549
+ private readonly timeoutMs;
1550
+ private readonly cache;
1551
+ constructor(timeoutMs?: number);
1552
+ clear(): void;
1553
+ /**
1554
+ * Fetch and cache the sidecar at `url`. The loader validates the one field
1555
+ * every sidecar shape shares (`originCorrection`) and hands the rest to the
1556
+ * caller as `T` — the kind is a property of the URL, not of the loader.
1557
+ */
1558
+ fetch<T>(url: string): Promise<T | undefined>;
1559
+ }
1560
+ /**
1561
+ * Resolve the effective origin correction for a module sidecar — falls back
1562
+ * to identity when the sidecar is absent.
1563
+ */
1564
+ declare function resolveOriginCorrection(sidecar: {
1565
+ originCorrection?: OriginCorrection;
1566
+ } | undefined): OriginCorrection;
1567
+ /** Standard sidecar filename from a base id. */
1568
+ declare function moduleSidecarFilename(moduleType: ModuleType3D): string;
1569
+ declare function moverSidecarFilename(moverType: MoverType3D): string;
1570
+ declare function toolSidecarFilename(toolType: MoverToolType3D): string;
1571
+ declare function railSidecarFilename(railType: RailType3D): string;
1572
+ /** Anything that names a GLB per rail system — a sidecar or a manifest entry. */
1573
+ interface GlbByRailSystem {
1574
+ glbByRailSystem: {
1575
+ Beckhoff: string | null;
1576
+ HepcoGfx?: string | null;
1577
+ };
1578
+ }
1579
+ /**
1580
+ * Pure helper: pick the GLB filename with rail-system fallback. Returns null
1581
+ * when the entry marks the module 'prepared but not delivered'.
1582
+ *
1583
+ * Takes the structural shape rather than `ModuleSidecar` so
1584
+ * `resolveModuleGlbUrl`, which used to carry a line-for-line identical copy
1585
+ * of this body over manifest entries, can delegate here. Both are public
1586
+ * API, so both stay — but there is one implementation of the fallback rule.
1587
+ */
1588
+ declare function pickGlbForRail(entry: GlbByRailSystem | undefined, rail: RailSystem): string | null;
1589
+
1590
+ /**
1591
+ * Option shapes shared between the public `<XtsViewer3D>` props and the
1592
+ * internal `<XtsScene>` / `<ProcessingUnitRoot>` props.
1593
+ *
1594
+ * Each of these used to be declared twice — once on each side of that
1595
+ * boundary, character for character — so the two could drift silently, and
1596
+ * `SidecarOverrides` was even exported under two public names. Declared once
1597
+ * here and imported by both.
1598
+ */
1599
+
1600
+ /** Where the orientation gizmo sits in the canvas. */
1601
+ type ViewCubeAlignment = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center' | 'center-left' | 'center-right' | 'center-center';
1602
+ /**
1603
+ * The corner navigation gizmo: three labelled X / Y / Z axis handles that
1604
+ * track the camera and snap it onto an axis when clicked.
1605
+ *
1606
+ * Named `ViewCube*` for backwards compatibility — earlier versions drew a
1607
+ * navigation cube in the same spot.
1608
+ */
1609
+ interface ViewCubeOptions {
1610
+ enabled?: boolean;
1611
+ /** Default `'bottom-right'`. */
1612
+ alignment?: ViewCubeAlignment;
1613
+ /** Distance from the aligned corner, in CSS px. Default `[80, 80]`. */
1614
+ marginPx?: [number, number];
1615
+ }
1616
+ /**
1617
+ * Keyboard navigation — moving the camera *through* the scene rather than
1618
+ * orbiting it around a fixed point.
1619
+ *
1620
+ * The arrow keys (and the WASD block) walk the camera and the orbit pivot
1621
+ * together along the ground plane, PageUp / PageDown climb, Shift goes
1622
+ * faster and Alt finer. Keys are read from the canvas once it has focus,
1623
+ * which it takes on pointer-down, so an embedded viewer never swallows the
1624
+ * host page's arrow keys.
1625
+ *
1626
+ * Off automatically while `lock.pan` is set — moving through the scene is
1627
+ * panning by another input device.
1628
+ */
1629
+ interface KeyboardNavigationOptions {
1630
+ /** Default `true`. */
1631
+ enabled?: boolean;
1632
+ /**
1633
+ * Multiplier on the default travel rate, which is ~0.6 × the current
1634
+ * orbit distance per second. Default `1`.
1635
+ */
1636
+ speed?: number;
1637
+ }
1638
+ /** Interaction locks. Each `true` disables that interaction. */
1639
+ interface InteractionLocks {
1640
+ rotate?: boolean;
1641
+ pan?: boolean;
1642
+ zoom?: boolean;
1643
+ selection?: boolean;
1644
+ }
1645
+ interface PerformanceOptions {
1646
+ /** Cap the frameloop rate. Unset = uncapped. */
1647
+ maxFps?: number;
1648
+ /**
1649
+ * Draw a processing unit's movers as one `InstancedMesh` instead of one
1650
+ * scene node each, cutting mover draw calls from N to 1. Default: false.
1651
+ *
1652
+ * Applies from four movers up, and only to movers with nothing bound to
1653
+ * them: a mover carrying tools, a mover-bound custom asset, or a visible
1654
+ * drive status still gets its own node. Turning on the anchor marker or
1655
+ * mover-id labels gives every mover a child object, so the batch is
1656
+ * skipped for that unit entirely. Selection and clicks work either way.
1657
+ */
1658
+ instancing?: boolean;
1659
+ /** Stop the frameloop while the tab is hidden. Default: true. */
1660
+ autoPauseOnHidden?: boolean;
1661
+ /**
1662
+ * Enable demand rendering: frames only render when something changes
1663
+ * (camera move, mover position update, selection). Saves >90% GPU time
1664
+ * when the scene is idle. Default: true.
1665
+ */
1666
+ demandRendering?: boolean;
1667
+ /**
1668
+ * Cap the device pixel ratio. Values above 2 rarely improve perceptible
1669
+ * quality but double fill-rate cost on Retina/4K displays. Default: 2.
1670
+ */
1671
+ maxDpr?: number;
1672
+ /**
1673
+ * Disable automatic per-frame shadow map updates. Shadows only re-render
1674
+ * on camera change. Default: true (when shadows enabled).
1675
+ */
1676
+ shadowOnDemand?: boolean;
1677
+ /**
1678
+ * Opt into the WebGPU renderer when the browser supports it. Falls back to
1679
+ * WebGL automatically. Default: false (experimental).
1680
+ */
1681
+ webgpu?: boolean;
1682
+ /**
1683
+ * Override the KTX2 (Basis) transcoder path used to decode compressed
1684
+ * textures in the GLB assets. Defaults to a CDN copy pinned to the bundled
1685
+ * three.js revision. Point this at a self-hosted
1686
+ * `basis_transcoder.{js,wasm}` directory for offline / air-gapped use.
1687
+ */
1688
+ ktx2TranscoderUrl?: string;
1689
+ }
1690
+ /**
1691
+ * Live override of the per-asset origin-correction sidecars, keyed by asset
1692
+ * type. Useful for calibration tooling that adjusts a model's origin without
1693
+ * re-publishing its `.meta.json`.
1694
+ */
1695
+ interface SidecarOverrides {
1696
+ modules?: Partial<Record<ModuleType3D, OriginCorrection>>;
1697
+ movers?: Partial<Record<MoverType3D, OriginCorrection>>;
1698
+ tools?: Partial<Record<MoverToolType3D, OriginCorrection>>;
1699
+ magnetPlates?: Partial<Record<MagnetPlateType3D, OriginCorrection>>;
1700
+ rails?: Partial<Record<RailType3D, OriginCorrection>>;
1701
+ }
1702
+
1423
1703
  /**
1424
1704
  * Module catalog (3D-canonical).
1425
1705
  *
@@ -1556,6 +1836,67 @@ declare function moverWorldAt(chain: BuiltChain, partPositionMm: number): Planar
1556
1836
  /** Convenience: chain from full ModuleEntry[] returning trackLength alone. */
1557
1837
  declare function trackLengthOf(modules: ReadonlyArray<ModuleEntry>): number;
1558
1838
 
1839
+ /**
1840
+ * normalizeXtsConfig — pre-render pass.
1841
+ *
1842
+ * Fail-fast validation + AT2050 half-clothoid merging:
1843
+ * AT2050_0500 + AT2050_0501 (gap=0) → AT2050_0500_180
1844
+ * ATH2050_0500 + ATH2050_0501 (gap=0) → ATH2050_0500_180
1845
+ *
1846
+ * Errors thrown via XtsViewerErrorException:
1847
+ * - 'unknown-module-type'
1848
+ * - 'unknown-mover-type'
1849
+ * - 'unmatched-clothoid-half' (0500 without 0501 or vice versa)
1850
+ *
1851
+ * Warnings (collected, non-fatal) returned alongside the normalized config:
1852
+ * - mover.partOid not in part-list
1853
+ * - duplicate mover.id within an XPU
1854
+ * - mover.partPositionMm out of range (clamped)
1855
+ */
1856
+
1857
+ interface NormalizedXtsConfig extends XtsConfig {
1858
+ /**
1859
+ * Same shape as the input, with all parts' modules pre-merged + validated.
1860
+ * Mover positions are clamped, but the original values are preserved in
1861
+ * `__warnings` for diagnostics.
1862
+ */
1863
+ readonly __warnings: NormalizationWarning[];
1864
+ }
1865
+ interface NormalizationWarning {
1866
+ code: 'mover-out-of-range' | 'mover-orphan' | 'mover-duplicate-id' | 'mover-missing-id' | 'mover-invalid-id' | 'empty-part' | 'prepared-module' | 'duplicate-xpu-object-id' | 'missing-xpu-object-id' | 'invalid-xpu-object-id' | 'duplicate-part-object-id' | 'missing-part-object-id' | 'invalid-part-object-id' | 'duplicate-part-global-number' | 'missing-part-global-number' | 'invalid-part-global-number' | 'duplicate-module-global-number' | 'missing-module-global-number' | 'invalid-module-global-number';
1867
+ message: string;
1868
+ details?: unknown;
1869
+ }
1870
+ /**
1871
+ * Normalizer handle.
1872
+ *
1873
+ * This used to carry a single-slot structural cache keyed on a
1874
+ * `JSON.stringify` of the config with the frame-paced fields stripped out.
1875
+ * It was removed: the only production caller is `useNormalizedConfig`, whose
1876
+ * `useMemo` already collapses repeat calls on an unchanged config, so the
1877
+ * cache could fire only when the config identity changed but its structure
1878
+ * did not — and in exactly that case the "fast path" cost a full stringify
1879
+ * of the config plus a full rebuild of every XPU, which is strictly more
1880
+ * work than the single O(n) pass it was avoiding.
1881
+ *
1882
+ * It also cost correctness twice over. The key stripped `status` by name at
1883
+ * every depth, so it removed `ModuleEntry.status` along with the mover field
1884
+ * it was aimed at, while the clone path rebuilt only movers — freezing
1885
+ * declarative module warnings after the first normalize. And it forced a
1886
+ * second copy of the mover clamp that had already drifted from `clampMover`
1887
+ * (different `details` key, different `original` value), so hosts parsing
1888
+ * warning details saw two shapes for one warning code.
1889
+ *
1890
+ * The class stays because it is part of the published API. It is now a thin
1891
+ * handle over the pure function.
1892
+ */
1893
+ declare class XtsConfigNormalizer {
1894
+ normalize(config: XtsConfig): NormalizedXtsConfig;
1895
+ /** No-op. Kept so existing callers keep compiling; there is no cache. */
1896
+ clearCache(): void;
1897
+ }
1898
+ declare function normalizeXtsConfig(config: XtsConfig): NormalizedXtsConfig;
1899
+
1559
1900
  /**
1560
1901
  * Mover collision detection.
1561
1902
  *
@@ -1665,6 +2006,20 @@ interface MoverProbe {
1665
2006
  * appear first.
1666
2007
  */
1667
2008
  declare function checkSamePathCollisions(chain: BuiltChain, partOid: number, probes: ReadonlyArray<MoverProbe>, opts?: CheckMoverCollisionsOptions): MoverCollision[];
2009
+ /**
2010
+ * Collision check across a whole config: build a probe per mover, group by
2011
+ * part, and run `checkSamePathCollisions` on each group with two or more.
2012
+ *
2013
+ * `livePositionOf` supplies the current position of a mover when one is
2014
+ * being driven imperatively — the `MoverPositionStore` at 60 Hz — falling
2015
+ * back to the config value. Passing it as a function rather than the store
2016
+ * itself is what keeps this a pure geometry function: it was previously
2017
+ * trapped inside the `useImperativeHandle` closure of `useXtsViewerHandle`,
2018
+ * where it could only be exercised through a mounted canvas.
2019
+ *
2020
+ * Results are sorted so the deepest collision in the scene comes first.
2021
+ */
2022
+ declare function checkAllMoverCollisions(config: NormalizedXtsConfig, livePositionOf: (moverId: number) => number | undefined, opts?: CheckMoverCollisionsOptions): MoverCollision[];
1668
2023
 
1669
2024
  /**
1670
2025
  * Module collision / overlap detection.
@@ -1700,12 +2055,17 @@ declare function checkSamePathCollisions(chain: BuiltChain, partOid: number, pro
1700
2055
  * are excluded; everything else (cross-part, cross-XPU, same-part
1701
2056
  * non-adjacent) is tested.
1702
2057
  *
1703
- * The box cross-section (`halfWidthMm` 50, `heightMm` 100) is a coarse but
1704
- * deterministic, asset-independent envelope. Real per-module CAD bounds live
1705
- * in the GLB sidecars (`approximateBoundsMm`) and could refine it later.
1706
- * Likewise the `warningGapMm` near-miss is an axis-projected SAT separation,
1707
- * not a true Euclidean (GJK) distance — slightly conservative for
1708
- * corner-to-corner cases.
2058
+ * The box height comes from the module's own GLB sidecar
2059
+ * (`moduleHeightSpan`, moduleCrossSection.ts): a motor module measures about
2060
+ * 40 mm and straddles the path plane. It used to be one coarse box per type,
2061
+ * `0 … 100` mm and entirely above the plane, which reported an overlap between
2062
+ * any two tracks stacked closer than 100 mm — the arrangement a doublesided
2063
+ * guide rail is built as. The half-width stays the coarse `halfWidthMm` 50,
2064
+ * because a curved module's sidecar box spans its whole arc in X and Y and is
2065
+ * a cross-section only in Z; moduleCrossSection.ts sets that out.
2066
+ *
2067
+ * The `warningGapMm` near-miss is an axis-projected SAT separation, not a true
2068
+ * Euclidean (GJK) distance — slightly conservative for corner-to-corner cases.
1709
2069
  */
1710
2070
 
1711
2071
  /** Default box half-width (Y) — matches `approximateModuleBounds`. */
@@ -1752,7 +2112,16 @@ interface CheckModuleCollisionsOptions {
1752
2112
  includeSamePart?: boolean;
1753
2113
  /** Box half-width in Y (mm). Default 50. */
1754
2114
  halfWidthMm?: number;
1755
- /** Box height in Z (mm). Default 100. */
2115
+ /**
2116
+ * Force one box height in Z (mm), spanning `0 … heightMm` above the path
2117
+ * plane for every module type.
2118
+ *
2119
+ * Left unset — the default — each module is given the vertical extent its
2120
+ * GLB sidecar reports (`moduleHeightSpan`), which both is the real height
2121
+ * and sits on the real side of the path plane. Setting this restores the one
2122
+ * coarse box the check used before that, for a caller who wants a
2123
+ * deliberately conservative envelope.
2124
+ */
1756
2125
  heightMm?: number;
1757
2126
  }
1758
2127
  /** Oriented bounding box in world space (unit axes + scaled half-extents). */
@@ -1819,58 +2188,25 @@ declare function checkModuleCollisions(probes: ReadonlyArray<ModuleProbe>, adjac
1819
2188
  count: number;
1820
2189
  closed: boolean;
1821
2190
  }>, opts?: CheckModuleCollisionsOptions): ModuleCollision[];
1822
-
1823
2191
  /**
1824
- * normalizeXtsConfig — pre-render pass.
1825
- *
1826
- * Fail-fast validation + AT2050 half-clothoid merging:
1827
- * AT2050_0500 + AT2050_0501 (gap=0) → AT2050_0500_180
1828
- * ATH2050_0500 + ATH2050_0501 (gap=0) → ATH2050_0500_180
1829
- *
1830
- * Errors thrown via XtsViewerErrorException:
1831
- * - 'unknown-module-type'
1832
- * - 'unknown-mover-type'
1833
- * - 'unmatched-clothoid-half' (0500 without 0501 or vice versa)
2192
+ * Resolve a normalized config into the per-XPU inputs `buildModuleProbes`
2193
+ * takes, applying the same override precedence the scene uses: an imperative
2194
+ * `trackTransformOverrides` entry beats the config value.
1834
2195
  *
1835
- * Warnings (collected, non-fatal) returned alongside the normalized config:
1836
- * - mover.partOid not in part-list
1837
- * - duplicate mover.id within an XPU
1838
- * - mover.partPositionMm out of range (clamped)
1839
- */
1840
-
1841
- interface NormalizedXtsConfig extends XtsConfig {
1842
- /**
1843
- * Same shape as the input, with all parts' modules pre-merged + validated.
1844
- * Mover positions are clamped, but the original values are preserved in
1845
- * `__warnings` for diagnostics.
1846
- */
1847
- readonly __warnings: NormalizationWarning[];
1848
- }
1849
- interface NormalizationWarning {
1850
- code: 'mover-out-of-range' | 'mover-orphan' | 'mover-duplicate-id' | 'mover-missing-id' | 'mover-invalid-id' | 'empty-part' | 'prepared-module' | 'duplicate-xpu-object-id' | 'missing-xpu-object-id' | 'invalid-xpu-object-id' | 'duplicate-part-object-id' | 'missing-part-object-id' | 'invalid-part-object-id' | 'duplicate-module-global-number' | 'missing-module-global-number' | 'invalid-module-global-number';
1851
- message: string;
1852
- details?: unknown;
1853
- }
1854
- /**
1855
- * Stateful normalizer with a single-slot structural cache. The key
1856
- * intentionally excludes `mover.partPositionMm` so that frame-paced
1857
- * position updates fall into the fast path: when the structure (modules,
1858
- * parts, transforms, mover ids/partOids) hasn't changed between calls,
1859
- * we return a clone of the cached result with only the mover positions
1860
- * re-clamped against the current input.
2196
+ * This adapter was written out twice, character for character — once in
2197
+ * `<ModuleCollisionMonitor>` and once inside the `checkModuleCollisions`
2198
+ * closure of `useXtsViewerHandle` — with the only difference being where the
2199
+ * orientation came from.
1861
2200
  *
1862
- * Construct your own instance when you need an isolated cache (e.g. in
1863
- * tests, or when multiple viewers in the same JS realm would otherwise
1864
- * thrash a shared single-slot cache). The standalone `normalizeXtsConfig`
1865
- * function delegates to a process-wide singleton for backwards
1866
- * compatibility.
2201
+ * A part whose chain cannot be built is skipped, as before, but no longer in
2202
+ * silence: `normalizeXtsConfig` has already made that impossible, so if it
2203
+ * ever happens it means a part has silently dropped out of collision
2204
+ * detection, which is a safety feature. Reported rather than thrown, because
2205
+ * both callers run inside React and a throw would take the scene down.
1867
2206
  */
1868
- declare class XtsConfigNormalizer {
1869
- private _cache;
1870
- normalize(config: XtsConfig): NormalizedXtsConfig;
1871
- clearCache(): void;
1872
- }
1873
- declare function normalizeXtsConfig(config: XtsConfig): NormalizedXtsConfig;
2207
+ declare function toModuleCollisionInputs(config: NormalizedXtsConfig, trackTransformOverrides?: Record<number, TrackTransform>): ModuleCollisionXpuInput[];
2208
+ /** `toModuleCollisionInputs` → `buildModuleProbes` → `checkModuleCollisions`. */
2209
+ declare function checkAllModuleCollisions(config: NormalizedXtsConfig, orientation: Orientation | undefined, trackTransformOverrides?: Record<number, TrackTransform>, opts?: CheckModuleCollisionsOptions): ModuleCollision[];
1874
2210
 
1875
2211
  /**
1876
2212
  * Shared, mostly-pure plumbing behind `exportScreenshot` and
@@ -1891,16 +2227,99 @@ declare function normalizeXtsConfig(config: XtsConfig): NormalizedXtsConfig;
1891
2227
  */
1892
2228
 
1893
2229
  type CaptureMode = 'current' | 'top-down' | 'custom';
1894
-
1895
2230
  /**
1896
- * captureScreenshot — render the live r3f scene into an offscreen
1897
- * WebGLRenderTarget at any resolution, in any of three camera modes:
2231
+ * The options `exportScreenshot` and `beginFrameCapture` share.
1898
2232
  *
1899
- * • 'current' — clone the live camera at its current pose (image aspect
1900
- * adjusted to the requested width/height). Handles both
1901
- * the perspective view and the orthographic 2D plan view.
1902
- * • 'top-down' — orthographic camera placed above the scene's AABB
1903
- * centre, looking straight down -Z, with up = world +Y.
2233
+ * They used to be written out twice — `ScreenshotOptions` and
2234
+ * `FrameCaptureOptions` each hand-documented the same nine fields, and the
2235
+ * two `updateShadows` comments already described the same behaviour in
2236
+ * different words. Documented once here; each path intersects this with the
2237
+ * handful of options that are genuinely its own.
2238
+ */
2239
+ interface CaptureRequest {
2240
+ /** Camera mode. Default `'current'`. */
2241
+ mode?: CaptureMode;
2242
+ /** Required when `mode === 'custom'`. Ignored otherwise. */
2243
+ camera?: CameraState;
2244
+ /**
2245
+ * Dimensions in pixels. Supply at least one; the other follows the scene
2246
+ * aspect (`top-down`) or the live canvas aspect (`current` / `custom`).
2247
+ * Omit both to use the live canvas dimensions x pixel ratio.
2248
+ *
2249
+ * Both are clamped to what the GPU can actually allocate — read
2250
+ * `widthPx` / `heightPx` / `clamped` back for what was really rendered.
2251
+ */
2252
+ width?: number;
2253
+ height?: number;
2254
+ /**
2255
+ * Background colour:
2256
+ * - `null` (default) → transparent frames, matching the live canvas
2257
+ * contract. JPEG falls back to white because it has no alpha.
2258
+ * - `'#RRGGBB'` / any three.js ColorRepresentation → solid fill.
2259
+ */
2260
+ backgroundColor?: ColorRepresentation | null;
2261
+ /** Margin around the scene AABB in `top-down` mode. Default 1.1 (10 %). */
2262
+ paddingFactor?: number;
2263
+ /**
2264
+ * Multiplier applied to every Light in the scene for the render, restored
2265
+ * afterwards. The default differs per path: 1.35 for a still, which
2266
+ * otherwise reads dim next to the live canvas, and 1 for a clip, where
2267
+ * that boost would make the whole video brighter than the run the user
2268
+ * watched. Pass 1 to disable.
2269
+ */
2270
+ exposureBoost?: number;
2271
+ /**
2272
+ * MSAA sample count for the offscreen target. Default 4 where the context
2273
+ * supports it, 0 otherwise; 0 disables. The live canvas is created with
2274
+ * `antialias: true`, so without this every geometry edge in an export is
2275
+ * hard-aliased while the live view stays smooth. The effective count,
2276
+ * clamped to the hardware maximum, is reported back.
2277
+ */
2278
+ samples?: number;
2279
+ /**
2280
+ * Refresh the shadow map before rendering. Default `true`.
2281
+ *
2282
+ * `performance.shadowOnDemand` leaves `shadowMap.autoUpdate` off and only
2283
+ * dirties the map on camera change, so movers driven through
2284
+ * `setMoverPositions` would otherwise cast shadows from a stale position.
2285
+ * Costs one shadow pass; pass `false` to skip it.
2286
+ */
2287
+ updateShadows?: boolean;
2288
+ /**
2289
+ * Pin wall-clock animations (the drive-status blink) for the render.
2290
+ * Default `true`.
2291
+ *
2292
+ * Unpinned, a capture catches whatever phase the last live frame left
2293
+ * behind: two stills of the same scene differ, and a clip flickers at an
2294
+ * irregular rate because its frames are not produced at wall-clock speed.
2295
+ */
2296
+ freezeAnimations?: boolean;
2297
+ }
2298
+ /** What a capture reports back about the frame it actually produced. */
2299
+ interface CaptureDimensions {
2300
+ /** Dimensions actually rendered — after hardware / budget clamping. */
2301
+ widthPx: number;
2302
+ heightPx: number;
2303
+ /** Dimensions requested, before clamping. */
2304
+ requestedWidthPx: number;
2305
+ requestedHeightPx: number;
2306
+ /** True when the request exceeded what the GPU could allocate. */
2307
+ clamped: boolean;
2308
+ /** MSAA samples actually used (0 = off). */
2309
+ samples: number;
2310
+ /** Camera the frame was rendered with. */
2311
+ camera: CameraState;
2312
+ }
2313
+
2314
+ /**
2315
+ * captureScreenshot — render the live r3f scene into an offscreen
2316
+ * WebGLRenderTarget at any resolution, in any of three camera modes:
2317
+ *
2318
+ * • 'current' — clone the live camera at its current pose (image aspect
2319
+ * adjusted to the requested width/height). Handles both
2320
+ * the perspective view and the orthographic 2D plan view.
2321
+ * • 'top-down' — orthographic camera placed above the scene's AABB
2322
+ * centre, looking straight down -Z, with up = world +Y.
1904
2323
  * Frustum sized to fit the AABB plus padding. Mirrors
1905
2324
  * the 2D viewer convention (path along image X,
1906
2325
  * encoder-side along image Y).
@@ -1931,79 +2350,26 @@ type CaptureMode = 'current' | 'top-down' | 'custom';
1931
2350
  * background and boosted lights.
1932
2351
  */
1933
2352
 
1934
- type ScreenshotMode = 'current' | 'top-down' | 'custom';
2353
+ /**
2354
+ * @deprecated Use `CaptureMode`. This was a byte-for-byte re-declaration of
2355
+ * it, so the package shipped two public names for one union.
2356
+ */
2357
+ type ScreenshotMode = CaptureMode;
1935
2358
  type ScreenshotFormat = 'png' | 'jpeg' | 'webp';
1936
- interface ScreenshotOptions {
1937
- /** Camera mode. Default: `'current'`. */
1938
- mode?: ScreenshotMode;
1939
- /** Required when `mode === 'custom'`. Ignored otherwise. */
1940
- camera?: CameraState;
1941
- /**
1942
- * Image dimensions in pixels.
1943
- * - Both supplied: the rendered frustum (top-down) is adjusted to
1944
- * match the resulting aspect; the camera (current / custom) is
1945
- * fitted to `width / height`.
1946
- * - One supplied: the other is derived to keep the scene's aspect
1947
- * ratio (top-down) or the live canvas aspect (current / custom).
1948
- * - Neither: canvas CSS dimensions × `pixelRatio`.
1949
- *
1950
- * Both are clamped to what the GPU can actually allocate — check
1951
- * `widthPx` / `heightPx` / `clamped` on the result for what was
1952
- * really rendered.
1953
- */
1954
- width?: number;
1955
- height?: number;
2359
+ /**
2360
+ * `CaptureRequest` carries the nine options shared with `beginFrameCapture`;
2361
+ * these three are the still-image path's own.
2362
+ */
2363
+ interface ScreenshotOptions extends CaptureRequest {
1956
2364
  format?: ScreenshotFormat;
1957
2365
  /** 0..1 — JPEG / WebP quality. Default 0.92. Ignored for PNG. */
1958
2366
  quality?: number;
1959
2367
  /**
1960
- * Padding factor around the scene AABB in `top-down` mode. Default
1961
- * 1.1 (10 % margin on each side). Higher = more whitespace.
1962
- */
1963
- paddingFactor?: number;
1964
- /**
1965
- * Background colour:
1966
- * - `null` (default) → transparent PNG, matches the live canvas
1967
- * contract. JPEG falls back to white because the format has no
1968
- * alpha channel.
1969
- * - `'#RRGGBB'` / any three.js ColorRepresentation → solid fill.
1970
- */
1971
- backgroundColor?: ColorRepresentation | null;
1972
- /**
1973
- * Pixel ratio for offscreen renders. Default = renderer's current
1974
- * pixel ratio (matches the live canvas sharpness on high-DPI
1975
- * displays). Set explicitly for fixed-resolution exports.
2368
+ * Pixel ratio for the offscreen render. Default = the renderer's current
2369
+ * pixel ratio, which matches live-canvas sharpness on high-DPI displays.
2370
+ * Set explicitly for fixed-resolution exports.
1976
2371
  */
1977
2372
  pixelRatio?: number;
1978
- /**
1979
- * Multiplier applied to every Light in the scene during the offscreen
1980
- * render. Default 1.35 — screenshots tend to feel dim compared to
1981
- * the live canvas because the live tone-mapping pipeline picks up
1982
- * the surrounding monitor luminance, which the offscreen render
1983
- * doesn't. Pass 1 to disable, > 1 for a brighter result. Restored
1984
- * after capture.
1985
- */
1986
- exposureBoost?: number;
1987
- /**
1988
- * MSAA sample count for the offscreen target. Default 4 where the
1989
- * context supports it, 0 otherwise. 0 disables.
1990
- *
1991
- * The live canvas is created with `antialias: true`; without this the
1992
- * offscreen pass had no MSAA at all, so every geometry edge in an
1993
- * export was hard-aliased while the live view stayed smooth. The
1994
- * effective count (clamped to the hardware maximum) comes back on the
1995
- * result.
1996
- */
1997
- samples?: number;
1998
- /**
1999
- * Refresh the shadow map before rendering. Default `true`.
2000
- *
2001
- * `performance.shadowOnDemand` leaves `shadowMap.autoUpdate` off and
2002
- * only dirties the map when the camera moves, so movers driven purely
2003
- * through `setMoverPositions` would otherwise cast shadows from a
2004
- * stale position. Costs one shadow pass; pass `false` to skip it.
2005
- */
2006
- updateShadows?: boolean;
2007
2373
  }
2008
2374
  interface ScreenshotResult {
2009
2375
  blob: Blob;
@@ -2079,69 +2445,13 @@ interface ScreenshotResult {
2079
2445
  * `exportScreenshot` does.
2080
2446
  */
2081
2447
 
2082
- interface FrameCaptureOptions {
2083
- /**
2084
- * Frame dimensions in px. Supply at least one; the other follows the scene
2085
- * aspect (`top-down`) or the live canvas aspect (`current` / `custom`).
2086
- * Omit both to use the live canvas dimensions × pixel ratio.
2087
- *
2088
- * Sessions tolerate a render size well above the live canvas — supersampling
2089
- * for the encoder is the expected usage. Both values are clamped to what the
2090
- * GPU can allocate; check `widthPx` / `heightPx` / `clamped` for what you
2091
- * actually get.
2092
- */
2093
- width?: number;
2094
- height?: number;
2095
- /** Camera mode. Default `'current'`. */
2096
- mode?: CaptureMode;
2097
- /** Required when `mode === 'custom'`. */
2098
- camera?: CameraState;
2099
- /** `null` (default) → transparent frames. */
2100
- backgroundColor?: ColorRepresentation | null;
2101
- /** Top-down margin. Fixed for the session. Default 1.1. */
2102
- paddingFactor?: number;
2103
- /**
2104
- * Light multiplier. Default **1** — unlike `exportScreenshot`, which uses
2105
- * 1.35 because a single still reads dim next to the live canvas. Applying
2106
- * that to every frame of a clip would make the whole video uniformly
2107
- * brighter than the run the user watched.
2108
- */
2109
- exposureBoost?: number;
2110
- /** MSAA samples. Default 4, clamped to the hardware maximum. 0 disables. */
2111
- samples?: number;
2112
- /**
2113
- * Pin wall-clock-driven effects (the drive-status blink) to a fixed value
2114
- * for every frame. Default `true`.
2115
- *
2116
- * Export frames are not produced at wall-clock speed, so an unpinned blink
2117
- * flickers at an irregular rate in the clip and two exports of the same run
2118
- * differ pixel-wise.
2119
- */
2120
- freezeAnimations?: boolean;
2121
- /**
2122
- * Refresh the shadow map on every frame. Default `true` when shadows are
2123
- * enabled.
2124
- *
2125
- * `performance.shadowOnDemand` leaves `shadowMap.autoUpdate` off and only
2126
- * dirties on camera change, so movers driven through `setMoverPositions`
2127
- * would drag their first-frame shadows through the whole clip. Costs one
2128
- * shadow pass per frame; set `false` to trade correctness for speed.
2129
- */
2130
- updateShadows?: boolean;
2131
- }
2132
- interface FrameCaptureSession {
2133
- /** Dimensions actually rendered — after hardware / budget clamping. */
2134
- readonly widthPx: number;
2135
- readonly heightPx: number;
2136
- /** Dimensions requested, before clamping. */
2137
- readonly requestedWidthPx: number;
2138
- readonly requestedHeightPx: number;
2139
- /** True when the request exceeded what the GPU could allocate. */
2140
- readonly clamped: boolean;
2141
- /** MSAA samples actually used (0 = off). */
2142
- readonly samples: number;
2143
- /** Camera pinned for the whole session. */
2144
- readonly camera: CameraState;
2448
+ /**
2449
+ * `CaptureRequest` carries the nine options shared with `exportScreenshot`.
2450
+ * The clip path adds nothing of its own; it only differs in the default for
2451
+ * `exposureBoost` (1 rather than 1.35 — see the field's doc).
2452
+ */
2453
+ type FrameCaptureOptions = CaptureRequest;
2454
+ interface FrameCaptureSession extends Readonly<CaptureDimensions> {
2145
2455
  /** True until `dispose()`. */
2146
2456
  readonly disposed: boolean;
2147
2457
  /**
@@ -2408,6 +2718,14 @@ interface XtsViewer3DRef {
2408
2718
  focusOn(target: FocusTarget, opts?: FocusOptions): void;
2409
2719
  setCamera(cam: CameraState): void;
2410
2720
  getCamera(): CameraState | null;
2721
+ /**
2722
+ * @deprecated Not implemented — this is a no-op that warns in development.
2723
+ *
2724
+ * `orientation` is a static prop in this build; there is no state behind
2725
+ * it to update. Set it through `<XtsViewer3D orientation={…}>` instead.
2726
+ * The method is kept only so the published interface does not change
2727
+ * shape mid-major, and will be removed in the next major.
2728
+ */
2411
2729
  setOrientation(orientation: XtsConfig['orientation']): void;
2412
2730
  getMoverWorldTransform(ref: MoverRef): {
2413
2731
  positionMm: [number, number, number];
@@ -2472,6 +2790,15 @@ interface XtsViewer3DRef {
2472
2790
  * frameloop tick.
2473
2791
  */
2474
2792
  renderFrameNow(): void;
2793
+ /**
2794
+ * @deprecated Not implemented — this is a no-op that warns in development.
2795
+ *
2796
+ * GLB caching lives in `useGLTF`'s global cache, which r3f exposes no
2797
+ * per-instance handle to clear. Use `useGLTF.clear()` from
2798
+ * `@react-three/drei` directly if you need to drop cached assets. Kept
2799
+ * only so the published interface does not change shape mid-major, and
2800
+ * will be removed in the next major.
2801
+ */
2475
2802
  reloadAssets(): Promise<void>;
2476
2803
  setMoverPosition(moverId: number, partPositionMm: number): void;
2477
2804
  /**
@@ -2528,107 +2855,6 @@ interface XtsViewer3DRef {
2528
2855
  measurements: MeasurementApi;
2529
2856
  }
2530
2857
 
2531
- /**
2532
- * SidecarLoader — fetches per-asset JSON sidecars.
2533
- *
2534
- * Sidecars carry origin-correction (translateMm + rotationDegEuler) plus
2535
- * path metadata. They live next to the GLB at <baseUrl>/<id>.meta.json (or
2536
- * <id>.tool.meta.json for mover tools).
2537
- *
2538
- * On 404 the loader falls back to default origin-correction `(0,0,0)` /
2539
- * `(0,0,0)`. The renderer can still place the GLB; the calibration tool is
2540
- * the authoritative source for sidecar values.
2541
- */
2542
-
2543
- interface OriginCorrection {
2544
- translateMm: Vec3;
2545
- rotationDegEuler: Vec3;
2546
- }
2547
- interface ModuleSidecar {
2548
- moduleType: ModuleType3D;
2549
- glbByRailSystem: {
2550
- Beckhoff: string | null;
2551
- HepcoGfx?: string | null;
2552
- };
2553
- originCorrection: OriginCorrection;
2554
- pathType: 'Straight' | 'Curve' | 'Free';
2555
- moduleLengthMm: number;
2556
- endAngleDeg: number;
2557
- freePathFile?: string;
2558
- approximateBoundsMm?: {
2559
- min: Vec3;
2560
- max: Vec3;
2561
- };
2562
- }
2563
- interface MoverSidecar {
2564
- moverType: MoverType3D;
2565
- glb: string;
2566
- originCorrection: OriginCorrection;
2567
- magnetPlateCenterMm: Vec3;
2568
- pathLengthMm: number;
2569
- }
2570
- interface MoverToolSidecar {
2571
- toolType: MoverToolType3D;
2572
- glbUrl: string;
2573
- originCorrection: OriginCorrection;
2574
- defaultOffsetMm: Vec3;
2575
- approximateBoundsMm?: {
2576
- min: Vec3;
2577
- max: Vec3;
2578
- };
2579
- label: string;
2580
- }
2581
- /**
2582
- * Calibration sidecar for a guiding-rail GLB. The rail is rendered in the
2583
- * same module-local frame as the module (i.e. under the module's
2584
- * `startWorldPose`) with this correction applied to the GLB scene.
2585
- */
2586
- interface RailSidecar {
2587
- railType: RailType3D;
2588
- glb: string;
2589
- originCorrection: OriginCorrection;
2590
- approximateBoundsMm?: {
2591
- min: Vec3;
2592
- max: Vec3;
2593
- };
2594
- }
2595
- declare const DEFAULT_ORIGIN_CORRECTION: OriginCorrection;
2596
- /**
2597
- * Generic sidecar fetch with promise caching + graceful fallback. Returns
2598
- * `undefined` on 404 (caller substitutes a default), and surfaces network or
2599
- * parse errors as `Error` (caller is expected to forward to onError).
2600
- */
2601
- declare class SidecarLoader {
2602
- private readonly timeoutMs;
2603
- private readonly cache;
2604
- constructor(timeoutMs?: number);
2605
- clear(): void;
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>;
2612
- }
2613
- /**
2614
- * Resolve the effective origin correction for a module sidecar — falls back
2615
- * to identity when the sidecar is absent.
2616
- */
2617
- declare function resolveOriginCorrection(sidecar: {
2618
- originCorrection?: OriginCorrection;
2619
- } | undefined): OriginCorrection;
2620
- /** Standard sidecar filename from a base id. */
2621
- declare function moduleSidecarFilename(moduleType: ModuleType3D): string;
2622
- declare function moverSidecarFilename(moverType: MoverType3D): string;
2623
- declare function toolSidecarFilename(toolType: MoverToolType3D): string;
2624
- declare function railSidecarFilename(railType: RailType3D): string;
2625
- /**
2626
- * Pure helper: pick the GLB filename for a moduleSidecar with rail-system
2627
- * fallback. Returns null when the sidecar marks the module 'prepared but
2628
- * not delivered'.
2629
- */
2630
- declare function pickGlbForRail(sidecar: ModuleSidecar | undefined, rail: RailSystem): string | null;
2631
-
2632
2858
  /**
2633
2859
  * HepcoGfxRailProfile — cross-section of the Hepco GFX guiding rail used by
2634
2860
  * the procedural extrusion in <HepcoGfxRail>.
@@ -2756,12 +2982,6 @@ declare function createHepcoGfxRailShape(dims?: HepcoGfxRailProfileDims): Shape;
2756
2982
  * Live overrides for module/mover/tool origin-correction sidecars. Used by
2757
2983
  * the calibration tool to preview adjustments without writing files.
2758
2984
  */
2759
- interface SidecarOverrides {
2760
- modules?: Partial<Record<ModuleType3D, OriginCorrection>>;
2761
- movers?: Partial<Record<MoverType3D, OriginCorrection>>;
2762
- tools?: Partial<Record<MoverToolType3D, OriginCorrection>>;
2763
- rails?: Partial<Record<RailType3D, OriginCorrection>>;
2764
- }
2765
2985
  interface XtsViewer3DProps {
2766
2986
  /** Complete XTS configuration. */
2767
2987
  config: XtsConfig;
@@ -2806,12 +3026,15 @@ interface XtsViewer3DProps {
2806
3026
  */
2807
3027
  topDown?: boolean;
2808
3028
  /** Lock interactions. */
2809
- lock?: {
2810
- rotate?: boolean;
2811
- pan?: boolean;
2812
- zoom?: boolean;
2813
- selection?: boolean;
2814
- };
3029
+ lock?: InteractionLocks;
3030
+ /**
3031
+ * Move the camera through the scene with the keyboard instead of only
3032
+ * orbiting it: arrow keys / WASD walk along the ground plane, PageUp /
3033
+ * PageDown climb, Shift is faster and Alt finer. On by default; the canvas
3034
+ * takes focus on pointer-down, so the keys arm themselves on the first
3035
+ * click into the viewer.
3036
+ */
3037
+ keyboardNavigation?: KeyboardNavigationOptions;
2815
3038
  /** Selection-Mode. Default: 'Off'. */
2816
3039
  selectionMode?: SelectionMode;
2817
3040
  /** Controlled selection. */
@@ -2896,40 +3119,7 @@ interface XtsViewer3DProps {
2896
3119
  onError?: (err: XtsViewerError) => void;
2897
3120
  onAssetsLoaded?: () => void;
2898
3121
  /** Performance-Tuning. */
2899
- performance?: {
2900
- maxFps?: number;
2901
- instancing?: boolean;
2902
- autoPauseOnHidden?: boolean;
2903
- /**
2904
- * Enable demand rendering: frames only render when something changes
2905
- * (camera move, mover position update, selection). Saves >90% GPU
2906
- * time when the scene is idle. Default: true.
2907
- */
2908
- demandRendering?: boolean;
2909
- /**
2910
- * Cap the device pixel ratio. Values above 2 rarely improve
2911
- * perceptible quality but double fill-rate cost on Retina/4K
2912
- * displays. Default: 2.
2913
- */
2914
- maxDpr?: number;
2915
- /**
2916
- * Disable automatic per-frame shadow map updates. Shadows only
2917
- * re-render on camera change. Default: true (when shadows enabled).
2918
- */
2919
- shadowOnDemand?: boolean;
2920
- /**
2921
- * Opt into the WebGPU renderer when the browser supports it.
2922
- * Falls back to WebGL automatically. Default: false (experimental).
2923
- */
2924
- webgpu?: boolean;
2925
- /**
2926
- * Override the KTX2 (Basis) transcoder path used to decode compressed
2927
- * textures in the GLB assets. Defaults to a CDN copy pinned to the
2928
- * bundled three.js revision. Point this at a self-hosted
2929
- * `basis_transcoder.{js,wasm}` directory for offline / air-gapped use.
2930
- */
2931
- ktx2TranscoderUrl?: string;
2932
- };
3122
+ performance?: PerformanceOptions;
2933
3123
  /**
2934
3124
  * Live override of the per-asset origin-correction sidecars. Useful for
2935
3125
  * the calibration tool.
@@ -2946,11 +3136,7 @@ interface XtsViewer3DProps {
2946
3136
  */
2947
3137
  hepcoGfxProfile?: Partial<HepcoGfxRailProfileDims>;
2948
3138
  /** CAD-style ViewCube overlay. */
2949
- viewCube?: {
2950
- enabled?: boolean;
2951
- alignment?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center' | 'center-left' | 'center-right' | 'center-center';
2952
- marginPx?: [number, number];
2953
- };
3139
+ viewCube?: ViewCubeOptions;
2954
3140
  /** Stator heatmap data — coloured tube along each part's centerline. */
2955
3141
  statorHeatmap?: StatorHeatmap;
2956
3142
  /** Continuous mover collision monitoring. */
@@ -3032,9 +3218,61 @@ interface MoverCatalogEntry {
3032
3218
  magnetPlateCenterMm: Vec3;
3033
3219
  }
3034
3220
  declare const MOVER_CATALOG: Readonly<Record<MoverType3D, MoverCatalogEntry>>;
3221
+ /**
3222
+ * The magnet plate set already modelled into each mover's own GLB, or `null`
3223
+ * for a carriage that ships bare and needs one attached.
3224
+ *
3225
+ * Nearly every mover asset is a combined carriage+plate assembly: the Beckhoff
3226
+ * movers say so in the filename (`AT9014-0055-0550` carries an AT9001-0550),
3227
+ * and each Hepco carriage records the plate it was ordered with in its
3228
+ * sidecar's `_calibration.hepcoPartNumber` (`…_AT9001-0775` and friends).
3229
+ * Attaching a second plate to one of those would draw a plate through a plate,
3230
+ * so `<XtsMoverPlate>` renders only where this table says `null`.
3231
+ *
3232
+ * `Custom` is `null` because its geometry comes from `customMoverLayout`,
3233
+ * which may or may not include a plate — the host decides.
3234
+ *
3235
+ * Exhaustive over `MoverType3D` on purpose (`Record`, not `Partial<Record>`):
3236
+ * a new mover type has to state which it is, so it cannot silently end up
3237
+ * double-plated or plate-less.
3238
+ */
3239
+ declare const MOVER_INTEGRATED_PLATE: Readonly<Record<MoverType3D, MagnetPlateType3D | null>>;
3035
3240
  declare function getMoverEntry(moverType: MoverType3D): MoverCatalogEntry | undefined;
3036
3241
  declare function isKnownMoverType(value: string): value is MoverType3D;
3037
3242
 
3243
+ /**
3244
+ * Magnet plate set catalog (3D-canonical).
3245
+ *
3246
+ * A plate set is the part that carries the mover's magnets. Most mover GLBs
3247
+ * ship with one already modelled in (see `MOVER_INTEGRATED_PLATE`); these
3248
+ * entries describe the sets that exist as assets of their own, so a bare
3249
+ * carriage can be given any pole count without a new combined CAD assembly.
3250
+ *
3251
+ * `lengthMm` is the plate's extent along the travel axis, measured from the
3252
+ * converted GLB rather than inferred from the part number. Pole count was
3253
+ * likewise counted in the mesh: each magnet is 7.7 mm wide and sits in a row
3254
+ * just proud of its backing plate, so the magnets separate cleanly along the
3255
+ * travel axis. That count is what establishes the part-number rule (the digit
3256
+ * after the dash is the pole count in base 16) — it is not assumed from it.
3257
+ */
3258
+
3259
+ interface MagnetPlateCatalogEntry {
3260
+ plateType: MagnetPlateType3D;
3261
+ /** Magnets in one row, i.e. the pole count the part number encodes. */
3262
+ poleCount: number;
3263
+ /** Plate extent along the travel axis, mm (GLB-measured). */
3264
+ lengthMm: number;
3265
+ /**
3266
+ * Whether the set models the encoder flag ("Geberfahne"). The AT9001 sets
3267
+ * all carry it as a thin sheet reaching back along −Z; the hygienic ATH9001
3268
+ * sets encapsulate their rows in sealed stainless and model none.
3269
+ */
3270
+ hasEncoderFlag: boolean;
3271
+ }
3272
+ declare const MAGNET_PLATE_CATALOG: Readonly<Record<MagnetPlateType3D, MagnetPlateCatalogEntry>>;
3273
+ declare function getMagnetPlateEntry(plateType: MagnetPlateType3D): MagnetPlateCatalogEntry | undefined;
3274
+ declare function isKnownMagnetPlateType(value: unknown): value is MagnetPlateType3D;
3275
+
3038
3276
  /**
3039
3277
  * Module-local path math.
3040
3278
  *
@@ -3145,6 +3383,29 @@ declare function buildPartPath(modules: ReadonlyArray<ModuleEntry>): PartPath;
3145
3383
  */
3146
3384
  declare function poseToMatrix4(pose: PlanarPose, out?: Matrix4): Matrix4;
3147
3385
 
3386
+ /**
3387
+ * Pure helpers for composing per-XPU `trackTransform` and root
3388
+ * `XtsConfig.orientation` into Three.js Matrix4 instances. Lives in geometry/
3389
+ * so the component layer (XtsViewer3D, XtsScene) and the test suite share a
3390
+ * single source of truth — no duplicated math.
3391
+ *
3392
+ * Composition order:
3393
+ *
3394
+ * world = orientation ⊗ trackTransform ⊗ partTransformation ⊗ chain
3395
+ *
3396
+ * `partTransformation` is applied inside the per-XPU group by XtsScene, so
3397
+ * the helpers here only cover the outermost two layers.
3398
+ */
3399
+
3400
+ /**
3401
+ * Default Z lift for a track that does not specify `positionMm`. The XTS
3402
+ * track sits on a base plate / table top in real installations, so 0 mm
3403
+ * (= floor) is rarely what the user wants. 200 mm matches the standard
3404
+ * Beckhoff table height used throughout the demo configs and renders
3405
+ * correctly under the default shadow-casting ground plane.
3406
+ */
3407
+ declare const DEFAULT_TRACK_Z_MM = 200;
3408
+
3148
3409
  /**
3149
3410
  * Point-cloud registry for Free-path modules.
3150
3411
  *
@@ -3249,6 +3510,67 @@ declare function interpolateHeatmapValue(samples: ReadonlyArray<HeatmapSample>,
3249
3510
  */
3250
3511
  declare function normaliseToHeatmapRange(value: number, min: number, max: number): number;
3251
3512
 
3513
+ /**
3514
+ * The vertical extent of a track module, in the path frame.
3515
+ *
3516
+ * `moduleCollision.ts` needs to know how tall a module is and where it sits
3517
+ * relative to the mover path. It used to assume one coarse box for every type —
3518
+ * `0 … DEFAULT_MODULE_HEIGHT_MM`, entirely *above* the path plane — which is
3519
+ * neither the right height nor the right side: a motor module measures about
3520
+ * 40 mm and straddles the plane, roughly half above and half below. Two tracks
3521
+ * stacked closer than 100 mm therefore reported an overlap that does not exist,
3522
+ * and the deepest ones reported it on every module pair.
3523
+ *
3524
+ * ## Why only Z, and not the width too
3525
+ *
3526
+ * The GLB sidecar carries `approximateBoundsMm`, the axis-aligned box of the
3527
+ * CAD body *before* `originCorrection` is applied. Composing the two gives the
3528
+ * body in the path frame — but only its **Z** range is a cross-section.
3529
+ *
3530
+ * Module paths are planar in XY and curves bend about Z, so a curved module's
3531
+ * GLB spans its whole arc in X and Y: the 180° AT2050_0500 measures 307 mm
3532
+ * across in Y, which is the sweep of the U-turn and not how wide the beam is.
3533
+ * Its Z range, on the other hand, is the same 40.1 mm as the straight it is
3534
+ * built from, because the sweep never leaves the plane.
3535
+ *
3536
+ * So the height is read from the asset and the half-width stays the coarse
3537
+ * constant. Taking the width from the same box would inflate every curve
3538
+ * segment to the size of its arc and report a collision for every oval.
3539
+ *
3540
+ * Overrides are not consulted: `SidecarOverrides` can replace a module's
3541
+ * `originCorrection` but carries no bounds, so there is nothing to override.
3542
+ */
3543
+
3544
+ /** Where a module's body sits along Z, relative to the mover path plane. */
3545
+ interface ModuleHeightSpan {
3546
+ /** Lowest point of the body [mm]; negative means below the path plane. */
3547
+ minZMm: number;
3548
+ /** Highest point of the body [mm]. */
3549
+ maxZMm: number;
3550
+ }
3551
+ /**
3552
+ * The span a module without usable bounds falls back to: the historic coarse
3553
+ * box, so a type the asset package does not describe behaves as it always did.
3554
+ */
3555
+ declare function coarseHeightSpan(heightMm: number): ModuleHeightSpan;
3556
+ /**
3557
+ * Compose `approximateBoundsMm` through `originCorrection` and keep the Z range.
3558
+ *
3559
+ * All eight corners are transformed rather than only the two extremes: every
3560
+ * shipped correction happens to be a multiple of 90°, which would keep the box
3561
+ * axis-aligned, but nothing in the sidecar format promises that, and under a
3562
+ * 45° correction two corners would describe a box far too small.
3563
+ */
3564
+ declare function heightSpanOf(sidecar: ModuleSidecar): ModuleHeightSpan | null;
3565
+ /**
3566
+ * How tall `moduleType` is and where it sits, or the coarse fallback.
3567
+ *
3568
+ * Resolved per module type rather than once per run: an Eco straight, a
3569
+ * hygienic module with its tower and an AT2002 with its infeed box are three
3570
+ * different heights, and a stack has to be judged against the one it holds.
3571
+ */
3572
+ declare function moduleHeightSpan(moduleType: ModuleType3D, fallbackHeightMm: number): ModuleHeightSpan;
3573
+
3252
3574
  /**
3253
3575
  * Shared closed-loop detection for built chains.
3254
3576
  *
@@ -3451,24 +3773,61 @@ interface Props {
3451
3773
  declare const ModuleCornerMarkers: React__default.FC<Props>;
3452
3774
 
3453
3775
  /**
3454
- * <DimensionLabel> — camera-facing text used by the Dimensions overlay
3455
- * (and re-usable elsewhere). Wraps drei's `<Billboard>` + `<Text>` so the
3456
- * label always reads horizontally regardless of camera orbit, and is
3457
- * placed at a small +Z offset above the dimension tick so the text and
3458
- * the marker don't z-fight.
3776
+ * labelFont — resolution of the font URL every 3D text label is drawn
3777
+ * with, and the reason none of them can stall the scene any more.
3778
+ *
3779
+ * troika-three-text loads its font over XHR and, when that request
3780
+ * fails, only logs `Failure loading font <url>`: the internal callback
3781
+ * queue for that URL is never drained. drei's `<Text>` wraps exactly
3782
+ * that callback in `suspend(...)`, so a dangling load becomes a promise
3783
+ * that never settles. With one `<Suspense fallback={null}>` around the
3784
+ * scene, a single unreachable font blanked the whole viewer — geometry,
3785
+ * grid, ViewCube and all — with nothing in the console beyond troika's
3786
+ * own line. Every sample that carried a station or an area label went
3787
+ * dark that way.
3788
+ *
3789
+ * Two things prevent that here:
3790
+ *
3791
+ * 1. The default font is compiled in (`labelFontData.ts`) and handed to
3792
+ * troika as a `blob:` URL, so the common path never touches the
3793
+ * network and cannot fail.
3794
+ * 2. A caller-supplied font URL is HEAD/GET-probed before it is handed
3795
+ * over. Until it answers, labels render in the built-in font; if it
3796
+ * never answers, they keep rendering in the built-in font and the
3797
+ * failure is reported once, loudly. troika is never given a URL that
3798
+ * has not already been fetched successfully.
3799
+ *
3800
+ * `<DimensionLabel>` / `<MoverIdLabel>` additionally wrap their `<Text>`
3801
+ * in a local Suspense boundary, so even a font that parses badly costs
3802
+ * one label rather than the scene.
3459
3803
  */
3460
-
3461
3804
  /**
3462
- * Default 3D-text font: Roboto Condensed (Latin, regular weight) served
3463
- * from jsDelivr's @fontsource mirror. Pinned to a fixed major version so
3464
- * the stack stays reproducible; the URL responds with a long
3465
- * Cache-Control TTL so repeated reads come from the browser cache.
3805
+ * Default 3D-text font: Roboto Condensed (Latin, regular weight).
3806
+ *
3807
+ * The value is the jsDelivr `@fontsource` mirror it was originally
3808
+ * fetched from, kept stable because it is part of the public API. It is
3809
+ * no longer fetched: the viewer ships the same WOFF compiled in, and
3810
+ * treats this URL as "use the built-in copy", so passing it explicitly
3811
+ * is equivalent to passing nothing and costs no request.
3466
3812
  *
3467
- * `font` on troika-three-text expects an OTF / TTF / WOFF URL — NOT a CSS
3468
- * font-family name. Consumers can override via `<DimensionLabel
3469
- * fontFamily="https://…/whatever.woff" />` to swap in a brand font.
3813
+ * `font` on troika-three-text expects an OTF / TTF / WOFF URL — NOT a
3814
+ * CSS font-family name. Any other URL is used as given, once a probe
3815
+ * has confirmed it actually loads.
3470
3816
  */
3471
3817
  declare const DEFAULT_LABEL_FONT_URL: "https://cdn.jsdelivr.net/npm/@fontsource/roboto-condensed@5.2.7/files/roboto-condensed-latin-400-normal.woff";
3818
+ /**
3819
+ * URL of the compiled-in Roboto Condensed WOFF, as a `blob:` object URL
3820
+ * created on first use and cached for the lifetime of the document.
3821
+ *
3822
+ * A blob URL rather than a `data:` URL on purpose: troika loads fonts
3823
+ * through `XMLHttpRequest` and discards any response whose `status` is
3824
+ * 0, which is what several engines report for non-HTTP schemes. Blob
3825
+ * URLs answer with a regular 200.
3826
+ *
3827
+ * Falls back to a `data:` URL where `URL.createObjectURL` is missing
3828
+ * (jsdom, some SSR shims) so the value is always a usable string.
3829
+ */
3830
+ declare function bundledLabelFontUrl(): string;
3472
3831
 
3473
3832
  /**
3474
3833
  * <MoverIdLabel> — Camera-facing 3D text label that shows a mover's
@@ -3479,7 +3838,7 @@ declare const DEFAULT_LABEL_FONT_URL: "https://cdn.jsdelivr.net/npm/@fontsource/
3479
3838
  * the hood) so the label stays readable as the camera orbits. All
3480
3839
  * styling is driven by `MoverIdLabelOptions`; the component supplies
3481
3840
  * the documented defaults (15 mm black text, 55 mm above the mover's
3482
- * magnet-plate origin, Roboto Condensed via jsDelivr).
3841
+ * magnet-plate origin, the viewer's built-in Roboto Condensed).
3483
3842
  */
3484
3843
 
3485
3844
  interface MoverIdLabelProps {
@@ -3502,7 +3861,8 @@ declare const MoverIdLabel: React__default.FC<MoverIdLabelProps>;
3502
3861
  * Module count: 27
3503
3862
  * Mover count: 13
3504
3863
  * Tool count: 2
3505
- * Rail count: 8
3864
+ * Plate count: 7
3865
+ * Rail count: 11
3506
3866
  */
3507
3867
 
3508
3868
  /**
@@ -3556,8 +3916,8 @@ declare function useSidecarSource(): SidecarSourceConfig;
3556
3916
  * only set `Beckhoff`; the loader silently falls back to that variant when
3557
3917
  * `HepcoGfx` is requested but unavailable.
3558
3918
  *
3559
- * `null` for `Beckhoff` marks "prepared but not delivered"
3560
- * (AT2042 / AT2140) — the loader renders a placeholder AABB mesh.
3919
+ * A `null` `Beckhoff` entry marks a module type that is catalogued but has
3920
+ * no GLB — the loader renders a placeholder AABB mesh instead.
3561
3921
  */
3562
3922
 
3563
3923
  /** Filename mapping per ModuleType3D (relative to assetsBaseUrl). */
@@ -3583,7 +3943,7 @@ declare const DEFAULT_RAIL_MANIFEST: Readonly<Record<RailType3D, {
3583
3943
  * - Straights pick the matching-length AT9000 rail (0249 / 0250 / 0500).
3584
3944
  * - +22.5° curves (AT2020 / AT2021) use AT9020.
3585
3945
  * - −22.5° curves (AT2025 / AT2026) use AT9025.
3586
- * - 45° curves (AT2040 / AT2041 / AT2042 / AT2140) use AT9040.
3946
+ * - 45° curves (AT2040 / AT2041) use AT9040.
3587
3947
  * - The 180° clothoid pair (AT2050 0500/0501 + merged 0500_180) uses
3588
3948
  * AT9050, whose GLB already covers the full 500 mm path.
3589
3949
  * - Hygienic ATH modules already include the rail in their module GLB
@@ -3595,10 +3955,12 @@ declare function resolveModuleGlbUrl(moduleType: ModuleType3D, rail: RailSystem,
3595
3955
  declare function resolveMoverGlbUrl(moverType: MoverType3D, override?: AssetManifest['movers']): string;
3596
3956
  declare function resolveToolGlbUrl(toolType: MoverToolType3D, override?: AssetManifest['tools']): string;
3597
3957
  /**
3598
- * Pick the guiding-rail RailType for a module, or `null` if no separate
3599
- * rail mesh should render (ATH variants, undeclared module types).
3958
+ * Which guiding rail a module gets, given the mover running on it.
3959
+ *
3960
+ * `moverType` is optional so existing callers keep the -0055 default; pass it
3961
+ * to get the variant Beckhoff actually pairs with that mover.
3600
3962
  */
3601
- declare function resolveGuidingRailType(moduleType: ModuleType3D): RailType3D | null;
3963
+ declare function resolveGuidingRailType(moduleType: ModuleType3D, moverType?: MoverType3D): RailType3D | null;
3602
3964
  /**
3603
3965
  * Resolve the GLB filename for a guiding-rail type, honoring user-supplied
3604
3966
  * manifest overrides. Returns empty string when the rail is unknown.
@@ -3695,9 +4057,9 @@ declare function getSharedAssetLoader(): AssetLoader;
3695
4057
  declare function composeAssetUrl(baseUrl: string, filename: string): string;
3696
4058
 
3697
4059
  /**
3698
- * The four sidecar kinds as data.
4060
+ * The five sidecar kinds as data.
3699
4061
  *
3700
- * Modules, movers, tools and rails all resolve their calibration the same
4062
+ * Modules, movers, tools, magnet plates and rails all resolve the same
3701
4063
  * way — compiled-in table, else `<assetsBaseUrl>/<filename>`, else identity.
3702
4064
  * Only two things differ per kind: which built-in table to read and how the
3703
4065
  * filename is spelled. Keeping those two as a table (rather than four copies
@@ -3724,6 +4086,10 @@ interface SidecarKinds {
3724
4086
  id: MoverToolType3D;
3725
4087
  sidecar: MoverToolSidecar;
3726
4088
  };
4089
+ plate: {
4090
+ id: MagnetPlateType3D;
4091
+ sidecar: MagnetPlateSidecar;
4092
+ };
3727
4093
  rail: {
3728
4094
  id: RailType3D;
3729
4095
  sidecar: RailSidecar;
@@ -3843,4 +4209,4 @@ declare function snapToGrid(positionMm: Vec3, gridMm: number): Vec3;
3843
4209
  /** Convenience: `resolveSceneContext` + `toEntityRef`. */
3844
4210
  declare function resolveEntityFromObject(object: Object3D | null, instanceId?: number): MeasuredEntityRef | null;
3845
4211
 
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 };
4212
+ 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 CaptureDimensions, type CaptureMode, type CaptureRequest, 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, DEFAULT_TRACK_Z_MM, 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, type InteractionLocks, JSDELIVR_ASSETS_BASE_URL, type KeyboardNavigationOptions, MAGNET_PLATE_CATALOG, MODULE_CATALOG, MODULE_GUIDING_RAIL_MAP, MOVER_CATALOG, MOVER_INTEGRATED_PLATE, type MagnetPlateAssetEntry, type MagnetPlateCatalogEntry, type MagnetPlateType3D, 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 ModuleHeightSpan, 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 PerformanceOptions, 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, type ViewCubeAlignment, type ViewCubeOptions, 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, bundledLabelFontUrl, chainToUserPositionMm, checkAllModuleCollisions, checkAllMoverCollisions, checkModuleCollisions, checkSamePathCollisions, chooseSnapCandidate, coarseHeightSpan, collectFeedSegments, collectFeedSegmentsForXpu, collectTriangleSnapCandidates, composeAssetUrl, computeMeasurementResult, createHepcoGfxBaseplateShape, createHepcoGfxRailShape, deselectAll, distanceMm, findModuleAt, formatAngleDeg, formatLengthMm, getMagnetPlateEntry, getModuleEntry, getMoverEntry, getPointCloud, getSharedAssetLoader, heightSpanOf, interpolateHeatmapValue, isChainClosed, isKnownMagnetPlateType, isKnownModuleType, isKnownMoverType, isModuleSelected, isMoverSelected, labelAnchorFor, moduleAnchorWorld, moduleEndDelta, moduleHeightSpan, 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, toModuleCollisionInputs, toolSidecarFilename, trackAnchorWorld, trackDistanceMm, trackLengthOf, trackPolylineWorld, unregisterPointCloud, useSidecarSource, userToChainPositionMm, worldPointOnPart };