three-slicer 0.2.3 → 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; }
@@ -21,6 +21,7 @@ export { SLA_CAPABILITIES, SLA_JOB_VERSION, SlaRequestError } from './sla_reques
21
21
  const REPLY_OF = {
22
22
  warmup: 'warm', prepare: 'prepared', paint: 'painted', erase: 'painted', clear: 'painted',
23
23
  importPaint: 'painted', exportPaint: 'paintExport', overlay: 'overlay', slice: 'done', sla: 'done',
24
+ fillPreview: 'fillPreview', paintMode: 'paintMode',
24
25
  }
25
26
 
26
27
  const asBuffer = (data) => (data instanceof ArrayBuffer ? data : data?.buffer ?? data)
@@ -86,8 +87,9 @@ export function createSlicerClient(worker = new Worker(new URL('./slicer.worker.
86
87
  return {
87
88
  worker,
88
89
 
89
- /** Load the kernel ahead of the first slice, so the first one is not also a 4MB download. */
90
- 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 })),
91
93
 
92
94
  /**
93
95
  * Slice. `params` may be an object — it is stringified here, which the raw protocol does not do for you.
@@ -143,6 +145,14 @@ export function createSlicerClient(worker = new Worker(new URL('./slicer.worker.
143
145
  overlay: (states) => send({ cmd: 'overlay', ...(states ? { states } : {}) })
144
146
  .then(reply => ({ enforcer: reply.enf, blocker: reply.blk, overlays: reply.overlays })),
145
147
 
148
+ /** What a fill would select under `{tool, facet, hx, hy, hz, angle}`, without applying it. `{clear:true}`
149
+ * drops the standing selection. `supported` is false on a kernel built before the preview existed. */
150
+ fillPreview: (args) => send({ cmd: 'fillPreview', ...args })
151
+ .then(reply => ({ supported: reply.supported, triangles: reply.tris })),
152
+ /** Brush-wide options: `{overhangDeg}` restricts every stroke to overhangs (0 = off), `{clipPlane:[nx,ny,nz,d]}`
153
+ * sets the section plane every cursor clips against (null turns it off). */
154
+ paintMode: (args) => send({ cmd: 'paintMode', ...args }).then(() => undefined),
155
+
146
156
  /** Load a 3mf's painting. Replaces every mark, so run it directly after `prepare`. */
147
157
  importPaint: (facets, hex, states) => brush('importPaint', { facets, hex, ...(states ? { states } : {}) }),
148
158
  exportPaint: () => send({ cmd: 'exportPaint' })
@@ -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
 
@@ -95,6 +100,16 @@ const cursorShapeOf = (message) => (message.cursor === 'circle' ? 1 : 0)
95
100
  // a radius or a camera: what they select comes from the mesh's own topology, which is why they are a branch rather
96
101
  // than more optional fields on the brush call. `state === null` means erase (the entry points with no state
97
102
  // argument), matching how the brush splits paint from erase.
103
+ // The previous sample of the current stroke, when the message carries one. Upstream paints the CAPSULE between two
104
+ // consecutive mouse positions (DoublePointCursor, GLGizmoPainterBase.cpp:878) — a pointer stream is sampled, not
105
+ // continuous, so a ball per sample leaves a fast drag as a row of blobs with gaps between them. Optional on the
106
+ // wire and feature-detected on the kernel, so a caller or a build without it keeps painting single points.
107
+ const strokeFrom = (message) =>
108
+ Number.isFinite(message.px) && Number.isFinite(message.py) && Number.isFinite(message.pz)
109
+
110
+ // The fill modes the kernel's fill_preview takes (selector_bridge.h FILL_*), by the tool name the protocol uses.
111
+ const FILL_PREVIEW_MODE = { smart: 0, bucket: 1, triangle: 2 }
112
+
98
113
  const FILL_TOOLS = new Set(['smart', 'bucket', 'triangle'])
99
114
  const applyFill = (Module, message, state) => {
100
115
  const angle = Number.isFinite(message.angle) ? message.angle : 30
@@ -203,7 +218,7 @@ self.onmessage = async (e) => {
203
218
  if (!modPromise) modPromise = loadCore()
204
219
  const Module = await modPromise
205
220
  // Warmup: only load the kernel (+ spawn the mt pthread pool) ahead of time — removes the perceived load on the first slice
206
- if (d.cmd === 'warmup') { self.postMessage({ type: 'warm' }); return }
221
+ if (d.cmd === 'warmup') { self.postMessage({ type: 'warm', kernel: kernelKind }); return }
207
222
  // Stage 20: manual support painting — selector state persists in this worker Module (slicing uses the same Module).
208
223
  // `keepPaint` says the mesh is the same model in a new place, so the marks carry over — the reply reports whether
209
224
  // they actually did, since a face-count change makes the kernel fall back to a clean registration.
@@ -224,6 +239,8 @@ self.onmessage = async (e) => {
224
239
  applyFill(Module, d, paintedState)
225
240
  }
226
241
  else if (paintedState === null) Module.selector_paint(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, d.enforcer)
242
+ else if (strokeFrom(d) && Module.selector_paint_stroke)
243
+ Module.selector_paint_stroke(d.facet, d.px, d.py, d.pz, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, paintedState, cursorShapeOf(d))
227
244
  else Module.selector_paint_shape(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, paintedState, cursorShapeOf(d))
228
245
  self.postMessage(paintedReply(Module, d, paintedState)); return
229
246
  }
@@ -236,6 +253,11 @@ self.onmessage = async (e) => {
236
253
  // change to the same counts, and the viewer's listeners must not need to know which brush produced it.
237
254
  if (d.cmd === 'erase') {
238
255
  if (FILL_TOOLS.has(d.tool)) applyFill(Module, d, null)
256
+ else if (strokeFrom(d) && Module.selector_erase_stroke)
257
+ Module.selector_erase_stroke(d.facet, d.px, d.py, d.pz, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, cursorShapeOf(d))
258
+ // The eraser gets the cursor shape too, or a shift+drag with the circle brush rubs out the far side of a wall
259
+ // the paint brush never touched. `selector_erase` (no shape) stays the fallback for an older kernel.
260
+ else if (Module.selector_erase_shape) Module.selector_erase_shape(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, cursorShapeOf(d))
239
261
  else Module.selector_erase(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius)
240
262
  self.postMessage(paintedReply(Module, d, null)); return
241
263
  }
@@ -292,6 +314,34 @@ self.onmessage = async (e) => {
292
314
  self.postMessage(reply, transfer); return
293
315
  }
294
316
 
317
+ // fillPreview: what a fill WOULD select, without applying it. Upstream runs this on every mouse move while a
318
+ // fill tool is active and draws the result in a lighter shade (GLGizmoPainterBase.cpp:929) — a preview marks
319
+ // nothing, so it needs no state at all and there is no NONE for a stray boolean to become. `{clear:true}`
320
+ // drops the standing selection, which is what leaving the model under the pointer means.
321
+ if (d.cmd === 'fillPreview') {
322
+ if (!Module.selector_fill_preview) { self.postMessage({ type: 'fillPreview', supported: false, tris: EMPTY_OVERLAY }); return }
323
+ if (d.clear) { Module.selector_fill_preview_clear(); self.postMessage({ type: 'fillPreview', supported: true, tris: EMPTY_OVERLAY }); return }
324
+ const angle = Number.isFinite(d.angle) ? d.angle : 30
325
+ const mode = FILL_PREVIEW_MODE[d.tool] ?? FILL_PREVIEW_MODE.smart
326
+ // Upstream's -1 for the non-propagating case, the same value applyFill sends: one facet has no neighbour to
327
+ // measure an angle against.
328
+ const tris = Module.selector_fill_preview(d.facet, d.hx, d.hy, d.hz, mode === FILL_PREVIEW_MODE.triangle ? -1 : angle, mode)
329
+ self.postMessage({ type: 'fillPreview', supported: true, tris },
330
+ tris?.buffer && tris.byteLength > 0 ? [tris.buffer] : [])
331
+ return
332
+ }
333
+ // paintMode: the brush-wide options upstream keeps on the gizmo — the overhang restriction and the section
334
+ // plane. Both are set once and read by every stroke, so they are their own command rather than fields on one.
335
+ if (d.cmd === 'paintMode') {
336
+ if (Number.isFinite(d.overhangDeg) && Module.selector_set_overhang_limit) Module.selector_set_overhang_limit(d.overhangDeg)
337
+ if (Module.selector_set_clip_plane) {
338
+ const plane = d.clipPlane
339
+ if (plane) Module.selector_set_clip_plane(plane[0], plane[1], plane[2], plane[3], true)
340
+ else if (d.clipPlane === null) Module.selector_set_clip_plane(0, 0, 1, 0, false)
341
+ }
342
+ self.postMessage({ type: 'paintMode', ok: true }); return
343
+ }
344
+
295
345
  if (d.stall) return // stage-30 test hook: simulate a hang -> verifies the main-thread watchdog fires (not set in production)
296
346
  // mt (SAB): share the address of the real support progress counter (u32) with main once — even while the kernel is
297
347
  // blocked inside C++, the UI thread polls the SAB directly to show support progress. st (non-shared buffer) is unsupported -> not sent.
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-slicer",
3
- "version": "0.2.3",
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",
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 1,
3
+ "entries": {
4
+ "79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
5
+ "last_emitted_at_ms": 1788437194779,
6
+ "message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
7
+ }
8
+ },
9
+ "updated_at": "2026-09-03T12:06:34.779Z"
10
+ }
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>