three-slicer 0.3.2 → 0.3.4
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 +134 -0
- package/README.md +6 -0
- package/data/config-schema-lean.json +1 -1
- package/data/config-schema.json +6 -34
- package/data/filaments.js +1 -1
- package/data/printers.json +1 -1
- package/data/processes.js +1 -1
- package/engine/PARAMS.md +86 -8
- 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 +25 -13
- package/engine/src/slicer.worker.js +50 -4
- 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 +13 -3
- package/types/data/processes.js.d.ts +6 -1
- package/types/settings.d.ts +16 -1
- 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,40 +95,41 @@ 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
|
})
|
|
93
102
|
return filamentsPromise
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
// `parents` holds the abstract machine presets (`instantiation: false`): never offered by the picker, but a user
|
|
106
|
+
// preset file names one as its `inherits`, so printerSettings has to resolve it by name.
|
|
96
107
|
function printerEntry(profileName) {
|
|
97
108
|
for (const models of Object.values(printers.byVendor)) {
|
|
98
109
|
const entry = models[profileName]
|
|
99
110
|
if (entry) return entry
|
|
100
111
|
}
|
|
101
|
-
return null
|
|
112
|
+
return printers.parents?.[profileName] ?? null
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
/** The settings a printer profile applies, ready to merge into the settings map. `null` when unknown. */
|
|
105
116
|
export function printerSettings(profileName) {
|
|
106
117
|
const entry = printerEntry(profileName)
|
|
107
118
|
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
|
|
119
|
+
return rowSettings(printers, printers.sets[entry[1]])
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
/** The vendor's recommended process preset for a printer, or '' when the profile names none. */
|
|
115
123
|
export function printerDefaultPreset(profileName) { return printerEntry(profileName)?.[3] ?? '' }
|
|
116
124
|
|
|
125
|
+
/** The bed type the printer's model defaults to ("Textured PEI Plate"), or '' when the model names none. Upstream
|
|
126
|
+
* writes it to curr_bed_type when the printer is picked; the first-layer bed temperature follows it. */
|
|
127
|
+
export function printerDefaultBedType(profileName) { return printerEntry(profileName)?.[4] ?? '' }
|
|
128
|
+
|
|
117
129
|
// ---- The catalog as one object -------------------------------------------------------------------------------
|
|
118
|
-
// What `three-slicer-viewer`'s `<Viewport catalog>` takes: the
|
|
130
|
+
// What `three-slicer-viewer`'s `<Viewport catalog>` takes: the ten lookups above, bundled. The permissive package
|
|
119
131
|
// ships no catalog (these are OrcaSlicer's profile bundles); `three-slicer/viewer` passes this one by default.
|
|
120
132
|
export const bundledCatalog = Object.freeze({
|
|
121
|
-
printerKeys, printerSettings, printersByVendor, printerTechByVendor, printerDefaultPreset,
|
|
133
|
+
printerKeys, printerSettings, printersByVendor, printerTechByVendor, printerDefaultPreset, printerDefaultBedType,
|
|
122
134
|
processPresets, filamentPresets, resinCatalog, resinSettingsFor,
|
|
123
135
|
})
|
|
@@ -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') {
|
|
@@ -138,7 +177,14 @@ const paintedReply = (Module, message, paintedState) => {
|
|
|
138
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
|
|
143
189
|
// Every reply to a request that carried a `requestId` carries it back, the error reply included — so a caller
|
|
144
190
|
// waiting on one request cannot take another request's answer (or another command's error) for its own. A
|
|
@@ -375,7 +421,7 @@ self.onmessage = async (e) => {
|
|
|
375
421
|
// Timed around the kernel call INCLUDING the layer sink, which posts each layer to the main thread from inside
|
|
376
422
|
// it — that transfer is part of what a streamed slice costs and leaving it out would flatter the number.
|
|
377
423
|
const started = performance.now()
|
|
378
|
-
try { r =
|
|
424
|
+
try { r = await runSlice(Module, d, onProgress) }
|
|
379
425
|
finally { Module.clear_layer_sink() }
|
|
380
426
|
if (r && r.error) { reply({ type: 'error', error: String(r.error) }); return }
|
|
381
427
|
// streamed=true -> g-code/layers were already emitted as 'layer' (result holds stats only). batch/MM keep them in result.
|
|
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.4",
|
|
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.4"
|
|
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,16 +7,26 @@
|
|
|
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>>
|
|
27
|
+
/** The abstract machine presets (`instantiation: false`) by name: not offered for picking, but resolvable as
|
|
28
|
+
* the parent a user preset file `inherits` */
|
|
29
|
+
parents?: Record<string, PrinterEntry>
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
declare const printers: PrinterData
|
|
@@ -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
|
@@ -17,7 +17,20 @@ export function settingScalar(settings: SlicerSettings | null | undefined, key:
|
|
|
17
17
|
* ponytail: 53 returned keys, so Record instead of listing each — kernel params are passed straight to slice(),
|
|
18
18
|
* not an API consumers read field by field. Expand it when field access is actually needed.
|
|
19
19
|
*/
|
|
20
|
-
export function deriveKernelParams(settings: SlicerSettings | null | undefined, opts?: { plate?: number }): Record<string, unknown>
|
|
20
|
+
export function deriveKernelParams(settings: SlicerSettings | null | undefined, opts?: { plate?: number; objectNames?: string[] }): Record<string, unknown>
|
|
21
|
+
/**
|
|
22
|
+
* The printable area's lower-left corner and its centre in printer coordinates, from deriveKernelParams' output.
|
|
23
|
+
* The kernel slices in a plate frame centred on the bed and adds the centre on output, so a stored printer
|
|
24
|
+
* coordinate (wipe_tower_x/y, a 3mf position, G-code) converts to that frame by subtracting `bedCenter`.
|
|
25
|
+
*/
|
|
26
|
+
export function bedOrigin(params: Record<string, unknown> | null | undefined): { x: number; y: number }
|
|
27
|
+
/** Code of a printer profile setting the template path reads but does not do yet. */
|
|
28
|
+
export const PARTIAL_SUPPORT_CODE: 'PROFILE_PARTIAL_SUPPORT'
|
|
29
|
+
/** The tool change settings a profile sets that the template path does not apply (empty off the template path or
|
|
30
|
+
* with one tool), each with what happens instead. */
|
|
31
|
+
export function partialSupport(settings: SlicerSettings | null | undefined, opts?: { toolCount?: number }):
|
|
32
|
+
{ code: 'PROFILE_PARTIAL_SUPPORT'; key: string; effect: string }[]
|
|
33
|
+
export function bedCenter(params: Record<string, unknown> | null | undefined): { x: number; y: number }
|
|
21
34
|
|
|
22
35
|
/**
|
|
23
36
|
* Clear-then-merge preset application: deletes `keys` (the catalog's own key set) from `settings`, then
|
|
@@ -168,6 +181,8 @@ export function printerSettings(profileName: string): SlicerSettings | null
|
|
|
168
181
|
|
|
169
182
|
/** The vendor's recommended process preset for that printer, or `''` when the profile names none. */
|
|
170
183
|
export function printerDefaultPreset(profileName: string): string
|
|
184
|
+
/** The bed type the printer's model defaults to, `''` when the model names none — written to `curr_bed_type` on a pick. */
|
|
185
|
+
export function printerDefaultBedType(profileName: string): string
|
|
171
186
|
|
|
172
187
|
/** The schema keys the kernel's machine limits are read from — for UI that shows which ones are in play. */
|
|
173
188
|
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). */
|