@omrips/viewstl 2.0.0-rc.8 → 2.0.0-rc.9

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/docs/v2-usage.md CHANGED
@@ -1,993 +1,945 @@
1
- # ViewSTL 2 — native WebGL2 plugin
2
-
3
- Version: `2.0.0-rc.5` (ViewSTL v2.00 release candidate, GitHub and npm).
4
- On `main`, the new plugin is in `src/` and `build/`; the legacy tree is in
5
- `legacy/v1/`. The unchanged `master` branch retains original legacy paths.
6
- The v2 website is available at https://preview.viewstl.com/.
7
-
8
- ## Quick start
9
-
10
- Download the plugin ZIP from the [GitHub release](https://github.com/omrips/viewstl/releases/tag/v2.0.0-rc.5),
11
- then copy its complete `build/` folder to `/viewstl-v2/` on your website.
12
- Paste these three lines into your page's `<body>`:
13
-
14
- ```html
15
- <div id="viewer" style="height:500px"></div>
16
- <script src="/viewstl-v2/viewstl.min.js"></script>
17
- <script>new StlViewer(document.getElementById('viewer'), { models: [{ filename: 'model.stl' }] });</script>
18
- ```
19
-
20
- Replace `model.stl` with your model's path and open the page over HTTP(S).
21
- You can also drag local files onto the viewer. The full distribution folder is
22
- required so the viewer can find its worker and optional format assets.
23
-
24
- ## Browser script
25
-
26
- Copy the complete `build/` directory to your server, including the
27
- worker, its `chunks/` and `cad/` directories and third-party notices. Then:
28
-
29
- ```html
30
- <div id="viewer" style="width:100%;height:500px"></div>
31
- <script src="/viewstl-v2/stl_viewer.min.js"></script>
32
- <script>
33
- const viewer = new StlViewer(document.getElementById('viewer'), {
34
- models: [{ id: 0, filename: '/models/part.stl', color: '#568bdc' }],
35
- load_error_callback: message => console.error(message)
36
- });
37
- </script>
38
- ```
39
-
40
- `viewstl.min.js` is an identical classic-script alias. The canvas requires
41
- a container with a nonzero height. Files must be served over HTTP(S),
42
- including during local development; browsers restrict workers on `file:` URLs.
43
-
44
- ## ES modules and npm
45
-
46
- ```js
47
- import StlViewer from './viewstl-v2/viewstl.js';
48
-
49
- const viewer = new StlViewer(container, {
50
- models: [{ id: 0, local_file: fileInput.files[0] }]
51
- });
52
-
53
- await viewer.when_idle();
54
- console.log(viewer.get_model_info(0));
55
- ```
56
-
57
- The package contains ESM, CommonJS and TypeScript entry points. Importing
58
- the package is safe during server-side rendering; constructing a viewer
59
- requires a browser DOM and WebGL2.
60
-
61
- Install with `npm install viewstl`. No version suffix is required: the `latest`
62
- tag selects the current release candidate. The established `@omrips/viewstl`
63
- package is also updated with the same plugin; `npm install @omrips/viewstl`
64
- works without a version suffix. Use either package name consistently in imports
65
- and asset-copy paths; there is no need to install both.
66
-
67
- With a bundler, copy the complete `node_modules/viewstl/build/`
68
- directory to your app's public assets (for example `public/viewstl-v2/`), keeping
69
- the worker, `chunks/`, `cad/` and notices together. Set `worker_url` to its served
70
- URL, or provide your bundler's worker factory.
71
- Do not assume a bundler copies a module worker's dynamic chunks merely
72
- because it sees a URL string.
73
-
74
- ```js
75
- import StlViewer from 'viewstl';
76
-
77
- const viewer = new StlViewer(container, {
78
- worker_url: '/viewstl-v2/viewstl.worker.js'
79
- });
80
- ```
81
-
82
- For Vite, the optional factory form is:
83
-
84
- ```js
85
- import ViewerWorker from 'viewstl/worker?worker';
86
- const viewer = new StlViewer(container, {
87
- worker_factory: () => new ViewerWorker()
88
- });
89
- ```
90
-
91
- The static-assets/`worker_url` integration above was verified in production
92
- builds using Vite 8.3.4 and Webpack 5.112.0, including STEP/IGES and lazy archive
93
- assets. The optional Vite worker-factory example is separate integration guidance
94
- and was not part of those checks. A factory must return a new worker for each
95
- scheduled job and arrange for all dynamic chunks and CAD assets to be served.
96
- The CommonJS entry point also requires
97
- `worker_url` or `worker_factory` because it cannot infer a browser asset URL.
98
-
99
- ## Node.js model analysis
100
-
101
- Run `npm install viewstl`. Node.js 20+ resolves the analysis
102
- worker and optional CAD WASM directly inside the package; there is no asset-copy
103
- step for server use.
104
-
105
- ```js
106
- import { analyzeFile } from 'viewstl/node';
107
- const report = await analyzeFile('part.stl', { units: 'mm' });
108
- console.log(report.dimensions_mm, report.surface_area_mm2, report.volume_mm3);
109
- ```
110
-
111
- CommonJS is supported with `const { analyzeFile } = require('viewstl/node')`.
112
- The `/node` entry point is separate from the browser viewer and has no DOM,
113
- WebGL or Three.js dependency. It supports STL, OBJ, 3MF, STEP/STP and IGES/IGS.
114
- STEP/IGES measurements describe the tessellated mesh, not exact CAD surfaces.
115
- Scene files, rendering, thumbnails and server-side exporting are outside this API.
116
-
117
- ### Input and units
118
-
119
- `analyzeFile(path, options)` accepts a local regular-file path or a `file:` URL
120
- object. `analyzeBuffer(data, options)` accepts a Node Buffer, Uint8Array or
121
- ArrayBuffer, and requires `filename` or `format` to identify the format:
122
-
123
- ```js
124
- import { analyzeBuffer } from 'viewstl/node';
125
- const report = await analyzeBuffer(uploadBuffer, { filename: 'part.stl', units: 'mm' });
126
- ```
127
-
128
- The input buffer is copied without modifying or detaching the caller's data.
129
- HTTP(S) URLs and network-share paths are rejected. The module does not fetch
130
- external model resources, run a web service, or write input files to disk.
131
- An application's own upload handler determines how files reach its server and
132
- whether they are stored. Browser-selected files sent to a server are uploads;
133
- the preview website continues to perform its checks on the user's device.
134
-
135
- STL and OBJ carry no reliable physical units. Supply `units: 'mm'`, `'cm'`, `'m'`,
136
- `'in'`, `'ft'` or `'um'` when the input scale is known. With no scale, `units` and
137
- all physical measurement fields (`dimensions_mm`, `surface_area_mm2`,
138
- `volume_mm3`) are null; the unit check is inconclusive. The generic `dimensions`,
139
- `surface_area` and `volume` fields then use input coordinate units. When units
140
- are known, these generic fields also use mm, mm² and mm³.
141
-
142
- 3MF and CAD file units are honored and normalized to mm automatically. They
143
- cannot be overridden with a different input scale. `units_source` is `'file'`,
144
- `'option'` or null. `volume_valid` describes validation of the mesh volume;
145
- it does not confirm a physical scale or guarantee printability. Invalid or
146
- unverified volume is returned as null. `level: 'basic'` skips detailed
147
- intersection and validated-volume checks; the default is `'detailed'`.
148
-
149
- ### Jobs, limits and cancellation
150
-
151
- The convenience functions share an analyzer with two active workers, up to
152
- 16 waiting jobs, and at most 128 MiB of copied buffer data waiting in the queue.
153
- Create an independent analyzer when the host needs different concurrency:
154
-
155
- ```js
156
- import { createAnalyzer } from 'viewstl/node';
157
- const analyzer = createAnalyzer({ concurrency: 1, max_queue: 4 });
158
- try {
159
- const reports = await Promise.all(paths.map(path => analyzer.analyzeFile(path, {
160
- units: 'mm', signal: controller.signal, timeout_ms: 60000,
161
- limits: { max_file_bytes: 32 * 1024 ** 2, max_memory_bytes: 512 * 1024 ** 2 }
162
- })));
163
- } finally {
164
- await analyzer.dispose();
165
- }
166
- ```
167
-
168
- Here `paths` is the application's array of file paths and `controller` is its
169
- AbortController. Keep submitted batches within the configured queue capacity.
170
- `concurrency` is 1–8; `max_queue` may be zero; `max_queued_bytes` controls queued
171
- buffer copies. Exceeding queue admission rejects with `ERR_ANALYZER_BUSY`.
172
- Use a shared analyzer for a service rather than creating an unbounded number
173
- of independent analyzers per request.
174
-
175
- Per-job defaults are 64 MiB input, 1,000,000 triangles, 128 MiB expanded archive
176
- data, 1,024 archive entries and a 1 GiB estimated working-set budget. These are
177
- independent ceilings: the memory estimate can reject a mesh below the triangle
178
- ceiling. Override them in `limits` with `max_file_bytes`, `max_triangles`,
179
- `max_uncompressed_bytes`, `max_archive_entries` and `max_memory_bytes`.
180
- Unknown limit names are rejected. CAD input/output and WASM-heap checks are
181
- also applied, and retained CAD memory counts towards the analysis budget.
182
-
183
- `timeout_ms` defaults to 120,000 and covers waiting, reading, parsing and checks.
184
- Expiry terminates the worker and rejects with `ERR_ANALYSIS_TIMEOUT`.
185
- `signal` cancels queued or active work with an `AbortError` (`ABORT_ERR`).
186
- `dispose()` cancels outstanding jobs and waits for their workers to terminate.
187
- Workers are also terminated after success or failure before their slots are
188
- reused, releasing parser and CAD state. Processing failures do not disable
189
- the analyzer.
190
-
191
- `max_time_ms` separately limits the mesh-check stage (30,000 by default, up to
192
- 120,000). It may yield a partial report with `limit_reason: 'time'`. Inspect
193
- `summary` and each check's status; an incomplete check is not a pass.
194
- `on_progress` receives phase and elapsed-time updates. The optional `profile`
195
- uses the same FDM build-volume, overhang, bed-contact and sampled wall-thickness
196
- settings as browser print checks. FDM results remain estimates.
197
-
198
- Memory limits are conservative admission estimates plus a worker JS-heap limit,
199
- not a hard limit on process RSS. ArrayBuffers and WASM memory are not fully
200
- covered by the JS-heap limit. Size host/container limits for total concurrency;
201
- use process/container isolation when serving untrusted uploads. The library
202
- does not provide an HTTP endpoint, authentication or a filesystem sandbox.
203
-
204
- ## Full app controls
205
-
206
- `mountViewstlApp` is the optional app controller, exported from the same
207
- `viewstl.js` bundle as `StlViewer` (and as a global in the classic script).
208
- It owns the file queue, memory admission, cancellation, selection, scene
209
- placement, translation/rotation/depth gestures, viewing modes, sectioning,
210
- colors, grid, PNG downloads, keyboard handling, and control state.
211
-
212
- ```js
213
- import { mountViewstlApp } from './viewer/viewstl.js';
214
- const app = mountViewstlApp(document, {
215
- sample_url: new URL('./sample-vase.stl', import.meta.url).href,
216
- sample_name: 'Twist vase.stl'
217
- });
218
- await app.openFiles(fileInput.files);
219
- console.log(app.getState()); // detached metadata snapshot, no geometry copies
220
- app.dispose(); // cancels work and releases listeners, workers and WebGL
221
- ```
222
-
223
- This controller binds the preview's HTML control layout; the host supplies
224
- markup and CSS. The reference layout is `Viewstl/site/index.html` in the sibling
225
- website workspace. `requiredControls` in `src/app.js` validates the control
226
- IDs before registering handlers. Missing controls and duplicate mounts produce
227
- a descriptive error. Custom integrations without that layout use `StlViewer`.
228
- Branding, page metadata, advertisements and static help/privacy text belong
229
- to the host page. The preview's JavaScript is only the initialization call.
230
-
231
- For an embedded app, pass an `HTMLElement` containing the layout. Queries,
232
- keyboard events and drops are scoped to that root. For multiple roots, prefix
233
- each layout's IDs and label/ARIA references and pass the matching `id_prefix`;
234
- the controller separates radio groups as well. `dispose()` permits remounting
235
- and prevents late reads from updating the new app.
236
-
237
- Optional shortcut buttons can reuse the app actions without extra host-side
238
- JavaScript. Use `data-open-file` for Open, and `data-viewstl-shortcut` with `save`,
239
- `select`, `front`, `right`, `top`, `back`, `left`, `bottom` or `isometric` for the
240
- other actions. Save requires the normal save-dialog control group. These
241
- buttons do not add required control IDs, so existing app layouts remain valid.
242
- Use real buttons with descriptive labels/tooltips. The host supplies responsive
243
- CSS; the ViewSTL website shows its shortcut row only on wide desktop screens.
244
-
245
- `select` toggles model selection/editing and reflects it through `aria-pressed`.
246
- It opens the compact Move/Rotate/Depth box immediately, starting with Move.
247
- Camera gestures pause; a click selects a model, and dragging a model selects
248
- and manipulates it with the current tool. Empty-canvas drags do nothing.
249
- Changing the tool or selected model keeps the compact box open. Turning Select
250
- off, Done, Escape or a camera shortcut hides it and restores camera controls.
251
- A viewport resize also exits when all selection shortcuts become hidden.
252
- This is temporary interaction state, not a saved scene property. The existing
253
- double-click and Models-list selection mechanisms remain available.
254
-
255
- `openFiles()` accepts Files or configured `{name, url, size?}` sources and
256
- resolves when the queue settles. File errors appear in the notice; success is
257
- reflected by `getState().models`. Other methods are `select(id)`, `remove(id)`,
258
- `cancelLoading()` and `clear()`. An omitted `sample_url` hides the sample action.
259
- `worker_url`, `worker_factory` and `worker_timeout` support alternate hosting,
260
- bundlers and CommonJS use.
261
-
262
- Loading the bundle does not create a canvas or worker. The first file creates
263
- the WebGL2 viewer. App source modules are bundled into `viewstl.js`; the host
264
- ships no separate memory/layout/transform helpers. Worker JS, archive chunks
265
- and CAD WASM remain separate plugin-owned assets to keep parsing off the main
266
- thread and avoid downloading CAD code for STL/OBJ users.
267
-
268
- `get_model_info()` also exposes copied source `bounds`, `has_colors`, and
269
- `source_units` (`null` for unitless sources), so consumers need not inspect
270
- internal geometry or retain its buffers.
271
-
272
- ## Rendering and file processing
273
-
274
- - WebGL2 only; no CanvasRenderer, WebGL1 or IE fallback.
275
- - No Three.js or other third-party library in the STL/OBJ rendering core.
276
- - ASCII/binary STL, VisCAM/MeshLab and Materialise colors, plus the legacy
277
- ViewSTL colored-STL convention.
278
- - OBJ polygon fan triangulation and negative indices. OBJ materials,
279
- textures and robust triangulation of concave polygons are not included.
280
- - Core 3MF meshes, nested components, build transforms, units, base
281
- materials and vertex colors. Root model relationships are followed.
282
- Texture/composite materials, external component parts and unsupported
283
- required extensions produce explicit errors rather than silent loss.
284
- - STEP/STP and IGES/IGS solid/surface import via the optional pinned
285
- Open CASCADE WASM reader. Units normalize to millimetres; assembly placements
286
- and mesh/face colors are retained. All surfaces in a file form one model.
287
- The display mesh is an approximation (relative deflection 0.001, angular
288
- deflection 0.5 radians), not an exact CAD editing or measurement kernel.
289
- Use self-contained exports: external part files are not fetched, and native
290
- SolidWorks/Solid Edge formats, feature history, PMI and curve-only files are
291
- unsupported. Incomplete external-reference assemblies may lack parts.
292
- - Deploy the entire `build/cad/` directory alongside `chunks/` and the worker.
293
- CAD code/WASM is loaded only for CAD files; local input bytes are never uploaded.
294
- Each job's worker is terminated on completion, cancellation, error or timeout.
295
- CAD admission estimates 128 MiB plus 40 times the input size. The WASM heap
296
- is separately capped to at most 512 MiB and to part of the remaining scene
297
- budget. Output arrays and triangle counts are also checked; these checks
298
- cannot predict exact peak browser/GPU memory for every CAD file.
299
- - A strict CSP must allow `script-src 'self' 'wasm-unsafe-eval'` for the worker;
300
- JavaScript `unsafe-eval` is not needed. Serve `.wasm` as `application/wasm`.
301
- `scripts/prepare-cad.mjs` replaces the pinned glue's dynamic bindings with
302
- static closures and adds its heap ceiling. The WASM is unmodified. Adapted
303
- glue, the recipe, license texts and source references ship with the assets.
304
- - VSJ JSON and VSB archives containing `json_data.vsj` and STL files.
305
- - ZIP/3MF uses separately loaded worker chunks containing pinned fflate,
306
- saxes and xmlchars. Those chunks are not fetched for ordinary STL/OBJ.
307
- - ZIP accepts stored and deflated entries, validates CRC/length and bounds
308
- expanded data. ZIP64, encryption and multi-disk archives are unsupported.
309
- - Local files never leave the browser. Remote URLs obey normal CORS and
310
- same-origin credential rules; there is no upload service or CORS proxy.
311
-
312
- Parsing, bounds, flat normals and initial requested smooth/edge geometry
313
- are calculated in workers. Results use transferable buffers. Workers are
314
- bounded and terminated after jobs, on cancellation, and on disposal.
315
-
316
- Changing an already-loaded model to smooth/edges/wireframe derives its
317
- additional data in a worker. That operation intentionally copies positions
318
- once, since the main thread retains them for synchronous export and picking.
319
- Nonuniform-scale area queries also require a cached synchronous scan. These
320
- are distinct from the initial file handoff, which does not clone geometry.
321
-
322
- Rendering runs only when needed, including during interaction and animation.
323
- Transparency uses model-level back-to-front sorting; intersecting transparent
324
- surfaces can still have ordering artifacts. STL export stores face color,
325
- so a 3MF triangle's per-vertex gradient cannot survive STL/VSB export exactly.
326
-
327
- ## Model animation
328
-
329
- Animation is included in `2.0.0-rc.5` on GitHub and in both npm packages.
330
- Playback remains inside the native WebGL2 plugin.
331
-
332
- After a model has loaded, one call starts continuous rotation:
333
-
334
- ```js
335
- await viewer.when_idle();
336
- viewer.animate_model(0, {
337
- delta: { rotationz: 2 * Math.PI, msec: 5000, loop: true },
338
- });
339
- ```
340
-
341
- The legacy `animation` model option also works. `delta` adds a change to the
342
- starting pose; `exact` interpolates toward an absolute target. They can operate
343
- together on different properties and finish independently:
344
-
345
- ```js
346
- viewer.animate_model(0, {
347
- delta: { rotationx: 1, rotationy: 1.1, rotationz: 1.2, msec: 1000, loop: true },
348
- exact: { x: 1000, y: 150, z: 200, scale: 5, msec: 3000 },
349
- });
350
-
351
- viewer.pause_model_animation(0); // Keep the pose and elapsed time.
352
- viewer.resume_model_animation(0);
353
- console.log(viewer.get_model_animation_state(0));
354
- viewer.animate_model(0, null); // Stop at the current pose; discard playback.
355
- ```
356
-
357
- Supported properties are `x/y/z`, `rotationx/y/z` in radians and
358
- `scale` or `scalex/scaley/scalez`. Durations are positive milliseconds, defaulting
359
- to 300. Scale deltas are additive, as in the legacy API; all resulting scales
360
- must stay positive. Only delta tracks repeat, accumulating their change each
361
- cycle. A negative looping scale delta is rejected. Delta and exact may not claim
362
- the same property. Inputs must be finite and within JavaScript's safe numeric
363
- range. Invalid replacements leave the current animation intact.
364
-
365
- Starting a new animation replaces the old one from the current pose. Manual
366
- position, rotation, scale and unit setters stop that model's playback first.
367
- Camera interaction, color and viewing-mode changes remain usable. Completed
368
- tracks no longer write their properties. Removal, `clean()` and `dispose()`
369
- release playback; paused/completed models do not keep the render loop running.
370
-
371
- Timing follows elapsed frame timestamps, not frame count. Hidden documents and
372
- lost WebGL contexts suspend playback without including that gap on resumption.
373
- User pause is independent of this automatic suspension. State queries return
374
- `status`, `elapsed_ms`, `suspended` and active `tracks`; unknown IDs return null.
375
-
376
- Set callbacks in the viewer options:
377
-
378
- ```js
379
- const viewer = new StlViewer(container, {
380
- animation_state_callback(id, state, reason) {
381
- console.log(id, state.status, reason);
382
- },
383
- animation_complete_callback(id, state) {
384
- console.log('Finished', id, state.elapsed_ms);
385
- },
386
- });
387
- ```
388
-
389
- Callbacks describe transitions, not every frame, and are deferred until the
390
- current operation finishes. Completion fires once when every track finishes;
391
- an animation with a looping track does not complete. Call playback controls on
392
- loaded models, for example after `when_idle()`.
393
-
394
- VSB/VSJ preserve the animation definition, original pose, elapsed phase, rotation
395
- pivot and pause flag. Both scene-loading APIs start incoming animations together
396
- after all incoming models load. VSJ still needs its referenced model files.
397
- Animated VSB entries retain unscaled mesh data and explicit scale metadata;
398
- static entries retain the previous scale-baked convention. This prevents scale
399
- being applied twice. Older viewers may read the pose but cannot restore the new
400
- playback-state extension exactly.
401
-
402
- PNG and modern `export_models()` capture the current pose without stopping
403
- playback. Legacy `get_stl_bin()` keeps its documented scale/centering-only export
404
- convention. Regular model files do not store animation. Print checks should run
405
- while the affected models are paused; subsequent motion makes the report stale.
406
-
407
- A self-contained example is in `examples/v2/animation.html` in the source
408
- checkout (serve with `pnpm start`). It includes Play, Pause, Resume, Stop and Reset.
409
-
410
- The app controller also supports an optional `animation-*` group (see the new
411
- site's HTML template): `animation-details`, `animation-fields`, `animation-axis`,
412
- `animation-seconds`, `animation-direction`, `animation-play`, `animation-pause`,
413
- `animation-stop`, `animation-status` and `animation-error`. Supply the whole group
414
- or omit it. It exposes selected-model spin around the mesh center, duration
415
- (0.1–3,600 seconds per turn), direction and playback buttons. Other models keep
416
- playing when selection changes. Manual tools capture the current animated pose.
417
- Readouts update at most four times per second; geometry and GPU buffers are reused.
418
-
419
- `mountViewstlApp` composes `animation_state_callback` and
420
- `animation_complete_callback` options with its own UI behavior. `app.getState()`
421
- includes each model's current transform and `animation` state. Ordinary files
422
- never auto-spin. Imported running animations are paused when the browser reports
423
- `prefers-reduced-motion: reduce`; explicit user playback remains available.
424
-
425
- App print checks pause running target models. Optional `print-resume` and
426
- `print-animation-note` elements expose resumption; closing the panel does not
427
- silently restart motion. Resuming clears stale results and keeps the panel open.
428
- Scene saving includes playback; PNG and regular model saving capture a pose.
429
-
430
- ## Existing API and intentional differences
431
-
432
- The existing constructor, model options and documented method names are
433
- implemented. See [the full inventory](v2-compatibility.md). Existing getters
434
- remain synchronous. `get_vsb_blob()` continues to return a Promise.
435
-
436
- - `width` / `height` override their `canvas_width` / `canvas_height` aliases.
437
- - `bg_color` overrides `bgcolor`.
438
- - `on_model_mousedown` overrides `on_model_mouseclick`; both setter names work.
439
- - `set_model_units(model_id, units, scale)` keeps the executable three-argument
440
- signature. The old documentation omitted `model_id` incorrectly.
441
- - `load_three_files: 'directory/'` remains a legacy alias for the worker
442
- directory, resolved against the page. It never loads Three.js. An explicit
443
- `worker_url` takes precedence; false/omitted uses the entry point's directory.
444
- - `jszip_path` and `jszip_utils_path` are accepted deprecated options, with
445
- no effect: archive support is shipped in the package's lazy worker chunks.
446
- No scripts are fetched from those legacy paths.
447
- - `get_model_info().dims` now updates after scaling. The old implementation
448
- returned stale dimensions even when area and volume had changed.
449
- - Zero IDs, zero coordinates, zero rotations and zero opacity are valid.
450
- Invalid positive-only scales, colors, transforms and IDs report errors.
451
- - Model-loading batches preserve the legacy final callback order:
452
- `all_loaded_callback` precedes the final `model_loaded_callback`.
453
- Failed/cancelled jobs settle the batch instead of leaving it hanging.
454
- An empty initial viewer does not emit `all_loaded_callback`.
455
- - `set_center_models` controls centering of subsequently loaded models.
456
- - Model data remains CPU-resident for synchronous export/picking and context
457
- restoration. `clean()` and `dispose()` release GPU buffers and worker jobs.
458
- - `dispose()` removes only the viewer's owned DOM/listeners, not its siblings.
459
- - The renderer, camera and models are native objects. Direct Three.js calls
460
- into `.camera`, `.scene`, `.renderer`, or `.models` are not compatible.
461
-
462
- STL export retains the legacy convention: geometry scale and centering are
463
- baked into vertices; scene position and rotation are not baked. VSB saves
464
- position/rotation in its manifest and avoids applying scale twice.
465
-
466
- Volume is a signed-tetrahedron mesh calculation reported as an absolute
467
- value, not a watertightness or printability certification. For inch models,
468
- area/volume follow the legacy conversion convention.
469
-
470
- ## Optional Three.js mesh bridge
471
-
472
- The ordinary viewer never imports Three.js. To request a real Three.js
473
- Mesh from `get_model_mesh`, pass your own instance:
474
-
475
- ```js
476
- const viewer = new StlViewer(container, { three: THREE });
477
- // after loading:
478
- const mesh = viewer.get_model_mesh(0);
479
- ```
480
-
481
- The bridge uses `options.three` or `globalThis.THREE`. With neither present,
482
- `get_model_mesh` reports a load error and returns null. It does not return
483
- a misleading imitation Mesh.
484
-
485
- Import supports static BufferGeometry meshes and legacy Geometry vertices/
486
- faces. It copies geometry and basic material color/opacity. Placement is
487
- controlled by the supplied model options, as in the old plugin. Skinning,
488
- instances, morph targets, child objects, multiple/textured/shader materials
489
- are rejected. Export returns an independent BufferGeometry Mesh. Dispose
490
- the returned mesh's geometry and material when finished with it.
491
-
492
- Use `get_model_data(id)` for a native copy of raw geometry, and
493
- `add_model({ data, ...options })` to add it without Three.js. Data does not
494
- include scene transforms; copy those options explicitly when duplicating.
495
-
496
- Use `get_image_blob()` for screenshots; direct `renderer.domElement.toDataURL`
497
- outside a draw is not guaranteed because continuous drawing-buffer
498
- preservation is disabled.
499
-
500
- ## Additional options and helpers
501
-
502
- | Option | Default |
503
- | --- | --- |
504
- | `worker_url` | Beside the ESM/classic script |
505
- | `worker_factory` | None |
506
- | `worker_count` | 2, allowed range 1–4 |
507
- | `worker_timeout` | 120,000 ms per job |
508
- | `pixel_ratio` | Device pixel ratio capped at 2 |
509
- | `max_file_bytes` | 256 MiB |
510
- | `max_triangles` | 5,000,000 |
511
- | `max_uncompressed_bytes` | 256 MiB per archive |
512
- | `max_archive_entries` | 1,024 |
513
- | `max_memory_bytes` | `Number.MAX_SAFE_INTEGER` (optional estimated working-set budget) |
514
-
515
- Limits are configurable positive integers. These are ceilings, not guarantees
516
- that every mobile device has enough memory for the maximum. Derived smooth
517
- normals/edge maps can use substantially more memory than flat rendering.
518
-
519
- `max_memory_bytes` adds conservative checks for parsed geometry, text parsing,
520
- archive expansion, expanded 3MF instances, and derived smooth/wire/edge maps.
521
- It estimates working memory, including GPU buffers; it does not measure free
522
- memory. Existing default byte/triangle limits remain unchanged for embedders.
523
- The ViewSTL preview site configures its own device-dependent limits: half the
524
- approximate `navigator.deviceMemory` report, or half a 2 GiB fallback if absent.
525
- This leaves a 50% margin against that estimate, not a guaranteed allocation or
526
- protection against an OS/browser killing an exhausted tab.
527
-
528
- Local binary STL files are parsed in chunks of at most 65,536 triangles
529
- (3.125 MiB input data) plus their 84-byte header. This avoids holding a complete
530
- input file and a second assembled copy alongside geometry. ASCII/OBJ/archive
531
- input is still read as a complete buffer; OBJ no longer creates a full array
532
- of every input line. Native GPU allocation errors clean up partial resources
533
- and report through `load_error_callback`.
534
-
535
- `when_ready` resolves after renderer initialization, before initial models
536
- finish. `when_idle()` waits for pending model, scene and derived-geometry
537
- work; inspect `load_error_callback` for individual failures. `get_image_blob`
538
- waits for that work, draws and returns a PNG Blob by default.
539
-
540
- Keyboard: arrows orbit; + / - zoom; F fits the scene. Mouse: drag orbits,
541
- shift/right/middle drag pans, wheel zooms. Touch: one finger rotates and
542
- two fingers pan/pinch. Real mobile-device validation remains necessary.
543
-
544
- For a same-origin standalone deployment, allow your script and worker assets
545
- in the site's Content Security Policy (`script-src 'self'; worker-src 'self'`),
546
- and permit intended remote model hosts in `connect-src`. SharedArrayBuffer,
547
- cross-origin isolation, eval and worker blob bootstrapping are not required.
548
-
549
- ## Additional views and section cuts
550
-
551
- The existing `flat`, `smooth` and `wireframe` display values remain supported.
552
- New display values are `edges` (solid with 30-degree feature edges), `mesh`
553
- (triangle overlay), `xray`, `studio` and `orientation` (blue front faces, red
554
- back faces). None requires Three.js. `view_edges` / `set_edges` retain the
555
- legacy one-degree edge threshold.
556
-
557
- ```js
558
- viewer.set_view(0, {display: 'studio', smooth_shading: true});
559
- await viewer.when_idle();
560
- viewer.set_smooth(0, false); // Independent of the selected modern view
561
- viewer.set_section(0, {axis: 'z', position: 0.5, flip: false});
562
- viewer.set_section(0, null); // Restore the complete model
563
- ```
564
-
565
- These fields are also accepted in model options, returned by `get_model_info`,
566
- and preserved by VSJ/VSB scene export. `set_display('flat')` and
567
- `set_display('smooth')` keep their legacy shading behavior. Smooth shading is
568
- ignored visually in wireframe and retained for the next surface view. Derived
569
- view changes commit after preparation succeeds; a rejected memory estimate or
570
- failed worker preserves the previous appearance. Wait for `when_idle()` and
571
- inspect `load_error_callback` as with model loading.
572
-
573
- Mesh overlay uses shader barycentric coordinates without an extra geometry
574
- buffer. Studio, orientation, X-ray and clipping also reuse existing geometry.
575
- Smooth normals, wireframe and feature edges are computed on demand in a worker
576
- and checked against the memory budget, including cached derived arrays.
577
-
578
- Section position is a fraction from 0 to 1 of the source model's bounding box
579
- along its chosen axis. The plane follows model transforms, clips picking and
580
- PNG rendering, and does not modify exported STL geometry or model statistics.
581
- The cut remains open, with an amber boundary highlight; no cap is generated.
582
- X-ray uses lightweight back-face/front-face alpha passes; it is an illustrative
583
- view, not exact order-independent transparency for intersecting surfaces.
584
- Face orientation describes winding relative to the camera, not an automatic
585
- watertightness or printability diagnosis.
586
-
587
- ## Optional streaming URL imports
588
-
589
- The plugin does not silently route existing `filename` requests through a server.
590
- To retrieve a public URL without depending on the source server's CORS policy,
591
- configure a same-origin relay and explicitly use `remote_url`:
592
-
593
- ```js
594
- const viewer = new StlViewer(container, {url_import_endpoint: '/api/import-url'});
595
- viewer.add_model({id: 0, remote_url: 'https://example.com/model.stl'});
596
- // Optional remote_format: 'stl', 'obj', '3mf', 'step', 'stp', 'iges', 'igs'.
597
- ```
598
-
599
- `mountViewstlApp` accepts the same endpoint and exposes `app.openURL(url, format)`.
600
- Its optional `url-dialog`/`url-form` controls are shown in the preview site's HTML.
601
- The site supplies markup, styles and configuration; URL interaction, loading,
602
- progress, cancellation and parsing live in the plugin. App jobs allow ten minutes
603
- by default; low-level viewer worker_timeout remains configurable as before.
604
-
605
- The relay receives the URL in a JSON POST body and streams the response without
606
- file storage. A signed URL's query string is not displayed as a model name or
607
- included in scene exports. Local files still never leave the browser. URL imports
608
- are a separate privacy path: the relay sees the remote bytes in transit.
609
-
610
- Binary STL is decoded incrementally in the worker. Other formats use bounded
611
- browser buffers, including archives and CAD. A complete model appears after
612
- download and validation; progressive partial rendering is not implemented.
613
- `get_model_info().file_bytes` reports the imported byte count when available.
614
- Downloaded names are metadata, not reloadable URLs; use VSB to package a scene
615
- containing these models. Embedders must provide their own compatible relay; no
616
- ViewSTL server is implicitly used by the library.
617
-
618
- When `url_import_endpoint` is configured, `mountViewstlApp` also accepts dropped
619
- links (`text/uri-list`), Firefox URL/title data, and plain HTTP/HTTPS URL text.
620
- URL drops use `openURL` and its existing queue, relay, progress, cancellation and
621
- error handling. Multiple URI-list links are deduplicated per drop. Local files
622
- take priority over accompanying URL metadata. Plain text is not searched for
623
- embedded links, HTML drag contents are not parsed, and drops into text inputs or
624
- editable content retain their normal behavior. This belongs to the optional app
625
- controller; the low-level viewer's existing file-drop contract is unchanged.
626
-
627
- The reference relay lives in the website repository's `relay/` directory. Its
628
- request is `{url, format, max_bytes}` and its response supplies X-ViewSTL-Format,
629
- percent-encoded X-ViewSTL-Filename, and optional Content-Length. All relay and
630
- parser failures use the existing error callback. Cancellation terminates the
631
- worker/fetch; the server then closes the upstream connection.
632
-
633
- ## Local mesh export
634
-
635
- The optional `mountViewstlApp` controller can bind a Save dialog using the
636
- `save-*` control group in the preview site's `site/index.html`. Include the entire
637
- group or omit it; incomplete groups fail before mounting. All event handling,
638
- selection, progress, cancellation and download dispatch compile into `viewstl.js`.
639
- The host supplies markup, styles and explanatory copy (format option `data-note`
640
- attributes and the CAD limitation paragraph). No Save UI is injected by the
641
- low-level viewer.
642
-
643
- The reference dialog exports only the current app selection for mesh formats,
644
- with positions/rotations applied and millimetre output. Unitless STL/OBJ inputs
645
- are assumed to be millimetres. Its optional color setting applies to OBJ/3MF;
646
- colored OBJ uses ZIP. It calls `download_models` with explicit model IDs, so
647
- closing an editing panel does not clear the export selection. VSB/VSJ always
648
- export the whole scene. `save-scope` is a text status element displaying the
649
- selected filename or whole-scene model count, not a scope dropdown. Cancel, Escape,
650
- the close button, backdrop dismissal, changing selection, starting an import,
651
- removing/clearing models and app disposal cancel an active export. Errors stay
652
- in the dialog and permit another format. Existing PNG controls remain separate.
653
-
654
- All exporters live in the plugin and run in disposable workers. They generate
655
- local Blobs; no relay, upload, server conversion or third-party network endpoint
656
- is used. Export modules and ZIP compression load only on demand. STEP/IGES writing
657
- uses JavaScript and does **not** download the CAD importer or WASM engine.
658
-
659
- ```js
660
- await viewer.when_idle();
661
- const controller = new AbortController();
662
- const result = await viewer.export_models({
663
- format: '3mf', // stl, obj, 3mf, step/stp, iges/igs
664
- model_ids: [0, 2], // omitted = all; 'selected' = selected model
665
- filename: 'assembly', // correct extension added/replaced automatically
666
- units: 'mm', // mm (default), inch
667
- apply_transforms: true, // default: bake position, rotation, scale and centering
668
- colors: true, // default for OBJ/3MF; ignored for STL/STEP/IGES
669
- signal: controller.signal,
670
- on_progress({ loaded, total, phase }) {
671
- // Triangle counts. phase is 'exporting' or 'complete'.
672
- // Encoding can reach total before ZIP packaging completes.
673
- },
674
- });
675
- // {blob, filename, format, units, model_ids, triangles, representation: 'mesh'}
676
- // Keep result.blob, or use the convenience method to initiate a browser download:
677
- await viewer.download_models({format: 'step', model_ids: 'selected'});
678
- // controller.abort() rejects an active/queued operation with AbortError.
679
- ```
680
-
681
- Exports capture the model state at dispatch; subsequent edits/removal do not
682
- change an in-flight export. Buffers used for rendering are never transferred or
683
- detached. No selection, an unknown ID, duplicate IDs or an unfinished model
684
- rejects the promise. Call `when_idle()` first to include pending imports. Errors
685
- reject the promise and progress goes only to the supplied callback; exporters do
686
- not show dialogs or invoke the viewer's import-error callback. A throwing progress
687
- callback rejects its export and terminates the worker. `clean()`/`dispose()` cancel
688
- active and queued jobs. The caller owns the returned Blob and its lifetime.
689
-
690
- `apply_transforms: false` exports source coordinates and their accumulated unit
691
- conversion, without centering, presentation movement/rotation or user scaling.
692
- Scene coordinates are treated as millimeters; `units: 'inch'` divides output
693
- coordinates by 25.4 and writes inch metadata where the format supports it.
694
- Unitless inputs must be interpreted/scaled appropriately by the embedding app.
695
- STL and OBJ have no standardized units; OBJ includes an informational comment.
696
-
697
- | Format | Output and scope |
698
- | --- | --- |
699
- | STL | Standard binary STL; selected meshes combined into one triangle stream. Attributes are zero, so no ambiguous vendor color encoding. |
700
- | OBJ | With `colors: true`, a ZIP containing `model.obj` and `materials.mtl`, including object groups, face normals, per-face colors and opacity. Vertex colors are averaged per face and colors/opacity quantized to 8-bit channels. With `colors: false`, a plain `.obj` file. |
701
- | 3MF | ZIP/XML package with separate mesh objects, indexed vertices, units and per-vertex color/opacity using the Materials extension. Requires non-degenerate triangles, paired consistently oriented edges, and positive volume. Open/inconsistently wound meshes reject; there is no automatic repair or full self-intersection validation. |
702
- | STEP | AP214 `AUTOMOTIVE_DESIGN`: separate named products made of planar triangle faces in open shells. Exact displayed mesh coordinates are written; this is a surface model, not a reconstructed CAD solid. |
703
- | IGES | IGES 5.3 bounded planes (108/form 1) with closed 3D polylines (106/form 12). Models use separate levels. Triangle surfaces, not stitched CAD solids. |
704
-
705
- STEP/IGES preserve mesh geometry and placement, but omit colors, textures,
706
- analytic curves, original CAD topology/assemblies and feature history. They work
707
- with open triangle meshes, but reject zero-area triangles. No F3D export is
708
- provided. A consuming site can describe these limitations in its own interface;
709
- the low-level export API is UI-free. CAD interoperability has been verified
710
- against Open CASCADE, not every commercial CAD application.
711
-
712
- Section cuts, camera orientation, grid, selection frames and viewing/shading modes
713
- are presentation settings and do not modify exported geometry. Use VSJ/VSB for
714
- ViewSTL scene settings. Texture/UV data discarded at import cannot be recreated.
715
-
716
- The scene memory budget includes pending exports. Output is encoded in chunks
717
- instead of building one large text string; Blobs still require browser storage
718
- and this API does not stream directly to a filesystem handle. Output obeys
719
- `max_file_bytes`, archives also obey `max_uncompressed_bytes` and
720
- `max_archive_entries`, and the complete selection obeys `max_triangles`.
721
- Conservative format-specific memory estimates can reject an export before copying
722
- geometry. Cancellation terminates its worker, including compression.
723
-
724
- The existing synchronous `get_stl_bin`/`download_model` and VSJ/VSB methods retain
725
- their legacy behavior. Use the new API for scene transforms, correct filename
726
- extensions, asynchronous execution and cancellation.
727
-
728
- ## Development and packaging
729
-
730
- ```sh
731
- pnpm install --frozen-lockfile
732
- pnpm build
733
- pnpm test
734
- pnpm check:types
735
- pnpm check:node-types
736
- pnpm check:legacy
737
- pnpm start
738
- pnpm pack
739
- ```
740
-
741
- Open `/examples/v2/` for the demo and `/tests/v2/browser.html` for real worker/
742
- WebGL checks. `pnpm pack` builds a local tarball; it does not publish anything.
743
- The package allowlist includes v2 artifacts, types, documentation and license
744
- notices. It excludes legacy snapshots, old dependencies, tests and server data.
745
-
746
- Before a stable release, validate real mobile hardware, Safari and Firefox,
747
- larger user fixtures, npm/bundler integrations and the integrations used by
748
- the production site. Automated and desktop browser results are recorded in
749
- [verification](v2-verification.md).
750
-
751
- ## Multiple models, colors and an infinite grid
752
-
753
- Use `center_models: false` to preserve the coordinates in assembly files, then call
754
- `add_model` / `add_models` to append files. `set_position` changes presentation
755
- coordinates without rewriting source geometry. The preview site's Models panel
756
- adds selection, individual removal and reversible side-by-side placement.
757
-
758
- ```js
759
- const viewer = new StlViewer(element, { center_models: false });
760
- viewer.add_models([{id: 0, local_file: partA}, {id: 1, local_file: partB}]);
761
- await viewer.when_idle();
762
- viewer.set_color(0, '#dd766b');
763
- viewer.set_color(0, null); // Restore embedded file colors (or neutral gray if absent).
764
- viewer.set_bg_color('#202d35');
765
- viewer.set_infinite_grid(true, 1, 0); // enabled, base spacing, Z height
766
- ```
767
-
768
- The infinite XY grid is a procedural shader. Its spacing adapts as the camera
769
- zooms and it fades near the horizon. Panning does not allocate a larger grid;
770
- opaque models occlude it. `set_grid` retains its existing finite-grid behavior.
771
- Infinite settings round trip through VSJ/VSB as `infinite_grid: {spacing,height}`.
772
- Colors change shader uniforms without rebuilding the mesh buffers.
773
-
774
- An explicit `max_memory_bytes` budget now covers the whole scene, including
775
- resident geometry, cached view arrays and pending derived geometry. Parsing is
776
- serialized under that budget so each new model receives the remaining allowance.
777
- Removing a model frees its allowance and cancels its pending derived work.
778
- These remain conservative estimates, not measurements of free RAM or GPU memory.
779
-
780
- ### Selecting a model for editing
781
-
782
- `set_selection(id)` draws a light 3D bounding frame using that model's transform.
783
- `set_selection(null)` clears it. The frame uses one 288-byte vertex buffer, follows
784
- rotation/translation/scale without rebuilding mesh buffers, and is excluded from
785
- PNG and scene exports. Removing or clearing its model releases the frame.
786
-
787
- `pick_model(clientX, clientY)` returns `{id, distance}` for the nearest visible
788
- surface, or `null` for empty space. Coordinates use the same viewport coordinate
789
- system as mouse events. It respects transforms, hidden models and section cuts.
790
- The preview calls it on double-click only; ordinary dragging does not run a mesh
791
- intersection scan. The existing legacy click callbacks retain their behavior.
792
-
793
- The preview provides Move, Rotate and Depth controls per model. Rotation uses
794
- degrees in the UI and compensates for the original mesh center; the plugin's
795
- `set_rotation` API continues to accept radians with its existing origin behavior.
796
- Depth translates along the eye-to-model ray while holding the camera fixed.
797
-
798
- ## Saving ViewSTL scenes
799
-
800
- Use `export_scene` for a Blob or `download_scene` for a browser download:
801
-
802
- ```js
803
- const result = await viewer.export_scene({
804
- format: 'vsb', // or 'vsj'
805
- filename: 'my-scene',
806
- signal: abortController.signal,
807
- on_progress({ loaded, total, phase }) {
808
- // Model counts; exporting, packaging (VSB only), complete.
809
- },
810
- });
811
- await viewer.download_scene({ format: 'vsj', filename: 'scene', force_basename: true });
812
- ```
813
-
814
- Both methods always capture the entire loaded scene. They return `{blob,
815
- filename, format, model_ids, triangles, representation: 'scene'}`. Pending loads,
816
- empty scenes, excess model/triangle counts, memory and output limits reject.
817
- Errors reject the Promise instead of using the import-error callback. Cancellation
818
- and throwing progress callbacks release the worker and memory reservation.
819
-
820
- VSB uses the existing ZIP layout: `json_data.vsj` plus one colored binary STL per
821
- model. Mesh encoding and compression run in a disposable worker using copied
822
- snapshot buffers; renderer buffers remain intact. Camera, background, grid,
823
- transforms, model colors/opacity, viewing modes and sections stay in the manifest.
824
- Original model names are kept as `orig_filename`. As with the legacy VSB format,
825
- STL geometry uses float32 positions and 5-bit per-face colors, taking the first
826
- vertex color of each triangle. Texture/UV data and original CAD topology/history
827
- are not reconstructed or included.
828
-
829
- VSJ is JSON settings and model references, without embedded geometry. The
830
- `force_basename` option defaults to false for API callers; the preview uses true
831
- to write only filenames. Referenced files must be supplied separately. A local-file
832
- reference alone does not grant browser access to a file on disk. VSB is the
833
- self-contained choice. The preview opens both formats through the file picker,
834
- drag and drop, and URL opening. Existing `get_vsj`, `get_vsb`, `get_vsb_blob` and their legacy
835
- download methods retain their signatures and behavior.
836
-
837
- In the app Save dialog, VSB/VSJ save the entire scene. STL/OBJ/3MF/STEP/IGES save
838
- only the selected model. There is no user-selectable scope dropdown. The low-level
839
- `export_models` API still permits explicit multiple-model selections for embedders.
840
-
841
- ## Opening ViewSTL scenes
842
-
843
- Use the Promise-based `import_scene` API for local files or public URLs:
844
-
845
- ```js
846
- const result = await viewer.import_scene(sceneFile, {
847
- files: companionFiles, // File[]: model files referenced by a local VSJ.
848
- signal: abortController.signal,
849
- on_progress({ loaded, total, phase }) {
850
- // reading-scene: reader progress; opening-model/complete: model counts.
851
- },
852
- });
853
- console.log(result.model_ids, result.scene, result.used_files);
854
-
855
- // Requires the viewer's same-origin url_import_endpoint configuration.
856
- await viewer.import_scene('https://example.com/scene.vsb');
857
- await viewer.import_scene('https://example.com/download?id=123', { format: 'vsj' });
858
- ```
859
-
860
- VSB archives contain `json_data.vsj` and their model files. VSJ manifests reference
861
- separate files: supplied local files match by exact relative path or unambiguous
862
- basename. Missing or ambiguous files reject with an actionable error. Absolute
863
- HTTP/HTTPS references use the configured URL relay. For a remote VSJ, relative
864
- references resolve against its final directory after redirects, also through the
865
- relay. Local files and manifests are not uploaded. The relay streams remote files
866
- without saving them. It still applies its request, size and network limits, so a
867
- scene with many remote references can hit the relay's rate limit.
868
-
869
- Imports append models with new IDs and restore camera, background/grid, transforms,
870
- colors/opacity, view modes, smoothing and sections. Saved centering is converted
871
- to equivalent model positions without changing existing models' centering. Embed
872
- canvas size and resize/mouse controls are not imported. The development animation
873
- update also restores per-model animation and preserves its original pivot.
874
- Scene reading and mesh parsing use workers and the existing memory/triangle/archive
875
- limits. Models become visible together after all have loaded. Errors, cancellation,
876
- or a throwing progress callback remove the incoming models and retain the existing
877
- scene. `clean()` and `dispose()` also cancel pending scene imports. Existing
878
- `load_vsj` and `load_vsb` retain their previous signatures and behavior.
879
-
880
- In the preview, choose or drop a local VSJ together with its referenced model files;
881
- companions used by the scene are not added twice. A VSB needs no separate files.
882
- Choose VSB/VSJ explicitly in Open URL for a URL without a scene extension. Scene
883
- opening, progress, cancellation and restored editing controls all live in the
884
- plugin's app controller; the website initializer remains minimal.
885
-
886
- ## Checking meshes for 3D printing
887
-
888
- The optional checks run locally on demand. They analyze the loaded triangle mesh
889
- from any supported format, including models restored from VSB/VSJ. CAD accuracy
890
- is limited by the imported tessellation. Analysis does not repair or modify models.
891
-
892
- ```js
893
- const controller = new AbortController();
894
- const report = await viewer.check_printability({
895
- model_ids: [selectedId], // Omit for all loaded models; 'selected' uses the editing selection.
896
- level: 'detailed', // 'basic' is the default.
897
- apply_transforms: true, // Default: current placement/rotation/scale, like mesh export.
898
- units_confirmed: true, // Confirm the current mm scale for unitless input.
899
- profile: { // Optional; omit for geometry checks only.
900
- technology: 'fdm',
901
- build_volume_mm: [220, 220, 250],
902
- min_wall_mm: 0.8, // User-chosen thresholds, not universal printing rules.
903
- overhang_degrees: 45 // Measured from vertical; +Z is print up.
904
- },
905
- max_time_ms: 30000, // Per model, maximum 120000.
906
- signal: controller.signal,
907
- on_progress: ({ phase, model_index, total_models }) => console.log(phase)
908
- });
909
- if (viewer.is_printability_current(report)) {
910
- viewer.show_printability(report, { model_id: selectedId, issue: 'boundary_edges' });
911
- }
912
- viewer.clear_printability();
913
- ```
914
-
915
- Basic checks cover degenerate and nearly degenerate triangles, duplicates, open
916
- boundaries, non-manifold edges and vertices, inconsistent winding, connected
917
- components and physical-unit provenance. Detailed checks add a native-JavaScript
918
- BVH for self-intersections (including coplanar overlaps), shell containment,
919
- orientation and enclosed volume. Separate components can be intentional. Hollow
920
- models require opposite cavity-shell orientation. Vertex connectivity uses exact
921
- source coordinates; small gaps are not silently welded closed. Intersection
922
- checks use the reported scale-dependent numerical tolerance; near-contact cases
923
- can be inconclusive. The existing 3MF export guards remain independent and unchanged.
924
-
925
- Optional FDM checks compare dimensions with a build-volume box, identify potential
926
- overhang regions, estimate bed contact and sample wall thickness. `line_width_mm`
927
- can supply the wall threshold when `min_wall_mm` is omitted. No printer defaults
928
- are assumed. Build fit tests current orientation dimensions, not actual placement,
929
- exclusion zones or alternative rotations. Bed contact virtually lowers each model
930
- to its minimum Z; the visible grid and camera do not define the print bed.
931
- Thickness tests at most 2,048 face centres using inward normal rays and can miss
932
- small features or oblique thin regions. No finding from those samples is explicitly
933
- inconclusive, never proof of a minimum thickness. Overhangs do not simulate
934
- bridging/supports; contact area does not predict adhesion/stability. Resin, toolpaths,
935
- cross-model collisions and Boolean unions are outside these checks.
936
-
937
- Reports include each check's `pass`, `issue`, `inconclusive` or `not_run` status,
938
- severity, counts, explanations, bounded highlight samples, dimensions, tolerance,
939
- timing and optional estimates. `volume_valid` means a geometrically validated
940
- enclosed volume; its physical size still depends on the `units` check. Overall
941
- status is `issues_found`, `incomplete` or `no_issues_found`. A basic report remains
942
- incomplete because detailed checks were not run. Errors and time/memory limits
943
- never imply a pass. This API does not certify a successful print.
944
-
945
- Workers and analysis modules load only when requested. Models are checked in
946
- sequence, with cooperative snapshot copying and aggregate memory admission.
947
- The working estimate is 64 MiB plus 2,400 bytes per triangle, with bounded report
948
- allowances; a model that can be viewed may be too large to analyze on the same
949
- device. One bounded report cache is retained, not the topology/BVH. Geometry,
950
- transforms, scale and unit changes invalidate reports; colors and camera changes
951
- do not. Removing a checked model, clean/dispose, AbortSignal cancellation or a
952
- throwing progress callback releases the active worker and reservations. Report
953
- metadata uses weak references so retaining a report cannot retain removed meshes.
954
-
955
- Highlights use temporary native WebGL line buffers and are excluded from PNG,
956
- mesh and scene exports. They show up to 128 faces or 256 edges per check, through
957
- occluding surfaces. Closing the site panel clears highlights. The preview exposes
958
- the feature under **Info → Check for 3D printing**, with selected/all model scope,
959
- basic/detailed depth and collapsed optional FDM settings. Unit confirmation resets
960
- when the checked scope or an existing result's geometry/transform changes. A complete
961
- optional `print-*` control group is bound by `mountViewstlApp`; hosts omitting the
962
- entire group keep their previous behavior. The website only supplies markup/styles.
963
-
964
- The check panel stays open while using the canvas, other controls or dialogs,
965
- loading files, selecting models and clearing the scene. Only its close button or
966
- Escape with focus inside the panel dismisses it. Reopening it from Info reveals
967
- and focuses the existing panel. Loading or changing the checked selection cancels
968
- an active check; stale results are cleared. Loading and empty scenes disable Run
969
- and show the current availability without closing the panel.
970
-
971
- ## Model size, units, duplicates, cameras and measurements (rc.6)
972
-
973
- All operations below are implemented inside the plugin. The optional site controls remain a thin HTML/CSS host.
974
-
975
- `viewer.resize_model(id, [2, 2, 2])` doubles the model about its world-space bounds center, preserving rotation and placement. Positive finite XYZ factors are required; independent factors allow nonuniform scaling. It stops only that model's animation. `get_model_size(id)` returns dimensions and the reset baseline in millimetres. `reset_model_size(id)` restores that baseline while retaining file-unit corrections.
976
-
977
- `viewer.set_file_units(id, 'inch')` reinterprets a unitless STL/OBJ mesh (default mm) without moving its center. Switching back reverses the conversion. Explicit 3MF/STEP/IGES units are normalized to mm during parsing and cannot be reinterpreted. `set_display_units('inch')` changes the optional app's size/Info readouts only; scene coordinates, position controls and the grid remain in mm. Existing `set_model_units` and `set_scale` retain their legacy semantics.
978
-
979
- `const copy = viewer.duplicate_model(id)` returns a new ID at the same pose, with independent appearance, transforms and metadata. Immutable geometry and matching GPU buffers are shared; memory admission remains conservative. The source reference remains unchanged for VSJ, while `display_name` identifies the copy. Copied animation definitions and phase are retained but paused; the original keeps playing.
980
-
981
- `viewer.set_camera_view('top')` fits the scene from front/back/left/right/top/bottom/isometric using Z up and the existing perspective projection. It never rotates the models. Camera bookmarks are explicit data:
982
-
983
- `viewer.set_camera_bookmarks([{name: 'Detail', camera: viewer.get_camera_state()}])`
984
-
985
- Use `get_camera_bookmarks()` to obtain a defensive copy and `set_camera_state(bookmark.camera)` to restore one. Up to 32 uniquely named views (80 characters per name) are supported. They are kept in memory and in scene exports, not localStorage.
986
-
987
- `const {area_mm2, volume_mm3, volume_valid, volume_reason} = await viewer.measure_model(id)`
988
-
989
- Measurements use workers and support `signal`, `max_time_ms` (default 30 seconds) and `on_progress`. Area includes all triangles at current scale. Volume is nullable: detailed topology, intersections and shell-orientation checks must establish a closed volume. An open/invalid mesh or an inconclusive/time-limited check never becomes a plausible-looking volume. Results are cached by mesh and scale; position/rotation do not invalidate them and uniform resizing updates cached measurements algebraically. Nonuniform area is recalculated in the worker. Memory limits can reject the calculation before buffers are copied. This is a bounded mesh analysis, not a guarantee of printability.
990
-
991
- VSB/VSJ retain size baselines, file-unit interpretation, display units, duplicate labels and bookmarks. VSB still embeds geometry; VSJ still needs the referenced source files. No second unit conversion occurs during import. Appending scenes merges bookmark names with numbered suffixes; a combined total above 32 rejects the incoming scene. Old files without the new optional fields continue to load. The `size_state.unit_factor` serialization field preserves source-unit correction separately from resize factors for exports with `apply_transforms:false`.
992
-
993
- The optional app control group is listed in `src/app-tools.js` (`toolsControls`). Provide the whole group or omit it when embedding older markup. The preview's `site/index.html` is a complete example. Models contains collapsed Size & units and Duplicate; View contains collapsed Camera & saved views; Info contains area and the volume calculation. No server is involved for local files or measurements.
1
+ # ViewSTL 2 — native WebGL2 plugin
2
+
3
+ Version: `2.0.0-rc.5` (ViewSTL v2.00 release candidate, GitHub and npm).
4
+ On `main`, the new plugin is in `src/` and `build/`; the legacy tree is in
5
+ `legacy/v1/`. The unchanged `master` branch retains original legacy paths.
6
+ The v2 website is available at https://preview.viewstl.com/.
7
+
8
+ ## Quick start
9
+
10
+ Download the plugin ZIP from the [GitHub release](https://github.com/omrips/viewstl/releases/tag/v2.0.0-rc.5),
11
+ then copy its complete `build/` folder to `/viewstl-v2/` on your website.
12
+ Paste these three lines into your page's `<body>`:
13
+
14
+ ```html
15
+ <div id="viewer" style="height:500px"></div>
16
+ <script src="/viewstl-v2/viewstl.min.js"></script>
17
+ <script>new StlViewer(document.getElementById('viewer'), { models: [{ filename: 'model.stl' }] });</script>
18
+ ```
19
+
20
+ Replace `model.stl` with your model's path and open the page over HTTP(S).
21
+ You can also drag local files onto the viewer. The full distribution folder is
22
+ required so the viewer can find its worker and optional format assets.
23
+
24
+ ## Browser script
25
+
26
+ Copy the complete `build/` directory to your server, including the
27
+ worker, its `chunks/` and `cad/` directories and third-party notices. Then:
28
+
29
+ ```html
30
+ <div id="viewer" style="width:100%;height:500px"></div>
31
+ <script src="/viewstl-v2/stl_viewer.min.js"></script>
32
+ <script>
33
+ const viewer = new StlViewer(document.getElementById('viewer'), {
34
+ models: [{ id: 0, filename: '/models/part.stl', color: '#568bdc' }],
35
+ load_error_callback: message => console.error(message)
36
+ });
37
+ </script>
38
+ ```
39
+
40
+ `viewstl.min.js` is an identical classic-script alias. The canvas requires
41
+ a container with a nonzero height. Files must be served over HTTP(S),
42
+ including during local development; browsers restrict workers on `file:` URLs.
43
+
44
+ ## ES modules and npm
45
+
46
+ ```js
47
+ import StlViewer from './viewstl-v2/viewstl.js';
48
+
49
+ const viewer = new StlViewer(container, {
50
+ models: [{ id: 0, local_file: fileInput.files[0] }]
51
+ });
52
+
53
+ await viewer.when_idle();
54
+ console.log(viewer.get_model_info(0));
55
+ ```
56
+
57
+ The package contains ESM, CommonJS and TypeScript entry points. Importing
58
+ the package is safe during server-side rendering; constructing a viewer
59
+ requires a browser DOM and WebGL2.
60
+
61
+ Install with `npm install viewstl`. No version suffix is required: the `latest`
62
+ tag selects the current release candidate. The established `@omrips/viewstl`
63
+ package is also updated with the same plugin; `npm install @omrips/viewstl`
64
+ works without a version suffix. Use either package name consistently in imports
65
+ and asset-copy paths; there is no need to install both.
66
+
67
+ With a bundler, copy the complete `node_modules/viewstl/build/`
68
+ directory to your app's public assets (for example `public/viewstl-v2/`), keeping
69
+ the worker, `chunks/`, `cad/` and notices together. Set `worker_url` to its served
70
+ URL, or provide your bundler's worker factory.
71
+ Do not assume a bundler copies a module worker's dynamic chunks merely
72
+ because it sees a URL string.
73
+
74
+ ```js
75
+ import StlViewer from 'viewstl';
76
+
77
+ const viewer = new StlViewer(container, {
78
+ worker_url: '/viewstl-v2/viewstl.worker.js'
79
+ });
80
+ ```
81
+
82
+ For Vite, the optional factory form is:
83
+
84
+ ```js
85
+ import ViewerWorker from 'viewstl/worker?worker';
86
+ const viewer = new StlViewer(container, {
87
+ worker_factory: () => new ViewerWorker()
88
+ });
89
+ ```
90
+
91
+ The static-assets/`worker_url` integration above was verified in production
92
+ builds using Vite 8.3.4 and Webpack 5.112.0, including STEP/IGES and lazy archive
93
+ assets. The optional Vite worker-factory example is separate integration guidance
94
+ and was not part of those checks. A factory must return a new worker for each
95
+ scheduled job and arrange for all dynamic chunks and CAD assets to be served.
96
+ The CommonJS entry point also requires
97
+ `worker_url` or `worker_factory` because it cannot infer a browser asset URL.
98
+
99
+ ## Node.js model analysis
100
+
101
+ Run `npm install viewstl`. Node.js 20+ resolves the analysis
102
+ worker and optional CAD WASM directly inside the package; there is no asset-copy
103
+ step for server use.
104
+
105
+ ```js
106
+ import { analyzeFile } from 'viewstl/node';
107
+ const report = await analyzeFile('part.stl', { units: 'mm' });
108
+ console.log(report.dimensions_mm, report.surface_area_mm2, report.volume_mm3);
109
+ ```
110
+
111
+ CommonJS is supported with `const { analyzeFile } = require('viewstl/node')`.
112
+ The `/node` entry point is separate from the browser viewer and has no DOM,
113
+ WebGL or Three.js dependency. It supports STL, OBJ, 3MF, STEP/STP and IGES/IGS.
114
+ STEP/IGES measurements describe the tessellated mesh, not exact CAD surfaces.
115
+ Scene files, rendering, thumbnails and server-side exporting are outside this API.
116
+
117
+ ### Input and units
118
+
119
+ `analyzeFile(path, options)` accepts a local regular-file path or a `file:` URL
120
+ object. `analyzeBuffer(data, options)` accepts a Node Buffer, Uint8Array or
121
+ ArrayBuffer, and requires `filename` or `format` to identify the format:
122
+
123
+ ```js
124
+ import { analyzeBuffer } from 'viewstl/node';
125
+ const report = await analyzeBuffer(uploadBuffer, { filename: 'part.stl', units: 'mm' });
126
+ ```
127
+
128
+ The input buffer is copied without modifying or detaching the caller's data.
129
+ HTTP(S) URLs and network-share paths are rejected. The module does not fetch
130
+ external model resources, run a web service, or write input files to disk.
131
+ An application's own upload handler determines how files reach its server and
132
+ whether they are stored. Browser-selected files sent to a server are uploads;
133
+ the preview website continues to perform its checks on the user's device.
134
+
135
+ STL and OBJ carry no reliable physical units. Supply `units: 'mm'`, `'cm'`, `'m'`,
136
+ `'in'`, `'ft'` or `'um'` when the input scale is known. With no scale, `units` and
137
+ all physical measurement fields (`dimensions_mm`, `surface_area_mm2`,
138
+ `volume_mm3`) are null; the unit check is inconclusive. The generic `dimensions`,
139
+ `surface_area` and `volume` fields then use input coordinate units. When units
140
+ are known, these generic fields also use mm, mm² and mm³.
141
+
142
+ 3MF and CAD file units are honored and normalized to mm automatically. They
143
+ cannot be overridden with a different input scale. `units_source` is `'file'`,
144
+ `'option'` or null. `volume_valid` describes validation of the mesh volume;
145
+ it does not confirm a physical scale or guarantee printability. Invalid or
146
+ unverified volume is returned as null. `level: 'basic'` skips detailed
147
+ intersection and validated-volume checks; the default is `'detailed'`.
148
+
149
+ ### Jobs, limits and cancellation
150
+
151
+ The convenience functions share an analyzer with two active workers, up to
152
+ 16 waiting jobs, and at most 128 MiB of copied buffer data waiting in the queue.
153
+ Create an independent analyzer when the host needs different concurrency:
154
+
155
+ ```js
156
+ import { createAnalyzer } from 'viewstl/node';
157
+ const analyzer = createAnalyzer({ concurrency: 1, max_queue: 4 });
158
+ try {
159
+ const reports = await Promise.all(paths.map(path => analyzer.analyzeFile(path, {
160
+ units: 'mm', signal: controller.signal, timeout_ms: 60000,
161
+ limits: { max_file_bytes: 32 * 1024 ** 2, max_memory_bytes: 512 * 1024 ** 2 }
162
+ })));
163
+ } finally {
164
+ await analyzer.dispose();
165
+ }
166
+ ```
167
+
168
+ Here `paths` is the application's array of file paths and `controller` is its
169
+ AbortController. Keep submitted batches within the configured queue capacity.
170
+ `concurrency` is 1–8; `max_queue` may be zero; `max_queued_bytes` controls queued
171
+ buffer copies. Exceeding queue admission rejects with `ERR_ANALYZER_BUSY`.
172
+ Use a shared analyzer for a service rather than creating an unbounded number
173
+ of independent analyzers per request.
174
+
175
+ Per-job defaults are 64 MiB input, 1,000,000 triangles, 128 MiB expanded archive
176
+ data, 1,024 archive entries and a 1 GiB estimated working-set budget. These are
177
+ independent ceilings: the memory estimate can reject a mesh below the triangle
178
+ ceiling. Override them in `limits` with `max_file_bytes`, `max_triangles`,
179
+ `max_uncompressed_bytes`, `max_archive_entries` and `max_memory_bytes`.
180
+ Unknown limit names are rejected. CAD input/output and WASM-heap checks are
181
+ also applied, and retained CAD memory counts towards the analysis budget.
182
+
183
+ `timeout_ms` defaults to 120,000 and covers waiting, reading, parsing and checks.
184
+ Expiry terminates the worker and rejects with `ERR_ANALYSIS_TIMEOUT`.
185
+ `signal` cancels queued or active work with an `AbortError` (`ABORT_ERR`).
186
+ `dispose()` cancels outstanding jobs and waits for their workers to terminate.
187
+ Workers are also terminated after success or failure before their slots are
188
+ reused, releasing parser and CAD state. Processing failures do not disable
189
+ the analyzer.
190
+
191
+ `max_time_ms` separately limits the mesh-check stage (30,000 by default, up to
192
+ 120,000). It may yield a partial report with `limit_reason: 'time'`. Inspect
193
+ `summary` and each check's status; an incomplete check is not a pass.
194
+ `on_progress` receives phase and elapsed-time updates. The optional `profile`
195
+ uses the same FDM build-volume, overhang, bed-contact and sampled wall-thickness
196
+ settings as browser print checks. FDM results remain estimates.
197
+
198
+ Memory limits are conservative admission estimates plus a worker JS-heap limit,
199
+ not a hard limit on process RSS. ArrayBuffers and WASM memory are not fully
200
+ covered by the JS-heap limit. Size host/container limits for total concurrency;
201
+ use process/container isolation when serving untrusted uploads. The library
202
+ does not provide an HTTP endpoint, authentication or a filesystem sandbox.
203
+
204
+ ## Viewer sessions and custom interfaces
205
+
206
+ Use `StlViewer` directly for a viewer, or `ViewerSession` for a reusable loading
207
+ queue and model editing. Neither requires the ViewSTL website's controls, labels
208
+ or dialogs. Each session belongs to one container and starts its canvas/workers
209
+ only when loading begins (or when you explicitly call `ready()`).
210
+
211
+ ```js
212
+ import { ViewerSession } from './viewer/viewstl.js';
213
+ const session = new ViewerSession(document.getElementById('viewer'), {
214
+ device_memory: navigator.deviceMemory,
215
+ viewer_options: { worker_url: '/viewer/viewstl.worker.js' },
216
+ on_event(event) {
217
+ if (event.type === 'error') console.error(event.code, event.message);
218
+ if (event.type === 'progress') console.log(event.progress);
219
+ }
220
+ });
221
+ await session.openFiles(fileInput.files);
222
+ const models = session.getModels(); // detached metadata; no mesh copies
223
+ if (models.length) session.setRotation(models[0].id, {x: 0, y: 0, z: 90});
224
+ // Existing export/analysis/camera APIs stay on StlViewer:
225
+ await session.viewer.download_scene({format: 'vsb'});
226
+ session.dispose();
227
+ ```
228
+
229
+ The host supplies any file input, toolbar and status display. `viewer_options`
230
+ accepts existing constructor options except `models` and `allow_drag_and_drop`;
231
+ pass initial/dropped files to the session loading methods so it tracks imported models. Existing viewer callbacks are
232
+ composed with session notifications. Optional `colors` supplies fallback colors
233
+ for files without their own colors. Without an override the memory policy uses
234
+ half the browser's supplied device-memory estimate, or half a conservative 2 GiB
235
+ fallback. This is a limit estimate, not a free-memory measurement.
236
+
237
+ | Session API | Contract |
238
+ | --- | --- |
239
+ | `ready()`, `viewer` | Explicitly initialize and await the viewer; the property is null before initialization. |
240
+ | `openFiles(files)` | Queue Files or configured {name, url, size?} sources. VSB/VSJ imports and VSJ companions use the same path. |
241
+ | `openURL(url, format?)` | Queue a remote model/scene through the explicitly configured same-origin relay. |
242
+ | `cancelLoading()` | Cancel queued/active loading, retaining existing models. |
243
+ | `getModel(id)`, `getModels()` | Detached metadata including name, bounds, transforms, reset baselines and animation definition/state; no private geometry buffers. |
244
+ | `setPosition(id, xyz)`, `setRotation(id, degrees)` | Edit center-preserving transforms; return false for invalid input. Stop only the edited model's animation. Original StlViewer methods continue to use radians. |
245
+ | `prepareManual(id)`, `centerPivot(id)` | Capture/stop current playback; optionally rebase the pivot without moving any rendered vertex. |
246
+ | `moveDepth(id, amount)` | Translate along the eye-to-model ray. Positive moves away, negative moves nearer. |
247
+ | `setArrangement(mode)` | original, side-by-side or custom; keep source geometry unchanged. |
248
+ | `setGrid(enabled, spacing?, height?)` | Configure the infinite XY grid. |
249
+ | `duplicate(id)`, `remove(id)`, `clear()` | Manage session models through these methods to retain session metadata/reset baselines. |
250
+ | `bindManipulation(options)` | After ready, bind move/rotate/depth canvas gestures; options supply getModelId(event?), getTool(), onChange/onStart/onEnd. Returns setEnabled/dispose. No toolbar or styles are created. |
251
+ | `dispose()` | Cancel work and release manipulation listeners, workers and viewer resources. Idempotent; later operations reject. |
252
+
253
+ Read-only `busy`, `arrangement`, `grid` and `memoryPolicy` expose current settings.
254
+ Events are `ready`, `busy`, `opening`, `progress`, `loaded`, `scene-loaded`,
255
+ `load-failed`, `error` and `animation`; their shapes are in the public types.
256
+ Queue promises settle after work drains, including per-file failures reported
257
+ through events. Inspect the resulting models/events rather than treating promise
258
+ resolution alone as proof every file succeeded. Invalid URL arguments throw.
259
+
260
+ **Migration from rc.8:** `mountViewstlApp` and its `ViewstlApp*` types are no longer
261
+ exported. That helper depended on ViewSTL-specific HTML IDs and UI policy. Use
262
+ `ViewerSession` or `StlViewer` with your own UI, or retain your pinned rc.8 build
263
+ while migrating. Existing published releases and legacy v1 remain available.
264
+ The ViewSTL website's controller is maintained separately, not distributed in
265
+ the plugin repository or npm package. Ordinary `StlViewer` integrations keep their API.
266
+
267
+ ## Rendering and file processing
268
+
269
+ - WebGL2 only; no CanvasRenderer, WebGL1 or IE fallback.
270
+ - No Three.js or other third-party library in the STL/OBJ rendering core.
271
+ - ASCII/binary STL, VisCAM/MeshLab and Materialise colors, plus the legacy
272
+ ViewSTL colored-STL convention.
273
+ - OBJ polygon fan triangulation and negative indices. OBJ materials,
274
+ textures and robust triangulation of concave polygons are not included.
275
+ - Core 3MF meshes, nested components, build transforms, units, base
276
+ materials and vertex colors. Root model relationships are followed.
277
+ Texture/composite materials, external component parts and unsupported
278
+ required extensions produce explicit errors rather than silent loss.
279
+ - STEP/STP and IGES/IGS solid/surface import via the optional pinned
280
+ Open CASCADE WASM reader. Units normalize to millimetres; assembly placements
281
+ and mesh/face colors are retained. All surfaces in a file form one model.
282
+ The display mesh is an approximation (relative deflection 0.001, angular
283
+ deflection 0.5 radians), not an exact CAD editing or measurement kernel.
284
+ Use self-contained exports: external part files are not fetched, and native
285
+ SolidWorks/Solid Edge formats, feature history, PMI and curve-only files are
286
+ unsupported. Incomplete external-reference assemblies may lack parts.
287
+ - Deploy the entire `build/cad/` directory alongside `chunks/` and the worker.
288
+ CAD code/WASM is loaded only for CAD files; local input bytes are never uploaded.
289
+ Each job's worker is terminated on completion, cancellation, error or timeout.
290
+ CAD admission estimates 128 MiB plus 40 times the input size. The WASM heap
291
+ is separately capped to at most 512 MiB and to part of the remaining scene
292
+ budget. Output arrays and triangle counts are also checked; these checks
293
+ cannot predict exact peak browser/GPU memory for every CAD file.
294
+ - A strict CSP must allow `script-src 'self' 'wasm-unsafe-eval'` for the worker;
295
+ JavaScript `unsafe-eval` is not needed. Serve `.wasm` as `application/wasm`.
296
+ `scripts/prepare-cad.mjs` replaces the pinned glue's dynamic bindings with
297
+ static closures and adds its heap ceiling. The WASM is unmodified. Adapted
298
+ glue, the recipe, license texts and source references ship with the assets.
299
+ - VSJ JSON and VSB archives containing `json_data.vsj` and STL files.
300
+ - ZIP/3MF uses separately loaded worker chunks containing pinned fflate,
301
+ saxes and xmlchars. Those chunks are not fetched for ordinary STL/OBJ.
302
+ - ZIP accepts stored and deflated entries, validates CRC/length and bounds
303
+ expanded data. ZIP64, encryption and multi-disk archives are unsupported.
304
+ - Local files never leave the browser. Remote URLs obey normal CORS and
305
+ same-origin credential rules; there is no upload service or CORS proxy.
306
+
307
+ Parsing, bounds, flat normals and initial requested smooth/edge geometry
308
+ are calculated in workers. Results use transferable buffers. Workers are
309
+ bounded and terminated after jobs, on cancellation, and on disposal.
310
+
311
+ Changing an already-loaded model to smooth/edges/wireframe derives its
312
+ additional data in a worker. That operation intentionally copies positions
313
+ once, since the main thread retains them for synchronous export and picking.
314
+ Nonuniform-scale area queries also require a cached synchronous scan. These
315
+ are distinct from the initial file handoff, which does not clone geometry.
316
+
317
+ Rendering runs only when needed, including during interaction and animation.
318
+ Transparency uses model-level back-to-front sorting; intersecting transparent
319
+ surfaces can still have ordering artifacts. STL export stores face color,
320
+ so a 3MF triangle's per-vertex gradient cannot survive STL/VSB export exactly.
321
+
322
+ ## Model animation
323
+
324
+ Animation is included in `2.0.0-rc.5` on GitHub and in both npm packages.
325
+ Playback remains inside the native WebGL2 plugin.
326
+
327
+ After a model has loaded, one call starts continuous rotation:
328
+
329
+ ```js
330
+ await viewer.when_idle();
331
+ viewer.animate_model(0, {
332
+ delta: { rotationz: 2 * Math.PI, msec: 5000, loop: true },
333
+ });
334
+ ```
335
+
336
+ The legacy `animation` model option also works. `delta` adds a change to the
337
+ starting pose; `exact` interpolates toward an absolute target. They can operate
338
+ together on different properties and finish independently:
339
+
340
+ ```js
341
+ viewer.animate_model(0, {
342
+ delta: { rotationx: 1, rotationy: 1.1, rotationz: 1.2, msec: 1000, loop: true },
343
+ exact: { x: 1000, y: 150, z: 200, scale: 5, msec: 3000 },
344
+ });
345
+
346
+ viewer.pause_model_animation(0); // Keep the pose and elapsed time.
347
+ viewer.resume_model_animation(0);
348
+ console.log(viewer.get_model_animation_state(0));
349
+ viewer.animate_model(0, null); // Stop at the current pose; discard playback.
350
+ ```
351
+
352
+ Supported properties are `x/y/z`, `rotationx/y/z` in radians and
353
+ `scale` or `scalex/scaley/scalez`. Durations are positive milliseconds, defaulting
354
+ to 300. Scale deltas are additive, as in the legacy API; all resulting scales
355
+ must stay positive. Only delta tracks repeat, accumulating their change each
356
+ cycle. A negative looping scale delta is rejected. Delta and exact may not claim
357
+ the same property. Inputs must be finite and within JavaScript's safe numeric
358
+ range. Invalid replacements leave the current animation intact.
359
+
360
+ Starting a new animation replaces the old one from the current pose. Manual
361
+ position, rotation, scale and unit setters stop that model's playback first.
362
+ Camera interaction, color and viewing-mode changes remain usable. Completed
363
+ tracks no longer write their properties. Removal, `clean()` and `dispose()`
364
+ release playback; paused/completed models do not keep the render loop running.
365
+
366
+ Timing follows elapsed frame timestamps, not frame count. Hidden documents and
367
+ lost WebGL contexts suspend playback without including that gap on resumption.
368
+ User pause is independent of this automatic suspension. State queries return
369
+ `status`, `elapsed_ms`, `suspended` and active `tracks`; unknown IDs return null.
370
+
371
+ Set callbacks in the viewer options:
372
+
373
+ ```js
374
+ const viewer = new StlViewer(container, {
375
+ animation_state_callback(id, state, reason) {
376
+ console.log(id, state.status, reason);
377
+ },
378
+ animation_complete_callback(id, state) {
379
+ console.log('Finished', id, state.elapsed_ms);
380
+ },
381
+ });
382
+ ```
383
+
384
+ Callbacks describe transitions, not every frame, and are deferred until the
385
+ current operation finishes. Completion fires once when every track finishes;
386
+ an animation with a looping track does not complete. Call playback controls on
387
+ loaded models, for example after `when_idle()`.
388
+
389
+ VSB/VSJ preserve the animation definition, original pose, elapsed phase, rotation
390
+ pivot and pause flag. Both scene-loading APIs start incoming animations together
391
+ after all incoming models load. VSJ still needs its referenced model files.
392
+ Animated VSB entries retain unscaled mesh data and explicit scale metadata;
393
+ static entries retain the previous scale-baked convention. This prevents scale
394
+ being applied twice. Older viewers may read the pose but cannot restore the new
395
+ playback-state extension exactly.
396
+
397
+ PNG and modern `export_models()` capture the current pose without stopping
398
+ playback. Legacy `get_stl_bin()` keeps its documented scale/centering-only export
399
+ convention. Regular model files do not store animation. Print checks should run
400
+ while the affected models are paused; subsequent motion makes the report stale.
401
+
402
+ A self-contained example is in `examples/v2/animation.html` in the source
403
+ checkout (serve with `pnpm start`). It includes Play, Pause, Resume, Stop and Reset.
404
+
405
+ Website animation controls are host-owned. Use the animation API above and
406
+ `ViewerSession.centerPivot(id)` for a spin around the mesh center. Session model
407
+ snapshots expose playback and definitions without accessing private model data.
408
+ The host decides how to respect reduced-motion preferences, pause for a print
409
+ check, present a Resume action and update its readouts. Scene saving retains
410
+ playback; PNG and regular model exports snapshot the current pose.
411
+
412
+ ## Existing API and intentional differences
413
+
414
+ The existing constructor, model options and documented method names are
415
+ implemented. See [the full inventory](v2-compatibility.md). Existing getters
416
+ remain synchronous. `get_vsb_blob()` continues to return a Promise.
417
+
418
+ - `width` / `height` override their `canvas_width` / `canvas_height` aliases.
419
+ - `bg_color` overrides `bgcolor`.
420
+ - `on_model_mousedown` overrides `on_model_mouseclick`; both setter names work.
421
+ - `set_model_units(model_id, units, scale)` keeps the executable three-argument
422
+ signature. The old documentation omitted `model_id` incorrectly.
423
+ - `load_three_files: 'directory/'` remains a legacy alias for the worker
424
+ directory, resolved against the page. It never loads Three.js. An explicit
425
+ `worker_url` takes precedence; false/omitted uses the entry point's directory.
426
+ - `jszip_path` and `jszip_utils_path` are accepted deprecated options, with
427
+ no effect: archive support is shipped in the package's lazy worker chunks.
428
+ No scripts are fetched from those legacy paths.
429
+ - `get_model_info().dims` now updates after scaling. The old implementation
430
+ returned stale dimensions even when area and volume had changed.
431
+ - Zero IDs, zero coordinates, zero rotations and zero opacity are valid.
432
+ Invalid positive-only scales, colors, transforms and IDs report errors.
433
+ - Model-loading batches preserve the legacy final callback order:
434
+ `all_loaded_callback` precedes the final `model_loaded_callback`.
435
+ Failed/cancelled jobs settle the batch instead of leaving it hanging.
436
+ An empty initial viewer does not emit `all_loaded_callback`.
437
+ - `set_center_models` controls centering of subsequently loaded models.
438
+ - Model data remains CPU-resident for synchronous export/picking and context
439
+ restoration. `clean()` and `dispose()` release GPU buffers and worker jobs.
440
+ - `dispose()` removes only the viewer's owned DOM/listeners, not its siblings.
441
+ - The renderer, camera and models are native objects. Direct Three.js calls
442
+ into `.camera`, `.scene`, `.renderer`, or `.models` are not compatible.
443
+
444
+ STL export retains the legacy convention: geometry scale and centering are
445
+ baked into vertices; scene position and rotation are not baked. VSB saves
446
+ position/rotation in its manifest and avoids applying scale twice.
447
+
448
+ Volume is a signed-tetrahedron mesh calculation reported as an absolute
449
+ value, not a watertightness or printability certification. For inch models,
450
+ area/volume follow the legacy conversion convention.
451
+
452
+ ## Optional Three.js mesh bridge
453
+
454
+ The ordinary viewer never imports Three.js. To request a real Three.js
455
+ Mesh from `get_model_mesh`, pass your own instance:
456
+
457
+ ```js
458
+ const viewer = new StlViewer(container, { three: THREE });
459
+ // after loading:
460
+ const mesh = viewer.get_model_mesh(0);
461
+ ```
462
+
463
+ The bridge uses `options.three` or `globalThis.THREE`. With neither present,
464
+ `get_model_mesh` reports a load error and returns null. It does not return
465
+ a misleading imitation Mesh.
466
+
467
+ Import supports static BufferGeometry meshes and legacy Geometry vertices/
468
+ faces. It copies geometry and basic material color/opacity. Placement is
469
+ controlled by the supplied model options, as in the old plugin. Skinning,
470
+ instances, morph targets, child objects, multiple/textured/shader materials
471
+ are rejected. Export returns an independent BufferGeometry Mesh. Dispose
472
+ the returned mesh's geometry and material when finished with it.
473
+
474
+ Use `get_model_data(id)` for a native copy of raw geometry, and
475
+ `add_model({ data, ...options })` to add it without Three.js. Data does not
476
+ include scene transforms; copy those options explicitly when duplicating.
477
+
478
+ Use `get_image_blob()` for screenshots; direct `renderer.domElement.toDataURL`
479
+ outside a draw is not guaranteed because continuous drawing-buffer
480
+ preservation is disabled.
481
+
482
+ ## Additional options and helpers
483
+
484
+ | Option | Default |
485
+ | --- | --- |
486
+ | `worker_url` | Beside the ESM/classic script |
487
+ | `worker_factory` | None |
488
+ | `worker_count` | 2, allowed range 1–4 |
489
+ | `worker_timeout` | 120,000 ms per job |
490
+ | `pixel_ratio` | Device pixel ratio capped at 2 |
491
+ | `max_file_bytes` | 256 MiB |
492
+ | `max_triangles` | 5,000,000 |
493
+ | `max_uncompressed_bytes` | 256 MiB per archive |
494
+ | `max_archive_entries` | 1,024 |
495
+ | `max_memory_bytes` | `Number.MAX_SAFE_INTEGER` (optional estimated working-set budget) |
496
+
497
+ Limits are configurable positive integers. These are ceilings, not guarantees
498
+ that every mobile device has enough memory for the maximum. Derived smooth
499
+ normals/edge maps can use substantially more memory than flat rendering.
500
+
501
+ `max_memory_bytes` adds conservative checks for parsed geometry, text parsing,
502
+ archive expansion, expanded 3MF instances, and derived smooth/wire/edge maps.
503
+ It estimates working memory, including GPU buffers; it does not measure free
504
+ memory. Existing default byte/triangle limits remain unchanged for embedders.
505
+ The ViewSTL preview site configures its own device-dependent limits: half the
506
+ approximate `navigator.deviceMemory` report, or half a 2 GiB fallback if absent.
507
+ This leaves a 50% margin against that estimate, not a guaranteed allocation or
508
+ protection against an OS/browser killing an exhausted tab.
509
+
510
+ Local binary STL files are parsed in chunks of at most 65,536 triangles
511
+ (3.125 MiB input data) plus their 84-byte header. This avoids holding a complete
512
+ input file and a second assembled copy alongside geometry. ASCII/OBJ/archive
513
+ input is still read as a complete buffer; OBJ no longer creates a full array
514
+ of every input line. Native GPU allocation errors clean up partial resources
515
+ and report through `load_error_callback`.
516
+
517
+ `when_ready` resolves after renderer initialization, before initial models
518
+ finish. `when_idle()` waits for pending model, scene and derived-geometry
519
+ work; inspect `load_error_callback` for individual failures. `get_image_blob`
520
+ waits for that work, draws and returns a PNG Blob by default.
521
+
522
+ Keyboard: arrows orbit; + / - zoom; F fits the scene. Mouse: drag orbits,
523
+ shift/right/middle drag pans, wheel zooms. Touch: one finger rotates and
524
+ two fingers pan/pinch. Real mobile-device validation remains necessary.
525
+
526
+ For a same-origin standalone deployment, allow your script and worker assets
527
+ in the site's Content Security Policy (`script-src 'self'; worker-src 'self'`),
528
+ and permit intended remote model hosts in `connect-src`. SharedArrayBuffer,
529
+ cross-origin isolation, eval and worker blob bootstrapping are not required.
530
+
531
+ ## Additional views and section cuts
532
+
533
+ The existing `flat`, `smooth` and `wireframe` display values remain supported.
534
+ New display values are `edges` (solid with 30-degree feature edges), `mesh`
535
+ (triangle overlay), `xray`, `studio` and `orientation` (blue front faces, red
536
+ back faces). None requires Three.js. `view_edges` / `set_edges` retain the
537
+ legacy one-degree edge threshold.
538
+
539
+ ```js
540
+ viewer.set_view(0, {display: 'studio', smooth_shading: true});
541
+ await viewer.when_idle();
542
+ viewer.set_smooth(0, false); // Independent of the selected modern view
543
+ viewer.set_section(0, {axis: 'z', position: 0.5, flip: false});
544
+ viewer.set_section(0, null); // Restore the complete model
545
+ ```
546
+
547
+ These fields are also accepted in model options, returned by `get_model_info`,
548
+ and preserved by VSJ/VSB scene export. `set_display('flat')` and
549
+ `set_display('smooth')` keep their legacy shading behavior. Smooth shading is
550
+ ignored visually in wireframe and retained for the next surface view. Derived
551
+ view changes commit after preparation succeeds; a rejected memory estimate or
552
+ failed worker preserves the previous appearance. Wait for `when_idle()` and
553
+ inspect `load_error_callback` as with model loading.
554
+
555
+ Mesh overlay uses shader barycentric coordinates without an extra geometry
556
+ buffer. Studio, orientation, X-ray and clipping also reuse existing geometry.
557
+ Smooth normals, wireframe and feature edges are computed on demand in a worker
558
+ and checked against the memory budget, including cached derived arrays.
559
+
560
+ Section position is a fraction from 0 to 1 of the source model's bounding box
561
+ along its chosen axis. The plane follows model transforms, clips picking and
562
+ PNG rendering, and does not modify exported STL geometry or model statistics.
563
+ The cut remains open, with an amber boundary highlight; no cap is generated.
564
+ X-ray uses lightweight back-face/front-face alpha passes; it is an illustrative
565
+ view, not exact order-independent transparency for intersecting surfaces.
566
+ Face orientation describes winding relative to the camera, not an automatic
567
+ watertightness or printability diagnosis.
568
+
569
+ ## Optional streaming URL imports
570
+
571
+ The plugin does not silently route existing `filename` requests through a server.
572
+ To retrieve a public URL without depending on the source server's CORS policy,
573
+ configure a same-origin relay and explicitly use `remote_url`:
574
+
575
+ ```js
576
+ const viewer = new StlViewer(container, {url_import_endpoint: '/api/import-url'});
577
+ viewer.add_model({id: 0, remote_url: 'https://example.com/model.stl'});
578
+ // Optional remote_format: 'stl', 'obj', '3mf', 'step', 'stp', 'iges', 'igs'.
579
+ ```
580
+
581
+ `ViewerSession` accepts the endpoint inside `viewer_options` and exposes
582
+ `session.openURL(url, format)`. The host owns URL input and progress presentation;
583
+ the plugin owns validation, streaming, parsing, the queue and cancellation.
584
+ Session jobs default to a ten-minute worker timeout.
585
+
586
+ The relay receives the URL in a JSON POST body and streams the response without
587
+ file storage. A signed URL's query string is not displayed as a model name or
588
+ included in scene exports. Local files still never leave the browser. URL imports
589
+ are a separate privacy path: the relay sees the remote bytes in transit.
590
+
591
+ Binary STL is decoded incrementally in the worker. Other formats use bounded
592
+ browser buffers, including archives and CAD. A complete model appears after
593
+ download and validation; progressive partial rendering is not implemented.
594
+ `get_model_info().file_bytes` reports the imported byte count when available.
595
+ Downloaded names are metadata, not reloadable URLs; use VSB to package a scene
596
+ containing these models. Embedders must provide their own compatible relay; no
597
+ ViewSTL server is implicitly used by the library.
598
+
599
+ A host can extract links from its own drop events and call `session.openURL`.
600
+ Page-wide drag overlays, editable-field handling and URL dialogs belong to that
601
+ host. The low-level viewer's existing local file-drop contract is unchanged.
602
+
603
+ The reference relay lives in the website repository's `relay/` directory. Its
604
+ request is `{url, format, max_bytes}` and its response supplies X-ViewSTL-Format,
605
+ percent-encoded X-ViewSTL-Filename, and optional Content-Length. All relay and
606
+ parser failures use the existing error callback. Cancellation terminates the
607
+ worker/fetch; the server then closes the upstream connection.
608
+
609
+ ## Local mesh export
610
+
611
+ The host owns its Save dialog and passes explicit model IDs/options to the
612
+ export methods. Model exports and whole-scene VSB/VSJ exports remain distinct.
613
+ Use AbortSignal to cancel when the host dismisses or changes an active operation;
614
+ the plugin does not prescribe a modal layout or panel-closing policy.
615
+
616
+ All exporters live in the plugin and run in disposable workers. They generate
617
+ local Blobs; no relay, upload, server conversion or third-party network endpoint
618
+ is used. Export modules and ZIP compression load only on demand. STEP/IGES writing
619
+ uses JavaScript and does **not** download the CAD importer or WASM engine.
620
+
621
+ ```js
622
+ await viewer.when_idle();
623
+ const controller = new AbortController();
624
+ const result = await viewer.export_models({
625
+ format: '3mf', // stl, obj, 3mf, step/stp, iges/igs
626
+ model_ids: [0, 2], // omitted = all; 'selected' = selected model
627
+ filename: 'assembly', // correct extension added/replaced automatically
628
+ units: 'mm', // mm (default), inch
629
+ apply_transforms: true, // default: bake position, rotation, scale and centering
630
+ colors: true, // default for OBJ/3MF; ignored for STL/STEP/IGES
631
+ signal: controller.signal,
632
+ on_progress({ loaded, total, phase }) {
633
+ // Triangle counts. phase is 'exporting' or 'complete'.
634
+ // Encoding can reach total before ZIP packaging completes.
635
+ },
636
+ });
637
+ // {blob, filename, format, units, model_ids, triangles, representation: 'mesh'}
638
+ // Keep result.blob, or use the convenience method to initiate a browser download:
639
+ await viewer.download_models({format: 'step', model_ids: 'selected'});
640
+ // controller.abort() rejects an active/queued operation with AbortError.
641
+ ```
642
+
643
+ Exports capture the model state at dispatch; subsequent edits/removal do not
644
+ change an in-flight export. Buffers used for rendering are never transferred or
645
+ detached. No selection, an unknown ID, duplicate IDs or an unfinished model
646
+ rejects the promise. Call `when_idle()` first to include pending imports. Errors
647
+ reject the promise and progress goes only to the supplied callback; exporters do
648
+ not show dialogs or invoke the viewer's import-error callback. A throwing progress
649
+ callback rejects its export and terminates the worker. `clean()`/`dispose()` cancel
650
+ active and queued jobs. The caller owns the returned Blob and its lifetime.
651
+
652
+ `apply_transforms: false` exports source coordinates and their accumulated unit
653
+ conversion, without centering, presentation movement/rotation or user scaling.
654
+ Scene coordinates are treated as millimeters; `units: 'inch'` divides output
655
+ coordinates by 25.4 and writes inch metadata where the format supports it.
656
+ Unitless inputs must be interpreted/scaled appropriately by the embedding app.
657
+ STL and OBJ have no standardized units; OBJ includes an informational comment.
658
+
659
+ | Format | Output and scope |
660
+ | --- | --- |
661
+ | STL | Standard binary STL; selected meshes combined into one triangle stream. Attributes are zero, so no ambiguous vendor color encoding. |
662
+ | OBJ | With `colors: true`, a ZIP containing `model.obj` and `materials.mtl`, including object groups, face normals, per-face colors and opacity. Vertex colors are averaged per face and colors/opacity quantized to 8-bit channels. With `colors: false`, a plain `.obj` file. |
663
+ | 3MF | ZIP/XML package with separate mesh objects, indexed vertices, units and per-vertex color/opacity using the Materials extension. Requires non-degenerate triangles, paired consistently oriented edges, and positive volume. Open/inconsistently wound meshes reject; there is no automatic repair or full self-intersection validation. |
664
+ | STEP | AP214 `AUTOMOTIVE_DESIGN`: separate named products made of planar triangle faces in open shells. Exact displayed mesh coordinates are written; this is a surface model, not a reconstructed CAD solid. |
665
+ | IGES | IGES 5.3 bounded planes (108/form 1) with closed 3D polylines (106/form 12). Models use separate levels. Triangle surfaces, not stitched CAD solids. |
666
+
667
+ STEP/IGES preserve mesh geometry and placement, but omit colors, textures,
668
+ analytic curves, original CAD topology/assemblies and feature history. They work
669
+ with open triangle meshes, but reject zero-area triangles. No F3D export is
670
+ provided. A consuming site can describe these limitations in its own interface;
671
+ the low-level export API is UI-free. CAD interoperability has been verified
672
+ against Open CASCADE, not every commercial CAD application.
673
+
674
+ Section cuts, camera orientation, grid, selection frames and viewing/shading modes
675
+ are presentation settings and do not modify exported geometry. Use VSJ/VSB for
676
+ ViewSTL scene settings. Texture/UV data discarded at import cannot be recreated.
677
+
678
+ The scene memory budget includes pending exports. Output is encoded in chunks
679
+ instead of building one large text string; Blobs still require browser storage
680
+ and this API does not stream directly to a filesystem handle. Output obeys
681
+ `max_file_bytes`, archives also obey `max_uncompressed_bytes` and
682
+ `max_archive_entries`, and the complete selection obeys `max_triangles`.
683
+ Conservative format-specific memory estimates can reject an export before copying
684
+ geometry. Cancellation terminates its worker, including compression.
685
+
686
+ The existing synchronous `get_stl_bin`/`download_model` and VSJ/VSB methods retain
687
+ their legacy behavior. Use the new API for scene transforms, correct filename
688
+ extensions, asynchronous execution and cancellation.
689
+
690
+ ## Development and packaging
691
+
692
+ ```sh
693
+ pnpm install --frozen-lockfile
694
+ pnpm build
695
+ pnpm test
696
+ pnpm check:types
697
+ pnpm check:node-types
698
+ pnpm check:legacy
699
+ pnpm start
700
+ pnpm pack
701
+ ```
702
+
703
+ Open `/examples/v2/` for the demo and `/tests/v2/browser.html` for real worker/
704
+ WebGL checks. `pnpm pack` builds a local tarball; it does not publish anything.
705
+ The package allowlist includes v2 artifacts, types, documentation and license
706
+ notices. It excludes legacy snapshots, old dependencies, tests and server data.
707
+
708
+ Before a stable release, validate real mobile hardware, Safari and Firefox,
709
+ larger user fixtures, npm/bundler integrations and the integrations used by
710
+ the production site. Automated and desktop browser results are recorded in
711
+ [verification](v2-verification.md).
712
+
713
+ ## Multiple models, colors and an infinite grid
714
+
715
+ Use `center_models: false` to preserve the coordinates in assembly files, then call
716
+ `add_model` / `add_models` to append files. `set_position` changes presentation
717
+ coordinates without rewriting source geometry. The preview site's Models panel
718
+ adds selection, individual removal and reversible side-by-side placement.
719
+
720
+ ```js
721
+ const viewer = new StlViewer(element, { center_models: false });
722
+ viewer.add_models([{id: 0, local_file: partA}, {id: 1, local_file: partB}]);
723
+ await viewer.when_idle();
724
+ viewer.set_color(0, '#dd766b');
725
+ viewer.set_color(0, null); // Restore embedded file colors (or neutral gray if absent).
726
+ viewer.set_bg_color('#202d35');
727
+ viewer.set_infinite_grid(true, 1, 0); // enabled, base spacing, Z height
728
+ ```
729
+
730
+ The infinite XY grid is a procedural shader. Its spacing adapts as the camera
731
+ zooms and it fades near the horizon. Panning does not allocate a larger grid;
732
+ opaque models occlude it. `set_grid` retains its existing finite-grid behavior.
733
+ Infinite settings round trip through VSJ/VSB as `infinite_grid: {spacing,height}`.
734
+ Colors change shader uniforms without rebuilding the mesh buffers.
735
+
736
+ An explicit `max_memory_bytes` budget now covers the whole scene, including
737
+ resident geometry, cached view arrays and pending derived geometry. Parsing is
738
+ serialized under that budget so each new model receives the remaining allowance.
739
+ Removing a model frees its allowance and cancels its pending derived work.
740
+ These remain conservative estimates, not measurements of free RAM or GPU memory.
741
+
742
+ ### Selecting a model for editing
743
+
744
+ `set_selection(id)` draws a light 3D bounding frame using that model's transform.
745
+ `set_selection(null)` clears it. The frame uses one 288-byte vertex buffer, follows
746
+ rotation/translation/scale without rebuilding mesh buffers, and is excluded from
747
+ PNG and scene exports. Removing or clearing its model releases the frame.
748
+
749
+ `pick_model(clientX, clientY)` returns `{id, distance}` for the nearest visible
750
+ surface, or `null` for empty space. Coordinates use the same viewport coordinate
751
+ system as mouse events. It respects transforms, hidden models and section cuts.
752
+ The host chooses when to pick: for example, on double-click or at the start of a selection drag. The existing legacy click callbacks retain their behavior.
753
+
754
+ The preview provides Move, Rotate and Depth controls per model. Rotation uses
755
+ degrees in the UI and compensates for the original mesh center; the plugin's
756
+ `set_rotation` API continues to accept radians with its existing origin behavior.
757
+ Depth translates along the eye-to-model ray while holding the camera fixed.
758
+
759
+ ## Saving ViewSTL scenes
760
+
761
+ Use `export_scene` for a Blob or `download_scene` for a browser download:
762
+
763
+ ```js
764
+ const result = await viewer.export_scene({
765
+ format: 'vsb', // or 'vsj'
766
+ filename: 'my-scene',
767
+ signal: abortController.signal,
768
+ on_progress({ loaded, total, phase }) {
769
+ // Model counts; exporting, packaging (VSB only), complete.
770
+ },
771
+ });
772
+ await viewer.download_scene({ format: 'vsj', filename: 'scene', force_basename: true });
773
+ ```
774
+
775
+ Both methods always capture the entire loaded scene. They return `{blob,
776
+ filename, format, model_ids, triangles, representation: 'scene'}`. Pending loads,
777
+ empty scenes, excess model/triangle counts, memory and output limits reject.
778
+ Errors reject the Promise instead of using the import-error callback. Cancellation
779
+ and throwing progress callbacks release the worker and memory reservation.
780
+
781
+ VSB uses the existing ZIP layout: `json_data.vsj` plus one colored binary STL per
782
+ model. Mesh encoding and compression run in a disposable worker using copied
783
+ snapshot buffers; renderer buffers remain intact. Camera, background, grid,
784
+ transforms, model colors/opacity, viewing modes and sections stay in the manifest.
785
+ Original model names are kept as `orig_filename`. As with the legacy VSB format,
786
+ STL geometry uses float32 positions and 5-bit per-face colors, taking the first
787
+ vertex color of each triangle. Texture/UV data and original CAD topology/history
788
+ are not reconstructed or included.
789
+
790
+ VSJ is JSON settings and model references, without embedded geometry. The
791
+ `force_basename` option defaults to false for API callers; the preview uses true
792
+ to write only filenames. Referenced files must be supplied separately. A local-file
793
+ reference alone does not grant browser access to a file on disk. VSB is the
794
+ self-contained choice. The preview opens both formats through the file picker,
795
+ drag and drop, and URL opening. Existing `get_vsj`, `get_vsb`, `get_vsb_blob` and their legacy
796
+ download methods retain their signatures and behavior.
797
+
798
+ In the app Save dialog, VSB/VSJ save the entire scene. STL/OBJ/3MF/STEP/IGES save
799
+ only the selected model. There is no user-selectable scope dropdown. The low-level
800
+ `export_models` API still permits explicit multiple-model selections for embedders.
801
+
802
+ ## Opening ViewSTL scenes
803
+
804
+ Use the Promise-based `import_scene` API for local files or public URLs:
805
+
806
+ ```js
807
+ const result = await viewer.import_scene(sceneFile, {
808
+ files: companionFiles, // File[]: model files referenced by a local VSJ.
809
+ signal: abortController.signal,
810
+ on_progress({ loaded, total, phase }) {
811
+ // reading-scene: reader progress; opening-model/complete: model counts.
812
+ },
813
+ });
814
+ console.log(result.model_ids, result.scene, result.used_files);
815
+
816
+ // Requires the viewer's same-origin url_import_endpoint configuration.
817
+ await viewer.import_scene('https://example.com/scene.vsb');
818
+ await viewer.import_scene('https://example.com/download?id=123', { format: 'vsj' });
819
+ ```
820
+
821
+ VSB archives contain `json_data.vsj` and their model files. VSJ manifests reference
822
+ separate files: supplied local files match by exact relative path or unambiguous
823
+ basename. Missing or ambiguous files reject with an actionable error. Absolute
824
+ HTTP/HTTPS references use the configured URL relay. For a remote VSJ, relative
825
+ references resolve against its final directory after redirects, also through the
826
+ relay. Local files and manifests are not uploaded. The relay streams remote files
827
+ without saving them. It still applies its request, size and network limits, so a
828
+ scene with many remote references can hit the relay's rate limit.
829
+
830
+ Imports append models with new IDs and restore camera, background/grid, transforms,
831
+ colors/opacity, view modes, smoothing and sections. Saved centering is converted
832
+ to equivalent model positions without changing existing models' centering. Embed
833
+ canvas size and resize/mouse controls are not imported. The development animation
834
+ update also restores per-model animation and preserves its original pivot.
835
+ Scene reading and mesh parsing use workers and the existing memory/triangle/archive
836
+ limits. Models become visible together after all have loaded. Errors, cancellation,
837
+ or a throwing progress callback remove the incoming models and retain the existing
838
+ scene. `clean()` and `dispose()` also cancel pending scene imports. Existing
839
+ `load_vsj` and `load_vsb` retain their previous signatures and behavior.
840
+
841
+ In the preview, choose or drop a local VSJ together with its referenced model files;
842
+ companions used by the scene are not added twice. A VSB needs no separate files.
843
+ Choose VSB/VSJ explicitly in Open URL for a URL without a scene extension. Scene
844
+ opening, progress, cancellation and restored editing controls all live in the
845
+ plugin's app controller; the website initializer remains minimal.
846
+
847
+ ## Checking meshes for 3D printing
848
+
849
+ The optional checks run locally on demand. They analyze the loaded triangle mesh
850
+ from any supported format, including models restored from VSB/VSJ. CAD accuracy
851
+ is limited by the imported tessellation. Analysis does not repair or modify models.
852
+
853
+ ```js
854
+ const controller = new AbortController();
855
+ const report = await viewer.check_printability({
856
+ model_ids: [selectedId], // Omit for all loaded models; 'selected' uses the editing selection.
857
+ level: 'detailed', // 'basic' is the default.
858
+ apply_transforms: true, // Default: current placement/rotation/scale, like mesh export.
859
+ units_confirmed: true, // Confirm the current mm scale for unitless input.
860
+ profile: { // Optional; omit for geometry checks only.
861
+ technology: 'fdm',
862
+ build_volume_mm: [220, 220, 250],
863
+ min_wall_mm: 0.8, // User-chosen thresholds, not universal printing rules.
864
+ overhang_degrees: 45 // Measured from vertical; +Z is print up.
865
+ },
866
+ max_time_ms: 30000, // Per model, maximum 120000.
867
+ signal: controller.signal,
868
+ on_progress: ({ phase, model_index, total_models }) => console.log(phase)
869
+ });
870
+ if (viewer.is_printability_current(report)) {
871
+ viewer.show_printability(report, { model_id: selectedId, issue: 'boundary_edges' });
872
+ }
873
+ viewer.clear_printability();
874
+ ```
875
+
876
+ Basic checks cover degenerate and nearly degenerate triangles, duplicates, open
877
+ boundaries, non-manifold edges and vertices, inconsistent winding, connected
878
+ components and physical-unit provenance. Detailed checks add a native-JavaScript
879
+ BVH for self-intersections (including coplanar overlaps), shell containment,
880
+ orientation and enclosed volume. Separate components can be intentional. Hollow
881
+ models require opposite cavity-shell orientation. Vertex connectivity uses exact
882
+ source coordinates; small gaps are not silently welded closed. Intersection
883
+ checks use the reported scale-dependent numerical tolerance; near-contact cases
884
+ can be inconclusive. The existing 3MF export guards remain independent and unchanged.
885
+
886
+ Optional FDM checks compare dimensions with a build-volume box, identify potential
887
+ overhang regions, estimate bed contact and sample wall thickness. `line_width_mm`
888
+ can supply the wall threshold when `min_wall_mm` is omitted. No printer defaults
889
+ are assumed. Build fit tests current orientation dimensions, not actual placement,
890
+ exclusion zones or alternative rotations. Bed contact virtually lowers each model
891
+ to its minimum Z; the visible grid and camera do not define the print bed.
892
+ Thickness tests at most 2,048 face centres using inward normal rays and can miss
893
+ small features or oblique thin regions. No finding from those samples is explicitly
894
+ inconclusive, never proof of a minimum thickness. Overhangs do not simulate
895
+ bridging/supports; contact area does not predict adhesion/stability. Resin, toolpaths,
896
+ cross-model collisions and Boolean unions are outside these checks.
897
+
898
+ Reports include each check's `pass`, `issue`, `inconclusive` or `not_run` status,
899
+ severity, counts, explanations, bounded highlight samples, dimensions, tolerance,
900
+ timing and optional estimates. `volume_valid` means a geometrically validated
901
+ enclosed volume; its physical size still depends on the `units` check. Overall
902
+ status is `issues_found`, `incomplete` or `no_issues_found`. A basic report remains
903
+ incomplete because detailed checks were not run. Errors and time/memory limits
904
+ never imply a pass. This API does not certify a successful print.
905
+
906
+ Workers and analysis modules load only when requested. Models are checked in
907
+ sequence, with cooperative snapshot copying and aggregate memory admission.
908
+ The working estimate is 64 MiB plus 2,400 bytes per triangle, with bounded report
909
+ allowances; a model that can be viewed may be too large to analyze on the same
910
+ device. One bounded report cache is retained, not the topology/BVH. Geometry,
911
+ transforms, scale and unit changes invalidate reports; colors and camera changes
912
+ do not. Removing a checked model, clean/dispose, AbortSignal cancellation or a
913
+ throwing progress callback releases the active worker and reservations. Report
914
+ metadata uses weak references so retaining a report cannot retain removed meshes.
915
+
916
+ Highlights use temporary native WebGL line buffers and are excluded from PNG,
917
+ mesh and scene exports. They show up to 128 faces or 256 edges per check, through
918
+ occluding surfaces. The host controls results presentation and calls
919
+ `clear_printability()` when appropriate. It decides when its panel closes, how
920
+ stale results are shown, and whether to pause/resume animation. The plugin exposes
921
+ analysis, cancellation, validity checks and geometry highlighting without a UI.
922
+
923
+ ## Model size, units, duplicates, cameras and measurements (rc.6)
924
+
925
+ All operations below are implemented inside the plugin. Their interface controls and presentation belong to the host.
926
+
927
+ `viewer.resize_model(id, [2, 2, 2])` doubles the model about its world-space bounds center, preserving rotation and placement. Positive finite XYZ factors are required; independent factors allow nonuniform scaling. It stops only that model's animation. `get_model_size(id)` returns dimensions and the reset baseline in millimetres. `reset_model_size(id)` restores that baseline while retaining file-unit corrections.
928
+
929
+ `viewer.set_file_units(id, 'inch')` reinterprets a unitless STL/OBJ mesh (default mm) without moving its center. Switching back reverses the conversion. Explicit 3MF/STEP/IGES units are normalized to mm during parsing and cannot be reinterpreted. `set_display_units('inch')` changes the optional app's size/Info readouts only; scene coordinates, position controls and the grid remain in mm. Existing `set_model_units` and `set_scale` retain their legacy semantics.
930
+
931
+ `const copy = viewer.duplicate_model(id)` returns a new ID at the same pose, with independent appearance, transforms and metadata. Immutable geometry and matching GPU buffers are shared; memory admission remains conservative. The source reference remains unchanged for VSJ, while `display_name` identifies the copy. Copied animation definitions and phase are retained but paused; the original keeps playing.
932
+
933
+ `viewer.set_camera_view('top')` fits the scene from front/back/left/right/top/bottom/isometric using Z up and the existing perspective projection. It never rotates the models. Camera bookmarks are explicit data:
934
+
935
+ `viewer.set_camera_bookmarks([{name: 'Detail', camera: viewer.get_camera_state()}])`
936
+
937
+ Use `get_camera_bookmarks()` to obtain a defensive copy and `set_camera_state(bookmark.camera)` to restore one. Up to 32 uniquely named views (80 characters per name) are supported. They are kept in memory and in scene exports, not localStorage.
938
+
939
+ `const {area_mm2, volume_mm3, volume_valid, volume_reason} = await viewer.measure_model(id)`
940
+
941
+ Measurements use workers and support `signal`, `max_time_ms` (default 30 seconds) and `on_progress`. Area includes all triangles at current scale. Volume is nullable: detailed topology, intersections and shell-orientation checks must establish a closed volume. An open/invalid mesh or an inconclusive/time-limited check never becomes a plausible-looking volume. Results are cached by mesh and scale; position/rotation do not invalidate them and uniform resizing updates cached measurements algebraically. Nonuniform area is recalculated in the worker. Memory limits can reject the calculation before buffers are copied. This is a bounded mesh analysis, not a guarantee of printability.
942
+
943
+ VSB/VSJ retain size baselines, file-unit interpretation, display units, duplicate labels and bookmarks. VSB still embeds geometry; VSJ still needs the referenced source files. No second unit conversion occurs during import. Appending scenes merges bookmark names with numbered suffixes; a combined total above 32 rejects the incoming scene. Old files without the new optional fields continue to load. The `size_state.unit_factor` serialization field preserves source-unit correction separately from resize factors for exports with `apply_transforms:false`.
944
+
945
+ Local measurements and model operations need no server. Use the public APIs with the interface of your choice.