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

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