three-slicer 0.3.4 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-10-24
4
+
5
+ ### Removed (breaking)
6
+
7
+ - `three-slicer/viewer`, `three-slicer/viewer/toolpath`, `three-slicer/viewer/loaders`, `three-slicer/viewer/gcode`
8
+ and `three-slicer/components`. They were wrappers over `three-slicer-viewer` kept for the pre-split import paths.
9
+ `three-slicer` now contains no React code and no longer declares `react`, `react-dom` or `three` as peers.
10
+ Migration: import the components from `three-slicer-viewer` (list it in your own dependencies at the same
11
+ version as `three-slicer`) and pass the kernel and upstream's data as props:
12
+
13
+ ```jsx
14
+ import Viewport, { useSlicer } from 'three-slicer-viewer'
15
+ import SettingsPanel from 'three-slicer-viewer/components'
16
+ import { makeSlicerWorker } from 'three-slicer/client'
17
+ import { bundledCatalog } from 'three-slicer/settings'
18
+ import { schema, uiTree } from 'three-slicer/data'
19
+ import { makeCfg, disabledKeys } from 'three-slicer/toggle'
20
+
21
+ const slicer = (deps) => useSlicer({ ...deps, makeWorker: makeSlicerWorker }) // module-level: it is a hook
22
+ const toggle = { makeCfg, disabledKeys }
23
+
24
+ <Viewport slicer={slicer} catalog={bundledCatalog} settings={settings} setSettings={setSettings} />
25
+ <SettingsPanel schema={schema} uiTree={uiTree} toggle={toggle} settings={settings} setSettings={setSettings} />
26
+ ```
27
+
28
+ The subpaths `/viewer/toolpath`, `/viewer/loaders` and `/viewer/gcode` are `three-slicer-viewer/toolpath`,
29
+ `/loaders` and `/gcode`, unchanged.
30
+
31
+ ## 0.3.5 — 2026-10-16
32
+
33
+ ### Fixed
34
+
35
+ - The real wipe tower (`wipe_tower_real: true`) read past the end of its filament table on a tool change to or from
36
+ T2 or above (#69). The kernel registered filaments for tools 0 and 1 only, so the third tool's wipe moves took
37
+ their feedrate from whatever memory followed: `F0` or a value that changed from one slice to the next, and the
38
+ toolchange comment named no material. Every tool the change involves is now registered. On a three-extruder
39
+ slice the wipe moves went from 114 `F0` lines to none, and three runs give the same G-code. Each block now has the
40
+ shape the two-extruder tower always had. The default tower (the ring) and two-extruder slices are unchanged.
41
+ `wipe_tower_real` still defaults to `false`.
42
+
43
+ ### Changed
44
+
45
+ - The kernel's own C++ and its tests were rewritten for readability: abbreviated names written out, one statement
46
+ per line, no ternaries, and one shared module for the test meshes. The kernel's output is unchanged, checked with
47
+ the golden G-code dump and a per-call hash of every kernel result in the test suite. Parameter keys, `stats`
48
+ fields and the worker protocol are unchanged.
49
+
3
50
  ## 0.3.4 — 2026-10-11
4
51
 
5
52
  ### Added
package/README.md CHANGED
@@ -1,17 +1,16 @@
1
1
  # three-slicer
2
2
 
3
- Browser/WASM 3D-printing slicer package derived from [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer). It can slice binary STL files to G-code in Node or the browser, and it also ships React UI pieces for building a browser slicer: a three.js viewport, GPU toolpath preview, and a schema-driven settings panel. SLA (resin) printers are a second technology in the same kernel — supports and pad from PrusaSlicer's ported chain, previewed in the same viewer, exported as an `.sl1` archive.
3
+ Browser/WASM 3D-printing slicer package derived from [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer). It can slice binary STL files to G-code in Node or the browser, and it supplies the kernel worker, OrcaSlicer's vendor presets and its settings schema to the React viewer and settings panel in [`three-slicer-viewer`](https://www.npmjs.com/package/three-slicer-viewer). SLA (resin) printers are a second technology in the same kernel — supports and pad from PrusaSlicer's ported chain, previewed in the same viewer, exported as an `.sl1` archive.
4
4
 
5
- The package is published as a single npm package, `three-slicer`, with subpath exports for the engine, viewer, components, worker, and extracted OrcaSlicer data.
5
+ The package is published as a single npm package, `three-slicer`, with subpath exports for the engine, settings, worker, client, and extracted OrcaSlicer data.
6
6
 
7
7
  ![A sliced Benchy in the viewer's Preview tab — organic tree supports, per-feature toolpath colors, dual layer-range slider, filament and print-time estimates](https://raw.githubusercontent.com/kimgh06/Three_Slicer/main/web/viewer/public/usage.png)
8
8
 
9
9
  ## Links
10
10
 
11
11
  - Permissive viewer: [`three-slicer-viewer`](https://www.npmjs.com/package/three-slicer-viewer) (MIT) — the
12
- viewer, the settings form, G-code parsing and toolpath rendering without the slicer. This package plugs the
13
- kernel and OrcaSlicer's vendor presets into it; `three-slicer/viewer` and `three-slicer/components` are that
14
- composition. Which code carries which licence, and why: [`PROVENANCE.md`](PROVENANCE.md).
12
+ viewer, the settings form, G-code parsing and toolpath rendering without the slicer. A host plugs this
13
+ package's kernel worker and OrcaSlicer's vendor presets into it (see the React quick starts below). Which code carries which licence, and why: [`PROVENANCE.md`](PROVENANCE.md).
15
14
 
16
15
  - Package: [npmjs.com/package/three-slicer](https://www.npmjs.com/package/three-slicer)
17
16
  - Source: [kimgh06/Three_Slicer](https://github.com/kimgh06/Three_Slicer)
@@ -26,7 +25,7 @@ reading whole; everything else is lookup.
26
25
  | --- | --- | --- | --- |
27
26
  | "Just the numbers" — quotes, farms, automation. No UI, no React | **Headless** | [Quick Start: Headless Slicing](#quick-start-headless-slicing) | [engine/README](engine/README.md) (134 lines), params via [PARAMS.md](engine/PARAMS.md) as lookup |
28
27
  | A slicer inside your existing page | **Embed** | [Quick Start: React Viewer](#quick-start-react-viewer) | [viewer/README](../viewer-package/VIEWER.md) (330 lines — the props contracts live there) |
29
- | The full slicer UI — viewport + settings panel | **Full UI** | [Full React Example](#full-react-example) | viewer/README + [components/README](components/README.md) |
28
+ | The full slicer UI — viewport + settings panel | **Full UI** | [Full React Example](#full-react-example) | viewer/README + [COMPONENTS.md](../viewer-package/COMPONENTS.md) |
30
29
 
31
30
  Whichever path: read [Before You Ship](#before-you-ship) before deploying — those traps return plausible
32
31
  results instead of errors, and the first two never show up in `npm run dev`.
@@ -53,19 +52,21 @@ results instead of errors, and the first two never show up in `npm run dev`.
53
52
  npm i three-slicer
54
53
  ```
55
54
 
56
- `react`, `react-dom`, and `three` are **optional** peer dependencies. They stay peers rather than dependencies because React and three must be single instances — a duplicate copy breaks hooks and `instanceof` — and optional because only the UI half needs them: a headless consumer installs none of the three.
57
-
58
- Install them yourself when you use the viewer or the settings panel:
55
+ Nothing in `three-slicer` needs React or three.js; a headless consumer installs neither. The React viewer and the
56
+ settings panel are [`three-slicer-viewer`](https://www.npmjs.com/package/three-slicer-viewer), which `three-slicer`
57
+ depends on at the same exact version. Import it by its own name, so list it in your own `package.json` at that
58
+ version, together with its optional peers:
59
59
 
60
60
  ```bash
61
- npm i three-slicer react react-dom three
61
+ npm i three-slicer three-slicer-viewer@<same version> react react-dom three
62
62
  ```
63
63
 
64
64
  | Import path | Needs react/three |
65
65
  | --- | --- |
66
- | `three-slicer`, `/settings`, `/toggle`, `/client`, `/worker`, `/wasm`, `/data`, `/viewer/gcode`, `/viewer/toolpath` | no — `makeToolpath(THREE, data)` takes `three` as an argument |
67
- | `three-slicer/viewer`, `/viewer/loaders` | yes |
68
- | `three-slicer/components` | react only |
66
+ | `three-slicer`, `/settings`, `/toggle`, `/client`, `/worker`, `/wasm`, `/data` | no |
67
+ | `three-slicer-viewer`, `/loaders` | yes |
68
+ | `three-slicer-viewer/components` | react only |
69
+ | `three-slicer-viewer/gcode`, `/toolpath` | no — `makeToolpath(THREE, data)` takes `three` as an argument |
69
70
 
70
71
  ## Quick Start: Headless Slicing
71
72
 
@@ -147,11 +148,19 @@ When `onLayer` is set, the returned result carries stats; assemble G-code from t
147
148
 
148
149
  ## Quick Start: React Viewer
149
150
 
150
- Use `three-slicer/viewer` when you want a ready-made browser slicer viewport.
151
+ Use `three-slicer-viewer`'s `<Viewport/>` with this package's kernel worker and vendor presets plugged in when you
152
+ want a ready-made browser slicer viewport. Without the `slicer` and `catalog` props it loads, displays and exports,
153
+ but never starts a kernel and offers no presets.
151
154
 
152
155
  ```jsx
153
156
  import { useState } from 'react'
154
- import Viewport from 'three-slicer/viewer'
157
+ import Viewport, { useSlicer } from 'three-slicer-viewer'
158
+ import { makeSlicerWorker } from 'three-slicer/client'
159
+ import { bundledCatalog } from 'three-slicer/settings'
160
+
161
+ // The slicing hook bound to the WASM kernel's worker. Module-level: it is called as a hook, so its identity
162
+ // must not change between renders.
163
+ const slicer = (deps) => useSlicer({ ...deps, makeWorker: makeSlicerWorker })
155
164
 
156
165
  export default function App() {
157
166
  const [settings, setSettings] = useState({})
@@ -159,6 +168,8 @@ export default function App() {
159
168
  return (
160
169
  <div style={{ position: 'relative', width: '100%', height: '100vh' }}>
161
170
  <Viewport
171
+ slicer={slicer}
172
+ catalog={bundledCatalog}
162
173
  settings={settings}
163
174
  setSettings={setSettings}
164
175
  />
@@ -176,11 +187,18 @@ The viewer handles model loading, drag and drop, transform controls, multi-plate
176
187
 
177
188
  ## Quick Start: Settings Panel
178
189
 
179
- Use `three-slicer/components` when you want the OrcaSlicer-style settings UI without building the form yourself.
190
+ Use `three-slicer-viewer/components`' `<SettingsPanel/>` with this package's schema, tab tree and toggle rules
191
+ plugged in when you want the OrcaSlicer-style settings UI without building the form yourself. Without them it labels
192
+ every field by its key and disables nothing.
180
193
 
181
194
  ```jsx
182
195
  import { useState } from 'react'
183
- import SettingsPanel from 'three-slicer/components'
196
+ import SettingsPanelCore from 'three-slicer-viewer/components'
197
+ import { schema, uiTree } from 'three-slicer/data'
198
+ import { makeCfg, disabledKeys } from 'three-slicer/toggle'
199
+
200
+ const toggle = { makeCfg, disabledKeys }
201
+ const SettingsPanel = (props) => <SettingsPanelCore schema={schema} uiTree={uiTree} toggle={toggle} {...props} />
184
202
 
185
203
  export default function App() {
186
204
  const [settings, setSettings] = useState({})
@@ -202,8 +220,19 @@ The viewer and settings panel are designed to share the same `settings` state.
202
220
 
203
221
  ```jsx
204
222
  import { useState } from 'react'
205
- import Viewport from 'three-slicer/viewer'
206
- import SettingsPanel from 'three-slicer/components'
223
+ import Viewport, { useSlicer } from 'three-slicer-viewer'
224
+ import { makeSlicerWorker } from 'three-slicer/client'
225
+ import { bundledCatalog } from 'three-slicer/settings'
226
+
227
+ // The slicing hook bound to the WASM kernel's worker. Module-level: it is called as a hook, so its identity
228
+ // must not change between renders.
229
+ const slicer = (deps) => useSlicer({ ...deps, makeWorker: makeSlicerWorker })
230
+ import SettingsPanelCore from 'three-slicer-viewer/components'
231
+ import { schema, uiTree } from 'three-slicer/data'
232
+ import { makeCfg, disabledKeys } from 'three-slicer/toggle'
233
+
234
+ const toggle = { makeCfg, disabledKeys }
235
+ const SettingsPanel = (props) => <SettingsPanelCore schema={schema} uiTree={uiTree} toggle={toggle} {...props} />
207
236
 
208
237
  export default function App() {
209
238
  const [settings, setSettings] = useState({})
@@ -211,6 +240,8 @@ export default function App() {
211
240
  return (
212
241
  <main style={{ display: 'flex', height: '100vh' }}>
213
242
  <Viewport
243
+ slicer={slicer}
244
+ catalog={bundledCatalog}
214
245
  settings={settings}
215
246
  setSettings={setSettings}
216
247
  />
@@ -231,8 +262,7 @@ The same sparse settings map can feed the settings panel, the viewer, and `deriv
231
262
  import { createSlicer, engineWorkerURL } from 'three-slicer'
232
263
  import { deriveKernelParams } from 'three-slicer/settings'
233
264
  import { disabledKeys, makeCfg } from 'three-slicer/toggle'
234
- import Viewport from 'three-slicer/viewer'
235
- import SettingsPanel from 'three-slicer/components'
265
+ import { makeSlicerWorker } from 'three-slicer/client'
236
266
  import { schema, uiTree } from 'three-slicer/data'
237
267
  ```
238
268
 
@@ -247,40 +277,33 @@ Available subpaths:
247
277
  | `three-slicer/client` | `createSlicerClient()` — promises over that protocol |
248
278
  | `three-slicer/wasm` | Single-threaded Emscripten WASM glue |
249
279
  | `three-slicer/wasm-mt` | Multithreaded Emscripten WASM glue |
250
- | `three-slicer/viewer` | React `<Viewport/>` |
251
- | `three-slicer/viewer/toolpath` | GPU toolpath renderer utilities |
252
- | `three-slicer/viewer/loaders` | Model loaders |
253
- | `three-slicer/viewer/gcode` | `parseGcode(text)` — G-code back into the layer stream the renderer consumes |
254
- | `three-slicer/components` | React `<SettingsPanel/>` |
255
280
  | `three-slicer/data` | Named exports for extracted metadata |
256
281
  | `three-slicer/data/*.json` | Extracted OrcaSlicer metadata |
257
282
 
258
283
  ## Architecture: How the Pieces Connect
259
284
 
260
- One npm package, five modules. The dependency direction is one-way: `wasm-core` →(build)→ `engine`
261
- ←(data)← `data`, with `components` and `viewer` consuming different surfaces of the engine, `types`
262
- generating and gating the contract from the side — and the glue at the top is always a single sparse
263
- settings map held by the host app.
285
+ One npm package, four modules, beside the separately published React half (`three-slicer-viewer`). The dependency
286
+ direction is one-way: `wasm-core` →(build)→ `engine` ←(data)← `data`, with `types` generating and gating the
287
+ contract from the side. The host composes the React half with the kernel worker, the vendor catalog and the schema —
288
+ and the glue at the top is always a single sparse settings map held by the host app.
264
289
 
265
290
  | Module | Role | Published as | Depends on | Consumed by |
266
291
  | --- | --- | --- | --- | --- |
267
292
  | `wasm-core` | C++ kernel sources + vendored third_party | not published | (ported upstream sources) | `engine` — as build output only |
268
- | `engine` | kernel SDK: direct handle, worker, client, settings derivation | `three-slicer` root + `/settings` `/toggle` `/client` `/worker` `/wasm(-mt)` `/data` | committed wasm-core output, `data`, the generated `kernel_setting_keys.js` | `components`, `viewer`, every consumer |
269
- | `data` | extracted schema + catalogs (config-schema, printers, processes, filaments…) | `/data` | `extract_all.py` ← upstream | `engine`'s settings, `components`, `viewer` |
293
+ | `engine` | kernel SDK: direct handle, worker, client, the vendor catalog; re-exports the settings derivation and toggle evaluator from `three-slicer-viewer` | `three-slicer` root + `/settings` `/toggle` `/client` `/worker` `/wasm(-mt)` `/data` | committed wasm-core output, `data`, `three-slicer-viewer`'s `/settings` `/toggle` `/paint` | host apps, every consumer |
294
+ | `data` | extracted schema + catalogs (config-schema, printers, processes, filaments…) | `/data` | `extract_all.py` ← upstream | `engine`'s settings, host apps (passed to the React half as props) |
270
295
  | `types` | every `.d.ts` + two generators | attached per subpath via the exports map's `types` fields | runs `engine` at generation time (probing), reads `data`'s schema | consumers' IDE/TS — no runtime dependency |
271
- | `components` | `<SettingsPanel/>` (Shadow DOM) | `/components` | `data` (schema + UI tree), `engine`'s `/toggle` | host apps |
272
- | `viewer` | `<Viewport/>` (three.js, Shadow DOM) | `/viewer` + `/viewer/toolpath` `/loaders` `/gcode` | `engine`'s `/settings`, the worker, `/data`; `three` (peer) | host apps |
273
296
 
274
297
  Some of the connections are not imports, and knowing which is which explains most of the layout:
275
298
 
276
299
  | Connection | Mechanism | Notes |
277
300
  | --- | --- | --- |
278
301
  | `wasm-core` → `engine` | `build.sh` (emscripten) commits its output into `engine/src/` | not an import — the golden byte-identical gate and the invariant suite verify kernel changes against the engine's output |
279
- | `data` → `engine`/`components`/`viewer` | `/data` subpath imports | the source of truth is `extract_all.py` over the upstream checkout |
302
+ | `data` → `engine`, host apps | `/data` subpath imports | the source of truth is `extract_all.py` over the upstream checkout |
280
303
  | `types` → `engine` | the generator **executes** `deriveKernelParams` (probing) and writes `engine/PARAMS.md` + `../viewer-package/src/settings/kernel_setting_keys.js` back into the engine | `test_kernel_params.mjs` is the staleness gate |
281
304
  | `types` → `data` | `gen_settings_types.mjs` generates `settings-keys.d.ts` (976 keys) from the schema | run automatically by the build |
282
- | `viewer` ↔ `components` | **no direct link** | the host shares one settings map with both as props, and mounts `<SettingsPanel embedded/>` into the viewer's panel slots |
283
- | inside `viewer` | `core` (pure, node-runnable) ← `scene` (three/DOM) ← `actions` ← `ui` | `test_layers.mjs` enforces the boundary |
305
+ | `engine` → `three-slicer-viewer` | the host passes `makeSlicerWorker` (as `useSlicer`'s `makeWorker`) and `bundledCatalog` to `<Viewport/>`, and `schema`/`uiTree`/the bound toggle to `<SettingsPanel/>` | props, not imports — the React half ships without the kernel or upstream's data |
306
+ | `<Viewport/>` ↔ `<SettingsPanel/>` | **no direct link** | the host shares one settings map with both as props, and mounts `<SettingsPanel embedded/>` into the viewer's panel slots |
284
307
 
285
308
  ## TypeScript
286
309
 
@@ -331,7 +354,7 @@ const result = slicer.slice(stlArrayBuffer, params)
331
354
 
332
355
  A slicer-written `.3mf` is a project, not a mesh file: `Metadata/project_settings.config` holds the flattened
333
356
  preset its author sliced with. `<Viewport/>` reads all of it on import. The project parser/writer currently remain
334
- viewer internals; there is no `three-slicer/viewer/project` public export yet. Hosts can normalize or serialize a
357
+ viewer internals; there is no `three-slicer-viewer/project` public export yet. Hosts can normalize or serialize a
335
358
  raw project settings object with the public settings helpers below, but should not import viewer source files.
336
359
 
337
360
  **Every value in that file is a string**, and reading it raw is not a cosmetic problem — a disabled option is the
@@ -758,7 +781,9 @@ the layout by hand.
758
781
  | `disabledKeys(cfg)` | Setting key → the `enable_if` expression that evaluated false |
759
782
  | `evalEnableIf(expr, locals, cfg)` | Evaluates a single rule; `null` when it cannot be resolved |
760
783
 
761
- ### `three-slicer/viewer` and `three-slicer/components`
784
+ ### `three-slicer-viewer` (the React half)
785
+
786
+ Published separately (MIT); listed here because a slicer UI uses both packages.
762
787
 
763
788
  | API | Description |
764
789
  | --- | --- |
@@ -789,7 +814,7 @@ the layout by hand.
789
814
  | Browser main thread | Headless slicing |
790
815
  | Browser worker | Off-main-thread slicing and streaming |
791
816
  | React | Viewer and settings panel |
792
- | three.js | Required by `three-slicer/viewer` |
817
+ | three.js | Required by `three-slicer-viewer`'s `<Viewport/>` |
793
818
 
794
819
  ## Before You Ship
795
820
 
@@ -830,7 +855,7 @@ production while `npm run dev` passed. The no-argument form now emits a real wor
830
855
 
831
856
  - `slice()` currently takes binary STL input at the engine API boundary.
832
857
  - The viewer can import STL, OBJ, 3MF, AMF, and PLY, then converts them for slicing.
833
- - `registerLoader(exts, fn)` from `three-slicer/viewer/loaders` adds any other format. Formats needing a heavy dependency are kept out of the package so it stays runtime-dependency-free — see `web/viewer/src/step_loader.js` for a STEP loader built on `occt-import-js` (OCCT WASM).
858
+ - `registerLoader(exts, fn)` from `three-slicer-viewer/loaders` adds any other format. Formats needing a heavy dependency are kept out of the package so it stays runtime-dependency-free — see `web/viewer/src/step_loader.js` for a STEP loader built on `occt-import-js` (OCCT WASM).
834
859
  - Not every OrcaSlicer schema key is wired into the WASM kernel yet.
835
860
  - Some vector settings are simplified to their first element.
836
861
  - One triangle selector serves both the support brush and the material brush (upstream's shared enum), so a support **blocker** paint and an Extruder 2 paint are the same mark — the two brushes cannot mark the same model independently.
@@ -839,7 +864,7 @@ production while `npm run dev` passed. The no-argument form now emits a real wor
839
864
  - The `.sl1` export lives in the viewer (its masks need a canvas); there is no headless `.sl1` writer export yet.
840
865
  - SLA mask edges are canvas anti-aliasing, not upstream's AGG rasterizer — same geometry, slightly different edge pixels.
841
866
  - Multithreaded WASM requires cross-origin isolation.
842
- - `three` is pinned as a peer dependency because viewer internals depend on the `TransformControls` API shape.
867
+ - `three` is pinned as a peer dependency of `three-slicer-viewer` because viewer internals depend on the `TransformControls` API shape.
843
868
 
844
869
  ## License
845
870
 
@@ -25,8 +25,8 @@ live under `packages/wasm-core/third_party/deps_src/`.
25
25
 
26
26
  ## The viewer and the settings panel
27
27
 
28
- Since the split, `three-slicer/viewer` and `three-slicer/components` are re-exports of `three-slicer-viewer` (MIT), a
29
- separate package with its own `THIRD-PARTY-NOTICES.md`. This package bundles no viewer code of its own.
28
+ The viewer and the settings panel are `three-slicer-viewer` (MIT), a separate package with its own
29
+ `THIRD-PARTY-NOTICES.md`. This package bundles no viewer code of its own; since 0.4.0 it does not re-export it either.
30
30
 
31
31
  ## Bundled into the viewer/components builds (historical — see above)
32
32
 
package/data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # three-slicer/data
2
2
 
3
- OrcaSlicer configuration metadata extracted from the original C++ sources, shipped inside the `three-slicer` package as its `/data` subpath. Used by `three-slicer` (defaults, settings mapping) and `three-slicer/components` (settings UI), and usable standalone for building slicer UIs.
3
+ OrcaSlicer configuration metadata extracted from the original C++ sources, shipped inside the `three-slicer` package as its `/data` subpath. Used by `three-slicer` (defaults, settings mapping) and the settings UI (`three-slicer-viewer/components` takes the schema and UI tree as props), and usable standalone for building slicer UIs.
4
4
 
5
5
  | File | Contents |
6
6
  |---|---|
package/engine/PARAMS.md CHANGED
@@ -230,7 +230,7 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
230
230
  | `extruder_retract_speed` | `?` | `[]` | `filament_retraction_speed` | undefined |
231
231
  | `extruder_z_hop` | `?` | `[]` | `filament_z_hop` | undefined |
232
232
  | `extrusion_rate_slope_segment_length` | `number` | `1.0` | — | segment split length for the real PE (mm) |
233
- | `filament_max_volumetric_speed` | `number[]` | `[]` | `filament_max_volumetric_speed` | mm³/s, per filament; empty = no cap (GW::capped_feed) |
233
+ | `filament_max_volumetric_speed` | `number[]` | `[]` | `filament_max_volumetric_speed` | mm³/s, per filament; empty = no cap (GcodeWriter::capped_feed) |
234
234
  | `filter_out_gap_fill` | `number` | `0.0` | `filter_out_gap_fill` | filter_out_gap_fill (mm): gap fill shorter than this is dropped |
235
235
  | `first_layer_bed_temp` | `number` | `-1.0` | `before_layer_change_gcode`, `change_filament_gcode`, `filament_end_gcode`, `filament_start_gcode`, `file_start_gcode`, `hot_plate_temp_initial_layer`, `layer_change_gcode`, `machine_end_gcode`, `machine_start_gcode`, `time_lapse_gcode` | the plate type's _initial_layer bed option (<0 = none sent) |
236
236
  | `first_layer_flow_ratio` | `?` | — | `first_layer_flow_ratio` | undefined |
package/engine/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Browser/WASM 3D-slicing kernel reverse-engineered from [OrcaSlicer](https://github.com/OrcaSlicer/OrcaSlicer). Slices binary STL to G-code entirely client-side (or in Node) — no server, no native binaries.
4
4
 
5
- **No three.js dependency.** This is a headless SDK with zero runtime dependencies; the config metadata it reads ships inside the same package (`three-slicer/data`). For a ready-made React viewer see `three-slicer/viewer`.
5
+ **No three.js dependency.** This is a headless SDK with zero runtime dependencies; the config metadata it reads ships inside the same package (`three-slicer/data`). For a ready-made React viewer see [`three-slicer-viewer`](../../viewer-package/VIEWER.md), which takes this package's kernel worker as a prop.
6
6
 
7
7
  Ported from the original C++ sources: Clipper polygon ops, Arachne variable-width walls, real fill patterns (gyroid TPMS / honeycomb / 3D-honeycomb / crosshatch / concentric), tree supports, pressure equalizer, arc fitting (G2/G3), scarf seams, ironing, multi-material with prime tower. Kernel changes are gated by a 120+ invariant test suite and golden byte-identical G-code checks.
8
8
 
@@ -41,8 +41,8 @@ const slaTransferables = (job) => job.objects.flatMap(object => [object, ...(obj
41
41
  // keeps make_worker.js out of its own bundle. Do not factor this expression out.
42
42
  /**
43
43
  * The WASM kernel's worker, as a factory. This is the one thing a slicing viewer needs that the permissive
44
- * package cannot ship: `three-slicer-viewer`'s `useSlicer` takes it as `deps.makeWorker`, and
45
- * `three-slicer/viewer` binds it. Written as the full literal expression for the same reason as below.
44
+ * package cannot ship: `three-slicer-viewer`'s `useSlicer` takes it as `deps.makeWorker`, and a
45
+ * host binds it there. Written as the full literal expression for the same reason as below.
46
46
  */
47
47
  export function makeSlicerWorker() {
48
48
  return new Worker(new URL('./slicer.worker.js', import.meta.url), { type: 'module' })
@@ -3,11 +3,11 @@
3
3
  // between the cut and the union.
4
4
  //
5
5
  // What runs here (WGSL: contour_front_shaders.js), every layer at once:
6
- // cut per triangle, the layers it spans (exact z compares), each crossing in double-float, oriented as tri_plane
6
+ // cut per triangle, the layers it spans (exact z compares), each crossing in double-float, oriented as intersect_triangle_with_plane
7
7
  // cancel segments coinciding exactly in opposite directions removed in pairs (cancel_coincident), by a hash on place
8
- // chain each end continues into the smallest unused segment starting at its 1e-3 mm key (chain_polys' greedy rule)
8
+ // chain each end continues into the smallest unused segment starting at its 1e-3 mm key (chain_polygons' greedy rule)
9
9
  // loops pointer jumping for each loop's head and each segment's rank; tails that lead into a cycle stay out
10
- // layout loops sorted by (layer, head), laid out as chain_polys' n + 1 points, write_polygon's duplicates dropped
10
+ // layout loops sorted by (layer, head), laid out as chain_polygons' n + 1 points, write_polygon's duplicates dropped
11
11
  // tables layer origins and grids, polygon ranges, the cell-list size
12
12
  //
13
13
  // Measured (design report gpu-full-port-trial-2026-10-01.md section 3): 3.0M triangles, 420 layers in 30 ms (the kernel's
@@ -205,10 +205,10 @@ export async function makeContourFront(device) {
205
205
  clear(loopStart, (headCount + 1) * 4)
206
206
  run(LOOPS_SHADER, 'lengths', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), headCount)
207
207
  scan(loopStart, headCount + 1)
208
- const [slotCount] = await readWords(loopStart, 1, headCount) // points of the closed loops, chain_polys' n + 1 each
208
+ const [slotCount] = await readWords(loopStart, 1, headCount) // points of the closed loops, chain_polygons' n + 1 each
209
209
  run(LOOPS_SHADER, 'place', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), segmentCount)
210
210
 
211
- // the points write_polygon keeps, the polygons chain_polys keeps, the layer boxes
211
+ // the points write_polygon keeps, the polygons chain_polygons keeps, the layer boxes
212
212
  const kept = buffer('kept', (slotCount + 1) * 4), polygonKept = buffer('polygonKept', (headCount + 1) * 4)
213
213
  const layerBox = buffer('layerBox', layerCount * 32), slotPolygon = buffer('slotPolygon', slotCount * 4)
214
214
  const boxStart = new Int32Array(layerCount * 8)
@@ -1,4 +1,4 @@
1
- // WGSL of the GPU front of PASS1 (contour_front_gpu.js): tri_plane -> chain_polys -> cancel_coincident ->
1
+ // WGSL of the GPU front of PASS1 (contour_front_gpu.js): intersect_triangle_with_plane -> chain_polygons -> cancel_coincident ->
2
2
  // build_gpu_input, every layer at once. Ported from the measured experiment (.omc/plans/gpu-work/fullport/gpu_slice.mjs,
3
3
  // design report gpu-full-port-trial-2026-10-01.md section 3); edited only with a measurement beside it, as
4
4
  // contour_gpu_shaders.js.
@@ -128,7 +128,7 @@ export function printerDefaultBedType(profileName) { return printerEntry(profile
128
128
 
129
129
  // ---- The catalog as one object -------------------------------------------------------------------------------
130
130
  // What `three-slicer-viewer`'s `<Viewport catalog>` takes: the ten lookups above, bundled. The permissive package
131
- // ships no catalog (these are OrcaSlicer's profile bundles); `three-slicer/viewer` passes this one by default.
131
+ // ships no catalog (these are OrcaSlicer's profile bundles); a host passes this one.
132
132
  export const bundledCatalog = Object.freeze({
133
133
  printerKeys, printerSettings, printersByVendor, printerTechByVendor, printerDefaultPreset, printerDefaultBedType,
134
134
  processPresets, filamentPresets, resinCatalog, resinSettingsFor,
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // Stage 30 (OOM tolerance): set_layer_sink lets the kernel emit layers as it produces them; each is transferred to main
7
7
  // immediately (Float32Array buffers moved -> the worker copy is released at once) and the kernel frees that layer buffer
8
- // from the heap. The resident result (the full gw.s + the full layersArr) disappears, so the WASM heap peak drops sharply
8
+ // from the heap. The resident result (the full GcodeWriter::gcode + the full layersArray) disappears, so the WASM heap peak drops sharply
9
9
  // -> large models avoid OOM. The final 'done' carries stats only.
10
10
  // In economy mode (params.economy) only G-code chunks are emitted, without toolpaths (finishes without a preview).
11
11
  // The MM / real-PE paths fall back to batch mode.
@@ -27,7 +27,10 @@ import { FILL_TOOLS } from 'three-slicer-viewer/paint'
27
27
  // and createSlicerClient both take an object and stringify it for you. One object, two shapes, and the difference
28
28
  // showed up only at the boundary. The worker now accepts either; the SLA branch always did, which is what made
29
29
  // the split arbitrary rather than principled.
30
- const paramsText = (params) => (typeof params === 'string' ? params : JSON.stringify(params ?? {}))
30
+ const paramsText = (params) => {
31
+ if (typeof params === 'string') return params
32
+ return JSON.stringify(params ?? {})
33
+ }
31
34
 
32
35
  let quiet = false
33
36
  const say = (level, ...args) => { if (!quiet) console[level](...args) }
@@ -120,7 +123,8 @@ const validatedPaintStates = (states) => {
120
123
  // flow (viewer use_slicer.js) reads those two fields and nothing else.
121
124
  const reportedPaintStates = (message, paintedState) => {
122
125
  if (Array.isArray(message.states)) return validatedPaintStates(message.states)
123
- return paintedState === null ? null : [paintedState]
126
+ if (paintedState === null) return null
127
+ return [paintedState]
124
128
  }
125
129
 
126
130
  // The state a paint message asks for: an explicit `state` wins, otherwise null meaning "use the legacy boolean path".
@@ -133,7 +137,10 @@ const explicitPaintState = (message) => {
133
137
 
134
138
  // Brush cursor shape (selector_bridge.h CURSOR_*). Sent as a name because the message is a protocol a host writes
135
139
  // by hand; the integer stays on this side of it. Anything unknown is the sphere the brush has always used.
136
- const cursorShapeOf = (message) => (message.cursor === 'circle' ? 1 : 0)
140
+ const cursorShapeOf = (message) => {
141
+ if (message.cursor === 'circle') return 1
142
+ return 0
143
+ }
137
144
 
138
145
  // The tools that are not the radius brush: upstream's Smart fill and Bucket fill, plus its single-triangle POINTER
139
146
  // cursor — which is literally bucket fill with propagation off, so the three share one dispatch. None of them takes
@@ -150,8 +157,15 @@ const strokeFrom = (message) =>
150
157
  // The fill modes the kernel's fill_preview takes (selector_bridge.h FILL_*), by the tool name the protocol uses.
151
158
  const FILL_PREVIEW_MODE = { smart: 0, bucket: 1, triangle: 2 }
152
159
 
160
+ // A fill's angle limit in degrees, 30 when the message does not carry one. applyFill and the fill preview both
161
+ // read it here, so the preview selects what the fill will.
162
+ const fillAngleOf = (message) => {
163
+ if (Number.isFinite(message.angle)) return message.angle
164
+ return 30
165
+ }
166
+
153
167
  const applyFill = (Module, message, state) => {
154
- const angle = Number.isFinite(message.angle) ? message.angle : 30
168
+ const angle = fillAngleOf(message)
155
169
  if (message.tool === 'smart') {
156
170
  if (state === null) Module.selector_seed_fill_erase(message.facet, message.hx, message.hy, message.hz, angle)
157
171
  else Module.selector_seed_fill(message.facet, message.hx, message.hy, message.hz, angle, state)
@@ -160,7 +174,8 @@ const applyFill = (Module, message, state) => {
160
174
  // Upstream passes -1 as the angle when it is not propagating (GLGizmoPainterBase.cpp ~861): with a single facet
161
175
  // there is no neighbour to measure an angle against, and -1 is how that call says "no angle limit".
162
176
  const propagate = message.tool === 'bucket'
163
- const bucketAngle = propagate ? angle : -1
177
+ let bucketAngle = -1
178
+ if (propagate) bucketAngle = angle
164
179
  if (state === null) Module.selector_bucket_fill_erase(message.facet, message.hx, message.hy, message.hz, bucketAngle, propagate)
165
180
  else Module.selector_bucket_fill(message.facet, message.hx, message.hy, message.hz, bucketAngle, propagate, state)
166
181
  }
@@ -276,7 +291,9 @@ const handleMessage = async (e) => {
276
291
  // `keepPaint` says the mesh is the same model in a new place, so the marks carry over — the reply reports whether
277
292
  // they actually did, since a face-count change makes the kernel fall back to a clean registration.
278
293
  if (d.cmd === 'prepare') {
279
- const kept = d.keepPaint ? Module.selector_reprepare(new Uint8Array(d.stl)) : (Module.selector_prepare(new Uint8Array(d.stl)), false)
294
+ let kept = false
295
+ if (d.keepPaint) kept = Module.selector_reprepare(new Uint8Array(d.stl))
296
+ else Module.selector_prepare(new Uint8Array(d.stl))
280
297
  reply({ type: 'prepared', facets: Module.selector_facet_count(), kept })
281
298
  return
282
299
  }
@@ -321,7 +338,8 @@ const handleMessage = async (e) => {
321
338
  // A kernel built before this binding existed simply has no selector_import_paint; the reply says applied:0
322
339
  // rather than throwing, because losing a 3mf's paint must not also lose its geometry.
323
340
  if (d.cmd === 'importPaint') {
324
- const applied = Module.selector_import_paint ? Module.selector_import_paint(d.facets, d.hex ?? '') : 0
341
+ let applied = 0
342
+ if (Module.selector_import_paint) applied = Module.selector_import_paint(d.facets, d.hex ?? '')
325
343
  const importedReply = paintedReply(Module, d, null)
326
344
  importedReply.applied = applied
327
345
  reply(importedReply); return
@@ -332,7 +350,8 @@ const handleMessage = async (e) => {
332
350
  // which objects were merged in what order is the viewer's knowledge. A kernel built before the binding existed
333
351
  // reports supported:false rather than throwing — the save then falls back to the paint it imported.
334
352
  if (d.cmd === 'exportPaint') {
335
- const exported = Module.selector_export_paint ? Module.selector_export_paint() : null
353
+ let exported = null
354
+ if (Module.selector_export_paint) exported = Module.selector_export_paint()
336
355
  reply({ type: 'paintExport', supported: !!Module.selector_export_paint,
337
356
  facets: exported?.facets ?? [], hex: exported?.hex ?? '' })
338
357
  return
@@ -358,9 +377,11 @@ const handleMessage = async (e) => {
358
377
  // enf/blk stay unconditional for the listener that predates the per-state map. When the request names states,
359
378
  // they are the ONLY ones read, so building the other two would be pure waste on the hot path.
360
379
  const wanted = states && !states.includes(PAINT_STATE_ENFORCER) && !states.includes(PAINT_STATE_BLOCKER)
361
- const overlayReply = { type: 'overlay',
362
- enf: wanted ? EMPTY_OVERLAY : Module.selector_overlay(true),
363
- blk: wanted ? EMPTY_OVERLAY : Module.selector_overlay(false) }
380
+ const overlayReply = { type: 'overlay', enf: EMPTY_OVERLAY, blk: EMPTY_OVERLAY }
381
+ if (!wanted) {
382
+ overlayReply.enf = Module.selector_overlay(true)
383
+ overlayReply.blk = Module.selector_overlay(false)
384
+ }
364
385
  if (states) overlayReply.overlays = Object.fromEntries(states.map(state => [state, Module.selector_overlay_state(state)]))
365
386
  const transfer = [overlayReply.enf, overlayReply.blk, ...Object.values(overlayReply.overlays ?? {})]
366
387
  .filter(a => a?.buffer && a.byteLength > 0).map(a => a.buffer)
@@ -374,13 +395,15 @@ const handleMessage = async (e) => {
374
395
  if (d.cmd === 'fillPreview') {
375
396
  if (!Module.selector_fill_preview) { reply({ type: 'fillPreview', supported: false, tris: EMPTY_OVERLAY }); return }
376
397
  if (d.clear) { Module.selector_fill_preview_clear(); reply({ type: 'fillPreview', supported: true, tris: EMPTY_OVERLAY }); return }
377
- const angle = Number.isFinite(d.angle) ? d.angle : 30
378
398
  const mode = FILL_PREVIEW_MODE[d.tool] ?? FILL_PREVIEW_MODE.smart
379
399
  // Upstream's -1 for the non-propagating case, the same value applyFill sends: one facet has no neighbour to
380
400
  // measure an angle against.
381
- const tris = Module.selector_fill_preview(d.facet, d.hx, d.hy, d.hz, mode === FILL_PREVIEW_MODE.triangle ? -1 : angle, mode)
382
- reply({ type: 'fillPreview', supported: true, tris },
383
- tris?.buffer && tris.byteLength > 0 ? [tris.buffer] : [])
401
+ let previewAngle = fillAngleOf(d)
402
+ if (mode === FILL_PREVIEW_MODE.triangle) previewAngle = -1
403
+ const previewTriangles = Module.selector_fill_preview(d.facet, d.hx, d.hy, d.hz, previewAngle, mode)
404
+ const transfer = []
405
+ if (previewTriangles?.buffer && previewTriangles.byteLength > 0) transfer.push(previewTriangles.buffer)
406
+ reply({ type: 'fillPreview', supported: true, tris: previewTriangles }, transfer)
384
407
  return
385
408
  }
386
409
  // paintMode: the brush-wide options upstream keeps on the gizmo — the overhang restriction and the section
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-slicer",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
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",
@@ -33,26 +33,6 @@
33
33
  "types": "./types/wasm.d.ts",
34
34
  "default": "./engine/src/slicer_core.mt.js"
35
35
  },
36
- "./viewer": {
37
- "types": "./types/viewer.d.ts",
38
- "default": "./viewer/dist/Viewport.js"
39
- },
40
- "./viewer/toolpath": {
41
- "types": "./types/viewer-toolpath.d.ts",
42
- "default": "./viewer/dist/toolpath_reexport.js"
43
- },
44
- "./viewer/loaders": {
45
- "types": "./types/viewer-loaders.d.ts",
46
- "default": "./viewer/dist/loaders_reexport.js"
47
- },
48
- "./viewer/gcode": {
49
- "types": "./types/viewer-gcode.d.ts",
50
- "default": "./viewer/dist/gcode_reexport.js"
51
- },
52
- "./components": {
53
- "types": "./types/components.d.ts",
54
- "default": "./components/dist/SettingsPanel.js"
55
- },
56
36
  "./data": {
57
37
  "types": "./types/data.d.ts",
58
38
  "default": "./engine/src/data.js"
@@ -65,25 +45,9 @@
65
45
  },
66
46
  "sideEffects": false,
67
47
  "scripts": {
68
- "build": "node types/gen_settings_types.mjs && node types/gen_kernel_params.mjs && vite build --config components/vite.config.js && vite build --config viewer/vite.config.js",
48
+ "build": "node types/gen_settings_types.mjs && node types/gen_kernel_params.mjs",
69
49
  "check": "bash pack_check.sh"
70
50
  },
71
- "peerDependencies": {
72
- "react": ">=18",
73
- "react-dom": ">=18",
74
- "three": "^0.160.0"
75
- },
76
- "peerDependenciesMeta": {
77
- "react": {
78
- "optional": true
79
- },
80
- "react-dom": {
81
- "optional": true
82
- },
83
- "three": {
84
- "optional": true
85
- }
86
- },
87
51
  "devDependencies": {
88
52
  "typescript": "^5.6.0",
89
53
  "vite": "^5.4.11"
@@ -93,8 +57,6 @@
93
57
  "types",
94
58
  "!**/tests/**",
95
59
  "!types/gen_*.mjs",
96
- "components/dist",
97
- "viewer/dist",
98
60
  "data",
99
61
  "data/ui-tree-keys.json",
100
62
  "data/config-schema-lean.json",
@@ -134,6 +96,6 @@
134
96
  "clipping"
135
97
  ],
136
98
  "dependencies": {
137
- "three-slicer-viewer": "0.3.4"
99
+ "three-slicer-viewer": "0.4.0"
138
100
  }
139
101
  }
@@ -1,15 +0,0 @@
1
- import r from "react";
2
- import t from "three-slicer-viewer/components";
3
- import { uiTree as o, schema as m } from "three-slicer/data";
4
- import { schema as d, uiTree as h } from "three-slicer/data";
5
- import { disabledKeys as a, makeCfg as i } from "three-slicer/toggle";
6
- const f = { makeCfg: i, disabledKeys: a };
7
- function l(e) {
8
- return /* @__PURE__ */ r.createElement(t, { schema: m, uiTree: o, toggle: f, ...e });
9
- }
10
- export {
11
- l as default,
12
- d as schema,
13
- f as toggle,
14
- h as uiTree
15
- };
@@ -1,3 +0,0 @@
1
- // three-slicer/components — the permissive settings panel with upstream's data plugged in. Same surface.
2
- export * from 'three-slicer-viewer/components'
3
- export { default } from 'three-slicer-viewer/components'
@@ -1,2 +0,0 @@
1
- // Re-exported from the permissive package (MIT), where the implementation now lives.
2
- export * from 'three-slicer-viewer/gcode'
@@ -1 +0,0 @@
1
- export * from 'three-slicer-viewer/loaders'
@@ -1,3 +0,0 @@
1
- // Re-exported from the permissive package (MIT), where the implementation now lives.
2
- // three-slicer/viewer/toolpath stays a valid entry point for every existing consumer.
3
- export * from 'three-slicer-viewer'
package/types/viewer.d.ts DELETED
@@ -1,7 +0,0 @@
1
- // three-slicer/viewer — the permissive viewer (three-slicer-viewer, MIT) with the kernel plugged in.
2
- export * from 'three-slicer-viewer'
3
- import type { ViewportProps, SlicerHandle } from 'three-slicer-viewer'
4
- /** The permissive `useSlicer` bound to the WASM kernel worker (three-slicer/client's makeSlicerWorker). */
5
- export function useSlicer(deps: Record<string, unknown>): SlicerHandle
6
- declare function Viewport(props: ViewportProps): JSX.Element
7
- export default Viewport
@@ -1,12 +0,0 @@
1
- import t from "react";
2
- import i, { useSlicer as m } from "three-slicer-viewer";
3
- import { makeSlicerWorker as a } from "three-slicer/client";
4
- import { bundledCatalog as c } from "three-slicer/settings";
5
- const l = (e) => m({ ...e, makeWorker: a });
6
- function p({ slicer: e, catalog: r, ...o }) {
7
- return /* @__PURE__ */ t.createElement(i, { ...o, slicer: e ?? l, catalog: r === void 0 ? c : r });
8
- }
9
- export {
10
- p as default,
11
- l as useSlicer
12
- };
@@ -1 +0,0 @@
1
- export * from "three-slicer-viewer/gcode";
@@ -1 +0,0 @@
1
- export * from "three-slicer-viewer/loaders";
@@ -1 +0,0 @@
1
- export * from "three-slicer-viewer/toolpath";