beckhoff-xts-viewer-3d 4.9.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,21 +3,45 @@
3
3
  [![npm](https://img.shields.io/npm/v/beckhoff-xts-viewer-3d.svg)](https://www.npmjs.com/package/beckhoff-xts-viewer-3d)
4
4
  [![npm assets](https://img.shields.io/npm/v/beckhoff-xts-viewer-3d-assets.svg?label=assets)](https://www.npmjs.com/package/beckhoff-xts-viewer-3d-assets)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
- [![Release](https://github.com/philippleidig/beckhoff-xts-viewer-3d/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/philippleidig/beckhoff-xts-viewer-3d/actions/workflows/release.yml)
6
+ [![CI](https://github.com/philippleidig/beckhoff-xts-viewer-3d/actions/workflows/ci.yml/badge.svg)](https://github.com/philippleidig/beckhoff-xts-viewer-3d/actions/workflows/ci.yml)
7
7
 
8
- A reusable React component that renders **Beckhoff XTS** linear-motor systems
9
- (plus Hepco GFX rail variants) in 3D. Drop a single `<XtsViewer3D>` into a
10
- React app, hand it a config that describes your modules + movers + tools, and
11
- the viewer takes care of the path math, GLB loading, mover animation,
12
- selection, calibration, multi-track placement, **PBR-realistic lighting**,
13
- soft shadows, and a CAD-style ViewCube.
8
+ A React component that renders Beckhoff XTS linear-motor systems — and Hepco
9
+ GFX rail variants in 3D. It takes a declarative description of modules,
10
+ movers and tools and handles the path math, GLB loading, mover animation,
11
+ selection, lighting and camera work.
14
12
 
15
- Functionally mirrors the official 2D `Beckhoff.TwinCAT.HMI.XTS.Controls`
16
- viewer but with full 3D, real CAD geometry, free orientation, and a clean
17
- declarative API.
13
+ It covers the same ground as the official 2D `Beckhoff.TwinCAT.HMI.XTS.Controls`
14
+ viewer, using real CAD geometry in three dimensions.
18
15
 
19
16
  ![Oval loop demo](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/01-oval-loop.png)
20
17
 
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install beckhoff-xts-viewer-3d
22
+ npm install react react-dom three
23
+ ```
24
+
25
+ | Requirement | Version | Why |
26
+ | ------------------------------------------------ | ------------------------ | -------------------------------------------------- |
27
+ | `react` / `react-dom` | `^19.0.0` | required by `@react-three/fiber` 9 |
28
+ | `three` | `>= 0.181.0` | the viewer uses the console-hook API added in r181 |
29
+ | `@react-three/fiber` | `>= 9.0.0` | |
30
+ | `@react-three/drei` | `>= 10.0.0` | |
31
+ | `three-stdlib` | `>= 2.36.0` | GLTF, KTX2 and Meshopt loaders |
32
+ | `postprocessing` + `@react-three/postprocessing` | `>= 6.39.2` / `>= 3.0.0` | optional — only needed for SSAO and bloom |
33
+ | Node | `>= 20` | build tooling only; the runtime is browser ESM |
34
+
35
+ The package is `"type": "module"` and ships ESM and CJS from
36
+ `./dist/index.{js,cjs,d.ts}`. `sideEffects: false`, so unused exports
37
+ tree-shake out.
38
+
39
+ Any modern bundler works (Vite, Webpack, Next.js, Remix, Astro). If your
40
+ bundler installs more than one copy of `three`, deduplicate it — see
41
+ [Troubleshooting](#troubleshooting).
42
+
43
+ ## Minimal example
44
+
21
45
  ```tsx
22
46
  import { XtsViewer3D } from 'beckhoff-xts-viewer-3d';
23
47
 
@@ -43,542 +67,81 @@ import { XtsViewer3D } from 'beckhoff-xts-viewer-3d';
43
67
  ],
44
68
  },
45
69
  ],
46
- movers: [
47
- { index: 0, id: 0, partOid: 0, partPositionMm: 200 },
48
- ],
70
+ movers: [{ index: 0, id: 0, partOid: 0, partPositionMm: 200 }],
49
71
  },
50
72
  ],
51
73
  }}
52
74
  />;
53
75
  ```
54
76
 
55
- That's it. No asset hosting required GLBs stream from the matching
56
- `beckhoff-xts-viewer-3d-assets` release on jsDelivr. PBR
57
- reflections, ACES tone mapping and anisotropic textures are on by default.
58
-
59
- ---
60
-
61
- ## Table of contents
62
-
63
- - [Highlights](#highlights)
64
- - [Gallery](#gallery)
65
- - [Installation](#installation)
66
- - [Getting started](#getting-started)
67
- - [GLB assets — zero-config by default](#1-glb-assets--zero-config-by-default)
68
- - [Build a config](#2-build-a-config)
69
- - [Drive mover positions](#3-drive-mover-positions)
70
- - [Read selection + errors](#4-read-selection--errors)
71
- - [Detect mover collisions](#5-detect-mover-collisions)
72
- - [Capture screenshots](#6-capture-screenshots)
73
- - [Live 2D plan view (orthographic top-down)](#7-live-2d-plan-view-orthographic-top-down)
74
- - [Fly the camera to an object (focusOn)](#8-fly-the-camera-to-an-object-focuson)
75
- - [Mark zones with Areas](#9-mark-zones-with-areas)
76
- - [Stator heatmap](#10-stator-heatmap)
77
- - [Track direction + zero offset](#11-track-direction--zero-offset)
78
- - [Realism + performance](#realism--performance)
79
- - [Troubleshooting](#troubleshooting)
80
- - [Documentation](#documentation)
81
- - [Development setup](#development-setup)
82
- - [Releasing](#releasing)
83
- - [License](#license)
84
-
85
- ---
86
-
87
- ## Highlights
88
-
89
- - **Every module + mover variant** in the Beckhoff catalogue — Standard AT,
90
- Eco AT2200, NCT (AT2002 / AT2102 + AT8200 tools), Hygienic ATH, plus Hepco
91
- GFX2-1TC-S25. Drop in a STP file, wire the type — done.
92
- - **Path math 1:1 with the 2D reference** — straights, ±22.5° / ±45° curves,
93
- AT2050 / ATH2050 180° clothoid kehres. Module-to-module C0/C1 continuity
94
- guaranteed by golden fixtures.
95
- - **PBR-realistic rendering** — ACES filmic tone mapping, image-based
96
- lighting via a procedural indoor environment, anisotropic textures, soft
97
- PCF shadows. Zero asset fetches; the environment map is built on-device
98
- from three.js's `RoomEnvironment`. All tunable via `display.*` props or
99
- off-by-default for direct-lighting parity.
100
- - **Mover animation via imperative ref** — `viewerRef.current.setMoverPositions(...)`
101
- bypasses React reconciliation entirely (useFrame + Three.js scene-graph),
102
- so a 60-Hz drive loop costs zero React renders.
103
- - **Selection + drive status** — click to select modules / movers; the GLB
104
- itself tints / blinks (no wireframe overlays), and a small modern 3D status
105
- icon (warning ▲ / error ⊙) floats above affected objects.
106
- - **Multi-track** — per-XPU `trackTransform` (position + rotation +
107
- uniform scale) so independent XTS lines can sit side-by-side in one scene.
108
- - **Live calibration overrides** — push origin-correction edits to module /
109
- mover / tool sidecars in real time without touching files.
110
- - **Stations, Areas, Dimensions, InfoBars** — full 2D feature parity, plus
111
- camera-facing mm-value labels, intermediate ticks, and a 7-shape stop-marker
112
- palette (Diamond / Tick / Sphere / Cone / Cube / Cylinder / None) settable
113
- per-station. **Areas** are stop-position-free zone overlays (cleanroom,
114
- safety loop, manual access) — text + colour, multi-part.
115
- Stop-position values can be track-relative (default) or station-relative.
116
- - **Stop-position ghost movers** — `display.showStopPositionMovers` renders a
117
- static, semi-transparent mover GLB at every active stop, tinted to the
118
- station colour by default. Useful for layout reviews ("where will the
119
- mover end up").
120
- - **Mover collision detection** — sub-millimetre 1D arc-length test on the
121
- shared chain. One-shot via `viewerRef.current.checkMoverCollisions()` or
122
- continuous via `<XtsViewer3D collisionDetection={{ enabled, onCollisionsChange }} />`.
123
- Closed-loop seam handled automatically.
124
- - **Stator heatmap overlay** — coloured tube along each part's centerline
125
- with vertex colours linearly interpolated across consumer-supplied
126
- `(positionMm, value)` samples. Default green → red gradient; configurable
127
- min / max colours, thickness, opacity, lateral / vertical offset.
128
- - **Screenshots** — `viewerRef.current.exportScreenshot({ mode })` renders to
129
- an offscreen target at any resolution. `'current'` captures the live camera,
130
- `'top-down'` produces a 2D-viewer-style overhead AABB-fit, `'custom'`
131
- reproduces a saved `CameraState`. Returns a Blob plus camera state +
132
- bounding box for reproducible exports.
133
- - **Track position frame** — per-XPU `positionFrame: { direction, originMm }`
134
- remaps every `partPositionMm`-style value (movers, stations, areas, stops,
135
- ghosts, world transforms) into the host's coordinate convention. Reverse
136
- direction or shift zero without editing any other field.
137
- - **Custom assets** — static, mover-bound, all-movers; opacity + scale
138
- per-instance, never leaks back into the source GLB.
139
- - **CAD ViewCube** — opt-in, snap to standard orthogonal views.
140
- - **Live 2D plan view** — `projection="orthographic"` flips the live canvas to
141
- a flat top-down view, pixel-consistent with the `'top-down'` screenshot, with
142
- no WebGL-context remount and rotation auto-locked.
143
- - **Animated focus** — `viewerRef.current.focusOn({ kind: 'station' | 'area' |
144
- 'mover' | 'module' | 'scene', ... })` flies the camera so the whole target
145
- fits the frame, in both 3D and 2D.
146
- - **Imperative ref API** — `zoomToFit` / `frameTopDown` / `focusOn` /
147
- `setCamera` / `getCamera` /
148
- `getMoverWorldTransform` / `getBoundingBox` / `exportModel` /
149
- `exportScreenshot` / `setMoverPosition(s)` (Record or
150
- `MoverPositionEntry[]` indexed by `MoverConfig.index`) / `getMoverPosition` /
151
- `setModuleStatuses` / `clearModuleStatuses` / `checkMoverCollisions` /
152
- `reloadAssets`.
153
- - **Designed for scale** — the `⚡ Perf stress` demo runs three ovals × 250
154
- movers each (= 750 simultaneously animated movers) without React commits
155
- during the steady state.
156
-
157
- ---
158
-
159
- ## Gallery
160
-
161
- | | |
162
- |---|---|
163
- | ![Multi-track](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/02-multi-track.png)<br>**Multi-track placement** — two independent XTS lines composed via `trackTransform`. | ![Stations + Areas](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/03-stations-areas.png)<br>**Stations + Areas** — cleanroom / safety-area zone overlays, station tubes with stop markers. |
164
- | ![Stator heatmap](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/04-stator-heatmap.png)<br>**Stator heatmap** — vertex-colour gradient along the centerline, fed from your live drive currents. | ![Collision detection](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/05-collision.png)<br>**Sub-mm collision detection** — continuous monitor with banner; pair-wise 1D arc-length test on the shared chain. |
165
- | ![Drive status](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/06-drive-status.png)<br>**Drive status** — emissive blink at 1 Hz on the GLB itself + camera-facing 3D icons (▲ warning, ⊙ error). | ![Perf stress](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/07-perf-stress.png)<br>**Perf stress** — 750 movers animated at 60 Hz with zero React commits in steady state. |
166
- | ![Shadows + IBL](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/08-shadows.png)<br>**PCF-soft shadows + IBL** — opt-in shadows on a transparent canvas; image-based lighting on by default. | ![Top-down export](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/09-screenshot-export.png)<br>**`exportScreenshot('top-down')`** — orthographic, AABB-fit, mirrors the 2D viewer convention. |
167
-
168
- ---
77
+ No asset hosting is required. GLBs stream from the matching
78
+ `beckhoff-xts-viewer-3d-assets` release on jsDelivr; see
79
+ [asset hosting](docs/USING-THE-COMPONENT.md#1-glb-assets--zero-config-by-default)
80
+ to self-host them instead.
169
81
 
170
- ## Installation
82
+ ## Features
171
83
 
172
- ```bash
173
- npm install beckhoff-xts-viewer-3d
174
- ```
84
+ **Geometry and layout.** The full Beckhoff module and mover catalogue —
85
+ Standard AT, Eco AT2200, NCT (AT2002 / AT2102 with AT8200 tools), Hygienic ATH
86
+ and Hepco GFX2 — with straights, ±22.5° and ±45° curves and the AT2050 / ATH2050
87
+ 180° clothoid reversal. Module-to-module C0/C1 continuity is pinned by golden
88
+ fixtures. Per-XPU `trackTransform` places independent lines in one scene, and
89
+ `positionFrame` remaps every position value into the host's coordinate
90
+ convention without touching the rest of the config.
175
91
 
176
- Peer dependencies (you almost certainly have these already):
92
+ **Mover motion.** `setMoverPositions()` writes into a per-component store that
93
+ `useFrame` drains directly into the three.js scene graph, so a 60 Hz drive loop
94
+ produces no React renders. An instanced fast path batches mover bodies into a
95
+ single draw call where per-mover scene nodes are not needed.
177
96
 
178
- ```bash
179
- npm install react react-dom three
180
- ```
97
+ **Rendering.** ACES filmic tone mapping, image-based lighting from a
98
+ procedurally built indoor environment, anisotropic filtering and optional
99
+ PCF-soft shadows, all switchable through `display.*`. SSAO and bloom are
100
+ available when the optional post-processing peers are installed.
181
101
 
182
- Compatibility:
102
+ **Interaction and status.** Click selection for modules and movers, tinting the
103
+ GLB itself rather than overlaying wireframes; drive-status blink plus
104
+ camera-facing warning and error icons; feed-segment (Einspeisestrang) tinting
105
+ that wraps the seam of a closed loop; stations, areas, dimensions and info bars;
106
+ stop-position ghost movers; and a stator heatmap driven by
107
+ `(positionMm, value)` samples.
183
108
 
184
- - React 18 (tested on 19)
185
- - Three.js ≥ 0.150
186
- - Modern bundler (Vite, Webpack, Next.js, Remix, Astro, plain CRA — all fine)
109
+ **Analysis.** Sub-millimetre mover collision detection, as a one-shot call or a
110
+ continuous monitor, plus module-level collision probes.
187
111
 
188
- The package is `"type": "module"` and ships ESM + CJS via
189
- `./dist/index.{js,cjs,d.ts}`. `sideEffects: false` so unused exports
190
- tree-shake out.
191
-
192
- ---
193
-
194
- ## Getting started
195
-
196
- ### 1. GLB assets — zero-config by default
197
-
198
- The viewer's default `assetsBaseUrl` points at the jsDelivr CDN, version-
199
- pinned to the matching [`beckhoff-xts-viewer-3d-assets`](https://www.npmjs.com/package/beckhoff-xts-viewer-3d-assets)
200
- release:
201
-
202
- ```text
203
- https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d-assets@<version>/models
204
- ```
205
-
206
- So in the typical case there is **nothing to install or host** — drop in
207
- `<XtsViewer3D config={…} />` and the GLBs stream from jsDelivr.
208
-
209
- Calibration metadata (origin-correction, path lengths, AABBs) is compiled
210
- into the JS bundle, so the viewer never makes a sidecar HTTP request for
211
- known module / mover / tool types.
212
-
213
- #### Self-hosting
214
-
215
- If you can't reach jsDelivr (air-gapped network, corporate proxy, regulated
216
- environment), install the assets package and serve the GLBs yourself:
217
-
218
- ```bash
219
- npm install beckhoff-xts-viewer-3d-assets
220
- ```
221
-
222
- Copy `node_modules/beckhoff-xts-viewer-3d-assets/models` into your
223
- app's static folder during build, and point at it:
224
-
225
- ```tsx
226
- <XtsViewer3D config={cfg} assetsBaseUrl="/models" />
227
- ```
228
-
229
- Or use any other URL prefix:
230
-
231
- ```tsx
232
- <XtsViewer3D config={cfg} assetsBaseUrl="https://cdn.example.com/xts/" />
233
- ```
112
+ **Export.** `exportScreenshot()` renders offscreen at any resolution with MSAA,
113
+ in current-camera, top-down or saved-camera mode. `beginFrameCapture()` opens a
114
+ reusable session whose `grab()` returns a frame synchronously without image
115
+ encoding, and keeps producing frames while the window is minimised. Both bypass
116
+ the post-processing chain; the result reports whether that happened.
234
117
 
235
- #### Why a separate assets package?
236
-
237
- The viewer JS bundle is ~110 kB compressed. The CAD-derived GLBs total
238
- ~28 MB. Splitting them lets `npm install beckhoff-xts-viewer-3d`
239
- stay tiny, while the assets are version-pinned and fetched on demand from
240
- a globally cached CDN.
241
-
242
- ### 2. Build a config
243
-
244
- The minimum: one `ProcessingUnitConfig` with one `Part` containing your
245
- module list and one or more `Mover`s. See the snippet at the top of this
246
- README for a working oval loop.
247
-
248
- The component fills its parent (`width: 100%; height: 100%`) — make sure
249
- the parent has a definite height. The canvas is **transparent**: whatever
250
- sits behind the host element shows through, so wrap in a styled container
251
- if you want a solid backdrop.
252
-
253
- ### 3. Drive mover positions
254
-
255
- Movers don't animate themselves — your app pushes positions through the
256
- imperative ref:
257
-
258
- ```tsx
259
- import { useRef, useEffect } from 'react';
260
- import { XtsViewer3D, type XtsViewer3DRef } from 'beckhoff-xts-viewer-3d';
261
-
262
- function App() {
263
- const viewerRef = useRef<XtsViewer3DRef>(null);
264
-
265
- useEffect(() => {
266
- let raf = 0;
267
- const tick = (t: number) => {
268
- viewerRef.current?.setMoverPosition(0, (t / 5) % 3000);
269
- raf = requestAnimationFrame(tick);
270
- };
271
- raf = requestAnimationFrame(tick);
272
- return () => cancelAnimationFrame(raf);
273
- }, []);
274
-
275
- return <XtsViewer3D ref={viewerRef} config={config} />;
276
- }
277
- ```
278
-
279
- The push goes straight into a per-component store; `<XtsMover>` reads it in
280
- `useFrame` and mutates its Three.js group. **Zero React renders per tick.**
281
-
282
- For state-driven flows the legacy contract still works: just keep
283
- `MoverConfig.partPositionMm` updated in the config and pass the new config
284
- through. Whichever you set last wins (imperative store > config prop).
285
-
286
- ### 4. Read selection + errors
287
-
288
- ```tsx
289
- <XtsViewer3D
290
- config={config}
291
- selectionMode="Single" // | 'Off' | 'Multi'
292
- onSelectionChange={(s) => console.log('selection', s)}
293
- onError={(err) => console.error(err.code, err.message)}
294
- />
295
- ```
296
-
297
- `SelectionState` carries `{ modules: ModuleRef[], movers: MoverRef[] }`.
298
- Errors flow through `onError` with typed codes:
299
- `asset-load-failed | unknown-module-type | unmatched-clothoid-half | …`.
300
-
301
- ### 5. Detect mover collisions
302
-
303
- Two flavours — pick one.
304
-
305
- **Continuous monitoring** (callback fires when the collision set changes):
306
-
307
- ```tsx
308
- import type { MoverCollision } from 'beckhoff-xts-viewer-3d';
309
-
310
- <XtsViewer3D
311
- config={config}
312
- collisionDetection={{
313
- enabled: true,
314
- warningGapMm: 0, // 0 = real collisions; > 0 also reports near-misses
315
- intervalMs: 0, // 0 = check every frame; e.g. 50 for 20 Hz
316
- onCollisionsChange: (collisions: MoverCollision[]) => {
317
- // Fires only when the set actually changes (pair appears / disappears
318
- // / penetrationMm shifts by > 0.01 mm).
319
- if (collisions.length) console.warn('crash:', collisions[0]);
320
- },
321
- }}
322
- />;
323
- ```
324
-
325
- **One-shot query** via the imperative ref:
326
-
327
- ```tsx
328
- const list = viewerRef.current?.checkMoverCollisions({ warningGapMm: 5 });
329
- // → MoverCollision[] sorted deepest-penetration first
330
- ```
331
-
332
- Each `MoverCollision` carries:
333
-
334
- ```ts
335
- {
336
- a: MoverRef; b: MoverRef;
337
- idA: string; idB: string;
338
- penetrationMm: number; // > 0 = overlap, 0 = touching, < 0 = warning gap
339
- positionAMm: number; positionBMm: number;
340
- pathLengthAMm: number; pathLengthBMm: number;
341
- viaWraparound: boolean; // true when measured across the closed-loop seam
342
- }
343
- ```
344
-
345
- Sub-millimetre accurate (pure float64 arc-length math). The check covers
346
- movers travelling on the same chain; cross-track collisions in multi-XPU
347
- setups are out of scope.
348
-
349
- ### 6. Capture screenshots
350
-
351
- Trigger from anywhere in your app via the imperative ref:
352
-
353
- ```tsx
354
- const viewer = useRef<XtsViewer3DRef>(null);
355
-
356
- async function saveTopDown() {
357
- const result = await viewer.current!.exportScreenshot({
358
- mode: 'top-down', // | 'current' | 'custom'
359
- pixelRatio: 2, // 2× sharpness even on a 1× display
360
- paddingFactor: 1.15,
361
- format: 'png', // | 'jpeg' | 'webp'
362
- backgroundColor: null, // null = transparent PNG; '#0e1116' for a solid bg
363
- });
364
- // result: { blob, widthPx, heightPx, camera, boundingBoxMm, mode }
365
- saveAs(result.blob, `layout-${Date.now()}.png`);
366
- }
367
- ```
368
-
369
- `'top-down'` mirrors the 2D viewer: orthographic camera centred on the
370
- scene's bounding box, world +X = image right, world +Y = image up.
371
- `'custom'` lets you reproduce a saved framing exactly:
372
-
373
- ```tsx
374
- viewer.current!.exportScreenshot({
375
- mode: 'custom',
376
- camera: previousResult.camera, // round-trip a saved CameraState
377
- });
378
- ```
118
+ **Camera.** Orthographic top-down projection for a live 2D plan view, an
119
+ animated `focusOn()` for stations, areas, movers, modules or the whole scene,
120
+ and an opt-in CAD ViewCube.
379
121
 
380
- Renders go to an offscreen WebGLRenderTarget — the live canvas keeps
381
- running at full speed, no `preserveDrawingBuffer` perf cost.
382
-
383
- ### 7. Live 2D plan view (orthographic top-down)
384
-
385
- Set `projection="orthographic"` to switch the **live** canvas into a flat 2D
386
- plan view — straight down +Z, world +Y up — that is pixel-consistent with
387
- `exportScreenshot({ mode: 'top-down' })`. Switching at runtime does **not**
388
- recreate the WebGL context, so toggling between 3D and 2D is instant.
389
-
390
- ```tsx
391
- const [is2D, setIs2D] = useState(false);
392
-
393
- <XtsViewer3D
394
- config={config}
395
- projection={is2D ? 'orthographic' : 'perspective'}
396
- // shadows add nothing to a flat plan — drop them in 2D
397
- display={is2D ? { ...display, shadows: false } : display}
398
- />
399
- ```
400
-
401
- In orthographic top-down, **rotation is auto-disabled** (a 2D plan has no
402
- meaningful orbit); pan and zoom stay on. Set `lock={{ rotate: false }}` to
403
- opt rotation back in. The frustum re-fits automatically on container resize
404
- and whenever the scene's bounding box changes. From the ref, `frameTopDown()`
405
- re-fits on demand and `zoomToFit()` adjusts the ortho frustum (instead of
406
- dollying) when the live camera is orthographic.
407
-
408
- ### 8. Fly the camera to an object (focusOn)
409
-
410
- `viewerRef.current.focusOn(target, opts)` animates the camera so a **station,
411
- area, mover, module** — or the whole `scene` — fits the frame. In perspective
412
- the current view angle is preserved (the camera only dollies + re-centres); in
413
- orthographic top-down the frustum re-frames and the camera pans straight over
414
- the target. Works in both 2D and 3D.
415
-
416
- ```tsx
417
- // Frame a station, 700 ms ease-in-out (defaults)
418
- viewerRef.current?.focusOn({ kind: 'station', stationId: 3 });
419
-
420
- // Frame a mover, faster
421
- viewerRef.current?.focusOn(
422
- { kind: 'mover', ref: { processingUnitObjectId: 0, moverIndex: 2 } },
423
- { durationMs: 500 },
424
- );
425
-
426
- // Frame an area / a module / the whole scene
427
- viewerRef.current?.focusOn({ kind: 'area', areaId: 1 });
428
- viewerRef.current?.focusOn({
429
- kind: 'module',
430
- ref: { processingUnitObjectId: 0, partObjectId: 10, moduleIndex: 4 },
431
- });
432
- viewerRef.current?.focusOn({ kind: 'scene' }, { durationMs: 0 }); // 0 = jump
433
- ```
434
-
435
- `FocusOptions`: `durationMs` (default `700`, `0` jumps), `paddingFactor`
436
- (default `1.2` perspective / `1.1` ortho), `easing` (`'easeInOutCubic'` |
437
- `'linear'`). A new `focusOn` call supersedes any in-flight animation.
438
-
439
- ### 9. Mark zones with Areas
440
-
441
- Areas are stop-position-free range overlays — like Stations, but with no
442
- markers. Use them for cleanroom / safety-area / manual-access
443
- zones that don't drive any mover behaviour:
444
-
445
- ```tsx
446
- <XtsViewer3D
447
- config={{
448
- processingUnits: [/* … */],
449
- areas: [
450
- {
451
- areaId: 1,
452
- description: 'Cleanroom',
453
- isEnabled: true,
454
- partOids: [0],
455
- startPositionOnPart: 250,
456
- endPositionOnPart: 1000,
457
- color: 0xff_4d_9d_e0, // ARGB
458
- },
459
- ],
460
- }}
461
- display={{
462
- areaOptions: {
463
- thicknessMm: 8,
464
- displacementMm: 60,
465
- opacity: 0.7,
466
- showAreaDescription: true,
467
- },
468
- }}
469
- />;
470
- ```
471
-
472
- ### 10. Stator heatmap
473
-
474
- Coloured tube along each part's centerline, vertex colours interpolated
475
- across consumer-supplied `(positionMm, value)` samples — perfect for
476
- streaming live drive currents or stator temperatures:
477
-
478
- ```tsx
479
- import type { StatorHeatmap } from 'beckhoff-xts-viewer-3d';
480
-
481
- const heatmap: StatorHeatmap = {
482
- parts: [{ partOid: 0, samples: [{ positionMm: 0, value: 25 }, /* … */] }],
483
- min: 0, max: 100,
484
- minColor: '#22c55e', // default green
485
- maxColor: '#ef4444', // default red
486
- };
487
-
488
- <XtsViewer3D
489
- config={config}
490
- statorHeatmap={heatmap}
491
- display={{ showStatorHeatmap: true }}
492
- />;
493
- ```
494
-
495
- ### 11. Track direction + zero offset
496
-
497
- When the host machine uses a different sign convention or zero point
498
- than the GLB chain, set a `positionFrame` on the XPU. Movers, stations,
499
- areas, stops, ghosts, and `getMoverWorldTransform` all follow:
500
-
501
- ```tsx
502
- processingUnits: [
503
- {
504
- objectId: 0,
505
- moverType: 'AT9014_0055',
506
- positionFrame: { direction: 'negative', originMm: 1500 },
507
- parts: [/* … */],
508
- movers: [
509
- { index: 0, id: 0, partOid: 0, partPositionMm: 0 },
510
- ],
511
- },
512
- ]
513
- ```
514
-
515
- For more — every prop, every ref method, every helper, every type —
516
- read **[docs/USING-THE-COMPONENT.md](docs/USING-THE-COMPONENT.md)**.
517
-
518
- ---
519
-
520
- ## Realism + performance
521
-
522
- The viewer is tuned to look like a CAD-quality render out of the box while
523
- holding 60 Hz on mid-range integrated GPUs. Every realism feature is
524
- opt-out via `display.*` so consumers who liked the old direct-lighting
525
- look can revert with a single prop.
526
-
527
- | Feature | Default | Knob | Cost |
528
- |---|---|---|---|
529
- | Image-based lighting (PMREM-prefiltered `RoomEnvironment`) | **on** | `display.environmentLighting` | one-time PMREM build (~1.5 MB GPU); zero per-frame overhead beyond standard PBR shader |
530
- | Environment intensity | `0.4` | `display.environmentIntensity` | — |
531
- | ACES Filmic tone mapping | **on** | `display.toneMapping` (`'aces' \| 'linear' \| 'reinhard' \| 'cineon' \| 'agx' \| 'none'`) | shader-side, ~free |
532
- | Tone-mapping exposure | `1.0` | `display.toneMappingExposure` | — |
533
- | Anisotropic texture filtering (max hardware) | **on** | (always on; pure sampler state) | none |
534
- | `castShadow` / `receiveShadow` on every GLB mesh | **on** | (auto) | none unless shadows are enabled |
535
- | PCF-soft shadows on the directional light | **off** | `display.shadows` | one extra render pass on the shadow map (4096²) |
536
- | Shadow-catcher plane (transparent ground) | tied to `shadows` | — | trivial |
537
- | Hemisphere fill (when env lighting is off) | **on (when no IBL)** | (auto) | vertex-frequency |
538
- | Auto-pause render loop when tab hidden | **on** | `performance.autoPauseOnHidden` | — |
539
- | Mover updates via `MoverPositionStore` (no React commits) | **always** | — | — |
540
-
541
- The PBR pipeline picks up automatically: any `MeshStandardMaterial` /
542
- `MeshPhysicalMaterial` already exported in your GLBs (the standard glTF
543
- metal/rough workflow) inherits `scene.environment` for reflections.
544
- Custom GLBs with non-PBR materials are unaffected — they render exactly
545
- the same.
546
-
547
- If you need flat direct-lighting parity:
548
-
549
- ```tsx
550
- <XtsViewer3D
551
- config={config}
552
- display={{
553
- environmentLighting: false, // disable IBL
554
- toneMapping: 'none', // no tone-mapping curve
555
- }}
556
- />
557
- ```
558
-
559
- Stress baseline: `⚡ Perf stress` (3 ovals × 250 movers = 750 mover groups
560
- animated at 60 Hz) runs steady at ~16 ms/frame in Chrome on a mid-range
561
- laptop with 0 React commits in steady state, IBL + ACES + anisotropy on.
122
+ ## Gallery
562
123
 
563
- ---
124
+ | | |
125
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
126
+ | ![Multi-track](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/02-multi-track.png)<br>Two independent lines composed with `trackTransform`. | ![Stations and areas](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/03-stations-areas.png)<br>Station tubes with stop markers, and zone overlays for cleanroom or safety areas. |
127
+ | ![Stator heatmap](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/04-stator-heatmap.png)<br>Vertex-colour gradient along the centerline, fed from live drive currents. | ![Collision detection](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/05-collision.png)<br>Continuous pair-wise arc-length collision monitoring. |
128
+ | ![Drive status](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/06-drive-status.png)<br>Emissive blink on the GLB plus camera-facing warning and error icons. | ![Many movers](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/07-perf-stress.png)<br>750 movers animated at 60 Hz with no React commits in steady state. |
129
+ | ![Shadows and IBL](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/08-shadows.png)<br>Opt-in PCF-soft shadows on a transparent canvas, image-based lighting on by default. | ![Top-down export](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/09-screenshot-export.png)<br>`exportScreenshot({ mode: 'top-down' })` — orthographic and AABB-fit. |
130
+ | ![Feed segments](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/10-feed-segments.png)<br>`feedSegmentHighlights` tints whole electrical strands; the pink one wraps the loop seam. | |
564
131
 
565
132
  ## Troubleshooting
566
133
 
567
- ### Modules render as yellow boxes, movers as blue boxes
134
+ ### Modules render as yellow boxes and movers as blue boxes
568
135
 
569
- **Symptom.** GLBs never appear; the viewer shows wireframe placeholders
570
- (modules in `#FFB000`, movers in `#3D88E0`) and the browser console
571
- prints `THREE.WARNING: Multiple instances of Three.js being imported`.
572
- No `models/*.glb` requests show up in the Network panel.
136
+ The viewer is showing wireframe placeholders because no GLB was accepted. The
137
+ console also reports `THREE.WARNING: Multiple instances of Three.js being
138
+ imported`, and no `models/*.glb` requests appear in the network panel.
573
139
 
574
- **Cause.** A transitive dep (`stats-gl`, via `@react-three/drei`) pins
575
- `three` in its own `dependencies`, so npm installs a second
576
- `three`-copy under `node_modules/stats-gl/node_modules/three`. The two
577
- copies produce two `THREE.*` namespaces; `useGLTF`'s `instanceof`
578
- checks fail across the boundary and silently reject every parsed scene.
140
+ This happens when a transitive dependency pins its own copy of `three`. Two
141
+ copies mean two `THREE.*` namespaces, and the `instanceof` checks inside
142
+ `useGLTF` reject every parsed scene across that boundary.
579
143
 
580
- **Fix.** Force your bundler to deduplicate `three`. For Vite, add
581
- `resolve.dedupe`:
144
+ Deduplicate `three` in your bundler:
582
145
 
583
146
  ```ts
584
147
  // vite.config.ts
@@ -588,167 +151,88 @@ export default defineConfig({
588
151
  });
589
152
  ```
590
153
 
591
- Webpack / Next.js: alias `three` to your root `node_modules/three`. See
592
- [USING-THE-COMPONENT.md § Bundler configuration](docs/USING-THE-COMPONENT.md#bundler-configuration--deduplicate-three)
593
- for the full snippets and how to bust Vite's pre-bundle cache after the
594
- change.
595
-
596
- ---
154
+ For Webpack and Next.js, alias `three` to your root `node_modules/three`.
155
+ [Bundler configuration](docs/USING-THE-COMPONENT.md#bundler-configuration--deduplicate-three)
156
+ has the full snippets and explains how to clear Vite's pre-bundle cache
157
+ afterwards.
597
158
 
598
159
  ## Documentation
599
160
 
600
- - **[docs/USING-THE-COMPONENT.md](docs/USING-THE-COMPONENT.md)**
601
- consumer guide: install, asset hosting, every prop, every ref method,
602
- recipes, performance tuning.
603
- - **[docs/ADDING-A-MODULE.md](docs/ADDING-A-MODULE.md)** —
604
- developer guide: add a new module / mover / tool type from STP to
605
- calibrated GLB.
606
- - **[docs/RELEASING.md](docs/RELEASING.md)**
607
- one-click release flow: how to publish a new version, npm Trusted
608
- Publishers setup, calibration safety, emergency manual release.
609
- - **[docs/screenshots/README.md](docs/screenshots/README.md)** —
610
- how to refresh the README + docs gallery from the playground.
161
+ - [Using the component](docs/USING-THE-COMPONENT.md) — every prop, every ref
162
+ method, asset hosting, recipes and performance tuning.
163
+ - [Adding a module](docs/ADDING-A-MODULE.md) — taking a new module, mover or
164
+ tool type from STP file to calibrated GLB.
165
+ - [Performance](docs/PERFORMANCE.md) the asset compression pipeline and the
166
+ runtime budget.
167
+ - [Releasing](docs/RELEASING.md) — the automated publish flow.
611
168
 
612
- ---
169
+ ## Development
613
170
 
614
- ## Development setup
615
-
616
- This repo is a pnpm workspace — `pnpm-lock.yaml` is the source of
617
- truth and `npm ci` / `npm install` will not work. Get pnpm via
618
- `npm install -g pnpm@10` (or any other installer), then:
171
+ The repo is a pnpm workspace; `pnpm-lock.yaml` is the source of truth, so
172
+ `npm install` will not work. Install pnpm 10, then:
619
173
 
620
174
  ```bash
621
175
  pnpm install
622
- pnpm test # 310 unit + property tests
176
+ pnpm lint
623
177
  pnpm typecheck
178
+ pnpm test
179
+ pnpm run test:coverage
180
+ pnpm build
624
181
  ```
625
182
 
626
- ### Run the playground
627
-
628
- The playground is a small Vite app at `playground/` that demonstrates every
629
- feature — selection, calibration, composer, multi-track, shadows, ViewCube,
630
- drive-status icons, the perf stress test, IBL toggle, intensity slider.
631
-
632
- ```bash
633
- pnpm dev # http://127.0.0.1:5173
634
- ```
635
-
636
- Sidebar controls let you switch demos, animate movers, toggle shadows /
637
- IBL / ViewCube / theme, drag-and-drop tracks together in the composer,
638
- and live-edit calibration overrides.
639
-
640
- ### Build the library
641
-
642
- ```bash
643
- pnpm build # → dist/
644
- pnpm playground:build
645
- ```
183
+ `pnpm dev` starts the playground at `http://127.0.0.1:5173`. It exercises every
184
+ feature and lets you switch demos, drive movers, toggle lighting and shadows,
185
+ compose multi-track layouts and live-edit calibration overrides.
646
186
 
647
187
  ### Asset pipeline
648
188
 
649
- CAD source files (`stepfiles/*.stp`) are converted to runtime-ready GLBs
650
- and per-asset JSON sidecars. To regenerate after touching a STP:
189
+ CAD sources in `stepfiles/*.stp` are converted to runtime GLBs plus per-asset
190
+ JSON sidecars:
651
191
 
652
192
  ```bash
653
- pnpm assets:convert # STP → GLB via occt-import-js
193
+ pnpm assets:convert # STP → GLB
654
194
  pnpm assets:inspect # refresh docs/data/glb-inspection.json
655
- pnpm assets:generate-sidecars # module .meta.json (origin-correction)
195
+ pnpm assets:generate-sidecars # module .meta.json origin corrections
656
196
  pnpm assets:generate-mover-sidecars
657
197
  ```
658
198
 
659
- The sidecar generators are idempotent: existing files are skipped so they
660
- never stomp hand-tuned calibration values. Pass `--force` to regenerate.
661
-
662
- The release pipeline **never** invokes the generators — `bundle-sidecars`
663
- reads existing JSONs read-only into the JS bundle. See
664
- [`docs/RELEASING.md`](docs/RELEASING.md#calibration-safety-the-metajson-story).
665
-
666
- To add a new module / mover / tool type from scratch — naming convention,
667
- type registration, sidecar generation, calibration workflow — follow
668
- **[docs/ADDING-A-MODULE.md](docs/ADDING-A-MODULE.md)**.
669
-
670
- ### Project layout
671
-
672
- ```
673
- .
674
- ├── src/ Library source — published as the npm package
675
- │ ├── components/ <XtsViewer3D> + internal scene tree
676
- │ ├── geometry/ Path math, ChainBuilder, normalizeXtsConfig, …
677
- │ ├── assets/ AssetManifest, SidecarLoader, AssetLoader
678
- │ └── interaction/ SelectionManager
679
- ├── packages/
680
- │ └── assets/ Sibling npm package (GLB-only mirror)
681
- ├── playground/ Vite app exercising every feature
682
- ├── public/models/ GLBs + .meta.json sidecars (sources)
683
- ├── stepfiles/ Source CAD STP files (NOT shipped)
684
- ├── scripts/ Asset pipeline + version-sync utilities
685
- ├── docs/
686
- │ ├── USING-THE-COMPONENT.md Consumer guide (props, ref API, recipes)
687
- │ ├── ADDING-A-MODULE.md How to register a new module / mover / tool
688
- │ ├── RELEASING.md How to publish a new release
689
- │ ├── screenshots/ README + docs gallery
690
- │ └── data/ GLB AABB inspection JSON
691
- ├── .github/workflows/ release.yml + release-assets.yml + deploy-docs.yml
692
- └── README.md
693
- ```
694
-
695
- ### Tests
696
-
697
- `vitest` covers path math, normalize-config, chain-building, sample helpers,
698
- selection logic, asset URL composition, the composer reducer, dimension
699
- ticks, and the multi-track transform composition. Run focused:
700
-
701
- ```bash
702
- pnpm vitest run src/geometry/__tests__/ChainBuilder.test.ts
703
- ```
704
-
705
- ---
706
-
707
- ## Releasing
199
+ The generators skip existing files so hand-tuned calibration is never
200
+ overwritten; pass `--force` to regenerate. The release pipeline never runs them
201
+ and reads the sidecars read-only.
708
202
 
709
- The pipeline is **fully automated and triggered by every push to
710
- `main`**. [semantic-release](https://semantic-release.gitbook.io/) reads
711
- your Conventional Commit messages, decides the next version, writes
712
- `CHANGELOG.md`, publishes both packages to npm, and opens a GitHub
713
- Release.
203
+ ### Layout
714
204
 
715
205
  ```text
716
- git push origin main # commits like `fix: …`, `feat: …`, `feat!: …`
717
-
718
- .github/workflows/release.yml
719
-
720
- npx semantic-release # version bump + npm publish + GH release
721
- ```
722
-
723
- You **never** call `npm version`, write a changelog, create a tag, or
724
- run `npm publish` by hand. Commit-type → bump:
725
-
726
- | Prefix | Bump |
727
- |---|---|
728
- | `fix:` / `perf:` | patch |
729
- | `feat:` | minor |
730
- | `feat!:` / `fix!:` / `BREAKING CHANGE:` footer | major |
731
- | `chore:` / `docs:` / `ci:` / `refactor:` / `test:` | none (no release) |
732
-
733
- Plus a SHA-256 guard around `public/models/*.meta.json` so hand-tuned
734
- calibration values can never be overwritten by the pipeline.
735
-
736
- Preview the next release locally:
737
-
738
- ```bash
739
- pnpm release:dry-run
740
- ```
741
-
742
- See **[docs/RELEASING.md](docs/RELEASING.md)** for the full guide:
743
- plugin order, calibration safety, Trusted Publishers graduation, and
744
- emergency manual flow.
745
-
746
- ---
206
+ src/ Library source this is what ships
207
+ components/ <XtsViewer3D> and the internal scene tree
208
+ geometry/ Path math, ChainBuilder, normalizeXtsConfig
209
+ assets/ AssetManifest, AssetLoader, SidecarLoader
210
+ interaction/ SelectionManager
211
+ packages/assets/ Sibling npm package: GLB-only mirror
212
+ playground/ Vite app exercising every feature
213
+ public/models/ GLBs and .meta.json calibration sidecars
214
+ stepfiles/ Source CAD files (not published)
215
+ scripts/ Asset pipeline and version-sync utilities
216
+ docs/ VitePress site and guides
217
+ ```
218
+
219
+ ### Releasing
220
+
221
+ Every push to `main` runs [semantic-release](https://semantic-release.gitbook.io/),
222
+ which derives the version from Conventional Commit messages, writes
223
+ `CHANGELOG.md` and publishes both packages. Never run `npm version`, edit the
224
+ changelog or publish by hand.
225
+
226
+ | Prefix | Bump |
227
+ | -------------------------------------------------- | ----- |
228
+ | `fix:` / `perf:` | patch |
229
+ | `feat:` | minor |
230
+ | `feat!:` / `fix!:` / `BREAKING CHANGE:` footer | major |
231
+ | `chore:` / `docs:` / `ci:` / `refactor:` / `test:` | none |
232
+
233
+ Preview with `pnpm release:dry-run`. [Releasing](docs/RELEASING.md) covers the
234
+ plugin chain, the calibration-safety guard and the manual fallback.
747
235
 
748
236
  ## License
749
237
 
750
238
  MIT — see [LICENSE](LICENSE).
751
-
752
- The Beckhoff CAD source files under `stepfiles/` are included for
753
- verification only and remain subject to their respective Beckhoff
754
- licensing terms — they are not shipped with the published npm package.