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.
- package/components/dist/SettingsPanel.js +138 -116
- package/components/styles.css +4 -0
- package/engine/src/client.js +12 -2
- package/engine/src/slicer.worker.js +51 -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/.omc/state/sessions/02e5ddf5-4378-46e1-8fca-e90412c30804/pre-tool-advisory-throttle.json +10 -0
- package/types/client.d.ts +2 -2
- package/types/viewer.d.ts +41 -2
- package/types/worker.d.ts +30 -1
- package/viewer/README.md +69 -1
- package/viewer/dist/Viewport.js +5091 -3644
- 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 +45 -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
|
@@ -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
|
-
|
|
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
|
+
"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<
|
|
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>
|