beckhoff-xts-viewer-3d 5.2.2 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,315 +1,315 @@
1
- # beckhoff-xts-viewer-3d
2
-
3
- [![npm](https://img.shields.io/npm/v/beckhoff-xts-viewer-3d.svg)](https://www.npmjs.com/package/beckhoff-xts-viewer-3d)
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
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
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
-
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.
12
-
13
- It covers the same ground as the official 2D `Beckhoff.TwinCAT.HMI.XTS.Controls`
14
- viewer, using real CAD geometry in three dimensions.
15
-
16
- ![A 5 m XTS packaging line rendered by the viewer](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/01-packaging-line.webp)
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
-
45
- ```tsx
46
- import { XtsViewer3D } from 'beckhoff-xts-viewer-3d';
47
-
48
- <XtsViewer3D
49
- config={{
50
- processingUnits: [
51
- {
52
- objectId: 0,
53
- moverType: 'AT9014_0055',
54
- parts: [
55
- {
56
- objectId: 0,
57
- globalNumber: 0,
58
- modules: [
59
- { moduleType: 'AT2001_0250', globalNumber: 1 },
60
- { moduleType: 'AT2000_0250', globalNumber: 2 },
61
- { moduleType: 'AT2050_0500', globalNumber: 3 },
62
- { moduleType: 'AT2050_0501', globalNumber: 4 },
63
- { moduleType: 'AT2001_0250', globalNumber: 5 },
64
- { moduleType: 'AT2000_0250', globalNumber: 6 },
65
- { moduleType: 'AT2050_0500', globalNumber: 7 },
66
- { moduleType: 'AT2050_0501', globalNumber: 8 },
67
- ],
68
- },
69
- ],
70
- movers: [{ index: 0, id: 0, partOid: 0, partPositionMm: 200 }],
71
- },
72
- ],
73
- }}
74
- />;
75
- ```
76
-
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.
81
-
82
- ## Module catalogue
83
-
84
- Every image on this page comes out of the component itself, through the same
85
- `exportScreenshot()` a consumer calls, and shows a layout a line would
86
- actually be built as — a 5 m racetrack with four stations, a buffer loop
87
- beside it. See [docs/screenshots](docs/screenshots/README.md) for the capture
88
- script and [`playground/src/configs.ts`](playground/src/configs.ts) for the
89
- layouts themselves.
90
-
91
- | | | |
92
- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
- | ![Standard AT](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/02-standard-at.webp)<br>**Standard AT** — AT2001 infeed, AT2000 straights and the AT2050 180° clothoid reversal. | ![Eco AT2200](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/03-eco-at2200.webp)<br>**Eco AT2200 / AT2202** — 500 mm straights, which pair naturally with the 500 mm reversal. | ![NCT with AT8200 tools](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/04-nct-tools.webp)<br>**NCT AT2002 / AT2102** — high modules carrying AT8200 tool carriers on the movers. |
94
- | ![Hygienic ATH](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/05-hygienic-ath.webp)<br>**Hygienic ATH** — stainless housings, sealed joints and the ATH9011 mover. | ![Hepco GFX2](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/06-hepco-gfx.webp)<br>**Hepco GFX2** — the procedural GFX rail profile with a GFX2 1TC carriage instead of the Beckhoff guiding rail. | ![Material close-up](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/07-materials-closeup.webp)<br>**Real materials, not CAD colours** — anodised housing, stator packs, printed type plate with its LED row. |
95
-
96
- The GLBs ship with a named PBR material library rather than the CAD sources'
97
- placeholder colours — see [Asset pipeline](#asset-pipeline).
98
-
99
- ## Features
100
-
101
- **Geometry and layout.** The full Beckhoff module and mover catalogue —
102
- Standard AT, Eco AT2200, NCT (AT2002 / AT2102 with AT8200 tools), Hygienic ATH
103
- and Hepco GFX2 — with straights, ±22.5° and ±45° curves and the AT2050 / ATH2050
104
- 180° clothoid reversal. Module-to-module C0/C1 continuity is pinned by golden
105
- fixtures. Per-XPU `trackTransform` places independent lines in one scene, and
106
- `positionFrame` remaps every position value into the host's coordinate
107
- convention without touching the rest of the config.
108
-
109
- **Mover motion.** `setMoverPositions()` writes into a per-component store that
110
- `useFrame` drains directly into the three.js scene graph, so a 60 Hz drive loop
111
- produces no React renders. An instanced fast path batches mover bodies into a
112
- single draw call where per-mover scene nodes are not needed.
113
-
114
- **Rendering.** ACES filmic tone mapping, image-based lighting from a
115
- procedurally built indoor environment, anisotropic filtering and optional
116
- PCF-soft shadows, all switchable through `display.*`. SSAO and bloom are
117
- available when the optional post-processing peers are installed.
118
-
119
- **Interaction and status.** Click selection for modules and movers, tinting the
120
- GLB itself rather than overlaying wireframes; drive-status blink plus
121
- camera-facing warning and error icons; feed-segment (Einspeisestrang) tinting
122
- that wraps the seam of a closed loop; stations, areas, dimensions and info bars;
123
- stop-position ghost movers; and a stator heatmap driven by
124
- `(positionMm, value)` samples.
125
-
126
- **Measuring and annotating.** CAD-style tools — point-to-point distance with
127
- optional ΔX/ΔY/ΔZ guides, angle, polyline, and distance _along_ the XTS path —
128
- picked in the scene with snapping to geometry corners and edges as well as to
129
- mover centres, module boundaries, station stops and the track path itself.
130
- Annotations pin a label anywhere, including to a mover, where they follow it
131
- while it drives. Everything works the same in the 2D plan view, renders into
132
- screenshots and video frames, and is fully drivable from
133
- `viewerRef.current.measurements`.
134
-
135
- **Analysis.** Sub-millimetre mover collision detection, as a one-shot call or a
136
- continuous monitor, plus module-level collision probes.
137
-
138
- **Export.** `exportScreenshot()` renders offscreen at any resolution with MSAA,
139
- in current-camera, top-down or saved-camera mode. `beginFrameCapture()` opens a
140
- reusable session whose `grab()` returns a frame synchronously without image
141
- encoding, and keeps producing frames while the window is minimised. Both bypass
142
- the post-processing chain; the result reports whether that happened.
143
-
144
- **Camera.** Orthographic top-down projection for a live 2D plan view, an
145
- animated `focusOn()` for stations, areas, movers, modules or the whole scene,
146
- and an opt-in CAD ViewCube.
147
-
148
- ### Measurements and annotations
149
-
150
- ```tsx
151
- const viewer = useRef<XtsViewer3DRef>(null);
152
- const [tool, setTool] = useState<MeasurementTool>('none');
153
-
154
- <XtsViewer3D
155
- ref={viewer}
156
- config={config}
157
- // The same tools work in the 3D view and the 2D plan view.
158
- projection={plan2D ? 'orthographic' : 'perspective'}
159
- measurement={{
160
- tool, // 'distance' | 'angle' | 'path' | 'trackDistance' | 'annotation'
161
- snap: { enabled: true, radiusPx: 14 },
162
- onMeasurementCreate: (m, result) => console.log(m.kind, result.text),
163
- }}
164
- />;
165
-
166
- // …or place and read measurements without any user interaction:
167
- viewer.current.measurements.add({
168
- kind: 'distance',
169
- points: [{ positionMm: [0, 0, 200] }, { positionMm: [1000, 0, 200] }],
170
- });
171
- viewer.current.measurements.results(); // [{ value: 1000, text: '1 000.0 mm', … }]
172
- const saved = viewer.current.measurements.toJSON(); // persist, restore with fromJSON()
173
- ```
174
-
175
- See [Measurements + annotations](docs/USING-THE-COMPONENT.md#17-measurements--annotations)
176
- for the tools, the snap kinds and the full API.
177
-
178
- ## Gallery
179
-
180
- | | |
181
- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
182
- | ![Two parallel lines](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/08-multi-track.webp)<br>A packaging line and a buffer loop side by side, placed with `trackTransform`. | ![Stations and areas](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/09-stations-areas.webp)<br>Station tubes with their stop markers, ghost movers parked on the stops, and cleanroom / manual-access zones. |
183
- | ![Dimensions and stations](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/10-dimensions.webp)<br>Dimensions along the track, stepped out from the modules they measure. | ![Stator heatmap](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/11-stator-heatmap.webp)<br>Vertex-colour gradient along the centerline, fed from `(positionMm, value)` samples. |
184
- | ![Collision detection](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/12-collision.webp)<br>Two movers of the line parked at a 1 mm overlap — `checkMoverCollisions()` reports the penetration. | ![Drive status](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/13-drive-status.webp)<br>Emissive tint on the GLB plus camera-facing warning and error icons, on modules and movers alike. |
185
- | ![Feed segments](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/14-feed-segments.webp)<br>`feedSegmentHighlights` tints whole electrical strands; the pink one wraps the loop seam. | ![Measurements and annotations](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/15-measurements.webp)<br>A distance across the loop with ΔX/ΔY guides and two annotations, placed through `viewerRef.current.measurements`. |
186
- | ![Selection](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/16-selection.webp)<br>Selection tints the GLB itself — two modules green, one mover orange — rather than overlaying a wireframe. | ![Shadows and IBL](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/18-shadows.webp)<br>Opt-in PCF-soft shadows over the procedurally built indoor environment. |
187
- | ![Many movers](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/19-perf-stress.webp)<br>750 movers on three ovals, animated at 60 Hz with no React commits in steady state. | |
188
-
189
- ![Top-down export](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/17-plan-view.webp)
190
-
191
- `exportScreenshot({ mode: 'top-down' })` — orthographic, fitted to the scene
192
- AABB. The same view is available live through `projection="orthographic"`.
193
-
194
- ## Troubleshooting
195
-
196
- ### Modules render as yellow boxes and movers as blue boxes
197
-
198
- The viewer is showing wireframe placeholders because no GLB was accepted. The
199
- console also reports `THREE.WARNING: Multiple instances of Three.js being
200
- imported`, and no `models/*.glb` requests appear in the network panel.
201
-
202
- This happens when a transitive dependency pins its own copy of `three`. Two
203
- copies mean two `THREE.*` namespaces, and the `instanceof` checks inside
204
- `useGLTF` reject every parsed scene across that boundary.
205
-
206
- Deduplicate `three` in your bundler:
207
-
208
- ```ts
209
- // vite.config.ts
210
- export default defineConfig({
211
- plugins: [react()],
212
- resolve: { dedupe: ['three'] },
213
- });
214
- ```
215
-
216
- For Webpack and Next.js, alias `three` to your root `node_modules/three`.
217
- [Bundler configuration](docs/USING-THE-COMPONENT.md#bundler-configuration--deduplicate-three)
218
- has the full snippets and explains how to clear Vite's pre-bundle cache
219
- afterwards.
220
-
221
- ## Documentation
222
-
223
- - [Using the component](docs/USING-THE-COMPONENT.md) — every prop, every ref
224
- method, asset hosting, recipes and performance tuning.
225
- - [Adding a module](docs/ADDING-A-MODULE.md) — taking a new module, mover or
226
- tool type from STP file to calibrated GLB.
227
- - [Performance](docs/PERFORMANCE.md) — the asset compression pipeline and the
228
- runtime budget.
229
- - [Releasing](docs/RELEASING.md) — the automated publish flow.
230
-
231
- ## Development
232
-
233
- The repo is a pnpm workspace; `pnpm-lock.yaml` is the source of truth, so
234
- `npm install` will not work. Install pnpm 10, then:
235
-
236
- ```bash
237
- pnpm install
238
- pnpm lint
239
- pnpm typecheck
240
- pnpm test
241
- pnpm run test:coverage
242
- pnpm build
243
- ```
244
-
245
- `pnpm dev` starts the playground at `http://127.0.0.1:5173`. It exercises every
246
- feature and lets you switch demos, drive movers, toggle lighting and shadows,
247
- compose multi-track layouts and live-edit calibration overrides.
248
-
249
- `pnpm docs:capture-screenshots` re-shoots the whole image strip on this page
250
- from that playground, unattended — see
251
- [docs/screenshots](docs/screenshots/README.md).
252
-
253
- ### Asset pipeline
254
-
255
- CAD sources in `stepfiles/*.stp` are converted to runtime GLBs plus per-asset
256
- JSON sidecars:
257
-
258
- ```bash
259
- pnpm assets:convert # STP → GLB
260
- pnpm assets:optimize # meshopt compression
261
- pnpm assets:apply-materials # CAD placeholder colours → real PBR materials
262
- pnpm assets:verify-colors # no authored STEP colour got lost
263
- pnpm assets:inspect # refresh docs/data/glb-inspection.json
264
- pnpm assets:generate-sidecars # module .meta.json origin corrections
265
- pnpm assets:generate-mover-sidecars
266
- ```
267
-
268
- The CAD sources carry Autodesk Inventor placeholder colours, not product
269
- colours — an AT2 motor module's whole solid is styled #696969 — so
270
- `assets:apply-materials` maps them onto the named PBR library in
271
- `scripts/materialLibrary.mjs` (anodised aluminium, stainless steel, hardened
272
- rail steel, plastics, printed labels, LEDs). It rewrites only the GLB's JSON
273
- chunk, leaving geometry and the compressed binary chunk byte-identical, and is
274
- idempotent. See [docs/ADDING-A-MODULE.md](docs/ADDING-A-MODULE.md).
275
-
276
- The generators skip existing files so hand-tuned calibration is never
277
- overwritten; pass `--force` to regenerate. The release pipeline never runs them
278
- and reads the sidecars read-only.
279
-
280
- ### Layout
281
-
282
- ```text
283
- src/ Library source — this is what ships
284
- components/ <XtsViewer3D> and the internal scene tree
285
- geometry/ Path math, ChainBuilder, normalizeXtsConfig
286
- assets/ AssetManifest, AssetLoader, SidecarLoader
287
- interaction/ SelectionManager
288
- packages/assets/ Sibling npm package: GLB-only mirror
289
- playground/ Vite app exercising every feature
290
- public/models/ GLBs and .meta.json calibration sidecars
291
- stepfiles/ Source CAD files (not published)
292
- scripts/ Asset pipeline and version-sync utilities
293
- docs/ VitePress site and guides
294
- ```
295
-
296
- ### Releasing
297
-
298
- Every push to `main` runs [semantic-release](https://semantic-release.gitbook.io/),
299
- which derives the version from Conventional Commit messages, writes
300
- `CHANGELOG.md` and publishes both packages. Never run `npm version`, edit the
301
- changelog or publish by hand.
302
-
303
- | Prefix | Bump |
304
- | -------------------------------------------------- | ----- |
305
- | `fix:` / `perf:` | patch |
306
- | `feat:` | minor |
307
- | `feat!:` / `fix!:` / `BREAKING CHANGE:` footer | major |
308
- | `chore:` / `docs:` / `ci:` / `refactor:` / `test:` | none |
309
-
310
- Preview with `pnpm release:dry-run`. [Releasing](docs/RELEASING.md) covers the
311
- plugin chain, the calibration-safety guard and the manual fallback.
312
-
313
- ## License
314
-
315
- MIT — see [LICENSE](LICENSE).
1
+ # beckhoff-xts-viewer-3d
2
+
3
+ [![npm](https://img.shields.io/npm/v/beckhoff-xts-viewer-3d.svg)](https://www.npmjs.com/package/beckhoff-xts-viewer-3d)
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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
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
+
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.
12
+
13
+ It covers the same ground as the official 2D `Beckhoff.TwinCAT.HMI.XTS.Controls`
14
+ viewer, using real CAD geometry in three dimensions.
15
+
16
+ ![A 5 m XTS packaging line rendered by the viewer](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/01-packaging-line.webp)
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
+
45
+ ```tsx
46
+ import { XtsViewer3D } from 'beckhoff-xts-viewer-3d';
47
+
48
+ <XtsViewer3D
49
+ config={{
50
+ processingUnits: [
51
+ {
52
+ objectId: 0,
53
+ moverType: 'AT9014_0055',
54
+ parts: [
55
+ {
56
+ objectId: 0,
57
+ globalNumber: 0,
58
+ modules: [
59
+ { moduleType: 'AT2001_0250', globalNumber: 1 },
60
+ { moduleType: 'AT2000_0250', globalNumber: 2 },
61
+ { moduleType: 'AT2050_0500', globalNumber: 3 },
62
+ { moduleType: 'AT2050_0501', globalNumber: 4 },
63
+ { moduleType: 'AT2001_0250', globalNumber: 5 },
64
+ { moduleType: 'AT2000_0250', globalNumber: 6 },
65
+ { moduleType: 'AT2050_0500', globalNumber: 7 },
66
+ { moduleType: 'AT2050_0501', globalNumber: 8 },
67
+ ],
68
+ },
69
+ ],
70
+ movers: [{ index: 0, id: 0, partOid: 0, partPositionMm: 200 }],
71
+ },
72
+ ],
73
+ }}
74
+ />;
75
+ ```
76
+
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.
81
+
82
+ ## Module catalogue
83
+
84
+ Every image on this page comes out of the component itself, through the same
85
+ `exportScreenshot()` a consumer calls, and shows a layout a line would
86
+ actually be built as — a 5 m racetrack with four stations, a buffer loop
87
+ beside it. See [docs/screenshots](docs/screenshots/README.md) for the capture
88
+ script and [`playground/src/configs.ts`](playground/src/configs.ts) for the
89
+ layouts themselves.
90
+
91
+ | | | |
92
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | ![Standard AT](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/02-standard-at.webp)<br>**Standard AT** — AT2001 infeed, AT2000 straights and the AT2050 180° clothoid reversal. | ![Eco AT2200](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/03-eco-at2200.webp)<br>**Eco AT2200 / AT2202** — 500 mm straights, which pair naturally with the 500 mm reversal. | ![NCT with AT8200 tools](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/04-nct-tools.webp)<br>**NCT AT2002 / AT2102** — high modules carrying AT8200 tool carriers on the movers. |
94
+ | ![Hygienic ATH](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/05-hygienic-ath.webp)<br>**Hygienic ATH** — stainless housings, sealed joints and the ATH9011 mover. | ![Hepco GFX2](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/06-hepco-gfx.webp)<br>**Hepco GFX2** — the procedural GFX rail profile with a GFX2 1TC carriage instead of the Beckhoff guiding rail. | ![Material close-up](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/07-materials-closeup.webp)<br>**Real materials, not CAD colours** — anodised housing, stator packs, printed type plate with its LED row. |
95
+
96
+ The GLBs ship with a named PBR material library rather than the CAD sources'
97
+ placeholder colours — see [Asset pipeline](#asset-pipeline).
98
+
99
+ ## Features
100
+
101
+ **Geometry and layout.** The full Beckhoff module and mover catalogue —
102
+ Standard AT, Eco AT2200, NCT (AT2002 / AT2102 with AT8200 tools), Hygienic ATH
103
+ and Hepco GFX2 — with straights, ±22.5° and ±45° curves and the AT2050 / ATH2050
104
+ 180° clothoid reversal. Module-to-module C0/C1 continuity is pinned by golden
105
+ fixtures. Per-XPU `trackTransform` places independent lines in one scene, and
106
+ `positionFrame` remaps every position value into the host's coordinate
107
+ convention without touching the rest of the config.
108
+
109
+ **Mover motion.** `setMoverPositions()` writes into a per-component store that
110
+ `useFrame` drains directly into the three.js scene graph, so a 60 Hz drive loop
111
+ produces no React renders. An instanced fast path batches mover bodies into a
112
+ single draw call where per-mover scene nodes are not needed.
113
+
114
+ **Rendering.** ACES filmic tone mapping, image-based lighting from a
115
+ procedurally built indoor environment, anisotropic filtering and optional
116
+ PCF-soft shadows, all switchable through `display.*`. SSAO and bloom are
117
+ available when the optional post-processing peers are installed.
118
+
119
+ **Interaction and status.** Click selection for modules and movers, tinting the
120
+ GLB itself rather than overlaying wireframes; drive-status blink plus
121
+ camera-facing warning and error icons; feed-segment (Einspeisestrang) tinting
122
+ that wraps the seam of a closed loop; stations, areas, dimensions and info bars;
123
+ stop-position ghost movers; and a stator heatmap driven by
124
+ `(positionMm, value)` samples.
125
+
126
+ **Measuring and annotating.** CAD-style tools — point-to-point distance with
127
+ optional ΔX/ΔY/ΔZ guides, angle, polyline, and distance _along_ the XTS path —
128
+ picked in the scene with snapping to geometry corners and edges as well as to
129
+ mover centres, module boundaries, station stops and the track path itself.
130
+ Annotations pin a label anywhere, including to a mover, where they follow it
131
+ while it drives. Everything works the same in the 2D plan view, renders into
132
+ screenshots and video frames, and is fully drivable from
133
+ `viewerRef.current.measurements`.
134
+
135
+ **Analysis.** Sub-millimetre mover collision detection, as a one-shot call or a
136
+ continuous monitor, plus module-level collision probes.
137
+
138
+ **Export.** `exportScreenshot()` renders offscreen at any resolution with MSAA,
139
+ in current-camera, top-down or saved-camera mode. `beginFrameCapture()` opens a
140
+ reusable session whose `grab()` returns a frame synchronously without image
141
+ encoding, and keeps producing frames while the window is minimised. Both bypass
142
+ the post-processing chain; the result reports whether that happened.
143
+
144
+ **Camera.** Orthographic top-down projection for a live 2D plan view, an
145
+ animated `focusOn()` for stations, areas, movers, modules or the whole scene,
146
+ and an opt-in X / Y / Z orientation gizmo.
147
+
148
+ ### Measurements and annotations
149
+
150
+ ```tsx
151
+ const viewer = useRef<XtsViewer3DRef>(null);
152
+ const [tool, setTool] = useState<MeasurementTool>('none');
153
+
154
+ <XtsViewer3D
155
+ ref={viewer}
156
+ config={config}
157
+ // The same tools work in the 3D view and the 2D plan view.
158
+ projection={plan2D ? 'orthographic' : 'perspective'}
159
+ measurement={{
160
+ tool, // 'distance' | 'angle' | 'path' | 'trackDistance' | 'annotation'
161
+ snap: { enabled: true, radiusPx: 14 },
162
+ onMeasurementCreate: (m, result) => console.log(m.kind, result.text),
163
+ }}
164
+ />;
165
+
166
+ // …or place and read measurements without any user interaction:
167
+ viewer.current.measurements.add({
168
+ kind: 'distance',
169
+ points: [{ positionMm: [0, 0, 200] }, { positionMm: [1000, 0, 200] }],
170
+ });
171
+ viewer.current.measurements.results(); // [{ value: 1000, text: '1 000.0 mm', … }]
172
+ const saved = viewer.current.measurements.toJSON(); // persist, restore with fromJSON()
173
+ ```
174
+
175
+ See [Measurements + annotations](docs/USING-THE-COMPONENT.md#17-measurements--annotations)
176
+ for the tools, the snap kinds and the full API.
177
+
178
+ ## Gallery
179
+
180
+ | | |
181
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
182
+ | ![Two parallel lines](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/08-multi-track.webp)<br>A packaging line and a buffer loop side by side, placed with `trackTransform`. | ![Stations and areas](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/09-stations-areas.webp)<br>Station tubes with their stop markers, ghost movers parked on the stops, and cleanroom / manual-access zones. |
183
+ | ![Dimensions and stations](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/10-dimensions.webp)<br>Dimensions along the track, stepped out from the modules they measure. | ![Stator heatmap](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/11-stator-heatmap.webp)<br>Vertex-colour gradient along the centerline, fed from `(positionMm, value)` samples. |
184
+ | ![Collision detection](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/12-collision.webp)<br>Two movers of the line parked at a 1 mm overlap — `checkMoverCollisions()` reports the penetration. | ![Drive status](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/13-drive-status.webp)<br>Emissive tint on the GLB plus camera-facing warning and error icons, on modules and movers alike. |
185
+ | ![Feed segments](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/14-feed-segments.webp)<br>`feedSegmentHighlights` tints whole electrical strands; the pink one wraps the loop seam. | ![Measurements and annotations](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/15-measurements.webp)<br>A distance across the loop with ΔX/ΔY guides and two annotations, placed through `viewerRef.current.measurements`. |
186
+ | ![Selection](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/16-selection.webp)<br>Selection tints the GLB itself — two modules green, one mover orange — rather than overlaying a wireframe. | ![Shadows and IBL](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/18-shadows.webp)<br>Opt-in PCF-soft shadows over the procedurally built indoor environment. |
187
+ | ![Many movers](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/19-perf-stress.webp)<br>750 movers on three ovals, animated at 60 Hz with no React commits in steady state. | |
188
+
189
+ ![Top-down export](https://cdn.jsdelivr.net/npm/beckhoff-xts-viewer-3d/docs/screenshots/17-plan-view.webp)
190
+
191
+ `exportScreenshot({ mode: 'top-down' })` — orthographic, fitted to the scene
192
+ AABB. The same view is available live through `projection="orthographic"`.
193
+
194
+ ## Troubleshooting
195
+
196
+ ### Modules render as yellow boxes and movers as blue boxes
197
+
198
+ The viewer is showing wireframe placeholders because no GLB was accepted. The
199
+ console also reports `THREE.WARNING: Multiple instances of Three.js being
200
+ imported`, and no `models/*.glb` requests appear in the network panel.
201
+
202
+ This happens when a transitive dependency pins its own copy of `three`. Two
203
+ copies mean two `THREE.*` namespaces, and the `instanceof` checks inside
204
+ `useGLTF` reject every parsed scene across that boundary.
205
+
206
+ Deduplicate `three` in your bundler:
207
+
208
+ ```ts
209
+ // vite.config.ts
210
+ export default defineConfig({
211
+ plugins: [react()],
212
+ resolve: { dedupe: ['three'] },
213
+ });
214
+ ```
215
+
216
+ For Webpack and Next.js, alias `three` to your root `node_modules/three`.
217
+ [Bundler configuration](docs/USING-THE-COMPONENT.md#bundler-configuration--deduplicate-three)
218
+ has the full snippets and explains how to clear Vite's pre-bundle cache
219
+ afterwards.
220
+
221
+ ## Documentation
222
+
223
+ - [Using the component](docs/USING-THE-COMPONENT.md) — every prop, every ref
224
+ method, asset hosting, recipes and performance tuning.
225
+ - [Adding a module](docs/ADDING-A-MODULE.md) — taking a new module, mover or
226
+ tool type from STP file to calibrated GLB.
227
+ - [Performance](docs/PERFORMANCE.md) — the asset compression pipeline and the
228
+ runtime budget.
229
+ - [Releasing](docs/RELEASING.md) — the automated publish flow.
230
+
231
+ ## Development
232
+
233
+ The repo is a pnpm workspace; `pnpm-lock.yaml` is the source of truth, so
234
+ `npm install` will not work. Install pnpm 10, then:
235
+
236
+ ```bash
237
+ pnpm install
238
+ pnpm lint
239
+ pnpm typecheck
240
+ pnpm test
241
+ pnpm run test:coverage
242
+ pnpm build
243
+ ```
244
+
245
+ `pnpm dev` starts the playground at `http://127.0.0.1:5173`. It exercises every
246
+ feature and lets you switch demos, drive movers, toggle lighting and shadows,
247
+ compose multi-track layouts and live-edit calibration overrides.
248
+
249
+ `pnpm docs:capture-screenshots` re-shoots the whole image strip on this page
250
+ from that playground, unattended — see
251
+ [docs/screenshots](docs/screenshots/README.md).
252
+
253
+ ### Asset pipeline
254
+
255
+ CAD sources in `stepfiles/*.stp` are converted to runtime GLBs plus per-asset
256
+ JSON sidecars:
257
+
258
+ ```bash
259
+ pnpm assets:convert # STP → GLB
260
+ pnpm assets:optimize # meshopt compression
261
+ pnpm assets:apply-materials # CAD placeholder colours → real PBR materials
262
+ pnpm assets:verify-colors # no authored STEP colour got lost
263
+ pnpm assets:inspect # refresh docs/data/glb-inspection.json
264
+ pnpm assets:generate-sidecars # module .meta.json origin corrections
265
+ pnpm assets:generate-mover-sidecars
266
+ ```
267
+
268
+ The CAD sources carry Autodesk Inventor placeholder colours, not product
269
+ colours — an AT2 motor module's whole solid is styled #696969 — so
270
+ `assets:apply-materials` maps them onto the named PBR library in
271
+ `scripts/materialLibrary.mjs` (anodised aluminium, stainless steel, hardened
272
+ rail steel, plastics, printed labels, LEDs). It rewrites only the GLB's JSON
273
+ chunk, leaving geometry and the compressed binary chunk byte-identical, and is
274
+ idempotent. See [docs/ADDING-A-MODULE.md](docs/ADDING-A-MODULE.md).
275
+
276
+ The generators skip existing files so hand-tuned calibration is never
277
+ overwritten; pass `--force` to regenerate. The release pipeline never runs them
278
+ and reads the sidecars read-only.
279
+
280
+ ### Layout
281
+
282
+ ```text
283
+ src/ Library source — this is what ships
284
+ components/ <XtsViewer3D> and the internal scene tree
285
+ geometry/ Path math, ChainBuilder, normalizeXtsConfig
286
+ assets/ AssetManifest, AssetLoader, SidecarLoader
287
+ interaction/ SelectionManager
288
+ packages/assets/ Sibling npm package: GLB-only mirror
289
+ playground/ Vite app exercising every feature
290
+ public/models/ GLBs and .meta.json calibration sidecars
291
+ stepfiles/ Source CAD files (not published)
292
+ scripts/ Asset pipeline and version-sync utilities
293
+ docs/ VitePress site and guides
294
+ ```
295
+
296
+ ### Releasing
297
+
298
+ Every push to `main` runs [semantic-release](https://semantic-release.gitbook.io/),
299
+ which derives the version from Conventional Commit messages, writes
300
+ `CHANGELOG.md` and publishes both packages. Never run `npm version`, edit the
301
+ changelog or publish by hand.
302
+
303
+ | Prefix | Bump |
304
+ | -------------------------------------------------- | ----- |
305
+ | `fix:` / `perf:` | patch |
306
+ | `feat:` | minor |
307
+ | `feat!:` / `fix!:` / `BREAKING CHANGE:` footer | major |
308
+ | `chore:` / `docs:` / `ci:` / `refactor:` / `test:` | none |
309
+
310
+ Preview with `pnpm release:dry-run`. [Releasing](docs/RELEASING.md) covers the
311
+ plugin chain, the calibration-safety guard and the manual fallback.
312
+
313
+ ## License
314
+
315
+ MIT — see [LICENSE](LICENSE).