three-slicer 0.1.2 → 0.1.4

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
@@ -17,7 +17,9 @@ The package is published as a single npm package, `three-slicer`, with subpath e
17
17
  - Browser worker entry for off-main-thread slicing.
18
18
  - React `<Viewport/>` for model import, plate layout, transform controls, slicing, G-code export, and GPU toolpath preview.
19
19
  - React `<SettingsPanel/>` generated from extracted OrcaSlicer metadata.
20
- - Extracted data files for custom UIs: config schema, UI tree, toggle rules, and invalidation map.
20
+ - Vendor printer, print-process and filament preset catalogs, with one material assignable per extruder.
21
+ - Support painting and material painting, plus per-tool filament and purge statistics.
22
+ - Extracted data files for custom UIs: config schema, UI tree, toggle rules, invalidation map, and the printer/process/filament catalogs.
21
23
  - Automatic multithreaded WASM selection on cross-origin-isolated browser pages.
22
24
 
23
25
  ## Installation
@@ -188,7 +190,7 @@ Available subpaths:
188
190
 
189
191
  ## TypeScript
190
192
 
191
- Type declarations ship with the package — no `@types/*` needed. All 907 setting keys are typed from the config schema, enum values included:
193
+ Type declarations ship with the package — no `@types/*` needed. All 923 setting keys are typed from the config schema, enum values included:
192
194
 
193
195
  ```ts
194
196
  import { createSlicer, type SlicerSettings } from 'three-slicer'
@@ -229,7 +231,52 @@ const params = deriveKernelParams(settings)
229
231
  const result = slicer.slice(stlArrayBuffer, params)
230
232
  ```
231
233
 
232
- `deriveKernelParams()` maps the curated set of schema keys currently supported by the kernel. 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.
234
+ `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.
235
+
236
+ ## Materials and Multi-Material
237
+
238
+ 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:
239
+
240
+ ```js
241
+ import { filamentPresets } from 'three-slicer/settings'
242
+
243
+ const filaments = await filamentPresets()
244
+ filaments.listFor('Bambu Lab X1 Carbon 0.4 nozzle') // [{name, type, vendor}, …]
245
+ filaments.recommendedFor('Bambu Lab X1 Carbon 0.4 nozzle') // the vendor's shortlist for that machine model
246
+ const material = filaments.settingsFor('Bambu PLA Basic @BBL X1C')
247
+ ```
248
+
249
+ The filament key set is disjoint from the process key set, so applying a material never clears a process pick and vice versa.
250
+
251
+ **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:
252
+
253
+ ```js
254
+ const params = {
255
+ ...deriveKernelParams({
256
+ nozzle_temperature: [255, 220], // T0 ABS, T1 PLA
257
+ filament_flow_ratio: [0.95, 1.0],
258
+ }),
259
+ extruder_count: 2, // set on the params directly; not a derived schema key
260
+ }
261
+ ```
262
+
263
+ The kernel reloads the loaded-filament settings at every tool change and reports the split back as `stats.filament_mm_by_tool` (indexed by tool number, sums to `filament_mm`) plus `stats.filament_mm_purge` for the prime/wipe tower share. `support_filament` and `support_interface_filament` are 1-based filament indices — `0` means "keep the loaded tool" — choosing which extruder prints the support base/raft and the support interface.
264
+
265
+ A single-material slice sends none of these keys and produces exactly the G-code it produced before multi-material existed.
266
+
267
+ ### Painting a region onto another extruder
268
+
269
+ Painting is the only way a single object can print in two materials. Painting states follow OrcaSlicer's own enum, in which state `1` is both the support enforcer and Extruder 1, state `2` is both the support blocker and Extruder 2, and `3..16` are Extruders 3 to 16. The state-addressed protocol is on the worker; `slicer.paint()` on the direct handle is the original enforcer/blocker boolean pair.
270
+
271
+ ```js
272
+ worker.postMessage({ cmd: 'prepare', stl })
273
+ worker.postMessage({ cmd: 'paint', state: 3, facet, hx, hy, hz, cx, cy, cz, radius, states: [1, 2, 3] })
274
+ worker.postMessage({ cmd: 'erase', facet, hx, hy, hz, cx, cy, cz, radius })
275
+ ```
276
+
277
+ `erase` is a separate command rather than `state: 0`, because a JS `false` coerces to `0` at the WASM boundary and the legacy blocker brush sends exactly that — so the state path refuses `0` outright.
278
+
279
+ Because a single enum serves both jobs, a support blocker paint and an Extruder 2 paint are the same mark on a facet, and painted materials only take effect with support turned off (the painted multi-material path emits no support). Support painting is unaffected: a support-enabled slice stays on the single-material path.
233
280
 
234
281
  ## Worker Usage
235
282
 
@@ -315,10 +362,17 @@ The package includes extracted OrcaSlicer metadata for custom interfaces:
315
362
 
316
363
  | File | Use |
317
364
  | --- | --- |
318
- | `three-slicer/data/config-schema.json` | Setting definitions, defaults, labels, units, and option metadata |
365
+ | `three-slicer/data/config-schema.json` | Setting definitions, defaults, labels, units, and option metadata (923 options) |
319
366
  | `three-slicer/data/ui-tree.json` | Tab, page, group, and option layout |
320
367
  | `three-slicer/data/toggle-rules.json` | Original enable/disable rule metadata |
321
368
  | `three-slicer/data/invalidation-map.json` | Setting invalidation/dependency metadata |
369
+ | `three-slicer/data/printers.json` | 1,035 vendor machine profiles across 64 vendors: motion limits, bed, nozzle |
370
+ | `three-slicer/data/processes.js` | 2,243 print presets (speeds, accelerations) |
371
+ | `three-slicer/data/filaments.js` | 5,999 material presets over 81 filament types, plus each printer model's recommended list |
372
+
373
+ The last three are large, column-oriented and deduplicated, and they load on demand. Read them through
374
+ `printerSettings()`, `processPresets()` and `filamentPresets()` from `three-slicer/settings` rather than decoding
375
+ the layout by hand.
322
376
 
323
377
  ## API Reference
324
378
 
@@ -326,14 +380,17 @@ The package includes extracted OrcaSlicer metadata for custom interfaces:
326
380
  | --- | --- |
327
381
  | `createSlicer()` | Loads the WASM kernel and returns a slicer handle |
328
382
  | `slicer.slice(stl, params, callbacks?)` | Slices binary STL input to G-code or streamed layers |
329
- | `slicer.paintPrepare(stl)` | Prepares support painting against a model |
330
- | `slicer.paint(args)` | Paints support enforcer/blocker data |
331
- | `slicer.paintClear()` | Clears support painting data |
332
- | `slicer.overlay(enforcer)` | Returns support painting overlay data |
383
+ | `slicer.paintPrepare(stl)` | Prepares painting against a model |
384
+ | `slicer.paint(args)` | Paints enforcer/blocker data (boolean pair; the state-addressed form is worker-only) |
385
+ | `slicer.paintClear()` | Clears painting data for every state |
386
+ | `slicer.overlay(enforcer)` | Returns painting overlay data |
333
387
  | `slicer.heapSize()` | Returns current WASM heap size |
334
388
  | `slicer.dispose()` | Releases the slicer handle for garbage collection |
335
389
  | `engineWorkerURL()` | Returns a browser worker URL |
336
390
  | `deriveKernelParams(settings)` | Converts sparse OrcaSlicer settings to kernel params |
391
+ | `printerSettings(name)` | Settings a vendor machine profile applies |
392
+ | `processPresets()` | Lazy facade over the print (process) preset catalog |
393
+ | `filamentPresets()` | Lazy facade over the material preset catalog |
337
394
  | `<Viewport/>` | React slicer viewport |
338
395
  | `<SettingsPanel/>` | React settings form |
339
396
 
@@ -354,6 +411,8 @@ The package includes extracted OrcaSlicer metadata for custom interfaces:
354
411
  - `registerLoader(exts, fn)` from `three-slicer/viewer/loaders` adds any other format. Formats needing a heavy dependency are kept out of the package so it stays runtime-dependency-free — see `web/viewer/src/step_loader.js` for a STEP loader built on `occt-import-js` (OCCT WASM).
355
412
  - Not every OrcaSlicer schema key is wired into the WASM kernel yet.
356
413
  - Some vector settings are simplified to their first element.
414
+ - Material painting and support cannot currently produce two materials on the same slice: the painted multi-material path emits no support, and one triangle selector serves both brushes.
415
+ - The multi-material prime tower is a real ported wipe tower, but the fallback square ring used when it fails is not.
357
416
  - Multithreaded WASM requires cross-origin isolation.
358
417
  - `three` is pinned as a peer dependency because viewer internals depend on the `TransformControls` API shape.
359
418
 
@@ -4,7 +4,7 @@ Reusable React components for the browser slicer. Props-driven — no global sta
4
4
 
5
5
  ## `<SettingsPanel/>`
6
6
 
7
- An OrcaSlicer-style settings form generated from `@three-slicer/data` (schema + UI tree + toggle rules): tab/page/group navigation, mode filter (simple/advanced/expert), search across 907 options, dirty markers with per-option reset, and enable/disable rules evaluation.
7
+ An OrcaSlicer-style settings form generated from `@three-slicer/data` (schema + UI tree + toggle rules): tab/page/group navigation, mode filter (simple/advanced/expert), search across 923 options, dirty markers with per-option reset, and enable/disable rules evaluation.
8
8
 
9
9
  ```bash
10
10
  npm i three-slicer/components react
@@ -1,33 +1,33 @@
1
- import e, { useRef as D, useState as h, useLayoutEffect as O, useEffect as P, useMemo as A } from "react";
1
+ import e, { useRef as _, useState as h, useLayoutEffect as D, useEffect as P, useMemo as A } from "react";
2
2
  import { createPortal as F } from "react-dom";
3
3
  import { uiTree as S, schema as C } from "three-slicer/data";
4
4
  import { settingRaw as J } from "three-slicer/settings";
5
5
  import { disabledKeys as I, makeCfg as B } from "three-slicer/toggle";
6
6
  function L({ css: t, className: s, children: r }) {
7
- const i = D(null), [c] = h(() => {
7
+ const i = _(null), [c] = h(() => {
8
8
  const l = document.createElement("div");
9
9
  return l.style.display = "contents", l;
10
10
  });
11
- return O(() => {
11
+ return D(() => {
12
12
  const l = i.current, o = l.shadowRoot || l.attachShadow({ mode: "open" });
13
13
  let p = o.querySelector("style[data-ts-css]");
14
14
  p || (p = document.createElement("style"), p.setAttribute("data-ts-css", ""), o.append(p)), p.textContent = t, o.contains(c) || o.append(c);
15
15
  }, [t, c]), /* @__PURE__ */ e.createElement("div", { ref: i, className: s }, F(r, c));
16
16
  }
17
- const T = ':host{display:contents}*{box-sizing:border-box}.row{display:grid;grid-template-columns:260px 220px 70px 1fr;gap:8px;align-items:center;padding:3px 0;border-bottom:1px dashed #f0f0f0}.row .lbl{color:#222;text-decoration:none}.row .lbl:hover{color:#0a7d4f;text-decoration:underline}.row .key{font-size:11px;color:#999;overflow:hidden;text-overflow:ellipsis}.row input[type=text],.row input:not([type]),.row select,.row textarea{width:100%;max-width:260px;padding:2px 6px}.inputwrap{display:inline-flex;align-items:center;gap:4px}.unit{color:#888;font-size:12px;white-space:nowrap}.pts{font-size:11px;color:#666}.badge{font-size:10px;padding:1px 6px;border-radius:8px;color:#fff;text-align:center}.mode-simple{background:#2e9e5b}.mode-advanced{background:#3673c3}.mode-expert{background:#d9822b}.mode-develop{background:#c0392b}.mode-none,.mode-undefined{background:#aaa}.muted{color:#999;font-weight:400;font-size:12px}.row.custom{grid-template-columns:1fr}.row.disabled{opacity:.42}.row.disabled input,.row.disabled select,.row.disabled textarea{cursor:not-allowed;background:#f2f2f2}.lbl-cell{display:inline-flex;align-items:center;gap:5px;min-width:0}.lbl-cell .lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dirty-dot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:#e8873a;box-shadow:0 0 0 2px #fff3e6}.widget-cell{display:inline-flex;align-items:center;gap:6px;min-width:0}.reset-btn{flex:0 0 auto;border:1px solid #d8b48a;background:#fbf1e4;color:#b5701f;border-radius:5px;cursor:pointer;font-size:13px;line-height:1;padding:1px 5px}.reset-btn:hover{background:#f3e2cb}.settings-panel{flex:0 0 400px;width:400px;border-left:1px solid #e2e2e2;display:flex;flex-direction:column;min-height:0;background:#fff;color:#222;font:14px/1.5 -apple-system,Segoe UI,sans-serif}.settings-panel .sp-top{padding:10px 12px 8px;border-bottom:1px solid #eee}.sp-builder{display:flex;align-items:center;gap:8px;font-size:12px;color:#666}.sp-builder select{flex:1;padding:4px 6px}.sp-pages{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}.sp-pages button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:4px 8px;font-size:12px;cursor:pointer}.sp-pages button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.sp-modes{display:flex;gap:4px;margin-top:8px}.sp-modes button{border:0;background:none;font-size:11px;color:#888;padding:2px 6px;border-radius:6px;cursor:pointer}.sp-modes button.on{background:#eef2f0;color:#0a7d4f;font-weight:700}.settings-panel .sp-body{flex:1 1 auto;overflow-y:auto;padding:8px 14px 24px}.settings-panel .sp-body h2{font-size:16px}.settings-panel .row{grid-template-columns:1fr auto;grid-template-areas:"lbl badge" "key widget";row-gap:2px;padding:6px 0}.settings-panel .row .lbl{grid-area:lbl;font-weight:600}.settings-panel .row .badge{grid-area:badge;justify-self:end}.settings-panel .row .key{grid-area:key}.settings-panel .row .inputwrap,.settings-panel .row>select,.settings-panel .row>input,.settings-panel .row>textarea,.settings-panel .row>.pts,.settings-panel .row>.muted{grid-area:widget;justify-self:end}.settings-panel .row input:not([type=checkbox]):not([type=color]),.settings-panel .row select,.settings-panel .row textarea{max-width:200px}.settings-panel .row.custom{grid-template-columns:1fr;grid-template-areas:"lbl"}.sp-search{width:100%;box-sizing:border-box;padding:6px 9px;border:1px solid #d6d6d6;border-radius:6px;font-size:13px;margin-bottom:8px}:host(.sp-embedded) .settings-panel{position:static;width:auto;flex:none;border:0;height:auto}:host(.sp-embedded) .sp-body{padding:4px 2px 8px;max-height:460px}.bse{display:flex;flex-direction:column;gap:6px;min-width:0;max-width:220px}.bse-modes{display:flex;gap:4px}.bse-modes button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer}.bse-modes button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.bse-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 8px}.bse-grid label{display:flex;align-items:center;gap:4px;font-size:11px;color:#666;white-space:nowrap}.bse-grid input{width:56px;padding:2px 4px;font-size:12px}.bse-json{width:100%;font:11px ui-monospace,monospace;padding:4px 6px;border:1px solid #d6d6d6;border-radius:6px;box-sizing:border-box}.bse-json.err{border-color:#d0435a;background:#fdf3f4}', j = (t, s, r, i) => [[r, i], [t + r, i], [t + r, s + i], [r, s + i]], $ = (t) => {
17
+ const $ = ':host{display:contents}*{box-sizing:border-box}.row{display:grid;grid-template-columns:260px 220px 70px 1fr;gap:8px;align-items:center;padding:3px 0;border-bottom:1px dashed #f0f0f0}.row .lbl{color:#222;text-decoration:none}.row .lbl:hover{color:#0a7d4f;text-decoration:underline}.row .key{font-size:11px;color:#999;overflow:hidden;text-overflow:ellipsis}.row input[type=text],.row input:not([type]),.row select,.row textarea{width:100%;max-width:260px;padding:2px 6px}.inputwrap{display:inline-flex;align-items:center;gap:4px}.unit{color:#888;font-size:12px;white-space:nowrap}.pts{font-size:11px;color:#666}.badge{font-size:10px;padding:1px 6px;border-radius:8px;color:#fff;text-align:center}.mode-simple{background:#2e9e5b}.mode-advanced{background:#3673c3}.mode-expert{background:#d9822b}.mode-develop{background:#c0392b}.mode-none,.mode-undefined{background:#aaa}.muted{color:#999;font-weight:400;font-size:12px}.row.custom{grid-template-columns:1fr}.row.disabled{opacity:.42}.row.disabled input,.row.disabled select,.row.disabled textarea{cursor:not-allowed;background:#f2f2f2}.lbl-cell{display:inline-flex;align-items:center;gap:5px;min-width:0}.lbl-cell .lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dirty-dot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:#e8873a;box-shadow:0 0 0 2px #fff3e6}.widget-cell{display:inline-flex;align-items:center;gap:6px;min-width:0}.reset-btn{flex:0 0 auto;border:1px solid #d8b48a;background:#fbf1e4;color:#b5701f;border-radius:5px;cursor:pointer;font-size:13px;line-height:1;padding:1px 5px}.reset-btn:hover{background:#f3e2cb}.settings-panel{flex:0 0 400px;width:400px;border-left:1px solid #e2e2e2;display:flex;flex-direction:column;min-height:0;background:#fff;color:#222;font:14px/1.5 -apple-system,Segoe UI,sans-serif}.settings-panel .sp-top{padding:10px 12px 8px;border-bottom:1px solid #eee}.sp-builder{display:flex;align-items:center;gap:8px;font-size:12px;color:#666}.sp-builder select{flex:1;padding:4px 6px}.sp-pages{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}.sp-pages button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:4px 8px;font-size:12px;cursor:pointer}.sp-pages button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.sp-modes{display:flex;gap:4px;margin-top:8px}.sp-modes button{border:0;background:none;font-size:11px;color:#888;padding:2px 6px;border-radius:6px;cursor:pointer}.sp-modes button.on{background:#eef2f0;color:#0a7d4f;font-weight:700}.settings-panel .sp-body{flex:1 1 auto;overflow-y:auto;padding:8px 14px 24px}.settings-panel .sp-body h2{font-size:16px}.settings-panel .row{grid-template-columns:1fr auto;grid-template-areas:"lbl badge" "key widget";row-gap:2px;padding:6px 0}.settings-panel .row .lbl{grid-area:lbl;font-weight:600}.settings-panel .row .badge{grid-area:badge;justify-self:end}.settings-panel .row .key{grid-area:key}.settings-panel .row .inputwrap,.settings-panel .row>select,.settings-panel .row>input,.settings-panel .row>textarea,.settings-panel .row>.pts,.settings-panel .row>.muted{grid-area:widget;justify-self:end}.settings-panel .row input:not([type=checkbox]):not([type=color]),.settings-panel .row select,.settings-panel .row textarea{max-width:200px}.settings-panel .row.custom{grid-template-columns:1fr;grid-template-areas:"lbl"}.sp-search{width:100%;box-sizing:border-box;padding:6px 9px;border:1px solid #d6d6d6;border-radius:6px;font-size:13px;margin-bottom:8px}:host(.sp-embedded) .settings-panel{position:static;width:auto;flex:none;border:0;height:auto}:host(.sp-embedded) .sp-body{padding:4px 2px 8px;max-height:460px}.bse{display:flex;flex-direction:column;gap:6px;min-width:0;max-width:220px}.bse-modes{display:flex;gap:4px}.bse-modes button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer}.bse-modes button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.bse-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 8px}.bse-grid label{display:flex;align-items:center;gap:4px;font-size:11px;color:#666;white-space:nowrap}.bse-grid input{width:56px;padding:2px 4px;font-size:12px}.bse-json{width:100%;font:11px ui-monospace,monospace;padding:4px 6px;border:1px solid #d6d6d6;border-radius:6px;box-sizing:border-box}.bse-json.err{border-color:#d0435a;background:#fdf3f4}', j = (t, s, r, i) => [[r, i], [t + r, i], [t + r, s + i], [r, s + i]], T = (t) => {
18
18
  const s = t / 2, r = 32;
19
19
  return Array.from({ length: r }, (i, c) => {
20
20
  const l = 2 * Math.PI * c / r;
21
21
  return [+(s + s * Math.cos(l)).toFixed(2), +(s + s * Math.sin(l)).toFixed(2)];
22
22
  });
23
- }, R = (t) => {
23
+ }, O = (t) => {
24
24
  const s = t.map((i) => i[0]), r = t.map((i) => i[1]);
25
25
  return { x0: Math.min(...s), y0: Math.min(...r), w: Math.max(...s) - Math.min(...s), d: Math.max(...r) - Math.min(...r) };
26
26
  };
27
27
  function U({ value: t, onChange: s, disabled: r }) {
28
- const i = Array.isArray(t) && t.length >= 3 ? t : j(200, 200, 0, 0), c = R(i), [l, o] = h(i.length === 4 ? "rect" : i.length >= 12 ? "circle" : "custom"), [p, x] = h({ w: c.w, d: c.d, ox: c.x0, oy: c.y0 }), [w, u] = h(Math.round(c.w)), [d, f] = h(JSON.stringify(i)), [E, N] = h(!1);
28
+ const i = Array.isArray(t) && t.length >= 3 ? t : j(200, 200, 0, 0), c = O(i), [l, o] = h(i.length === 4 ? "rect" : i.length >= 12 ? "circle" : "custom"), [p, x] = h({ w: c.w, d: c.d, ox: c.x0, oy: c.y0 }), [w, u] = h(Math.round(c.w)), [d, f] = h(JSON.stringify(i)), [E, N] = h(!1);
29
29
  P(() => {
30
- const a = R(i);
30
+ const a = O(i);
31
31
  x({ w: a.w, d: a.d, ox: a.x0, oy: a.y0 }), u(Math.round(a.w)), f(JSON.stringify(i)), N(!1);
32
32
  }, [JSON.stringify(t)]);
33
33
  const y = (a) => {
@@ -35,7 +35,7 @@ function U({ value: t, onChange: s, disabled: r }) {
35
35
  const { w: m, d: n, ox: b, oy: v } = a;
36
36
  m > 0 && n > 0 && s(j(m, n, b, v));
37
37
  }, k = (a) => {
38
- u(a), a > 0 && s($(a));
38
+ u(a), a > 0 && s(T(a));
39
39
  }, M = (a) => {
40
40
  f(a);
41
41
  try {
@@ -110,7 +110,7 @@ function U({ value: t, onChange: s, disabled: r }) {
110
110
  }
111
111
  const W = {
112
112
  printable_area: U
113
- }, _ = Object.keys(S).filter((t) => S[t].length > 0), q = ["all", "simple", "advanced", "expert", "develop"], G = (t) => t.replace("::build", "").replace("Tab", "");
113
+ }, R = Object.keys(S).filter((t) => S[t].length > 0), q = ["all", "simple", "advanced", "expert", "develop"], G = (t) => t.replace("::build", "").replace("Tab", "");
114
114
  function H(t, s) {
115
115
  var c;
116
116
  if (!t || !(s in t)) return !1;
@@ -163,13 +163,13 @@ function Z({ optKey: t, settings: s, setSettings: r, disabled: i, onOptionOpen:
163
163
  return /* @__PURE__ */ e.createElement("div", { className: "row" + (x ? " disabled" : ""), title: x ? `Disabled when: ${p}` : (o == null ? void 0 : o.tooltip) ?? "", "data-testid": `row-${t}` }, /* @__PURE__ */ e.createElement("span", { className: "lbl-cell" }, w && /* @__PURE__ */ e.createElement("span", { className: "dirty-dot", title: "Changed from default", "data-testid": `dirty-${t}` }), c ? /* @__PURE__ */ e.createElement("button", { type: "button", className: "lbl lbl-link", onClick: () => c(t), title: "Details" }, d) : /* @__PURE__ */ e.createElement("span", { className: "lbl" }, d)), /* @__PURE__ */ e.createElement("code", { className: "key" }, t), /* @__PURE__ */ e.createElement(Y, { mode: o == null ? void 0 : o.mode }), /* @__PURE__ */ e.createElement("span", { className: "widget-cell" }, /* @__PURE__ */ e.createElement(V, { def: o, optKey: t, settings: s, setSettings: r, disabled: x, customWidgets: l }), w && /* @__PURE__ */ e.createElement("button", { className: "reset-btn", onClick: u, title: "Reset to default", "data-testid": `reset-${t}` }, "↺")));
164
164
  }
165
165
  function re({ settings: t, setSettings: s, onOptionOpen: r, embedded: i = !1, customWidgets: c, only: l }) {
166
- const [o, p] = h((l == null ? void 0 : l.builder) ?? _[0] ?? ""), [x, w] = h(0), [u, d] = h("all"), [f, E] = h(""), N = S[(l == null ? void 0 : l.builder) ?? o] ?? [], y = l != null && l.page ? N.filter((n) => n.page === l.page) : N, k = y[Math.min(x, y.length - 1)], M = A(() => I(B(t)), [t]), g = f.trim().toLowerCase(), a = A(() => g ? Object.entries(C).filter(([n, b]) => {
166
+ const [o, p] = h((l == null ? void 0 : l.builder) ?? R[0] ?? ""), [x, w] = h(0), [u, d] = h("all"), [f, E] = h(""), N = S[(l == null ? void 0 : l.builder) ?? o] ?? [], y = l != null && l.page ? N.filter((n) => n.page === l.page) : N, k = y[Math.min(x, y.length - 1)], M = A(() => I(B(t)), [t]), g = f.trim().toLowerCase(), a = A(() => g ? Object.entries(C).filter(([n, b]) => {
167
167
  var v, z;
168
168
  return n.includes(g) || ((v = b.label) == null ? void 0 : v.toLowerCase().includes(g)) || ((z = b.full_label) == null ? void 0 : z.toLowerCase().includes(g));
169
169
  }).slice(0, 60).map(([n]) => n) : [], [g]), m = (n, b) => /* @__PURE__ */ e.createElement(Z, { key: n + b, optKey: n, settings: t, setSettings: s, disabled: M, onOptionOpen: r, customWidgets: c });
170
- return /* @__PURE__ */ e.createElement(L, { css: T, className: i ? "sp-embedded" : void 0 }, /* @__PURE__ */ e.createElement("div", { className: "settings-panel" }, !l && /* @__PURE__ */ e.createElement("div", { className: "sp-top" }, /* @__PURE__ */ e.createElement("input", { className: "sp-search", placeholder: "Search options (907)…", value: f, onChange: (n) => E(n.target.value), "data-testid": "opt-search" }), !g && /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement("label", { className: "sp-builder" }, /* @__PURE__ */ e.createElement("span", null, "Settings group"), /* @__PURE__ */ e.createElement("select", { value: o, onChange: (n) => {
170
+ return /* @__PURE__ */ e.createElement(L, { css: $, className: i ? "sp-embedded" : void 0 }, /* @__PURE__ */ e.createElement("div", { className: "settings-panel" }, !l && /* @__PURE__ */ e.createElement("div", { className: "sp-top" }, /* @__PURE__ */ e.createElement("input", { className: "sp-search", placeholder: `Search options (${Object.keys(C).length})…`, value: f, onChange: (n) => E(n.target.value), "data-testid": "opt-search" }), !g && /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement("label", { className: "sp-builder" }, /* @__PURE__ */ e.createElement("span", null, "Settings group"), /* @__PURE__ */ e.createElement("select", { value: o, onChange: (n) => {
171
171
  p(n.target.value), w(0);
172
- } }, _.map((n) => /* @__PURE__ */ e.createElement("option", { key: n, value: n }, G(n))))), /* @__PURE__ */ e.createElement("div", { className: "sp-pages" }, y.map((n, b) => /* @__PURE__ */ e.createElement("button", { key: n.page + b, className: b === Math.min(x, y.length - 1) ? "on" : "", onClick: () => w(b) }, n.page))), /* @__PURE__ */ e.createElement("div", { className: "sp-modes" }, q.map((n) => /* @__PURE__ */ e.createElement("button", { key: n, className: u === n ? "on" : "", onClick: () => d(n) }, n))))), /* @__PURE__ */ e.createElement("div", { className: "sp-body" }, g ? /* @__PURE__ */ e.createElement("section", { "data-testid": "search-results" }, /* @__PURE__ */ e.createElement("h3", null, "Search: “", f, "” — ", a.length, " hit", a.length === 1 ? "" : "s", a.length === 60 ? "+" : ""), a.map(m), !a.length && /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No matches")) : k ? /* @__PURE__ */ e.createElement(e.Fragment, null, !l && /* @__PURE__ */ e.createElement("h2", null, k.page, " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, "Tab.cpp:", k.line)), k.groups.map((n) => {
172
+ } }, R.map((n) => /* @__PURE__ */ e.createElement("option", { key: n, value: n }, G(n))))), /* @__PURE__ */ e.createElement("div", { className: "sp-pages" }, y.map((n, b) => /* @__PURE__ */ e.createElement("button", { key: n.page + b, className: b === Math.min(x, y.length - 1) ? "on" : "", onClick: () => w(b) }, n.page))), /* @__PURE__ */ e.createElement("div", { className: "sp-modes" }, q.map((n) => /* @__PURE__ */ e.createElement("button", { key: n, className: u === n ? "on" : "", onClick: () => d(n) }, n))))), /* @__PURE__ */ e.createElement("div", { className: "sp-body" }, g ? /* @__PURE__ */ e.createElement("section", { "data-testid": "search-results" }, /* @__PURE__ */ e.createElement("h3", null, "Search: “", f, "” — ", a.length, " hit", a.length === 1 ? "" : "s", a.length === 60 ? "+" : ""), a.map(m), !a.length && /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No matches")) : k ? /* @__PURE__ */ e.createElement(e.Fragment, null, !l && /* @__PURE__ */ e.createElement("h2", null, k.page, " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, "Tab.cpp:", k.line)), k.groups.map((n) => {
173
173
  const b = n.options.filter((v) => Q(v, u) && !(l && v.startsWith("<")));
174
174
  return b.length ? /* @__PURE__ */ e.createElement("section", { key: n.group + n.line }, /* @__PURE__ */ e.createElement("h3", null, n.group || "(untitled group)", " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, ":", n.line)), b.map(m)) : null;
175
175
  })) : /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No builder"))));
package/data/README.md CHANGED
@@ -4,11 +4,48 @@ OrcaSlicer configuration metadata extracted from the original C++ sources, as pl
4
4
 
5
5
  | File | Contents |
6
6
  |---|---|
7
- | `config-schema.json` | 907 print options: type, label, category, tooltip, min/max, mode, enum values/labels, defaults, source line numbers |
7
+ | `config-schema.json` | 923 print options: type, label, category, tooltip, min/max, mode, enum values/labels, defaults, source line numbers |
8
8
  | `config-schema-builddump.json` | Build-verified cross-check: the real `print_config_def` (817 options) dumped from a WASM compile of the original `PrintConfig.cpp` |
9
- | `ui-tree.json` | Tab → page → group → option tree from `Tab.cpp` (34 pages, 587 option references) |
9
+ | `ui-tree.json` | Tab → page → group → option tree from `Tab.cpp` (20 pages, 542 option references) |
10
10
  | `toggle-rules.json` | 231 enable/disable rules with original C++ `enable_if` condition source |
11
11
  | `invalidation-map.json` | Option-change → re-slice step mapping (Print/PrintObject invalidation branches) |
12
+ | `printers.json` | 1,035 vendor machine profiles across 64 vendors: motion limits, bed and nozzle |
13
+ | `processes.js` | 2,243 print presets (speeds, accelerations), joined to printers by `compatible_printers` |
14
+ | `filaments.js` | 5,999 material presets over 81 filament types, plus each printer model's recommended list |
15
+
16
+ The last two are column-oriented and deduplicated, and load on demand. Read them through `processPresets()` /
17
+ `filamentPresets()` in `three-slicer/settings` rather than decoding the layout by hand.
18
+
19
+ ## Material presets
20
+
21
+ `filaments.js` carries only the keys the kernel actually reads (14 today: temperatures, flow, diameter, cooling,
22
+ and the retraction/z-hop overrides a material is allowed to apply on top of the machine's). That column set is
23
+ disjoint from `processes.js`' 59, which is what lets a material pick and a process pick be applied independently —
24
+ neither clears the other's values.
25
+
26
+ `filamentPresets()` is the facade:
27
+
28
+ ```js
29
+ import { filamentPresets } from 'three-slicer/settings'
30
+
31
+ const filaments = await filamentPresets()
32
+ filaments.listFor('Bambu Lab X1 Carbon 0.4 nozzle') // [{name, type, vendor}, …] compatible materials
33
+ filaments.recommendedFor('Bambu Lab X1 Carbon 0.4 nozzle') // the vendor's shortlist, filtered to the above
34
+ filaments.settingsFor('Bambu PLA Basic @BBL X1C') // {schemaKey: value} ready to merge
35
+ filaments.keys // clear these before applying a different material
36
+ ```
37
+
38
+ `recommendedFor()` intersects with the compatible list rather than returning the raw recommendation, and the
39
+ filtering is not cosmetic: the recommendation is declared on the machine **model** and so is nozzle-agnostic,
40
+ while its entries name nozzle-specific presets. Across all 1,035 printer profiles, 1,749 of 4,259 raw
41
+ recommendation entries (41%) name a material whose own compatible list excludes that profile.
42
+
43
+ `type` and `vendor` are empty strings when the profile chain declares neither — 265 of the 5,999 presets. That is
44
+ why the type count above is 81 and not 82: the empty bucket is "no type declared", not a type.
45
+
46
+ Filament options are per-extruder vectors upstream (one entry per extruder), so a preset — which describes a single
47
+ material — always contributes the single first entry. Building a multi-material settings map means writing each
48
+ extruder's material at its own index in those vectors; see `deriveKernelParams` in `three-slicer/settings`.
12
49
 
13
50
  ```js
14
51
  import schema from 'three-slicer/data/config-schema.json'