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