three-slicer-viewer 0.3.1 → 0.3.3

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/VIEWER.md CHANGED
@@ -95,7 +95,8 @@ Every panel can be switched off, and the values the component owns can be seeded
95
95
  resin masks on a filament bed. The one number it cannot supply is the display's PHYSICAL size: millimetres
96
96
  appear nowhere in an SL1, so the preview is scaled by the printer set in the viewer and the import notice says
97
97
  so. Export hands an imported archive back byte-identical rather than re-rasterizing empty geometry.
98
- - **`gcode`** — G-code text drawn on the selected plate instead of a slice result. `parseGcode` recovers roles from
98
+ - **`gcode`** — G-code text drawn on the selected plate instead of a slice result, or a `{ plateIndex: text }` map
99
+ drawn plate by plate (what an opened `.gcode.3mf` becomes; the viewer is preview-only while one is shown). `parseGcode` recovers roles from
99
100
  `;TYPE:` (OrcaSlicer/PrusaSlicer/Cura), from `;_EXTRUSION_ROLE:` tags and from this kernel's own feature comments;
100
101
  bead width comes from `;WIDTH:` or, absent that, from E. What the file never states cannot be recovered: an
101
102
  unmarked run reads as wall, and there is no print-time estimate (that needs the machine's acceleration limits).
@@ -255,6 +256,7 @@ opt-out only, everything on unless you say `false`, an unknown key stays on.
255
256
  | `filePicker` | The file dialog never opens, from any of the buttons that open it. |
256
257
  | `contextMenu` | No right-click menu. The browser's own menu stays suppressed over the canvas either way — OrbitControls preventDefaults `contextmenu` itself. |
257
258
  | `logs` | No console output, from the component or from the slice worker. |
259
+ | `versionBadge` | The build number at the right end of the status bar is not drawn. It is there so a bug report can name a build; turn it off when the viewer is embedded as part of your own product. `panels.status: false` hides it too, since it rides that bar. |
258
260
 
259
261
  ### Read-only panels
260
262
 
@@ -394,6 +396,29 @@ for 143MB) against a 4GB pool budget, which gives 2 for that model and does not
394
396
  manual count is capped the same way. What the browser has NOT measured yet is
395
397
  the arachne + tree-support preset, where the harness gains were largest.
396
398
 
399
+ ### The contour union on the GPU
400
+
401
+ The slice bar's **GPU** select (`gpu_acceleration` in the settings map: `auto`, `on`, `off`; absent is `auto`; a viewer
402
+ knob like `slice_workers`, not a kernel parameter) lets the slicer worker compute every layer's contour union with
403
+ WebGPU instead of Clipper. `auto` asks for it only when the single-threaded kernel loaded, `on` on either kernel,
404
+ `off` never. It applies to the selected plate's filament slice; pool workers, multi-material and resin slices compute
405
+ their contours themselves.
406
+
407
+ Measured in node (Dawn, Metal, 15 cores) with the kernel consuming the GPU's contours:
408
+
409
+ | model | kernel | CPU | GPU |
410
+ |---|---|---|---|
411
+ | 3M-facet scan, tree slim support, 420 layers | st | 9.6-9.8 s | 7.6-7.7 s |
412
+ | 1.13M-facet plate, tree hybrid support, 849 layers | st | 105 s | 102-104 s |
413
+ | either | mt | | within run-to-run spread of the CPU slice |
414
+
415
+ The union is about 2.5 s of the single-threaded slice and 50-100 ms on the GPU; on the threaded kernel the same union is
416
+ already spread over the threads, which is why `auto` leaves it alone. What it costs: the GPU's contours have the CPU
417
+ union's area (within 2e-10 relative) and other vertices, so the G-code is not the CPU slice's byte for byte (filament
418
+ within 0.3 % in those two models). The slice never depends on the GPU: without a device, or when the GPU refuses an
419
+ input, the kernel slices by itself, a layer whose loops the GPU leaves open gets the kernel's own union, and the stats
420
+ card says which happened (`Contours on the GPU`, with the count of layers that went back to the CPU).
421
+
397
422
  ## Theming
398
423
 
399
424
  Both shadow roots (the viewer and `<SettingsPanel/>`) paint their chrome from one palette, `UI_TOKENS` in
@@ -8,39 +8,39 @@ import { S as X } from "./shadow_host-CQ3LirzD.js";
8
8
  const Y = ':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 var(--_light-divider)}.row .lbl{color:var(--_light-text);text-decoration:none}.row .lbl:hover{color:var(--_accent-ink);text-decoration:underline}.row .key{font-size:11px;color:var(--_light-text-faint);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:var(--_light-text-faint);font-size:12px;white-space:nowrap}.pts{font-size:11px;color:var(--_light-text-muted)}.badge{font-size:10px;padding:1px 6px;border-radius:8px;color:var(--_on-accent);text-align:center}.mode-simple{background:var(--_accent-ink)}.mode-advanced{background:var(--_info-ink)}.mode-expert{background:var(--_warn)}.mode-develop{background:var(--_danger)}.mode-none,.mode-undefined{background:var(--_light-text-faint)}.muted{color:var(--_light-text-faint);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:var(--_light-subtle)}.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:var(--_warn);box-shadow:0 0 0 2px var(--_warn-soft)}.override-dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:var(--_warn);border:0;padding:0;cursor:pointer;box-shadow:0 0 0 2px var(--_warn-soft)}.override-dot:hover{background:var(--_warn-ink)}.widget-cell{display:inline-flex;align-items:center;gap:6px;min-width:0}.reset-btn{flex:0 0 auto;border:1px solid var(--_warn-border);background:var(--_warn-soft);color:var(--_warn-ink);border-radius:5px;cursor:pointer;font-size:13px;line-height:1;padding:1px 5px}.reset-btn:hover{background:var(--_warn-soft-hover)}.settings-panel{flex:0 0 400px;width:400px;border-left:1px solid var(--_light-divider);display:flex;flex-direction:column;min-height:0;background:var(--_light-surface);color:var(--_light-text);font:14px/1.5 -apple-system,Segoe UI,sans-serif}.settings-panel .sp-top{padding:10px 12px 8px;border-bottom:1px solid var(--_light-divider)}.sp-builder{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--_light-text-muted)}.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 var(--_light-border);background:var(--_light-subtle);border-radius:6px;padding:4px 8px;font-size:12px;cursor:pointer}.sp-pages button.on{background:var(--_accent-soft);color:var(--_accent-ink);border-color:var(--_accent-soft-border);font-weight:700}.sp-modes{display:flex;gap:4px;margin-top:8px}.sp-modes button{border:0;background:none;font-size:11px;color:var(--_light-text-faint);padding:2px 6px;border-radius:6px;cursor:pointer}.sp-modes button.on{background:var(--_accent-soft);color:var(--_accent-ink);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 var(--_light-border);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 var(--_light-border);background:var(--_light-subtle);border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer}.bse-modes button.on{background:var(--_accent-soft);color:var(--_accent-ink);border-color:var(--_accent-soft-border);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:var(--_light-text-muted);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 var(--_light-border);border-radius:6px;box-sizing:border-box}.bse-json.err{border-color:var(--_danger);background:var(--_danger-soft)}', q = (t, a, r, o) => [[r, o], [t + r, o], [t + r, a + o], [r, a + o]], L = R.printable_area.default, V = (t) => {
9
9
  const a = t / 2, r = 32;
10
10
  return Array.from({ length: r }, (o, i) => {
11
- const u = 2 * Math.PI * i / r;
12
- return [+(a + a * Math.cos(u)).toFixed(2), +(a + a * Math.sin(u)).toFixed(2)];
11
+ const m = 2 * Math.PI * i / r;
12
+ return [+(a + a * Math.cos(m)).toFixed(2), +(a + a * Math.sin(m)).toFixed(2)];
13
13
  });
14
14
  }, T = (t) => {
15
15
  const a = t.map((o) => o[0]), r = t.map((o) => o[1]);
16
16
  return { x0: Math.min(...a), y0: Math.min(...r), w: Math.max(...a) - Math.min(...a), d: Math.max(...r) - Math.min(...r) };
17
17
  };
18
18
  function Z({ value: t, onChange: a, disabled: r }) {
19
- const o = Array.isArray(t) && t.length >= 3 ? t : L, i = T(o), [u, d] = y(o.length === 4 ? "rect" : o.length >= 12 ? "circle" : "custom"), [c, p] = y({ w: i.w, d: i.d, ox: i.x0, oy: i.y0 }), [x, g] = y(Math.round(i.w)), [l, b] = y(JSON.stringify(o)), [k, w] = y(!1);
19
+ const o = Array.isArray(t) && t.length >= 3 ? t : L, i = T(o), [m, d] = y(o.length === 4 ? "rect" : o.length >= 12 ? "circle" : "custom"), [c, p] = y({ w: i.w, d: i.d, ox: i.x0, oy: i.y0 }), [x, b] = y(Math.round(i.w)), [l, u] = y(JSON.stringify(o)), [_, w] = y(!1);
20
20
  P(() => {
21
21
  const n = T(o);
22
- p({ w: n.w, d: n.d, ox: n.x0, oy: n.y0 }), g(Math.round(n.w)), b(JSON.stringify(o)), w(!1);
22
+ p({ w: n.w, d: n.d, ox: n.x0, oy: n.y0 }), b(Math.round(n.w)), u(JSON.stringify(o)), w(!1);
23
23
  }, [JSON.stringify(t)]);
24
24
  const v = (n) => {
25
25
  p(n);
26
- const { w: m, d: f, ox: S, oy: M } = n;
27
- m > 0 && f > 0 && a(q(m, f, S, M));
28
- }, _ = (n) => {
29
- g(n), n > 0 && a(V(n));
26
+ const { w: g, d: f, ox: S, oy: M } = n;
27
+ g > 0 && f > 0 && a(q(g, f, S, M));
28
+ }, N = (n) => {
29
+ b(n), n > 0 && a(V(n));
30
30
  }, z = (n) => {
31
- b(n);
31
+ u(n);
32
32
  try {
33
- const m = JSON.parse(n);
34
- if (!Array.isArray(m) || m.length < 3 || !m.every((f) => Array.isArray(f) && f.length === 2 && f.every(Number.isFinite))) throw 0;
35
- w(!1), a(m);
33
+ const g = JSON.parse(n);
34
+ if (!Array.isArray(g) || g.length < 3 || !g.every((f) => Array.isArray(f) && f.length === 2 && f.every(Number.isFinite))) throw 0;
35
+ w(!1), a(g);
36
36
  } catch {
37
37
  w(!0);
38
38
  }
39
39
  }, E = (n) => {
40
- const m = parseFloat(n);
41
- return Number.isFinite(m) ? m : 0;
40
+ const g = parseFloat(n);
41
+ return Number.isFinite(g) ? g : 0;
42
42
  };
43
- 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(([n, m]) => /* @__PURE__ */ e.createElement("button", { key: n, type: "button", className: u === n ? "on" : "", disabled: r, onClick: () => d(n) }, m))), u === "rect" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Width ", /* @__PURE__ */ e.createElement(
43
+ 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(([n, g]) => /* @__PURE__ */ e.createElement("button", { key: n, type: "button", className: m === n ? "on" : "", disabled: r, onClick: () => d(n) }, g))), m === "rect" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Width ", /* @__PURE__ */ e.createElement(
44
44
  "input",
45
45
  {
46
46
  type: "number",
@@ -76,7 +76,7 @@ function Z({ value: t, onChange: a, disabled: r }) {
76
76
  disabled: r,
77
77
  onChange: (n) => v({ ...c, oy: E(n.target.value) })
78
78
  }
79
- ))), u === "circle" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Diameter ", /* @__PURE__ */ e.createElement(
79
+ ))), m === "circle" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "Diameter ", /* @__PURE__ */ e.createElement(
80
80
  "input",
81
81
  {
82
82
  type: "number",
@@ -84,12 +84,12 @@ function Z({ value: t, onChange: a, disabled: r }) {
84
84
  value: x,
85
85
  disabled: r,
86
86
  "data-testid": "bed-dia",
87
- onChange: (n) => _(E(n.target.value))
87
+ onChange: (n) => N(E(n.target.value))
88
88
  }
89
- ), " mm")), u === "custom" && /* @__PURE__ */ e.createElement(
89
+ ), " mm")), m === "custom" && /* @__PURE__ */ e.createElement(
90
90
  "textarea",
91
91
  {
92
- className: "bse-json" + (k ? " err" : ""),
92
+ className: "bse-json" + (_ ? " err" : ""),
93
93
  rows: 3,
94
94
  value: l,
95
95
  disabled: r,
@@ -118,35 +118,39 @@ function le(t) {
118
118
  function ie({ mode: t }) {
119
119
  return /* @__PURE__ */ e.createElement("span", { className: `badge mode-${t ?? "none"}` }, t ?? "—");
120
120
  }
121
- function ce({ def: t, optKey: a, settings: r, setSettings: o, disabled: i, customWidgets: u }) {
122
- const d = I(r, a), c = (l) => o((b) => ({ ...b, [a]: l })), p = u?.[a] ?? K[a];
121
+ function ce({ def: t, optKey: a, settings: r, setSettings: o, disabled: i, customWidgets: m }) {
122
+ const d = I(r, a), c = (l) => o((u) => ({ ...u, [a]: l })), p = m?.[a] ?? K[a];
123
123
  if (p) return /* @__PURE__ */ e.createElement(p, { def: t, value: d, onChange: c, disabled: i });
124
- const x = le(t), g = Array.isArray(d) ? d[0] : d;
124
+ const x = le(t), b = Array.isArray(d) ? d[0] : d;
125
125
  switch (x) {
126
126
  case "select":
127
- return /* @__PURE__ */ e.createElement("select", { value: g ?? "", onChange: (l) => c(l.target.value), disabled: i }, t.enum_values.map((l, b) => /* @__PURE__ */ e.createElement("option", { key: l, value: l }, t.enum_labels?.[b] ?? l)));
127
+ return /* @__PURE__ */ e.createElement("select", { value: b ?? "", onChange: (l) => c(l.target.value), disabled: i }, t.enum_values.map((l, u) => /* @__PURE__ */ e.createElement("option", { key: l, value: l }, t.enum_labels?.[u] ?? l)));
128
128
  case "checkbox":
129
129
  return /* @__PURE__ */ e.createElement("input", { type: "checkbox", checked: Array.isArray(d) ? !!d[0] : !!d, onChange: (l) => c(l.target.checked), disabled: i });
130
130
  case "color":
131
- return /* @__PURE__ */ e.createElement("input", { type: "color", value: typeof g == "string" && g.startsWith("#") ? g : H, onChange: (l) => c(l.target.value), disabled: i });
132
- case "textarea":
133
- return /* @__PURE__ */ e.createElement("textarea", { rows: 2, value: Array.isArray(d) ? d.join(`
134
- `) : d ?? "", onChange: (l) => c(l.target.value), disabled: i });
131
+ return /* @__PURE__ */ e.createElement("input", { type: "color", value: typeof b == "string" && b.startsWith("#") ? b : H, onChange: (l) => c(l.target.value), disabled: i });
132
+ case "textarea": {
133
+ let l = 2;
134
+ t.full_width && (l = 12);
135
+ let u = d ?? "";
136
+ return Array.isArray(d) && (u = d.join(`
137
+ `)), /* @__PURE__ */ e.createElement("textarea", { rows: l, value: u, onChange: (_) => c(_.target.value), disabled: i });
138
+ }
135
139
  case "points":
136
140
  return /* @__PURE__ */ e.createElement("code", { className: "pts" }, JSON.stringify(d ?? []));
137
141
  case "unknown":
138
142
  return /* @__PURE__ */ e.createElement("span", { className: "muted" }, "No schema");
139
143
  default:
140
- return /* @__PURE__ */ e.createElement("span", { className: "inputwrap" }, /* @__PURE__ */ e.createElement("input", { value: g ?? "", onChange: (l) => c(l.target.value), disabled: i }), t.sidetext && /* @__PURE__ */ e.createElement("span", { className: "unit" }, t.sidetext));
144
+ return /* @__PURE__ */ e.createElement("span", { className: "inputwrap" }, /* @__PURE__ */ e.createElement("input", { value: b ?? "", onChange: (l) => c(l.target.value), disabled: i }), t.sidetext && /* @__PURE__ */ e.createElement("span", { className: "unit" }, t.sidetext));
141
145
  }
142
146
  }
143
- function de({ schema: t, optKey: a, settings: r, setSettings: o, disabled: i, onOptionOpen: u, customWidgets: d, overridden: c, onRevertKey: p }) {
147
+ function de({ schema: t, optKey: a, settings: r, setSettings: o, disabled: i, onOptionOpen: m, customWidgets: d, overridden: c, onRevertKey: p }) {
144
148
  if (a.startsWith("<")) return /* @__PURE__ */ e.createElement("div", { className: "row custom" }, /* @__PURE__ */ e.createElement("span", { className: "muted" }, "⚙ Custom widget ", a));
145
- const x = t[a], g = i ? i[a] : void 0, l = g != null, b = oe(t, r, a), k = () => o((v) => {
146
- const _ = { ...v };
147
- return delete _[a], _;
149
+ const x = t[a], b = i ? i[a] : void 0, l = b != null, u = oe(t, r, a), _ = () => o((v) => {
150
+ const N = { ...v };
151
+ return delete N[a], N;
148
152
  }), w = x?.label || x?.full_label || a;
149
- return /* @__PURE__ */ e.createElement("div", { className: "row" + (l ? " disabled" : ""), title: l ? `Disabled when: ${g}` : x?.tooltip ?? "", "data-testid": `row-${a}` }, /* @__PURE__ */ e.createElement("span", { className: "lbl-cell" }, c && /* @__PURE__ */ e.createElement(
153
+ return /* @__PURE__ */ e.createElement("div", { className: "row" + (l ? " disabled" : ""), title: l ? `Disabled when: ${b}` : x?.tooltip ?? "", "data-testid": `row-${a}` }, /* @__PURE__ */ e.createElement("span", { className: "lbl-cell" }, c && /* @__PURE__ */ e.createElement(
150
154
  "button",
151
155
  {
152
156
  type: "button",
@@ -155,7 +159,7 @@ function de({ schema: t, optKey: a, settings: r, setSettings: o, disabled: i, on
155
159
  title: "This plate's override — click to follow the global value again",
156
160
  "data-testid": `override-${a}`
157
161
  }
158
- ), b && /* @__PURE__ */ e.createElement("span", { className: "dirty-dot", title: "Changed from default", "data-testid": `dirty-${a}` }), u ? /* @__PURE__ */ e.createElement("button", { type: "button", className: "lbl lbl-link", onClick: () => u(a), title: "Details" }, w) : /* @__PURE__ */ e.createElement("span", { className: "lbl" }, w)), /* @__PURE__ */ e.createElement("code", { className: "key" }, a), /* @__PURE__ */ e.createElement(ie, { mode: x?.mode }), /* @__PURE__ */ e.createElement("span", { className: "widget-cell" }, /* @__PURE__ */ e.createElement(ce, { def: x, optKey: a, settings: r, setSettings: o, disabled: l, customWidgets: d }), b && /* @__PURE__ */ e.createElement("button", { className: "reset-btn", onClick: k, title: "Reset to default", "data-testid": `reset-${a}` }, "↺")));
162
+ ), u && /* @__PURE__ */ e.createElement("span", { className: "dirty-dot", title: "Changed from default", "data-testid": `dirty-${a}` }), m ? /* @__PURE__ */ e.createElement("button", { type: "button", className: "lbl lbl-link", onClick: () => m(a), title: "Details" }, w) : /* @__PURE__ */ e.createElement("span", { className: "lbl" }, w)), /* @__PURE__ */ e.createElement("code", { className: "key" }, a), /* @__PURE__ */ e.createElement(ie, { mode: x?.mode }), /* @__PURE__ */ e.createElement("span", { className: "widget-cell" }, /* @__PURE__ */ e.createElement(ce, { def: x, optKey: a, settings: r, setSettings: o, disabled: l, customWidgets: d }), u && /* @__PURE__ */ e.createElement("button", { className: "reset-btn", onClick: _, title: "Reset to default", "data-testid": `reset-${a}` }, "↺")));
159
163
  }
160
164
  function fe({
161
165
  schema: t = R,
@@ -163,14 +167,14 @@ function fe({
163
167
  toggle: r = re,
164
168
  settings: o,
165
169
  setSettings: i,
166
- onOptionOpen: u,
170
+ onOptionOpen: m,
167
171
  embedded: d = !1,
168
172
  customWidgets: c,
169
173
  only: p,
170
174
  overriddenKeys: x,
171
- onRevertKey: g
175
+ onRevertKey: b
172
176
  }) {
173
- const l = j(() => ne(a), [a]), [b, k] = y(p?.builder ?? l[0] ?? ""), [w, v] = y(0), [_, z] = y("all"), [E, n] = y(""), m = W(o), f = l.filter((s) => te(s) === m), S = p?.builder ?? (f.includes(b) ? b : f[0] ?? ""), M = a[p?.builder ?? S] ?? [], A = p?.page ? M.filter((s) => s.page === p.page) : M, O = A[Math.min(w, A.length - 1)], J = j(() => r.disabledKeys(r.makeCfg(o)), [o, r]), N = E.trim().toLowerCase(), C = j(() => N ? Object.entries(t).filter(([s, h]) => s.includes(N) || h.label?.toLowerCase().includes(N) || h.full_label?.toLowerCase().includes(N)).slice(0, 60).map(([s]) => s) : [], [N]), D = (s, h) => /* @__PURE__ */ e.createElement(
177
+ const l = j(() => ne(a), [a]), [u, _] = y(p?.builder ?? l[0] ?? ""), [w, v] = y(0), [N, z] = y("all"), [E, n] = y(""), g = W(o), f = l.filter((s) => te(s) === g), S = p?.builder ?? (f.includes(u) ? u : f[0] ?? ""), M = a[p?.builder ?? S] ?? [], A = p?.page ? M.filter((s) => s.page === p.page) : M, O = A[Math.min(w, A.length - 1)], J = j(() => r.disabledKeys(r.makeCfg(o)), [o, r]), k = E.trim().toLowerCase(), C = j(() => k ? Object.entries(t).filter(([s, h]) => s.includes(k) || h.label?.toLowerCase().includes(k) || h.full_label?.toLowerCase().includes(k)).slice(0, 60).map(([s]) => s) : [], [k]), D = (s, h) => /* @__PURE__ */ e.createElement(
174
178
  de,
175
179
  {
176
180
  schema: t,
@@ -179,16 +183,16 @@ function fe({
179
183
  settings: o,
180
184
  setSettings: i,
181
185
  disabled: J,
182
- onOptionOpen: u,
186
+ onOptionOpen: m,
183
187
  customWidgets: c,
184
188
  overridden: x?.includes(s),
185
- onRevertKey: g
189
+ onRevertKey: b
186
190
  }
187
191
  );
188
- return /* @__PURE__ */ e.createElement(X, { css: Q + Y, className: d ? "sp-embedded" : void 0 }, /* @__PURE__ */ e.createElement("div", { className: "settings-panel" }, !p && /* @__PURE__ */ e.createElement("div", { className: "sp-top" }, /* @__PURE__ */ e.createElement("input", { className: "sp-search", placeholder: `Search options (${Object.keys(t).length})…`, value: E, onChange: (s) => n(s.target.value), "data-testid": "opt-search" }), !N && /* @__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: S, onChange: (s) => {
189
- k(s.target.value), v(0);
190
- } }, f.map((s) => /* @__PURE__ */ e.createElement("option", { key: s, value: s }, B(s))))), /* @__PURE__ */ e.createElement("div", { className: "sp-pages" }, A.map((s, h) => /* @__PURE__ */ e.createElement("button", { key: s.page + h, className: h === Math.min(w, A.length - 1) ? "on" : "", onClick: () => v(h) }, s.page))), /* @__PURE__ */ e.createElement("div", { className: "sp-modes" }, ee.map((s) => /* @__PURE__ */ e.createElement("button", { key: s, className: _ === s ? "on" : "", onClick: () => z(s) }, s))))), /* @__PURE__ */ e.createElement("div", { className: "sp-body" }, N ? /* @__PURE__ */ e.createElement("section", { "data-testid": "search-results" }, /* @__PURE__ */ e.createElement("h3", null, "Search: “", E, "” — ", C.length, " hit", C.length === 1 ? "" : "s", C.length === 60 ? "+" : ""), C.map(D), !C.length && /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No matches")) : O ? /* @__PURE__ */ e.createElement(e.Fragment, null, !p && /* @__PURE__ */ e.createElement("h2", null, O.page, " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, "Tab.cpp:", O.line)), O.groups.map((s) => {
191
- const h = s.options.filter((F) => se(t, F, _) && !(p && F.startsWith("<")));
192
+ return /* @__PURE__ */ e.createElement(X, { css: Q + Y, className: d ? "sp-embedded" : void 0 }, /* @__PURE__ */ e.createElement("div", { className: "settings-panel" }, !p && /* @__PURE__ */ e.createElement("div", { className: "sp-top" }, /* @__PURE__ */ e.createElement("input", { className: "sp-search", placeholder: `Search options (${Object.keys(t).length})…`, value: E, onChange: (s) => n(s.target.value), "data-testid": "opt-search" }), !k && /* @__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: S, onChange: (s) => {
193
+ _(s.target.value), v(0);
194
+ } }, f.map((s) => /* @__PURE__ */ e.createElement("option", { key: s, value: s }, B(s))))), /* @__PURE__ */ e.createElement("div", { className: "sp-pages" }, A.map((s, h) => /* @__PURE__ */ e.createElement("button", { key: s.page + h, className: h === Math.min(w, A.length - 1) ? "on" : "", onClick: () => v(h) }, s.page))), /* @__PURE__ */ e.createElement("div", { className: "sp-modes" }, ee.map((s) => /* @__PURE__ */ e.createElement("button", { key: s, className: N === s ? "on" : "", onClick: () => z(s) }, s))))), /* @__PURE__ */ e.createElement("div", { className: "sp-body" }, k ? /* @__PURE__ */ e.createElement("section", { "data-testid": "search-results" }, /* @__PURE__ */ e.createElement("h3", null, "Search: “", E, "” — ", C.length, " hit", C.length === 1 ? "" : "s", C.length === 60 ? "+" : ""), C.map(D), !C.length && /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No matches")) : O ? /* @__PURE__ */ e.createElement(e.Fragment, null, !p && /* @__PURE__ */ e.createElement("h2", null, O.page, " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, "Tab.cpp:", O.line)), O.groups.map((s) => {
195
+ const h = s.options.filter((F) => se(t, F, N) && !(p && F.startsWith("<")));
192
196
  return h.length ? /* @__PURE__ */ e.createElement("section", { key: s.group + s.line }, /* @__PURE__ */ e.createElement("h3", null, s.group || "(untitled group)", " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, ":", s.line)), h.map(D)) : null;
193
197
  })) : /* @__PURE__ */ e.createElement("p", { className: "muted" }, "No builder"))));
194
198
  }
@@ -1,6 +1,32 @@
1
- import { D as at, a as rt } from "./viewer_defaults-B7UUZ582.js";
2
- import { R as t, e as B } from "./toolpath_encoding-BuYs95qZ.js";
3
- const F = 1e-6, lt = {
1
+ import { D as Nt, a as Rt } from "./viewer_defaults-B7UUZ582.js";
2
+ import { R as t, e as Z } from "./toolpath_encoding-BuYs95qZ.js";
3
+ import { T as ft } from "./toolpath_palette-B8xEl-qa.js";
4
+ const K = 1e-6, It = 254, At = /^(filament|extruder)_colour\s*=\s*(.*)$/i, ut = (n) => n.split(/[;,]/).map((r) => {
5
+ const o = /^"?#?([0-9a-f]{6})(?:[0-9a-f]{2})?"?$/i.exec(r.trim());
6
+ return o ? "#" + o[1].toUpperCase() : null;
7
+ }), dt = (n) => "#" + ft[n % ft.length].map((r) => Math.round(r * 255).toString(16).padStart(2, "0")).join("");
8
+ function gt(n, r) {
9
+ let o = [];
10
+ Array.isArray(n?.colors) && (o = n.colors);
11
+ const d = r ?? [], u = Math.max(o.length, d.length, n?.filament_mm_by_tool?.length ?? 0);
12
+ return Array.from({ length: u }, (h, c) => o[c] || d[c] || dt(c));
13
+ }
14
+ function wt(n, r) {
15
+ return St(n.gcode, gt(n.stats, r));
16
+ }
17
+ function St(n, r) {
18
+ const o = String(n ?? ""), d = [r].flat().filter(Boolean);
19
+ if (!d.length || /^;\s*filament_colour\s*=/mi.test(o)) return o;
20
+ const u = "; filament_colour = " + d.join(";") + `
21
+ `, h = o.lastIndexOf(`
22
+ ; CONFIG_BLOCK_END`);
23
+ if (h >= 0) return o.slice(0, h + 1) + u + o.slice(h + 1);
24
+ let c = `
25
+ `;
26
+ return (!o || o.endsWith(`
27
+ `)) && (c = ""), o + c + u;
28
+ }
29
+ const ht = {
4
30
  // OrcaSlicer / PrusaSlicer ;TYPE: names
5
31
  "outer wall": t.WALL,
6
32
  "external perimeter": t.WALL,
@@ -43,7 +69,7 @@ const F = 1e-6, lt = {
43
69
  skin: t.SOLID,
44
70
  "support-interface": t.SUPPORT,
45
71
  "prime-tower": t.PRIME
46
- }, ot = [
72
+ }, Pt = [
47
73
  ["skirt/brim", t.SKIRT],
48
74
  ["skirt", t.SKIRT],
49
75
  ["walls", t.WALL],
@@ -54,7 +80,7 @@ const F = 1e-6, lt = {
54
80
  ["support", t.SUPPORT],
55
81
  ["prime tower", t.PRIME],
56
82
  ["wipe_tower_real", t.PRIME]
57
- ], ct = (I, f) => I === f || f.length < I.length && " (:/".includes(I[f.length]) && I.startsWith(f), ft = {
83
+ ], Ot = (n, r) => n === r || r.length < n.length && " (:/".includes(n[r.length]) && n.startsWith(r), Et = {
58
84
  1: t.WALL,
59
85
  2: t.WALL,
60
86
  3: t.WALL,
@@ -73,120 +99,140 @@ const F = 1e-6, lt = {
73
99
  16: t.SUPPORT,
74
100
  17: t.PRIME
75
101
  };
76
- function Lt(I, f = {}) {
77
- const it = Math.PI / 4 * (f.filamentDiameter > 0 ? f.filamentDiameter : at) ** 2, nt = f.defaultLayerHeight > 0 ? f.defaultLayerHeight : rt;
78
- let p = 0, u = 0, h = 0, G = 0, d = !0, O = !0, w = 0, T = t.WALL, W = 0, L = null;
79
- const P = [];
80
- let b = !1, D = !0, K = NaN, z = 0, H = 0, k = 0;
81
- const Y = /* @__PURE__ */ new Set([0]), y = (a) => {
82
- L = { z: a, p: [], w: [] }, P.push(L), D = !1, K = NaN, T = t.WALL, W = 0;
83
- }, st = () => P.length > 1 ? P[P.length - 2].z : 0, Z = (a, c, R, A, m, i, e) => {
84
- const r = Math.hypot(A - a, m - c, i - R);
85
- if (!(r < F))
86
- if (e > F) {
87
- k += e, D ? y(Number.isNaN(K) ? i : K) : !b && i > L.z + 1e-3 && y(i);
88
- const l = B(T, w);
89
- L.p.push(a, c, R, l, A, m, i, l);
90
- const n = Math.max(0.02, L.z - st() || nt), _ = W > 0 ? W : e * it / (r * n) + n * (1 - Math.PI / 4);
91
- L.w.push(Math.min(3, Math.max(0.05, _))), z++;
92
- } else L && (L.p.push(a, c, R, B(t.TRAVEL, w), A, m, i, B(t.TRAVEL, w)), L.w.push(0), H++);
102
+ function Wt(n, r = {}) {
103
+ const o = Math.PI / 4 * (r.filamentDiameter > 0 ? r.filamentDiameter : Nt) ** 2, d = r.defaultLayerHeight > 0 ? r.defaultLayerHeight : Rt;
104
+ let u = 0, h = 0, c = 0, b = 0, w = !0, y = !0, g = 0, O = t.WALL, W = 0, R = null;
105
+ const E = [];
106
+ let D = !1, x = !0, v = NaN, q = 0, X = 0, $ = 0;
107
+ const z = /* @__PURE__ */ new Set([0]), H = [];
108
+ let k = [], j = [], U = !1;
109
+ const V = () => {
110
+ O = t.WALL, W = 0;
111
+ }, C = (s, a = !1) => {
112
+ R = { z: s, p: [], w: [] }, E.push(R), x = !1, v = NaN, (a || !U) && V();
113
+ }, mt = () => E.length > 1 ? E[E.length - 2].z : 0, J = (s, a, S, P, A, i, e) => {
114
+ const m = Math.hypot(P - s, A - a, i - S);
115
+ if (!(m < K))
116
+ if (e > K) {
117
+ $ += e, H[g] = (H[g] ?? 0) + e, x ? C(Number.isNaN(v) ? i : v) : !D && i > R.z + 1e-3 && C(i);
118
+ const p = Z(O, g);
119
+ R.p.push(s, a, S, p, P, A, i, p);
120
+ const f = Math.max(0.02, R.z - mt() || d), L = W > 0 ? W : e * o / (m * f) + f * (1 - Math.PI / 4);
121
+ R.w.push(Math.min(3, Math.max(0.05, L))), q++;
122
+ } else R && (R.p.push(s, a, S, Z(t.TRAVEL, g), P, A, i, Z(t.TRAVEL, g)), R.w.push(0), X++);
93
123
  };
94
- for (const a of I.split(`
124
+ for (const s of n.split(`
95
125
  `)) {
96
- const c = a.indexOf(";"), R = (c < 0 ? a : a.slice(0, c)).trim(), A = c < 0 ? "" : a.slice(c + 1).trim();
97
- if (A && !R) {
98
- const e = A.toLowerCase();
126
+ const a = s.indexOf(";"), S = (a < 0 ? s : s.slice(0, a)).trim(), P = a < 0 ? "" : s.slice(a + 1).trim();
127
+ if (P && !S) {
128
+ const e = P.toLowerCase(), m = At.exec(P);
129
+ if (m) {
130
+ m[1].toLowerCase() === "filament" ? k = ut(m[2]) : j = ut(m[2]);
131
+ continue;
132
+ }
99
133
  if (e.startsWith("type:")) {
100
- T = lt[e.slice(5).trim()] ?? t.WALL;
134
+ O = ht[e.slice(5).trim()] ?? t.WALL, U = !0;
135
+ continue;
136
+ }
137
+ if (e.startsWith("feature:")) {
138
+ O = ht[e.slice(8).trim()] ?? t.WALL, U = !0;
101
139
  continue;
102
140
  }
103
141
  if (e.startsWith("_extrusion_role:")) {
104
- T = ft[parseInt(e.slice(16), 10)] ?? t.WALL;
142
+ O = Et[parseInt(e.slice(16), 10)] ?? t.WALL, U = !0;
105
143
  continue;
106
144
  }
107
- if (e.startsWith("width:")) {
108
- const n = parseFloat(e.slice(6));
109
- W = n > 0 ? n : 0;
145
+ if (e.startsWith("width:") || e.startsWith("line_width:")) {
146
+ const L = parseFloat(e.slice(e.indexOf(":") + 1));
147
+ W = 0, L > 0 && (W = L);
110
148
  continue;
111
149
  }
112
- if (e.startsWith("layer_change") || e.startsWith("layer:")) {
113
- b = !0, D = !0;
150
+ if (e.startsWith("layer_change") || e.startsWith("change_layer") || e.startsWith("layer:")) {
151
+ D = !0, x = !0, V();
114
152
  continue;
115
153
  }
116
- if (e.startsWith("z:")) {
117
- const n = parseFloat(e.slice(2));
118
- D && n > 0 && (b = !0, y(n));
154
+ if (e.startsWith("z:") || e.startsWith("z_height:")) {
155
+ const L = parseFloat(e.slice(e.indexOf(":") + 1));
156
+ x && L > 0 && (D = !0, C(L));
119
157
  continue;
120
158
  }
121
- const r = /^layer \d+ z([-\d.]+)/.exec(e);
122
- if (r) {
123
- b = !0, y(parseFloat(r[1]));
159
+ const p = /^layer \d+ z([-\d.]+)/.exec(e);
160
+ if (p) {
161
+ D = !0, C(parseFloat(p[1]), !0);
124
162
  continue;
125
163
  }
126
- const l = ot.find(([n]) => ct(e, n));
127
- l && (T = l[1]);
164
+ const f = Pt.find(([L]) => Ot(e, L));
165
+ f && (O = f[1]);
128
166
  continue;
129
167
  }
130
- if (!R) continue;
131
- const m = R.split(/\s+/), i = m[0].toUpperCase();
168
+ if (!S) continue;
169
+ const A = S.split(/\s+/), i = A[0].toUpperCase();
132
170
  if (i === "G90") {
133
- d = !0;
171
+ w = !0;
134
172
  continue;
135
173
  }
136
174
  if (i === "G91") {
137
- d = !1;
175
+ w = !1;
138
176
  continue;
139
177
  }
140
178
  if (i === "M82") {
141
- O = !0;
179
+ y = !0;
142
180
  continue;
143
181
  }
144
182
  if (i === "M83") {
145
- O = !1;
183
+ y = !1;
146
184
  continue;
147
185
  }
148
186
  if (/^T\d+$/.test(i)) {
149
- w = parseInt(i.slice(1), 10), Y.add(w);
187
+ const e = parseInt(i.slice(1), 10);
188
+ e <= It && (g = e, z.add(g));
150
189
  continue;
151
190
  }
152
191
  if (i === "G92" || i === "G0" || i === "G1" || i === "G2" || i === "G3") {
153
- let e = NaN, r = NaN, l = NaN, n = NaN, _ = 0, q = 0;
154
- for (let N = 1; N < m.length; N++) {
155
- const s = m[N][0], o = parseFloat(m[N].slice(1));
156
- Number.isNaN(o) || (s === "X" || s === "x" ? e = o : s === "Y" || s === "y" ? r = o : s === "Z" || s === "z" ? l = o : s === "E" || s === "e" ? n = o : s === "I" || s === "i" ? _ = o : (s === "J" || s === "j") && (q = o));
192
+ let e = NaN, m = NaN, p = NaN, f = NaN, L = 0, tt = 0;
193
+ for (let I = 1; I < A.length; I++) {
194
+ const l = A[I][0], N = parseFloat(A[I].slice(1));
195
+ Number.isNaN(N) || (l === "X" || l === "x" ? e = N : l === "Y" || l === "y" ? m = N : l === "Z" || l === "z" ? p = N : l === "E" || l === "e" ? f = N : l === "I" || l === "i" ? L = N : (l === "J" || l === "j") && (tt = N));
157
196
  }
158
197
  if (i === "G92") {
159
- Number.isNaN(e) || (p = e), Number.isNaN(r) || (u = r), Number.isNaN(l) || (h = l), Number.isNaN(n) || (G = n);
198
+ Number.isNaN(e) || (u = e), Number.isNaN(m) || (h = m), Number.isNaN(p) || (c = p), Number.isNaN(f) || (b = f);
160
199
  continue;
161
200
  }
162
- const U = Number.isNaN(e) ? p : d ? e : p + e, x = Number.isNaN(r) ? u : d ? r : u + r, v = Number.isNaN(l) ? h : d ? l : h + l, j = Number.isNaN(n) ? 0 : O ? n - G : n;
201
+ const B = Number.isNaN(e) ? u : w ? e : u + e, F = Number.isNaN(m) ? h : w ? m : h + m, Y = Number.isNaN(p) ? c : w ? p : c + p, et = Number.isNaN(f) ? 0 : y ? f - b : f;
163
202
  if (i === "G2" || i === "G3") {
164
- const N = p + _, s = u + q, o = Math.hypot(p - N, u - s), g = Math.atan2(u - s, p - N);
165
- let S = Math.atan2(x - s, U - N);
166
- const C = i === "G3";
167
- C && S <= g + F && (S += 2 * Math.PI), !C && S >= g - F && (S -= 2 * Math.PI);
168
- const E = Math.max(1, Math.min(360, Math.ceil(Math.abs(S - g) * o / 0.3)));
169
- let V = p, X = u, J = h;
170
- for (let M = 1; M <= E; M++) {
171
- const $ = g + (S - g) * M / E, Q = M === E ? U : N + o * Math.cos($), tt = M === E ? x : s + o * Math.sin($), et = h + (v - h) * M / E;
172
- Z(V, X, J, Q, tt, et, j / E), V = Q, X = tt, J = et;
203
+ const I = u + L, l = h + tt, N = Math.hypot(u - I, h - l), G = Math.atan2(h - l, u - I);
204
+ let M = Math.atan2(F - l, B - I);
205
+ const it = i === "G3";
206
+ it && M <= G + K && (M += 2 * Math.PI), !it && M >= G - K && (M -= 2 * Math.PI);
207
+ const _ = Math.max(1, Math.min(360, Math.ceil(Math.abs(M - G) * N / 0.3)));
208
+ let nt = u, rt = h, st = c;
209
+ for (let T = 1; T <= _; T++) {
210
+ const ot = G + (M - G) * T / _, at = T === _ ? B : I + N * Math.cos(ot), lt = T === _ ? F : l + N * Math.sin(ot), ct = c + (Y - c) * T / _;
211
+ J(nt, rt, st, at, lt, ct, et / _), nt = at, rt = lt, st = ct;
173
212
  }
174
213
  } else
175
- Z(p, u, h, U, x, v, j);
176
- p = U, u = x, h = v, Number.isNaN(n) || (G = O ? n : G + n);
214
+ J(u, h, c, B, F, Y, et);
215
+ u = B, h = F, c = Y, Number.isNaN(f) || (b = y ? f : b + f);
177
216
  }
178
217
  }
218
+ const pt = Math.max(k.length, j.length), Q = Array.from({ length: pt }, (s, a) => j[a] || k[a] || null), Lt = Array.from({ length: Math.max(...z) + 1 }, (s, a) => H[a] || 0);
179
219
  return {
180
- layers: P.map((a) => ({ z: a.z, paths: Float32Array.from(a.p), widths: Float32Array.from(a.w) })),
220
+ layers: E.map((s) => ({ z: s.z, paths: Float32Array.from(s.p), widths: Float32Array.from(s.w) })),
181
221
  stats: {
182
- layers: P.length,
183
- path_segments: z,
184
- travel_segments: H,
185
- filament_mm: k,
186
- tools: [...Y].sort((a, c) => a - c)
222
+ layers: E.length,
223
+ path_segments: q,
224
+ travel_segments: X,
225
+ filament_mm: $,
226
+ tools: [...z].sort((s, a) => s - a),
227
+ // The kernel's own stat name, so the Filament-view switch and the stats card's per-tool split work unchanged.
228
+ filament_mm_by_tool: Lt,
229
+ ...Q.some(Boolean) && { colors: Q }
187
230
  }
188
231
  };
189
232
  }
190
233
  export {
191
- Lt as parseGcode
234
+ wt as exportedGcode,
235
+ Wt as parseGcode,
236
+ gt as resultToolColors,
237
+ St as withFilamentColours
192
238
  };