three-slicer 0.2.4 → 0.2.5

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.
@@ -1,179 +1,201 @@
1
- import e, { useRef as L, useState as w, useLayoutEffect as P, useEffect as J, useMemo as F } from "react";
2
- import { createPortal as B } from "react-dom";
3
- import { uiTree as _, schema as S } from "three-slicer/data";
4
- import { printerTechnology as I, settingRaw as T } from "three-slicer/settings";
5
- import { disabledKeys as $, makeCfg as U } from "three-slicer/toggle";
6
- function W({ css: t, className: s, children: n }) {
7
- const i = L(null), [c] = w(() => {
1
+ import t, { useRef as T, useState as h, useLayoutEffect as B, useEffect as I, useMemo as D } from "react";
2
+ import { createPortal as R } from "react-dom";
3
+ import { uiTree as j, schema as A } from "three-slicer/data";
4
+ import { printerTechnology as $, settingRaw as U } from "three-slicer/settings";
5
+ import { disabledKeys as W, makeCfg as q } from "three-slicer/toggle";
6
+ function G({ css: e, className: s, children: a }) {
7
+ const l = T(null), [i] = h(() => {
8
8
  const r = document.createElement("div");
9
9
  return r.style.display = "contents", r;
10
10
  });
11
- return P(() => {
12
- const r = i.current, o = r.shadowRoot || r.attachShadow({ mode: "open" });
13
- let p = o.querySelector("style[data-ts-css]");
14
- p || (p = document.createElement("style"), p.setAttribute("data-ts-css", ""), o.append(p)), p.textContent = t, o.contains(c) || o.append(c);
15
- }, [t, c]), /* @__PURE__ */ e.createElement("div", { ref: i, className: s }, B(n, c));
11
+ return B(() => {
12
+ const r = l.current, d = r.shadowRoot || r.attachShadow({ mode: "open" });
13
+ let c = d.querySelector("style[data-ts-css]");
14
+ c || (c = document.createElement("style"), c.setAttribute("data-ts-css", ""), d.append(c)), c.textContent = e, d.contains(i) || d.append(i);
15
+ }, [e, i]), /* @__PURE__ */ t.createElement("div", { ref: l, className: s }, R(a, i));
16
16
  }
17
- const q = ':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}', O = (t, s, n, i) => [[n, i], [t + n, i], [t + n, s + i], [n, s + i]], G = (t) => {
18
- const s = t / 2, n = 32;
19
- return Array.from({ length: n }, (i, c) => {
20
- const r = 2 * Math.PI * c / n;
17
+ const H = ':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}.override-dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:#f0a542;border:0;padding:0;cursor:pointer;box-shadow:0 0 0 2px #fdf1dd}.override-dot:hover{background:#d98c26}.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}', L = (e, s, a, l) => [[a, l], [e + a, l], [e + a, s + l], [a, s + l]], Q = (e) => {
18
+ const s = e / 2, a = 32;
19
+ return Array.from({ length: a }, (l, i) => {
20
+ const r = 2 * Math.PI * i / a;
21
21
  return [+(s + s * Math.cos(r)).toFixed(2), +(s + s * Math.sin(r)).toFixed(2)];
22
22
  });
23
- }, R = (t) => {
24
- const s = t.map((i) => i[0]), n = t.map((i) => i[1]);
25
- return { x0: Math.min(...s), y0: Math.min(...n), w: Math.max(...s) - Math.min(...s), d: Math.max(...n) - Math.min(...n) };
23
+ }, P = (e) => {
24
+ const s = e.map((l) => l[0]), a = e.map((l) => l[1]);
25
+ return { x0: Math.min(...s), y0: Math.min(...a), w: Math.max(...s) - Math.min(...s), d: Math.max(...a) - Math.min(...a) };
26
26
  };
27
- function H({ value: t, onChange: s, disabled: n }) {
28
- const i = Array.isArray(t) && t.length >= 3 ? t : O(200, 200, 0, 0), c = R(i), [r, o] = w(i.length === 4 ? "rect" : i.length >= 12 ? "circle" : "custom"), [p, b] = w({ w: c.w, d: c.d, ox: c.x0, oy: c.y0 }), [E, g] = w(Math.round(c.w)), [d, x] = w(JSON.stringify(i)), [y, k] = w(!1);
29
- J(() => {
30
- const a = R(i);
31
- b({ w: a.w, d: a.d, ox: a.x0, oy: a.y0 }), g(Math.round(a.w)), x(JSON.stringify(i)), k(!1);
32
- }, [JSON.stringify(t)]);
33
- const v = (a) => {
34
- b(a);
35
- const { w: u, d: m, ox: N, oy: z } = a;
36
- u > 0 && m > 0 && s(O(u, m, N, z));
37
- }, M = (a) => {
38
- g(a), a > 0 && s(G(a));
39
- }, A = (a) => {
40
- x(a);
27
+ function X({ value: e, onChange: s, disabled: a }) {
28
+ const l = Array.isArray(e) && e.length >= 3 ? e : L(200, 200, 0, 0), i = P(l), [r, d] = h(l.length === 4 ? "rect" : l.length >= 12 ? "circle" : "custom"), [c, u] = h({ w: i.w, d: i.d, ox: i.x0, oy: i.y0 }), [N, g] = h(Math.round(i.w)), [p, b] = h(JSON.stringify(l)), [w, E] = h(!1);
29
+ I(() => {
30
+ const n = P(l);
31
+ u({ w: n.w, d: n.d, ox: n.x0, oy: n.y0 }), g(Math.round(n.w)), b(JSON.stringify(l)), E(!1);
32
+ }, [JSON.stringify(e)]);
33
+ const v = (n) => {
34
+ u(n);
35
+ const { w: m, d: x, ox: _, oy: y } = n;
36
+ m > 0 && x > 0 && s(L(m, x, _, y));
37
+ }, z = (n) => {
38
+ g(n), n > 0 && s(Q(n));
39
+ }, C = (n) => {
40
+ b(n);
41
41
  try {
42
- const u = JSON.parse(a);
43
- if (!Array.isArray(u) || u.length < 3 || !u.every((m) => Array.isArray(m) && m.length === 2 && m.every(Number.isFinite))) throw 0;
44
- k(!1), s(u);
42
+ const m = JSON.parse(n);
43
+ if (!Array.isArray(m) || m.length < 3 || !m.every((x) => Array.isArray(x) && x.length === 2 && x.every(Number.isFinite))) throw 0;
44
+ E(!1), s(m);
45
45
  } catch {
46
- k(!0);
46
+ E(!0);
47
47
  }
48
- }, h = (a) => {
49
- const u = parseFloat(a);
50
- return Number.isFinite(u) ? u : 0;
48
+ }, k = (n) => {
49
+ const m = parseFloat(n);
50
+ return Number.isFinite(m) ? m : 0;
51
51
  };
52
- return /* @__PURE__ */ e.createElement("div", { className: "bse", "data-testid": "bed-shape-editor" }, /* @__PURE__ */ e.createElement("div", { className: "bse-modes" }, [["rect", "Rectangular"], ["circle", "Circular"], ["custom", "Custom"]].map(([a, u]) => /* @__PURE__ */ e.createElement("button", { key: a, type: "button", className: r === a ? "on" : "", disabled: n, onClick: () => o(a) }, u))), r === "rect" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Width ", /* @__PURE__ */ e.createElement(
52
+ return /* @__PURE__ */ t.createElement("div", { className: "bse", "data-testid": "bed-shape-editor" }, /* @__PURE__ */ t.createElement("div", { className: "bse-modes" }, [["rect", "Rectangular"], ["circle", "Circular"], ["custom", "Custom"]].map(([n, m]) => /* @__PURE__ */ t.createElement("button", { key: n, type: "button", className: r === n ? "on" : "", disabled: a, onClick: () => d(n) }, m))), r === "rect" && /* @__PURE__ */ t.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ t.createElement("label", null, "Width ", /* @__PURE__ */ t.createElement(
53
53
  "input",
54
54
  {
55
55
  type: "number",
56
56
  min: "1",
57
- value: p.w,
58
- disabled: n,
57
+ value: c.w,
58
+ disabled: a,
59
59
  "data-testid": "bed-w",
60
- onChange: (a) => v({ ...p, w: h(a.target.value) })
60
+ onChange: (n) => v({ ...c, w: k(n.target.value) })
61
61
  }
62
- ), " mm"), /* @__PURE__ */ e.createElement("label", null, "Depth ", /* @__PURE__ */ e.createElement(
62
+ ), " mm"), /* @__PURE__ */ t.createElement("label", null, "Depth ", /* @__PURE__ */ t.createElement(
63
63
  "input",
64
64
  {
65
65
  type: "number",
66
66
  min: "1",
67
- value: p.d,
68
- disabled: n,
67
+ value: c.d,
68
+ disabled: a,
69
69
  "data-testid": "bed-d",
70
- onChange: (a) => v({ ...p, d: h(a.target.value) })
70
+ onChange: (n) => v({ ...c, d: k(n.target.value) })
71
71
  }
72
- ), " mm"), /* @__PURE__ */ e.createElement("label", null, "Origin X ", /* @__PURE__ */ e.createElement(
72
+ ), " mm"), /* @__PURE__ */ t.createElement("label", null, "Origin X ", /* @__PURE__ */ t.createElement(
73
73
  "input",
74
74
  {
75
75
  type: "number",
76
- value: p.ox,
77
- disabled: n,
78
- onChange: (a) => v({ ...p, ox: h(a.target.value) })
76
+ value: c.ox,
77
+ disabled: a,
78
+ onChange: (n) => v({ ...c, ox: k(n.target.value) })
79
79
  }
80
- )), /* @__PURE__ */ e.createElement("label", null, "Origin Y ", /* @__PURE__ */ e.createElement(
80
+ )), /* @__PURE__ */ t.createElement("label", null, "Origin Y ", /* @__PURE__ */ t.createElement(
81
81
  "input",
82
82
  {
83
83
  type: "number",
84
- value: p.oy,
85
- disabled: n,
86
- onChange: (a) => v({ ...p, oy: h(a.target.value) })
84
+ value: c.oy,
85
+ disabled: a,
86
+ onChange: (n) => v({ ...c, oy: k(n.target.value) })
87
87
  }
88
- ))), r === "circle" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Diameter ", /* @__PURE__ */ e.createElement(
88
+ ))), r === "circle" && /* @__PURE__ */ t.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ t.createElement("label", null, "Diameter ", /* @__PURE__ */ t.createElement(
89
89
  "input",
90
90
  {
91
91
  type: "number",
92
92
  min: "1",
93
- value: E,
94
- disabled: n,
93
+ value: N,
94
+ disabled: a,
95
95
  "data-testid": "bed-dia",
96
- onChange: (a) => M(h(a.target.value))
96
+ onChange: (n) => z(k(n.target.value))
97
97
  }
98
- ), " mm")), r === "custom" && /* @__PURE__ */ e.createElement(
98
+ ), " mm")), r === "custom" && /* @__PURE__ */ t.createElement(
99
99
  "textarea",
100
100
  {
101
- className: "bse-json" + (y ? " err" : ""),
101
+ className: "bse-json" + (w ? " err" : ""),
102
102
  rows: 3,
103
- value: d,
104
- disabled: n,
103
+ value: p,
104
+ disabled: a,
105
105
  "data-testid": "bed-json",
106
- onChange: (a) => A(a.target.value),
106
+ onChange: (n) => C(n.target.value),
107
107
  placeholder: "[[0,0],[200,0],[200,200],[0,200]]"
108
108
  }
109
109
  ));
110
110
  }
111
- const Q = {
112
- printable_area: H
113
- }, D = Object.keys(_).filter((t) => _[t].length > 0), X = ["all", "simple", "advanced", "expert", "develop"], Y = (t) => t.replace("::build", "").replace("Tab", "").replace("_sla", " (SLA)"), V = (t) => /SLA|build_sla\b/.test(t) ? "SLA" : "FFF";
114
- function Z(t, s) {
115
- var c;
116
- if (!t || !(s in t)) return !1;
117
- const n = (c = S[s]) == null ? void 0 : c.default, i = Array.isArray(n) ? n[0] : n;
118
- return String(t[s]) !== String(i ?? "");
119
- }
120
- function K(t, s) {
111
+ const Y = {
112
+ printable_area: X
113
+ }, J = Object.keys(j).filter((e) => j[e].length > 0), V = ["all", "simple", "advanced", "expert", "develop"], Z = (e) => e.replace("::build", "").replace("Tab", "").replace("_sla", " (SLA)"), K = (e) => /SLA|build_sla\b/.test(e) ? "SLA" : "FFF";
114
+ function ee(e, s) {
121
115
  var i;
122
- if (s === "all" || t.startsWith("<")) return !0;
123
- const n = (i = S[t]) == null ? void 0 : i.mode;
124
- return s === "simple" ? n === "simple" : s === "advanced" ? n === "simple" || n === "advanced" : !0;
116
+ if (!e || !(s in e)) return !1;
117
+ const a = (i = A[s]) == null ? void 0 : i.default, l = Array.isArray(a) ? a[0] : a;
118
+ return String(e[s]) !== String(l ?? "");
119
+ }
120
+ function te(e, s) {
121
+ var l;
122
+ if (s === "all" || e.startsWith("<")) return !0;
123
+ const a = (l = A[e]) == null ? void 0 : l.mode;
124
+ return s === "simple" ? a === "simple" : s === "advanced" ? a === "simple" || a === "advanced" : !0;
125
125
  }
126
- function ee(t) {
127
- return t ? t.gui_type === "color" ? "color" : t.enum_values ? "select" : t.type === "coBool" || t.type === "coBools" ? "checkbox" : t.multiline || t.is_code ? "textarea" : t.type === "coPoints" || t.type === "coPoint" ? "points" : "input" : "unknown";
126
+ function ae(e) {
127
+ return e ? e.gui_type === "color" ? "color" : e.enum_values ? "select" : e.type === "coBool" || e.type === "coBools" ? "checkbox" : e.multiline || e.is_code ? "textarea" : e.type === "coPoints" || e.type === "coPoint" ? "points" : "input" : "unknown";
128
128
  }
129
- function te({ mode: t }) {
130
- return /* @__PURE__ */ e.createElement("span", { className: `badge mode-${t ?? "none"}` }, t ?? "—");
129
+ function re({ mode: e }) {
130
+ return /* @__PURE__ */ t.createElement("span", { className: `badge mode-${e ?? "none"}` }, e ?? "—");
131
131
  }
132
- function ae({ def: t, optKey: s, settings: n, setSettings: i, disabled: c, customWidgets: r }) {
133
- const o = T(n, s), p = (d) => i((x) => ({ ...x, [s]: d })), b = (r == null ? void 0 : r[s]) ?? Q[s];
134
- if (b) return /* @__PURE__ */ e.createElement(b, { def: t, value: o, onChange: p, disabled: c });
135
- const E = ee(t), g = Array.isArray(o) ? o[0] : o;
136
- switch (E) {
132
+ function ne({ def: e, optKey: s, settings: a, setSettings: l, disabled: i, customWidgets: r }) {
133
+ const d = U(a, s), c = (p) => l((b) => ({ ...b, [s]: p })), u = (r == null ? void 0 : r[s]) ?? Y[s];
134
+ if (u) return /* @__PURE__ */ t.createElement(u, { def: e, value: d, onChange: c, disabled: i });
135
+ const N = ae(e), g = Array.isArray(d) ? d[0] : d;
136
+ switch (N) {
137
137
  case "select":
138
- return /* @__PURE__ */ e.createElement("select", { value: g ?? "", onChange: (d) => p(d.target.value), disabled: c }, t.enum_values.map((d, x) => {
139
- var y;
140
- return /* @__PURE__ */ e.createElement("option", { key: d, value: d }, ((y = t.enum_labels) == null ? void 0 : y[x]) ?? d);
138
+ return /* @__PURE__ */ t.createElement("select", { value: g ?? "", onChange: (p) => c(p.target.value), disabled: i }, e.enum_values.map((p, b) => {
139
+ var w;
140
+ return /* @__PURE__ */ t.createElement("option", { key: p, value: p }, ((w = e.enum_labels) == null ? void 0 : w[b]) ?? p);
141
141
  }));
142
142
  case "checkbox":
143
- return /* @__PURE__ */ e.createElement("input", { type: "checkbox", checked: Array.isArray(o) ? !!o[0] : !!o, onChange: (d) => p(d.target.checked), disabled: c });
143
+ return /* @__PURE__ */ t.createElement("input", { type: "checkbox", checked: Array.isArray(d) ? !!d[0] : !!d, onChange: (p) => c(p.target.checked), disabled: i });
144
144
  case "color":
145
- return /* @__PURE__ */ e.createElement("input", { type: "color", value: typeof g == "string" && g.startsWith("#") ? g : "#00ae42", onChange: (d) => p(d.target.value), disabled: c });
145
+ return /* @__PURE__ */ t.createElement("input", { type: "color", value: typeof g == "string" && g.startsWith("#") ? g : "#00ae42", onChange: (p) => c(p.target.value), disabled: i });
146
146
  case "textarea":
147
- return /* @__PURE__ */ e.createElement("textarea", { rows: 2, value: Array.isArray(o) ? o.join(`
148
- `) : o ?? "", onChange: (d) => p(d.target.value), disabled: c });
147
+ return /* @__PURE__ */ t.createElement("textarea", { rows: 2, value: Array.isArray(d) ? d.join(`
148
+ `) : d ?? "", onChange: (p) => c(p.target.value), disabled: i });
149
149
  case "points":
150
- return /* @__PURE__ */ e.createElement("code", { className: "pts" }, JSON.stringify(o ?? []));
150
+ return /* @__PURE__ */ t.createElement("code", { className: "pts" }, JSON.stringify(d ?? []));
151
151
  case "unknown":
152
- return /* @__PURE__ */ e.createElement("span", { className: "muted" }, "No schema");
152
+ return /* @__PURE__ */ t.createElement("span", { className: "muted" }, "No schema");
153
153
  default:
154
- return /* @__PURE__ */ e.createElement("span", { className: "inputwrap" }, /* @__PURE__ */ e.createElement("input", { value: g ?? "", onChange: (d) => p(d.target.value), disabled: c }), t.sidetext && /* @__PURE__ */ e.createElement("span", { className: "unit" }, t.sidetext));
154
+ return /* @__PURE__ */ t.createElement("span", { className: "inputwrap" }, /* @__PURE__ */ t.createElement("input", { value: g ?? "", onChange: (p) => c(p.target.value), disabled: i }), e.sidetext && /* @__PURE__ */ t.createElement("span", { className: "unit" }, e.sidetext));
155
155
  }
156
156
  }
157
- function ne({ optKey: t, settings: s, setSettings: n, disabled: i, onOptionOpen: c, customWidgets: r }) {
158
- if (t.startsWith("<")) return /* @__PURE__ */ e.createElement("div", { className: "row custom" }, /* @__PURE__ */ e.createElement("span", { className: "muted" }, "⚙ Custom widget ", t));
159
- const o = S[t], p = i ? i[t] : void 0, b = p != null, E = Z(s, t), g = () => n((x) => {
160
- const y = { ...x };
161
- return delete y[t], y;
162
- }), d = (o == null ? void 0 : o.label) || (o == null ? void 0 : o.full_label) || t;
163
- return /* @__PURE__ */ e.createElement("div", { className: "row" + (b ? " disabled" : ""), title: b ? `Disabled when: ${p}` : (o == null ? void 0 : o.tooltip) ?? "", "data-testid": `row-${t}` }, /* @__PURE__ */ e.createElement("span", { className: "lbl-cell" }, E && /* @__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(te, { mode: o == null ? void 0 : o.mode }), /* @__PURE__ */ e.createElement("span", { className: "widget-cell" }, /* @__PURE__ */ e.createElement(ae, { def: o, optKey: t, settings: s, setSettings: n, disabled: b, customWidgets: r }), E && /* @__PURE__ */ e.createElement("button", { className: "reset-btn", onClick: g, title: "Reset to default", "data-testid": `reset-${t}` }, "↺")));
157
+ function se({ optKey: e, settings: s, setSettings: a, disabled: l, onOptionOpen: i, customWidgets: r, overridden: d, onRevertKey: c }) {
158
+ if (e.startsWith("<")) return /* @__PURE__ */ t.createElement("div", { className: "row custom" }, /* @__PURE__ */ t.createElement("span", { className: "muted" }, "⚙ Custom widget ", e));
159
+ const u = A[e], N = l ? l[e] : void 0, g = N != null, p = ee(s, e), b = () => a((E) => {
160
+ const v = { ...E };
161
+ return delete v[e], v;
162
+ }), w = (u == null ? void 0 : u.label) || (u == null ? void 0 : u.full_label) || e;
163
+ return /* @__PURE__ */ t.createElement("div", { className: "row" + (g ? " disabled" : ""), title: g ? `Disabled when: ${N}` : (u == null ? void 0 : u.tooltip) ?? "", "data-testid": `row-${e}` }, /* @__PURE__ */ t.createElement("span", { className: "lbl-cell" }, d && /* @__PURE__ */ t.createElement(
164
+ "button",
165
+ {
166
+ type: "button",
167
+ className: "override-dot",
168
+ onClick: () => c == null ? void 0 : c(e),
169
+ title: "This plate's override — click to follow the global value again",
170
+ "data-testid": `override-${e}`
171
+ }
172
+ ), p && /* @__PURE__ */ t.createElement("span", { className: "dirty-dot", title: "Changed from default", "data-testid": `dirty-${e}` }), i ? /* @__PURE__ */ t.createElement("button", { type: "button", className: "lbl lbl-link", onClick: () => i(e), title: "Details" }, w) : /* @__PURE__ */ t.createElement("span", { className: "lbl" }, w)), /* @__PURE__ */ t.createElement("code", { className: "key" }, e), /* @__PURE__ */ t.createElement(re, { mode: u == null ? void 0 : u.mode }), /* @__PURE__ */ t.createElement("span", { className: "widget-cell" }, /* @__PURE__ */ t.createElement(ne, { def: u, optKey: e, settings: s, setSettings: a, disabled: g, customWidgets: r }), p && /* @__PURE__ */ t.createElement("button", { className: "reset-btn", onClick: b, title: "Reset to default", "data-testid": `reset-${e}` }, "↺")));
164
173
  }
165
- function ce({ settings: t, setSettings: s, onOptionOpen: n, embedded: i = !1, customWidgets: c, only: r }) {
166
- const [o, p] = w((r == null ? void 0 : r.builder) ?? D[0] ?? ""), [b, E] = w(0), [g, d] = w("all"), [x, y] = w(""), k = I(t), v = D.filter((l) => V(l) === k), M = (r == null ? void 0 : r.builder) ?? (v.includes(o) ? o : v[0] ?? ""), A = _[(r == null ? void 0 : r.builder) ?? M] ?? [], h = r != null && r.page ? A.filter((l) => l.page === r.page) : A, a = h[Math.min(b, h.length - 1)], u = F(() => $(U(t)), [t]), m = x.trim().toLowerCase(), N = F(() => m ? Object.entries(S).filter(([l, f]) => {
167
- var C, j;
168
- return l.includes(m) || ((C = f.label) == null ? void 0 : C.toLowerCase().includes(m)) || ((j = f.full_label) == null ? void 0 : j.toLowerCase().includes(m));
169
- }).slice(0, 60).map(([l]) => l) : [], [m]), z = (l, f) => /* @__PURE__ */ e.createElement(ne, { key: l + f, optKey: l, settings: t, setSettings: s, disabled: u, onOptionOpen: n, customWidgets: c });
170
- return /* @__PURE__ */ e.createElement(W, { css: q, className: i ? "sp-embedded" : void 0 }, /* @__PURE__ */ e.createElement("div", { className: "settings-panel" }, !r && /* @__PURE__ */ e.createElement("div", { className: "sp-top" }, /* @__PURE__ */ e.createElement("input", { className: "sp-search", placeholder: `Search options (${Object.keys(S).length})…`, value: x, onChange: (l) => y(l.target.value), "data-testid": "opt-search" }), !m && /* @__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: M, onChange: (l) => {
171
- p(l.target.value), E(0);
172
- } }, v.map((l) => /* @__PURE__ */ e.createElement("option", { key: l, value: l }, Y(l))))), /* @__PURE__ */ e.createElement("div", { className: "sp-pages" }, h.map((l, f) => /* @__PURE__ */ e.createElement("button", { key: l.page + f, className: f === Math.min(b, h.length - 1) ? "on" : "", onClick: () => E(f) }, l.page))), /* @__PURE__ */ e.createElement("div", { className: "sp-modes" }, X.map((l) => /* @__PURE__ */ e.createElement("button", { key: l, className: g === l ? "on" : "", onClick: () => d(l) }, l))))), /* @__PURE__ */ e.createElement("div", { className: "sp-body" }, m ? /* @__PURE__ */ e.createElement("section", { "data-testid": "search-results" }, /* @__PURE__ */ e.createElement("h3", null, "Search: “", x, "” — ", N.length, " hit", N.length === 1 ? "" : "s", N.length === 60 ? "+" : ""), N.map(z), !N.length && /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No matches")) : a ? /* @__PURE__ */ e.createElement(e.Fragment, null, !r && /* @__PURE__ */ e.createElement("h2", null, a.page, " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, "Tab.cpp:", a.line)), a.groups.map((l) => {
173
- const f = l.options.filter((C) => K(C, g) && !(r && C.startsWith("<")));
174
- return f.length ? /* @__PURE__ */ e.createElement("section", { key: l.group + l.line }, /* @__PURE__ */ e.createElement("h3", null, l.group || "(untitled group)", " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, ":", l.line)), f.map(z)) : null;
175
- })) : /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No builder"))));
174
+ function pe({ settings: e, setSettings: s, onOptionOpen: a, embedded: l = !1, customWidgets: i, only: r, overriddenKeys: d, onRevertKey: c }) {
175
+ const [u, N] = h((r == null ? void 0 : r.builder) ?? J[0] ?? ""), [g, p] = h(0), [b, w] = h("all"), [E, v] = h(""), z = $(e), C = J.filter((o) => K(o) === z), k = (r == null ? void 0 : r.builder) ?? (C.includes(u) ? u : C[0] ?? ""), n = j[(r == null ? void 0 : r.builder) ?? k] ?? [], m = r != null && r.page ? n.filter((o) => o.page === r.page) : n, x = m[Math.min(g, m.length - 1)], _ = D(() => W(q(e)), [e]), y = E.trim().toLowerCase(), S = D(() => y ? Object.entries(A).filter(([o, f]) => {
176
+ var M, O;
177
+ return o.includes(y) || ((M = f.label) == null ? void 0 : M.toLowerCase().includes(y)) || ((O = f.full_label) == null ? void 0 : O.toLowerCase().includes(y));
178
+ }).slice(0, 60).map(([o]) => o) : [], [y]), F = (o, f) => /* @__PURE__ */ t.createElement(
179
+ se,
180
+ {
181
+ key: o + f,
182
+ optKey: o,
183
+ settings: e,
184
+ setSettings: s,
185
+ disabled: _,
186
+ onOptionOpen: a,
187
+ customWidgets: i,
188
+ overridden: d == null ? void 0 : d.includes(o),
189
+ onRevertKey: c
190
+ }
191
+ );
192
+ return /* @__PURE__ */ t.createElement(G, { css: H, className: l ? "sp-embedded" : void 0 }, /* @__PURE__ */ t.createElement("div", { className: "settings-panel" }, !r && /* @__PURE__ */ t.createElement("div", { className: "sp-top" }, /* @__PURE__ */ t.createElement("input", { className: "sp-search", placeholder: `Search options (${Object.keys(A).length})…`, value: E, onChange: (o) => v(o.target.value), "data-testid": "opt-search" }), !y && /* @__PURE__ */ t.createElement(t.Fragment, null, /* @__PURE__ */ t.createElement("label", { className: "sp-builder" }, /* @__PURE__ */ t.createElement("span", null, "Settings group"), /* @__PURE__ */ t.createElement("select", { value: k, onChange: (o) => {
193
+ N(o.target.value), p(0);
194
+ } }, C.map((o) => /* @__PURE__ */ t.createElement("option", { key: o, value: o }, Z(o))))), /* @__PURE__ */ t.createElement("div", { className: "sp-pages" }, m.map((o, f) => /* @__PURE__ */ t.createElement("button", { key: o.page + f, className: f === Math.min(g, m.length - 1) ? "on" : "", onClick: () => p(f) }, o.page))), /* @__PURE__ */ t.createElement("div", { className: "sp-modes" }, V.map((o) => /* @__PURE__ */ t.createElement("button", { key: o, className: b === o ? "on" : "", onClick: () => w(o) }, o))))), /* @__PURE__ */ t.createElement("div", { className: "sp-body" }, y ? /* @__PURE__ */ t.createElement("section", { "data-testid": "search-results" }, /* @__PURE__ */ t.createElement("h3", null, "Search: “", E, "” — ", S.length, " hit", S.length === 1 ? "" : "s", S.length === 60 ? "+" : ""), S.map(F), !S.length && /* @__PURE__ */ t.createElement("p", { className: "muted" }, "No matches")) : x ? /* @__PURE__ */ t.createElement(t.Fragment, null, !r && /* @__PURE__ */ t.createElement("h2", null, x.page, " ", /* @__PURE__ */ t.createElement("span", { className: "muted" }, "Tab.cpp:", x.line)), x.groups.map((o) => {
195
+ const f = o.options.filter((M) => te(M, b) && !(r && M.startsWith("<")));
196
+ return f.length ? /* @__PURE__ */ t.createElement("section", { key: o.group + o.line }, /* @__PURE__ */ t.createElement("h3", null, o.group || "(untitled group)", " ", /* @__PURE__ */ t.createElement("span", { className: "muted" }, ":", o.line)), f.map(F)) : null;
197
+ })) : /* @__PURE__ */ t.createElement("p", { className: "muted" }, "No builder"))));
176
198
  }
177
199
  export {
178
- ce as default
200
+ pe as default
179
201
  };
@@ -29,6 +29,10 @@
29
29
  .lbl-cell { display: inline-flex; align-items: center; gap: 5px; min-width: 0; }
30
30
  .lbl-cell .lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
31
31
  .dirty-dot { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%; background: #e8873a; box-shadow: 0 0 0 2px #fff3e6; }
32
+ /* Per-plate override marker: this value is the PLATE's, not the global one; clicking follows global again. */
33
+ .override-dot { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%; background: #f0a542; border: 0; padding: 0;
34
+ cursor: pointer; box-shadow: 0 0 0 2px #fdf1dd; }
35
+ .override-dot:hover { background: #d98c26; }
32
36
  .widget-cell { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
33
37
  .reset-btn { flex: 0 0 auto; border: 1px solid #d8b48a; background: #fbf1e4; color: #b5701f;
34
38
  border-radius: 5px; cursor: pointer; font-size: 13px; line-height: 1; padding: 1px 5px; }
@@ -87,8 +87,9 @@ export function createSlicerClient(worker = new Worker(new URL('./slicer.worker.
87
87
  return {
88
88
  worker,
89
89
 
90
- /** Load the kernel ahead of the first slice, so the first one is not also a 4MB download. */
91
- warmup: () => send({ cmd: 'warmup' }).then(() => undefined),
90
+ /** Load the kernel ahead of the first slice, so the first one is not also a 4MB download. Says which variant
91
+ * loaded — mt (threads) or st — because the two are a measured 9.8x apart and a host may want to say so. */
92
+ warmup: () => send({ cmd: 'warmup' }).then(reply => ({ kernel: reply?.kernel ?? null })),
92
93
 
93
94
  /**
94
95
  * Slice. `params` may be an object — it is stringified here, which the raw protocol does not do for you.
@@ -28,17 +28,22 @@ const paramsText = (params) => (typeof params === 'string' ? params : JSON.strin
28
28
  let quiet = false
29
29
  const say = (level, ...args) => { if (!quiet) console[level](...args) }
30
30
 
31
+ // Which variant loaded is also REPORTED (the 'warm' reply carries `kernel`), not just logged: mt vs st is a
32
+ // measured 9.8x on a 3M-facet model, and the viewer sizes its plate-parallel worker pool by it.
33
+ let kernelKind = null
31
34
  const loadCore = async () => {
32
35
  const isolated = typeof crossOriginIsolated !== 'undefined' && crossOriginIsolated
33
36
  if (isolated) {
34
37
  try {
35
38
  const M = await (await import('./slicer_core.mt.js')).default()
36
39
  say('info', '[slicer.worker] core: mt (threads)')
40
+ kernelKind = 'mt'
37
41
  return M
38
42
  } catch (e) { say('warn', '[slicer.worker] mt load failed — falling back to st:', e) }
39
43
  }
40
44
  const M = await (await import('./slicer_core.js')).default()
41
45
  say('info', '[slicer.worker] core: st')
46
+ kernelKind = 'st'
42
47
  return M
43
48
  }
44
49
 
@@ -213,7 +218,7 @@ self.onmessage = async (e) => {
213
218
  if (!modPromise) modPromise = loadCore()
214
219
  const Module = await modPromise
215
220
  // Warmup: only load the kernel (+ spawn the mt pthread pool) ahead of time — removes the perceived load on the first slice
216
- if (d.cmd === 'warmup') { self.postMessage({ type: 'warm' }); return }
221
+ if (d.cmd === 'warmup') { self.postMessage({ type: 'warm', kernel: kernelKind }); return }
217
222
  // Stage 20: manual support painting — selector state persists in this worker Module (slicing uses the same Module).
218
223
  // `keepPaint` says the mesh is the same model in a new place, so the marks carry over — the reply reports whether
219
224
  // they actually did, since a face-count change makes the kernel fall back to a clean registration.
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-slicer",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Browser/WASM 3D-printing slicer reverse-engineered from OrcaSlicer: slicing engine (Arachne walls, tree/grid supports, multi-material), SLA resin slicing with PrusaSlicer's ported support/pad chain and .sl1 export, React viewer with GPU toolpath preview, and schema-driven settings UI.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
package/types/client.d.ts CHANGED
@@ -35,8 +35,8 @@ export interface SlicerClient {
35
35
  /** The worker being driven — for a host that also wants to listen in. */
36
36
  worker: Worker
37
37
 
38
- /** Load the kernel ahead of the first slice. */
39
- warmup(): Promise<void>
38
+ /** Load the kernel ahead of the first slice. Resolves with which variant loaded ('mt' threads / 'st'). */
39
+ warmup(): Promise<{ kernel: 'mt' | 'st' | null }>
40
40
 
41
41
  /** `params` may be an object; it is stringified for you, which the raw protocol does not do. */
42
42
  slice(stl: ArrayBuffer | Uint8Array, params: object | string, cb?: ClientSliceCallbacks): Promise<SliceResult>
package/types/viewer.d.ts CHANGED
@@ -3,12 +3,42 @@ import type * as React from 'react'
3
3
  import type { SlicerSettings } from './settings-keys.d.ts'
4
4
 
5
5
  export interface ViewportProps {
6
- /** Sparse settings map. Defaults to `{}`. */
6
+ /**
7
+ * Sparse settings map. Defaults to `{}`. Two viewer knobs ride in it beside the schema keys: `sla_antialias`
8
+ * (SL1 mask MSAA, 1|2|4) and `slice_workers` — how many plates an all-plates run slices at once (0/absent =
9
+ * Auto: half the cores on the threaded kernel, one per core on the single-threaded one; the in-app select
10
+ * writes it). Neither reaches a 3mf.
11
+ */
7
12
  settings?: SlicerSettings
8
13
  /** React setState shape. Defaults to a no-op. */
9
14
  setSettings?: React.Dispatch<React.SetStateAction<SlicerSettings>>
10
- /** Left process-panel slot — usually `<SettingsPanel embedded/>`. Defaults to null. */
15
+ /**
16
+ * Per-plate overrides: `{[plateIndex]: sparse map}`, merged over `settings` for that plate's slice only —
17
+ * a key's absence means "follow the global value" (delete the key to clear an override). Plate identity is
18
+ * the plate index; deleting the last plate truncates its entry. Editing one plate's override invalidates only
19
+ * that plate's cached result. Any key can be overridden: `printer_technology` routes that plate to the
20
+ * FFF or SLA slicer (an SLA plate's grid cell is sized to its resin display), and a bed override
21
+ * (`printable_area`/`printable_height`) resizes that plate's own grid cell. What a 3mf cannot represent —
22
+ * a mixed-technology or mixed-bed project — is refused at project export with a typed error
23
+ * (`error.code` `'UNSUPPORTED_MIXED_TECH_3MF'` / `'UNSUPPORTED_MIXED_BED_3MF'`); per-plate G-code/.sl1
24
+ * exports always work. Defaults to `{}`.
25
+ */
26
+ plateSettings?: Record<number, SlicerSettings>
27
+ /** React setState shape for {@link ViewportProps.plateSettings}. Defaults to a no-op. */
28
+ setPlateSettings?: React.Dispatch<React.SetStateAction<Record<number, SlicerSettings>>>
29
+ /**
30
+ * Left process-panel slot — usually `<SettingsPanel embedded/>`. Defaults to null.
31
+ *
32
+ * Pass a FUNCTION to opt into the per-plate scope toggle (shown with more than one plate): in plate scope
33
+ * the panel receives the selected plate's EFFECTIVE map and a setter that writes only the changed keys into
34
+ * {@link ViewportProps.plateSettings}; `meta.overriddenKeys`/`meta.onRevertKey` drive the override badges
35
+ * (`<SettingsPanel overriddenKeys={...} onRevertKey={...}/>`). In global scope the pair is simply
36
+ * `settings`/`setSettings`. A plain node keeps the global-only binding.
37
+ */
11
38
  processPanel?: React.ReactNode
39
+ | ((settings: SlicerSettings, setSettings: React.Dispatch<React.SetStateAction<SlicerSettings>>,
40
+ meta: { scope: 'global' | 'plate', selectedPlate: number, overriddenKeys: string[],
41
+ onRevertKey: ((key: string) => void) | null }) => React.ReactNode)
12
42
  /** Motion-limits editor, folded into the printer card — usually `<SettingsPanel only={{builder:'TabPrinter::build_kinematics_page'}}/>`. */
13
43
  motionPanel?: React.ReactNode
14
44
  /**
@@ -99,6 +129,15 @@ export interface ViewportProps {
99
129
  plate: number; stats: Record<string, unknown>; gcode: string
100
130
  throughput?: { ms: number; kernelMs: number | null; layersPerSecond: number; msPerMsegment: number | null }
101
131
  }) => void
132
+ /**
133
+ * An all-plates run as it moves: the run map on every change, `null` when none is on. `workers.pool` is how
134
+ * many slice at once, `workers.kernel` which kernel loaded ('mt' threads / 'st'); each plate carries its state
135
+ * and its own progress (0..1) and layers/second. Fired several times a second while a run is on.
136
+ */
137
+ onSliceRun?: (run: {
138
+ workers: { pool: number; active: number; kernel: 'mt' | 'st' | null }
139
+ plates: Record<number, { state: 'queued' | 'busy' | 'done' | 'failed'; progress: number; rate: number }>
140
+ } | null) => void
102
141
  }
103
142
 
104
143
  /**
package/types/worker.d.ts CHANGED
@@ -98,7 +98,8 @@ export type SlicerRequest =
98
98
  | { stl: ArrayBuffer; params: string | object }
99
99
 
100
100
  export type SlicerResponse =
101
- | { type: 'warm' }
101
+ /** `kernel` says which variant loaded: 'mt' under crossOriginIsolated, 'st' otherwise (or after an mt load failure). */
102
+ | { type: 'warm'; kernel: 'mt' | 'st' | null }
102
103
  | { type: 'prepared'; facets: number; kept: boolean }
103
104
  /**
104
105
  * `enf`/`blk` are the counts for states 1 and 2 and are always present, so a listener predating the
package/viewer/README.md CHANGED
@@ -130,6 +130,8 @@ Every panel can be switched off, and the values the component owns can be seeded
130
130
  - **`onSliced`** — `{plate, stats, gcode, throughput}` when a slice is cached. Switching plate tabs does not
131
131
  re-fire it. `throughput` sits beside `stats` rather than inside it, because that is where the engine puts it:
132
132
  it is measured around the call, not reported by the kernel.
133
+ - **`onSliceRun`** — an all-plates run as it moves: `{workers: {pool, active, kernel}, plates: {[i]: {state,
134
+ progress, rate}}}` on every change, `null` between runs. For a host drawing its own per-plate progress.
133
135
 
134
136
  `sliceRate` is live throughput — layers finished per second over a 250ms window, `0` between slices, and shown in
135
137
  the slice bar as well. When it appears depends on the technology, because the two publish per-layer news at
@@ -341,7 +343,53 @@ Cross-Origin-Opener-Policy: same-origin
341
343
  Cross-Origin-Embedder-Policy: require-corp
342
344
  ```
343
345
 
344
- No headers → falls back to the single-threaded kernel. Nothing else to change.
346
+ No headers → falls back to the single-threaded kernel. Nothing else to change. Which one loaded is shown as a
347
+ badge (`mt`/`st`) beside the Workers select in the slice menu, and the `warmup()` client call resolves with it —
348
+ the two are a measured 9.8x apart on a 3M-facet model, which is too much to leave to the console.
349
+
350
+ ### Slicing several plates at once
351
+
352
+ **All plates** in the slice menu drains the plates through a pool of workers instead of one after another. The
353
+ **Workers** select beside it sets the pool: *Auto* is half the cores on the threaded kernel and one per core on the
354
+ single-threaded one, capped at the plate count and by model size (below); the select offers every value up to
355
+ that memory cap and nothing past it, because past it the tab itself dies (measured: five workers on a 143MB
356
+ model), which nothing in the page can catch. The plate tabs show each plate's state (queued, slicing with its own percentage, done, failed)
357
+ independently of which one is selected, and the slice bar lists them on one row. Cancel stops every worker. A worker that dies mid-run (memory, most
358
+ often) is dropped and its plate is re-run alone once the others finish, so a count that was too high for the
359
+ model costs time rather than results; the notice says which plates that happened to.
360
+
361
+ The numbers behind Auto (node harness, 3M-facet model, 15 cores, 30 plates, wall time against one worker):
362
+
363
+ | kernel | preset | 3 workers | 5 | 10 | 15 |
364
+ |---|---|---|---|---|---|
365
+ | mt | classic walls, no support | 1.10x | 1.15x | 1.15x | 1.14x |
366
+ | mt | arachne + tree support | 2.08x | 2.60x | 3.16x | 3.31x |
367
+ | st | classic walls, no support | — | — | — | 7.2x (14 workers) |
368
+
369
+ The gain is the kernel's serial share: with classic walls one worker already keeps ~77% of the cores busy, with
370
+ arachne (whose emission pass runs serial) and tree support far less, and the extra workers fill that. More workers
371
+ than cores gained nothing (17 = 15), and no count ever made a run slower — what a worker costs is memory: each
372
+ holds its own copy of the model and its own wasm heap, measured 2.5GB for one, 8.8GB for five and 11.6GB for
373
+ fifteen on that model. That is why Auto stops at half the cores and why the select goes higher only by hand.
374
+ Three things were measured NOT to matter and are deliberately absent: giving each worker a thread budget, slicing
375
+ the largest plate first, and whether the count divides the plate count. The value is `slice_workers` in the
376
+ settings map (0 = Auto), a viewer knob like `sla_antialias`, and it does not reach a 3mf.
377
+
378
+ The browser is tighter than that harness, because every worker's wasm heap lives in the one renderer process that
379
+ already holds each plate's STL buffer and three.js geometry. Measured in Chromium on the same 143MB-STL model
380
+ over nine plates (classic preset, 15 cores):
381
+
382
+ | workers | wall | Chromium RSS peak |
383
+ |---|---|---|
384
+ | 1 | 17.8s | 10.4GB |
385
+ | 2 | 15.7s (1.13x) | 13.7GB |
386
+ | 3 | crashed the tab once; 18.7s once | 10.2GB |
387
+ | 8 (Auto by cores) | crashed the tab | 11.6GB |
388
+
389
+ So Auto is also capped by model size — a worker's heap is taken as 12x the STL it slices (node measured ~1.7GB
390
+ for 143MB) against a 4GB pool budget, which gives 2 for that model and does not bind below ~40MB per plate. A
391
+ manual count is capped the same way. What the browser has NOT measured yet is
392
+ the arachne + tree-support preset, where the harness gains were largest.
345
393
 
346
394
  ## Bundler notes
347
395