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.
- package/components/dist/SettingsPanel.js +138 -116
- package/components/styles.css +4 -0
- package/engine/src/client.js +3 -2
- package/engine/src/slicer.worker.js +6 -1
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/package.json +1 -1
- package/types/client.d.ts +2 -2
- package/types/viewer.d.ts +41 -2
- package/types/worker.d.ts +2 -1
- package/viewer/README.md +49 -1
- package/viewer/dist/Viewport.js +4917 -3835
- package/viewer/dist/sl1_encode.worker.js +8 -8
- package/viewer/dist/sl1_write-CtX8cVL7.js +302 -0
- package/viewer/dist/sla_slice.worker.js +1 -1
- package/viewer/styles.css +41 -0
- package/viewer/dist/sl1_write-UHmQ_itC.js +0 -170
|
@@ -1,179 +1,201 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createPortal as
|
|
3
|
-
import { uiTree as
|
|
4
|
-
import { printerTechnology as
|
|
5
|
-
import { disabledKeys as
|
|
6
|
-
function
|
|
7
|
-
const
|
|
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
|
|
12
|
-
const r =
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
}, [
|
|
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
|
|
18
|
-
const s =
|
|
19
|
-
return Array.from({ length:
|
|
20
|
-
const r = 2 * Math.PI *
|
|
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
|
-
},
|
|
24
|
-
const s =
|
|
25
|
-
return { x0: Math.min(...s), y0: Math.min(...
|
|
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
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
}, [JSON.stringify(
|
|
33
|
-
const v = (
|
|
34
|
-
|
|
35
|
-
const { w:
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
g(
|
|
39
|
-
},
|
|
40
|
-
|
|
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
|
|
43
|
-
if (!Array.isArray(
|
|
44
|
-
|
|
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
|
-
|
|
46
|
+
E(!0);
|
|
47
47
|
}
|
|
48
|
-
},
|
|
49
|
-
const
|
|
50
|
-
return Number.isFinite(
|
|
48
|
+
}, k = (n) => {
|
|
49
|
+
const m = parseFloat(n);
|
|
50
|
+
return Number.isFinite(m) ? m : 0;
|
|
51
51
|
};
|
|
52
|
-
return /* @__PURE__ */
|
|
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:
|
|
58
|
-
disabled:
|
|
57
|
+
value: c.w,
|
|
58
|
+
disabled: a,
|
|
59
59
|
"data-testid": "bed-w",
|
|
60
|
-
onChange: (
|
|
60
|
+
onChange: (n) => v({ ...c, w: k(n.target.value) })
|
|
61
61
|
}
|
|
62
|
-
), " mm"), /* @__PURE__ */
|
|
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:
|
|
68
|
-
disabled:
|
|
67
|
+
value: c.d,
|
|
68
|
+
disabled: a,
|
|
69
69
|
"data-testid": "bed-d",
|
|
70
|
-
onChange: (
|
|
70
|
+
onChange: (n) => v({ ...c, d: k(n.target.value) })
|
|
71
71
|
}
|
|
72
|
-
), " mm"), /* @__PURE__ */
|
|
72
|
+
), " mm"), /* @__PURE__ */ t.createElement("label", null, "Origin X ", /* @__PURE__ */ t.createElement(
|
|
73
73
|
"input",
|
|
74
74
|
{
|
|
75
75
|
type: "number",
|
|
76
|
-
value:
|
|
77
|
-
disabled:
|
|
78
|
-
onChange: (
|
|
76
|
+
value: c.ox,
|
|
77
|
+
disabled: a,
|
|
78
|
+
onChange: (n) => v({ ...c, ox: k(n.target.value) })
|
|
79
79
|
}
|
|
80
|
-
)), /* @__PURE__ */
|
|
80
|
+
)), /* @__PURE__ */ t.createElement("label", null, "Origin Y ", /* @__PURE__ */ t.createElement(
|
|
81
81
|
"input",
|
|
82
82
|
{
|
|
83
83
|
type: "number",
|
|
84
|
-
value:
|
|
85
|
-
disabled:
|
|
86
|
-
onChange: (
|
|
84
|
+
value: c.oy,
|
|
85
|
+
disabled: a,
|
|
86
|
+
onChange: (n) => v({ ...c, oy: k(n.target.value) })
|
|
87
87
|
}
|
|
88
|
-
))), r === "circle" && /* @__PURE__ */
|
|
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:
|
|
94
|
-
disabled:
|
|
93
|
+
value: N,
|
|
94
|
+
disabled: a,
|
|
95
95
|
"data-testid": "bed-dia",
|
|
96
|
-
onChange: (
|
|
96
|
+
onChange: (n) => z(k(n.target.value))
|
|
97
97
|
}
|
|
98
|
-
), " mm")), r === "custom" && /* @__PURE__ */
|
|
98
|
+
), " mm")), r === "custom" && /* @__PURE__ */ t.createElement(
|
|
99
99
|
"textarea",
|
|
100
100
|
{
|
|
101
|
-
className: "bse-json" + (
|
|
101
|
+
className: "bse-json" + (w ? " err" : ""),
|
|
102
102
|
rows: 3,
|
|
103
|
-
value:
|
|
104
|
-
disabled:
|
|
103
|
+
value: p,
|
|
104
|
+
disabled: a,
|
|
105
105
|
"data-testid": "bed-json",
|
|
106
|
-
onChange: (
|
|
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
|
|
112
|
-
printable_area:
|
|
113
|
-
},
|
|
114
|
-
function
|
|
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 (
|
|
123
|
-
const
|
|
124
|
-
return s
|
|
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
|
|
127
|
-
return
|
|
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
|
|
130
|
-
return /* @__PURE__ */
|
|
129
|
+
function re({ mode: e }) {
|
|
130
|
+
return /* @__PURE__ */ t.createElement("span", { className: `badge mode-${e ?? "none"}` }, e ?? "—");
|
|
131
131
|
}
|
|
132
|
-
function
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
const
|
|
136
|
-
switch (
|
|
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__ */
|
|
139
|
-
var
|
|
140
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
148
|
-
`) :
|
|
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__ */
|
|
150
|
+
return /* @__PURE__ */ t.createElement("code", { className: "pts" }, JSON.stringify(d ?? []));
|
|
151
151
|
case "unknown":
|
|
152
|
-
return /* @__PURE__ */
|
|
152
|
+
return /* @__PURE__ */ t.createElement("span", { className: "muted" }, "No schema");
|
|
153
153
|
default:
|
|
154
|
-
return /* @__PURE__ */
|
|
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
|
|
158
|
-
if (
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
return delete
|
|
162
|
-
}),
|
|
163
|
-
return /* @__PURE__ */
|
|
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
|
|
166
|
-
const [
|
|
167
|
-
var
|
|
168
|
-
return
|
|
169
|
-
}).slice(0, 60).map(([
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
200
|
+
pe as default
|
|
179
201
|
};
|
package/components/styles.css
CHANGED
|
@@ -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; }
|
package/engine/src/client.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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<
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|