three-slicer 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -0
- package/README.md +6 -0
- package/data/filaments.js +1 -1
- package/data/printers.json +1 -1
- package/data/processes.js +1 -1
- package/engine/PARAMS.md +37 -7
- package/engine/src/contour_front_gpu.js +291 -0
- package/engine/src/contour_front_shaders.js +449 -0
- package/engine/src/contour_gpu.js +367 -0
- package/engine/src/contour_gpu_shaders.js +450 -0
- package/engine/src/contour_slice.js +131 -0
- package/engine/src/settings.js +22 -12
- package/engine/src/slicer.worker.js +100 -45
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/engine/src/slicer_core.mt.wasm +0 -0
- package/package.json +2 -2
- package/types/data/filaments.js.d.ts +6 -1
- package/types/data/printers.json.d.ts +10 -3
- package/types/data/processes.js.d.ts +6 -1
- package/types/settings.d.ts +2 -0
- package/types/worker.d.ts +4 -1
package/engine/src/settings.js
CHANGED
|
@@ -32,6 +32,19 @@ export function resinSettingsFor(name) {
|
|
|
32
32
|
return out
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// One row of a preset table -> the settings it applies. Multi-line text (custom G-code, notes) is stored once in
|
|
36
|
+
// `text` and a row holds its index, for the columns listed in `textKeys` (web/extract_all.py _intern_text).
|
|
37
|
+
function rowSettings(data, row) {
|
|
38
|
+
const textKeys = new Set(data.textKeys ?? [])
|
|
39
|
+
const out = {}
|
|
40
|
+
data.keys.forEach((key, i) => {
|
|
41
|
+
if (row[i] == null) return
|
|
42
|
+
if (textKeys.has(key)) { out[key] = data.text[row[i]]; return }
|
|
43
|
+
out[key] = row[i]
|
|
44
|
+
})
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
// Process (print) presets live in the ~800 KB processes.json, so they load on demand — the first call fetches,
|
|
36
49
|
// later ones reuse the same promise. Returns a small facade so no caller has to know the column layout.
|
|
37
50
|
let processesPromise = null
|
|
@@ -46,9 +59,7 @@ export function processPresets() {
|
|
|
46
59
|
settingsFor: (presetName) => {
|
|
47
60
|
const preset = data.presets.find(([name]) => name === presetName)
|
|
48
61
|
if (!preset) return null
|
|
49
|
-
|
|
50
|
-
data.keys.forEach((key, i) => { if (row[i] != null) out[key] = row[i] })
|
|
51
|
-
return out
|
|
62
|
+
return rowSettings(data, data.sets[preset[1]])
|
|
52
63
|
},
|
|
53
64
|
}))
|
|
54
65
|
return processesPromise
|
|
@@ -84,9 +95,7 @@ export function filamentPresets() {
|
|
|
84
95
|
settingsFor: (presetName) => {
|
|
85
96
|
const preset = data.presets.find(([name]) => name === presetName)
|
|
86
97
|
if (!preset) return null
|
|
87
|
-
|
|
88
|
-
data.keys.forEach((key, i) => { if (row[i] != null) out[key] = row[i] })
|
|
89
|
-
return out
|
|
98
|
+
return rowSettings(data, data.sets[preset[1]])
|
|
90
99
|
},
|
|
91
100
|
}
|
|
92
101
|
})
|
|
@@ -105,19 +114,20 @@ function printerEntry(profileName) {
|
|
|
105
114
|
export function printerSettings(profileName) {
|
|
106
115
|
const entry = printerEntry(profileName)
|
|
107
116
|
if (!entry) return null
|
|
108
|
-
|
|
109
|
-
const out = {}
|
|
110
|
-
printers.keys.forEach((key, i) => { if (row[i] != null) out[key] = row[i] })
|
|
111
|
-
return out
|
|
117
|
+
return rowSettings(printers, printers.sets[entry[1]])
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
/** The vendor's recommended process preset for a printer, or '' when the profile names none. */
|
|
115
121
|
export function printerDefaultPreset(profileName) { return printerEntry(profileName)?.[3] ?? '' }
|
|
116
122
|
|
|
123
|
+
/** The bed type the printer's model defaults to ("Textured PEI Plate"), or '' when the model names none. Upstream
|
|
124
|
+
* writes it to curr_bed_type when the printer is picked; the first-layer bed temperature follows it. */
|
|
125
|
+
export function printerDefaultBedType(profileName) { return printerEntry(profileName)?.[4] ?? '' }
|
|
126
|
+
|
|
117
127
|
// ---- The catalog as one object -------------------------------------------------------------------------------
|
|
118
|
-
// What `three-slicer-viewer`'s `<Viewport catalog>` takes: the
|
|
128
|
+
// What `three-slicer-viewer`'s `<Viewport catalog>` takes: the ten lookups above, bundled. The permissive package
|
|
119
129
|
// ships no catalog (these are OrcaSlicer's profile bundles); `three-slicer/viewer` passes this one by default.
|
|
120
130
|
export const bundledCatalog = Object.freeze({
|
|
121
|
-
printerKeys, printerSettings, printersByVendor, printerTechByVendor, printerDefaultPreset,
|
|
131
|
+
printerKeys, printerSettings, printersByVendor, printerTechByVendor, printerDefaultPreset, printerDefaultBedType,
|
|
122
132
|
processPresets, filamentPresets, resinCatalog, resinSettingsFor,
|
|
123
133
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Runs slicing off the main thread (non-blocking UI) plus stage-30 layer streaming.
|
|
2
2
|
// Vite module worker: new Worker(new URL('./slicer.worker.js', import.meta.url), { type: 'module' }).
|
|
3
|
-
// SINGLE_FILE means the wasm is inlined into slicer_core.js -> no external fetch from the worker either.
|
|
3
|
+
// SINGLE_FILE means the wasm is inlined into slicer_core.js -> no external fetch from the worker either. The mt glue
|
|
4
|
+
// loads slicer_core.mt.wasm from beside itself instead: its pthread workers each parse the glue (build.sh, mt link).
|
|
4
5
|
//
|
|
5
6
|
// Stage 30 (OOM tolerance): set_layer_sink lets the kernel emit layers as it produces them; each is transferred to main
|
|
6
7
|
// immediately (Float32Array buffers moved -> the worker copy is released at once) and the kernel frees that layer buffer
|
|
@@ -18,6 +19,9 @@
|
|
|
18
19
|
import { assertLegacySlaFallback, parseSlaJob } from './sla_request.js'
|
|
19
20
|
import { withSliceWarnings } from './warnings.js'
|
|
20
21
|
import { withSliceThroughput } from './throughput.js'
|
|
22
|
+
import { acquireContourDevice, makeContourGpu } from './contour_gpu.js'
|
|
23
|
+
import { kernelHasContourPhase, sliceWithContourGpu } from './contour_slice.js'
|
|
24
|
+
import { FILL_TOOLS } from 'three-slicer-viewer/paint'
|
|
21
25
|
|
|
22
26
|
// The kernel parses `params` as JSON text, so the raw protocol used to require a string — while the direct handle
|
|
23
27
|
// and createSlicerClient both take an object and stringify it for you. One object, two shapes, and the difference
|
|
@@ -49,6 +53,42 @@ const loadCore = async () => {
|
|
|
49
53
|
|
|
50
54
|
let modPromise = null
|
|
51
55
|
|
|
56
|
+
// The contour union on the GPU (contour_gpu.js), made on the first slice that asks for it. `false` once this worker
|
|
57
|
+
// found no usable device, so the next slice does not ask again; a lost device is asked for again.
|
|
58
|
+
let contourGpu = null
|
|
59
|
+
const contourGpuOf = async () => {
|
|
60
|
+
if (contourGpu !== null) return contourGpu
|
|
61
|
+
try {
|
|
62
|
+
const device = await acquireContourDevice(self.navigator?.gpu)
|
|
63
|
+
if (!device) { contourGpu = false; return contourGpu }
|
|
64
|
+
contourGpu = await makeContourGpu(device)
|
|
65
|
+
device.lost.then(() => { contourGpu = null })
|
|
66
|
+
} catch (error) {
|
|
67
|
+
say('warn', '[slicer.worker] contour GPU unavailable:', error)
|
|
68
|
+
contourGpu = false
|
|
69
|
+
}
|
|
70
|
+
return contourGpu
|
|
71
|
+
}
|
|
72
|
+
// A slice's kernel call, through the GPU when the host asked for it (`gpu: true`) and this worker can.
|
|
73
|
+
const runSlice = async (Module, d, onProgress) => {
|
|
74
|
+
const stl = new Uint8Array(d.stl), text = paramsText(d.params)
|
|
75
|
+
if (!d.gpu) return Module.slice(stl, text, onProgress)
|
|
76
|
+
const cpu = (reason) => {
|
|
77
|
+
const result = Module.slice(stl, text, onProgress)
|
|
78
|
+
if (result?.stats) { result.stats.contour_engine = 'cpu'; result.stats.contour_engine_reason = reason }
|
|
79
|
+
return result
|
|
80
|
+
}
|
|
81
|
+
if (!kernelHasContourPhase(Module)) return cpu('this kernel build has no contour phase')
|
|
82
|
+
const gpu = await contourGpuOf()
|
|
83
|
+
if (!gpu) return cpu('no WebGPU device')
|
|
84
|
+
const canceled = () => { const flag = Module.cancel_flag_view?.(); return !!flag && flag[0] !== 0 }
|
|
85
|
+
// The mesh route (the GPU cuts and chains the layers too) only on the single-threaded kernel. Measured on a 3M-facet
|
|
86
|
+
// model (node, Dawn): st's own cut and chain is 419-442 ms against 34-154 ms of mesh hand-over + 64-153 ms on the GPU;
|
|
87
|
+
// mt's is 64-77 ms on its threads, and the mesh route came out 20 ms slower in the front and 0.02-0.18 s in the slice.
|
|
88
|
+
const meshRoute = kernelKind === 'st'
|
|
89
|
+
return sliceWithContourGpu({ kernel: Module, contourGpu: gpu, stl, paramsText: text, onProgress, canceled, meshRoute })
|
|
90
|
+
}
|
|
91
|
+
|
|
52
92
|
// Stage 20 painting states = upstream's EnforcerBlockerType (see packages/wasm-core/selector_bridge.h):
|
|
53
93
|
// 0=NONE, 1=ENFORCER(==Extruder1), 2=BLOCKER(==Extruder2), 3..16=Extruder3..Extruder16.
|
|
54
94
|
// The kernel exposes both a boolean pair (selector_paint/…_count/…_overlay) and integer-state twins (…_state);
|
|
@@ -110,7 +150,6 @@ const strokeFrom = (message) =>
|
|
|
110
150
|
// The fill modes the kernel's fill_preview takes (selector_bridge.h FILL_*), by the tool name the protocol uses.
|
|
111
151
|
const FILL_PREVIEW_MODE = { smart: 0, bucket: 1, triangle: 2 }
|
|
112
152
|
|
|
113
|
-
const FILL_TOOLS = new Set(['smart', 'bucket', 'triangle'])
|
|
114
153
|
const applyFill = (Module, message, state) => {
|
|
115
154
|
const angle = Number.isFinite(message.angle) ? message.angle : 30
|
|
116
155
|
if (message.tool === 'smart') {
|
|
@@ -132,14 +171,28 @@ const applyFill = (Module, message, state) => {
|
|
|
132
171
|
// listener predating the per-state map keeps working; `counts` appears only when the message asked for states,
|
|
133
172
|
// which is what keeps a legacy paint reply byte-identical to what it was before the protocol was widened.
|
|
134
173
|
const paintedReply = (Module, message, paintedState) => {
|
|
135
|
-
const
|
|
174
|
+
const answer = { type: 'painted', enf: Module.selector_painted_count(true), blk: Module.selector_painted_count(false) }
|
|
136
175
|
const states = reportedPaintStates(message, paintedState)
|
|
137
|
-
if (states)
|
|
138
|
-
return
|
|
176
|
+
if (states) answer.counts = Object.fromEntries(states.map(state => [state, Module.selector_painted_count_state(state)]))
|
|
177
|
+
return answer
|
|
139
178
|
}
|
|
140
179
|
|
|
141
|
-
|
|
180
|
+
// One message at a time. A GPU slice awaits the device between its two kernel calls, and a message handled in that
|
|
181
|
+
// gap would reach a kernel that is holding another slice's loops.
|
|
182
|
+
let messageChain = Promise.resolve()
|
|
183
|
+
self.onmessage = (e) => {
|
|
184
|
+
messageChain = messageChain.then(() => handleMessage(e)).catch((error) => say('warn', '[slicer.worker] message failed:', error))
|
|
185
|
+
return messageChain // a caller that drives the handler directly (tests) can await its message
|
|
186
|
+
}
|
|
187
|
+
const handleMessage = async (e) => {
|
|
142
188
|
const d = e.data
|
|
189
|
+
// Every reply to a request that carried a `requestId` carries it back, the error reply included — so a caller
|
|
190
|
+
// waiting on one request cannot take another request's answer (or another command's error) for its own. A
|
|
191
|
+
// request without one gets exactly the replies it always got.
|
|
192
|
+
const reply = (message, transfer) => {
|
|
193
|
+
if (d?.requestId === undefined) { self.postMessage(message, transfer); return }
|
|
194
|
+
self.postMessage({ ...message, requestId: d.requestId }, transfer)
|
|
195
|
+
}
|
|
143
196
|
// Read before the kernel loads: loadCore() below is what prints, so a `quiet` arriving with the first message
|
|
144
197
|
// (the warmup) has to take effect before that call, not after it.
|
|
145
198
|
if (d.quiet !== undefined) quiet = !!d.quiet
|
|
@@ -148,28 +201,28 @@ self.onmessage = async (e) => {
|
|
|
148
201
|
// has never loaded it then selector_prepare has never run either, so there is no painting to report — and
|
|
149
202
|
// making a save wait on a 4MB WASM load before the first slice is seconds of nothing, for an empty answer.
|
|
150
203
|
if (d.cmd === 'exportPaint' && !modPromise) {
|
|
151
|
-
|
|
204
|
+
reply({ type: 'paintExport', supported: false, facets: [], hex: '' }); return
|
|
152
205
|
}
|
|
153
206
|
if (d.cmd === 'slaJob') {
|
|
154
207
|
const job = parseSlaJob(d.job)
|
|
155
208
|
if (!modPromise) modPromise = loadCore()
|
|
156
209
|
const Kernel = await modPromise
|
|
157
|
-
const onProgress = (done, total) =>
|
|
210
|
+
const onProgress = (done, total) => reply({ type: 'progress', done, total })
|
|
158
211
|
if (Kernel.slice_sla_job) {
|
|
159
212
|
Kernel.set_layer_sink((z, idx, gcode, paths, widths) => {
|
|
160
213
|
const transfer = []
|
|
161
214
|
if (paths?.buffer) transfer.push(paths.buffer)
|
|
162
215
|
if (widths?.buffer) transfer.push(widths.buffer)
|
|
163
|
-
|
|
216
|
+
reply({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
|
|
164
217
|
})
|
|
165
218
|
let r
|
|
166
219
|
const started = performance.now()
|
|
167
220
|
try { r = Kernel.slice_sla_job(job, onProgress) }
|
|
168
221
|
finally { Kernel.clear_layer_sink() }
|
|
169
|
-
if (r?.error) {
|
|
170
|
-
|
|
222
|
+
if (r?.error) { reply({ type: 'error', error: String(r.error) }); return }
|
|
223
|
+
reply({ type: 'done', result: withSliceThroughput(r, performance.now() - started) }); return
|
|
171
224
|
}
|
|
172
|
-
|
|
225
|
+
reply({ type: 'error', code: 'SLA_UNSUPPORTED_OBJECT_AWARE',
|
|
173
226
|
error: 'Object-aware SLA slicing requires WASM kernel support' })
|
|
174
227
|
return
|
|
175
228
|
}
|
|
@@ -180,20 +233,20 @@ self.onmessage = async (e) => {
|
|
|
180
233
|
const params = paramsText(d.params)
|
|
181
234
|
if (!modPromise) modPromise = loadCore()
|
|
182
235
|
const Kernel = await modPromise
|
|
183
|
-
const onProgress = (done, total) =>
|
|
236
|
+
const onProgress = (done, total) => reply({ type: 'progress', done, total })
|
|
184
237
|
if (Kernel.slice_sla) {
|
|
185
238
|
Kernel.set_layer_sink((z, idx, gcode, paths, widths) => {
|
|
186
239
|
const transfer = []
|
|
187
240
|
if (paths && paths.buffer) transfer.push(paths.buffer)
|
|
188
241
|
if (widths && widths.buffer) transfer.push(widths.buffer)
|
|
189
|
-
|
|
242
|
+
reply({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
|
|
190
243
|
})
|
|
191
244
|
let r
|
|
192
245
|
const started = performance.now()
|
|
193
246
|
try { r = Kernel.slice_sla(new Uint8Array(d.stl), params, onProgress) }
|
|
194
247
|
finally { Kernel.clear_layer_sink() }
|
|
195
|
-
if (r && r.error) {
|
|
196
|
-
|
|
248
|
+
if (r && r.error) { reply({ type: 'error', error: String(r.error) }); return }
|
|
249
|
+
reply({ type: 'done', result: withSliceThroughput(r, performance.now() - started) })
|
|
197
250
|
return
|
|
198
251
|
}
|
|
199
252
|
const { sliceSla } = await import('./sla_core.js')
|
|
@@ -208,23 +261,23 @@ self.onmessage = async (e) => {
|
|
|
208
261
|
const transfer = []
|
|
209
262
|
if (L.paths?.buffer) transfer.push(L.paths.buffer)
|
|
210
263
|
if (L.widths?.buffer) transfer.push(L.widths.buffer)
|
|
211
|
-
|
|
264
|
+
reply({ type: 'layer', z: L.z, idx: L.idx, gcode: '', paths: L.paths, widths: L.widths }, transfer)
|
|
212
265
|
},
|
|
213
266
|
})
|
|
214
|
-
if (r && r.error) {
|
|
215
|
-
|
|
267
|
+
if (r && r.error) { reply({ type: 'error', error: String(r.error) }); return }
|
|
268
|
+
reply({ type: 'done', result: withSliceThroughput(r, performance.now() - startedFallback) })
|
|
216
269
|
return
|
|
217
270
|
}
|
|
218
271
|
if (!modPromise) modPromise = loadCore()
|
|
219
272
|
const Module = await modPromise
|
|
220
273
|
// 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') {
|
|
274
|
+
if (d.cmd === 'warmup') { reply({ type: 'warm', kernel: kernelKind }); return }
|
|
222
275
|
// Stage 20: manual support painting — selector state persists in this worker Module (slicing uses the same Module).
|
|
223
276
|
// `keepPaint` says the mesh is the same model in a new place, so the marks carry over — the reply reports whether
|
|
224
277
|
// they actually did, since a face-count change makes the kernel fall back to a clean registration.
|
|
225
278
|
if (d.cmd === 'prepare') {
|
|
226
279
|
const kept = d.keepPaint ? Module.selector_reprepare(new Uint8Array(d.stl)) : (Module.selector_prepare(new Uint8Array(d.stl)), false)
|
|
227
|
-
|
|
280
|
+
reply({ type: 'prepared', facets: Module.selector_facet_count(), kept })
|
|
228
281
|
return
|
|
229
282
|
}
|
|
230
283
|
// paint: legacy {enforcer:boolean} stays on the boolean binding (false == BLOCKER); {state:1..16} reaches any
|
|
@@ -242,7 +295,7 @@ self.onmessage = async (e) => {
|
|
|
242
295
|
else if (strokeFrom(d) && Module.selector_paint_stroke)
|
|
243
296
|
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
297
|
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
|
-
|
|
298
|
+
reply(paintedReply(Module, d, paintedState)); return
|
|
246
299
|
}
|
|
247
300
|
// erase: the brush writes NONE, returning the brushed facets to the default extruder (upstream's shift+drag).
|
|
248
301
|
// It is its OWN command and NOT `{cmd:'paint', state:0}`, because 0 is the one integer a boolean can become:
|
|
@@ -259,7 +312,7 @@ self.onmessage = async (e) => {
|
|
|
259
312
|
// the paint brush never touched. `selector_erase` (no shape) stays the fallback for an older kernel.
|
|
260
313
|
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
314
|
else Module.selector_erase(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius)
|
|
262
|
-
|
|
315
|
+
reply(paintedReply(Module, d, null)); return
|
|
263
316
|
}
|
|
264
317
|
// importPaint: load painting out of a 3mf. `facets` is an Int32Array of facet indices in the CURRENT selector's
|
|
265
318
|
// numbering and `hex` the newline-joined per-facet split-tree strings the 3mf carried, in the same order.
|
|
@@ -269,9 +322,9 @@ self.onmessage = async (e) => {
|
|
|
269
322
|
// rather than throwing, because losing a 3mf's paint must not also lose its geometry.
|
|
270
323
|
if (d.cmd === 'importPaint') {
|
|
271
324
|
const applied = Module.selector_import_paint ? Module.selector_import_paint(d.facets, d.hex ?? '') : 0
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
325
|
+
const importedReply = paintedReply(Module, d, null)
|
|
326
|
+
importedReply.applied = applied
|
|
327
|
+
reply(importedReply); return
|
|
275
328
|
}
|
|
276
329
|
// exportPaint: the reverse of importPaint — every marked facet's split tree as {facets, hex} in the CURRENT
|
|
277
330
|
// selector's numbering, the same parallel-array pairing importPaint takes (hex is one newline-joined blob).
|
|
@@ -280,17 +333,17 @@ self.onmessage = async (e) => {
|
|
|
280
333
|
// reports supported:false rather than throwing — the save then falls back to the paint it imported.
|
|
281
334
|
if (d.cmd === 'exportPaint') {
|
|
282
335
|
const exported = Module.selector_export_paint ? Module.selector_export_paint() : null
|
|
283
|
-
|
|
336
|
+
reply({ type: 'paintExport', supported: !!Module.selector_export_paint,
|
|
284
337
|
facets: exported?.facets ?? [], hex: exported?.hex ?? '' })
|
|
285
338
|
return
|
|
286
339
|
}
|
|
287
340
|
// clear wipes every state at once, so each requested count is 0 by construction — no need to ask the kernel back.
|
|
288
341
|
if (d.cmd === 'clear') {
|
|
289
342
|
Module.selector_clear()
|
|
290
|
-
const
|
|
343
|
+
const clearedReply = { type: 'painted', enf: 0, blk: 0 }
|
|
291
344
|
const states = reportedPaintStates(d, null)
|
|
292
|
-
if (states)
|
|
293
|
-
|
|
345
|
+
if (states) clearedReply.counts = Object.fromEntries(states.map(state => [state, 0]))
|
|
346
|
+
reply(clearedReply); return
|
|
294
347
|
}
|
|
295
348
|
// overlay: `enf`/`blk` (states 1/2) are always sent because the viewer's overlay rebuild consumes exactly those two;
|
|
296
349
|
// `overlays` adds the requested states, so an MMU caller can ask for 3..16 without losing the support overlays.
|
|
@@ -305,13 +358,13 @@ self.onmessage = async (e) => {
|
|
|
305
358
|
// enf/blk stay unconditional for the listener that predates the per-state map. When the request names states,
|
|
306
359
|
// they are the ONLY ones read, so building the other two would be pure waste on the hot path.
|
|
307
360
|
const wanted = states && !states.includes(PAINT_STATE_ENFORCER) && !states.includes(PAINT_STATE_BLOCKER)
|
|
308
|
-
const
|
|
361
|
+
const overlayReply = { type: 'overlay',
|
|
309
362
|
enf: wanted ? EMPTY_OVERLAY : Module.selector_overlay(true),
|
|
310
363
|
blk: wanted ? EMPTY_OVERLAY : Module.selector_overlay(false) }
|
|
311
|
-
if (states)
|
|
312
|
-
const transfer = [
|
|
364
|
+
if (states) overlayReply.overlays = Object.fromEntries(states.map(state => [state, Module.selector_overlay_state(state)]))
|
|
365
|
+
const transfer = [overlayReply.enf, overlayReply.blk, ...Object.values(overlayReply.overlays ?? {})]
|
|
313
366
|
.filter(a => a?.buffer && a.byteLength > 0).map(a => a.buffer)
|
|
314
|
-
|
|
367
|
+
reply(overlayReply, transfer); return
|
|
315
368
|
}
|
|
316
369
|
|
|
317
370
|
// fillPreview: what a fill WOULD select, without applying it. Upstream runs this on every mouse move while a
|
|
@@ -319,14 +372,14 @@ self.onmessage = async (e) => {
|
|
|
319
372
|
// nothing, so it needs no state at all and there is no NONE for a stray boolean to become. `{clear:true}`
|
|
320
373
|
// drops the standing selection, which is what leaving the model under the pointer means.
|
|
321
374
|
if (d.cmd === 'fillPreview') {
|
|
322
|
-
if (!Module.selector_fill_preview) {
|
|
323
|
-
if (d.clear) { Module.selector_fill_preview_clear();
|
|
375
|
+
if (!Module.selector_fill_preview) { reply({ type: 'fillPreview', supported: false, tris: EMPTY_OVERLAY }); return }
|
|
376
|
+
if (d.clear) { Module.selector_fill_preview_clear(); reply({ type: 'fillPreview', supported: true, tris: EMPTY_OVERLAY }); return }
|
|
324
377
|
const angle = Number.isFinite(d.angle) ? d.angle : 30
|
|
325
378
|
const mode = FILL_PREVIEW_MODE[d.tool] ?? FILL_PREVIEW_MODE.smart
|
|
326
379
|
// Upstream's -1 for the non-propagating case, the same value applyFill sends: one facet has no neighbour to
|
|
327
380
|
// measure an angle against.
|
|
328
381
|
const tris = Module.selector_fill_preview(d.facet, d.hx, d.hy, d.hz, mode === FILL_PREVIEW_MODE.triangle ? -1 : angle, mode)
|
|
329
|
-
|
|
382
|
+
reply({ type: 'fillPreview', supported: true, tris },
|
|
330
383
|
tris?.buffer && tris.byteLength > 0 ? [tris.buffer] : [])
|
|
331
384
|
return
|
|
332
385
|
}
|
|
@@ -339,7 +392,7 @@ self.onmessage = async (e) => {
|
|
|
339
392
|
if (plane) Module.selector_set_clip_plane(plane[0], plane[1], plane[2], plane[3], true)
|
|
340
393
|
else if (d.clipPlane === null) Module.selector_set_clip_plane(0, 0, 1, 0, false)
|
|
341
394
|
}
|
|
342
|
-
|
|
395
|
+
reply({ type: 'paintMode', ok: true }); return
|
|
343
396
|
}
|
|
344
397
|
|
|
345
398
|
if (d.stall) return // stage-30 test hook: simulate a hang -> verifies the main-thread watchdog fires (not set in production)
|
|
@@ -352,32 +405,34 @@ self.onmessage = async (e) => {
|
|
|
352
405
|
const v = Module.sup_progress_view && Module.sup_progress_view()
|
|
353
406
|
const c = Module.cancel_flag_view && Module.cancel_flag_view()
|
|
354
407
|
if (v && v.buffer instanceof SharedArrayBuffer)
|
|
355
|
-
|
|
408
|
+
reply({ type: 'supsab', buf: v.buffer, ptr: v.byteOffset, cancelPtr: c?.byteOffset ?? 0 })
|
|
356
409
|
} catch {}
|
|
357
410
|
}
|
|
358
411
|
// Default: slice. Register the layer sink -> the kernel calls back per layer (z, idx, gcodeChunk, pathsF32, widthsF32).
|
|
359
412
|
// 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) =>
|
|
413
|
+
const onProgress = (done, total) => reply({ type: 'progress', done, total })
|
|
361
414
|
Module.set_layer_sink((z, idx, gcode, paths, widths) => {
|
|
362
415
|
const transfer = []
|
|
363
416
|
if (paths && paths.buffer) transfer.push(paths.buffer) // economy mode yields empty arrays (no .buffer) -> nothing to transfer
|
|
364
417
|
if (widths && widths.buffer) transfer.push(widths.buffer)
|
|
365
|
-
|
|
418
|
+
reply({ type: 'layer', z, idx, gcode, paths, widths }, transfer)
|
|
366
419
|
})
|
|
367
420
|
let r
|
|
368
421
|
// Timed around the kernel call INCLUDING the layer sink, which posts each layer to the main thread from inside
|
|
369
422
|
// it — that transfer is part of what a streamed slice costs and leaving it out would flatter the number.
|
|
370
423
|
const started = performance.now()
|
|
371
|
-
try { r =
|
|
424
|
+
try { r = await runSlice(Module, d, onProgress) }
|
|
372
425
|
finally { Module.clear_layer_sink() }
|
|
373
|
-
if (r && r.error) {
|
|
426
|
+
if (r && r.error) { reply({ type: 'error', error: String(r.error) }); return }
|
|
374
427
|
// streamed=true -> g-code/layers were already emitted as 'layer' (result holds stats only). batch/MM keep them in result.
|
|
375
428
|
// withSliceWarnings names what the slice got away with (an off-bed model) on the result itself, and
|
|
376
429
|
// withSliceThroughput how fast it ran — the raw protocol carries both, so createSlicerClient's callers get
|
|
377
430
|
// them without the client having to re-derive anything.
|
|
378
|
-
|
|
431
|
+
reply({ type: 'done', result: withSliceThroughput(withSliceWarnings(r), performance.now() - started) })
|
|
379
432
|
} catch (err) {
|
|
380
433
|
// Includes the WASM abort("memory access out of bounds") — the main thread's OOM ladder decides on re-creation / economy retry.
|
|
381
|
-
|
|
434
|
+
const errorReply = { type: 'error', error: String((err && err.message) || err) }
|
|
435
|
+
if (err?.code) errorReply.code = err.code
|
|
436
|
+
reply(errorReply)
|
|
382
437
|
}
|
|
383
438
|
}
|
|
Binary file
|
|
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.3",
|
|
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.3"
|
|
138
138
|
}
|
|
139
139
|
}
|
|
@@ -11,10 +11,15 @@ export type FilamentRow = (unknown | null)[]
|
|
|
11
11
|
export type FilamentEntry = [string, number, string, string]
|
|
12
12
|
|
|
13
13
|
export interface FilamentData {
|
|
14
|
-
/** Option keys, in column order — the kernel keys that at least one filament profile sets */
|
|
14
|
+
/** Option keys, in column order — the kernel keys and custom-G-code keys that at least one filament profile sets */
|
|
15
15
|
keys: string[]
|
|
16
16
|
/** Deduplicated value rows */
|
|
17
17
|
sets: FilamentRow[]
|
|
18
|
+
/** Columns whose cells are indices into `text` rather than values: the multi-line options (custom G-code, notes),
|
|
19
|
+
* stored once because many rows share one. Absent when the table has none. */
|
|
20
|
+
textKeys?: string[]
|
|
21
|
+
/** The multi-line values the `textKeys` columns index */
|
|
22
|
+
text?: unknown[]
|
|
18
23
|
/** Preset entries, indexed by the numbers in `byPrinter` / `defaultsByModel` */
|
|
19
24
|
presets: FilamentEntry[]
|
|
20
25
|
/** Printer profile name (as keyed in printers.json) -> indices into `presets` — the compatible materials */
|
|
@@ -7,14 +7,21 @@
|
|
|
7
7
|
/** A positional row aligned to `keys`; `null` where the profile chain never set that option. */
|
|
8
8
|
export type PrinterRow = (unknown | null)[]
|
|
9
9
|
|
|
10
|
-
/** `[nozzle diameter, index into sets, model name without the nozzle suffix, default process preset
|
|
11
|
-
|
|
10
|
+
/** `[nozzle diameter, index into sets, model name without the nozzle suffix, default process preset,
|
|
11
|
+
* the model's default bed type ('' when none)]` — SLA entries carry the first four */
|
|
12
|
+
export type PrinterEntry = [string, number, string, string, string?]
|
|
12
13
|
|
|
13
14
|
export interface PrinterData {
|
|
14
|
-
/** Option keys, in column order — the same keys the settings panel edits
|
|
15
|
+
/** Option keys, in column order — the same keys the settings panel edits, including the custom G-code and the
|
|
16
|
+
* options it reads (issue 63) */
|
|
15
17
|
keys: string[]
|
|
16
18
|
/** Deduplicated value rows */
|
|
17
19
|
sets: PrinterRow[]
|
|
20
|
+
/** Columns whose cells are indices into `text` rather than values: the multi-line options (custom G-code, notes),
|
|
21
|
+
* stored once because many rows share one. Absent when the table has none. */
|
|
22
|
+
textKeys?: string[]
|
|
23
|
+
/** The multi-line values the `textKeys` columns index */
|
|
24
|
+
text?: unknown[]
|
|
18
25
|
/** Vendor -> printer profile name -> entry */
|
|
19
26
|
byVendor: Record<string, Record<string, PrinterEntry>>
|
|
20
27
|
}
|
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
export type ProcessRow = (unknown | null)[]
|
|
8
8
|
|
|
9
9
|
export interface ProcessData {
|
|
10
|
-
/** Option keys, in column order — the keys the kernel consumes
|
|
10
|
+
/** Option keys, in column order — the keys the kernel consumes plus the ones its custom G-code reads (extract_all.py _preset_keys) */
|
|
11
11
|
keys: string[]
|
|
12
12
|
/** Deduplicated value rows */
|
|
13
13
|
sets: ProcessRow[]
|
|
14
|
+
/** Columns whose cells are indices into `text` rather than values: the multi-line options (custom G-code, notes),
|
|
15
|
+
* stored once because many rows share one. Absent when the table has none. */
|
|
16
|
+
textKeys?: string[]
|
|
17
|
+
/** The multi-line values the `textKeys` columns index */
|
|
18
|
+
text?: unknown[]
|
|
14
19
|
/** `[preset name, index into sets]`, indexed by the numbers in `byPrinter` */
|
|
15
20
|
presets: [string, number][]
|
|
16
21
|
/** Printer profile name -> indices into `presets` */
|
package/types/settings.d.ts
CHANGED
|
@@ -168,6 +168,8 @@ export function printerSettings(profileName: string): SlicerSettings | null
|
|
|
168
168
|
|
|
169
169
|
/** The vendor's recommended process preset for that printer, or `''` when the profile names none. */
|
|
170
170
|
export function printerDefaultPreset(profileName: string): string
|
|
171
|
+
/** The bed type the printer's model defaults to, `''` when the model names none — written to `curr_bed_type` on a pick. */
|
|
172
|
+
export function printerDefaultBedType(profileName: string): string
|
|
171
173
|
|
|
172
174
|
/** The schema keys the kernel's machine limits are read from — for UI that shows which ones are in play. */
|
|
173
175
|
export const machineLimitKeys: string[]
|
package/types/worker.d.ts
CHANGED
|
@@ -94,8 +94,11 @@ export type SlicerRequest =
|
|
|
94
94
|
* stringified for you (it was string-only through 0.2.2, which made this the one call path with a different
|
|
95
95
|
* contract from `createSlicer().slice()` and `createSlicerClient().slice()`).
|
|
96
96
|
* Replies: `progress` and `layer` while it runs, then `done` — or `error`.
|
|
97
|
+
* `gpu: true` asks the worker to compute the layer contours with WebGPU (src/contour_gpu.js). The worker uses it
|
|
98
|
+
* when it has a device and the slice reaches the contour union; `done`'s `stats.contour_engine` says what ran
|
|
99
|
+
* ('gpu', 'cpu' with `contour_engine_reason`, or 'reused' for layers from the stage cache).
|
|
97
100
|
*/
|
|
98
|
-
| { stl: ArrayBuffer; params: string | object }
|
|
101
|
+
| { stl: ArrayBuffer; params: string | object; gpu?: boolean }
|
|
99
102
|
|
|
100
103
|
export type SlicerResponse =
|
|
101
104
|
/** `kernel` says which variant loaded: 'mt' under crossOriginIsolated, 'st' otherwise (or after an mt load failure). */
|