three-slicer 0.3.0 → 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/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 **159 parameters**. `deriveKernelParams()` produces 131 of them from the
12
- OrcaSlicer settings map; the remaining 28 have no schema key behind them and can only be set by writing
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`,
@@ -10,7 +10,13 @@
10
10
  // Determinism: iteration order is the STL's own triangle order, stitching maps insert in encounter order, and
11
11
  // vertices on a slicing plane are pushed up by a fixed epsilon — same input bytes, same output bytes.
12
12
 
13
- const ROLE_CONTOUR = 1 // rendered as 'wall' by the preview's feature colouring
13
+ // The toolpath stream and binary STL layouts. This worker is standalone (no imports), so it keeps its own copy of
14
+ // numbers the viewer holds in core/toolpath_encoding.js and core/stl_format.js; TOOLPATH_SPEC.md is the contract.
15
+ const STRIDE = 8 // floats per segment: [x0,y0,z0,enc, x1,y1,z1,enc]
16
+ const ROLE_CONTOUR = 1 // ROLE.WALL — rendered as 'wall' by the preview's feature colouring
17
+ const STL_HEADER_BYTES = 80
18
+ const STL_DATA_OFFSET = 84 // header + the uint32 triangle count
19
+ const STL_TRIANGLE_BYTES = 50
14
20
  const CONTOUR_WIDTH = 0.2 // display width of a contour segment (SLA has no bead width)
15
21
  const PLANE_EPS = 1e-9 // a vertex exactly on a plane counts as above it — deterministic tie-break
16
22
  const KEY_SCALE = 1e4 // endpoint welding: 0.1 micrometre grid
@@ -18,13 +24,13 @@ const KEY_SCALE = 1e4 // endpoint welding: 0.1 micrometre grid
18
24
  /** Binary STL -> Float32Array of 9 floats per triangle (vertices only; normals are recomputed). */
19
25
  export function parseBinarySTL(bytes) {
20
26
  if (!(bytes instanceof Uint8Array)) bytes = new Uint8Array(bytes)
21
- if (bytes.byteLength < 84) return { error: 'not a binary STL (shorter than its own header)' }
27
+ if (bytes.byteLength < STL_DATA_OFFSET) return { error: 'not a binary STL (shorter than its own header)' }
22
28
  const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
23
- const count = dv.getUint32(80, true)
24
- if (84 + 50 * count !== bytes.byteLength) return { error: 'not a binary STL (triangle count does not match the byte length)' }
29
+ const count = dv.getUint32(STL_HEADER_BYTES, true)
30
+ if (STL_DATA_OFFSET + STL_TRIANGLE_BYTES * count !== bytes.byteLength) return { error: 'not a binary STL (triangle count does not match the byte length)' }
25
31
  const tris = new Float32Array(count * 9)
26
32
  for (let i = 0; i < count; i++) {
27
- const base = 84 + 50 * i + 12 // skip the stored normal — recomputed from the winding below
33
+ const base = STL_DATA_OFFSET + STL_TRIANGLE_BYTES * i + 12 // skip the stored normal — recomputed from the winding below
28
34
  for (let f = 0; f < 9; f++) tris[i * 9 + f] = dv.getFloat32(base + 4 * f, true)
29
35
  }
30
36
  return { tris, count }
@@ -149,15 +155,15 @@ export function sliceSla(stlBytes, params = {}, hooks = {}) {
149
155
  for (const pts of loops) { area += loopArea(pts); segCount += pts.length / 2 }
150
156
  area = Math.max(0, area)
151
157
  volume += area * thickness
152
- const paths = new Float32Array(segCount * 8)
158
+ const paths = new Float32Array(segCount * STRIDE)
153
159
  const widths = new Float32Array(segCount)
154
160
  let w = 0
155
161
  for (const pts of loops) {
156
162
  const n = pts.length / 2
157
163
  for (let p = 0; p < n; p++) {
158
164
  const q = (p + 1) % n
159
- paths[w * 8] = pts[p * 2]; paths[w * 8 + 1] = pts[p * 2 + 1]; paths[w * 8 + 2] = top; paths[w * 8 + 3] = ROLE_CONTOUR
160
- paths[w * 8 + 4] = pts[q * 2]; paths[w * 8 + 5] = pts[q * 2 + 1]; paths[w * 8 + 6] = top; paths[w * 8 + 7] = ROLE_CONTOUR
165
+ paths[w * STRIDE] = pts[p * 2]; paths[w * STRIDE + 1] = pts[p * 2 + 1]; paths[w * STRIDE + 2] = top; paths[w * STRIDE + 3] = ROLE_CONTOUR
166
+ paths[w * STRIDE + 4] = pts[q * 2]; paths[w * STRIDE + 5] = pts[q * 2 + 1]; paths[w * STRIDE + 6] = top; paths[w * STRIDE + 7] = ROLE_CONTOUR
161
167
  widths[w] = CONTOUR_WIDTH
162
168
  w++
163
169
  }
@@ -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 reply = { type: 'painted', enf: Module.selector_painted_count(true), blk: Module.selector_painted_count(false) }
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) reply.counts = Object.fromEntries(states.map(state => [state, Module.selector_painted_count_state(state)]))
138
- return reply
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
- self.postMessage({ type: 'paintExport', supported: false, facets: [], hex: '' }); return
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) => self.postMessage({ type: 'progress', 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
- self.postMessage({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
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) { self.postMessage({ type: 'error', error: String(r.error) }); return }
170
- self.postMessage({ type: 'done', result: withSliceThroughput(r, performance.now() - started) }); return
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
- self.postMessage({ type: 'error', code: 'SLA_UNSUPPORTED_OBJECT_AWARE',
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) => self.postMessage({ type: 'progress', 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
- self.postMessage({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
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) { self.postMessage({ type: 'error', error: String(r.error) }); return }
196
- self.postMessage({ type: 'done', result: withSliceThroughput(r, performance.now() - started) })
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
- self.postMessage({ type: 'layer', z: L.z, idx: L.idx, gcode: '', paths: L.paths, widths: L.widths }, transfer)
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) { self.postMessage({ type: 'error', error: String(r.error) }); return }
215
- self.postMessage({ type: 'done', result: withSliceThroughput(r, performance.now() - startedFallback) })
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') { self.postMessage({ type: 'warm', kernel: kernelKind }); return }
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
- self.postMessage({ type: 'prepared', facets: Module.selector_facet_count(), kept })
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
- self.postMessage(paintedReply(Module, d, paintedState)); return
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
- self.postMessage(paintedReply(Module, d, null)); return
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 reply = paintedReply(Module, d, null)
273
- reply.applied = applied
274
- self.postMessage(reply); return
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
- self.postMessage({ type: 'paintExport', supported: !!Module.selector_export_paint,
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 reply = { type: 'painted', enf: 0, blk: 0 }
297
+ const clearedReply = { type: 'painted', enf: 0, blk: 0 }
291
298
  const states = reportedPaintStates(d, null)
292
- if (states) reply.counts = Object.fromEntries(states.map(state => [state, 0]))
293
- self.postMessage(reply); return
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 reply = { type: 'overlay',
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) reply.overlays = Object.fromEntries(states.map(state => [state, Module.selector_overlay_state(state)]))
312
- const transfer = [reply.enf, reply.blk, ...Object.values(reply.overlays ?? {})]
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
- self.postMessage(reply, transfer); return
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) { self.postMessage({ type: 'fillPreview', supported: false, tris: EMPTY_OVERLAY }); return }
323
- if (d.clear) { Module.selector_fill_preview_clear(); self.postMessage({ type: 'fillPreview', supported: true, tris: EMPTY_OVERLAY }); return }
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
- self.postMessage({ type: 'fillPreview', supported: true, tris },
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
- self.postMessage({ type: 'paintMode', ok: true }); return
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
- self.postMessage({ type: 'supsab', buf: v.buffer, ptr: v.byteOffset, cancelPtr: c ? c.byteOffset : 0 })
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) => self.postMessage({ type: 'progress', 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
- self.postMessage({ type: 'layer', z, idx, gcode, paths, widths }, transfer)
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) { self.postMessage({ type: 'error', error: String(r.error) }); return }
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
- self.postMessage({ type: 'done', result: withSliceThroughput(withSliceWarnings(r), performance.now() - started) })
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
- self.postMessage({ type: 'error', error: String((err && err.message) || err), ...(err?.code ? { code: err.code } : {}) })
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.0",
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",
@@ -90,27 +90,24 @@
90
90
  },
91
91
  "files": [
92
92
  "engine",
93
- "!engine/**/.omc",
94
93
  "types",
95
- "!**/test_*.mjs",
94
+ "!**/tests/**",
96
95
  "!types/gen_*.mjs",
97
96
  "components/dist",
98
97
  "viewer/dist",
99
- "viewer/styles.css",
100
- "components/styles.css",
101
98
  "data",
102
99
  "data/ui-tree-keys.json",
103
100
  "data/config-schema-lean.json",
104
101
  "!data/config-schema-builddump.json",
105
- "!data/**/.omc",
106
102
  "README.md",
107
103
  "CHANGELOG.md",
108
104
  "THIRD-PARTY-NOTICES.md",
109
- "LICENSE.txt"
105
+ "LICENSE.txt",
106
+ "!**/.omc"
110
107
  ],
111
108
  "repository": {
112
109
  "type": "git",
113
- "url": "git+https://github.com/kimgh06/Web_Three_Slicer.git",
110
+ "url": "git+https://github.com/kimgh06/Three_Slicer.git",
114
111
  "directory": "packages"
115
112
  },
116
113
  "homepage": "https://slicer.kimgh06.com",
@@ -137,6 +134,6 @@
137
134
  "clipping"
138
135
  ],
139
136
  "dependencies": {
140
- "three-slicer-viewer": "0.3.0"
137
+ "three-slicer-viewer": "0.3.2"
141
138
  }
142
139
  }
@@ -172,6 +172,18 @@ export function printerDefaultPreset(profileName: string): string
172
172
  /** The schema keys the kernel's machine limits are read from — for UI that shows which ones are in play. */
173
173
  export const machineLimitKeys: string[]
174
174
 
175
+ /** deriveKernelParams' fallbacks when neither the settings map nor the schema gives a usable value, keyed by schema
176
+ * key. NOT the schema defaults — several differ on purpose. Frozen. */
177
+ export const FFF_FALLBACKS: Readonly<Record<string, number | boolean | string>>
178
+ /** deriveSlaParams' fallbacks (its reader treats non-positive values as absent). Frozen. */
179
+ export const SLA_FALLBACKS: Readonly<Record<string, number>>
180
+ /** What `line_width` 0 ("auto") resolves to. */
181
+ export const AUTO_LINE_WIDTH: number
182
+ /** The bed when `printable_area` is empty or malformed — the bbox of the schema's own default. */
183
+ export const BED_FALLBACK: Readonly<{ width: number; depth: number }>
184
+ /** Kernel machine-limit parameter -> [schema key, fallback]. Frozen. */
185
+ export const MACHINE_LIMITS: Readonly<Record<string, readonly [string, number]>>
186
+
175
187
  // ---- Lazily loaded presets --------------------------------------------------
176
188
  // Both facades hide the column layout of the artifact they wrap; the promise is created once and reused.
177
189
 
@@ -1,10 +0,0 @@
1
- {
2
- "version": 1,
3
- "entries": {
4
- "79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
5
- "last_emitted_at_ms": 1788437194779,
6
- "message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
7
- }
8
- },
9
- "updated_at": "2026-09-03T12:06:34.779Z"
10
- }