three-slicer 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +310 -22
- package/THIRD-PARTY-NOTICES.md +37 -0
- package/components/README.md +2 -0
- package/components/dist/SettingsPanel.js +92 -92
- package/data/config-schema.json +698 -0
- package/data/filaments.js +1 -1
- package/data/preset-keys.json +1 -0
- package/data/printers.json +1 -1
- package/data/processes.js +1 -1
- package/data/ui-tree.json +342 -0
- package/engine/README.md +279 -3
- package/engine/examples/sla_headless.mjs +51 -0
- package/engine/index.d.ts +77 -5
- package/engine/index.js +15 -4
- package/engine/src/client.js +165 -0
- package/engine/src/data.js +3 -0
- package/engine/src/preset_file.js +99 -0
- package/engine/src/settings.js +232 -0
- package/engine/src/sla_core.js +188 -0
- package/engine/src/sla_request.js +185 -0
- package/engine/src/slicer.worker.js +76 -4
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/package.json +26 -11
- package/types/client.d.ts +88 -0
- package/types/components.d.ts +10 -0
- package/types/data/preset-keys.json.d.ts +20 -0
- package/types/data/printers.json.d.ts +2 -2
- package/types/data.d.ts +4 -0
- package/types/settings-keys.d.ts +107 -1
- package/types/settings.d.ts +116 -1
- package/types/sla.d.ts +167 -0
- package/types/viewer.d.ts +59 -1
- package/types/worker.d.ts +95 -0
- package/viewer/README.md +163 -3
- package/viewer/dist/Viewport.js +3633 -3137
- package/viewer/dist/model_loaders.js +1 -1
- package/viewer/dist/parse_3mf-C2X_W442.js +300 -0
- package/viewer/dist/parse_3mf.worker.js +1 -1
- package/viewer/styles.css +5 -0
- package/types/gen_settings_types.mjs +0 -70
- package/viewer/dist/parse_3mf-8oCnCJNK.js +0 -216
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// three-slicer SLA (resin) headless example: slice a cube with supports + pad through the public API,
|
|
2
|
+
// then show the typed capability refusal — an unsupported request errors, it never approximates.
|
|
3
|
+
// run: node packages/engine/examples/sla_headless.mjs
|
|
4
|
+
import { createSlicer } from 'three-slicer'
|
|
5
|
+
import { deriveSlaParams, printerTechnology } from 'three-slicer/settings'
|
|
6
|
+
|
|
7
|
+
// minimal binary-STL writer for a 10mm cube. Winding matters here, unlike the FFF example: the SLA
|
|
8
|
+
// support generator reads facet normals to find downward faces, so every face is CCW seen from outside.
|
|
9
|
+
function boxSTL() {
|
|
10
|
+
const vertices = [[-5,-5,0],[5,-5,0],[5,5,0],[-5,5,0],[-5,-5,10],[5,-5,10],[5,5,10],[-5,5,10]]
|
|
11
|
+
const faces = [[0,2,1],[0,3,2],[4,5,6],[4,6,7],[0,1,5],[0,5,4],[1,2,6],[1,6,5],[2,3,7],[2,7,6],[3,0,4],[3,4,7]]
|
|
12
|
+
const buf = Buffer.alloc(84 + faces.length * 50)
|
|
13
|
+
buf.writeUInt32LE(faces.length, 80)
|
|
14
|
+
faces.forEach((face, i) => {
|
|
15
|
+
let off = 84 + i * 50 + 12
|
|
16
|
+
for (const vi of face) for (const v of vertices[vi]) { buf.writeFloatLE(v, off); off += 4 }
|
|
17
|
+
})
|
|
18
|
+
return buf
|
|
19
|
+
}
|
|
20
|
+
const cube = boxSTL()
|
|
21
|
+
|
|
22
|
+
// A sparse settings map, the same shape the UI holds. printer_technology is what routes to slice_sla.
|
|
23
|
+
const settings = {
|
|
24
|
+
printer_technology: 'SLA',
|
|
25
|
+
layer_height: 0.05,
|
|
26
|
+
exposure_time: 7,
|
|
27
|
+
supports_enable: true,
|
|
28
|
+
support_object_elevation: 5,
|
|
29
|
+
pad_enable: true,
|
|
30
|
+
}
|
|
31
|
+
console.log(`technology: ${printerTechnology(settings)}`)
|
|
32
|
+
|
|
33
|
+
const slicer = await createSlicer()
|
|
34
|
+
const params = deriveSlaParams(settings) // fills display/support/pad defaults (SL1 reference machine)
|
|
35
|
+
|
|
36
|
+
// (a) SLA slice — no G-code; masks, meshes and resin stats instead.
|
|
37
|
+
const r = slicer.sliceSla(cube, params)
|
|
38
|
+
if (r.error) { console.error('FAIL', r.error); process.exit(1) }
|
|
39
|
+
console.log(`sla : layers=${r.stats.layers} (lift=${r.stats.lift_layers}) points=${r.stats.support_points}`
|
|
40
|
+
+ ` pad_layers=${r.stats.pad_layers} resin=${r.stats.resin_ml.toFixed(2)}ml`)
|
|
41
|
+
console.log(`meshes: support=${r.support_mesh.length / 9} tris, pad=${r.pad_mesh.length / 9} tris`)
|
|
42
|
+
|
|
43
|
+
// (b) the capability gate: hollowing is not ported, so the request is REFUSED with a stable code —
|
|
44
|
+
// a solid slice answered to a hollow request would be a mislabeled print.
|
|
45
|
+
const refused = slicer.sliceSla(cube, deriveSlaParams({ ...settings, hollowing_enable: true }))
|
|
46
|
+
console.log(`gate : ${refused.error}`)
|
|
47
|
+
|
|
48
|
+
if (r.stats.support_points > 0 && r.stats.pad_layers > 0 && /^SLA_UNSUPPORTED_HOLLOWING/.test(refused.error ?? ''))
|
|
49
|
+
console.log('OK — SLA slices headlessly via the public API')
|
|
50
|
+
else { console.error('FAIL'); process.exit(1) }
|
|
51
|
+
slicer.dispose()
|
package/engine/index.d.ts
CHANGED
|
@@ -6,22 +6,94 @@ export interface SliceCallbacks {
|
|
|
6
6
|
onProgress?: (done: number, total: number) => void
|
|
7
7
|
onLayer?: (layer: SliceLayer) => void
|
|
8
8
|
}
|
|
9
|
+
/** The estimator's machine limits, as they were actually applied (`stats.machine_limits`). */
|
|
10
|
+
export interface SliceMachineLimits {
|
|
11
|
+
max_speed_xy: number; max_speed_z: number; max_speed_e: number
|
|
12
|
+
max_accel_xy: number; max_accel_z: number; max_accel_e: number
|
|
13
|
+
jerk_xy: number; jerk_z: number; jerk_e: number
|
|
14
|
+
accel_print: number; accel_travel: number; accel_retract: number
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
export interface SliceStats {
|
|
10
|
-
layers
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
/** Emitted layers, raft included — the length of `result.layers` */
|
|
19
|
+
layers: number
|
|
20
|
+
/** Layers belonging to the model itself */
|
|
21
|
+
model_layers: number
|
|
22
|
+
raft_layers: number
|
|
23
|
+
path_segments: number
|
|
24
|
+
/** Filament consumed, mm of input filament */
|
|
25
|
+
filament_mm: number
|
|
26
|
+
/** Wall-crossing travels — the cross-check for `reduce_crossing_wall` */
|
|
27
|
+
wall_crossings: number
|
|
28
|
+
|
|
29
|
+
/** Seconds. `0` in economy mode, which skips the estimate */
|
|
30
|
+
time_estimate: number
|
|
31
|
+
first_layer_time: number
|
|
32
|
+
time_extrude: number
|
|
33
|
+
time_travel: number
|
|
34
|
+
/** Filament total the estimator parsed back out of the G-code — compare against `filament_mm` */
|
|
35
|
+
time_filament_mm: number
|
|
36
|
+
time_moves: number
|
|
37
|
+
/** Seconds per layer index */
|
|
38
|
+
layer_times: Record<string, number>
|
|
39
|
+
/** Seconds per extrusion role, keyed by the role number (the low nibble of `paths[k+3]`) */
|
|
40
|
+
role_times: Record<string, number>
|
|
41
|
+
/** Which estimator ran: the ported GCodeProcessor, the transcription, or neither */
|
|
42
|
+
time_engine: 'full' | 'transcribed' | 'fallback' | string
|
|
43
|
+
machine_limits: SliceMachineLimits
|
|
44
|
+
|
|
45
|
+
/** True when anything printed leaves the bed — support, skirt, brim and raft included */
|
|
46
|
+
over_bed: boolean
|
|
47
|
+
/** True when the MODEL itself is off the bed, not just what was printed around it */
|
|
48
|
+
over_bed_model: boolean
|
|
49
|
+
/** How far past the bed the toolpaths reach, mm. `0` = inside */
|
|
50
|
+
over_bed_x: number; over_bed_y: number
|
|
51
|
+
/** Emitted top layer past `printable_height`, mm */
|
|
52
|
+
over_bed_z: number
|
|
53
|
+
|
|
54
|
+
/** True when G-code and layers were delivered through `onLayer` and are NOT on the result */
|
|
55
|
+
streamed: boolean
|
|
56
|
+
/** True when it finished in economy mode: no preview toolpaths and no time estimate */
|
|
57
|
+
economy: boolean
|
|
58
|
+
|
|
59
|
+
// ---- multi-material only (absent from a single-material slice) ----
|
|
60
|
+
/** Indexed by tool number; sums to `filament_mm` */
|
|
61
|
+
filament_mm_by_tool?: number[]
|
|
62
|
+
/** The prime/wipe tower's share — already counted inside the per-tool figures above */
|
|
63
|
+
filament_mm_purge?: number
|
|
64
|
+
filament_mm_purge_by_tool?: number[]
|
|
65
|
+
|
|
66
|
+
// ---- phase timings, present when the kernel was built with them ----
|
|
67
|
+
t_pass1_ms?: number; t_surface_ms?: number; t_support_ms?: number; t_emit_ms?: number
|
|
68
|
+
/** The JS boundary's share during emit */
|
|
69
|
+
t_flush_ms?: number
|
|
70
|
+
|
|
71
|
+
[k: string]: unknown
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SliceResult {
|
|
75
|
+
/** Absent when `onLayer` was set — assemble it from the callback instead */
|
|
76
|
+
gcode?: string
|
|
77
|
+
stats: SliceStats
|
|
78
|
+
/** Absent when `onLayer` was set */
|
|
79
|
+
layers?: unknown[]
|
|
80
|
+
/** Set instead of a result when the slice failed or was cancelled (`'canceled'`) */
|
|
81
|
+
error?: string
|
|
13
82
|
}
|
|
14
|
-
export interface SliceResult { gcode?: string; stats: SliceStats; layers?: unknown[]; error?: string }
|
|
15
83
|
export interface PaintArgs {
|
|
16
84
|
facet: number; hx: number; hy: number; hz: number;
|
|
17
85
|
cx: number; cy: number; cz: number; radius: number; enforcer: boolean
|
|
18
86
|
}
|
|
19
87
|
export interface Slicer {
|
|
20
88
|
slice(stl: ArrayBuffer | Uint8Array, params: object | string, cb?: SliceCallbacks): SliceResult
|
|
89
|
+
/** SLA (resin) slice — no G-code: mask segment streams per layer, support/pad meshes, resin stats.
|
|
90
|
+
* Params from deriveSlaParams(); a refused capability (e.g. hollowing) sets `error` to its typed code. */
|
|
91
|
+
sliceSla(stl: ArrayBuffer | Uint8Array, params: object | string, onProgress?: (done: number, total: number) => void): SliceResult
|
|
21
92
|
paintPrepare(stl: ArrayBuffer | Uint8Array): number
|
|
22
93
|
paint(args: PaintArgs): { enf: number; blk: number }
|
|
23
94
|
paintClear(): void
|
|
24
|
-
overlay
|
|
95
|
+
/** The painted overlay triangles for one state, as the kernel's own detached copy. */
|
|
96
|
+
overlay(enforcer: boolean): Float32Array
|
|
25
97
|
heapSize(): number
|
|
26
98
|
module: unknown
|
|
27
99
|
dispose(): void
|
package/engine/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// three-slicer — browser/WASM 3D-slicing SDK (reverse-engineered from OrcaSlicer).
|
|
2
|
-
// Public API. Node-safe (
|
|
2
|
+
// Public API. Node-safe (loads only the pure-JS WASM factory; no JSON so it loads in plain Node ESM).
|
|
3
3
|
// Schema-driven helpers (deriveKernelParams etc.) live in the "three-slicer/settings" subpath because
|
|
4
4
|
// they import JSON — use them in a bundler (vite) or with Node JSON import attributes.
|
|
5
|
-
import createModule from './src/slicer_core.js'
|
|
6
5
|
|
|
7
6
|
const u8 = (b) => (b instanceof Uint8Array ? b : new Uint8Array(b))
|
|
8
7
|
|
|
@@ -21,7 +20,10 @@ export const engineWorkerURL = () => new URL('./src/slicer.worker.js', import.me
|
|
|
21
20
|
// paintPrepare/paint/paintClear/overlay: manual support enforcer/blocker painting (TriangleSelector).
|
|
22
21
|
// heapSize(): current WASM heap bytes (peak, monotonic). module: escape hatch. dispose(): drop the module.
|
|
23
22
|
export async function createSlicer() {
|
|
24
|
-
|
|
23
|
+
// Loaded here rather than at module scope so the 3.5MB emscripten glue is a separate chunk: a host that imports
|
|
24
|
+
// this module only for engineWorkerURL() — the recommended browser path, where the kernel runs in the worker —
|
|
25
|
+
// used to pull the whole thing into its bundle for a function that returns a URL.
|
|
26
|
+
const M = await (await import('./src/slicer_core.js')).default()
|
|
25
27
|
return {
|
|
26
28
|
slice(stl, params, { onProgress, onLayer } = {}) {
|
|
27
29
|
const p = typeof params === 'string' ? params : JSON.stringify(params || {})
|
|
@@ -29,13 +31,22 @@ export async function createSlicer() {
|
|
|
29
31
|
try { return M.slice(u8(stl), p, onProgress || (() => {})) }
|
|
30
32
|
finally { if (onLayer) M.clear_layer_sink() }
|
|
31
33
|
},
|
|
34
|
+
// SLA (resin) slice: no G-code — layers carry the mask segment stream, support_mesh/pad_mesh the preview
|
|
35
|
+
// soups, stats the resin figures and lift_layers. Same params-object-or-string contract as slice().
|
|
36
|
+
sliceSla(stl, params, onProgress) {
|
|
37
|
+
const p = typeof params === 'string' ? params : JSON.stringify(params || {})
|
|
38
|
+
return M.slice_sla(u8(stl), p, onProgress || (() => {}))
|
|
39
|
+
},
|
|
32
40
|
paintPrepare(stl) { M.selector_prepare(u8(stl)); return M.selector_facet_count() },
|
|
33
41
|
paint(a) {
|
|
34
42
|
M.selector_paint(a.facet, a.hx, a.hy, a.hz, a.cx, a.cy, a.cz, a.radius, a.enforcer)
|
|
35
43
|
return { enf: M.selector_painted_count(true), blk: M.selector_painted_count(false) }
|
|
36
44
|
},
|
|
37
45
|
paintClear() { M.selector_clear() },
|
|
38
|
-
|
|
46
|
+
// Returned as the kernel's own Float32Array. It is already a detached copy (bindings.cpp to_f32 slices the
|
|
47
|
+
// memory view), and the Array.from() this used to do cost 1.1ms of a measured 3.6ms stroke on a 40k-triangle
|
|
48
|
+
// mesh — the same conversion the worker dropped for the same reason (slicer.worker.js, 'overlay').
|
|
49
|
+
overlay(enforcer) { return M.selector_overlay(!!enforcer) },
|
|
39
50
|
heapSize() { return M.heap_size() },
|
|
40
51
|
module: M,
|
|
41
52
|
dispose() { /* emscripten module is GC'd when dropped; no explicit teardown required */ },
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// three-slicer/client — a promise wrapper over the slice worker's message protocol.
|
|
2
|
+
//
|
|
3
|
+
// Why it exists: the worker is the recommended way to slice in a browser (the kernel blocks its thread for
|
|
4
|
+
// seconds), but driving it meant hand-writing a `cmd` string protocol, assembling the streamed layers, knowing
|
|
5
|
+
// that a slice request is the one message with NO cmd and that its params must already be a JSON string, and
|
|
6
|
+
// reaching into a SharedArrayBuffer at an offset the worker posts once to cancel anything. All of that lived in
|
|
7
|
+
// the viewer and was not published.
|
|
8
|
+
//
|
|
9
|
+
// What it deliberately does NOT do: the viewer's progress weighting (its 15/5/40/40 phase budget is measured
|
|
10
|
+
// against ITS models) and its out-of-memory retry ladder (re-slicing simplified, then in economy mode). Those are
|
|
11
|
+
// UI policy — a host wanting them should own them, and lifting them here would make this a copy of the viewer.
|
|
12
|
+
//
|
|
13
|
+
// Requests are answered in order. The worker's onmessage handler processes one message at a time and each command
|
|
14
|
+
// replies exactly once, so a FIFO of pending requests is enough to match a reply to its caller — the protocol
|
|
15
|
+
// carries no request ids and does not need them.
|
|
16
|
+
import { engineWorkerURL } from '../index.js'
|
|
17
|
+
import { parseSlaJob, SlaRequestError } from './sla_request.js'
|
|
18
|
+
|
|
19
|
+
export { SLA_CAPABILITIES, SLA_JOB_VERSION, SlaRequestError } from './sla_request.js'
|
|
20
|
+
|
|
21
|
+
// Which reply type ends which command. A slice has no cmd (that is what selects slicing) and ends on 'done'.
|
|
22
|
+
const REPLY_OF = {
|
|
23
|
+
warmup: 'warm', prepare: 'prepared', paint: 'painted', erase: 'painted', clear: 'painted',
|
|
24
|
+
importPaint: 'painted', exportPaint: 'paintExport', overlay: 'overlay', slice: 'done', sla: 'done',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const asBuffer = (data) => (data instanceof ArrayBuffer ? data : data?.buffer ?? data)
|
|
28
|
+
const slaTransferables = (job) => job.objects.flatMap(object => [object, ...(object.modifierVolumes ?? [])])
|
|
29
|
+
.map(record => asBuffer(record.stl)).filter(buffer => buffer instanceof ArrayBuffer)
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {Worker} [worker] an existing worker to drive. Omit to create one — which only works in a browser,
|
|
33
|
+
* since Node has no global Worker; a Node caller should use createSlicer() from the root entry instead.
|
|
34
|
+
*/
|
|
35
|
+
export function createSlicerClient(worker = new Worker(engineWorkerURL(), { type: 'module' })) {
|
|
36
|
+
const pending = [] // FIFO of {expect, resolve, reject, onProgress, onLayer, chunks, layers}
|
|
37
|
+
let cancelFlag = null // Uint32Array over the worker's SharedArrayBuffer — mt kernel only
|
|
38
|
+
let closed = false
|
|
39
|
+
|
|
40
|
+
worker.addEventListener('message', ({ data }) => {
|
|
41
|
+
// Unprompted and not a reply to anything: the addresses of the progress counter and the cancel flag.
|
|
42
|
+
if (data.type === 'supsab') {
|
|
43
|
+
try { if (data.cancelPtr) cancelFlag = new Uint32Array(data.buf, data.cancelPtr, 1) } catch { /* not shared */ }
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
const head = pending[0]
|
|
47
|
+
if (!head) return
|
|
48
|
+
if (data.type === 'progress') { head.onProgress?.(data.done, data.total); return }
|
|
49
|
+
if (data.type === 'layer') {
|
|
50
|
+
head.onLayer?.({ z: data.z, idx: data.idx, gcode: data.gcode, paths: data.paths, widths: data.widths })
|
|
51
|
+
// Accumulated only when the caller did not take the layers itself — otherwise a streamed slice would hold
|
|
52
|
+
// the whole model in memory here, which is the one thing streaming exists to avoid.
|
|
53
|
+
if (!head.onLayer) { if (data.gcode) head.chunks.push(data.gcode); head.layers.push(data) }
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
pending.shift()
|
|
57
|
+
if (data.type === 'error') {
|
|
58
|
+
head.reject(data.code ? new SlaRequestError(data.code, data.error) : new Error(data.error)); return
|
|
59
|
+
}
|
|
60
|
+
if (data.type !== head.expect) { head.reject(new Error(`expected '${head.expect}', got '${data.type}'`)); return }
|
|
61
|
+
// What the layer messages accumulated, if this caller did not take them itself.
|
|
62
|
+
if (data.type === 'done' && head.layers.length) data.assembled = { gcode: head.chunks.join(''), layers: head.layers }
|
|
63
|
+
head.resolve(data)
|
|
64
|
+
})
|
|
65
|
+
worker.addEventListener('error', (event) => {
|
|
66
|
+
const head = pending.shift()
|
|
67
|
+
head?.reject(new Error(event.message || 'worker failed'))
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const send = (message, { transfer = [], onProgress, onLayer } = {}) => new Promise((resolve, reject) => {
|
|
71
|
+
if (closed) { reject(new Error('slicer client terminated')); return }
|
|
72
|
+
pending.push({ expect: REPLY_OF[message.cmd ?? 'slice'], resolve, reject, onProgress, onLayer, chunks: [], layers: [] })
|
|
73
|
+
worker.postMessage(message, transfer)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const brush = (cmd, args) => send({ ...args, cmd }).then(reply => ({
|
|
77
|
+
enforcer: reply.enf, blocker: reply.blk, counts: reply.counts, applied: reply.applied,
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
worker,
|
|
82
|
+
|
|
83
|
+
/** Load the kernel ahead of the first slice, so the first one is not also a 4MB download. */
|
|
84
|
+
warmup: () => send({ cmd: 'warmup' }).then(() => undefined),
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Slice. `params` may be an object — it is stringified here, which the raw protocol does not do for you.
|
|
88
|
+
* With `onLayer` the layers are yours and the result carries stats only; without it they are assembled and
|
|
89
|
+
* the result looks like the direct handle's.
|
|
90
|
+
*/
|
|
91
|
+
async slice(stl, params, { onProgress, onLayer } = {}) {
|
|
92
|
+
const buffer = asBuffer(stl)
|
|
93
|
+
const entry = { stl: buffer, params: typeof params === 'string' ? params : JSON.stringify(params ?? {}) }
|
|
94
|
+
const reply = await send(entry, { transfer: [buffer], onProgress, onLayer })
|
|
95
|
+
const result = reply.result ?? {}
|
|
96
|
+
// A streamed slice returns stats only, with the content delivered as `layer` messages. Unless the caller
|
|
97
|
+
// took those itself, hand back the same shape a batch slice produces.
|
|
98
|
+
if (!reply.assembled) return result
|
|
99
|
+
return { ...result, gcode: reply.assembled.gcode, layers: reply.assembled.layers }
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Legacy single-object SLA slice: per-layer closed contours instead of toolpaths. It uses WASM when available;
|
|
104
|
+
* its contour-only JS fallback rejects requested supports and pads. Same streaming shape as slice(): `onLayer` hands you the layers and the result
|
|
105
|
+
* carries stats only; without it they are assembled onto the result.
|
|
106
|
+
*/
|
|
107
|
+
async sliceSla(stl, params, { onProgress, onLayer } = {}) {
|
|
108
|
+
const buffer = asBuffer(stl)
|
|
109
|
+
const entry = { cmd: 'sla', stl: buffer, params: typeof params === 'string' ? params : JSON.stringify(params ?? {}) }
|
|
110
|
+
const reply = await send(entry, { transfer: [buffer], onProgress, onLayer })
|
|
111
|
+
const result = reply.result ?? {}
|
|
112
|
+
if (!reply.assembled) return result
|
|
113
|
+
return { ...result, layers: reply.assembled.layers }
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
async sliceSlaJob(job, { onProgress, onLayer } = {}) {
|
|
117
|
+
const parsed = parseSlaJob(job)
|
|
118
|
+
const reply = await send({ cmd: 'slaJob', job: parsed }, { transfer: slaTransferables(parsed), onProgress, onLayer })
|
|
119
|
+
const result = reply.result ?? {}
|
|
120
|
+
if (!reply.assembled) return result
|
|
121
|
+
return { ...result, layers: reply.assembled.layers }
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/** Register a mesh for painting. `keepPaint` carries existing marks across a move. */
|
|
125
|
+
prepare: (stl, { keepPaint = false } = {}) => {
|
|
126
|
+
const buffer = asBuffer(stl)
|
|
127
|
+
return send({ cmd: 'prepare', stl: buffer, keepPaint }, { transfer: [buffer] })
|
|
128
|
+
.then(reply => ({ facets: reply.facets, kept: reply.kept }))
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/** Paint. `{state: 1..16}` addresses any extruder; `{enforcer: false}` is the legacy blocker brush. */
|
|
132
|
+
paint: (args) => brush('paint', args),
|
|
133
|
+
/** Return the brushed facets to the default extruder. Not `paint` with state 0 — see types/worker.d.ts. */
|
|
134
|
+
erase: (args) => brush('erase', args),
|
|
135
|
+
clear: (states) => brush('clear', states ? { states } : {}),
|
|
136
|
+
|
|
137
|
+
overlay: (states) => send({ cmd: 'overlay', ...(states ? { states } : {}) })
|
|
138
|
+
.then(reply => ({ enforcer: reply.enf, blocker: reply.blk, overlays: reply.overlays })),
|
|
139
|
+
|
|
140
|
+
/** Load a 3mf's painting. Replaces every mark, so run it directly after `prepare`. */
|
|
141
|
+
importPaint: (facets, hex, states) => brush('importPaint', { facets, hex, ...(states ? { states } : {}) }),
|
|
142
|
+
exportPaint: () => send({ cmd: 'exportPaint' })
|
|
143
|
+
.then(reply => ({ supported: reply.supported, facets: reply.facets, hex: reply.hex })),
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Stop the running slice. The kernel polls this flag from inside its C++ loop, so it works while the worker
|
|
147
|
+
* is blocked in WASM — but the flag lives in a SharedArrayBuffer, so it exists only on the multithreaded
|
|
148
|
+
* kernel, i.e. on a cross-origin-isolated page. Returns false when there is nothing to write to; terminate
|
|
149
|
+
* the worker instead.
|
|
150
|
+
*/
|
|
151
|
+
cancel() {
|
|
152
|
+
if (!cancelFlag) return false
|
|
153
|
+
try { Atomics.store(cancelFlag, 0, 1) } catch { cancelFlag[0] = 1 }
|
|
154
|
+
return true
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
terminate() {
|
|
158
|
+
closed = true
|
|
159
|
+
while (pending.length) pending.shift().reject(new Error('slicer client terminated'))
|
|
160
|
+
worker.terminate()
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default createSlicerClient
|
package/engine/src/data.js
CHANGED
|
@@ -11,6 +11,9 @@ export { default as uiTree } from 'three-slicer/data/ui-tree.json' with { type:
|
|
|
11
11
|
export { default as toggleRules } from 'three-slicer/data/toggle-rules.json' with { type: 'json' }
|
|
12
12
|
export { default as invalidationMap } from 'three-slicer/data/invalidation-map.json' with { type: 'json' }
|
|
13
13
|
export { default as printers } from 'three-slicer/data/printers.json' with { type: 'json' }
|
|
14
|
+
// Which option keys belong to which preset type, straight from upstream's Preset.cpp. Small (17KB) and needed by
|
|
15
|
+
// anything that writes or reads a preset FILE, so it is static rather than lazy like the two big catalogs.
|
|
16
|
+
export { default as presetKeys } from 'three-slicer/data/preset-keys.json' with { type: 'json' }
|
|
14
17
|
// processes is the largest artifact (~800 KB) and is only needed once a printer is picked, so it stays a dynamic
|
|
15
18
|
// import. It is generated as a JS module rather than JSON precisely because of the attribute problem above, in its
|
|
16
19
|
// other direction: a dynamic JSON import needs the attribute in Node, and that same attribute makes browsers
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// OrcaSlicer preset files — the `.json` its "Config files (*.json;…)" import accepts, and that its
|
|
2
|
+
// "Export preset" writes. One file holds one preset of one type: machine, process or filament.
|
|
3
|
+
//
|
|
4
|
+
// The value encoding is the same one `project_settings.config` uses, and for the same reason: upstream writes
|
|
5
|
+
// every option through ConfigBase::save_to_json, which serializes each one to a string (a bool is "1"/"0", a point
|
|
6
|
+
// is "XxY", a vector becomes an array of strings). So normalize/serializeProjectSettings are reused verbatim
|
|
7
|
+
// rather than reimplemented — a second copy of that coercion table is exactly how the two would drift.
|
|
8
|
+
//
|
|
9
|
+
// Zero dependencies, so this lives in the engine and works headless. The zip bundle formats (.orca_printer) need
|
|
10
|
+
// a deflate and therefore live in the viewer, which already has one.
|
|
11
|
+
import { schema, presetKeys } from './data.js'
|
|
12
|
+
import { normalizeProjectSettings, serializeProjectSettings } from './settings.js'
|
|
13
|
+
|
|
14
|
+
/** `machine` for printers, `process` for print settings, `filament` for materials — upstream's own `type` field. */
|
|
15
|
+
const KEY_LIST = { machine: 'printer', process: 'process', filament: 'filament' }
|
|
16
|
+
|
|
17
|
+
// Preset bookkeeping, not settings. Upstream writes six of these; `inherits` is read (it names the parent) and the
|
|
18
|
+
// rest are carried only so a round trip does not lose them.
|
|
19
|
+
const ENVELOPE = new Set(['type', 'name', 'from', 'version', 'setting_id', 'instantiation', 'inherits',
|
|
20
|
+
'filament_id', 'is_custom_defined', 'filament_settings_id', 'print_settings_id', 'printer_settings_id'])
|
|
21
|
+
|
|
22
|
+
/** Every option key that belongs in a preset of this type. */
|
|
23
|
+
export function presetOptionKeys(type) {
|
|
24
|
+
const list = presetKeys[KEY_LIST[type]]
|
|
25
|
+
if (!list) throw new Error(`unknown preset type '${type}' (expected machine, process or filament)`)
|
|
26
|
+
return list
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A settings map -> the object an OrcaSlicer preset `.json` holds.
|
|
31
|
+
*
|
|
32
|
+
* Written **flattened**: no `inherits`, every key present. That is the whole difference between a file another
|
|
33
|
+
* program can read on its own and one that only means something next to the vendor database it was derived from —
|
|
34
|
+
* upstream's own exports are diffs against a parent (`Preset::save`: "only save difference if it has parent"),
|
|
35
|
+
* which is fine for OrcaSlicer reading its own files back and useless for anyone else.
|
|
36
|
+
*
|
|
37
|
+
* `complete: false` writes only the keys the settings map actually holds, for a diff-shaped file.
|
|
38
|
+
*/
|
|
39
|
+
export function writePresetFile(settings, { type = 'machine', name = 'Custom', complete = true, version = '1.0.0.0' } = {}) {
|
|
40
|
+
const keys = presetOptionKeys(type)
|
|
41
|
+
const picked = {}
|
|
42
|
+
for (const key of keys) {
|
|
43
|
+
// `inherits` is in upstream's printer_options list — it is an option to OrcaSlicer's config machinery, but it
|
|
44
|
+
// is bookkeeping here, and writing it would put an empty parent name in a file meant to stand alone. The same
|
|
45
|
+
// ENVELOPE set the reader strips is what the writer refuses to emit, so the two cannot disagree about it.
|
|
46
|
+
if (ENVELOPE.has(key)) continue
|
|
47
|
+
if (settings && key in settings && settings[key] != null) picked[key] = settings[key]
|
|
48
|
+
// A key with no schema default cannot be filled — there is nothing to fill it with. `complete` means "every
|
|
49
|
+
// option that HAS a value", not "every option name", and inventing one would be worse than leaving it out.
|
|
50
|
+
else if (complete && schema[key]?.default !== undefined) picked[key] = schema[key].default
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
type, name, from: 'User', version, instantiation: 'true',
|
|
54
|
+
...serializeProjectSettings(picked),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The inverse: a parsed preset `.json` -> a settings map this package accepts.
|
|
60
|
+
*
|
|
61
|
+
* `inherits` is the trap. Upstream stores a derived preset as the DIFF against its parent, so a vendor file on its
|
|
62
|
+
* own carries a fraction of the preset — the Bambu X1C machine json inherits `fdm_bbl_3dp_001_common` and lists
|
|
63
|
+
* about twenty keys. Resolution therefore needs a parent lookup, and this package has no full preset database:
|
|
64
|
+
* `resolveParent(name)` is the hook, and `printerSettings` covers the machine case for the keys the kernel reads.
|
|
65
|
+
*
|
|
66
|
+
* When the parent cannot be resolved the file's own values are still applied and `missingParent` names it —
|
|
67
|
+
* dropping a whole file because one name is unknown loses more than it protects, and the caller can decide.
|
|
68
|
+
*/
|
|
69
|
+
export function readPresetFile(raw, { resolveParent } = {}) {
|
|
70
|
+
if (!raw || typeof raw !== 'object') throw new Error('not a preset object')
|
|
71
|
+
const type = raw.type
|
|
72
|
+
if (!KEY_LIST[type]) throw new Error(`not an OrcaSlicer preset: type is ${JSON.stringify(type)}`)
|
|
73
|
+
|
|
74
|
+
const own = {}
|
|
75
|
+
for (const [key, value] of Object.entries(raw)) if (!ENVELOPE.has(key)) own[key] = value
|
|
76
|
+
const { settings, applied, skipped } = normalizeProjectSettings(own)
|
|
77
|
+
|
|
78
|
+
// The parent goes UNDER the file's own values — the file is the diff, so it wins.
|
|
79
|
+
let missingParent = null
|
|
80
|
+
let merged = settings
|
|
81
|
+
if (raw.inherits) {
|
|
82
|
+
const parent = resolveParent?.(raw.inherits, type) ?? null
|
|
83
|
+
if (parent) merged = { ...parent, ...settings }
|
|
84
|
+
else missingParent = raw.inherits
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
settings: merged,
|
|
89
|
+
type,
|
|
90
|
+
name: typeof raw.name === 'string' ? raw.name : '',
|
|
91
|
+
inherits: raw.inherits ?? null,
|
|
92
|
+
missingParent,
|
|
93
|
+
applied,
|
|
94
|
+
skipped,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Convenience: the JSON text an export writes. */
|
|
99
|
+
export const presetFileText = (settings, options) => JSON.stringify(writePresetFile(settings, options), null, 1)
|