@omrips/viewstl 0.0.0-stage → 2.0.0-rc.1

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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -2
  3. package/THIRD-PARTY-NOTICES.txt +1129 -0
  4. package/build/v2/README.md +11 -0
  5. package/build/v2/THIRD-PARTY-NOTICES.txt +1129 -0
  6. package/build/v2/cad/occt-import-js.js +8 -0
  7. package/build/v2/cad/occt-import-js.source.txt +57 -0
  8. package/build/v2/cad/occt-import-js.wasm +0 -0
  9. package/build/v2/cad/prepare-cad.source.txt +112 -0
  10. package/build/v2/chunks/archive-VGS4J4Z7.js +1 -0
  11. package/build/v2/chunks/cad-VY42CMP4.js +1 -0
  12. package/build/v2/chunks/chunk-JH3H3JWA.js +1 -0
  13. package/build/v2/chunks/chunk-JS56KC3I.js +1 -0
  14. package/build/v2/chunks/chunk-R3HNTB5A.js +38 -0
  15. package/build/v2/chunks/chunk-U33KANV7.js +1 -0
  16. package/build/v2/chunks/chunk-ZMK3UZZE.js +1 -0
  17. package/build/v2/chunks/chunk-ZSV4PTIA.js +1 -0
  18. package/build/v2/chunks/export-7OP4ZGGJ.js +29 -0
  19. package/build/v2/chunks/export-cad-QPBT756V.js +13 -0
  20. package/build/v2/chunks/export-zip-EDCWH5PX.js +1 -0
  21. package/build/v2/chunks/print-check-2PU7CSL3.js +1 -0
  22. package/build/v2/chunks/scene-export-KQBS7VRJ.js +1 -0
  23. package/build/v2/chunks/scene-reader-APUJOPUR.js +1 -0
  24. package/build/v2/stl_viewer.min.js +51 -0
  25. package/build/v2/viewstl.cjs +5445 -0
  26. package/build/v2/viewstl.d.cts +446 -0
  27. package/build/v2/viewstl.d.ts +446 -0
  28. package/build/v2/viewstl.js +5426 -0
  29. package/build/v2/viewstl.min.js +51 -0
  30. package/build/v2/viewstl.worker.js +1 -0
  31. package/docs/v2-compatibility.md +204 -0
  32. package/docs/v2-usage.md +723 -0
  33. package/docs/v2-verification.md +423 -0
  34. package/legacy/README.md +43 -0
  35. package/package.json +73 -4
@@ -0,0 +1,723 @@
1
+ # ViewSTL 2 — native WebGL2 plugin
2
+
3
+ Version: `2.0.0-rc.1` (ViewSTL v2.00 release candidate). The npm package is
4
+ `@omrips/viewstl`; install the candidate with `npm install @omrips/viewstl@2.0.0-rc.1`.
5
+ The legacy plugin remains at its original repository paths and on the production
6
+ website. The v2 website is available at https://preview.viewstl.com/.
7
+
8
+ ## Browser script
9
+
10
+ Copy the complete `build/v2/` directory to your server, including the
11
+ worker, its `chunks/` and `cad/` directories and third-party notices. Then:
12
+
13
+ ```html
14
+ <div id="viewer" style="width:100%;height:500px"></div>
15
+ <script src="/viewstl-v2/stl_viewer.min.js"></script>
16
+ <script>
17
+ const viewer = new StlViewer(document.getElementById('viewer'), {
18
+ models: [{ id: 0, filename: '/models/part.stl', color: '#568bdc' }],
19
+ load_error_callback: message => console.error(message)
20
+ });
21
+ </script>
22
+ ```
23
+
24
+ `viewstl.min.js` is an identical classic-script alias. The canvas requires
25
+ a container with a nonzero height. Files must be served over HTTP(S),
26
+ including during local development; browsers restrict workers on `file:` URLs.
27
+
28
+ ## ES modules and npm
29
+
30
+ ```js
31
+ import StlViewer from './viewstl-v2/viewstl.js';
32
+
33
+ const viewer = new StlViewer(container, {
34
+ models: [{ id: 0, local_file: fileInput.files[0] }]
35
+ });
36
+
37
+ await viewer.when_idle();
38
+ console.log(viewer.get_model_info(0));
39
+ ```
40
+
41
+ The package contains ESM, CommonJS and TypeScript entry points. Importing
42
+ the package is safe during server-side rendering; constructing a viewer
43
+ requires a browser DOM and WebGL2.
44
+
45
+ The package name is `@omrips/viewstl`, under the verified `omrips` npm account.
46
+ Release candidates use the `next` dist-tag. Pin the version above for a
47
+ repeatable installation; installing a local release tarball is also supported.
48
+
49
+ With a bundler, copy the complete `node_modules/@omrips/viewstl/build/v2/`
50
+ directory to your app's public assets (for example `public/viewstl-v2/`), keeping
51
+ the worker, `chunks/`, `cad/` and notices together. Set `worker_url` to its served
52
+ URL, or provide your bundler's worker factory.
53
+ Do not assume a bundler copies a module worker's dynamic chunks merely
54
+ because it sees a URL string.
55
+
56
+ ```js
57
+ import StlViewer from '@omrips/viewstl';
58
+
59
+ const viewer = new StlViewer(container, {
60
+ worker_url: '/viewstl-v2/viewstl.worker.js'
61
+ });
62
+ ```
63
+
64
+ For Vite, the optional factory form is:
65
+
66
+ ```js
67
+ import ViewerWorker from '@omrips/viewstl/worker?worker';
68
+ const viewer = new StlViewer(container, {
69
+ worker_factory: () => new ViewerWorker()
70
+ });
71
+ ```
72
+
73
+ The static-assets/`worker_url` integration above was verified in production
74
+ builds using Vite 8.3.4 and Webpack 5.112.0, including STEP/IGES and lazy archive
75
+ assets. The optional Vite worker-factory example is separate integration guidance
76
+ and was not part of those checks. A factory must return a new worker for each
77
+ scheduled job and arrange for all dynamic chunks and CAD assets to be served.
78
+ The CommonJS entry point also requires
79
+ `worker_url` or `worker_factory` because it cannot infer a browser asset URL.
80
+
81
+ ## Full app controls
82
+
83
+ `mountViewstlApp` is the optional app controller, exported from the same
84
+ `viewstl.js` bundle as `StlViewer` (and as a global in the classic script).
85
+ It owns the file queue, memory admission, cancellation, selection, scene
86
+ placement, translation/rotation/depth gestures, viewing modes, sectioning,
87
+ colors, grid, PNG downloads, keyboard handling, and control state.
88
+
89
+ ```js
90
+ import { mountViewstlApp } from './viewer/viewstl.js';
91
+ const app = mountViewstlApp(document, {
92
+ sample_url: new URL('./sample-vase.stl', import.meta.url).href,
93
+ sample_name: 'Twist vase.stl'
94
+ });
95
+ await app.openFiles(fileInput.files);
96
+ console.log(app.getState()); // detached metadata snapshot, no geometry copies
97
+ app.dispose(); // cancels work and releases listeners, workers and WebGL
98
+ ```
99
+
100
+ This controller binds the preview's HTML control layout; the host supplies
101
+ markup and CSS. The reference layout is `Viewstl/site/index.html` in the sibling
102
+ website workspace. `requiredControls` in `src/v2/app.js` validates the control
103
+ IDs before registering handlers. Missing controls and duplicate mounts produce
104
+ a descriptive error. Custom integrations without that layout use `StlViewer`.
105
+ Branding, page metadata, advertisements and static help/privacy text belong
106
+ to the host page. The preview's JavaScript is only the initialization call.
107
+
108
+ For an embedded app, pass an `HTMLElement` containing the layout. Queries,
109
+ keyboard events and drops are scoped to that root. For multiple roots, prefix
110
+ each layout's IDs and label/ARIA references and pass the matching `id_prefix`;
111
+ the controller separates radio groups as well. `dispose()` permits remounting
112
+ and prevents late reads from updating the new app.
113
+
114
+ `openFiles()` accepts Files or configured `{name, url, size?}` sources and
115
+ resolves when the queue settles. File errors appear in the notice; success is
116
+ reflected by `getState().models`. Other methods are `select(id)`, `remove(id)`,
117
+ `cancelLoading()` and `clear()`. An omitted `sample_url` hides the sample action.
118
+ `worker_url`, `worker_factory` and `worker_timeout` support alternate hosting,
119
+ bundlers and CommonJS use.
120
+
121
+ Loading the bundle does not create a canvas or worker. The first file creates
122
+ the WebGL2 viewer. App source modules are bundled into `viewstl.js`; the host
123
+ ships no separate memory/layout/transform helpers. Worker JS, archive chunks
124
+ and CAD WASM remain separate plugin-owned assets to keep parsing off the main
125
+ thread and avoid downloading CAD code for STL/OBJ users.
126
+
127
+ `get_model_info()` also exposes copied source `bounds`, `has_colors`, and
128
+ `source_units` (`null` for unitless sources), so consumers need not inspect
129
+ internal geometry or retain its buffers.
130
+
131
+ ## Rendering and file processing
132
+
133
+ - WebGL2 only; no CanvasRenderer, WebGL1 or IE fallback.
134
+ - No Three.js or other third-party library in the STL/OBJ rendering core.
135
+ - ASCII/binary STL, VisCAM/MeshLab and Materialise colors, plus the legacy
136
+ ViewSTL colored-STL convention.
137
+ - OBJ polygon fan triangulation and negative indices. OBJ materials,
138
+ textures and robust triangulation of concave polygons are not included.
139
+ - Core 3MF meshes, nested components, build transforms, units, base
140
+ materials and vertex colors. Root model relationships are followed.
141
+ Texture/composite materials, external component parts and unsupported
142
+ required extensions produce explicit errors rather than silent loss.
143
+ - STEP/STP and IGES/IGS solid/surface import via the optional pinned
144
+ Open CASCADE WASM reader. Units normalize to millimetres; assembly placements
145
+ and mesh/face colors are retained. All surfaces in a file form one model.
146
+ The display mesh is an approximation (relative deflection 0.001, angular
147
+ deflection 0.5 radians), not an exact CAD editing or measurement kernel.
148
+ Use self-contained exports: external part files are not fetched, and native
149
+ SolidWorks/Solid Edge formats, feature history, PMI and curve-only files are
150
+ unsupported. Incomplete external-reference assemblies may lack parts.
151
+ - Deploy the entire `build/v2/cad/` directory alongside `chunks/` and the worker.
152
+ CAD code/WASM is loaded only for CAD files; local input bytes are never uploaded.
153
+ Each job's worker is terminated on completion, cancellation, error or timeout.
154
+ CAD admission estimates 128 MiB plus 40 times the input size. The WASM heap
155
+ is separately capped to at most 512 MiB and to part of the remaining scene
156
+ budget. Output arrays and triangle counts are also checked; these checks
157
+ cannot predict exact peak browser/GPU memory for every CAD file.
158
+ - A strict CSP must allow `script-src 'self' 'wasm-unsafe-eval'` for the worker;
159
+ JavaScript `unsafe-eval` is not needed. Serve `.wasm` as `application/wasm`.
160
+ `scripts/prepare-cad.mjs` replaces the pinned glue's dynamic bindings with
161
+ static closures and adds its heap ceiling. The WASM is unmodified. Adapted
162
+ glue, the recipe, license texts and source references ship with the assets.
163
+ - VSJ JSON and VSB archives containing `json_data.vsj` and STL files.
164
+ - ZIP/3MF uses separately loaded worker chunks containing pinned fflate,
165
+ saxes and xmlchars. Those chunks are not fetched for ordinary STL/OBJ.
166
+ - ZIP accepts stored and deflated entries, validates CRC/length and bounds
167
+ expanded data. ZIP64, encryption and multi-disk archives are unsupported.
168
+ - Local files never leave the browser. Remote URLs obey normal CORS and
169
+ same-origin credential rules; there is no upload service or CORS proxy.
170
+
171
+ Parsing, bounds, flat normals and initial requested smooth/edge geometry
172
+ are calculated in workers. Results use transferable buffers. Workers are
173
+ bounded and terminated after jobs, on cancellation, and on disposal.
174
+
175
+ Changing an already-loaded model to smooth/edges/wireframe derives its
176
+ additional data in a worker. That operation intentionally copies positions
177
+ once, since the main thread retains them for synchronous export and picking.
178
+ Nonuniform-scale area queries also require a cached synchronous scan. These
179
+ are distinct from the initial file handoff, which does not clone geometry.
180
+
181
+ Rendering runs only when needed, including during interaction and animation.
182
+ Transparency uses model-level back-to-front sorting; intersecting transparent
183
+ surfaces can still have ordering artifacts. STL export stores face color,
184
+ so a 3MF triangle's per-vertex gradient cannot survive STL/VSB export exactly.
185
+
186
+ ## Existing API and intentional differences
187
+
188
+ The existing constructor, model options and documented method names are
189
+ implemented. See [the full inventory](v2-compatibility.md). Existing getters
190
+ remain synchronous. `get_vsb_blob()` continues to return a Promise.
191
+
192
+ - `width` / `height` override their `canvas_width` / `canvas_height` aliases.
193
+ - `bg_color` overrides `bgcolor`.
194
+ - `on_model_mousedown` overrides `on_model_mouseclick`; both setter names work.
195
+ - `set_model_units(model_id, units, scale)` keeps the executable three-argument
196
+ signature. The old documentation omitted `model_id` incorrectly.
197
+ - `load_three_files: 'directory/'` remains a legacy alias for the worker
198
+ directory, resolved against the page. It never loads Three.js. An explicit
199
+ `worker_url` takes precedence; false/omitted uses the entry point's directory.
200
+ - `jszip_path` and `jszip_utils_path` are accepted deprecated options, with
201
+ no effect: archive support is shipped in the package's lazy worker chunks.
202
+ No scripts are fetched from those legacy paths.
203
+ - `get_model_info().dims` now updates after scaling. The old implementation
204
+ returned stale dimensions even when area and volume had changed.
205
+ - Zero IDs, zero coordinates, zero rotations and zero opacity are valid.
206
+ Invalid positive-only scales, colors, transforms and IDs report errors.
207
+ - Model-loading batches preserve the legacy final callback order:
208
+ `all_loaded_callback` precedes the final `model_loaded_callback`.
209
+ Failed/cancelled jobs settle the batch instead of leaving it hanging.
210
+ An empty initial viewer does not emit `all_loaded_callback`.
211
+ - `set_center_models` controls centering of subsequently loaded models.
212
+ - Model data remains CPU-resident for synchronous export/picking and context
213
+ restoration. `clean()` and `dispose()` release GPU buffers and worker jobs.
214
+ - `dispose()` removes only the viewer's owned DOM/listeners, not its siblings.
215
+ - The renderer, camera and models are native objects. Direct Three.js calls
216
+ into `.camera`, `.scene`, `.renderer`, or `.models` are not compatible.
217
+
218
+ STL export retains the legacy convention: geometry scale and centering are
219
+ baked into vertices; scene position and rotation are not baked. VSB saves
220
+ position/rotation in its manifest and avoids applying scale twice.
221
+
222
+ Volume is a signed-tetrahedron mesh calculation reported as an absolute
223
+ value, not a watertightness or printability certification. For inch models,
224
+ area/volume follow the legacy conversion convention.
225
+
226
+ ## Optional Three.js mesh bridge
227
+
228
+ The ordinary viewer never imports Three.js. To request a real Three.js
229
+ Mesh from `get_model_mesh`, pass your own instance:
230
+
231
+ ```js
232
+ const viewer = new StlViewer(container, { three: THREE });
233
+ // after loading:
234
+ const mesh = viewer.get_model_mesh(0);
235
+ ```
236
+
237
+ The bridge uses `options.three` or `globalThis.THREE`. With neither present,
238
+ `get_model_mesh` reports a load error and returns null. It does not return
239
+ a misleading imitation Mesh.
240
+
241
+ Import supports static BufferGeometry meshes and legacy Geometry vertices/
242
+ faces. It copies geometry and basic material color/opacity. Placement is
243
+ controlled by the supplied model options, as in the old plugin. Skinning,
244
+ instances, morph targets, child objects, multiple/textured/shader materials
245
+ are rejected. Export returns an independent BufferGeometry Mesh. Dispose
246
+ the returned mesh's geometry and material when finished with it.
247
+
248
+ Use `get_model_data(id)` for a native copy of raw geometry, and
249
+ `add_model({ data, ...options })` to add it without Three.js. Data does not
250
+ include scene transforms; copy those options explicitly when duplicating.
251
+
252
+ Use `get_image_blob()` for screenshots; direct `renderer.domElement.toDataURL`
253
+ outside a draw is not guaranteed because continuous drawing-buffer
254
+ preservation is disabled.
255
+
256
+ ## Additional options and helpers
257
+
258
+ | Option | Default |
259
+ | --- | --- |
260
+ | `worker_url` | Beside the ESM/classic script |
261
+ | `worker_factory` | None |
262
+ | `worker_count` | 2, allowed range 1–4 |
263
+ | `worker_timeout` | 120,000 ms per job |
264
+ | `pixel_ratio` | Device pixel ratio capped at 2 |
265
+ | `max_file_bytes` | 256 MiB |
266
+ | `max_triangles` | 5,000,000 |
267
+ | `max_uncompressed_bytes` | 256 MiB per archive |
268
+ | `max_archive_entries` | 1,024 |
269
+ | `max_memory_bytes` | `Number.MAX_SAFE_INTEGER` (optional estimated working-set budget) |
270
+
271
+ Limits are configurable positive integers. These are ceilings, not guarantees
272
+ that every mobile device has enough memory for the maximum. Derived smooth
273
+ normals/edge maps can use substantially more memory than flat rendering.
274
+
275
+ `max_memory_bytes` adds conservative checks for parsed geometry, text parsing,
276
+ archive expansion, expanded 3MF instances, and derived smooth/wire/edge maps.
277
+ It estimates working memory, including GPU buffers; it does not measure free
278
+ memory. Existing default byte/triangle limits remain unchanged for embedders.
279
+ The ViewSTL preview site configures its own device-dependent limits: half the
280
+ approximate `navigator.deviceMemory` report, or half a 2 GiB fallback if absent.
281
+ This leaves a 50% margin against that estimate, not a guaranteed allocation or
282
+ protection against an OS/browser killing an exhausted tab.
283
+
284
+ Local binary STL files are parsed in chunks of at most 65,536 triangles
285
+ (3.125 MiB input data) plus their 84-byte header. This avoids holding a complete
286
+ input file and a second assembled copy alongside geometry. ASCII/OBJ/archive
287
+ input is still read as a complete buffer; OBJ no longer creates a full array
288
+ of every input line. Native GPU allocation errors clean up partial resources
289
+ and report through `load_error_callback`.
290
+
291
+ `when_ready` resolves after renderer initialization, before initial models
292
+ finish. `when_idle()` waits for pending model, scene and derived-geometry
293
+ work; inspect `load_error_callback` for individual failures. `get_image_blob`
294
+ waits for that work, draws and returns a PNG Blob by default.
295
+
296
+ Keyboard: arrows orbit; + / - zoom; F fits the scene. Mouse: drag orbits,
297
+ shift/right/middle drag pans, wheel zooms. Touch: one finger rotates and
298
+ two fingers pan/pinch. Real mobile-device validation remains necessary.
299
+
300
+ For a same-origin standalone deployment, allow your script and worker assets
301
+ in the site's Content Security Policy (`script-src 'self'; worker-src 'self'`),
302
+ and permit intended remote model hosts in `connect-src`. SharedArrayBuffer,
303
+ cross-origin isolation, eval and worker blob bootstrapping are not required.
304
+
305
+ ## Additional views and section cuts
306
+
307
+ The existing `flat`, `smooth` and `wireframe` display values remain supported.
308
+ New display values are `edges` (solid with 30-degree feature edges), `mesh`
309
+ (triangle overlay), `xray`, `studio` and `orientation` (blue front faces, red
310
+ back faces). None requires Three.js. `view_edges` / `set_edges` retain the
311
+ legacy one-degree edge threshold.
312
+
313
+ ```js
314
+ viewer.set_view(0, {display: 'studio', smooth_shading: true});
315
+ await viewer.when_idle();
316
+ viewer.set_smooth(0, false); // Independent of the selected modern view
317
+ viewer.set_section(0, {axis: 'z', position: 0.5, flip: false});
318
+ viewer.set_section(0, null); // Restore the complete model
319
+ ```
320
+
321
+ These fields are also accepted in model options, returned by `get_model_info`,
322
+ and preserved by VSJ/VSB scene export. `set_display('flat')` and
323
+ `set_display('smooth')` keep their legacy shading behavior. Smooth shading is
324
+ ignored visually in wireframe and retained for the next surface view. Derived
325
+ view changes commit after preparation succeeds; a rejected memory estimate or
326
+ failed worker preserves the previous appearance. Wait for `when_idle()` and
327
+ inspect `load_error_callback` as with model loading.
328
+
329
+ Mesh overlay uses shader barycentric coordinates without an extra geometry
330
+ buffer. Studio, orientation, X-ray and clipping also reuse existing geometry.
331
+ Smooth normals, wireframe and feature edges are computed on demand in a worker
332
+ and checked against the memory budget, including cached derived arrays.
333
+
334
+ Section position is a fraction from 0 to 1 of the source model's bounding box
335
+ along its chosen axis. The plane follows model transforms, clips picking and
336
+ PNG rendering, and does not modify exported STL geometry or model statistics.
337
+ The cut remains open, with an amber boundary highlight; no cap is generated.
338
+ X-ray uses lightweight back-face/front-face alpha passes; it is an illustrative
339
+ view, not exact order-independent transparency for intersecting surfaces.
340
+ Face orientation describes winding relative to the camera, not an automatic
341
+ watertightness or printability diagnosis.
342
+
343
+ ## Optional streaming URL imports
344
+
345
+ The plugin does not silently route existing `filename` requests through a server.
346
+ To retrieve a public URL without depending on the source server's CORS policy,
347
+ configure a same-origin relay and explicitly use `remote_url`:
348
+
349
+ ```js
350
+ const viewer = new StlViewer(container, {url_import_endpoint: '/api/import-url'});
351
+ viewer.add_model({id: 0, remote_url: 'https://example.com/model.stl'});
352
+ // Optional remote_format: 'stl', 'obj', '3mf', 'step', 'stp', 'iges', 'igs'.
353
+ ```
354
+
355
+ `mountViewstlApp` accepts the same endpoint and exposes `app.openURL(url, format)`.
356
+ Its optional `url-dialog`/`url-form` controls are shown in the preview site's HTML.
357
+ The site supplies markup, styles and configuration; URL interaction, loading,
358
+ progress, cancellation and parsing live in the plugin. App jobs allow ten minutes
359
+ by default; low-level viewer worker_timeout remains configurable as before.
360
+
361
+ The relay receives the URL in a JSON POST body and streams the response without
362
+ file storage. A signed URL's query string is not displayed as a model name or
363
+ included in scene exports. Local files still never leave the browser. URL imports
364
+ are a separate privacy path: the relay sees the remote bytes in transit.
365
+
366
+ Binary STL is decoded incrementally in the worker. Other formats use bounded
367
+ browser buffers, including archives and CAD. A complete model appears after
368
+ download and validation; progressive partial rendering is not implemented.
369
+ `get_model_info().file_bytes` reports the imported byte count when available.
370
+ Downloaded names are metadata, not reloadable URLs; use VSB to package a scene
371
+ containing these models. Embedders must provide their own compatible relay; no
372
+ ViewSTL server is implicitly used by the library.
373
+
374
+ When `url_import_endpoint` is configured, `mountViewstlApp` also accepts dropped
375
+ links (`text/uri-list`), Firefox URL/title data, and plain HTTP/HTTPS URL text.
376
+ URL drops use `openURL` and its existing queue, relay, progress, cancellation and
377
+ error handling. Multiple URI-list links are deduplicated per drop. Local files
378
+ take priority over accompanying URL metadata. Plain text is not searched for
379
+ embedded links, HTML drag contents are not parsed, and drops into text inputs or
380
+ editable content retain their normal behavior. This belongs to the optional app
381
+ controller; the low-level viewer's existing file-drop contract is unchanged.
382
+
383
+ The reference relay lives in the website repository's `relay/` directory. Its
384
+ request is `{url, format, max_bytes}` and its response supplies X-ViewSTL-Format,
385
+ percent-encoded X-ViewSTL-Filename, and optional Content-Length. All relay and
386
+ parser failures use the existing error callback. Cancellation terminates the
387
+ worker/fetch; the server then closes the upstream connection.
388
+
389
+ ## Local mesh export
390
+
391
+ The optional `mountViewstlApp` controller can bind a Save dialog using the
392
+ `save-*` control group in the preview site's `site/index.html`. Include the entire
393
+ group or omit it; incomplete groups fail before mounting. All event handling,
394
+ selection, progress, cancellation and download dispatch compile into `viewstl.js`.
395
+ The host supplies markup, styles and explanatory copy (format option `data-note`
396
+ attributes and the CAD limitation paragraph). No Save UI is injected by the
397
+ low-level viewer.
398
+
399
+ The reference dialog exports only the current app selection for mesh formats,
400
+ with positions/rotations applied and millimetre output. Unitless STL/OBJ inputs
401
+ are assumed to be millimetres. Its optional color setting applies to OBJ/3MF;
402
+ colored OBJ uses ZIP. It calls `download_models` with explicit model IDs, so
403
+ closing an editing panel does not clear the export selection. VSB/VSJ always
404
+ export the whole scene. `save-scope` is a text status element displaying the
405
+ selected filename or whole-scene model count, not a scope dropdown. Cancel, Escape,
406
+ the close button, backdrop dismissal, changing selection, starting an import,
407
+ removing/clearing models and app disposal cancel an active export. Errors stay
408
+ in the dialog and permit another format. Existing PNG controls remain separate.
409
+
410
+ All exporters live in the plugin and run in disposable workers. They generate
411
+ local Blobs; no relay, upload, server conversion or third-party network endpoint
412
+ is used. Export modules and ZIP compression load only on demand. STEP/IGES writing
413
+ uses JavaScript and does **not** download the CAD importer or WASM engine.
414
+
415
+ ```js
416
+ await viewer.when_idle();
417
+ const controller = new AbortController();
418
+ const result = await viewer.export_models({
419
+ format: '3mf', // stl, obj, 3mf, step/stp, iges/igs
420
+ model_ids: [0, 2], // omitted = all; 'selected' = selected model
421
+ filename: 'assembly', // correct extension added/replaced automatically
422
+ units: 'mm', // mm (default), inch
423
+ apply_transforms: true, // default: bake position, rotation, scale and centering
424
+ colors: true, // default for OBJ/3MF; ignored for STL/STEP/IGES
425
+ signal: controller.signal,
426
+ on_progress({ loaded, total, phase }) {
427
+ // Triangle counts. phase is 'exporting' or 'complete'.
428
+ // Encoding can reach total before ZIP packaging completes.
429
+ },
430
+ });
431
+ // {blob, filename, format, units, model_ids, triangles, representation: 'mesh'}
432
+ // Keep result.blob, or use the convenience method to initiate a browser download:
433
+ await viewer.download_models({format: 'step', model_ids: 'selected'});
434
+ // controller.abort() rejects an active/queued operation with AbortError.
435
+ ```
436
+
437
+ Exports capture the model state at dispatch; subsequent edits/removal do not
438
+ change an in-flight export. Buffers used for rendering are never transferred or
439
+ detached. No selection, an unknown ID, duplicate IDs or an unfinished model
440
+ rejects the promise. Call `when_idle()` first to include pending imports. Errors
441
+ reject the promise and progress goes only to the supplied callback; exporters do
442
+ not show dialogs or invoke the viewer's import-error callback. A throwing progress
443
+ callback rejects its export and terminates the worker. `clean()`/`dispose()` cancel
444
+ active and queued jobs. The caller owns the returned Blob and its lifetime.
445
+
446
+ `apply_transforms: false` exports source coordinates and their accumulated unit
447
+ conversion, without centering, presentation movement/rotation or user scaling.
448
+ Scene coordinates are treated as millimeters; `units: 'inch'` divides output
449
+ coordinates by 25.4 and writes inch metadata where the format supports it.
450
+ Unitless inputs must be interpreted/scaled appropriately by the embedding app.
451
+ STL and OBJ have no standardized units; OBJ includes an informational comment.
452
+
453
+ | Format | Output and scope |
454
+ | --- | --- |
455
+ | STL | Standard binary STL; selected meshes combined into one triangle stream. Attributes are zero, so no ambiguous vendor color encoding. |
456
+ | 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. |
457
+ | 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. |
458
+ | 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. |
459
+ | 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. |
460
+
461
+ STEP/IGES preserve mesh geometry and placement, but omit colors, textures,
462
+ analytic curves, original CAD topology/assemblies and feature history. They work
463
+ with open triangle meshes, but reject zero-area triangles. No F3D export is
464
+ provided. A consuming site can describe these limitations in its own interface;
465
+ the low-level export API is UI-free. CAD interoperability has been verified
466
+ against Open CASCADE, not every commercial CAD application.
467
+
468
+ Section cuts, camera orientation, grid, selection frames and viewing/shading modes
469
+ are presentation settings and do not modify exported geometry. Use VSJ/VSB for
470
+ ViewSTL scene settings. Texture/UV data discarded at import cannot be recreated.
471
+
472
+ The scene memory budget includes pending exports. Output is encoded in chunks
473
+ instead of building one large text string; Blobs still require browser storage
474
+ and this API does not stream directly to a filesystem handle. Output obeys
475
+ `max_file_bytes`, archives also obey `max_uncompressed_bytes` and
476
+ `max_archive_entries`, and the complete selection obeys `max_triangles`.
477
+ Conservative format-specific memory estimates can reject an export before copying
478
+ geometry. Cancellation terminates its worker, including compression.
479
+
480
+ The existing synchronous `get_stl_bin`/`download_model` and VSJ/VSB methods retain
481
+ their legacy behavior. Use the new API for scene transforms, correct filename
482
+ extensions, asynchronous execution and cancellation.
483
+
484
+ ## Development and packaging
485
+
486
+ ```sh
487
+ pnpm install --frozen-lockfile
488
+ pnpm build
489
+ pnpm test
490
+ pnpm check:types
491
+ pnpm check:legacy
492
+ pnpm start
493
+ pnpm pack
494
+ ```
495
+
496
+ Open `/examples/v2/` for the demo and `/tests/v2/browser.html` for real worker/
497
+ WebGL checks. `pnpm pack` builds a local tarball; it does not publish anything.
498
+ The package allowlist includes v2 artifacts, types, documentation and license
499
+ notices. It excludes legacy snapshots, old dependencies, tests and server data.
500
+
501
+ Before a stable release, validate real mobile hardware, Safari and Firefox,
502
+ larger user fixtures, npm/bundler integrations and the integrations used by
503
+ the production site. Automated and desktop browser results are recorded in
504
+ [verification](v2-verification.md).
505
+
506
+ ## Multiple models, colors and an infinite grid
507
+
508
+ Use `center_models: false` to preserve the coordinates in assembly files, then call
509
+ `add_model` / `add_models` to append files. `set_position` changes presentation
510
+ coordinates without rewriting source geometry. The preview site's Models panel
511
+ adds selection, individual removal and reversible side-by-side placement.
512
+
513
+ ```js
514
+ const viewer = new StlViewer(element, { center_models: false });
515
+ viewer.add_models([{id: 0, local_file: partA}, {id: 1, local_file: partB}]);
516
+ await viewer.when_idle();
517
+ viewer.set_color(0, '#dd766b');
518
+ viewer.set_color(0, null); // Restore embedded file colors (or neutral gray if absent).
519
+ viewer.set_bg_color('#202d35');
520
+ viewer.set_infinite_grid(true, 1, 0); // enabled, base spacing, Z height
521
+ ```
522
+
523
+ The infinite XY grid is a procedural shader. Its spacing adapts as the camera
524
+ zooms and it fades near the horizon. Panning does not allocate a larger grid;
525
+ opaque models occlude it. `set_grid` retains its existing finite-grid behavior.
526
+ Infinite settings round trip through VSJ/VSB as `infinite_grid: {spacing,height}`.
527
+ Colors change shader uniforms without rebuilding the mesh buffers.
528
+
529
+ An explicit `max_memory_bytes` budget now covers the whole scene, including
530
+ resident geometry, cached view arrays and pending derived geometry. Parsing is
531
+ serialized under that budget so each new model receives the remaining allowance.
532
+ Removing a model frees its allowance and cancels its pending derived work.
533
+ These remain conservative estimates, not measurements of free RAM or GPU memory.
534
+
535
+ ### Selecting a model for editing
536
+
537
+ `set_selection(id)` draws a light 3D bounding frame using that model's transform.
538
+ `set_selection(null)` clears it. The frame uses one 288-byte vertex buffer, follows
539
+ rotation/translation/scale without rebuilding mesh buffers, and is excluded from
540
+ PNG and scene exports. Removing or clearing its model releases the frame.
541
+
542
+ `pick_model(clientX, clientY)` returns `{id, distance}` for the nearest visible
543
+ surface, or `null` for empty space. Coordinates use the same viewport coordinate
544
+ system as mouse events. It respects transforms, hidden models and section cuts.
545
+ The preview calls it on double-click only; ordinary dragging does not run a mesh
546
+ intersection scan. The existing legacy click callbacks retain their behavior.
547
+
548
+ The preview provides Move, Rotate and Depth controls per model. Rotation uses
549
+ degrees in the UI and compensates for the original mesh center; the plugin's
550
+ `set_rotation` API continues to accept radians with its existing origin behavior.
551
+ Depth translates along the eye-to-model ray while holding the camera fixed.
552
+
553
+ ## Saving ViewSTL scenes
554
+
555
+ Use `export_scene` for a Blob or `download_scene` for a browser download:
556
+
557
+ ```js
558
+ const result = await viewer.export_scene({
559
+ format: 'vsb', // or 'vsj'
560
+ filename: 'my-scene',
561
+ signal: abortController.signal,
562
+ on_progress({ loaded, total, phase }) {
563
+ // Model counts; exporting, packaging (VSB only), complete.
564
+ },
565
+ });
566
+ await viewer.download_scene({ format: 'vsj', filename: 'scene', force_basename: true });
567
+ ```
568
+
569
+ Both methods always capture the entire loaded scene. They return `{blob,
570
+ filename, format, model_ids, triangles, representation: 'scene'}`. Pending loads,
571
+ empty scenes, excess model/triangle counts, memory and output limits reject.
572
+ Errors reject the Promise instead of using the import-error callback. Cancellation
573
+ and throwing progress callbacks release the worker and memory reservation.
574
+
575
+ VSB uses the existing ZIP layout: `json_data.vsj` plus one colored binary STL per
576
+ model. Mesh encoding and compression run in a disposable worker using copied
577
+ snapshot buffers; renderer buffers remain intact. Camera, background, grid,
578
+ transforms, model colors/opacity, viewing modes and sections stay in the manifest.
579
+ Original model names are kept as `orig_filename`. As with the legacy VSB format,
580
+ STL geometry uses float32 positions and 5-bit per-face colors, taking the first
581
+ vertex color of each triangle. Texture/UV data and original CAD topology/history
582
+ are not reconstructed or included.
583
+
584
+ VSJ is JSON settings and model references, without embedded geometry. The
585
+ `force_basename` option defaults to false for API callers; the preview uses true
586
+ to write only filenames. Referenced files must be supplied separately. A local-file
587
+ reference alone does not grant browser access to a file on disk. VSB is the
588
+ self-contained choice. The preview opens both formats through the file picker,
589
+ drag and drop, and URL opening. Existing `get_vsj`, `get_vsb`, `get_vsb_blob` and their legacy
590
+ download methods retain their signatures and behavior.
591
+
592
+ In the app Save dialog, VSB/VSJ save the entire scene. STL/OBJ/3MF/STEP/IGES save
593
+ only the selected model. There is no user-selectable scope dropdown. The low-level
594
+ `export_models` API still permits explicit multiple-model selections for embedders.
595
+
596
+ ## Opening ViewSTL scenes
597
+
598
+ Use the Promise-based `import_scene` API for local files or public URLs:
599
+
600
+ ```js
601
+ const result = await viewer.import_scene(sceneFile, {
602
+ files: companionFiles, // File[]: model files referenced by a local VSJ.
603
+ signal: abortController.signal,
604
+ on_progress({ loaded, total, phase }) {
605
+ // reading-scene: reader progress; opening-model/complete: model counts.
606
+ },
607
+ });
608
+ console.log(result.model_ids, result.scene, result.used_files);
609
+
610
+ // Requires the viewer's same-origin url_import_endpoint configuration.
611
+ await viewer.import_scene('https://example.com/scene.vsb');
612
+ await viewer.import_scene('https://example.com/download?id=123', { format: 'vsj' });
613
+ ```
614
+
615
+ VSB archives contain `json_data.vsj` and their model files. VSJ manifests reference
616
+ separate files: supplied local files match by exact relative path or unambiguous
617
+ basename. Missing or ambiguous files reject with an actionable error. Absolute
618
+ HTTP/HTTPS references use the configured URL relay. For a remote VSJ, relative
619
+ references resolve against its final directory after redirects, also through the
620
+ relay. Local files and manifests are not uploaded. The relay streams remote files
621
+ without saving them. It still applies its request, size and network limits, so a
622
+ scene with many remote references can hit the relay's rate limit.
623
+
624
+ Imports append models with new IDs and restore camera, background/grid, transforms,
625
+ colors/opacity, view modes, smoothing and sections. Saved centering is converted
626
+ to equivalent model positions without changing existing models' centering. Embed
627
+ canvas size, resize/mouse controls and per-model animation are not imported.
628
+ Scene reading and mesh parsing use workers and the existing memory/triangle/archive
629
+ limits. Models become visible together after all have loaded. Errors, cancellation,
630
+ or a throwing progress callback remove the incoming models and retain the existing
631
+ scene. `clean()` and `dispose()` also cancel pending scene imports. Existing
632
+ `load_vsj` and `load_vsb` retain their previous signatures and behavior.
633
+
634
+ In the preview, choose or drop a local VSJ together with its referenced model files;
635
+ companions used by the scene are not added twice. A VSB needs no separate files.
636
+ Choose VSB/VSJ explicitly in Open URL for a URL without a scene extension. Scene
637
+ opening, progress, cancellation and restored editing controls all live in the
638
+ plugin's app controller; the website initializer remains minimal.
639
+
640
+ ## Checking meshes for 3D printing
641
+
642
+ The optional checks run locally on demand. They analyze the loaded triangle mesh
643
+ from any supported format, including models restored from VSB/VSJ. CAD accuracy
644
+ is limited by the imported tessellation. Analysis does not repair or modify models.
645
+
646
+ ```js
647
+ const controller = new AbortController();
648
+ const report = await viewer.check_printability({
649
+ model_ids: [selectedId], // Omit for all loaded models; 'selected' uses the editing selection.
650
+ level: 'detailed', // 'basic' is the default.
651
+ apply_transforms: true, // Default: current placement/rotation/scale, like mesh export.
652
+ units_confirmed: true, // Confirm the current mm scale for unitless input.
653
+ profile: { // Optional; omit for geometry checks only.
654
+ technology: 'fdm',
655
+ build_volume_mm: [220, 220, 250],
656
+ min_wall_mm: 0.8, // User-chosen thresholds, not universal printing rules.
657
+ overhang_degrees: 45 // Measured from vertical; +Z is print up.
658
+ },
659
+ max_time_ms: 30000, // Per model, maximum 120000.
660
+ signal: controller.signal,
661
+ on_progress: ({ phase, model_index, total_models }) => console.log(phase)
662
+ });
663
+ if (viewer.is_printability_current(report)) {
664
+ viewer.show_printability(report, { model_id: selectedId, issue: 'boundary_edges' });
665
+ }
666
+ viewer.clear_printability();
667
+ ```
668
+
669
+ Basic checks cover degenerate and nearly degenerate triangles, duplicates, open
670
+ boundaries, non-manifold edges and vertices, inconsistent winding, connected
671
+ components and physical-unit provenance. Detailed checks add a native-JavaScript
672
+ BVH for self-intersections (including coplanar overlaps), shell containment,
673
+ orientation and enclosed volume. Separate components can be intentional. Hollow
674
+ models require opposite cavity-shell orientation. Vertex connectivity uses exact
675
+ source coordinates; small gaps are not silently welded closed. Intersection
676
+ checks use the reported scale-dependent numerical tolerance; near-contact cases
677
+ can be inconclusive. The existing 3MF export guards remain independent and unchanged.
678
+
679
+ Optional FDM checks compare dimensions with a build-volume box, identify potential
680
+ overhang regions, estimate bed contact and sample wall thickness. `line_width_mm`
681
+ can supply the wall threshold when `min_wall_mm` is omitted. No printer defaults
682
+ are assumed. Build fit tests current orientation dimensions, not actual placement,
683
+ exclusion zones or alternative rotations. Bed contact virtually lowers each model
684
+ to its minimum Z; the visible grid and camera do not define the print bed.
685
+ Thickness tests at most 2,048 face centres using inward normal rays and can miss
686
+ small features or oblique thin regions. No finding from those samples is explicitly
687
+ inconclusive, never proof of a minimum thickness. Overhangs do not simulate
688
+ bridging/supports; contact area does not predict adhesion/stability. Resin, toolpaths,
689
+ cross-model collisions and Boolean unions are outside these checks.
690
+
691
+ Reports include each check's `pass`, `issue`, `inconclusive` or `not_run` status,
692
+ severity, counts, explanations, bounded highlight samples, dimensions, tolerance,
693
+ timing and optional estimates. `volume_valid` means a geometrically validated
694
+ enclosed volume; its physical size still depends on the `units` check. Overall
695
+ status is `issues_found`, `incomplete` or `no_issues_found`. A basic report remains
696
+ incomplete because detailed checks were not run. Errors and time/memory limits
697
+ never imply a pass. This API does not certify a successful print.
698
+
699
+ Workers and analysis modules load only when requested. Models are checked in
700
+ sequence, with cooperative snapshot copying and aggregate memory admission.
701
+ The working estimate is 64 MiB plus 2,400 bytes per triangle, with bounded report
702
+ allowances; a model that can be viewed may be too large to analyze on the same
703
+ device. One bounded report cache is retained, not the topology/BVH. Geometry,
704
+ transforms, scale and unit changes invalidate reports; colors and camera changes
705
+ do not. Removing a checked model, clean/dispose, AbortSignal cancellation or a
706
+ throwing progress callback releases the active worker and reservations. Report
707
+ metadata uses weak references so retaining a report cannot retain removed meshes.
708
+
709
+ Highlights use temporary native WebGL line buffers and are excluded from PNG,
710
+ mesh and scene exports. They show up to 128 faces or 256 edges per check, through
711
+ occluding surfaces. Closing the site panel clears highlights. The preview exposes
712
+ the feature under **Info → Check for 3D printing**, with selected/all model scope,
713
+ basic/detailed depth and collapsed optional FDM settings. Unit confirmation resets
714
+ when the checked scope or an existing result's geometry/transform changes. A complete
715
+ optional `print-*` control group is bound by `mountViewstlApp`; hosts omitting the
716
+ entire group keep their previous behavior. The website only supplies markup/styles.
717
+
718
+ The check panel stays open while using the canvas, other controls or dialogs,
719
+ loading files, selecting models and clearing the scene. Only its close button or
720
+ Escape with focus inside the panel dismisses it. Reopening it from Info reveals
721
+ and focuses the existing panel. Loading or changing the checked selection cancels
722
+ an active check; stale results are cleared. Loading and empty scenes disable Run
723
+ and show the current availability without closing the panel.