three-slicer 0.3.1 → 0.3.2
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 +97 -0
- package/engine/PARAMS.md +4 -3
- package/engine/src/slicer.worker.js +50 -41
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 — 2026-09-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- The prime tower is set per plate. The Prime tower card carries the same Global | Plate switch as the Process,
|
|
8
|
+
Printer and Filament cards: in plate scope its mode, width, purge destination and purging table become that
|
|
9
|
+
plate's override. The position was already per plate (`wipe_tower_x/y[plate]`) and stays in the global map.
|
|
10
|
+
- A saved `.3mf` carries the per-plate overrides and the global map's non-schema keys (`wipe_tower_real`,
|
|
11
|
+
`sla_antialias`) in a member of its own, `Metadata/three_slicer_settings.json`, and an import restores them.
|
|
12
|
+
Upstream has no place for either and ignores the member, so OrcaSlicer still opens the file on the global preset.
|
|
13
|
+
- The kernel can tag every extrusion run with upstream's `;TYPE:` (`gcode_role_tags`, opt-in; the viewer turns it
|
|
14
|
+
on), from the same role the preview draws. A saved G-code read back as text colours as the slice did.
|
|
15
|
+
- "Export all" writes one `.gcode.3mf` (upstream's "Export all plate sliced file") instead of one `.gcode` download
|
|
16
|
+
per plate: each plate's G-code as `Metadata/plate_N.gcode` with its MD5, the estimate in `slice_info.config`, no
|
|
17
|
+
meshes. Opening one (drop, picker or `files`) puts every plate's G-code back on its plate and makes the viewer
|
|
18
|
+
preview-only until it is closed from the top bar or a model is loaded. The `gcode` prop also takes a
|
|
19
|
+
`{ plateIndex: text }` map. A resin plate still exports as its own `.sl1`.
|
|
20
|
+
- The sidebar's Export G-code button saves every sliced plate in one `.gcode.3mf` (upstream's "Export all sliced
|
|
21
|
+
file"). Its ▾ menu holds the viewed plate alone as a `.gcode.3mf` and the plain `.gcode`. It also works on an
|
|
22
|
+
opened `.gcode.3mf`, so a print job can be saved again.
|
|
23
|
+
|
|
24
|
+
- The Open button and the drop target take a plain `.gcode` / `.gco` / `.g`. It opens on the selected plate
|
|
25
|
+
through the same path a `.gcode.3mf` takes: the top bar names it and closes it. A file with no printable move is
|
|
26
|
+
refused before it opens. The demo app's own Open G-code button is gone.
|
|
27
|
+
- An exported G-code states its filament colours (`; filament_colour` in the config block, where upstream reads
|
|
28
|
+
settings), so it opens in its colours here and in OrcaSlicer. A file that already states a palette keeps it; the
|
|
29
|
+
kernel's G-code itself is unchanged.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- The ring / real tower choice is the settings key `wipe_tower_real` (a viewer knob, like `sla_antialias`) instead
|
|
34
|
+
of component state, so it follows plate overrides and survives a save. Absent means the ring, as before.
|
|
35
|
+
- The tower stand-ins are decided per plate: a plate gets one only when it changes tools itself, and each plate
|
|
36
|
+
draws its own width and on/off. A dragged or typed tower position is kept on the bed.
|
|
37
|
+
- The tower stand-in is selected alone and only translated. It no longer joins a multi-selection, takes a
|
|
38
|
+
rotate/scale gizmo, or adds an undo entry that undid nothing.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- An opened multi-material G-code drew its model as prime tower in the Feature view: the multi-material path wrote
|
|
43
|
+
no role marks, so everything after a tower's comment inherited it until the next layer.
|
|
44
|
+
- A loaded G-code was drawn in the session's palette instead of its own. The Filament view reads each plate's
|
|
45
|
+
palette from its result (the file's, with the session's filling the holes), and every tool the result used gets
|
|
46
|
+
a colour, so a file with more tools than the session has filaments no longer shows T3 in three different colours
|
|
47
|
+
across the toolpath, the stats card and the legend. The G-code reader reports per-tool filament
|
|
48
|
+
(`filament_mm_by_tool`).
|
|
49
|
+
- The drop overlay and the load-rejection message listed STL/OBJ/3MF/AMF/PLY by hand: neither named STEP, which the
|
|
50
|
+
demo registers, and the overlay named neither SL1 nor G-code. Both now read `SUPPORTED_EXT`.
|
|
51
|
+
- The G-code reader dropped a `;TYPE:` written between a layer marker and the layer's first move (Cura's
|
|
52
|
+
`;LAYER:` files, this kernel's raft layers), reading that run as wall.
|
|
53
|
+
- The drop highlight stayed on after a drop the host handled itself (the demo app's `.gcode` drop): the canvas's
|
|
54
|
+
own handler never ran and a drop fires no `dragleave`. The window's capture phase now clears it.
|
|
55
|
+
- Injected G-code moved half a bed off its plate whenever the plate grid was re-laid out (adding a plate, editing
|
|
56
|
+
the bed): the re-layout set every display offset to the plate origin, dropping the bed-corner offset injected
|
|
57
|
+
G-code carries. Offsets now move by the origin's delta.
|
|
58
|
+
- Two objects on the same spot sliced to nothing. The kernel slices the merge of every object as one mesh and filled
|
|
59
|
+
the layer loops even-odd, so coincident shells counted as "inside twice". Segments are now oriented by the facet
|
|
60
|
+
normal and filled NonZero, upstream's Regular slicing mode, in the FFF, multi-material and SLA paths. Measured: a
|
|
61
|
+
project imported twice printed 1,210 mm of a 42,984 mm model before, the full model now. Clean meshes slice the
|
|
62
|
+
same regions (0 area difference on the fixtures, under 0.0003 mm² per layer on the Benchy), but a loop can start
|
|
63
|
+
at a different vertex, so G-code of models other than the golden fixtures may differ in point order.
|
|
64
|
+
- A saved `.3mf` with a tower position on some plates and automatic placement on others wrote `null` into
|
|
65
|
+
`wipe_tower_x/y`. OrcaSlicer's loader stops reading the whole file at an array entry that is not a string, which
|
|
66
|
+
dropped the 17 settings after it, `z_hop` and `z_offset` among them. The hole is now written as the schema default
|
|
67
|
+
for upstream and kept as a hole in `Metadata/three_slicer_settings.json`.
|
|
68
|
+
- The GPU SL1 mask path (`sla_antialias`, `sl1_parity_gpu.js`) counted surfaces above the plane as a parity, so two
|
|
69
|
+
coincident objects came out empty in the masks while the slice and its supports treated them as solid. The count
|
|
70
|
+
is now signed by facing (NonZero), the kernel's fill rule.
|
|
71
|
+
- Painting one plate and then another discarded the first plate's paint, and a copy placed where the original sits on
|
|
72
|
+
its own plate took the original's paint over (the original then sliced single-material, the copy printed both).
|
|
73
|
+
Paint now lives on each object (`core/paint_store.js`): the selector is written back to it before it switches
|
|
74
|
+
mesh and loaded from it after; slice-all pool workers load their plate's paint; a 3mf save writes every plate's
|
|
75
|
+
brush strokes (it used to save them only for a single-plate project); copy, duplicate and paste carry the paint,
|
|
76
|
+
and a split hands each part the marks on its own facets.
|
|
77
|
+
- A brush stroke painted only the first object of the plate being painted: the hit facet was sent in the hit
|
|
78
|
+
object's own numbering, not the plate merge's. A stroke now also carries across plates — reaching an object on
|
|
79
|
+
another plate selects that plate and keeps painting there, and an object added to the plate while the brush is
|
|
80
|
+
open is painted too.
|
|
81
|
+
- Dragging a painted object no longer drags the paint of the other objects on its plate along with it.
|
|
82
|
+
- A paste pressed right after a copy pastes that copy; a double-pressed delete records one undo step; a drag
|
|
83
|
+
committed while a slice loads its plate's paint no longer reaches the worker before that slice.
|
|
84
|
+
- A paint export the worker fails no longer discards the unsaved strokes (the swap stops instead), and Clear
|
|
85
|
+
pressed during a plate switch clears the plate it lands on instead of doing nothing. Strokes made right after
|
|
86
|
+
switching between the support and material brushes are no longer saved as the other kind of paint.
|
|
87
|
+
- Switching between the support and material brushes with the section plane on no longer leaves the kernel a
|
|
88
|
+
shifted plane on plates past the first; support-only paint stays drawn after a drag on its plate; a slice that
|
|
89
|
+
leaves support paint out under material paint says so again.
|
|
90
|
+
- Paint for a filament that is not configured (T3 painted, then the list cut to two) no longer widens the extruder
|
|
91
|
+
count on a slice-all pool worker only; every path leaves it out and the slice says so.
|
|
92
|
+
Every plate's paint stays drawn, not only the plate being painted.
|
|
93
|
+
- Review fixes to the paint store, each reproduced before the fix: a slice-all pool worker printed the previous
|
|
94
|
+
plate's paint on an unpainted plate; opening the support brush filed another plate's material paint as support
|
|
95
|
+
paint; a 3mf save wrote every object under one paint attribute and let an empty support map hide material paint;
|
|
96
|
+
a worker replaced by the watchdog wiped the stored paint of the plate it held; and a save waiting on a worker
|
|
97
|
+
that was then terminated stayed on "Saving…" with the viewport blank. Worker replies now echo a `requestId`, so
|
|
98
|
+
an error reply ends only the request it belongs to.
|
|
99
|
+
|
|
3
100
|
## 0.3.1 — 2026-09-16
|
|
4
101
|
|
|
5
102
|
### Added
|
package/engine/PARAMS.md
CHANGED
|
@@ -8,8 +8,8 @@ cannot drift from what `slice()` actually honours. Regenerate with `node types/g
|
|
|
8
8
|
|
|
9
9
|
<!-- KERNEL-PARAMS:BEGIN — generated by types/gen_kernel_params.mjs, do not edit by hand -->
|
|
10
10
|
|
|
11
|
-
The kernel accepts **
|
|
12
|
-
OrcaSlicer settings map; the remaining
|
|
11
|
+
The kernel accepts **160 parameters**. `deriveKernelParams()` produces 131 of them from the
|
|
12
|
+
OrcaSlicer settings map; the remaining 29 have no schema key behind them and can only be set by writing
|
|
13
13
|
the params object directly — those rows show `—` in the *From setting* column.
|
|
14
14
|
|
|
15
15
|
A parameter left out of the object keeps the default listed here, so sending a partial object is normal and is
|
|
@@ -205,6 +205,7 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
|
|
|
205
205
|
| `flush_multiplier` | `number` | `1.0` | `flush_multiplier` | upstream scales the whole table by this |
|
|
206
206
|
| `flush_volumes_matrix` | `number[]` | `[]` | `flush_volumes_matrix` | |
|
|
207
207
|
| `gcode_config_block` | `boolean` | `false` | — | ; CONFIG_BLOCK_START … the parameters this slice ran with … END |
|
|
208
|
+
| `gcode_role_tags` | `boolean` | `false` | — | ;TYPE:<role> on every role change (upstream GCodeProcessor tag) |
|
|
208
209
|
| `gcode_stats_block` | `boolean` | `false` | — | ; filament used [mm]/[cm3]/[g], cost, total filament change |
|
|
209
210
|
| `independent_support_layer_height` | `boolean` | `false` | `independent_support_layer_height` | false by default because of the kernel z grid constraint (gap quantized to layers) |
|
|
210
211
|
| `keep_stages` | `boolean` | `false` | — | keep the stages cached after slicing (skips the early release — a memory trade-off) |
|
|
@@ -231,7 +232,7 @@ A `—` in *From setting* means there is no OrcaSlicer option behind that parame
|
|
|
231
232
|
in the params object yourself. They are not one kind of thing, and the difference matters before you reach for one:
|
|
232
233
|
|
|
233
234
|
- **Host runtime flags** — `economy`, `keep_stages`, `reuse_stages`, `time_engine`, `wipe_tower_real`,
|
|
234
|
-
`gcode_stats_block`, `gcode_config_block`. Decisions the *caller* makes about a particular run, not properties of
|
|
235
|
+
`gcode_stats_block`, `gcode_config_block`, `gcode_role_tags`. Decisions the *caller* makes about a particular run, not properties of
|
|
235
236
|
the print. `economy` is the last rung of an out-of-memory retry: it streams G-code and skips the preview
|
|
236
237
|
toolpaths and the time estimate, so a model that cannot finish any other way still finishes.
|
|
237
238
|
- **Values the viewer computes** — `extruder_count`, `mm_group_split`, `mm_group_splits`, `mm_group_tools`,
|
|
@@ -132,14 +132,21 @@ const applyFill = (Module, message, state) => {
|
|
|
132
132
|
// listener predating the per-state map keeps working; `counts` appears only when the message asked for states,
|
|
133
133
|
// which is what keeps a legacy paint reply byte-identical to what it was before the protocol was widened.
|
|
134
134
|
const paintedReply = (Module, message, paintedState) => {
|
|
135
|
-
const
|
|
135
|
+
const answer = { type: 'painted', enf: Module.selector_painted_count(true), blk: Module.selector_painted_count(false) }
|
|
136
136
|
const states = reportedPaintStates(message, paintedState)
|
|
137
|
-
if (states)
|
|
138
|
-
return
|
|
137
|
+
if (states) answer.counts = Object.fromEntries(states.map(state => [state, Module.selector_painted_count_state(state)]))
|
|
138
|
+
return answer
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
self.onmessage = async (e) => {
|
|
142
142
|
const d = e.data
|
|
143
|
+
// Every reply to a request that carried a `requestId` carries it back, the error reply included — so a caller
|
|
144
|
+
// waiting on one request cannot take another request's answer (or another command's error) for its own. A
|
|
145
|
+
// request without one gets exactly the replies it always got.
|
|
146
|
+
const reply = (message, transfer) => {
|
|
147
|
+
if (d?.requestId === undefined) { self.postMessage(message, transfer); return }
|
|
148
|
+
self.postMessage({ ...message, requestId: d.requestId }, transfer)
|
|
149
|
+
}
|
|
143
150
|
// Read before the kernel loads: loadCore() below is what prints, so a `quiet` arriving with the first message
|
|
144
151
|
// (the warmup) has to take effect before that call, not after it.
|
|
145
152
|
if (d.quiet !== undefined) quiet = !!d.quiet
|
|
@@ -148,28 +155,28 @@ self.onmessage = async (e) => {
|
|
|
148
155
|
// has never loaded it then selector_prepare has never run either, so there is no painting to report — and
|
|
149
156
|
// making a save wait on a 4MB WASM load before the first slice is seconds of nothing, for an empty answer.
|
|
150
157
|
if (d.cmd === 'exportPaint' && !modPromise) {
|
|
151
|
-
|
|
158
|
+
reply({ type: 'paintExport', supported: false, facets: [], hex: '' }); return
|
|
152
159
|
}
|
|
153
160
|
if (d.cmd === 'slaJob') {
|
|
154
161
|
const job = parseSlaJob(d.job)
|
|
155
162
|
if (!modPromise) modPromise = loadCore()
|
|
156
163
|
const Kernel = await modPromise
|
|
157
|
-
const onProgress = (done, total) =>
|
|
164
|
+
const onProgress = (done, total) => reply({ type: 'progress', done, total })
|
|
158
165
|
if (Kernel.slice_sla_job) {
|
|
159
166
|
Kernel.set_layer_sink((z, idx, gcode, paths, widths) => {
|
|
160
167
|
const transfer = []
|
|
161
168
|
if (paths?.buffer) transfer.push(paths.buffer)
|
|
162
169
|
if (widths?.buffer) transfer.push(widths.buffer)
|
|
163
|
-
|
|
170
|
+
reply({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
|
|
164
171
|
})
|
|
165
172
|
let r
|
|
166
173
|
const started = performance.now()
|
|
167
174
|
try { r = Kernel.slice_sla_job(job, onProgress) }
|
|
168
175
|
finally { Kernel.clear_layer_sink() }
|
|
169
|
-
if (r?.error) {
|
|
170
|
-
|
|
176
|
+
if (r?.error) { reply({ type: 'error', error: String(r.error) }); return }
|
|
177
|
+
reply({ type: 'done', result: withSliceThroughput(r, performance.now() - started) }); return
|
|
171
178
|
}
|
|
172
|
-
|
|
179
|
+
reply({ type: 'error', code: 'SLA_UNSUPPORTED_OBJECT_AWARE',
|
|
173
180
|
error: 'Object-aware SLA slicing requires WASM kernel support' })
|
|
174
181
|
return
|
|
175
182
|
}
|
|
@@ -180,20 +187,20 @@ self.onmessage = async (e) => {
|
|
|
180
187
|
const params = paramsText(d.params)
|
|
181
188
|
if (!modPromise) modPromise = loadCore()
|
|
182
189
|
const Kernel = await modPromise
|
|
183
|
-
const onProgress = (done, total) =>
|
|
190
|
+
const onProgress = (done, total) => reply({ type: 'progress', done, total })
|
|
184
191
|
if (Kernel.slice_sla) {
|
|
185
192
|
Kernel.set_layer_sink((z, idx, gcode, paths, widths) => {
|
|
186
193
|
const transfer = []
|
|
187
194
|
if (paths && paths.buffer) transfer.push(paths.buffer)
|
|
188
195
|
if (widths && widths.buffer) transfer.push(widths.buffer)
|
|
189
|
-
|
|
196
|
+
reply({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
|
|
190
197
|
})
|
|
191
198
|
let r
|
|
192
199
|
const started = performance.now()
|
|
193
200
|
try { r = Kernel.slice_sla(new Uint8Array(d.stl), params, onProgress) }
|
|
194
201
|
finally { Kernel.clear_layer_sink() }
|
|
195
|
-
if (r && r.error) {
|
|
196
|
-
|
|
202
|
+
if (r && r.error) { reply({ type: 'error', error: String(r.error) }); return }
|
|
203
|
+
reply({ type: 'done', result: withSliceThroughput(r, performance.now() - started) })
|
|
197
204
|
return
|
|
198
205
|
}
|
|
199
206
|
const { sliceSla } = await import('./sla_core.js')
|
|
@@ -208,23 +215,23 @@ self.onmessage = async (e) => {
|
|
|
208
215
|
const transfer = []
|
|
209
216
|
if (L.paths?.buffer) transfer.push(L.paths.buffer)
|
|
210
217
|
if (L.widths?.buffer) transfer.push(L.widths.buffer)
|
|
211
|
-
|
|
218
|
+
reply({ type: 'layer', z: L.z, idx: L.idx, gcode: '', paths: L.paths, widths: L.widths }, transfer)
|
|
212
219
|
},
|
|
213
220
|
})
|
|
214
|
-
if (r && r.error) {
|
|
215
|
-
|
|
221
|
+
if (r && r.error) { reply({ type: 'error', error: String(r.error) }); return }
|
|
222
|
+
reply({ type: 'done', result: withSliceThroughput(r, performance.now() - startedFallback) })
|
|
216
223
|
return
|
|
217
224
|
}
|
|
218
225
|
if (!modPromise) modPromise = loadCore()
|
|
219
226
|
const Module = await modPromise
|
|
220
227
|
// Warmup: only load the kernel (+ spawn the mt pthread pool) ahead of time — removes the perceived load on the first slice
|
|
221
|
-
if (d.cmd === 'warmup') {
|
|
228
|
+
if (d.cmd === 'warmup') { reply({ type: 'warm', kernel: kernelKind }); return }
|
|
222
229
|
// Stage 20: manual support painting — selector state persists in this worker Module (slicing uses the same Module).
|
|
223
230
|
// `keepPaint` says the mesh is the same model in a new place, so the marks carry over — the reply reports whether
|
|
224
231
|
// they actually did, since a face-count change makes the kernel fall back to a clean registration.
|
|
225
232
|
if (d.cmd === 'prepare') {
|
|
226
233
|
const kept = d.keepPaint ? Module.selector_reprepare(new Uint8Array(d.stl)) : (Module.selector_prepare(new Uint8Array(d.stl)), false)
|
|
227
|
-
|
|
234
|
+
reply({ type: 'prepared', facets: Module.selector_facet_count(), kept })
|
|
228
235
|
return
|
|
229
236
|
}
|
|
230
237
|
// paint: legacy {enforcer:boolean} stays on the boolean binding (false == BLOCKER); {state:1..16} reaches any
|
|
@@ -242,7 +249,7 @@ self.onmessage = async (e) => {
|
|
|
242
249
|
else if (strokeFrom(d) && Module.selector_paint_stroke)
|
|
243
250
|
Module.selector_paint_stroke(d.facet, d.px, d.py, d.pz, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, paintedState, cursorShapeOf(d))
|
|
244
251
|
else Module.selector_paint_shape(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, paintedState, cursorShapeOf(d))
|
|
245
|
-
|
|
252
|
+
reply(paintedReply(Module, d, paintedState)); return
|
|
246
253
|
}
|
|
247
254
|
// erase: the brush writes NONE, returning the brushed facets to the default extruder (upstream's shift+drag).
|
|
248
255
|
// It is its OWN command and NOT `{cmd:'paint', state:0}`, because 0 is the one integer a boolean can become:
|
|
@@ -259,7 +266,7 @@ self.onmessage = async (e) => {
|
|
|
259
266
|
// the paint brush never touched. `selector_erase` (no shape) stays the fallback for an older kernel.
|
|
260
267
|
else if (Module.selector_erase_shape) Module.selector_erase_shape(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, cursorShapeOf(d))
|
|
261
268
|
else Module.selector_erase(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius)
|
|
262
|
-
|
|
269
|
+
reply(paintedReply(Module, d, null)); return
|
|
263
270
|
}
|
|
264
271
|
// importPaint: load painting out of a 3mf. `facets` is an Int32Array of facet indices in the CURRENT selector's
|
|
265
272
|
// numbering and `hex` the newline-joined per-facet split-tree strings the 3mf carried, in the same order.
|
|
@@ -269,9 +276,9 @@ self.onmessage = async (e) => {
|
|
|
269
276
|
// rather than throwing, because losing a 3mf's paint must not also lose its geometry.
|
|
270
277
|
if (d.cmd === 'importPaint') {
|
|
271
278
|
const applied = Module.selector_import_paint ? Module.selector_import_paint(d.facets, d.hex ?? '') : 0
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
279
|
+
const importedReply = paintedReply(Module, d, null)
|
|
280
|
+
importedReply.applied = applied
|
|
281
|
+
reply(importedReply); return
|
|
275
282
|
}
|
|
276
283
|
// exportPaint: the reverse of importPaint — every marked facet's split tree as {facets, hex} in the CURRENT
|
|
277
284
|
// selector's numbering, the same parallel-array pairing importPaint takes (hex is one newline-joined blob).
|
|
@@ -280,17 +287,17 @@ self.onmessage = async (e) => {
|
|
|
280
287
|
// reports supported:false rather than throwing — the save then falls back to the paint it imported.
|
|
281
288
|
if (d.cmd === 'exportPaint') {
|
|
282
289
|
const exported = Module.selector_export_paint ? Module.selector_export_paint() : null
|
|
283
|
-
|
|
290
|
+
reply({ type: 'paintExport', supported: !!Module.selector_export_paint,
|
|
284
291
|
facets: exported?.facets ?? [], hex: exported?.hex ?? '' })
|
|
285
292
|
return
|
|
286
293
|
}
|
|
287
294
|
// clear wipes every state at once, so each requested count is 0 by construction — no need to ask the kernel back.
|
|
288
295
|
if (d.cmd === 'clear') {
|
|
289
296
|
Module.selector_clear()
|
|
290
|
-
const
|
|
297
|
+
const clearedReply = { type: 'painted', enf: 0, blk: 0 }
|
|
291
298
|
const states = reportedPaintStates(d, null)
|
|
292
|
-
if (states)
|
|
293
|
-
|
|
299
|
+
if (states) clearedReply.counts = Object.fromEntries(states.map(state => [state, 0]))
|
|
300
|
+
reply(clearedReply); return
|
|
294
301
|
}
|
|
295
302
|
// overlay: `enf`/`blk` (states 1/2) are always sent because the viewer's overlay rebuild consumes exactly those two;
|
|
296
303
|
// `overlays` adds the requested states, so an MMU caller can ask for 3..16 without losing the support overlays.
|
|
@@ -305,13 +312,13 @@ self.onmessage = async (e) => {
|
|
|
305
312
|
// enf/blk stay unconditional for the listener that predates the per-state map. When the request names states,
|
|
306
313
|
// they are the ONLY ones read, so building the other two would be pure waste on the hot path.
|
|
307
314
|
const wanted = states && !states.includes(PAINT_STATE_ENFORCER) && !states.includes(PAINT_STATE_BLOCKER)
|
|
308
|
-
const
|
|
315
|
+
const overlayReply = { type: 'overlay',
|
|
309
316
|
enf: wanted ? EMPTY_OVERLAY : Module.selector_overlay(true),
|
|
310
317
|
blk: wanted ? EMPTY_OVERLAY : Module.selector_overlay(false) }
|
|
311
|
-
if (states)
|
|
312
|
-
const transfer = [
|
|
318
|
+
if (states) overlayReply.overlays = Object.fromEntries(states.map(state => [state, Module.selector_overlay_state(state)]))
|
|
319
|
+
const transfer = [overlayReply.enf, overlayReply.blk, ...Object.values(overlayReply.overlays ?? {})]
|
|
313
320
|
.filter(a => a?.buffer && a.byteLength > 0).map(a => a.buffer)
|
|
314
|
-
|
|
321
|
+
reply(overlayReply, transfer); return
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
// fillPreview: what a fill WOULD select, without applying it. Upstream runs this on every mouse move while a
|
|
@@ -319,14 +326,14 @@ self.onmessage = async (e) => {
|
|
|
319
326
|
// nothing, so it needs no state at all and there is no NONE for a stray boolean to become. `{clear:true}`
|
|
320
327
|
// drops the standing selection, which is what leaving the model under the pointer means.
|
|
321
328
|
if (d.cmd === 'fillPreview') {
|
|
322
|
-
if (!Module.selector_fill_preview) {
|
|
323
|
-
if (d.clear) { Module.selector_fill_preview_clear();
|
|
329
|
+
if (!Module.selector_fill_preview) { reply({ type: 'fillPreview', supported: false, tris: EMPTY_OVERLAY }); return }
|
|
330
|
+
if (d.clear) { Module.selector_fill_preview_clear(); reply({ type: 'fillPreview', supported: true, tris: EMPTY_OVERLAY }); return }
|
|
324
331
|
const angle = Number.isFinite(d.angle) ? d.angle : 30
|
|
325
332
|
const mode = FILL_PREVIEW_MODE[d.tool] ?? FILL_PREVIEW_MODE.smart
|
|
326
333
|
// Upstream's -1 for the non-propagating case, the same value applyFill sends: one facet has no neighbour to
|
|
327
334
|
// measure an angle against.
|
|
328
335
|
const tris = Module.selector_fill_preview(d.facet, d.hx, d.hy, d.hz, mode === FILL_PREVIEW_MODE.triangle ? -1 : angle, mode)
|
|
329
|
-
|
|
336
|
+
reply({ type: 'fillPreview', supported: true, tris },
|
|
330
337
|
tris?.buffer && tris.byteLength > 0 ? [tris.buffer] : [])
|
|
331
338
|
return
|
|
332
339
|
}
|
|
@@ -339,7 +346,7 @@ self.onmessage = async (e) => {
|
|
|
339
346
|
if (plane) Module.selector_set_clip_plane(plane[0], plane[1], plane[2], plane[3], true)
|
|
340
347
|
else if (d.clipPlane === null) Module.selector_set_clip_plane(0, 0, 1, 0, false)
|
|
341
348
|
}
|
|
342
|
-
|
|
349
|
+
reply({ type: 'paintMode', ok: true }); return
|
|
343
350
|
}
|
|
344
351
|
|
|
345
352
|
if (d.stall) return // stage-30 test hook: simulate a hang -> verifies the main-thread watchdog fires (not set in production)
|
|
@@ -352,17 +359,17 @@ self.onmessage = async (e) => {
|
|
|
352
359
|
const v = Module.sup_progress_view && Module.sup_progress_view()
|
|
353
360
|
const c = Module.cancel_flag_view && Module.cancel_flag_view()
|
|
354
361
|
if (v && v.buffer instanceof SharedArrayBuffer)
|
|
355
|
-
|
|
362
|
+
reply({ type: 'supsab', buf: v.buffer, ptr: v.byteOffset, cancelPtr: c?.byteOffset ?? 0 })
|
|
356
363
|
} catch {}
|
|
357
364
|
}
|
|
358
365
|
// Default: slice. Register the layer sink -> the kernel calls back per layer (z, idx, gcodeChunk, pathsF32, widthsF32).
|
|
359
366
|
// Each layer is transferred to main immediately (toolpath buffers moved) -> the worker copy is freed -> heap headroom before the next layer.
|
|
360
|
-
const onProgress = (done, total) =>
|
|
367
|
+
const onProgress = (done, total) => reply({ type: 'progress', done, total })
|
|
361
368
|
Module.set_layer_sink((z, idx, gcode, paths, widths) => {
|
|
362
369
|
const transfer = []
|
|
363
370
|
if (paths && paths.buffer) transfer.push(paths.buffer) // economy mode yields empty arrays (no .buffer) -> nothing to transfer
|
|
364
371
|
if (widths && widths.buffer) transfer.push(widths.buffer)
|
|
365
|
-
|
|
372
|
+
reply({ type: 'layer', z, idx, gcode, paths, widths }, transfer)
|
|
366
373
|
})
|
|
367
374
|
let r
|
|
368
375
|
// Timed around the kernel call INCLUDING the layer sink, which posts each layer to the main thread from inside
|
|
@@ -370,14 +377,16 @@ self.onmessage = async (e) => {
|
|
|
370
377
|
const started = performance.now()
|
|
371
378
|
try { r = Module.slice(new Uint8Array(d.stl), paramsText(d.params), onProgress) }
|
|
372
379
|
finally { Module.clear_layer_sink() }
|
|
373
|
-
if (r && r.error) {
|
|
380
|
+
if (r && r.error) { reply({ type: 'error', error: String(r.error) }); return }
|
|
374
381
|
// streamed=true -> g-code/layers were already emitted as 'layer' (result holds stats only). batch/MM keep them in result.
|
|
375
382
|
// withSliceWarnings names what the slice got away with (an off-bed model) on the result itself, and
|
|
376
383
|
// withSliceThroughput how fast it ran — the raw protocol carries both, so createSlicerClient's callers get
|
|
377
384
|
// them without the client having to re-derive anything.
|
|
378
|
-
|
|
385
|
+
reply({ type: 'done', result: withSliceThroughput(withSliceWarnings(r), performance.now() - started) })
|
|
379
386
|
} catch (err) {
|
|
380
387
|
// Includes the WASM abort("memory access out of bounds") — the main thread's OOM ladder decides on re-creation / economy retry.
|
|
381
|
-
|
|
388
|
+
const errorReply = { type: 'error', error: String((err && err.message) || err) }
|
|
389
|
+
if (err?.code) errorReply.code = err.code
|
|
390
|
+
reply(errorReply)
|
|
382
391
|
}
|
|
383
392
|
}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-slicer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|
|
@@ -134,6 +134,6 @@
|
|
|
134
134
|
"clipping"
|
|
135
135
|
],
|
|
136
136
|
"dependencies": {
|
|
137
|
-
"three-slicer-viewer": "0.3.
|
|
137
|
+
"three-slicer-viewer": "0.3.2"
|
|
138
138
|
}
|
|
139
139
|
}
|