beckhoff-xts-viewer-3d 4.10.0 → 5.1.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 +176 -659
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +880 -17
- package/dist/index.d.ts +880 -17
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/screenshots/README.md +14 -14
- package/package.json +54 -29
package/README.md
CHANGED
|
@@ -3,21 +3,45 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/beckhoff-xts-viewer-3d)
|
|
4
4
|
[](https://www.npmjs.com/package/beckhoff-xts-viewer-3d-assets)
|
|
5
5
|
[](LICENSE)
|
|
6
|
-
[](https://github.com/philippleidig/beckhoff-xts-viewer-3d/actions/workflows/ci.yml)
|
|
7
7
|
|
|
8
|
-
A
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
viewer
|
|
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
|

|
|
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,120 @@ 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
|
-
|
|
56
|
-
`beckhoff-xts-viewer-3d-assets` release on jsDelivr
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
| <br>**Multi-track placement** — two independent XTS lines composed via `trackTransform`. | <br>**Stations + Areas** — cleanroom / safety-area zone overlays, station tubes with stop markers. |
|
|
169
|
-
| <br>**Stator heatmap** — vertex-colour gradient along the centerline, fed from your live drive currents. | <br>**Sub-mm collision detection** — continuous monitor with banner; pair-wise 1D arc-length test on the shared chain. |
|
|
170
|
-
| <br>**Drive status** — emissive blink at 1 Hz on the GLB itself + camera-facing 3D icons (▲ warning, ⊙ error). | <br>**Perf stress** — 750 movers animated at 60 Hz with zero React commits in steady state. |
|
|
171
|
-
| <br>**PCF-soft shadows + IBL** — opt-in shadows on a transparent canvas; image-based lighting on by default. | <br>**`exportScreenshot('top-down')`** — orthographic, AABB-fit, mirrors the 2D viewer convention. |
|
|
172
|
-
| <br>**Feed segments (Einspeisestränge)** — `feedSegmentHighlights` tints whole strands; the pink one wraps the loop seam. | |
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## Installation
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
npm install beckhoff-xts-viewer-3d
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
Peer dependencies (you almost certainly have these already):
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
npm install react react-dom three
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Compatibility:
|
|
189
|
-
|
|
190
|
-
- React ≥ 18 (tested on 19)
|
|
191
|
-
- Three.js ≥ 0.150
|
|
192
|
-
- Modern bundler (Vite, Webpack, Next.js, Remix, Astro, plain CRA — all fine)
|
|
193
|
-
|
|
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
|
-
```
|
|
240
|
-
|
|
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:
|
|
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
|
+
## Features
|
|
83
|
+
|
|
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.
|
|
91
|
+
|
|
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.
|
|
96
|
+
|
|
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.
|
|
101
|
+
|
|
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.
|
|
108
|
+
|
|
109
|
+
**Measuring and annotating.** CAD-style tools — point-to-point distance with
|
|
110
|
+
optional ΔX/ΔY/ΔZ guides, angle, polyline, and distance _along_ the XTS path —
|
|
111
|
+
picked in the scene with snapping to geometry corners and edges as well as to
|
|
112
|
+
mover centres, module boundaries, station stops and the track path itself.
|
|
113
|
+
Annotations pin a label anywhere, including to a mover, where they follow it
|
|
114
|
+
while it drives. Everything works the same in the 2D plan view, renders into
|
|
115
|
+
screenshots and video frames, and is fully drivable from
|
|
116
|
+
`viewerRef.current.measurements`.
|
|
117
|
+
|
|
118
|
+
**Analysis.** Sub-millimetre mover collision detection, as a one-shot call or a
|
|
119
|
+
continuous monitor, plus module-level collision probes.
|
|
120
|
+
|
|
121
|
+
**Export.** `exportScreenshot()` renders offscreen at any resolution with MSAA,
|
|
122
|
+
in current-camera, top-down or saved-camera mode. `beginFrameCapture()` opens a
|
|
123
|
+
reusable session whose `grab()` returns a frame synchronously without image
|
|
124
|
+
encoding, and keeps producing frames while the window is minimised. Both bypass
|
|
125
|
+
the post-processing chain; the result reports whether that happened.
|
|
126
|
+
|
|
127
|
+
**Camera.** Orthographic top-down projection for a live 2D plan view, an
|
|
128
|
+
animated `focusOn()` for stations, areas, movers, modules or the whole scene,
|
|
129
|
+
and an opt-in CAD ViewCube.
|
|
130
|
+
|
|
131
|
+
### Measurements and annotations
|
|
358
132
|
|
|
359
133
|
```tsx
|
|
360
134
|
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
|
-
```
|
|
385
|
-
|
|
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);
|
|
135
|
+
const [tool, setTool] = useState<MeasurementTool>('none');
|
|
398
136
|
|
|
399
137
|
<XtsViewer3D
|
|
138
|
+
ref={viewer}
|
|
400
139
|
config={config}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
},
|
|
140
|
+
// The same tools work in the 3D view and the 2D plan view.
|
|
141
|
+
projection={plan2D ? 'orthographic' : 'perspective'}
|
|
142
|
+
measurement={{
|
|
143
|
+
tool, // 'distance' | 'angle' | 'path' | 'trackDistance' | 'annotation'
|
|
144
|
+
snap: { enabled: true, radiusPx: 14 },
|
|
145
|
+
onMeasurementCreate: (m, result) => console.log(m.kind, result.text),
|
|
474
146
|
}}
|
|
475
147
|
/>;
|
|
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
148
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
movers: [
|
|
515
|
-
{ index: 0, id: 0, partOid: 0, partPositionMm: 0 },
|
|
516
|
-
],
|
|
517
|
-
},
|
|
518
|
-
]
|
|
149
|
+
// …or place and read measurements without any user interaction:
|
|
150
|
+
viewer.current.measurements.add({
|
|
151
|
+
kind: 'distance',
|
|
152
|
+
points: [{ positionMm: [0, 0, 200] }, { positionMm: [1000, 0, 200] }],
|
|
153
|
+
});
|
|
154
|
+
viewer.current.measurements.results(); // [{ value: 1000, text: '1 000.0 mm', … }]
|
|
155
|
+
const saved = viewer.current.measurements.toJSON(); // persist, restore with fromJSON()
|
|
519
156
|
```
|
|
520
157
|
|
|
521
|
-
|
|
522
|
-
|
|
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.
|
|
158
|
+
See [Measurements + annotations](docs/USING-THE-COMPONENT.md#17-measurements--annotations)
|
|
159
|
+
for the tools, the snap kinds and the full API.
|
|
532
160
|
|
|
533
|
-
|
|
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.
|
|
161
|
+
## Gallery
|
|
568
162
|
|
|
569
|
-
|
|
163
|
+
| | |
|
|
164
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
165
|
+
| <br>Two independent lines composed with `trackTransform`. | <br>Station tubes with stop markers, and zone overlays for cleanroom or safety areas. |
|
|
166
|
+
| <br>Vertex-colour gradient along the centerline, fed from live drive currents. | <br>Continuous pair-wise arc-length collision monitoring. |
|
|
167
|
+
| <br>Emissive blink on the GLB plus camera-facing warning and error icons. | <br>750 movers animated at 60 Hz with no React commits in steady state. |
|
|
168
|
+
| <br>Opt-in PCF-soft shadows on a transparent canvas, image-based lighting on by default. | <br>`exportScreenshot({ mode: 'top-down' })` — orthographic and AABB-fit. |
|
|
169
|
+
| <br>`feedSegmentHighlights` tints whole electrical strands; the pink one wraps the loop seam. | |
|
|
570
170
|
|
|
571
171
|
## Troubleshooting
|
|
572
172
|
|
|
573
|
-
### Modules render as yellow boxes
|
|
173
|
+
### Modules render as yellow boxes and movers as blue boxes
|
|
574
174
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
No `models/*.glb` requests show up in the Network panel.
|
|
175
|
+
The viewer is showing wireframe placeholders because no GLB was accepted. The
|
|
176
|
+
console also reports `THREE.WARNING: Multiple instances of Three.js being
|
|
177
|
+
imported`, and no `models/*.glb` requests appear in the network panel.
|
|
579
178
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
`
|
|
583
|
-
copies produce two `THREE.*` namespaces; `useGLTF`'s `instanceof`
|
|
584
|
-
checks fail across the boundary and silently reject every parsed scene.
|
|
179
|
+
This happens when a transitive dependency pins its own copy of `three`. Two
|
|
180
|
+
copies mean two `THREE.*` namespaces, and the `instanceof` checks inside
|
|
181
|
+
`useGLTF` reject every parsed scene across that boundary.
|
|
585
182
|
|
|
586
|
-
|
|
587
|
-
`resolve.dedupe`:
|
|
183
|
+
Deduplicate `three` in your bundler:
|
|
588
184
|
|
|
589
185
|
```ts
|
|
590
186
|
// vite.config.ts
|
|
@@ -594,167 +190,88 @@ export default defineConfig({
|
|
|
594
190
|
});
|
|
595
191
|
```
|
|
596
192
|
|
|
597
|
-
Webpack
|
|
598
|
-
[
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
---
|
|
193
|
+
For Webpack and Next.js, alias `three` to your root `node_modules/three`.
|
|
194
|
+
[Bundler configuration](docs/USING-THE-COMPONENT.md#bundler-configuration--deduplicate-three)
|
|
195
|
+
has the full snippets and explains how to clear Vite's pre-bundle cache
|
|
196
|
+
afterwards.
|
|
603
197
|
|
|
604
198
|
## Documentation
|
|
605
199
|
|
|
606
|
-
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
-
|
|
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.
|
|
617
|
-
|
|
618
|
-
---
|
|
200
|
+
- [Using the component](docs/USING-THE-COMPONENT.md) — every prop, every ref
|
|
201
|
+
method, asset hosting, recipes and performance tuning.
|
|
202
|
+
- [Adding a module](docs/ADDING-A-MODULE.md) — taking a new module, mover or
|
|
203
|
+
tool type from STP file to calibrated GLB.
|
|
204
|
+
- [Performance](docs/PERFORMANCE.md) — the asset compression pipeline and the
|
|
205
|
+
runtime budget.
|
|
206
|
+
- [Releasing](docs/RELEASING.md) — the automated publish flow.
|
|
619
207
|
|
|
620
|
-
## Development
|
|
208
|
+
## Development
|
|
621
209
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
`npm install -g pnpm@10` (or any other installer), then:
|
|
210
|
+
The repo is a pnpm workspace; `pnpm-lock.yaml` is the source of truth, so
|
|
211
|
+
`npm install` will not work. Install pnpm 10, then:
|
|
625
212
|
|
|
626
213
|
```bash
|
|
627
214
|
pnpm install
|
|
628
|
-
pnpm
|
|
215
|
+
pnpm lint
|
|
629
216
|
pnpm typecheck
|
|
217
|
+
pnpm test
|
|
218
|
+
pnpm run test:coverage
|
|
219
|
+
pnpm build
|
|
630
220
|
```
|
|
631
221
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
```
|
|
222
|
+
`pnpm dev` starts the playground at `http://127.0.0.1:5173`. It exercises every
|
|
223
|
+
feature and lets you switch demos, drive movers, toggle lighting and shadows,
|
|
224
|
+
compose multi-track layouts and live-edit calibration overrides.
|
|
652
225
|
|
|
653
226
|
### Asset pipeline
|
|
654
227
|
|
|
655
|
-
CAD
|
|
656
|
-
|
|
228
|
+
CAD sources in `stepfiles/*.stp` are converted to runtime GLBs plus per-asset
|
|
229
|
+
JSON sidecars:
|
|
657
230
|
|
|
658
231
|
```bash
|
|
659
|
-
pnpm assets:convert # STP → GLB
|
|
232
|
+
pnpm assets:convert # STP → GLB
|
|
660
233
|
pnpm assets:inspect # refresh docs/data/glb-inspection.json
|
|
661
|
-
pnpm assets:generate-sidecars # module .meta.json
|
|
234
|
+
pnpm assets:generate-sidecars # module .meta.json origin corrections
|
|
662
235
|
pnpm assets:generate-mover-sidecars
|
|
663
236
|
```
|
|
664
237
|
|
|
665
|
-
The
|
|
666
|
-
|
|
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
|
|
238
|
+
The generators skip existing files so hand-tuned calibration is never
|
|
239
|
+
overwritten; pass `--force` to regenerate. The release pipeline never runs them
|
|
240
|
+
and reads the sidecars read-only.
|
|
702
241
|
|
|
703
|
-
|
|
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
|
|
714
|
-
|
|
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.
|
|
242
|
+
### Layout
|
|
720
243
|
|
|
721
244
|
```text
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
245
|
+
src/ Library source — this is what ships
|
|
246
|
+
components/ <XtsViewer3D> and the internal scene tree
|
|
247
|
+
geometry/ Path math, ChainBuilder, normalizeXtsConfig
|
|
248
|
+
assets/ AssetManifest, AssetLoader, SidecarLoader
|
|
249
|
+
interaction/ SelectionManager
|
|
250
|
+
packages/assets/ Sibling npm package: GLB-only mirror
|
|
251
|
+
playground/ Vite app exercising every feature
|
|
252
|
+
public/models/ GLBs and .meta.json calibration sidecars
|
|
253
|
+
stepfiles/ Source CAD files (not published)
|
|
254
|
+
scripts/ Asset pipeline and version-sync utilities
|
|
255
|
+
docs/ VitePress site and guides
|
|
727
256
|
```
|
|
728
257
|
|
|
729
|
-
|
|
730
|
-
run `npm publish` by hand. Commit-type → bump:
|
|
258
|
+
### Releasing
|
|
731
259
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
| `feat!:` / `fix!:` / `BREAKING CHANGE:` footer | major |
|
|
737
|
-
| `chore:` / `docs:` / `ci:` / `refactor:` / `test:` | none (no release) |
|
|
260
|
+
Every push to `main` runs [semantic-release](https://semantic-release.gitbook.io/),
|
|
261
|
+
which derives the version from Conventional Commit messages, writes
|
|
262
|
+
`CHANGELOG.md` and publishes both packages. Never run `npm version`, edit the
|
|
263
|
+
changelog or publish by hand.
|
|
738
264
|
|
|
739
|
-
|
|
740
|
-
|
|
265
|
+
| Prefix | Bump |
|
|
266
|
+
| -------------------------------------------------- | ----- |
|
|
267
|
+
| `fix:` / `perf:` | patch |
|
|
268
|
+
| `feat:` | minor |
|
|
269
|
+
| `feat!:` / `fix!:` / `BREAKING CHANGE:` footer | major |
|
|
270
|
+
| `chore:` / `docs:` / `ci:` / `refactor:` / `test:` | none |
|
|
741
271
|
|
|
742
|
-
Preview
|
|
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
|
-
---
|
|
272
|
+
Preview with `pnpm release:dry-run`. [Releasing](docs/RELEASING.md) covers the
|
|
273
|
+
plugin chain, the calibration-safety guard and the manual fallback.
|
|
753
274
|
|
|
754
275
|
## License
|
|
755
276
|
|
|
756
277
|
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.
|