three-slicer 0.1.0

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.
Files changed (54) hide show
  1. package/LICENSE.txt +661 -0
  2. package/README.md +44 -0
  3. package/components/README.md +39 -0
  4. package/components/dist/SettingsPanel.js +180 -0
  5. package/components/styles.css +84 -0
  6. package/data/README.md +22 -0
  7. package/data/config-schema-builddump.json +12921 -0
  8. package/data/config-schema.json +12685 -0
  9. package/data/invalidation-map.json +631 -0
  10. package/data/toggle-rules.json +2277 -0
  11. package/data/ui-tree.json +1586 -0
  12. package/engine/README.md +52 -0
  13. package/engine/examples/headless.mjs +37 -0
  14. package/engine/index.d.ts +29 -0
  15. package/engine/index.js +45 -0
  16. package/engine/src/settings.js +130 -0
  17. package/engine/src/slicer.worker.js +73 -0
  18. package/engine/src/slicer_core.js +0 -0
  19. package/engine/src/slicer_core.mt.js +0 -0
  20. package/engine/src/toggle_eval.js +78 -0
  21. package/package.json +74 -0
  22. package/viewer/README.md +68 -0
  23. package/viewer/dist/Viewport.js +1108 -0
  24. package/viewer/dist/make_worker.js +9 -0
  25. package/viewer/dist/model_loaders.js +144 -0
  26. package/viewer/dist/toolpath_gpu.js +386 -0
  27. package/viewer/src/Viewport.jsx +1353 -0
  28. package/viewer/src/assets/add.svg +1 -0
  29. package/viewer/src/assets/arrange.svg +1 -0
  30. package/viewer/src/assets/boolean.svg +1 -0
  31. package/viewer/src/assets/cut.svg +1 -0
  32. package/viewer/src/assets/delete.svg +1 -0
  33. package/viewer/src/assets/deleteall.svg +1 -0
  34. package/viewer/src/assets/duplicate.svg +1 -0
  35. package/viewer/src/assets/measure.svg +1 -0
  36. package/viewer/src/assets/mmu.svg +1 -0
  37. package/viewer/src/assets/move.svg +1 -0
  38. package/viewer/src/assets/negative.svg +1 -0
  39. package/viewer/src/assets/onbed.svg +1 -0
  40. package/viewer/src/assets/open.svg +1 -0
  41. package/viewer/src/assets/orient.svg +1 -0
  42. package/viewer/src/assets/paint.svg +1 -0
  43. package/viewer/src/assets/rotate.svg +1 -0
  44. package/viewer/src/assets/scale.svg +1 -0
  45. package/viewer/src/assets/seam.svg +1 -0
  46. package/viewer/src/assets/split.svg +1 -0
  47. package/viewer/src/assets/textmark.svg +1 -0
  48. package/viewer/src/assets/varlayer.svg +1 -0
  49. package/viewer/src/icons.js +23 -0
  50. package/viewer/src/make_worker.js +9 -0
  51. package/viewer/src/model_loaders.js +178 -0
  52. package/viewer/src/shadow_host.jsx +30 -0
  53. package/viewer/src/toolpath_gpu.js +430 -0
  54. package/viewer/styles.css +231 -0
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # three-slicer
2
+
3
+ Browser/WASM 3D-printing slicer, reverse-engineered from [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer).
4
+ Slices STL to G-code entirely in the browser (or Node) — no server.
5
+
6
+ - **Engine** — WASM kernel: Arachne variable-width walls, gyroid/honeycomb/crosshatch infill,
7
+ tree/grid supports, support painting, raft/brim/skirt, ironing, arc fitting, multi-material,
8
+ layer streaming (OOM-tolerant), multithreaded kernel auto-selected on cross-origin-isolated sites.
9
+ - **Viewer** — React `<Viewport/>`: model import (STL/OBJ/3MF/AMF/PLY), transform gizmos,
10
+ multi-plate, GPU toolpath preview (libvgcode port). Shadow-DOM isolated — no CSS collisions.
11
+ - **Settings UI** — React `<SettingsPanel/>`: 907 options driven by the extracted OrcaSlicer
12
+ config schema, with search, mode filters, and custom widget registry.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm i three-slicer # engine only (Node-safe)
18
+ npm i three-slicer react react-dom three # + viewer/components
19
+ ```
20
+
21
+ ## Use
22
+
23
+ ```js
24
+ // Headless slicing (Node or browser main thread)
25
+ import { createSlicer } from 'three-slicer'
26
+ const s = await createSlicer()
27
+ const { gcode, stats } = s.slice(stlArrayBuffer, params)
28
+
29
+ // React viewer + settings panel
30
+ import Viewport from 'three-slicer/viewer'
31
+ import SettingsPanel from 'three-slicer/components'
32
+ // <Viewport settings={settings} setSettings={setSettings} />
33
+
34
+ // Extracted data
35
+ import schema from 'three-slicer/data/config-schema.json'
36
+ ```
37
+
38
+ Vite consumers need `worker: { format: 'es' }` and `build: { target: 'es2022' }`.
39
+ Serve with COOP/COEP headers to enable the multithreaded kernel (~2.2× faster).
40
+
41
+ ## License
42
+
43
+ AGPL-3.0-or-later — derived from OrcaSlicer.
44
+ Source: https://github.com/kimgh06/Web_Three_Slicer
@@ -0,0 +1,39 @@
1
+ # three-slicer/components
2
+
3
+ Reusable React components for the browser slicer. Props-driven — no global state, no React context, no router coupling (proven standalone by the tarball consumer gate `web/pack_check.sh`).
4
+
5
+ ## `<SettingsPanel/>`
6
+
7
+ An OrcaSlicer-style settings form generated from `@three-slicer/data` (schema + UI tree + toggle rules): tab/page/group navigation, mode filter (simple/advanced/expert), search across 907 options, dirty markers with per-option reset, and enable/disable rules evaluation.
8
+
9
+ ```bash
10
+ npm i three-slicer/components react
11
+ ```
12
+
13
+ ```jsx
14
+ import { useState } from 'react'
15
+ import SettingsPanel from 'three-slicer/components'
16
+
17
+ function App() {
18
+ const [settings, setSettings] = useState({}) // sparse map: edited keys only; missing = schema default
19
+ return <SettingsPanel settings={settings} setSettings={setSettings} />
20
+ }
21
+ ```
22
+
23
+ Props:
24
+
25
+ | Prop | Type | Description |
26
+ |---|---|---|
27
+ | `settings` | `{ [schemaKey]: value }` | Sparse settings map — the consumer's state is the single source of truth |
28
+ | `setSettings` | `(updater) => void` | React setState-style updater; the only way state leaves the component |
29
+ | `onOptionOpen` | `(key) => void` (optional) | Label click hook for deep links/detail views. Omit → plain labels |
30
+
31
+ The same `settings` object feeds `deriveKernelParams()` in `three-slicer` and the `<Viewport/>` in `three-slicer/viewer` — one state connects all three.
32
+
33
+ Ships transpiled ESM (`dist/`); react is a peer dependency. Styling is class-based and unstyled by default (bring your own CSS, or reuse the demo's).
34
+
35
+ The 3D viewport lives in its own package: `three-slicer/viewer`.
36
+
37
+ ## License
38
+
39
+ AGPL-3.0-or-later (derived from OrcaSlicer).
@@ -0,0 +1,180 @@
1
+ import e, { useRef as R, useState as f, useLayoutEffect as F, useEffect as J, useMemo as z } from "react";
2
+ import { createPortal as O } from "react-dom";
3
+ import C from "three-slicer/data/config-schema.json";
4
+ import M from "three-slicer/data/ui-tree.json";
5
+ import { settingRaw as P } from "three-slicer/settings";
6
+ import { disabledKeys as I, makeCfg as B } from "three-slicer/toggle";
7
+ function D({ css: t, className: r, children: n }) {
8
+ const s = R(null), [l] = f(() => {
9
+ const d = document.createElement("div");
10
+ return d.style.display = "contents", d;
11
+ });
12
+ return F(() => {
13
+ const d = s.current, o = d.shadowRoot || d.attachShadow({ mode: "open" });
14
+ let i = o.querySelector("style[data-ts-css]");
15
+ i || (i = document.createElement("style"), i.setAttribute("data-ts-css", ""), o.append(i)), i.textContent = t, o.contains(l) || o.append(l);
16
+ }, [t, l]), /* @__PURE__ */ e.createElement("div", { ref: s, className: r }, O(n, l));
17
+ }
18
+ const L = ':host{display:contents}*{box-sizing:border-box}.row{display:grid;grid-template-columns:260px 220px 70px 1fr;gap:8px;align-items:center;padding:3px 0;border-bottom:1px dashed #f0f0f0}.row .lbl{color:#222;text-decoration:none}.row .lbl:hover{color:#0a7d4f;text-decoration:underline}.row .key{font-size:11px;color:#999;overflow:hidden;text-overflow:ellipsis}.row input[type=text],.row input:not([type]),.row select,.row textarea{width:100%;max-width:260px;padding:2px 6px}.inputwrap{display:inline-flex;align-items:center;gap:4px}.unit{color:#888;font-size:12px;white-space:nowrap}.pts{font-size:11px;color:#666}.badge{font-size:10px;padding:1px 6px;border-radius:8px;color:#fff;text-align:center}.mode-simple{background:#2e9e5b}.mode-advanced{background:#3673c3}.mode-expert{background:#d9822b}.mode-develop{background:#c0392b}.mode-none,.mode-undefined{background:#aaa}.muted{color:#999;font-weight:400;font-size:12px}.row.custom{grid-template-columns:1fr}.row.disabled{opacity:.42}.row.disabled input,.row.disabled select,.row.disabled textarea{cursor:not-allowed;background:#f2f2f2}.lbl-cell{display:inline-flex;align-items:center;gap:5px;min-width:0}.lbl-cell .lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dirty-dot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:#e8873a;box-shadow:0 0 0 2px #fff3e6}.widget-cell{display:inline-flex;align-items:center;gap:6px;min-width:0}.reset-btn{flex:0 0 auto;border:1px solid #d8b48a;background:#fbf1e4;color:#b5701f;border-radius:5px;cursor:pointer;font-size:13px;line-height:1;padding:1px 5px}.reset-btn:hover{background:#f3e2cb}.settings-panel{flex:0 0 400px;width:400px;border-left:1px solid #e2e2e2;display:flex;flex-direction:column;min-height:0;background:#fff;color:#222;font:14px/1.5 -apple-system,Segoe UI,sans-serif}.settings-panel .sp-top{padding:10px 12px 8px;border-bottom:1px solid #eee}.sp-builder{display:flex;align-items:center;gap:8px;font-size:12px;color:#666}.sp-builder select{flex:1;padding:4px 6px}.sp-pages{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}.sp-pages button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:4px 8px;font-size:12px;cursor:pointer}.sp-pages button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.sp-modes{display:flex;gap:4px;margin-top:8px}.sp-modes button{border:0;background:none;font-size:11px;color:#888;padding:2px 6px;border-radius:6px;cursor:pointer}.sp-modes button.on{background:#eef2f0;color:#0a7d4f;font-weight:700}.settings-panel .sp-body{flex:1 1 auto;overflow-y:auto;padding:8px 14px 24px}.settings-panel .sp-body h2{font-size:16px}.settings-panel .row{grid-template-columns:1fr auto;grid-template-areas:"lbl badge" "key widget";row-gap:2px;padding:6px 0}.settings-panel .row .lbl{grid-area:lbl;font-weight:600}.settings-panel .row .badge{grid-area:badge;justify-self:end}.settings-panel .row .key{grid-area:key}.settings-panel .row .inputwrap,.settings-panel .row>select,.settings-panel .row>input,.settings-panel .row>textarea,.settings-panel .row>.pts,.settings-panel .row>.muted{grid-area:widget;justify-self:end}.settings-panel .row input:not([type=checkbox]):not([type=color]),.settings-panel .row select,.settings-panel .row textarea{max-width:200px}.settings-panel .row.custom{grid-template-columns:1fr;grid-template-areas:"lbl"}.sp-search{width:100%;box-sizing:border-box;padding:6px 9px;border:1px solid #d6d6d6;border-radius:6px;font-size:13px;margin-bottom:8px}:host(.sp-embedded) .settings-panel{position:static;width:auto;flex:none;border:0;height:auto}:host(.sp-embedded) .sp-body{padding:4px 2px 8px;max-height:460px}.bse{display:flex;flex-direction:column;gap:6px;min-width:0;max-width:220px}.bse-modes{display:flex;gap:4px}.bse-modes button{border:1px solid #e0e0e0;background:#fafafa;border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer}.bse-modes button.on{background:#e6f5ee;color:#0a7d4f;border-color:#b7e2cd;font-weight:700}.bse-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 8px}.bse-grid label{display:flex;align-items:center;gap:4px;font-size:11px;color:#666;white-space:nowrap}.bse-grid input{width:56px;padding:2px 4px;font-size:12px}.bse-json{width:100%;font:11px ui-monospace,monospace;padding:4px 6px;border:1px solid #d6d6d6;border-radius:6px;box-sizing:border-box}.bse-json.err{border-color:#d0435a;background:#fdf3f4}', A = (t, r, n, s) => [[n, s], [t + n, s], [t + n, r + s], [n, r + s]], T = (t) => {
19
+ const r = t / 2, n = 32;
20
+ return Array.from({ length: n }, (s, l) => {
21
+ const d = 2 * Math.PI * l / n;
22
+ return [+(r + r * Math.cos(d)).toFixed(2), +(r + r * Math.sin(d)).toFixed(2)];
23
+ });
24
+ }, S = (t) => {
25
+ const r = t.map((s) => s[0]), n = t.map((s) => s[1]);
26
+ return { x0: Math.min(...r), y0: Math.min(...n), w: Math.max(...r) - Math.min(...r), d: Math.max(...n) - Math.min(...n) };
27
+ };
28
+ function $({ value: t, onChange: r, disabled: n }) {
29
+ const s = Array.isArray(t) && t.length >= 3 ? t : A(200, 200, 0, 0), l = S(s), [d, o] = f(s.length === 4 ? "rect" : s.length >= 12 ? "circle" : "custom"), [i, m] = f({ w: l.w, d: l.d, ox: l.x0, oy: l.y0 }), [h, u] = f(Math.round(l.w)), [c, x] = f(JSON.stringify(s)), [g, y] = f(!1);
30
+ J(() => {
31
+ const a = S(s);
32
+ m({ w: a.w, d: a.d, ox: a.x0, oy: a.y0 }), u(Math.round(a.w)), x(JSON.stringify(s)), y(!1);
33
+ }, [JSON.stringify(t)]);
34
+ const v = (a) => {
35
+ m(a);
36
+ const { w: p, d: b, ox: k, oy: _ } = a;
37
+ p > 0 && b > 0 && r(A(p, b, k, _));
38
+ }, w = (a) => {
39
+ u(a), a > 0 && r(T(a));
40
+ }, N = (a) => {
41
+ x(a);
42
+ try {
43
+ const p = JSON.parse(a);
44
+ if (!Array.isArray(p) || p.length < 3 || !p.every((b) => Array.isArray(b) && b.length === 2 && b.every(Number.isFinite))) throw 0;
45
+ y(!1), r(p);
46
+ } catch {
47
+ y(!0);
48
+ }
49
+ }, E = (a) => {
50
+ const p = parseFloat(a);
51
+ return Number.isFinite(p) ? p : 0;
52
+ };
53
+ return /* @__PURE__ */ e.createElement("div", { className: "bse", "data-testid": "bed-shape-editor" }, /* @__PURE__ */ e.createElement("div", { className: "bse-modes" }, [["rect", "사각형"], ["circle", "원형"], ["custom", "커스텀"]].map(([a, p]) => /* @__PURE__ */ e.createElement("button", { key: a, type: "button", className: d === a ? "on" : "", disabled: n, onClick: () => o(a) }, p))), d === "rect" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "폭 ", /* @__PURE__ */ e.createElement(
54
+ "input",
55
+ {
56
+ type: "number",
57
+ min: "1",
58
+ value: i.w,
59
+ disabled: n,
60
+ "data-testid": "bed-w",
61
+ onChange: (a) => v({ ...i, w: E(a.target.value) })
62
+ }
63
+ ), " mm"), /* @__PURE__ */ e.createElement("label", null, "깊이 ", /* @__PURE__ */ e.createElement(
64
+ "input",
65
+ {
66
+ type: "number",
67
+ min: "1",
68
+ value: i.d,
69
+ disabled: n,
70
+ "data-testid": "bed-d",
71
+ onChange: (a) => v({ ...i, d: E(a.target.value) })
72
+ }
73
+ ), " mm"), /* @__PURE__ */ e.createElement("label", null, "원점 X ", /* @__PURE__ */ e.createElement(
74
+ "input",
75
+ {
76
+ type: "number",
77
+ value: i.ox,
78
+ disabled: n,
79
+ onChange: (a) => v({ ...i, ox: E(a.target.value) })
80
+ }
81
+ )), /* @__PURE__ */ e.createElement("label", null, "원점 Y ", /* @__PURE__ */ e.createElement(
82
+ "input",
83
+ {
84
+ type: "number",
85
+ value: i.oy,
86
+ disabled: n,
87
+ onChange: (a) => v({ ...i, oy: E(a.target.value) })
88
+ }
89
+ ))), d === "circle" && /* @__PURE__ */ e.createElement("div", { className: "bse-grid" }, /* @__PURE__ */ e.createElement("label", null, "지름 ", /* @__PURE__ */ e.createElement(
90
+ "input",
91
+ {
92
+ type: "number",
93
+ min: "1",
94
+ value: h,
95
+ disabled: n,
96
+ "data-testid": "bed-dia",
97
+ onChange: (a) => w(E(a.target.value))
98
+ }
99
+ ), " mm")), d === "custom" && /* @__PURE__ */ e.createElement(
100
+ "textarea",
101
+ {
102
+ className: "bse-json" + (g ? " err" : ""),
103
+ rows: 3,
104
+ value: c,
105
+ disabled: n,
106
+ "data-testid": "bed-json",
107
+ onChange: (a) => N(a.target.value),
108
+ placeholder: "[[0,0],[200,0],[200,200],[0,200]]"
109
+ }
110
+ ));
111
+ }
112
+ const U = {
113
+ printable_area: $
114
+ }, j = Object.keys(M).filter((t) => M[t].length > 0), q = ["all", "simple", "advanced", "expert", "develop"], G = (t) => t.replace("::build", "").replace("Tab", "");
115
+ function H(t, r) {
116
+ var l;
117
+ if (!t || !(r in t)) return !1;
118
+ const n = (l = C[r]) == null ? void 0 : l.default, s = Array.isArray(n) ? n[0] : n;
119
+ return String(t[r]) !== String(s ?? "");
120
+ }
121
+ function Q(t, r) {
122
+ var s;
123
+ if (r === "all" || t.startsWith("<")) return !0;
124
+ const n = (s = C[t]) == null ? void 0 : s.mode;
125
+ return r === "simple" ? n === "simple" : r === "advanced" ? n === "simple" || n === "advanced" : !0;
126
+ }
127
+ function W(t) {
128
+ return t ? t.gui_type === "color" ? "color" : t.enum_values ? "select" : t.type === "coBool" || t.type === "coBools" ? "checkbox" : t.multiline || t.is_code ? "textarea" : t.type === "coPoints" || t.type === "coPoint" ? "points" : "input" : "unknown";
129
+ }
130
+ function X({ mode: t }) {
131
+ return /* @__PURE__ */ e.createElement("span", { className: `badge mode-${t ?? "none"}` }, t ?? "—");
132
+ }
133
+ function Y({ def: t, optKey: r, settings: n, setSettings: s, disabled: l, customWidgets: d }) {
134
+ const o = P(n, r), i = (c) => s((x) => ({ ...x, [r]: c })), m = (d == null ? void 0 : d[r]) ?? U[r];
135
+ if (m) return /* @__PURE__ */ e.createElement(m, { def: t, value: o, onChange: i, disabled: l });
136
+ const h = W(t), u = Array.isArray(o) ? o[0] : o;
137
+ switch (h) {
138
+ case "select":
139
+ return /* @__PURE__ */ e.createElement("select", { value: u ?? "", onChange: (c) => i(c.target.value), disabled: l }, t.enum_values.map((c, x) => {
140
+ var g;
141
+ return /* @__PURE__ */ e.createElement("option", { key: c, value: c }, ((g = t.enum_labels) == null ? void 0 : g[x]) ?? c);
142
+ }));
143
+ case "checkbox":
144
+ return /* @__PURE__ */ e.createElement("input", { type: "checkbox", checked: Array.isArray(o) ? !!o[0] : !!o, onChange: (c) => i(c.target.checked), disabled: l });
145
+ case "color":
146
+ return /* @__PURE__ */ e.createElement("input", { type: "color", value: typeof u == "string" && u.startsWith("#") ? u : "#00ae42", onChange: (c) => i(c.target.value), disabled: l });
147
+ case "textarea":
148
+ return /* @__PURE__ */ e.createElement("textarea", { rows: 2, value: Array.isArray(o) ? o.join(`
149
+ `) : o ?? "", onChange: (c) => i(c.target.value), disabled: l });
150
+ case "points":
151
+ return /* @__PURE__ */ e.createElement("code", { className: "pts" }, JSON.stringify(o ?? []));
152
+ case "unknown":
153
+ return /* @__PURE__ */ e.createElement("span", { className: "muted" }, "스키마 없음");
154
+ default:
155
+ return /* @__PURE__ */ e.createElement("span", { className: "inputwrap" }, /* @__PURE__ */ e.createElement("input", { value: u ?? "", onChange: (c) => i(c.target.value), disabled: l }), t.sidetext && /* @__PURE__ */ e.createElement("span", { className: "unit" }, t.sidetext));
156
+ }
157
+ }
158
+ function V({ optKey: t, settings: r, setSettings: n, disabled: s, onOptionOpen: l, customWidgets: d }) {
159
+ if (t.startsWith("<")) return /* @__PURE__ */ e.createElement("div", { className: "row custom" }, /* @__PURE__ */ e.createElement("span", { className: "muted" }, "⚙ 커스텀 위젯 ", t));
160
+ const o = C[t], i = s ? s[t] : void 0, m = i != null, h = H(r, t), u = () => n((x) => {
161
+ const g = { ...x };
162
+ return delete g[t], g;
163
+ }), c = (o == null ? void 0 : o.label) || (o == null ? void 0 : o.full_label) || t;
164
+ return /* @__PURE__ */ e.createElement("div", { className: "row" + (m ? " disabled" : ""), title: m ? `비활성 조건: ${i}` : (o == null ? void 0 : o.tooltip) ?? "", "data-testid": `row-${t}` }, /* @__PURE__ */ e.createElement("span", { className: "lbl-cell" }, h && /* @__PURE__ */ e.createElement("span", { className: "dirty-dot", title: "기본값에서 변경됨", "data-testid": `dirty-${t}` }), l ? /* @__PURE__ */ e.createElement("button", { type: "button", className: "lbl lbl-link", onClick: () => l(t), title: "상세" }, c) : /* @__PURE__ */ e.createElement("span", { className: "lbl" }, c)), /* @__PURE__ */ e.createElement("code", { className: "key" }, t), /* @__PURE__ */ e.createElement(X, { mode: o == null ? void 0 : o.mode }), /* @__PURE__ */ e.createElement("span", { className: "widget-cell" }, /* @__PURE__ */ e.createElement(Y, { def: o, optKey: t, settings: r, setSettings: n, disabled: m, customWidgets: d }), h && /* @__PURE__ */ e.createElement("button", { className: "reset-btn", onClick: u, title: "기본값으로 리셋", "data-testid": `reset-${t}` }, "↺")));
165
+ }
166
+ function re({ settings: t, setSettings: r, onOptionOpen: n, embedded: s = !1, customWidgets: l }) {
167
+ const [d, o] = f(j[0] ?? ""), [i, m] = f(0), [h, u] = f("all"), [c, x] = f(""), g = M[d] ?? [], y = g[Math.min(i, g.length - 1)], v = z(() => I(B(t)), [t]), w = c.trim().toLowerCase(), N = z(() => w ? Object.entries(C).filter(([a, p]) => {
168
+ var b, k;
169
+ return a.includes(w) || ((b = p.label) == null ? void 0 : b.toLowerCase().includes(w)) || ((k = p.full_label) == null ? void 0 : k.toLowerCase().includes(w));
170
+ }).slice(0, 60).map(([a]) => a) : [], [w]), E = (a, p) => /* @__PURE__ */ e.createElement(V, { key: a + p, optKey: a, settings: t, setSettings: r, disabled: v, onOptionOpen: n, customWidgets: l });
171
+ return /* @__PURE__ */ e.createElement(D, { css: L, className: s ? "sp-embedded" : void 0 }, /* @__PURE__ */ e.createElement("div", { className: "settings-panel" }, /* @__PURE__ */ e.createElement("div", { className: "sp-top" }, /* @__PURE__ */ e.createElement("input", { className: "sp-search", placeholder: "옵션 검색 (907개)…", value: c, onChange: (a) => x(a.target.value), "data-testid": "opt-search" }), !w && /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement("label", { className: "sp-builder" }, /* @__PURE__ */ e.createElement("span", null, "설정 그룹"), /* @__PURE__ */ e.createElement("select", { value: d, onChange: (a) => {
172
+ o(a.target.value), m(0);
173
+ } }, j.map((a) => /* @__PURE__ */ e.createElement("option", { key: a, value: a }, G(a))))), /* @__PURE__ */ e.createElement("div", { className: "sp-pages" }, g.map((a, p) => /* @__PURE__ */ e.createElement("button", { key: a.page + p, className: p === Math.min(i, g.length - 1) ? "on" : "", onClick: () => m(p) }, a.page))), /* @__PURE__ */ e.createElement("div", { className: "sp-modes" }, q.map((a) => /* @__PURE__ */ e.createElement("button", { key: a, className: h === a ? "on" : "", onClick: () => u(a) }, a))))), /* @__PURE__ */ e.createElement("div", { className: "sp-body" }, w ? /* @__PURE__ */ e.createElement("section", { "data-testid": "search-results" }, /* @__PURE__ */ e.createElement("h3", null, "검색: “", c, "” — ", N.length, "건", N.length === 60 ? "+" : ""), N.map(E), !N.length && /* @__PURE__ */ e.createElement("p", { className: "muted" }, "일치 없음")) : y ? /* @__PURE__ */ e.createElement(e.Fragment, null, /* @__PURE__ */ e.createElement("h2", null, y.page, " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, "Tab.cpp:", y.line)), y.groups.map((a) => {
174
+ const p = a.options.filter((b) => Q(b, h));
175
+ return p.length ? /* @__PURE__ */ e.createElement("section", { key: a.group + a.line }, /* @__PURE__ */ e.createElement("h3", null, a.group || "(무제 그룹)", " ", /* @__PURE__ */ e.createElement("span", { className: "muted" }, ":", a.line)), p.map(E)) : null;
176
+ })) : /* @__PURE__ */ e.createElement("p", { className: "muted" }, "빌더 없음"))));
177
+ }
178
+ export {
179
+ re as default
180
+ };
@@ -0,0 +1,84 @@
1
+ /* @three-slicer/components — SettingsPanel 스타일.
2
+ Shadow DOM 격리(shadow_host.jsx): ?inline 으로 번들에 내장돼 패널의 shadow root 에
3
+ 주입된다. 호스트 앱 CSS 와 양방향 무간섭 — 클래스명 충돌이 불가능하다. */
4
+ :host { display: contents; }
5
+ * { box-sizing: border-box; }
6
+
7
+ /* ---- 옵션 행 원자 ---- */
8
+ .row { display: grid; grid-template-columns: 260px 220px 70px 1fr; gap: 8px;
9
+ align-items: center; padding: 3px 0; border-bottom: 1px dashed #f0f0f0; }
10
+ .row .lbl { color: #222; text-decoration: none; }
11
+ .row .lbl:hover { color: #0a7d4f; text-decoration: underline; }
12
+ .row .key { font-size: 11px; color: #999; overflow: hidden; text-overflow: ellipsis; }
13
+ .row input[type="text"], .row input:not([type]), .row select, .row textarea {
14
+ width: 100%; max-width: 260px; padding: 2px 6px; }
15
+ .inputwrap { display: inline-flex; align-items: center; gap: 4px; }
16
+ .unit { color: #888; font-size: 12px; white-space: nowrap; }
17
+ .pts { font-size: 11px; color: #666; }
18
+
19
+ .badge { font-size: 10px; padding: 1px 6px; border-radius: 8px; color: #fff; text-align: center; }
20
+ .mode-simple { background: #2e9e5b; } .mode-advanced { background: #3673c3; }
21
+ .mode-expert { background: #d9822b; } .mode-develop { background: #c0392b; }
22
+ .mode-none, .mode-undefined { background: #aaa; }
23
+ .muted { color: #999; font-weight: 400; font-size: 12px; }
24
+ .row.custom { grid-template-columns: 1fr; }
25
+
26
+ /* 25단계 S5: toggle-rules 비활성 + dirty/리셋 */
27
+ .row.disabled { opacity: 0.42; }
28
+ .row.disabled input, .row.disabled select, .row.disabled textarea { cursor: not-allowed; background: #f2f2f2; }
29
+ .lbl-cell { display: inline-flex; align-items: center; gap: 5px; min-width: 0; }
30
+ .lbl-cell .lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
31
+ .dirty-dot { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%; background: #e8873a; box-shadow: 0 0 0 2px #fff3e6; }
32
+ .widget-cell { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
33
+ .reset-btn { flex: 0 0 auto; border: 1px solid #d8b48a; background: #fbf1e4; color: #b5701f;
34
+ border-radius: 5px; cursor: pointer; font-size: 13px; line-height: 1; padding: 1px 5px; }
35
+ .reset-btn:hover { background: #f3e2cb; }
36
+
37
+ /* ---- 패널 셸 ---- */
38
+ .settings-panel { flex: 0 0 400px; width: 400px; border-left: 1px solid #e2e2e2;
39
+ display: flex; flex-direction: column; min-height: 0; background: #fff; color: #222;
40
+ font: 14px/1.5 -apple-system, "Segoe UI", sans-serif; }
41
+ .settings-panel .sp-top { padding: 10px 12px 8px; border-bottom: 1px solid #eee; }
42
+ .sp-builder { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #666; }
43
+ .sp-builder select { flex: 1; padding: 4px 6px; }
44
+ .sp-pages { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
45
+ .sp-pages button { border: 1px solid #e0e0e0; background: #fafafa; border-radius: 6px;
46
+ padding: 4px 8px; font-size: 12px; cursor: pointer; }
47
+ .sp-pages button.on { background: #e6f5ee; color: #0a7d4f; border-color: #b7e2cd; font-weight: 700; }
48
+ .sp-modes { display: flex; gap: 4px; margin-top: 8px; }
49
+ .sp-modes button { border: 0; background: none; font-size: 11px; color: #888;
50
+ padding: 2px 6px; border-radius: 6px; cursor: pointer; }
51
+ .sp-modes button.on { background: #eef2f0; color: #0a7d4f; font-weight: 700; }
52
+ .settings-panel .sp-body { flex: 1 1 auto; overflow-y: auto; padding: 8px 14px 24px; }
53
+ .settings-panel .sp-body h2 { font-size: 16px; }
54
+
55
+ /* 사이드바 폭이 좁으므로 옵션 행을 세로 스택으로 재배치 */
56
+ .settings-panel .row { grid-template-columns: 1fr auto; grid-template-areas: "lbl badge" "key widget";
57
+ row-gap: 2px; padding: 6px 0; }
58
+ .settings-panel .row .lbl { grid-area: lbl; font-weight: 600; }
59
+ .settings-panel .row .badge { grid-area: badge; justify-self: end; }
60
+ .settings-panel .row .key { grid-area: key; }
61
+ .settings-panel .row .inputwrap, .settings-panel .row > select,
62
+ .settings-panel .row > input, .settings-panel .row > textarea,
63
+ .settings-panel .row > .pts, .settings-panel .row > .muted { grid-area: widget; justify-self: end; }
64
+ .settings-panel .row input:not([type="checkbox"]):not([type="color"]),
65
+ .settings-panel .row select, .settings-panel .row textarea { max-width: 200px; }
66
+ .settings-panel .row.custom { grid-template-columns: 1fr; grid-template-areas: "lbl"; }
67
+ .sp-search { width: 100%; box-sizing: border-box; padding: 6px 9px; border: 1px solid #d6d6d6; border-radius: 6px; font-size: 13px; margin-bottom: 8px; }
68
+
69
+ /* ---- 임베드 변형 (:host 에 sp-embedded — viewer process-card 등 카드 안 전폭 배치) ---- */
70
+ :host(.sp-embedded) .settings-panel { position: static; width: auto; flex: none; border: 0; height: auto; }
71
+ :host(.sp-embedded) .sp-body { padding: 4px 2px 8px; max-height: 460px; }
72
+
73
+ /* ---- BedShapeEditor (printable_area 커스텀 위젯) ---- */
74
+ .bse { display: flex; flex-direction: column; gap: 6px; min-width: 0; max-width: 220px; }
75
+ .bse-modes { display: flex; gap: 4px; }
76
+ .bse-modes button { border: 1px solid #e0e0e0; background: #fafafa; border-radius: 6px;
77
+ padding: 2px 8px; font-size: 11px; cursor: pointer; }
78
+ .bse-modes button.on { background: #e6f5ee; color: #0a7d4f; border-color: #b7e2cd; font-weight: 700; }
79
+ .bse-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 8px; }
80
+ .bse-grid label { display: flex; align-items: center; gap: 4px; font-size: 11px; color: #666; white-space: nowrap; }
81
+ .bse-grid input { width: 56px; padding: 2px 4px; font-size: 12px; }
82
+ .bse-json { width: 100%; font: 11px ui-monospace, monospace; padding: 4px 6px;
83
+ border: 1px solid #d6d6d6; border-radius: 6px; box-sizing: border-box; }
84
+ .bse-json.err { border-color: #d0435a; background: #fdf3f4; }
package/data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # @three-slicer/data
2
+
3
+ OrcaSlicer configuration metadata extracted from the original C++ sources, as plain JSON. Used by `three-slicer` (defaults, settings mapping) and `three-slicer/components` (settings UI), and usable standalone for building slicer UIs.
4
+
5
+ | File | Contents |
6
+ |---|---|
7
+ | `config-schema.json` | 907 print options: type, label, category, tooltip, min/max, mode, enum values/labels, defaults, source line numbers |
8
+ | `config-schema-builddump.json` | Build-verified cross-check: the real `print_config_def` (817 options) dumped from a WASM compile of the original `PrintConfig.cpp` |
9
+ | `ui-tree.json` | Tab → page → group → option tree from `Tab.cpp` (34 pages, 587 option references) |
10
+ | `toggle-rules.json` | 231 enable/disable rules with original C++ `enable_if` condition source |
11
+ | `invalidation-map.json` | Option-change → re-slice step mapping (Print/PrintObject invalidation branches) |
12
+
13
+ ```js
14
+ import schema from 'three-slicer/data/config-schema.json'
15
+ schema.layer_height.default // 0.2
16
+ ```
17
+
18
+ Regenerated by `web/extract_all.py` from the upstream source tree (see repository).
19
+
20
+ ## License
21
+
22
+ AGPL-3.0-or-later (derived from OrcaSlicer).