import * as React from 'react'; import React__default from 'react'; import { Vector3, ColorRepresentation, Shape, Curve, CurvePath, Matrix4, WebGLRenderer } from 'three'; import { GLTF } from 'three-stdlib'; /** * AUTO-GENERATED — do not edit by hand. * * Source: package.json * Generator: scripts/sync-version.mjs * * Run `npm run sync-version` (or any script that depends on it) to refresh * after bumping the package version. */ declare const VERSION: "4.8.0"; /** * Default CDN URL for the GLB asset bundle (`beckhoff-xts-viewer-3d-assets`). * * Lives in its own leaf module so it can be imported directly by both * `` and the public `index.ts` barrel without going through * the barrel itself — pulling it through `../index.js` creates a circular * import (XtsViewer3D ↔ index.ts), which manifests at runtime as * `Cannot access 'JSDELIVR_ASSETS_BASE_URL' before initialization` * because the const sits in the TDZ when XtsViewer3D's module body runs. * * Pinned to the assets package version this viewer build targets (see * ./assetsVersion.ts). The assets release line is decoupled from the * viewer release line — assets only publish a new version when GLB * geometry actually changes — so `viewer@x.y.z` does NOT generally * resolve to `assets@x.y.z`. The exact version is captured at viewer * build time so every install streams from a known-compatible GLB * release. */ declare const JSDELIVR_ASSETS_BASE_URL: "https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d-assets@0.3.1/models"; /** * Public type surface for beckhoff-xts-viewer-3d. * * Anything that crosses the Component boundary — props, refs, callbacks — * is declared here. */ /** Beckhoff XTS module types supported by the 3D viewer. */ 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'; /** * Rail system beneath the module. Only affects the visual mesh * (Beckhoff aluminium rail vs. Hepco GFX), not the path math. * * `Beckhoff` is the Beckhoff aluminium guiding rail (all * AT-Standard + Eco families); `HepcoGfx` is the alternative * Hepco GFX profile. ATH modules ship with their rail baked into * the module GLB and are independent of this choice. */ type RailSystem = 'Beckhoff' | 'HepcoGfx'; /** Beckhoff XTS + Hepco mover types. `Custom` requires `customMoverLayout`. */ type MoverType3D = 'AT9011_0050' | 'AT9011_0070' | 'AT9012_0050' | 'AT9014_0055' | 'AT9014_0070' | 'ATH9011_0075' | 'ATH9013_0075' | 'Hepco_GFX2_1TC_S25' | 'Custom'; /** Predefined mover-tool / carrier-plate types. */ type MoverToolType3D = 'AT8200_1000_0100' | 'AT8200_2000_0100' | 'Custom'; /** * Beckhoff guiding-rail GLB variants. Rendered alongside the motor modules * when `RailSystem === 'Beckhoff'` to visualise the outer guide * profile that the mover wheels engage. * * Curve sign drives the selection (see `MODULE_GUIDING_RAIL_MAP`): * - Straights: AT9000_0249 / AT9000_0250 / AT9000_0500 * - +22.5° curve: AT9020_1250 * - −22.5° curve: AT9025_1466 * - 45° curve: AT9040_0750 * - 180° clothoid (entire AT2050 500 mm path): AT9050_0500 * * Hygienic ATH modules ship with their guide profile baked into the module * GLB and therefore do NOT receive a separate guiding-rail mesh. */ type RailType3D = 'AT9000_0249' | 'AT9000_0250' | 'AT9000_0500' | 'AT9020_1250' | 'AT9025_1466' | 'AT9040_0750' | 'AT9050_0500'; type Vec3 = [number, number, number]; type Vec2 = [number, number]; /** Pose of the entire XTS group in the world frame. */ interface Orientation { positionMm?: Vec3; /** Intrinsic XYZ Euler angles in degrees. */ rotationDegEuler?: Vec3; } /** * Per-part heatmap samples plus colour range. Rendered as a coloured * tube running along each part's centerline (with optional lateral / * vertical offset), with the tube's vertex colours interpolated between * `minColor` and `maxColor` according to the consumer-supplied * `(positionMm, value)` samples. * * Typical use: per-stator drive current, motor temperature, or fault * counters streamed from the controller and overlaid on the layout in * real time. Sample positions are interpreted in the XPU's user-frame * (`ProcessingUnitConfig.positionFrame`), same convention as mover / * station / area positions. */ interface StatorHeatmap { /** * One bucket per `PartConfig.globalNumber`. Parts not listed render * no heatmap. Samples within a part are sorted by `positionMm` * before interpolation; values between consecutive samples are * linearly interpolated, values outside the sample range are * held flat at the nearest sample. */ parts: Array<{ partOid: number; samples: Array<{ positionMm: number; value: number; }>; }>; /** Value mapped to `minColor`. Values ≤ min clamp to `minColor`. */ min: number; /** Value mapped to `maxColor`. Values ≥ max clamp to `maxColor`. */ max: number; /** Colour at `min`. Default `'#22c55e'` (green). */ minColor?: string; /** Colour at `max`. Default `'#ef4444'` (red). */ maxColor?: string; /** Tube thickness in mm. Default 6. */ thicknessMm?: number; /** Vertical Z lift (mm). Default -15 — sits just below the rail. */ displacementMm?: number; /** Lateral Y displacement (mm). Default 0. */ lateralDisplacementMm?: number; /** * Path segments per module along the heatmap tube. More = smoother * gradient, more triangles. Default 8 (= 32 segments on a 4-module * straight, ~1 GPU vertex per 8 mm of track). */ segmentsPerModule?: number; /** Tube opacity (0..1). Default 0.85. */ opacity?: number; } interface XtsConfig { orientation?: Orientation; processingUnits: ProcessingUnitConfig[]; stations?: StationConfig[]; /** * Labelled regions along the track. Like a StationConfig, but * without stop positions — only a coloured tube segment plus an * optional description billboard. For zone markings (e.g. "Manual * Access", "Safety Area", "Cleanroom") that have no * machine stops. */ areas?: AreaConfig[]; infoBars?: InfoBarConfig[]; customAssets?: CustomAssetConfig[]; } interface ProcessingUnitConfig { /** * Stable host-supplied 32-bit unique identifier for this XPU. Required * at runtime — must be a non-negative integer that fits in a uint32 * and is unique across all XPUs in the config. */ objectId: number; moverType: MoverType3D; customMoverLayout?: CustomMoverLayout; /** Default: 'Beckhoff'. */ railSystem?: RailSystem; parts: PartConfig[]; movers: MoverConfig[]; /** * Track-level pose, applied to the entire XPU subtree (modules, movers, * tools, stations, info bars, mover-bound custom assets). Composes * MULTIPLICATIVELY with the root `XtsConfig.orientation`: * * world = orientation ⊗ trackTransform ⊗ partTransformation ⊗ chain * * `scaleUniform` is applied as a uniform scale factor (default 1). Non- * uniform scaling is intentionally not supported — it would distort path * radii and break GLB lighting. */ trackTransform?: TrackTransform; /** * User-frame remap applied to every `partPositionMm`-style value inside * this XPU (mover positions, station start/end/stops, area start/end, * stop-position ghost movers). Lets a host that uses a different "zero" * or sign convention than the GLB chain enter positions in their own * frame; the renderer translates to the chain's intrinsic frame: * * chainPos = originMm + (direction === 'negative' ? -userPos : userPos) * * Defaults: `direction: 'positive'`, `originMm: 0` — i.e. user-frame * equals chain-frame, no transform. */ positionFrame?: PositionFrame; } /** * Sign + zero-offset convention for partPosition values within an XPU. * See `ProcessingUnitConfig.positionFrame` for semantics. */ interface PositionFrame { /** Travel direction. Default: `'positive'`. */ direction?: 'positive' | 'negative'; /** Zero-point shift (in chain-coordinates mm). Default: 0. */ originMm?: number; } interface TrackTransform { positionMm?: Vec3; /** Intrinsic XYZ Euler angles in degrees. */ rotationDegEuler?: Vec3; /** Uniform scale factor. Default 1. */ scaleUniform?: number; /** Hide the entire XPU when false. Default true. */ visible?: boolean; } interface PartConfig { /** * Stable host-supplied 32-bit unique identifier for this part. Distinct * from `globalNumber` (server-side ObjectId): callers reference parts * by `objectId` at runtime — e.g. in the array form of * `XtsViewer3DRef.setMoverPositions` (`{partObjectId, partPosition}`). * Required at runtime — must be a non-negative integer that fits in a * uint32 and is unique across all parts in the system. * `normalizeXtsConfig` emits a `missing-part-object-id` / * `invalid-part-object-id` / `duplicate-part-object-id` warning when * the contract is violated. */ objectId: number; /** = ServerTypes.PartModel.ObjectId — unique system-wide. */ globalNumber: number; modules: ModuleEntry[]; partTransformation?: PartTransformation; } interface ModuleEntry { moduleType: ModuleType3D; /** * Stable host-supplied number for this module. Required at runtime — * simply counted up per module (1, 2, 3, …) across the whole system * and used as the index in `XtsViewer3DRef.setModuleStatuses(array)` — * i.e. `statuses[m.globalNumber] === { warning?: …, error?: … }`. * `normalizeXtsConfig` emits a `missing-module-global-number` / * `invalid-module-global-number` / `duplicate-module-global-number` * warning when the contract is violated. Typed optional only so * legacy configs and tests keep compiling; new code MUST set it. */ globalNumber?: number; /** * Optional drive-status overlay for this module. Mirrors the * `MoverConfig.status` shape; the same per-mesh emissive blink + * billboard surfaces both. Filtered by * `display.showDriveWarnings` / `display.showDriveErrors`. * * For live updates (e.g. drive warnings/errors streamed from PLC), * prefer the imperative * `XtsViewer3DRef.setModuleStatuses(array)` channel — index = this * module's `globalNumber`. The store wins over this config-time value. */ status?: { warning?: boolean; error?: boolean; }; } /** * Per-part 3D pose, applied to EVERYTHING anchored to the part: * modules, guiding rails, movers, mover tools, mover-bound custom assets, * and the path-derived overlays (stations, areas, dimensions, info bars, * stop-position ghost movers, stator heatmap). Composes inside the * per-XPU `trackTransform`: * * world = orientation ⊗ trackTransform ⊗ partTransformation ⊗ chain * * All fields are optional and default to identity (offset = [0,0,0], * rotation = [0,0,0]). Whole-track placement is done via * `ProcessingUnitConfig.trackTransform`; this transform is for moving a * part WITHIN its XPU — e.g. lifts that translate along Z, parts * mounted at an angle, or live-editable kinematics in the playground. * * Multi-part overlays (stations / areas spanning multiple `partOids`) * follow the transform of their FIRST partOid — the chain math * upstream is part-local, and points from differently-transformed * parts can't be stitched into a single tube. Anchor multi-part * overlays to the part you want them to track. */ interface PartTransformation { /** Translation in mm, in the XPU's track-transform frame. */ offsetMm?: Vec3; /** Intrinsic XYZ Euler angles in degrees. */ rotationDegEuler?: Vec3; partSide?: 'Default' | 'Driver' | 'Encoder'; /** Hide everything on this part when false. Default true. */ visible?: boolean; } interface MoverConfig { /** Position-on-track index within the XPU. */ index: number; /** * Stable host-supplied 32-bit unique identifier for this mover. * Required, must be a non-negative integer that fits in a uint32 and * is unique across all movers in the config. */ id: number; /** References PartConfig.globalNumber. */ partOid: number; /** [0, part.trackLengthMm] — clamped when out-of-bounds. */ partPositionMm: number; status?: { warning?: boolean; error?: boolean; }; selected?: boolean; /** Predefined tool carriers or custom GLBs attached to this mover. */ tools?: MoverToolConfig[]; } /** * Runtime mover-position update entry — pushed through * `XtsViewer3DRef.setMoverPositions(array)`. The array is indexed by * `MoverConfig.index` (i.e. entry `i` targets the mover with * `index === i`); use `null` / `undefined` to skip a slot. The * `partObjectId` field references `PartConfig.objectId`. */ interface MoverPositionEntry { /** = PartConfig.objectId (the host-side 32-bit unique part ID). */ partObjectId: number; /** Position along the referenced part, in user-frame mm. */ partPosition: number; } /** * Runtime drive-status entry for a single module — pushed through * `XtsViewer3DRef.setModuleStatuses(array)`. The array is indexed by * `ModuleEntry.globalNumber`; use `null` / `undefined` to clear a slot. */ interface ModuleStatusEntry { warning?: boolean; error?: boolean; } interface MoverToolConfig { toolType: MoverToolType3D; /** Required when toolType === 'Custom'. */ customGlbUrl?: string; customOriginCorrection?: { translateMm: Vec3; rotationDegEuler: Vec3; }; /** Local placement on the mover, relative to the magnet-plate center. */ offsetMm?: Vec3; rotationDegEuler?: Vec3; opacity?: number; visible?: boolean; id?: string; } interface CustomMoverLayout { glbUrl: string; originCorrection?: { translateMm: Vec3; rotationDegEuler: Vec3; }; magnetPlateCenterMm?: Vec3; pathLengthMm?: number; imageFrontUrl?: string; imageBackUrl?: string; } type CustomAssetBinding = { type: 'static'; positionMm: Vec3; rotationDegEuler: Vec3; scale?: number; } | { type: 'mover'; moverRef: { processingUnitObjectId: number; moverIndex: number; }; offsetMm?: Vec3; rotationDegEuler?: Vec3; /** Uniform scale (default 1). */ scale?: number; } | { type: 'all-movers'; offsetMm?: Vec3; rotationDegEuler?: Vec3; /** Uniform scale (default 1). Applied per mover. */ scale?: number; }; interface CustomAssetConfig { id: string; glbUrl: string; binding: CustomAssetBinding; opacity?: number; visible?: boolean; } /** * Y-axis / rotation convention for incoming position data. * * - `'viewer'` (default): values are taken as-is in the viewer's * right-handed Z-up frame (+Y = left when looking along +X). * - `'beckhoff-xts'`: the Beckhoff XTS `OriginTransform` convention. * The viewer negates every Y translation (`offsetMm[1]`, * `trackTransform.positionMm[1]`) and every Z-Euler rotation * (`rotationDegEuler[2]`) so that data exported from XAE * Konfigurator / `.xti` files renders with the correct topology * without any consumer-side sign flip. */ type CoordinateSystem = 'viewer' | 'beckhoff-xts'; interface DisplayOptions { /** * Y-axis convention for all spatial inputs (`PartTransformation`, * `TrackTransform`). Set to `'beckhoff-xts'` when binding directly * to XAE / XTI / `` data so the viewer negates Y * translations and Z-Euler rotations internally. Default: * `'viewer'` (no transform — current behaviour). */ coordinateSystem?: CoordinateSystem; displayStations?: boolean; /** Render configured areas. Default: true (when `areas[]` is set). */ displayAreas?: boolean; displayDimensions?: boolean; showDriveErrors?: boolean; showDriveWarnings?: boolean; /** * Render each mover's `id` as a billboarded 3D text label above the * mover. Toggle on/off without re-creating the scene. Visual options * (font size, color, offset, font URL, outline) are configurable via * `moverIdLabelOptions`. Default: false. */ showMoverIds?: boolean; /** * Style + placement overrides for the mover-id labels. Only applied * when `showMoverIds === true`. All fields optional; sensible defaults * shipped by the renderer. */ moverIdLabelOptions?: MoverIdLabelOptions; invertDimensions?: boolean; stationOptions?: StationOptions; stationMarkerOptions?: MarkerOptions; /** * Render a static, semi-transparent ghost mover at every `stopPositions` * of every enabled `StationConfig`. Useful for layout reviews — shows * "where will the mover be when stopped" without driving any positions. * Ghosts are non-interactive, never participate in selection / collision * detection, and use the XPU's `moverType` for their GLB. * * Default: false. */ showStopPositionMovers?: boolean; /** Visual options for the stop-position ghost movers. */ stopPositionMoverOptions?: StopPositionMoverOptions; /** * Render the stator heatmap overlay. Requires the `statorHeatmap` * prop on `` to be set; without data the toggle is a * no-op. Default: false. */ showStatorHeatmap?: boolean; areaOptions?: AreaOptions; dimensionOptions?: DimensionOptions; infoBarOptions?: InfoBarOptions; /** * Render small red spheres at the 8 AABB corners of every module GLB * (the GLB's CAD-frame AABB, transformed through the origin-correction). * * Use during calibration to verify that adjacent modules' mating-faces * are in geometrically the same place — when the corner spheres of two * neighbouring modules overlap, the chain has C0 continuity at that * boundary. Off by default. */ showModuleCornerMarkers?: boolean; /** Diameter (mm) of the corner-marker spheres. Default: 10. */ moduleCornerMarkerSizeMm?: number; /** Color of the corner markers. Default: '#FF2030' (red). */ moduleCornerMarkerColor?: string; /** * Render shadows: enables the WebGL shadow map, makes one directional * light cast shadows, and renders a transparent shadow-catcher plane * so shadows from modules / movers / tools become visible. Default: * false (no shadow-map cost when disabled). All loaded GLB meshes * have their `castShadow` + `receiveShadow` flags set unconditionally * — turning shadows on at the canvas level is the only thing that * actually triggers the per-frame shadow pass. */ shadows?: boolean; /** * Image-based lighting via a procedural indoor environment map * (three's `RoomEnvironment` processed through `PMREMGenerator` — * no HTTP fetch, no asset shipped). Drives PBR reflections on * metallic / roughness materials so aluminium rails and brushed * mover plates read as actual metal instead of flat shaded surfaces. * * Cost: one-time PMREM build on mount (≈ 1.5 MB GPU), zero per-frame * overhead beyond the regular PBR shader's environment lookup. * * Default: `true` — looks noticeably better at no measurable cost. * Set `false` to fall back to direct-lighting only. */ environmentLighting?: boolean; /** * Strength of the environment-map contribution. Lower values keep * the look closer to the previous direct-lighting-only baseline; * higher values emphasise reflections. * * Default: `0.4` — reflections visible without overwhelming the * directional + ambient lights. Range typically `0.0` … `1.5`. */ environmentIntensity?: number; /** * Renderer tone-mapping operator. ACES filmic compresses bright * highlights into a perceptually pleasant rolloff and is the closest * match to film / professional CAD viewers; `'linear'` reproduces the * pre-tone-mapping behaviour for callers who relied on it. * * Default: `'aces'`. */ toneMapping?: 'aces' | 'linear' | 'reinhard' | 'cineon' | 'agx' | 'none'; /** * Tone-mapping exposure multiplier (analogous to a camera's exposure * stop). 1.0 = neutral. Bumped above 1 brightens the result, below 1 * darkens it. * * Default: `1.0`. */ toneMappingExposure?: number; /** * Screen-space ambient occlusion (GTAO). Adds contact shadows in * crevices and between adjacent surfaces for significantly more visual * depth. Cost: one half-res pass per frame. * * Default: `false`. */ ssao?: boolean; /** * SSAO blend intensity. Higher values darken crevices more. * Range: 0.0–4.0. Default: `1.0`. */ ssaoIntensity?: number; /** * SSAO sampling radius in world units (mm). Larger values spread the * occlusion over a wider area. Default: `20`. */ ssaoRadius?: number; /** * Bloom (glow) for bright metallic surfaces. Adds a soft halo around * polished rails and highlights, giving metals a more realistic * reflective appearance. Cost: one half-res pass per frame. * * Default: `false`. */ bloom?: boolean; /** * Bloom intensity (strength). Higher values produce brighter glow. * Range: 0.0–3.0. Default: `0.4`. */ bloomIntensity?: number; /** * Bloom luminance threshold. Only pixels brighter than this value * contribute to the bloom. Lower values bloom more of the scene. * Range: 0.0–1.0. Default: `0.85`. */ bloomThreshold?: number; /** * Bloom blur radius. Controls how far the glow spreads. * Range: 0.0–1.0. Default: `0.4`. */ bloomRadius?: number; } /** * Style + placement overrides for the per-mover-id 3D text labels * surfaced when `DisplayOptions.showMoverIds === true`. * * Defaults (all in scene units / mm; chosen for legibility on the * default mover GLB at typical XTS layouts): * - `fontSizeMm`: 15 * - `color`: '#000000' * - `offsetMm`: [0, 0, 55] // centred above the mover's magnet plate * - `fontUrl`: `DEFAULT_LABEL_FONT_URL` (Roboto Condensed) * - `outlineWidthMm`: 0 // no outline * - `outlineColor`: '#FFFFFF' */ interface MoverIdLabelOptions { /** Text height in mm (world units). Default: 15. */ fontSizeMm?: number; /** CSS color for the text fill. Default: '#000000'. */ color?: string; /** * Offset of the label's anchor point relative to the mover's local * origin (= magnet-plate center), in mm. * `[x, y, z]` — Z is up. Default: `[0, 0, 25]`. */ offsetMm?: Vec3; /** * URL of a TTF / OTF / WOFF font, passed straight to troika-three-text. * Default: `DEFAULT_LABEL_FONT_URL` (Roboto Condensed via jsDelivr). */ fontUrl?: string; /** Outline thickness in mm. 0 disables the outline. Default: 0. */ outlineWidthMm?: number; /** Outline color. Only applied if `outlineWidthMm > 0`. Default: '#FFFFFF'. */ outlineColor?: string; } interface StationOptions { thicknessMm?: number; /** Vertical Z lift (mm). Positive = above the table. */ displacementMm?: number; /** * Lateral Y displacement (mm) along the path's left-hand normal in the * XY plane. Mirrors the Dimensions overlay: each tick at centerline s mm * is rendered perpendicular to that point — values stay 1:1 with the * actual track. */ lateralDisplacementMm?: number; /** Text styling for station description + stop-position labels. */ textOptions?: TextOptions; /** * Render the StationConfig.description as a camera-facing billboard at * the centre of the station tube. Default: false. */ showStationDescription?: boolean; /** * Render each StopPosition's mm value as a billboard next to its stop * marker. Default: false. */ showStopPositionValues?: boolean; /** * Override the per-label color. Falls back to the per-station * `StationConfig.stationColor` (so colourful tubes keep matching their * labels by default). Set this when you want a single uniform label * colour across all stations regardless of tube colour. */ labelColorOverride?: string; } /** * Visual options for the stop-position ghost movers (rendered when * `display.showStopPositionMovers === true`). */ interface StopPositionMoverOptions { /** * Material opacity applied to the cloned mover GLB. Default 0.45 — * ghost-like without disappearing. Set 1 for fully opaque, 0 to hide. */ opacity?: number; /** * When set, applies an emissive tint matching the station colour * (`StationConfig.stationColor`). Default: `true` — ghosts adopt the * tube colour so different zones read as distinct. Set `false` for * a uniform pale ghost regardless of station colour. */ tintByStation?: boolean; } /** * Display options for `AreaConfig[]`. Mirror of `StationOptions` minus the * stop-position-related fields — Areas are pure range-overlays (tube + * optional description billboard). */ interface AreaOptions { /** Tube thickness in mm. Default: 8. */ thicknessMm?: number; /** Vertical Z lift (mm). Positive = above the table. Default: 60. */ displacementMm?: number; /** * Lateral Y displacement (mm) along the path's left-hand normal in the * XY plane. Each sample at centerline s is rendered perpendicular to that * point — values stay 1:1 with the actual track. */ lateralDisplacementMm?: number; /** Text styling for area description labels. */ textOptions?: TextOptions; /** * Render the AreaConfig.description as a camera-facing billboard at the * centre of the area tube. Default: true. */ showAreaDescription?: boolean; /** * Override the per-label color. Falls back to the per-area * `AreaConfig.color`. Set this when you want a single uniform label colour * across all areas regardless of tube colour. */ labelColorOverride?: string; /** * Tube opacity (0..1). Areas often cover long stretches of the track — * lowering opacity (e.g. 0.5) makes them feel like a translucent zone * marker rather than a solid pipe. Default: 1. */ opacity?: number; } /** * Marker shape palette. * * - Diamond : OctahedronGeometry, oriented with +X along path (existing) * - Tick : narrow BoxGeometry across the path (existing) * - Sphere : isotropic SphereGeometry (no orientation) * - Cone : ConeGeometry with apex up (+Z), base on path point * - Cube : cube oriented along the path tangent (chunkier than Tick) * - Cylinder : pillar standing perpendicular to the path (axis = +Z) * - None : not rendered */ type MarkerShape = 'Diamond' | 'Tick' | 'Sphere' | 'Cone' | 'Cube' | 'Cylinder' | 'None'; interface MarkerOptions { shape?: MarkerShape; sizeMm?: number; } interface DimensionOptions { thicknessMm?: number; /** * Vertical displacement (mm), added to the path centerline's Z when * sampling the dimension overlay. Positive = above the table. */ displacementMm?: number; /** * Lateral displacement (mm) along the path's left-hand normal in the XY * plane (+Y in path-frame). Lets you offset the dimension tube + ticks * sideways from the mover centerline. On curves the offset tube has a * different arc length than the centerline, but the displayed values * still match the centerline position — each tick at centerline s mm * is rendered perpendicular to that point. */ lateralDisplacementMm?: number; textOptions?: TextOptions; textPattern?: number; /** * Show numeric mm-value labels at every dimension tick (module boundaries * and — when enabled — intermediate steps). Labels are billboards that * always face the camera. Default: false. */ showValues?: boolean; /** * Place additional dimension ticks every N millimetres along the path, * on top of the per-module-boundary ticks. 0 disables intermediate * ticks (default). Typical values: 50, 100, 250. */ intermediateStepMm?: number; /** * Marker style + size for intermediate ticks. Defaults to a smaller * `Tick` marker (60% of the boundary tick size) so the intermediates * remain visually subordinate. */ intermediateMarkerOptions?: MarkerOptions; /** * Whether intermediate ticks also get value labels. Only applies when * `showValues` is true. Default: true. */ showIntermediateValues?: boolean; /** * What number to display next to each tick: * - 'partPosition' (default): cumulative mm from the part's start. * - 'fromModule' : mm from the start of the containing module. * - 'both' : " mm ( mm)". */ valueMode?: 'partPosition' | 'fromModule' | 'both'; } interface InfoBarOptions { defaultThicknessMm?: number; textOptions?: TextOptions; } interface TextOptions { sizeMm?: number; color?: string; fontFamily?: string; } interface StationConfig { stationId: number; description: string; isEnabled: boolean; partOids: number[]; startPositionOnPart: number; endPositionOnPart: number; /** ARGB int (same as 2D). */ stationColor: number; stopPositions?: number[]; /** * Reference frame for `stopPositions` values: * - `'track'` (default) — values are absolute partPositionMm from * track start. Same convention as * `startPositionOnPart` / `endPositionOnPart`. * - `'station'` — values are RELATIVE to `startPositionOnPart`. So * `0` puts a stop right at the station's start; * `100` puts one 100 mm into the station regardless * of where the station lives on the part. * * Useful when stops are conceptually tied to the station's geometry * (entry, mid, exit) rather than absolute mm marks on the track. */ stopPositionsRelativeTo?: 'track' | 'station'; /** * Per-station override of the stop-marker geometry. Falls back to * `display.stationMarkerOptions.shape` when omitted. Lets one station * render Diamonds while a neighbour uses Cones, etc. */ stopMarkerShape?: MarkerShape; /** Per-station override of the stop-marker size (mm). */ stopMarkerSizeMm?: number; } /** * Labelled region along the track. Identical to * `StationConfig`, but without stop positions — areas only carry text + * colour and describe a zone (e.g. "Cleanroom", "Manual Access", * "Safety Loop") that does not trigger machine stops. * * Multi-part: same semantics as `StationConfig.partOids`. With a single * partOid and `endPositionOnPart < startPositionOnPart`, a closed loop * is rendered (to the end of the track, then from 0 to End). */ interface AreaConfig { areaId: number; description: string; isEnabled: boolean; partOids: number[]; startPositionOnPart: number; endPositionOnPart: number; /** ARGB int (same as StationConfig). Used for tube + default label. */ color: number; } interface InfoBarConfig { processingUnitObjectId: number; partObjectId: number; partStartPositionMm: number; partEndPositionMm: number; thickness: number; displacement: number; color: { color: string; }; visible: boolean; text?: string; textPlacement?: 'Left' | 'Center' | 'Right'; textDisplacement?: number; textOptions?: TextOptions; markers?: InfoBarMarkerConfig[]; zIndex: number; } interface InfoBarMarkerConfig { positionMm: number; shape: 'Diamond' | 'Tick' | 'None'; sizeMm: number; color?: string; } interface ModuleRef { processingUnitObjectId: number; partObjectId: number; moduleIndex: number; } interface MoverRef { processingUnitObjectId: number; moverIndex: number; } interface SelectionState { modules: ModuleRef[]; movers: MoverRef[]; } type SelectionMode = 'Off' | 'Single' | 'Multi'; /** * Declarative module highlight. All modules whose `moduleType` appears in * `moduleTypes` are tinted with the given `color` — same emissive effect as * selection, but independent of the selection state. Multiple highlights can * overlap; the first matching entry wins. * * Toggle on/off by setting `moduleHighlights` on `` to an * array (enabled) or `undefined` / `[]` (disabled). */ interface ModuleHighlight { /** Module types to highlight. */ moduleTypes: ModuleType3D[]; /** Highlight color (CSS color string, e.g. `'#FF6600'`). */ color: string; } interface CameraState { positionMm: Vec3; targetMm: Vec3; /** Always 'Z' in the current spec. */ upAxis: 'Z' | 'Y'; zoom?: number; } /** * Live camera projection. * - `'perspective'` (default) — the regular 3D view. * - `'orthographic'` — paired with `topDown`, gives the flat 2D plan view * that matches `exportScreenshot({ mode: 'top-down' })`. */ type CameraProjection = 'perspective' | 'orthographic'; /** * Target for the imperative `viewerRef.current.focusOn(target, opts)` camera * animation. The viewer computes the target's world-space bounding box and * flies the camera so the whole object fits the frame: * - In `'perspective'` projection the current view angle is preserved * (the camera only dollies / re-centres). * - In `'orthographic'` top-down the frustum is re-framed and the camera * pans straight over the object. */ type FocusTarget = { kind: 'scene'; } | { kind: 'station'; stationId: number; } | { kind: 'area'; areaId: number; } | { kind: 'mover'; ref: MoverRef; } | { kind: 'module'; ref: ModuleRef; }; interface FocusOptions { /** Animation duration in ms. Default 700. `0` jumps immediately. */ durationMs?: number; /** * Padding around the target's bounding box (1 = tight fit). Default 1.2 in * perspective, 1.1 in orthographic top-down. */ paddingFactor?: number; /** Easing curve. Default `'easeInOutCubic'`. */ easing?: 'linear' | 'easeInOutCubic'; } type XtsViewerErrorCode = 'asset-load-failed' | 'unknown-module-type' | 'unknown-mover-type' | 'unknown-tool-type' | 'invalid-config' | 'unmatched-clothoid-half' | 'webgl-context-lost'; interface XtsViewerError { code: XtsViewerErrorCode; message: string; details?: unknown; } declare class XtsViewerErrorException extends Error { readonly code: XtsViewerErrorCode; readonly details?: unknown; constructor(code: XtsViewerErrorCode, message: string, details?: unknown); } interface AssetManifest { /** Override per moduleType. Falls back to default mapping otherwise. */ modules?: Partial>; movers?: Partial>; tools?: Partial>; guidingRails?: Partial>; } interface RailAssetEntry { glbUrl: string; sidecarUrl?: string; } interface ModuleAssetEntry { /** Per-RailSystem GLB URL (relative to assetsBaseUrl). */ glbByRailSystem: { Beckhoff: string | null; HepcoGfx?: string | null; }; sidecarUrl?: string; } interface MoverAssetEntry { glbUrl: string; sidecarUrl?: string; } interface MoverToolAssetEntry { glbUrl: string; sidecarUrl?: string; } interface XtsModelDocument { schemaVersion: 1; meta: { generatedAt: string; generator: string; sourceFormat?: string; sourceFileHash?: string; }; config: XtsConfig; runtime?: XtsRuntimeState; railSystem?: RailSystem | { byProcessingUnit: Record; }; } interface XtsRuntimeState { movers: Array<{ processingUnitObjectId: number; moverIndex: number; id: number; partOid: number; partPositionMm: number; velocity?: { mmPerSec: number; }; status?: { warning?: boolean; error?: boolean; }; }>; selection?: SelectionState; camera?: CameraState; display?: Partial; } /** * Module catalog (3D-canonical). * * Every module declares its path topology (Straight / Curve / Free), its * arc length, and — for curves — the signed end-of-module yaw delta in * degrees. Centerline radius for analytic curves is derived from * `R = L / |θ_rad|`. * * Hygienic ATH variants share path math with their AT pendants — only the * GLB mesh differs. * * This file is hand-curated. The geometry module imports from here directly * (no JSON fetch) so the library has zero runtime data deps. */ type PathType = 'Straight' | 'Curve' | 'Free'; interface ModuleCatalogEntry { moduleType: ModuleType3D; /** Arc length along the centerline, in mm. */ moduleLengthMm: number; pathType: PathType; /** * Encoder-side end-of-module yaw, in degrees, in the module-local 3D frame. * Sign convention: positive endAngle ⇒ Y-flip math curves toward * −Y (= right of path). 0 for Straight. 180 for AT2050_*_180 free path. */ endAngleDeg: number; /** * Centerline radius for analytic curves, in mm. `null` for Straight + Free. * Computed as R = L / |θ_rad|; equals 636.6198 for ±22.5° and 318.3099 for * ±45° curves at L=250. */ centerlineRadiusMm: number | null; /** * Free-path point cloud reference. Only set for `pathType==='Free'`. * The string identifies the cloud by name; the actual data lives in * geometry/PointClouds.ts. */ freePathId?: string; /** True if STP is not delivered yet. */ prepared?: boolean; /** True for Einspeisemodule (power feed-in variants). */ isInfeed?: boolean; } declare const MODULE_CATALOG: Readonly>; /** * Beckhoff XTS infeed module types (Einspeisemodule). Convenience constant * for use with `moduleHighlights` on ``. * * Derived from the catalog (`isInfeed` flag) so it never drifts out of sync — * mark a new module with `isInfeed: true` in `MODULE_CATALOG` and it is * automatically included here. */ declare const INFEED_MODULE_TYPES: readonly ModuleType3D[]; /** Lookup with type-narrowing. Returns undefined for unknown types. */ declare function getModuleEntry(moduleType: ModuleType3D): ModuleCatalogEntry | undefined; /** Type-guard for safe lookups from untyped (e.g. JSON) sources. */ declare function isKnownModuleType(value: string): value is ModuleType3D; /** * ChainBuilder — builds the per-module `startWorldMatrix` for a Part. * * Chain composition (simplified — modules butt up edge-to-edge in this build): * chain_world = Identity * for each module_i: * module_i.startWorldMatrix = chain_world * module_i.startPositionInPartMm = cumulative * module_i.trackLengthMm = module_i.moduleLengthMm * cumulative += trackLengthMm * end_local = T( ΔX, -ΔY_canvas, 0 ) ⊗ R_z( -θ ) * chain_world = chain_world ⊗ end_local * * `chain_world` is a planar pose: 2D translation (X,Y) + Z-yaw. We carry it as * a tiny struct rather than a full 4×4 to keep the pure-math layer free of * Three.js. A Three.js helper in PathBuilder.ts converts to Matrix4 on demand. */ /** Planar pose (X,Y,yaw) in a 3D-RH-Z-up frame. Z is always 0 here. */ interface PlanarPose { positionMm: Vec3; yawDeg: number; } declare const IDENTITY_POSE: PlanarPose; interface BuiltModule { moduleType: ModuleType3D; catalog: ModuleCatalogEntry; /** Pose of the module-local origin (= path entry). */ startWorldPose: PlanarPose; /** Distance along the part where this module starts (mm). */ startPositionInPartMm: number; /** moduleLength (mm). Used for findModuleContaining. */ trackLengthMm: number; /** Forwarded from `ModuleEntry.globalNumber` — index into the runtime * `ModuleStatusStore` so renderers can subscribe per module. */ globalNumber?: number; /** Forwarded from `ModuleEntry.status` so the renderer can light up * the per-module drive-status overlay without an extra lookup. */ status?: { warning?: boolean; error?: boolean; }; } interface BuiltChain { modules: BuiltModule[]; /** Σ moduleLength_i. */ trackLengthMm: number; /** End-of-chain pose; useful for closure-checks. */ endPose: PlanarPose; } /** * Build a chain from a part's module list. * * Throws XtsViewerErrorException with code `unknown-module-type` on lookup * miss. */ declare function buildChain(modules: ReadonlyArray): BuiltChain; /** * Locate the module containing `partPositionMm` along the chain. * First module with partPosition < startInPart + trackLength; at boundary * `partPosition == trackLength` the LAST module is returned with * `localPos = trackLength`. */ declare function findModuleAt(chain: BuiltChain, partPositionMm: number): { module: BuiltModule; localPosMm: number; } | null; /** * Mover world pose at `partPositionMm`. * * The module's startWorldPose is composed with the local path sample at * `localPosMm` along that module's path. */ declare function moverWorldAt(chain: BuiltChain, partPositionMm: number): PlanarPose | null; /** Convenience: chain from full ModuleEntry[] returning trackLength alone. */ declare function trackLengthOf(modules: ReadonlyArray): number; /** * Mover collision detection. * * Sub-millimetre accurate for the dominant XTS case: two movers travelling * along the same chain. Each mover occupies a 1D footprint of * `pathLengthMm` centred on `partPositionMm`, so the collision predicate * collapses to one subtraction in path-space: * * centerDistance = arc-length |pos_a − pos_b| * requiredGap = (pathLength_a + pathLength_b) / 2 * penetration = requiredGap − centerDistance * collision iff penetration > 0 (≥ 0 for "touching") * * For closed-loop chains (oval tracks, U-turn pairs) the arc-length is * taken as the *minimum* of the forward and the wrap-around path so two * movers near the seam (e.g. 50 mm and trackLength − 50 mm) are treated * as physically adjacent. Open chains are detected by comparing the * chain's start- and end-pose; for those the wrap-around distance is * simply not considered. * * Floating-point precision: positions are kept in mm in float64. A typical * XTS layout has trackLength ≤ 20 m → < 2×10⁴ mm, well inside the * 2⁵³-mantissa range, so subtractive cancellation costs at most ~10⁻¹² * mm — orders of magnitude below sub-millimetre. * * Multi-XPU (different chains): not handled here. Cross-track collisions * are dominated by track-transform layout and would need an OBB / mesh * test in 3D space; that's a different concern from "two movers on the * same line about to crash". */ /** * One detected pair. Result list is symmetric (a/b is reported once per * pair, with `a` carrying the lower index pair-canonical). */ interface MoverCollision { a: MoverRef; b: MoverRef; /** * Stable IDs of the two movers (mirrors `MoverConfig.id`). Useful when * the consumer keys by id rather than by `moverIndex`. */ idA: number; idB: number; /** * Penetration along the 1D path: * • `> 0` — movers overlap by this many mm. * • `= 0` — touching exactly. * • `< 0` — closest-approach gap (only present in result when * `warningGapMm` is set; the magnitude is the free-space * distance between the closer footprint edges). */ penetrationMm: number; /** * Both `partPositionMm` values at the time of the test. Lets the * consumer recompute or visualise without re-querying. */ positionAMm: number; positionBMm: number; /** Mover lengths (catalog `pathLengthMm`). */ pathLengthAMm: number; pathLengthBMm: number; /** True when measurement wrapped around the chain end (closed-loop seam). */ viaWraparound: boolean; } interface CheckMoverCollisionsOptions { /** * When `> 0`, near-misses with closest-approach gap up to this many mm * are also returned (with `penetrationMm < 0`). Default 0 — only actual * collisions / contacts. */ warningGapMm?: number; /** * Force-treat the chain as closed (consider wrap-around distance) or * open (don't). When omitted, auto-detected by comparing * `chain.endPose` to `startPose ≈ identity` within ~0.1 mm tolerance. */ forceClosedLoop?: boolean; } /** * Effective mover state passed into the predicate. Decoupled from * MoverConfig so consumers can plug in live positions from the * MoverPositionStore (60-Hz tick) without round-tripping through React * state. */ interface MoverProbe { ref: MoverRef; id: number; partOid: number; moverType: MoverType3D; partPositionMm: number; /** * Override the catalog `pathLengthMm`. Use for `Custom` movers where * the layout's `pathLengthMm` is the source of truth. */ pathLengthMm?: number; } /** * Pair-wise 1D collision test for movers on the same chain. * * `chain` is the BuiltChain corresponding to the partOid all probed * movers travel on. `probes` may contain movers from *any* part — this * function only considers pairs where `partOid` matches the chain. * * Returns one entry per colliding (or near-miss, when `warningGapMm > 0`) * pair, sorted by `penetrationMm` descending so the deepest collisions * appear first. */ declare function checkSamePathCollisions(chain: BuiltChain, partOid: number, probes: ReadonlyArray, opts?: CheckMoverCollisionsOptions): MoverCollision[]; /** * Module collision / overlap detection. * * The mover collision (moverCollision.ts) is a 1D arc-length test for two * movers on the *same* chain; its header explicitly declines cross-XPU / * cross-track cases, which "would need an OBB / mesh test in 3D space". * This file is exactly that test, for the physical track *modules*. * * Each module is modelled as a chain of oriented bounding boxes (OBBs) that * follow its real centerline: the path is sampled (sampleModulePath) and each * segment becomes a small box (±halfWidth in Y, 0..height in Z) oriented along * the local tangent, then transformed into world space by * * world = orientation ⊗ trackTransform ⊗ partTransformation ⊗ chainPose * * Sampling the path (rather than a single straight box per module) is what * keeps curves and U-turns faithful — a single straight box would poke * outside the arc and produce both false positives and false negatives on a * closed oval. Straights collapse to one box; curves / free paths use several. * * Pairs are tested with the Separating Axis Theorem (15 axes) over their * segment boxes, yielding a penetration depth (minimum translation distance) * as `overlapMm`: * * overlapMm > 0 — boxes overlap by this many mm (deepest sub-box pair). * overlapMm = 0 — touching exactly. * overlapMm < 0 — closest-approach gap (only reported when * `warningGapMm` is set). * * Adjacent modules in the same part butt up edge-to-edge (ChainBuilder), so * consecutive `(i, i+1)` pairs — and the `(N-1, 0)` seam of a closed loop — * are excluded; everything else (cross-part, cross-XPU, same-part * non-adjacent) is tested. * * The box cross-section (`halfWidthMm` 50, `heightMm` 100) is a coarse but * deterministic, asset-independent envelope. Real per-module CAD bounds live * in the GLB sidecars (`approximateBoundsMm`) and could refine it later. * Likewise the `warningGapMm` near-miss is an axis-projected SAT separation, * not a true Euclidean (GJK) distance — slightly conservative for * corner-to-corner cases. */ /** Default box half-width (Y) — matches `approximateModuleBounds`. */ declare const DEFAULT_MODULE_HALF_WIDTH_MM = 50; /** Default box height (Z) — matches `approximateModuleBounds`. */ declare const DEFAULT_MODULE_HEIGHT_MM = 100; /** * One detected pair. Symmetric: `a`/`b` are canonicalised so the lower * `(processingUnitObjectId, partObjectId, moduleIndex)` tuple is always `a`. */ interface ModuleCollision { a: ModuleRef; b: ModuleRef; moduleTypeA: ModuleType3D; moduleTypeB: ModuleType3D; /** Forwarded `ModuleEntry.globalNumber` (status-store key), if present. */ globalNumberA?: number; globalNumberB?: number; /** * Penetration depth along the minimum-translation axis of the deepest * sub-box pair: * • `> 0` — overlap (mm). * • `= 0` — touching. * • `< 0` — closest-approach gap (only when `warningGapMm` is set). */ overlapMm: number; /** Centre-to-centre distance of the deepest sub-box pair (mm). */ centerDistanceMm: number; /** Unit world-space axis of least penetration (the MTV direction). */ axis: Vec3; } interface CheckModuleCollisionsOptions { /** * When `> 0`, near-misses with an axis-projected gap up to this many mm * are also returned (with `overlapMm < 0`). Default 0 — only actual * overlaps / contacts. */ warningGapMm?: number; /** * Also test non-adjacent module pairs within the SAME part. Default true. * Consecutive `(i, i+1)` pairs and the closed-loop `(N-1, 0)` seam are * ALWAYS excluded regardless of this flag. */ includeSamePart?: boolean; /** Box half-width in Y (mm). Default 50. */ halfWidthMm?: number; /** Box height in Z (mm). Default 100. */ heightMm?: number; } /** Oriented bounding box in world space (unit axes + scaled half-extents). */ interface Obb { center: Vector3; axes: [Vector3, Vector3, Vector3]; halfExtents: [number, number, number]; /** Bounding-sphere radius (broad-phase reject). */ radius: number; } /** Pre-built per-module probe consumed by `checkModuleCollisions`. */ interface ModuleProbe { ref: ModuleRef; moduleType: ModuleType3D; globalNumber?: number; /** Adjacency grouping key (part.globalNumber — system-wide unique). */ partKey: number; /** Index of this module within its part's chain. */ moduleIndex: number; /** Segment boxes following the module centerline (≥ 1). */ obbs: Obb[]; /** Module-level bounding sphere enclosing all segment boxes. */ sphereCenter: Vector3; sphereRadius: number; } /** A part fed to `buildModuleProbes`. */ interface ModuleCollisionPartInput { /** ModuleRef.partObjectId (`PartConfig.objectId`). */ partObjectId: number; /** Adjacency key (`PartConfig.globalNumber`). */ partKey: number; partTransformation?: PartTransformation | null; chain: BuiltChain; } /** An XPU fed to `buildModuleProbes`. */ interface ModuleCollisionXpuInput { xpuObjectId: number; /** Effective (override-resolved) per-XPU track transform. */ trackTransform?: TrackTransform | null; parts: ReadonlyArray; } /** * Turn a list of XPUs (with effective track transforms) + parts (with built * chains) into flat module probes plus a per-part adjacency map. Shared by * the imperative ref and the reactive monitor so the world-matrix * composition lives in one place. * * Hidden XPUs / parts (`visible === false`) are skipped to match the scene. */ declare function buildModuleProbes(orientation: Orientation | undefined, xpus: ReadonlyArray, opts?: CheckModuleCollisionsOptions): { probes: ModuleProbe[]; adjacency: Map; }; /** * Pairwise OBB overlap test over all module probes. Same-part consecutive * and closed-loop-seam pairs are excluded; everything else is tested. * Returns one entry per overlapping (or near-miss, when `warningGapMm > 0`) * pair, sorted deepest-overlap-first. */ declare function checkModuleCollisions(probes: ReadonlyArray, adjacency: ReadonlyMap, opts?: CheckModuleCollisionsOptions): ModuleCollision[]; /** * normalizeXtsConfig — pre-render pass. * * Fail-fast validation + AT2050 half-clothoid merging: * AT2050_0500 + AT2050_0501 (gap=0) → AT2050_0500_180 * ATH2050_0500 + ATH2050_0501 (gap=0) → ATH2050_0500_180 * * Errors thrown via XtsViewerErrorException: * - 'unknown-module-type' * - 'unknown-mover-type' * - 'unmatched-clothoid-half' (0500 without 0501 or vice versa) * * Warnings (collected, non-fatal) returned alongside the normalized config: * - mover.partOid not in part-list * - duplicate mover.id within an XPU * - mover.partPositionMm out of range (clamped) */ interface NormalizedXtsConfig extends XtsConfig { /** * Same shape as the input, with all parts' modules pre-merged + validated. * Mover positions are clamped, but the original values are preserved in * `__warnings` for diagnostics. */ readonly __warnings: NormalizationWarning[]; } interface NormalizationWarning { 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'; message: string; details?: unknown; } /** * Stateful normalizer with a single-slot structural cache. The key * intentionally excludes `mover.partPositionMm` so that frame-paced * position updates fall into the fast path: when the structure (modules, * parts, transforms, mover ids/partOids) hasn't changed between calls, * we return a clone of the cached result with only the mover positions * re-clamped against the current input. * * Construct your own instance when you need an isolated cache (e.g. in * tests, or when multiple viewers in the same JS realm would otherwise * thrash a shared single-slot cache). The standalone `normalizeXtsConfig` * function delegates to a process-wide singleton for backwards * compatibility. */ declare class XtsConfigNormalizer { private _cache; normalize(config: XtsConfig): NormalizedXtsConfig; clearCache(): void; } declare function normalizeXtsConfig(config: XtsConfig): NormalizedXtsConfig; /** * captureScreenshot — render the live r3f scene into an offscreen * WebGLRenderTarget at any resolution, in any of three camera modes: * * • 'current' — clone the live perspective camera at its current pose * (image aspect adjusted to the requested width/height). * • 'top-down' — orthographic camera placed above the scene's AABB * centre, looking straight down -Z, with up = world +Y. * Frustum sized to fit the AABB plus padding. Mirrors * the 2D viewer convention (path along image X, * encoder-side along image Y). * • 'custom' — caller supplies a CameraState (positionMm, targetMm). * Lets you reproduce a saved shot exactly. * * Why an offscreen target instead of `gl.domElement.toBlob()`: * - Live canvas is created with `preserveDrawingBuffer: false` for * perf — `toBlob` would race with the next frame's clear. * - The screenshot resolution decouples from the live canvas DPR; * export 4K PNGs even from a 1× display. * - Switching the camera doesn't require disturbing OrbitControls. * * Result: * { blob, widthPx, heightPx, camera, boundingBoxMm, mode } * * Caller composes the URL via URL.createObjectURL() or downloads * directly with an trick. The blob mime is * `image/png` by default; jpeg / webp also supported with `quality`. * * Caveats: * - Assumes the scene has finished loading (GLBs in cache). When * called during a Suspense fallback, only what's currently in the * scene graph gets rendered. * - The renderer's pixel ratio, clear colour, and current render * target are saved + restored, so live rendering is not disturbed. */ type ScreenshotMode = 'current' | 'top-down' | 'custom'; type ScreenshotFormat = 'png' | 'jpeg' | 'webp'; interface ScreenshotOptions { /** Camera mode. Default: `'current'`. */ mode?: ScreenshotMode; /** Required when `mode === 'custom'`. Ignored otherwise. */ camera?: CameraState; /** * Image dimensions in pixels. * - Both supplied: the rendered frustum (top-down) is adjusted to * match the resulting aspect; the perspective camera (current / * custom) gets its `aspect` set to `width / height`. * - One supplied: the other is derived to keep the scene's aspect * ratio (top-down) or the live canvas aspect (current / custom). * - Neither: canvas CSS dimensions × `pixelRatio`. */ width?: number; height?: number; format?: ScreenshotFormat; /** 0..1 — JPEG / WebP quality. Default 0.92. Ignored for PNG. */ quality?: number; /** * Padding factor around the scene AABB in `top-down` mode. Default * 1.1 (10 % margin on each side). Higher = more whitespace. */ paddingFactor?: number; /** * Background colour: * - `null` (default) → transparent PNG, matches the live canvas * contract. JPEG falls back to white because the format has no * alpha channel. * - `'#RRGGBB'` / any three.js ColorRepresentation → solid fill. */ backgroundColor?: ColorRepresentation | null; /** * Pixel ratio for offscreen renders. Default = renderer's current * pixel ratio (matches the live canvas sharpness on high-DPI * displays). Set explicitly for fixed-resolution exports. */ pixelRatio?: number; /** * Multiplier applied to every Light in the scene during the offscreen * render. Default 1.35 — screenshots tend to feel dim compared to * the live canvas because the live tone-mapping pipeline picks up * the surrounding monitor luminance, which the offscreen render * doesn't. Pass 1 to disable, > 1 for a brighter result. Restored * after capture. */ exposureBoost?: number; } interface ScreenshotResult { blob: Blob; widthPx: number; heightPx: number; /** Effective camera state used for the render. Useful to log + reproduce. */ camera: CameraState; /** Scene AABB at capture time. Useful to overlay annotations later. */ boundingBoxMm: { min: Vec3; max: Vec3; }; mode: ScreenshotMode; } /** * Imperative ref API for . * * Encapsulates everything the host can call through `viewerRef.current.*`: * camera + zoomToFit, getMoverWorldTransform, exportModel, exportScreenshot, * mover-position channel writes, and the on-demand collision query. * * Pulled out of XtsViewer3D so the top-level component stays a thin * `` shell instead of a ~700-line handle definition. */ interface XtsViewer3DRef { zoomToFit(paddingFactor?: number): void; /** * Re-fit the orthographic top-down frustum to the current scene AABB. * No-op unless the live camera is orthographic (`projection='orthographic'`). */ frameTopDown(paddingFactor?: number): void; /** * Animate the camera so the given target (station / area / mover / module / * the whole scene) fits the frame. In perspective the current view angle is * preserved; in orthographic top-down the frustum is re-framed and the * camera pans straight over the target. */ focusOn(target: FocusTarget, opts?: FocusOptions): void; setCamera(cam: CameraState): void; getCamera(): CameraState | null; setOrientation(orientation: XtsConfig['orientation']): void; getMoverWorldTransform(ref: MoverRef): { positionMm: [number, number, number]; rotationDegEuler: [number, number, number]; } | null; getBoundingBox(): { min: [number, number, number]; max: [number, number, number]; }; getConfig(): XtsConfig; getRuntimeState(): XtsRuntimeState; exportModel(opts?: { includeRuntime?: boolean; }): XtsModelDocument; exportScreenshot(opts?: ScreenshotOptions): Promise; reloadAssets(): Promise; setMoverPosition(moverId: number, partPositionMm: number): void; /** * Push mover positions in bulk. Two accepted forms: * * 1. `Record` — backwards-compatible single- * part-per-mover update. The mover stays on its config-time part. * 2. `MoverPositionEntry[]` — array indexed by `MoverConfig.index` * (entry `i` targets the mover with `index === i`); each entry * supplies the `partObjectId` (= `PartConfig.objectId`) the mover is * currently on plus its position. `null` / `undefined` entries are * skipped — leave them untouched in the store. */ setMoverPositions(update: Record | ReadonlyArray): void; getMoverPosition(moverId: number): number | null; /** * Push live drive-status (warning / error) onto motor modules. The * `statuses` array is indexed by `ModuleEntry.globalNumber` — i.e. * `statuses[m.globalNumber] === { warning, error }`. `null` clears a * slot; `undefined` leaves it unchanged. */ setModuleStatuses(statuses: ReadonlyArray): void; /** Clear all live module statuses (revert to config-time `ModuleEntry.status`). */ clearModuleStatuses(): void; checkMoverCollisions(opts?: CheckMoverCollisionsOptions): MoverCollision[]; /** * On-demand check for physically overlapping track MODULES across the * whole layout — different XPUs / parts placed via `trackTransform` / * `orientation` / `partTransformation`, plus non-adjacent modules of the * same part. Adjacent (seam) modules and the closed-loop seam are * excluded. Override-aware: reflects any `trackTransform` set imperatively * via the `trackTransformOverrides` prop. Returns the overlapping pairs * sorted deepest-overlap-first. */ checkModuleCollisions(opts?: CheckModuleCollisionsOptions): ModuleCollision[]; } /** * SidecarLoader — fetches per-asset JSON sidecars. * * Sidecars carry origin-correction (translateMm + rotationDegEuler) plus * path metadata. They live next to the GLB at /.meta.json (or * .tool.meta.json for mover tools). * * On 404 the loader falls back to default origin-correction `(0,0,0)` / * `(0,0,0)`. The renderer can still place the GLB; the calibration tool is * the authoritative source for sidecar values. */ interface OriginCorrection { translateMm: Vec3; rotationDegEuler: Vec3; } interface ModuleSidecar { moduleType: ModuleType3D; glbByRailSystem: { Beckhoff: string | null; HepcoGfx?: string | null; }; originCorrection: OriginCorrection; pathType: 'Straight' | 'Curve' | 'Free'; moduleLengthMm: number; endAngleDeg: number; freePathFile?: string; approximateBoundsMm?: { min: Vec3; max: Vec3; }; } interface MoverSidecar { moverType: MoverType3D; glb: string; originCorrection: OriginCorrection; magnetPlateCenterMm: Vec3; pathLengthMm: number; } interface MoverToolSidecar { toolType: MoverToolType3D; glbUrl: string; originCorrection: OriginCorrection; defaultOffsetMm: Vec3; approximateBoundsMm?: { min: Vec3; max: Vec3; }; label: string; } /** * Calibration sidecar for a guiding-rail GLB. The rail is rendered in the * same module-local frame as the module (i.e. under the module's * `startWorldPose`) with this correction applied to the GLB scene. */ interface RailSidecar { railType: RailType3D; glb: string; originCorrection: OriginCorrection; approximateBoundsMm?: { min: Vec3; max: Vec3; }; } declare const DEFAULT_ORIGIN_CORRECTION: OriginCorrection; /** * Generic sidecar fetch with promise caching + graceful fallback. Returns * `undefined` on 404 (caller substitutes a default), and surfaces network or * parse errors as `Error` (caller is expected to forward to onError). */ declare class SidecarLoader { private readonly cache; fetchModule(url: string): Promise; fetchMover(url: string): Promise; fetchTool(url: string): Promise; fetchRail(url: string): Promise; clear(): void; private fetchJson; } /** * Resolve the effective origin correction for a module sidecar — falls back * to identity when the sidecar is absent. */ declare function resolveOriginCorrection(sidecar: { originCorrection?: OriginCorrection; } | undefined): OriginCorrection; /** Standard sidecar filename from a base id. */ declare function moduleSidecarFilename(moduleType: ModuleType3D): string; declare function moverSidecarFilename(moverType: MoverType3D): string; declare function toolSidecarFilename(toolType: MoverToolType3D): string; declare function railSidecarFilename(railType: RailType3D): string; /** * Pure helper: pick the GLB filename for a moduleSidecar with rail-system * fallback. Returns null when the sidecar marks the module 'prepared but * not delivered'. */ declare function pickGlbForRail(sidecar: ModuleSidecar | undefined, rail: RailSystem): string | null; /** * HepcoGfxRailProfile — cross-section of the Hepco GFX guiding rail used by * the procedural extrusion in . * * The profile is expressed in the Beckhoff path-frame convention: * - path tangent = +X (extrusion direction) * - lateral = ±Y (left/right of the path) * - vertical = ±Z (Z-up, path origin at Z=0) * * THREE.ExtrudeGeometry maps a Shape's local X to the curve's Frenet normal * and Shape's local Y to the binormal. For a planar curve in the world XY * plane the initial Frenet frame computed by three.js is: * normal = (0, 0, -1) -> shape.x maps to world -Z * binormal = lateral in XY -> shape.y maps to world ±Y * To keep this module's public dimensions in natural "Z-up" coordinates, * the Shape factory negates Z when emitting shape.x. Callers should think * in (lateral Y, vertical Z); the negation is an implementation detail. * * The Hepco GFX cross-section is TWO disconnected regions: * * 1. Baseplate — a wide flat plate at the bottom (134.45 mm × 30.1 mm). * One per track; the two plates of an XTS oval sit side-by-side and * together span the full ~268.9 mm system width. * * 2. Rail — a separate spear-profile bar (40 mm × 20 mm) sitting on top * of the baseplate at its +Y edge. Has horizontally pointed apexes on * both lateral sides; the mover's V-rollers engage these apexes. * * Two shape factories are exported so the consumer can extrude each region * as its own mesh; merging them into a single Shape would require a thin * connecting sliver that misrepresents the real geometry. * * The `stackYOffsetMm` / `stackZOffsetMm` fields shift both regions * together — used by the playground calibration UI to fine-tune position * relative to the path origin without changing the inherent geometry. */ interface HepcoGfxRailProfileDims { /** Baseplate Y-extent per track (mm). Half of the XTS oval's full * cross-section width. */ readonly baseplateWidthMm: number; /** Baseplate Z-extent (mm). */ readonly baseplateThicknessMm: number; /** Z-coordinate of the baseplate's top face in the path frame (before * stackZOffsetMm is applied). Also the Z of the rail's bottom face. */ readonly baseplateTopZMm: number; /** Rail Y-extent, spear-apex to spear-apex (mm). */ readonly railWidthMm: number; /** Rail Z-extent, main-body top to main-body bottom (mm). */ readonly railHeightMm: number; /** Z-coordinate of the rail's main-body top in the path frame (before * stackZOffsetMm is applied). */ readonly railTopZMm: number; /** Lateral protrusion of each spear apex beyond the rail's main body (mm). */ readonly spearProtrusionMm: number; /** Half-height of the spear at its inner (main-body-side) edge (mm) — * measured from the apex Z toward both top and bottom shoulders. */ readonly spearBaseHalfHeightMm: number; /** Concave-undercut inset on the rail's bottom face just inboard of * each spear (mm) — the rail's bottom edge is shorter than its top * edge by 2× this value. */ readonly undercutInsetMm: number; /** Calibration shift in Y applied to baseplate + rail together (mm). */ readonly stackYOffsetMm: number; /** Calibration shift in Z applied to baseplate + rail together (mm). */ readonly stackZOffsetMm: number; } /** * Scaffolding dimensions (mm). The stack offsets bake in the user- * requested calibration so the rendered geometry is correct out of the * box; the playground / a `*.meta.json` sidecar can override them. */ declare const HEPCO_GFX_PROFILE: HepcoGfxRailProfileDims; /** * Merge a partial profile override onto the default profile. Used by the * playground / sidecar loader so callers only need to supply the fields * they want to override. */ declare function resolveHepcoGfxProfile(override?: Partial): HepcoGfxRailProfileDims; /** * Build the baseplate cross-section (mm). Simple rectangle positioned so * the rail's +Y spear apex sits flush with the baseplate's +Y edge; the * baseplate extends in -Y direction from there. Both axes are shifted by * `stackYOffsetMm` / `stackZOffsetMm` before being emitted. */ declare function createHepcoGfxBaseplateShape(dims?: HepcoGfxRailProfileDims): Shape; /** * Build the rail cross-section (mm). Symmetric spear-profile bar centered * on Y=0 (before stackYOffsetMm). The mover's V-rollers grip the two * spear apexes (at Y=±railWidth/2, Z=midZ). * * The profile's top edge is flush with the spear shoulder top — there is * NO rectangular section above the spear shoulders. `railHeightMm` therefore * spans only from the main-body bottom up to the spear shoulder top. */ declare function createHepcoGfxRailShape(dims?: HepcoGfxRailProfileDims): Shape; /** * — Top-level React component. * * Thin wrapper around the r3f : * - useNormalizedConfig: pre-render validate + clamp pass * - useSelectionState: controlled-or-internal selection + click handlers * - useXtsViewerHandle: imperative ref API (camera, screenshots, etc.) * - : everything inside the canvas (lights, modules, * movers, overlays) * * Pan/Zoom = OrbitControls defaults; Z-up; 1 unit = 1 mm. */ /** * Live overrides for module/mover/tool origin-correction sidecars. Used by * the calibration tool to preview adjustments without writing files. */ interface SidecarOverrides { modules?: Partial>; movers?: Partial>; tools?: Partial>; rails?: Partial>; } interface XtsViewer3DProps { /** Complete XTS configuration. */ config: XtsConfig; /** * Base URL for GLB assets. * * Default: `JSDELIVR_ASSETS_BASE_URL`, which points to * `https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d-assets@/models` * — the sister package with all GLBs, version-pinned to the * viewer version. */ assetsBaseUrl?: string; /** * Source of the sidecar calibration data. * * • `true` (default) — sidecars come from the bundled * `BUILTIN_*_SIDECARS` constants in the JS library. * • `false` — each sidecar is fetched live from * `/.meta.json`. */ useBuiltinSidecars?: boolean; /** Override of the module / mover asset manifest (for tests, alternative skins). */ assetManifest?: AssetManifest; /** Display options. */ display?: DisplayOptions; /** Initial camera. */ initialCamera?: CameraState; /** * Live camera projection. Default `'perspective'`. * * `'orthographic'` (together with `topDown`, on by default) renders the flat * 2D plan view — straight down +Z, up = +Y — pixel-consistent with * `exportScreenshot({ mode: 'top-down' })`. Switching at runtime does NOT * recreate the WebGL context. In orthographic top-down, rotation is * auto-disabled (pan + zoom stay on) unless you set `lock.rotate` explicitly. */ projection?: CameraProjection; /** * When `projection === 'orthographic'`, frame the scene straight from above. * Default `true`. (Only `true` is meaningful today; reserved for future * orthographic side/front views.) */ topDown?: boolean; /** Lock interactions. */ lock?: { rotate?: boolean; pan?: boolean; zoom?: boolean; selection?: boolean; }; /** Selection-Mode. Default: 'Off'. */ selectionMode?: SelectionMode; /** Controlled selection. */ selection?: SelectionState; /** Selection-Highlight-Farben. */ selectionColors?: { module?: string; mover?: string; }; /** * Declarative module highlights — tint all modules matching the given * types with the specified color. Works independently of selection: * selection takes visual priority over highlights when both are active. * * Toggle on/off by setting to an array (enabled) or `undefined` / `[]` * (disabled). Multiple entries are supported; the first matching entry * per module type wins. * * @example * ```tsx * import { INFEED_MODULE_TYPES } from 'beckhoff-xts-viewer-3d'; * * * ``` */ moduleHighlights?: ModuleHighlight[]; /** * Convenience boolean to highlight all infeed modules (Einspeisemodule) * without assembling a `moduleHighlights` entry yourself. Internally maps * to a `moduleHighlights` entry over `INFEED_MODULE_TYPES`. * * Combines with `moduleHighlights`: explicit entries keep priority on * overlapping module types (first matching entry wins). * * @example * ```tsx * * ``` */ highlightInfeedModules?: boolean; /** * Color used when `highlightInfeedModules` is enabled. Ignored otherwise. * Default: `'#FF6600'`. */ infeedHighlightColor?: string; /** Callbacks. */ onSelectionChange?: (s: SelectionState) => void; onCameraChange?: (c: CameraState) => void; onMoverClick?: (m: MoverRef) => void; onModuleClick?: (m: ModuleRef) => void; onMoverHover?: (m: MoverRef | null) => void; onModuleHover?: (m: ModuleRef | null) => void; onError?: (err: XtsViewerError) => void; onAssetsLoaded?: () => void; /** Performance-Tuning. */ performance?: { maxFps?: number; instancing?: boolean; autoPauseOnHidden?: boolean; /** * Enable demand rendering: frames only render when something changes * (camera move, mover position update, selection). Saves >90% GPU * time when the scene is idle. Default: true. */ demandRendering?: boolean; /** * Cap the device pixel ratio. Values above 2 rarely improve * perceptible quality but double fill-rate cost on Retina/4K * displays. Default: 2. */ maxDpr?: number; /** * Disable automatic per-frame shadow map updates. Shadows only * re-render on camera change. Default: true (when shadows enabled). */ shadowOnDemand?: boolean; /** * Opt into the WebGPU renderer when the browser supports it. * Falls back to WebGL automatically. Default: false (experimental). */ webgpu?: boolean; /** * Override the KTX2 (Basis) transcoder path used to decode compressed * textures in the GLB assets. Defaults to a CDN copy pinned to the * bundled three.js revision. Point this at a self-hosted * `basis_transcoder.{js,wasm}` directory for offline / air-gapped use. */ ktx2TranscoderUrl?: string; }; /** * Live override of the per-asset origin-correction sidecars. Useful for * the calibration tool. */ sidecarOverrides?: SidecarOverrides; /** * Live override of per-XPU `trackTransform`. Keyed by `xpu.index`. */ trackTransformOverrides?: Record; /** * Live override of the procedural Hepco-GFX rail profile dimensions. * Only the fields you supply are merged onto the defaults — useful for * the playground calibration UI / a sidecar-style `*.meta.json`. */ hepcoGfxProfile?: Partial; /** CAD-style ViewCube overlay. */ viewCube?: { enabled?: boolean; alignment?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center' | 'center-left' | 'center-right' | 'center-center'; marginPx?: [number, number]; }; /** Stator heatmap data — coloured tube along each part's centerline. */ statorHeatmap?: StatorHeatmap; /** Continuous mover collision monitoring. */ collisionDetection?: { enabled?: boolean; warningGapMm?: number; intervalMs?: number; forceClosedLoop?: boolean; onCollisionsChange?: (collisions: MoverCollision[]) => void; }; /** * Static MODULE overlap monitoring. Detects track modules that physically * overlap in 3D space — different XPUs / parts placed via `trackTransform` * / `orientation` / `partTransformation`, plus non-adjacent modules of the * same part. Adjacent (seam) modules and the closed-loop seam are * excluded. Recomputed on config / track-transform change (NOT per-frame, * since modules don't move at 60 Hz) — `onCollisionsChange` fires only * when the overlap set actually changes. */ moduleCollisionDetection?: { enabled?: boolean; warningGapMm?: number; onCollisionsChange?: (collisions: ModuleCollision[]) => void; }; /** Dev/Debug. */ debug?: { showPathSplines?: boolean; showBoundingBoxes?: boolean; showOriginAxes?: boolean; showModuleLabels?: boolean; overlayFps?: boolean; showGrid?: boolean; }; className?: string; style?: React__default.CSSProperties; } declare const XtsViewer3D: React__default.ForwardRefExoticComponent>; /** * Mover catalog (3D-canonical). * * Magnet-plate-center-Mm is the canonical attachment point on the path * centerline (after originCorrection of the GLB). */ interface MoverCatalogEntry { moverType: MoverType3D; /** Mover length in travel direction, mm. */ pathLengthMm: number; /** Mover height in CAD Y-direction, mm (front side / encoder). */ heightMm: number; /** * Magnet plate center in mover-local 3D frame (after originCorrection). * +X = travel direction, +Z = up, Y = left of path. */ magnetPlateCenterMm: Vec3; } declare const MOVER_CATALOG: Readonly>; declare function getMoverEntry(moverType: MoverType3D): MoverCatalogEntry | undefined; declare function isKnownMoverType(value: string): value is MoverType3D; /** * Module-local path math. * * Pure functions, no Three.js dependency. Returns position + tangent + yaw * in the module-local 3D frame after originCorrection (centerline on Z=0). * * Coordinate conventions: * - Y-flip: 3D-Y = -2D-canvas-Y, so positive endAngle (2D-positive) * curves toward −Y in 3D. * - 1 unit = 1 mm. */ interface PathSample { /** Position in module-local 3D frame, mm. */ positionMm: Vec3; /** Unit tangent in module-local 3D frame. */ tangent: Vec3; /** Yaw around +Z in degrees (0 at module start). */ yawDeg: number; } /** * Sample a module's centerline at arc length s (mm). * `s` is clamped to `[0, moduleLengthMm]`. */ declare function sampleModulePath(module: ModuleCatalogEntry, sMm: number): PathSample; /** * End-of-module local transform: * Δ_local = T( ΔX, -ΔY_canvas, 0 ) ⊗ R_z( -θ ) * * We return the position delta and yaw delta separately so callers can compose * them with chain.startWorldMatrix as they prefer (matrix-based or pose-based). */ interface EndDelta { /** End-of-module position relative to startWorldMatrix, in mm. */ deltaPositionMm: Vec3; /** End-of-module yaw delta in degrees (sign-flipped per Y-flip). */ deltaYawDeg: number; } declare function moduleEndDelta(module: ModuleCatalogEntry): EndDelta; /** * XtsArcCurve3 — Three.js Curve3 wrapper around the analytic Curve sample. * * Each module exposes a THREE.Curve that the part-level CurvePath * chains together. For analytic Curves we just wrap the math from * pathMath.ts; for Free paths see XtsPointCloudCurve3. * * Note: CurvePath delegates to subcurve length-proportional, so `getLength()` * MUST return the actual arc length so subcurves are weighted correctly. * Our subcurves are arc-length parametrised (t = s / L), so this just-works. */ declare class XtsArcCurve3 extends Curve { readonly module: ModuleCatalogEntry; constructor(module: ModuleCatalogEntry); getPoint(t: number, optionalTarget?: Vector3): Vector3; getTangent(t: number, optionalTarget?: Vector3): Vector3; getLength(): number; } /** * XtsPointCloudCurve3 — Three.js Curve3 wrapper around the Free-path sample. * * The off-by-one clamp at s == ModuleLength is handled in * pathMath.sampleModulePath. */ declare class XtsPointCloudCurve3 extends Curve { readonly module: ModuleCatalogEntry; constructor(module: ModuleCatalogEntry); getPoint(t: number, optionalTarget?: Vector3): Vector3; getTangent(t: number, optionalTarget?: Vector3): Vector3; getLength(): number; } /** * PathBuilder — assemble a `THREE.CurvePath` for a Part. * * One CurvePath per part, chaining all module curves end-to-end. * Per module: * - Straight → THREE.LineCurve3 (module geometry baked in) * - Curve → XtsArcCurve3 (analytic) * - Free → XtsPointCloudCurve3 * * Each subcurve here is expressed in **module-local coordinates** (origin at * module entry). The world transform of the module = chain.startWorldPose * (planar) is applied at render time by the `` group node. * * Helper `chainPoseToMatrix4` converts a planar pose to a Three.js Matrix4 * (Z-up world). */ interface PartPath { chain: BuiltChain; /** A CurvePath chaining all modules (in module-local frames per subcurve). */ curvePath: CurvePath; } /** * Build a Part path. Returns the BuiltChain plus a CurvePath for visual * debug splines (continuity overlay). The CurvePath subcurves live in * MODULE-LOCAL coordinates; the consumer is responsible for placing each * module under a parent group with `startWorldPose` applied. */ declare function buildPartPath(modules: ReadonlyArray): PartPath; /** * Convert a planar pose (X,Y,yaw around +Z) to a Three.js Matrix4 * (Z-up world). */ declare function poseToMatrix4(pose: PlanarPose, out?: Matrix4): Matrix4; /** * Point-cloud registry for Free-path modules. * * The AT2050 / ATH2050 180° U-turn is sampled at Δs = 0.1 mm with 5001 * points covering [0..500] mm of arc length. The data ships as JSON files * co-located with this module under ./data/ — they're imported via * JSON-resolution so consumers don't have to fetch them at runtime. * * Y-axis convention in the JSON is 2D-canvas-down (positive = "into the * curve"). XtsPointCloudCurve3 applies the Y-flip when projecting into the * 3D RH-Z-up frame. * * Build note: `tsc` does not emit imported .json files. A postbuild copy * step (scripts/copy-runtime-data.mjs) mirrors src/geometry/data/*.json * into dist/geometry/data/ so the relative imports in the compiled output * resolve against the published tarball. */ interface PointCloud { /** Total arc length in mm (= (N-1) × samplingMm). */ lengthMm: number; /** Constant arc-length spacing between samples, in mm (always 0.1 here). */ samplingMm: number; /** [x_i, y_i] in mm; canvas Y-down convention. */ points: ReadonlyArray; } declare function getPointCloud(id: string): PointCloud | undefined; declare function registerPointCloud(id: string, cloud: PointCloud): void; /** * Remove a previously-registered cloud. Returns `true` when an entry was * removed, `false` when the id was not registered. Useful for test cleanup * and hot-reload scenarios where a calibration tool re-registers clouds. */ declare function unregisterPointCloud(id: string): boolean; /** * positionFrame — translate user-frame partPosition values into the * chain's intrinsic frame. * * Every `partPositionMm`-style value the host writes (mover position, * station start/end/stops, area start/end) is interpreted in the user's * frame and run through `userToChainPositionMm` before being fed to * `moverWorldAt` / `sampleChainPoint` / `sampleChainRange`. The opposite * direction (`chainToUserPositionMm`) lets the imperative * `getMoverPosition` API and the screenshot metadata report values back * in the host's frame. * * The transform is its own inverse when applied with the same frame, so * round-tripping through `chainToUser(userToChain(x))` always returns x. */ /** `chainPos = originMm + (direction === 'negative' ? -userPos : userPos)` */ declare function userToChainPositionMm(userPos: number, frame?: PositionFrame): number; /** Inverse of `userToChainPositionMm`. */ declare function chainToUserPositionMm(chainPos: number, frame?: PositionFrame): number; /** * Resolve a `StationConfig.stopPositions[i]` to an absolute (chain-frame) * partPosition value. * * Order of operations: * 1. If `stopPositionsRelativeTo === 'station'`, the value is treated * as an offset from the station's `startPositionOnPart` first. * 2. The resulting value is then passed through `userToChainPositionMm` * using the part's XPU `positionFrame` (if any). * * The user can therefore say "second stop is 100 mm into station 'Pickup', * regardless of where Pickup starts" while still benefiting from the * track's overall direction / origin convention. */ declare function resolveStopPositionMm(stopValue: number, station: StationConfig, frame?: PositionFrame): number; /** * Heatmap value interpolation along a 1D path. * * Pure numeric helper used by `` to compute the value * at any partPositionMm along the chain, given the consumer's sample * points. Behaviour: * * - Empty samples → `null`. * - Single sample → that sample's value at every position. * - Position before first sample → first sample's value (clamped). * - Position after last sample → last sample's value (clamped). * - Otherwise → linear interpolation between the two bracketing * samples. * * Samples must be sorted by `positionMm` ascending — the helper does * not sort defensively. Use `sortHeatmapSamples` once when ingesting * data. */ interface HeatmapSample { positionMm: number; value: number; } declare function sortHeatmapSamples(samples: ReadonlyArray): HeatmapSample[]; /** * Return the linearly interpolated value at `positionMm`. Returns * `null` when the sample list is empty. */ declare function interpolateHeatmapValue(samples: ReadonlyArray, positionMm: number): number | null; /** * Map a value into [0..1] across the [min..max] window, clamped at the * edges. Used to feed the gradient lerp. */ declare function normaliseToHeatmapRange(value: number, min: number, max: number): number; /** * Shared closed-loop detection for built chains. * * A closed loop (oval track, U-turn pair) returns to the origin pose after * one full revolution: end-pose translation ≈ 0 and accumulated yaw ≈ a * multiple of 360°. Both the mover collision (wrap-around seam on the same * chain) and the module collision (excluding the first/last module seam) * need the identical predicate — keeping it in one place avoids the two * tolerances silently drifting apart. */ /** * True when `chain` forms a closed loop — its end pose coincides with the * identity start pose within sub-mm / sub-tenth-degree tolerance. */ declare function isChainClosed(chain: BuiltChain): boolean; /** * Beckhoff XTS coordinate-convention transform. * * The Beckhoff XTS `OriginTransform` parameter uses a Y-axis sign that is * opposite to the viewer's right-handed Z-up world frame. When * `coordinateSystem === 'beckhoff-xts'`, this module negates: * * - `PartTransformation.offsetMm[1]` (Y translation) * - `PartTransformation.rotationDegEuler[2]` (Z-Euler rotation) * - `TrackTransform.positionMm[1]` (Y translation) * - `TrackTransform.rotationDegEuler[2]` (Z-Euler rotation) * * Applied as a post-normalization pass so every downstream consumer * (rendering, imperative API, bounding-box) sees the corrected values. */ declare function applyBeckhoffXtsConvention(config: NormalizedXtsConfig): NormalizedXtsConfig; /** * Path-sample helpers — convert a chain + a [startMm, endMm] range into a * world-frame polyline of Vector3s. Used by Stations, Dimensions and * InfoBars to build TubeGeometries that follow the path. */ /** * Sample a chain in part-local coords from startMm to endMm at approx. * `stepMm` spacing. Returns at least 2 points; clamps to [0, trackLength]. * * Offsets: * - `liftZ` : vertical displacement (mm), added to Z. Used for tubes * that should sit above/below the path centerline. * - `lateralMm` : lateral displacement (mm) along the path's left-hand * normal in the XY plane (+Y in path-frame). Lets you * render a parallel offset tube that, on curves, faithfully * tracks the centerline arc-by-arc — i.e. each sample at * centerline position `s` becomes the perpendicular point * at the same `s`, so labels still match centerline mm. */ declare function sampleChainRange(chain: BuiltChain, startMm: number, endMm: number, stepMm?: number, liftZ?: number, lateralMm?: number): Vector3[]; /** * Sample at a single position; returns part-local position + tangent. * Used by markers (Diamond / Tick) to orient themselves along the path. * * `liftZ` and `lateralMm` semantics match sampleChainRange — see there. */ declare function sampleChainPoint(chain: BuiltChain, positionMm: number, liftZ?: number, lateralMm?: number): { position: Vector3; tangent: Vector3; } | null; /** * SelectionManager — pure state container for the viewer's selection. * * The XtsViewer3D owns a SelectionManager instance internally and exposes * selection updates through the controlled `selection` prop and the * `onSelectionChange` callback. * * Selection modes: * - Off → ignore clicks * - Single → click replaces selection with one item * - Multi → ctrl/meta-click adds, shift-click range-selects (modules * within the same part only) * * Range-select is module-only. For movers, shift-click behaves as a * plain replacing click — there is no natural "range" along the mover * list. Use ctrl/meta for additive mover selection. */ type ClickModifiers = { ctrl?: boolean; shift?: boolean; meta?: boolean; }; declare const EMPTY_SELECTION: SelectionState; declare function isModuleSelected(selection: SelectionState, ref: ModuleRef): boolean; declare function isMoverSelected(selection: SelectionState, ref: MoverRef): boolean; declare function deselectAll(): SelectionState; declare function applyModuleClick(selection: SelectionState, ref: ModuleRef, mode: SelectionMode, modifiers?: ClickModifiers): SelectionState; declare function applyMoverClick(selection: SelectionState, ref: MoverRef, mode: SelectionMode, modifiers?: ClickModifiers): SelectionState; declare function applyEmptyClick(selection: SelectionState, mode: SelectionMode): SelectionState; /** * — 8 red spheres at the GLB's CAD-frame AABB corners. * * Calibration helper: when two adjacent modules are placed correctly in the * chain (C0/C1 continuity), the corner sphere on Module N's exit face * overlaps the matching corner sphere on Module N+1's entry face. A * visible gap or overlap signals a bad origin-correction (or a bad chain * transform — but the latter is unit-tested). * * Lives inside the same group as the GLB , so the markers move * along with the GLB through every transform (origin-correction, chain * start-world, layout-mode, orientation). * * `depthTest=false` keeps them visible even when sitting inside the stator * mesh — important for the U-turn module where the markers would otherwise * be hidden by the curved cover. */ interface Props { /** GLB-frame AABB. Pass the bounds BEFORE origin correction. */ bounds: { min: Vec3; max: Vec3; }; sizeMm?: number; color?: string; } declare const ModuleCornerMarkers: React__default.FC; /** * — camera-facing text used by the Dimensions overlay * (and re-usable elsewhere). Wraps drei's `` + `` so the * label always reads horizontally regardless of camera orbit, and is * placed at a small +Z offset above the dimension tick so the text and * the marker don't z-fight. */ /** * Default 3D-text font: Roboto Condensed (Latin, regular weight) served * from jsDelivr's @fontsource mirror. Pinned to a fixed major version so * the stack stays reproducible; the URL responds with a long * Cache-Control TTL so repeated reads come from the browser cache. * * `font` on troika-three-text expects an OTF / TTF / WOFF URL — NOT a CSS * font-family name. Consumers can override via `` to swap in a brand font. */ declare const DEFAULT_LABEL_FONT_URL: "https://cdn.jsdelivr.net/npm/@fontsource/roboto-condensed@5.2.7/files/roboto-condensed-latin-400-normal.woff"; /** * — Camera-facing 3D text label that shows a mover's * `id` above the mover GLB. Mounted by `` when * `DisplayOptions.showMoverIds === true`. * * Renders via drei's `` + `` (troika-three-text under * the hood) so the label stays readable as the camera orbits. All * styling is driven by `MoverIdLabelOptions`; the component supplies * the documented defaults (15 mm black text, 55 mm above the mover's * magnet-plate origin, Roboto Condensed via jsDelivr). */ interface MoverIdLabelProps { /** The mover's `id` — rendered verbatim. */ text: string; options?: MoverIdLabelOptions; } declare const MoverIdLabel: React__default.FC; /** * AUTO-GENERATED — do not edit by hand. * * Source: public/models/*.meta.json * Generator: scripts/bundle-sidecars.mjs * * Run `npm run assets:bundle-sidecars` to regenerate after editing a * sidecar in public/models/. Bundled sidecars ship inside the npm package * so the viewer renders correctly without a matching CDN mirror. * * Module count: 27 * Mover count: 4 * Tool count: 2 * Rail count: 8 */ /** * Built-in module sidecars. Generated from public/models/*.meta.json. * Identity-corrected entries are intentionally retained so the lookup * still resolves to a sidecar object (consumers can rely on truthy * results without a 404 fallback dance). */ declare const BUILTIN_MODULE_SIDECARS: Readonly>>; /** Built-in mover sidecars. Generated from public/models/*.meta.json. */ declare const BUILTIN_MOVER_SIDECARS: Readonly>>; /** Built-in tool sidecars. Generated from public/models/*.tool.meta.json. */ declare const BUILTIN_TOOL_SIDECARS: Readonly>>; /** Built-in rail sidecars. Generated from public/models/*.rail.meta.json. */ declare const BUILTIN_RAIL_SIDECARS: Readonly>>; /** * SidecarSourceContext — toggle between compiled-in sidecars and live HTTP * fetches. * * Default behaviour (`useBuiltins: true`) reads `BUILTIN_*_SIDECARS` from * `builtinSidecars.ts` synchronously — zero round-trips, calibration data * version-locked to the JS that consumes it. That's right for production. * * Setting `useBuiltins: false` (via the `useBuiltinSidecars` prop on * ``) makes every sidecar hook fall through to the HTTP fetch * against `/.meta.json`. That's the dev-loop mode used * by the playground / calibration tool: edit a JSON file in * `public/models/`, refresh, see the change, no bundle regeneration. */ interface SidecarSourceConfig { /** * When true (default), the sidecar hooks return the compiled-in * BUILTIN_*_SIDECARS entry without an HTTP fetch. When false, every * hook always fetches from `/.meta.json` so live * edits to those JSON files are picked up on reload. */ useBuiltins: boolean; } declare const SidecarSourceContext: React.Context; declare function useSidecarSource(): SidecarSourceConfig; /** * Default asset manifest (UPPERCASE-hyphen filenames). * * URLs are relative to `assetsBaseUrl` (default `/models`). The viewer can * override entries via the `assetManifest` prop (e.g. for tests, alternative * skins, or to plug in user GLBs). * * Hygienic ATH modules carry per-RailSystem URLs. Standard AT/Eco modules * only set `Beckhoff`; the loader silently falls back to that variant when * `HepcoGfx` is requested but unavailable. * * `null` for `Beckhoff` marks "prepared but not delivered" * (AT2042 / AT2140) — the loader renders a placeholder AABB mesh. */ /** Filename mapping per ModuleType3D (relative to assetsBaseUrl). */ declare const DEFAULT_MODULE_MANIFEST: Readonly>; declare const DEFAULT_MOVER_MANIFEST: Readonly>; declare const DEFAULT_TOOL_MANIFEST: Readonly>; /** * Beckhoff guiding-rail GLBs (the outer rail profile that the mover wheels * engage). Rendered per module when `RailSystem === 'Beckhoff'`; * Hepco GFX uses a different profile and is handled separately. */ declare const DEFAULT_RAIL_MANIFEST: Readonly>; /** * ModuleType → guiding-rail mapping. * * Picks the rail GLB by curve sign + arc length: * - Straights pick the matching-length AT9000 rail (0249 / 0250 / 0500). * - +22.5° curves (AT2020 / AT2021) use AT9020. * - −22.5° curves (AT2025 / AT2026) use AT9025. * - 45° curves (AT2040 / AT2041 / AT2042 / AT2140) use AT9040. * - The 180° clothoid pair (AT2050 0500/0501 + merged 0500_180) uses * AT9050, whose GLB already covers the full 500 mm path. * - Hygienic ATH modules already include the rail in their module GLB * and therefore map to `null` (no extra rail mesh). */ declare const MODULE_GUIDING_RAIL_MAP: Readonly>>; /** Resolve a module's GLB URL with manifest override + rail-system fallback. */ declare function resolveModuleGlbUrl(moduleType: ModuleType3D, rail: RailSystem, override?: AssetManifest['modules']): string | null; declare function resolveMoverGlbUrl(moverType: MoverType3D, override?: AssetManifest['movers']): string; declare function resolveToolGlbUrl(toolType: MoverToolType3D, override?: AssetManifest['tools']): string; /** * Pick the guiding-rail RailType for a module, or `null` if no separate * rail mesh should render (ATH variants, undeclared module types). */ declare function resolveGuidingRailType(moduleType: ModuleType3D): RailType3D | null; /** * Resolve the GLB filename for a guiding-rail type, honoring user-supplied * manifest overrides. Returns empty string when the rail is unknown. */ declare function resolveGuidingRailGlbUrl(railType: RailType3D, override?: AssetManifest['guidingRails']): string; /** * configureGltfLoaders — wire compression decoders onto a GLTFLoader. * * The viewer ships Meshopt-compressed geometry (EXT_meshopt_compression) * and KTX2/Basis-compressed textures (KHR_texture_basisu). Neither decodes * unless the GLTFLoader has the matching decoder attached, so this module * centralises that wiring for both load paths: * * - the render hot-path via drei's `useGLTF` (see `useGltfClone.ts`), and * - the public preload API (`AssetLoader`). * * Design: pure, framework-free functions so the wiring is unit-testable with * fake loader / renderer objects (node has no WebGL2, so KTX2 transcoding * itself is exercised in the browser — see docs/performance-analysis.md). * * Meshopt geometry decodes headless (no GPU needed) and is therefore always * attached. KTX2 transcoding needs the live `WebGLRenderer` to detect the * GPU's supported compressed-texture formats, so it is only attached when a * renderer is provided. A loader configured this way still loads plain, * uncompressed GLBs unchanged — attaching decoders is backward-compatible. */ interface ConfigureLoaderOptions { /** * The live WebGLRenderer. Required for KTX2 textures — the loader must * detect which GPU-compressed formats the device supports. When omitted * (SSR / node tests), only Meshopt geometry decoding is wired up. */ gl?: WebGLRenderer | null; /** Override the KTX2 transcoder (basis) path for this call. */ transcoderUrl?: string; } /** * AssetLoader — promise-cached GLB loader for the viewer. * * Performance strategy: exactly one GLB is loaded per module / mover type, * then `clone()` is called per instance. The loader is keyed by absolute URL * (concat of assetsBaseUrl + entry filename), and concurrent loads of the * same URL share a single in-flight Promise. * * Asset failure: a 404 (or any fetch error) surfaces as * XtsViewerErrorException(code='asset-load-failed'). The renderer is * expected to fall back to a wireframe-AABB placeholder. * * Concurrent-cancel: when `cancel()` is called for a URL, future awaiters * receive an AbortError. The cache entry is cleared so a fresh `load()` * triggers a new fetch. */ declare class AssetLoader { private readonly cache; private readonly loader; /** * Wire compression decoders (Meshopt always; KTX2 when a renderer is * supplied) onto the underlying GLTFLoader so Meshopt/KTX2-compressed GLBs * parse. Safe to call repeatedly and before/after loads — it only attaches * decoders and does not touch the cache. Without `gl`, only Meshopt * geometry is wired (KTX2 needs the renderer for GPU-format detection). */ configure(options?: ConfigureLoaderOptions & { gl?: WebGLRenderer | null; }): void; /** * Load a GLB. Repeated calls for the same URL return the cached Promise. * Returns the parsed GLTF object — the consumer is responsible for * cloning the scene per instance. */ load(url: string): Promise; /** * Pre-load multiple URLs in parallel. Resolves once all complete, even * if some failed (failed entries appear as `null`). */ preload(urls: ReadonlyArray): Promise>; /** Cancel an in-flight load and remove the cache entry. */ cancel(url: string): void; /** Clear all cache entries. In-flight loads are NOT aborted. */ clear(): void; /** Returns true if a load is in progress / completed for this URL. */ has(url: string): boolean; private fetchAndParse; } declare function getSharedAssetLoader(): AssetLoader; /** Compose a URL from a base + a filename, normalising trailing slashes. */ declare function composeAssetUrl(baseUrl: string, filename: string): string; export { type AreaConfig, type AreaOptions, AssetLoader, type AssetManifest, BUILTIN_MODULE_SIDECARS, BUILTIN_MOVER_SIDECARS, BUILTIN_RAIL_SIDECARS, BUILTIN_TOOL_SIDECARS, type BuiltChain, type BuiltModule, type CameraProjection, type CameraState, type CheckModuleCollisionsOptions, type CheckMoverCollisionsOptions, type ClickModifiers, type CoordinateSystem, type CustomAssetBinding, type CustomAssetConfig, type CustomMoverLayout, DEFAULT_LABEL_FONT_URL, DEFAULT_MODULE_HALF_WIDTH_MM, DEFAULT_MODULE_HEIGHT_MM, DEFAULT_MODULE_MANIFEST, DEFAULT_MOVER_MANIFEST, DEFAULT_ORIGIN_CORRECTION, DEFAULT_RAIL_MANIFEST, DEFAULT_TOOL_MANIFEST, type DimensionOptions, type DisplayOptions, EMPTY_SELECTION, type EndDelta, type FocusOptions, type FocusTarget, HEPCO_GFX_PROFILE, type HeatmapSample, type HepcoGfxRailProfileDims, IDENTITY_POSE, INFEED_MODULE_TYPES, type InfoBarConfig, type InfoBarMarkerConfig, type InfoBarOptions, JSDELIVR_ASSETS_BASE_URL, MODULE_CATALOG, MODULE_GUIDING_RAIL_MAP, MOVER_CATALOG, type MarkerOptions, type MarkerShape, type ModuleAssetEntry, type ModuleCatalogEntry, type ModuleCollision, type ModuleCollisionPartInput, type ModuleCollisionXpuInput, ModuleCornerMarkers, type ModuleEntry, type ModuleHighlight, type ModuleProbe, type ModuleRef, type ModuleSidecar, type ModuleStatusEntry, type ModuleType3D, type MoverAssetEntry, type MoverCatalogEntry, type MoverCollision, type MoverConfig, MoverIdLabel, type MoverIdLabelOptions, type MoverPositionEntry, type MoverProbe, type MoverRef, type MoverSidecar, type MoverToolAssetEntry, type MoverToolConfig, type MoverToolSidecar, type MoverToolType3D, type MoverType3D, type NormalizationWarning, type NormalizedXtsConfig, type Orientation, type OriginCorrection, type PartConfig, type PartPath, type PartTransformation, type PathSample, type PathType, type PlanarPose, type PointCloud, type PositionFrame, type ProcessingUnitConfig, type RailAssetEntry, type RailSidecar, type RailSystem, type RailType3D, type ScreenshotFormat, type ScreenshotMode, type ScreenshotOptions, type ScreenshotResult, type SelectionMode, type SelectionState, SidecarLoader, type SidecarOverrides, type SidecarSourceConfig, SidecarSourceContext, type StationConfig, type StationOptions, type StatorHeatmap, type StopPositionMoverOptions, type TextOptions, type TrackTransform, VERSION, type Vec2, type Vec3, XtsArcCurve3, type XtsConfig, XtsConfigNormalizer, type XtsModelDocument, XtsPointCloudCurve3, type XtsRuntimeState, XtsViewer3D, type XtsViewer3DProps, type XtsViewer3DRef, type XtsViewerError, type XtsViewerErrorCode, XtsViewerErrorException, applyBeckhoffXtsConvention, applyEmptyClick, applyModuleClick, applyMoverClick, buildChain, buildModuleProbes, buildPartPath, chainToUserPositionMm, checkModuleCollisions, checkSamePathCollisions, composeAssetUrl, createHepcoGfxBaseplateShape, createHepcoGfxRailShape, deselectAll, findModuleAt, getModuleEntry, getMoverEntry, getPointCloud, getSharedAssetLoader, interpolateHeatmapValue, isChainClosed, isKnownModuleType, isKnownMoverType, isModuleSelected, isMoverSelected, moduleEndDelta, moduleSidecarFilename, moverSidecarFilename, moverWorldAt, normaliseToHeatmapRange, normalizeXtsConfig, pickGlbForRail, poseToMatrix4, railSidecarFilename, registerPointCloud, resolveGuidingRailGlbUrl, resolveGuidingRailType, resolveHepcoGfxProfile, resolveModuleGlbUrl, resolveMoverGlbUrl, resolveOriginCorrection, resolveStopPositionMm, resolveToolGlbUrl, sampleChainPoint, sampleChainRange, sampleModulePath, sortHeatmapSamples, toolSidecarFilename, trackLengthOf, unregisterPointCloud, useSidecarSource, userToChainPositionMm };