three-slicer 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,14 +31,18 @@ The package is published as a single npm package, `three-slicer`, with subpath e
31
31
  npm i three-slicer
32
32
  ```
33
33
 
34
- `react`, `react-dom`, and `three` are peer dependencies and npm installs them for you. They stay peers rather than dependencies because React and three must be single instances — a duplicate copy breaks hooks and `instanceof`.
34
+ `react`, `react-dom`, and `three` are **optional** peer dependencies. They stay peers rather than dependencies because React and three must be single instances — a duplicate copy breaks hooks and `instanceof` — and optional because only the UI half needs them: a headless consumer installs none of the three.
35
35
 
36
- Only the viewer and components import them. If you use the headless engine, they are installed into `node_modules` but never enter your bundle:
36
+ Install them yourself when you use the viewer or the settings panel:
37
37
 
38
- | Import path | Needs react/three at runtime |
38
+ ```bash
39
+ npm i three-slicer react react-dom three
40
+ ```
41
+
42
+ | Import path | Needs react/three |
39
43
  | --- | --- |
40
- | `three-slicer`, `/settings`, `/toggle`, `/worker`, `/wasm`, `/data` | no |
41
- | `three-slicer/viewer`, `/viewer/toolpath`, `/viewer/loaders` | yes |
44
+ | `three-slicer`, `/settings`, `/toggle`, `/client`, `/worker`, `/wasm`, `/data` | no |
45
+ | `three-slicer/viewer`, `/viewer/toolpath`, `/viewer/loaders`, `/viewer/gcode` | yes |
42
46
  | `three-slicer/components` | react only |
43
47
 
44
48
  ## Quick Start: Headless Slicing
@@ -66,7 +70,11 @@ console.log(result.gcode)
66
70
  slicer.dispose()
67
71
  ```
68
72
 
69
- `slice()` accepts a binary STL as an `ArrayBuffer` or `Uint8Array`. Parameters can be either a kernel params object or a JSON string.
73
+ `slice()` accepts a binary STL as an `ArrayBuffer` or `Uint8Array`. Parameters can be either a kernel params object or a JSON string — every accepted parameter is listed in the [kernel parameter reference](engine/README.md#kernel-parameter-reference), which is generated from the kernel's own reader.
74
+
75
+ A runnable version of the above ships with the package: `node node_modules/three-slicer/engine/examples/headless.mjs` writes a cube, slices it batch and streamed, and prints the stats.
76
+
77
+ `result.error` is set instead of a result when a slice fails or is cancelled — check it before reading `gcode`.
70
78
 
71
79
  ## Streaming Layers
72
80
 
@@ -102,15 +110,22 @@ export default function App() {
102
110
  const [settings, setSettings] = useState({})
103
111
 
104
112
  return (
105
- <Viewport
106
- settings={settings}
107
- setSettings={setSettings}
108
- />
113
+ <div style={{ position: 'relative', width: '100%', height: '100vh' }}>
114
+ <Viewport
115
+ settings={settings}
116
+ setSettings={setSettings}
117
+ />
118
+ </div>
109
119
  )
110
120
  }
111
121
  ```
112
122
 
113
- The viewer handles model loading, drag and drop, transform controls, multi-plate layout, worker slicing, GPU toolpath preview, and G-code export. It supports STL, OBJ, 3MF (including the production extension used by Orca/Bambu/Prusa), AMF, and PLY out of the box; other formats such as STEP can be added with `registerLoader()`. A `.3mf` written by a slicer is treated as a project, not just geometry: its plate layout, project settings, and support/material painting are restored on import (where a facet carries both paint kinds, material paint wins and the dropped support paint is reported). Viewer and component styles are bundled into their Shadow DOM roots, so host app CSS does not need to import package CSS.
123
+ **The container is not optional.** The viewer fills its nearest *positioned* ancestor — the shadow host is
124
+ `display: contents` and the shell inside is `position: absolute; inset: 0` — so it needs a parent with
125
+ `position: relative` and a real height. In a plain static `<div>` it escapes to the page instead. There is no width
126
+ or height prop.
127
+
128
+ The viewer handles model loading, drag and drop, transform controls, multi-plate layout, worker slicing, GPU toolpath preview, and G-code export. Its keyboard and mouse bindings, the undo boundary, and what a host cannot drive are documented in [viewer/README.md](viewer/README.md). It supports STL, OBJ, 3MF (including the production extension used by Orca/Bambu/Prusa), AMF, and PLY out of the box; other formats such as STEP can be added with `registerLoader()`. A `.3mf` written by a slicer is treated as a project, not just geometry: its plate layout, project settings, and support/material painting are restored on import (where a facet carries both paint kinds, material paint wins and the dropped support paint is reported). Viewer and component styles are bundled into their Shadow DOM roots, so host app CSS does not need to import package CSS.
114
129
 
115
130
  ## Quick Start: Settings Panel
116
131
 
@@ -181,12 +196,14 @@ Available subpaths:
181
196
  | `three-slicer` | Headless WASM engine SDK |
182
197
  | `three-slicer/settings` | Schema defaults and settings-to-kernel mapping |
183
198
  | `three-slicer/toggle` | Enable/disable rule evaluation for settings UIs |
184
- | `three-slicer/worker` | Browser worker entry |
199
+ | `three-slicer/worker` | Browser worker entry, and the typed message protocol |
200
+ | `three-slicer/client` | `createSlicerClient()` — promises over that protocol |
185
201
  | `three-slicer/wasm` | Single-threaded Emscripten WASM glue |
186
202
  | `three-slicer/wasm-mt` | Multithreaded Emscripten WASM glue |
187
203
  | `three-slicer/viewer` | React `<Viewport/>` |
188
204
  | `three-slicer/viewer/toolpath` | GPU toolpath renderer utilities |
189
205
  | `three-slicer/viewer/loaders` | Model loaders |
206
+ | `three-slicer/viewer/gcode` | `parseGcode(text)` — G-code back into the layer stream the renderer consumes |
190
207
  | `three-slicer/components` | React `<SettingsPanel/>` |
191
208
  | `three-slicer/data` | Named exports for extracted metadata |
192
209
  | `three-slicer/data/*.json` | Extracted OrcaSlicer metadata |
@@ -236,6 +253,91 @@ const result = slicer.slice(stlArrayBuffer, params)
236
253
 
237
254
  `deriveKernelParams()` maps the curated set of schema keys currently supported by the kernel (92 today). Other schema keys can still be displayed by the UI, but they may not affect slicing output yet. Vector options are simplified to their first element, except the filament options described below, which keep every extruder's entry.
238
255
 
256
+ ## 3MF Projects
257
+
258
+ A slicer-written `.3mf` is a project, not a mesh file: `Metadata/project_settings.config` holds the flattened
259
+ preset its author sliced with. `<Viewport/>` reads all of it on import, and the two codecs behind that are exported
260
+ for hosts doing it themselves.
261
+
262
+ **Every value in that file is a string**, and reading it raw is not a cosmetic problem — a disabled option is the
263
+ string `"0"`, and `"0"` is truthy, so a raw import turns every disabled option ON. A point is the string `"256x256"`
264
+ while every consumer indexes it as `[x, y]`, so a raw `printable_area[1][0]` is the character `'2'`. Both are
265
+ handled by coercing each value by its config-schema type:
266
+
267
+ ```js
268
+ import { normalizeProjectSettings, serializeProjectSettings } from 'three-slicer/settings'
269
+
270
+ const { settings, applied, skipped } = normalizeProjectSettings(rawConfig)
271
+ // settings: real JS types, ready for deriveKernelParams() and <SettingsPanel/>
272
+ // applied : how many keys survived
273
+ // skipped : keys the config schema does not define (preset bookkeeping, version fields) — dropped, not coerced
274
+ ```
275
+
276
+ Writing one back is the exact inverse, and it matters for the same reason: a raw JS `false` would be read by any
277
+ other slicer's parser as the string `"false"`, which is truthy.
278
+
279
+ ```js
280
+ const config = serializeProjectSettings(settings) // bool -> "1"/"0", point -> "XxY", points group -> "X1xY1,X2xY2"
281
+ ```
282
+
283
+ Upstream's `inherits` / `different_settings_to_system` reconciliation is deliberately not reproduced: it exists to
284
+ rebase a stored preset onto a local vendor preset database, and `project_settings.config` is already flattened, so
285
+ its values are taken as written.
286
+
287
+ ## Preset Files
288
+
289
+ OrcaSlicer's "Config files" dialog imports a `.json` holding one preset — a printer (`machine`), print settings
290
+ (`process`) or a material (`filament`) — plus the zip forms `.orca_printer` / `.orca_bundle` / `.orca_filament`.
291
+ Both directions are supported.
292
+
293
+ ```js
294
+ import { writePresetFile, readPresetFile, presetOptionKeys, printerSettings } from 'three-slicer/settings'
295
+
296
+ // Write. Flattened on purpose: every option of that type, no `inherits`, so the file stands on its own.
297
+ const file = writePresetFile(settings, { type: 'machine', name: 'My printer' })
298
+ // -> { type: 'machine', name: 'My printer', from: 'User', printable_area: ['0x0','256x0',…], … }
299
+
300
+ // Read. `resolveParent` follows `inherits`; without it a vendor file arrives incomplete (see below).
301
+ const { settings: loaded, missingParent, skipped } = readPresetFile(file,
302
+ { resolveParent: (name) => printerSettings(name) })
303
+ ```
304
+
305
+ `presetOptionKeys(type)` is the key set for one type — 175 for `machine`, 370 for `process`, 149 for `filament`,
306
+ taken from upstream's own `Preset::printer_options()` and friends. It is what splits a settings map, which holds
307
+ every type's keys at once, into the file for one of them.
308
+
309
+ **A vendor preset is a diff, not a preset.** Upstream saves a derived preset as the difference against its parent
310
+ (`Preset::save`: *"only save difference if it has parent"*), so a file off the vendor profiles carries a fraction
311
+ of what it appears to. Read Bambu's X1C machine file without resolving `inherits` and it has no `printable_area`
312
+ and no `printable_height` at all — the bed is in the parent. `missingParent` names the parent when it could not be
313
+ resolved; the file's own values are still applied, because dropping the file entirely loses more than it protects.
314
+
315
+ Values are written and read in upstream's own encoding — every value a string, a point as `"XxY"`, per-extruder
316
+ options as arrays of strings — the same shape [3MF Projects](#3mf-projects) uses, since upstream writes both
317
+ through the same serializer.
318
+
319
+ The viewer adds the zip forms and a UI: the printer card's **Load** and **Save** read and write these files, and a
320
+ save routes through [`onExport`](viewer/README.md#taking-the-saves) like every other download.
321
+
322
+ ## Settings UI: Enable/Disable Rules
323
+
324
+ OrcaSlicer greys out options that another option makes irrelevant. Those rules are extracted, and
325
+ `three-slicer/toggle` evaluates them against a settings map — the panel does this internally, and a custom UI can
326
+ do the same:
327
+
328
+ ```js
329
+ import { makeCfg, disabledKeys } from 'three-slicer/toggle'
330
+
331
+ const disabled = disabledKeys(makeCfg(settings))
332
+ // setting key -> the enable_if expression that evaluated false, e.g.
333
+ // { outer_wall_jerk: 'false, variant_index', … }
334
+ if (disabled.outer_wall_jerk) { /* grey the control out; the value says why */ }
335
+ ```
336
+
337
+ Only rules that evaluate to an unambiguous `false` are reported — an expression referring to something the
338
+ evaluator cannot resolve is left out rather than guessed at, so a key missing from the map means "not known to be
339
+ disabled", not "definitely enabled".
340
+
239
341
  ## Materials and Multi-Material
240
342
 
241
343
  A material is a filament preset — temperatures, flow, diameter, cooling, and the retraction/z-hop overrides a material may apply on top of the machine's. Read the catalog through the facade rather than decoding the data file:
@@ -249,7 +351,22 @@ filaments.recommendedFor('Bambu Lab X1 Carbon 0.4 nozzle') // the vendor's sho
249
351
  const material = filaments.settingsFor('Bambu PLA Basic @BBL X1C')
250
352
  ```
251
353
 
252
- The filament key set is disjoint from the process key set, so applying a material never clears a process pick and vice versa.
354
+ **Clear before you apply.** A preset carries only the keys it sets, so merging one over another leaves the previous
355
+ pick's leftovers behind — an ABS material followed by a PLA one keeps ABS's chamber temperature. Every facade
356
+ exposes the exact key set to remove first:
357
+
358
+ ```js
359
+ function applyPreset(settings, preset, keys) {
360
+ const next = { ...settings }
361
+ for (const key of keys) delete next[key]
362
+ return Object.assign(next, preset)
363
+ }
364
+
365
+ setSettings(s => applyPreset(s, filaments.settingsFor(name), filaments.keys))
366
+ ```
367
+
368
+ The same applies to `processPresets().keys` and `printerKeys`. The filament key set is disjoint from the process key
369
+ set, so applying a material never clears a process pick and vice versa.
253
370
 
254
371
  **One material per extruder.** OrcaSlicer stores every filament option as one entry per extruder, so a multi-material settings map writes each extruder's material at its own index:
255
372
 
@@ -283,13 +400,49 @@ Because a single enum serves both jobs, a support blocker paint and an Extruder
283
400
 
284
401
  ## Worker Usage
285
402
 
286
- Use a module worker to run slicing off the browser main thread.
403
+ Slicing blocks its thread for seconds, so in a browser it belongs in a worker. `three-slicer/client` wraps the
404
+ worker's message protocol in promises:
405
+
406
+ ```js
407
+ import { createSlicerClient } from 'three-slicer/client'
408
+ import { deriveKernelParams } from 'three-slicer/settings'
409
+
410
+ const client = createSlicerClient() // creates and owns the worker
411
+ await client.warmup() // optional: load the kernel before the first slice needs it
412
+
413
+ const { gcode, stats } = await client.slice(stlArrayBuffer, deriveKernelParams(settings), {
414
+ onProgress: (done, total) => setProgress(done / total),
415
+ })
416
+
417
+ client.cancel() // stops a running slice — see below
418
+ client.terminate()
419
+ ```
420
+
421
+ Pass `onLayer` to take the layers as they are produced instead of having them assembled, which is what keeps a
422
+ large model out of memory:
423
+
424
+ ```js
425
+ await client.slice(stl, params, { onLayer: ({ z, idx, gcode }) => append(gcode) })
426
+ // the result then carries stats only
427
+ ```
428
+
429
+ The same client drives painting — `prepare`, `paint`, `erase`, `clear`, `overlay`, `importPaint`, `exportPaint`.
430
+ It deliberately leaves two things to the host: the viewer's progress weighting (measured against its own models)
431
+ and its out-of-memory retry ladder, both of which are UI policy rather than protocol.
432
+
433
+ ### Driving the worker directly
434
+
435
+ `createSlicerClient` is a thin wrapper; the protocol underneath is a plain message contract, fully typed in
436
+ `three-slicer/worker`.
287
437
 
288
438
  ```js
289
439
  import { engineWorkerURL } from 'three-slicer'
440
+ import { deriveKernelParams } from 'three-slicer/settings'
290
441
 
291
442
  const worker = new Worker(engineWorkerURL(), { type: 'module' })
292
443
 
444
+ const chunks = []
445
+
293
446
  worker.onmessage = (event) => {
294
447
  const message = event.data
295
448
 
@@ -298,20 +451,50 @@ worker.onmessage = (event) => {
298
451
  }
299
452
 
300
453
  if (message.type === 'layer') {
301
- console.log(message.idx, message.z, message.gcode)
454
+ chunks.push(message.gcode) // paths/widths arrive transferred, not copied
302
455
  }
303
456
 
304
457
  if (message.type === 'done') {
305
- console.log(message.stats)
458
+ console.log(chunks.join(''), message.result.stats)
306
459
  }
307
460
 
308
461
  if (message.type === 'error') {
309
462
  console.error(message.error)
310
463
  }
311
464
  }
465
+
466
+ // A slice request carries NO `cmd` — it is the worker's default action. Every other message (paint, prepare,
467
+ // overlay, …) is addressed by `cmd`, so leaving it off is what selects slicing.
468
+ // `params` must be a JSON STRING here. The direct handle's slice() also accepts an object and stringifies it for
469
+ // you; the worker hands its argument straight to the kernel, which parses JSON text.
470
+ worker.postMessage({ stl: stlArrayBuffer, params: JSON.stringify(deriveKernelParams(settings)) }, [stlArrayBuffer])
312
471
  ```
313
472
 
314
- The worker protocol emits `progress`, `layer`, `done`, and `error` messages. For bundler-specific setups, `three-slicer/worker` exposes the worker entry directly.
473
+ The worker replies with `progress`, `layer`, `done`, and `error` while slicing. `done` carries `{result}` — its
474
+ `stats` when the layers were streamed, plus `gcode` and `layers` when they were not. For bundler-specific setups,
475
+ `three-slicer/worker` exposes the worker entry directly.
476
+
477
+ ### Cancelling a slice
478
+
479
+ Cancellation is a flag the kernel polls from inside its C++ loop, so it works even while the worker is blocked in
480
+ WASM. It lives in a `SharedArrayBuffer`, which means it is only available on the multithreaded kernel — that is, on
481
+ a [cross-origin isolated](#multithreaded-wasm) page. `client.cancel()` handles this and returns `false` when there
482
+ is no flag to write. Driving the worker yourself, the address arrives once, unprompted:
483
+
484
+ ```js
485
+ let cancelView = null
486
+
487
+ worker.addEventListener('message', ({ data }) => {
488
+ if (data.type === 'supsab' && data.cancelPtr) {
489
+ cancelView = new Uint32Array(data.buf, data.cancelPtr, 1)
490
+ }
491
+ })
492
+
493
+ const cancel = () => cancelView && Atomics.store(cancelView, 0, 1)
494
+ ```
495
+
496
+ Without cross-origin isolation the engine runs single-threaded, no `supsab` message arrives, and a slice in
497
+ progress cannot be interrupted — terminate the worker instead.
315
498
 
316
499
  ## Bundler Notes
317
500
 
@@ -379,23 +562,72 @@ the layout by hand.
379
562
 
380
563
  ## API Reference
381
564
 
565
+ ### `three-slicer`
566
+
382
567
  | API | Description |
383
568
  | --- | --- |
384
569
  | `createSlicer()` | Loads the WASM kernel and returns a slicer handle |
385
570
  | `slicer.slice(stl, params, callbacks?)` | Slices binary STL input to G-code or streamed layers |
386
- | `slicer.paintPrepare(stl)` | Prepares painting against a model |
571
+ | `slicer.paintPrepare(stl)` | Prepares painting against a model; returns the facet count |
387
572
  | `slicer.paint(args)` | Paints enforcer/blocker data (boolean pair; the state-addressed form is worker-only) |
388
573
  | `slicer.paintClear()` | Clears painting data for every state |
389
- | `slicer.overlay(enforcer)` | Returns painting overlay data |
390
- | `slicer.heapSize()` | Returns current WASM heap size |
574
+ | `slicer.overlay(enforcer)` | The painted overlay triangles for one state, as a `Float32Array` |
575
+ | `slicer.heapSize()` | Current WASM heap size, bytes (peak, monotonic) |
391
576
  | `slicer.dispose()` | Releases the slicer handle for garbage collection |
392
577
  | `engineWorkerURL()` | Returns a browser worker URL |
393
- | `deriveKernelParams(settings)` | Converts sparse OrcaSlicer settings to kernel params |
578
+
579
+ ### `three-slicer/settings`
580
+
581
+ | API | Description |
582
+ | --- | --- |
583
+ | `deriveKernelParams(settings, opts?)` | Converts sparse OrcaSlicer settings to kernel params. `opts.plate` picks a plate's entry from per-plate options |
584
+ | `schemaDefault(key)` | The config-schema default for a key |
585
+ | `settingRaw(settings, key)` | The map's value, or the schema default |
586
+ | `settingScalar(settings, key)` | The same, reduced to a scalar — the first set entry of a per-extruder column |
587
+ | `normalizeProjectSettings(raw)` | A 3mf's all-strings `project_settings.config` → a typed settings map |
588
+ | `serializeProjectSettings(settings)` | The inverse, for writing one |
589
+ | `writePresetFile(settings, opts)` | A settings map → an OrcaSlicer preset `.json`, flattened |
590
+ | `presetFileText(settings, opts)` | The same, stringified |
591
+ | `readPresetFile(raw, opts)` | A preset `.json` → a settings map; follows `inherits` via `resolveParent` |
592
+ | `presetOptionKeys(type)` | The option keys belonging to `machine` / `process` / `filament` |
593
+ | `printersByVendor` | Vendor → profile name → entry, for building a picker |
394
594
  | `printerSettings(name)` | Settings a vendor machine profile applies |
595
+ | `printerKeys` | Every key a printer profile can set — clear these before applying another |
596
+ | `printerDefaultPreset(name)` | The vendor's recommended process preset for that printer |
597
+ | `machineLimitKeys` | The schema keys the kernel's machine limits are read from |
395
598
  | `processPresets()` | Lazy facade over the print (process) preset catalog |
396
599
  | `filamentPresets()` | Lazy facade over the material preset catalog |
600
+
601
+ ### `three-slicer/toggle`
602
+
603
+ | API | Description |
604
+ | --- | --- |
605
+ | `makeCfg(settings)` | Wraps a settings map for rule evaluation |
606
+ | `disabledKeys(cfg)` | Setting key → the `enable_if` expression that evaluated false |
607
+ | `evalEnableIf(expr, locals, cfg)` | Evaluates a single rule; `null` when it cannot be resolved |
608
+
609
+ ### `three-slicer/viewer` and `three-slicer/components`
610
+
611
+ | API | Description |
612
+ | --- | --- |
397
613
  | `<Viewport/>` | React slicer viewport |
398
614
  | `<SettingsPanel/>` | React settings form |
615
+ | `loadModel(name, buffer)` | Parses STL/OBJ/3MF/AMF/PLY into objects |
616
+ | `registerLoader(exts, fn)` | Adds a format — STEP and anything else needing a heavy dependency |
617
+ | `SUPPORTED_EXT` / `fileExt(name)` | The known extensions, and one file's |
618
+ | `splitConnectedComponents(pos)` | Splits a mesh into its connected components; `null` when there is only one |
619
+ | `parseGcode(text)` | G-code text → the layer stream the renderer consumes |
620
+ | `buildSegmentData(layers, w)` | Layers → the packed segment buffers |
621
+ | `computeColors(data, view, ctx)` | Per-segment colours for a view type |
622
+ | `makeToolpath(THREE, data)` | The GPU toolpath mesh handle |
623
+ | `VIEW_TYPES` / `roleRatios(lengths)` | The preview's view types, and the per-role length split |
624
+
625
+ ### `three-slicer/data`
626
+
627
+ | API | Description |
628
+ | --- | --- |
629
+ | `schema`, `uiTree`, `toggleRules`, `invalidationMap`, `printers` | The extracted metadata, import attributes already applied |
630
+ | `loadProcesses()` / `loadFilaments()` | The two large catalogs, on demand — prefer the `settings` facades |
399
631
 
400
632
  ## Runtime Support
401
633