three-slicer 0.2.2 → 0.2.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 +97 -0
- package/README.md +143 -19
- package/components/README.md +6 -3
- package/data/README.md +10 -9
- package/engine/PARAMS.md +248 -0
- package/engine/README.md +18 -264
- package/engine/examples/headless.mjs +4 -0
- package/engine/index.d.ts +26 -0
- package/engine/index.js +24 -4
- package/engine/src/client.js +17 -2
- package/engine/src/kernel_setting_keys.js +132 -0
- package/engine/src/settings.js +36 -2
- package/engine/src/slicer.worker.js +70 -6
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/engine/src/throughput.js +53 -0
- package/engine/src/warnings.js +23 -0
- package/package.json +1 -1
- package/types/.omc/state/sessions/02e5ddf5-4378-46e1-8fca-e90412c30804/pre-tool-advisory-throttle.json +10 -0
- package/types/settings.d.ts +20 -0
- package/types/viewer.d.ts +50 -1
- package/types/worker.d.ts +32 -3
- package/viewer/README.md +112 -16
- package/viewer/dist/Viewport.js +3772 -3295
- package/viewer/styles.css +18 -1
package/engine/index.d.ts
CHANGED
|
@@ -71,12 +71,38 @@ export interface SliceStats {
|
|
|
71
71
|
[k: string]: unknown
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** A condition the slice SUCCEEDED with. `'over_bed_model'`: the model itself sliced outside the printable area. */
|
|
75
|
+
export type SliceWarning = 'over_bed_model' | (string & {})
|
|
76
|
+
|
|
77
|
+
/** How fast the slice ran. Derived at the call site; the `stats` phase timings are untouched. */
|
|
78
|
+
export interface SliceThroughput {
|
|
79
|
+
/** Wall time around the slice call, ms — includes the STL crossing the WASM boundary and the layer stream */
|
|
80
|
+
ms: number
|
|
81
|
+
/** The kernel's own phase total (`t_pass1_ms` + surface + support + emit), or `null` if it reported none.
|
|
82
|
+
* `ms - kernelMs` is the boundary cost. */
|
|
83
|
+
kernelMs: number | null
|
|
84
|
+
/** Emitted layers per second of wall time — the figure to show a person */
|
|
85
|
+
layersPerSecond: number
|
|
86
|
+
/** Milliseconds per million path segments, or `null` when nothing was emitted. The figure to COMPARE runs with:
|
|
87
|
+
* the kernel is not deterministic in segment count (measured 15% apart on one input), so raw milliseconds
|
|
88
|
+
* compare two different amounts of work. */
|
|
89
|
+
msPerMsegment: number | null
|
|
90
|
+
}
|
|
91
|
+
|
|
74
92
|
export interface SliceResult {
|
|
75
93
|
/** Absent when `onLayer` was set — assemble it from the callback instead */
|
|
76
94
|
gcode?: string
|
|
77
95
|
stats: SliceStats
|
|
78
96
|
/** Absent when `onLayer` was set */
|
|
79
97
|
layers?: unknown[]
|
|
98
|
+
/**
|
|
99
|
+
* What the slice got away with, empty when nothing. Every entry restates a `stats` flag that is easy to miss
|
|
100
|
+
* among twenty numbers — an off-bed model slices with plausible time and material and no error — so check
|
|
101
|
+
* `result.warnings.length` once instead of learning which flags exist. Absent on a failed slice (see `error`).
|
|
102
|
+
*/
|
|
103
|
+
warnings?: SliceWarning[]
|
|
104
|
+
/** How fast it ran. Absent on a failed slice (see `error`). */
|
|
105
|
+
throughput?: SliceThroughput
|
|
80
106
|
/** Set instead of a result when the slice failed or was cancelled (`'canceled'`) */
|
|
81
107
|
error?: string
|
|
82
108
|
}
|
package/engine/index.js
CHANGED
|
@@ -3,10 +3,25 @@
|
|
|
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
5
|
|
|
6
|
+
import { withSliceWarnings } from './src/warnings.js'
|
|
7
|
+
import { withSliceThroughput } from './src/throughput.js'
|
|
8
|
+
|
|
6
9
|
const u8 = (b) => (b instanceof Uint8Array ? b : new Uint8Array(b))
|
|
7
10
|
|
|
8
|
-
// Off-main-thread worker URL for
|
|
11
|
+
// Off-main-thread worker URL for a NO-BUNDLER browser setup (native ESM, an import map, a CDN):
|
|
12
|
+
// `new Worker(engineWorkerURL(), { type: 'module' })`. Under a bundler use `createSlicerClient()`
|
|
13
|
+
// (three-slicer/client), which creates the worker through the static pattern bundlers recognize.
|
|
9
14
|
// The worker speaks the stage-30 streaming protocol ({type:'layer'|'done'|'error'|'progress'}).
|
|
15
|
+
//
|
|
16
|
+
// This expression on its OWN is what a bundler reads as a plain asset reference: through 0.2.2 it is how the
|
|
17
|
+
// client built its default worker, and Vite copied slicer.worker.js into dist verbatim — measured on a Vite 5 app
|
|
18
|
+
// importing three-slicer/client and nothing else, a 21KB copy of the untouched source, still importing
|
|
19
|
+
// `./slicer_core.js`, `./slicer_core.mt.js`, `./sla_request.js` and `./sla_core.js` in a 40KB dist that held none
|
|
20
|
+
// of them. It 404s on the worker's first message, and only after `vite build`, since dev serves the sources.
|
|
21
|
+
// What fixed it is client.js creating the worker through the static `new Worker(new URL('literal', …))` pattern
|
|
22
|
+
// bundlers treat as a worker ENTRY. Once that entry exists this reference resolves onto the same chunk and emits
|
|
23
|
+
// nothing extra (verified: dist byte-identical with the path written here inline or held in a const), so it is
|
|
24
|
+
// harmless — but it is not what makes a bundled worker work. Leave it for the no-bundler callers.
|
|
10
25
|
export const engineWorkerURL = () => new URL('./src/slicer.worker.js', import.meta.url)
|
|
11
26
|
|
|
12
27
|
// createSlicer(): load the WASM kernel and return a handle. Works in Node and the browser main thread.
|
|
@@ -20,7 +35,7 @@ export const engineWorkerURL = () => new URL('./src/slicer.worker.js', import.me
|
|
|
20
35
|
// paintPrepare/paint/paintClear/overlay: manual support enforcer/blocker painting (TriangleSelector).
|
|
21
36
|
// heapSize(): current WASM heap bytes (peak, monotonic). module: escape hatch. dispose(): drop the module.
|
|
22
37
|
export async function createSlicer() {
|
|
23
|
-
// Loaded here rather than at module scope so the
|
|
38
|
+
// Loaded here rather than at module scope so the ~4.8MB emscripten glue is a separate chunk: a host that imports
|
|
24
39
|
// this module only for engineWorkerURL() — the recommended browser path, where the kernel runs in the worker —
|
|
25
40
|
// used to pull the whole thing into its bundle for a function that returns a URL.
|
|
26
41
|
const M = await (await import('./src/slicer_core.js')).default()
|
|
@@ -28,14 +43,19 @@ export async function createSlicer() {
|
|
|
28
43
|
slice(stl, params, { onProgress, onLayer } = {}) {
|
|
29
44
|
const p = typeof params === 'string' ? params : JSON.stringify(params || {})
|
|
30
45
|
if (onLayer) M.set_layer_sink((z, idx, gcode, paths, widths) => onLayer({ z, idx, gcode, paths, widths }))
|
|
31
|
-
|
|
46
|
+
// `warnings` names the conditions a slice can SUCCEED with (an off-bed model above all) — see src/warnings.js.
|
|
47
|
+
// `throughput` is measured HERE rather than derived from the kernel's phase timings, because the wall time a
|
|
48
|
+
// caller feels includes the STL crossing the WASM boundary and the layer sink running on this side.
|
|
49
|
+
const started = performance.now()
|
|
50
|
+
try { return withSliceThroughput(withSliceWarnings(M.slice(u8(stl), p, onProgress || (() => {}))), performance.now() - started) }
|
|
32
51
|
finally { if (onLayer) M.clear_layer_sink() }
|
|
33
52
|
},
|
|
34
53
|
// SLA (resin) slice: no G-code — layers carry the mask segment stream, support_mesh/pad_mesh the preview
|
|
35
54
|
// soups, stats the resin figures and lift_layers. Same params-object-or-string contract as slice().
|
|
36
55
|
sliceSla(stl, params, onProgress) {
|
|
37
56
|
const p = typeof params === 'string' ? params : JSON.stringify(params || {})
|
|
38
|
-
|
|
57
|
+
const started = performance.now()
|
|
58
|
+
return withSliceThroughput(M.slice_sla(u8(stl), p, onProgress || (() => {})), performance.now() - started)
|
|
39
59
|
},
|
|
40
60
|
paintPrepare(stl) { M.selector_prepare(u8(stl)); return M.selector_facet_count() },
|
|
41
61
|
paint(a) {
|
package/engine/src/client.js
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
// Requests are answered in order. The worker's onmessage handler processes one message at a time and each command
|
|
14
14
|
// replies exactly once, so a FIFO of pending requests is enough to match a reply to its caller — the protocol
|
|
15
15
|
// carries no request ids and does not need them.
|
|
16
|
-
import { engineWorkerURL } from '../index.js'
|
|
17
16
|
import { parseSlaJob, SlaRequestError } from './sla_request.js'
|
|
18
17
|
|
|
19
18
|
export { SLA_CAPABILITIES, SLA_JOB_VERSION, SlaRequestError } from './sla_request.js'
|
|
@@ -22,6 +21,7 @@ export { SLA_CAPABILITIES, SLA_JOB_VERSION, SlaRequestError } from './sla_reques
|
|
|
22
21
|
const REPLY_OF = {
|
|
23
22
|
warmup: 'warm', prepare: 'prepared', paint: 'painted', erase: 'painted', clear: 'painted',
|
|
24
23
|
importPaint: 'painted', exportPaint: 'paintExport', overlay: 'overlay', slice: 'done', sla: 'done',
|
|
24
|
+
fillPreview: 'fillPreview', paintMode: 'paintMode',
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const asBuffer = (data) => (data instanceof ArrayBuffer ? data : data?.buffer ?? data)
|
|
@@ -32,7 +32,14 @@ const slaTransferables = (job) => job.objects.flatMap(object => [object, ...(obj
|
|
|
32
32
|
* @param {Worker} [worker] an existing worker to drive. Omit to create one — which only works in a browser,
|
|
33
33
|
* since Node has no global Worker; a Node caller should use createSlicer() from the root entry instead.
|
|
34
34
|
*/
|
|
35
|
-
|
|
35
|
+
// The default is written as the full `new Worker(new URL('literal', import.meta.url), { type: 'module' })`
|
|
36
|
+
// expression rather than through engineWorkerURL(), and that is the whole reason the no-argument form works
|
|
37
|
+
// after `vite build`. Bundlers recognize this exact shape as a worker ENTRY and emit it with its own chunk
|
|
38
|
+
// graph (the st/mt kernels it dynamically imports); split across a function call, the same file matched Vite's
|
|
39
|
+
// plain-asset rule instead and was copied verbatim, still importing an unhashed `./slicer_core.js` that dist
|
|
40
|
+
// did not contain — a 404 on the first message, visible only in a production build. Same reason the viewer
|
|
41
|
+
// keeps make_worker.js out of its own bundle. Do not factor this expression out.
|
|
42
|
+
export function createSlicerClient(worker = new Worker(new URL('./slicer.worker.js', import.meta.url), { type: 'module' })) {
|
|
36
43
|
const pending = [] // FIFO of {expect, resolve, reject, onProgress, onLayer, chunks, layers}
|
|
37
44
|
let cancelFlag = null // Uint32Array over the worker's SharedArrayBuffer — mt kernel only
|
|
38
45
|
let closed = false
|
|
@@ -137,6 +144,14 @@ export function createSlicerClient(worker = new Worker(engineWorkerURL(), { type
|
|
|
137
144
|
overlay: (states) => send({ cmd: 'overlay', ...(states ? { states } : {}) })
|
|
138
145
|
.then(reply => ({ enforcer: reply.enf, blocker: reply.blk, overlays: reply.overlays })),
|
|
139
146
|
|
|
147
|
+
/** What a fill would select under `{tool, facet, hx, hy, hz, angle}`, without applying it. `{clear:true}`
|
|
148
|
+
* drops the standing selection. `supported` is false on a kernel built before the preview existed. */
|
|
149
|
+
fillPreview: (args) => send({ cmd: 'fillPreview', ...args })
|
|
150
|
+
.then(reply => ({ supported: reply.supported, triangles: reply.tris })),
|
|
151
|
+
/** Brush-wide options: `{overhangDeg}` restricts every stroke to overhangs (0 = off), `{clipPlane:[nx,ny,nz,d]}`
|
|
152
|
+
* sets the section plane every cursor clips against (null turns it off). */
|
|
153
|
+
paintMode: (args) => send({ cmd: 'paintMode', ...args }).then(() => undefined),
|
|
154
|
+
|
|
140
155
|
/** Load a 3mf's painting. Replaces every mark, so run it directly after `prepare`. */
|
|
141
156
|
importPaint: (facets, hex, states) => brush('importPaint', { facets, hex, ...(states ? { states } : {}) }),
|
|
142
157
|
exportPaint: () => send({ cmd: 'exportPaint' })
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// AUTO-GENERATED by types/gen_kernel_params.mjs — DO NOT EDIT.
|
|
2
|
+
// The schema keys deriveKernelParams consumes (the distinct "From setting" column of engine/PARAMS.md).
|
|
3
|
+
export const KERNEL_SETTING_KEYS = [
|
|
4
|
+
"bottom_shell_layers",
|
|
5
|
+
"bottom_surface_filament_id",
|
|
6
|
+
"bridge_no_support",
|
|
7
|
+
"bridge_speed",
|
|
8
|
+
"brim_object_gap",
|
|
9
|
+
"brim_width",
|
|
10
|
+
"close_fan_the_first_x_layers",
|
|
11
|
+
"default_acceleration",
|
|
12
|
+
"enable_arc_fitting",
|
|
13
|
+
"enable_pressure_advance",
|
|
14
|
+
"enable_prime_tower",
|
|
15
|
+
"enable_support",
|
|
16
|
+
"fan_max_speed",
|
|
17
|
+
"filament_cost",
|
|
18
|
+
"filament_density",
|
|
19
|
+
"filament_diameter",
|
|
20
|
+
"filament_flow_ratio",
|
|
21
|
+
"filament_map",
|
|
22
|
+
"filament_retraction_length",
|
|
23
|
+
"filament_retraction_minimum_travel",
|
|
24
|
+
"filament_retraction_speed",
|
|
25
|
+
"filament_settings_id",
|
|
26
|
+
"filament_type",
|
|
27
|
+
"filament_z_hop",
|
|
28
|
+
"flush_into_infill",
|
|
29
|
+
"flush_multiplier",
|
|
30
|
+
"flush_volumes_matrix",
|
|
31
|
+
"full_fan_speed_layer",
|
|
32
|
+
"hot_plate_temp",
|
|
33
|
+
"independent_support_layer_height",
|
|
34
|
+
"infill_direction",
|
|
35
|
+
"initial_layer_line_width",
|
|
36
|
+
"initial_layer_print_height",
|
|
37
|
+
"initial_layer_speed",
|
|
38
|
+
"inner_wall_filament_id",
|
|
39
|
+
"inner_wall_line_width",
|
|
40
|
+
"internal_solid_filament_id",
|
|
41
|
+
"internal_solid_infill_line_width",
|
|
42
|
+
"ironing_flow",
|
|
43
|
+
"ironing_spacing",
|
|
44
|
+
"ironing_speed",
|
|
45
|
+
"ironing_type",
|
|
46
|
+
"layer_height",
|
|
47
|
+
"line_width",
|
|
48
|
+
"machine_end_gcode",
|
|
49
|
+
"machine_max_acceleration_e",
|
|
50
|
+
"machine_max_acceleration_retracting",
|
|
51
|
+
"machine_max_acceleration_x",
|
|
52
|
+
"machine_max_acceleration_z",
|
|
53
|
+
"machine_max_jerk_e",
|
|
54
|
+
"machine_max_jerk_x",
|
|
55
|
+
"machine_max_jerk_z",
|
|
56
|
+
"machine_max_speed_e",
|
|
57
|
+
"machine_max_speed_x",
|
|
58
|
+
"machine_max_speed_z",
|
|
59
|
+
"machine_start_gcode",
|
|
60
|
+
"max_volumetric_extrusion_rate_slope",
|
|
61
|
+
"nozzle_diameter",
|
|
62
|
+
"nozzle_temperature",
|
|
63
|
+
"outer_wall_filament_id",
|
|
64
|
+
"outer_wall_line_width",
|
|
65
|
+
"outer_wall_speed",
|
|
66
|
+
"pressure_advance",
|
|
67
|
+
"prime_tower_width",
|
|
68
|
+
"prime_volume",
|
|
69
|
+
"printable_area",
|
|
70
|
+
"printable_height",
|
|
71
|
+
"raft_contact_distance",
|
|
72
|
+
"raft_expansion",
|
|
73
|
+
"raft_layers",
|
|
74
|
+
"reduce_crossing_wall",
|
|
75
|
+
"resolution",
|
|
76
|
+
"retraction_length",
|
|
77
|
+
"retraction_minimum_travel",
|
|
78
|
+
"retraction_speed",
|
|
79
|
+
"seam_position",
|
|
80
|
+
"seam_slope_type",
|
|
81
|
+
"skirt_distance",
|
|
82
|
+
"skirt_height",
|
|
83
|
+
"skirt_loops",
|
|
84
|
+
"slow_down_layer_time",
|
|
85
|
+
"sparse_infill_density",
|
|
86
|
+
"sparse_infill_filament_id",
|
|
87
|
+
"sparse_infill_line_width",
|
|
88
|
+
"sparse_infill_pattern",
|
|
89
|
+
"spiral_mode",
|
|
90
|
+
"support_angle",
|
|
91
|
+
"support_base_pattern",
|
|
92
|
+
"support_base_pattern_spacing",
|
|
93
|
+
"support_bottom_z_distance",
|
|
94
|
+
"support_expansion",
|
|
95
|
+
"support_filament",
|
|
96
|
+
"support_interface_bottom_layers",
|
|
97
|
+
"support_interface_filament",
|
|
98
|
+
"support_interface_pattern",
|
|
99
|
+
"support_interface_spacing",
|
|
100
|
+
"support_interface_top_layers",
|
|
101
|
+
"support_line_width",
|
|
102
|
+
"support_object_first_layer_gap",
|
|
103
|
+
"support_object_xy_distance",
|
|
104
|
+
"support_on_build_plate_only",
|
|
105
|
+
"support_remove_small_overhang",
|
|
106
|
+
"support_style",
|
|
107
|
+
"support_threshold_angle",
|
|
108
|
+
"support_threshold_overlap",
|
|
109
|
+
"support_top_z_distance",
|
|
110
|
+
"support_type",
|
|
111
|
+
"top_shell_layers",
|
|
112
|
+
"top_surface_filament_id",
|
|
113
|
+
"top_surface_line_width",
|
|
114
|
+
"travel_acceleration",
|
|
115
|
+
"travel_speed",
|
|
116
|
+
"tree_support_angle_slow",
|
|
117
|
+
"tree_support_branch_angle",
|
|
118
|
+
"tree_support_branch_angle_organic",
|
|
119
|
+
"tree_support_branch_diameter",
|
|
120
|
+
"tree_support_branch_diameter_angle",
|
|
121
|
+
"tree_support_branch_diameter_organic",
|
|
122
|
+
"tree_support_branch_distance",
|
|
123
|
+
"tree_support_branch_distance_organic",
|
|
124
|
+
"tree_support_tip_diameter",
|
|
125
|
+
"tree_support_top_rate",
|
|
126
|
+
"tree_support_wall_count",
|
|
127
|
+
"wall_generator",
|
|
128
|
+
"wall_loops",
|
|
129
|
+
"wipe_tower_x",
|
|
130
|
+
"wipe_tower_y",
|
|
131
|
+
"z_hop",
|
|
132
|
+
]
|
package/engine/src/settings.js
CHANGED
|
@@ -23,7 +23,7 @@ const KERNEL_PATTERNS = ['rectilinear', 'grid', 'triangles', 'zigzag', 'gyroid',
|
|
|
23
23
|
const KERNEL_SEAMS = ['nearest', 'aligned', 'back', 'random']
|
|
24
24
|
|
|
25
25
|
// ---- Pass-through kernel parameters -----------------------------------------------------------------------
|
|
26
|
-
// The kernel reads 159 parameters (see the reference table in engine/
|
|
26
|
+
// The kernel reads 159 parameters (see the reference table in engine/PARAMS.md, generated from its own reader).
|
|
27
27
|
// These are the ones whose schema key carries the SAME name, so the mapping is the identity and the only real
|
|
28
28
|
// decision is WHEN to send them. They are sent only when the settings map actually holds the key — never filled
|
|
29
29
|
// in from the schema default — because the two defaults disagree for several of them:
|
|
@@ -340,8 +340,13 @@ export function deriveKernelParams(settings, opts) {
|
|
|
340
340
|
// Support style: schema enum (default/grid/snug/organic/tree_slim/tree_strong/tree_hybrid)
|
|
341
341
|
// -> kernel grid|tree|tree_lite. Since stage 18 the tree/organic family maps to the real organic TreeSupport ('tree',
|
|
342
342
|
// generate_tree_support_3D -> real type5 branch toolpaths); everything else maps to grid.
|
|
343
|
+
// support_type participates too, as upstream's UI does: a tree(auto)/tree(manual) TYPE whose style is
|
|
344
|
+
// still 'default' is a tree request — before this, an imported Orca project saved with tree(auto) and
|
|
345
|
+
// the default style silently sliced grid. An explicit non-tree style keeps winning over the type.
|
|
343
346
|
const styleRaw = String(S('support_style') ?? 'default')
|
|
344
|
-
const
|
|
347
|
+
const typeRaw = String(S('support_type') ?? 'normal(auto)')
|
|
348
|
+
const support_style =
|
|
349
|
+
/tree|organic/i.test(styleRaw) || (/tree/i.test(typeRaw) && styleRaw === 'default') ? 'tree' : 'grid'
|
|
345
350
|
|
|
346
351
|
// Bed: bounding box of the first rectangle in printable_area
|
|
347
352
|
const pa = settingRaw(settings, 'printable_area')
|
|
@@ -698,6 +703,35 @@ export function deriveSlaParams(settings) {
|
|
|
698
703
|
}
|
|
699
704
|
}
|
|
700
705
|
|
|
706
|
+
// ---- Preset application -------------------------------------------------------------------------------------
|
|
707
|
+
// The clear-then-merge every preset consumer needs and each demo used to rewrite: a preset carries only the
|
|
708
|
+
// keys it sets, so merging one over another leaves the previous pick's leftovers underneath (an ABS chamber
|
|
709
|
+
// temperature under a PLA pick). `keys` is the catalog's own key set (filaments.keys, processApi.keys,
|
|
710
|
+
// printerKeys) — cleared first, so applying a material never disturbs a process pick and vice versa.
|
|
711
|
+
// A null/undefined preset (a failed settingsFor lookup) applies NOTHING — the previous pick stays whole,
|
|
712
|
+
// because half-clearing on a bad name would be worse than doing nothing.
|
|
713
|
+
export function applyPreset(settings, preset, keys) {
|
|
714
|
+
if (!preset) return settings ?? {}
|
|
715
|
+
const next = { ...(settings ?? {}) }
|
|
716
|
+
for (const key of keys ?? []) delete next[key]
|
|
717
|
+
return Object.assign(next, preset)
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ---- Ignored-key reporting ----------------------------------------------------------------------------------
|
|
721
|
+
// The FFF derivation consumes a curated key set (generated by probing — types/gen_kernel_params.mjs). Every
|
|
722
|
+
// other key in a settings map is accepted and silently does nothing to a slice, which is exactly how a
|
|
723
|
+
// support_type request once sliced grid with no warning. This is the inspection surface for that class:
|
|
724
|
+
// which of MY map's keys does deriveKernelParams actually read?
|
|
725
|
+
// Scope: the FFF kernel derivation ONLY. A key ignored here can still be real elsewhere — the SLA derivation
|
|
726
|
+
// (slaSettingKeys), the viewer (enable_prime_tower, printable_area), or project metadata — so treat the
|
|
727
|
+
// result as "has no effect on an FFF slice", not "useless".
|
|
728
|
+
import { KERNEL_SETTING_KEYS } from './kernel_setting_keys.js'
|
|
729
|
+
export const kernelSettingKeys = Object.freeze([...KERNEL_SETTING_KEYS])
|
|
730
|
+
const KERNEL_SETTING_KEY_SET = new Set(KERNEL_SETTING_KEYS)
|
|
731
|
+
export function ignoredKernelSettings(settings) {
|
|
732
|
+
return Object.keys(settings ?? {}).filter(key => !KERNEL_SETTING_KEY_SET.has(key)).sort()
|
|
733
|
+
}
|
|
734
|
+
|
|
701
735
|
// ---- Preset files (the .json OrcaSlicer imports/exports) ----------------------------------------------------
|
|
702
736
|
// Re-exported here rather than given their own subpath: they are settings-map transforms like the 3mf project
|
|
703
737
|
// codecs above, and a consumer that has one hand on `settings` should not need a second import path for them.
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
// which sets `quiet` on the messages it sends): this is a separate module instance from the viewer's log.js and
|
|
17
17
|
// cannot read a flag set over there.
|
|
18
18
|
import { assertLegacySlaFallback, parseSlaJob } from './sla_request.js'
|
|
19
|
+
import { withSliceWarnings } from './warnings.js'
|
|
20
|
+
import { withSliceThroughput } from './throughput.js'
|
|
21
|
+
|
|
22
|
+
// The kernel parses `params` as JSON text, so the raw protocol used to require a string — while the direct handle
|
|
23
|
+
// and createSlicerClient both take an object and stringify it for you. One object, two shapes, and the difference
|
|
24
|
+
// showed up only at the boundary. The worker now accepts either; the SLA branch always did, which is what made
|
|
25
|
+
// the split arbitrary rather than principled.
|
|
26
|
+
const paramsText = (params) => (typeof params === 'string' ? params : JSON.stringify(params ?? {}))
|
|
19
27
|
|
|
20
28
|
let quiet = false
|
|
21
29
|
const say = (level, ...args) => { if (!quiet) console[level](...args) }
|
|
@@ -87,6 +95,16 @@ const cursorShapeOf = (message) => (message.cursor === 'circle' ? 1 : 0)
|
|
|
87
95
|
// a radius or a camera: what they select comes from the mesh's own topology, which is why they are a branch rather
|
|
88
96
|
// than more optional fields on the brush call. `state === null` means erase (the entry points with no state
|
|
89
97
|
// argument), matching how the brush splits paint from erase.
|
|
98
|
+
// The previous sample of the current stroke, when the message carries one. Upstream paints the CAPSULE between two
|
|
99
|
+
// consecutive mouse positions (DoublePointCursor, GLGizmoPainterBase.cpp:878) — a pointer stream is sampled, not
|
|
100
|
+
// continuous, so a ball per sample leaves a fast drag as a row of blobs with gaps between them. Optional on the
|
|
101
|
+
// wire and feature-detected on the kernel, so a caller or a build without it keeps painting single points.
|
|
102
|
+
const strokeFrom = (message) =>
|
|
103
|
+
Number.isFinite(message.px) && Number.isFinite(message.py) && Number.isFinite(message.pz)
|
|
104
|
+
|
|
105
|
+
// The fill modes the kernel's fill_preview takes (selector_bridge.h FILL_*), by the tool name the protocol uses.
|
|
106
|
+
const FILL_PREVIEW_MODE = { smart: 0, bucket: 1, triangle: 2 }
|
|
107
|
+
|
|
90
108
|
const FILL_TOOLS = new Set(['smart', 'bucket', 'triangle'])
|
|
91
109
|
const applyFill = (Module, message, state) => {
|
|
92
110
|
const angle = Number.isFinite(message.angle) ? message.angle : 30
|
|
@@ -140,10 +158,11 @@ self.onmessage = async (e) => {
|
|
|
140
158
|
self.postMessage({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
|
|
141
159
|
})
|
|
142
160
|
let r
|
|
161
|
+
const started = performance.now()
|
|
143
162
|
try { r = Kernel.slice_sla_job(job, onProgress) }
|
|
144
163
|
finally { Kernel.clear_layer_sink() }
|
|
145
164
|
if (r?.error) { self.postMessage({ type: 'error', error: String(r.error) }); return }
|
|
146
|
-
self.postMessage({ type: 'done', result: r }); return
|
|
165
|
+
self.postMessage({ type: 'done', result: withSliceThroughput(r, performance.now() - started) }); return
|
|
147
166
|
}
|
|
148
167
|
self.postMessage({ type: 'error', code: 'SLA_UNSUPPORTED_OBJECT_AWARE',
|
|
149
168
|
error: 'Object-aware SLA slicing requires WASM kernel support' })
|
|
@@ -153,7 +172,7 @@ self.onmessage = async (e) => {
|
|
|
153
172
|
// the same layer-sink streaming a kernel slice uses. The pure-JS contour slicer (sla_core.js) stays as the
|
|
154
173
|
// fallback for a loaded kernel that predates the binding — it produces the same stream, minus the supports.
|
|
155
174
|
if (d.cmd === 'sla') {
|
|
156
|
-
const params =
|
|
175
|
+
const params = paramsText(d.params)
|
|
157
176
|
if (!modPromise) modPromise = loadCore()
|
|
158
177
|
const Kernel = await modPromise
|
|
159
178
|
const onProgress = (done, total) => self.postMessage({ type: 'progress', done, total })
|
|
@@ -165,15 +184,19 @@ self.onmessage = async (e) => {
|
|
|
165
184
|
self.postMessage({ type: 'layer', z, idx, gcode: '', paths, widths }, transfer)
|
|
166
185
|
})
|
|
167
186
|
let r
|
|
187
|
+
const started = performance.now()
|
|
168
188
|
try { r = Kernel.slice_sla(new Uint8Array(d.stl), params, onProgress) }
|
|
169
189
|
finally { Kernel.clear_layer_sink() }
|
|
170
190
|
if (r && r.error) { self.postMessage({ type: 'error', error: String(r.error) }); return }
|
|
171
|
-
self.postMessage({ type: 'done', result: r })
|
|
191
|
+
self.postMessage({ type: 'done', result: withSliceThroughput(r, performance.now() - started) })
|
|
172
192
|
return
|
|
173
193
|
}
|
|
174
194
|
const { sliceSla } = await import('./sla_core.js')
|
|
175
195
|
const fallbackParams = JSON.parse(params)
|
|
176
196
|
assertLegacySlaFallback(fallbackParams)
|
|
197
|
+
// The JS fallback reports no phase timings at all, so its `kernelMs` comes back null rather than 0 — the
|
|
198
|
+
// wall time and the two rates are still the honest figures for what it did.
|
|
199
|
+
const startedFallback = performance.now()
|
|
177
200
|
const r = sliceSla(new Uint8Array(d.stl), fallbackParams, {
|
|
178
201
|
onProgress,
|
|
179
202
|
onLayer: (L) => {
|
|
@@ -184,7 +207,7 @@ self.onmessage = async (e) => {
|
|
|
184
207
|
},
|
|
185
208
|
})
|
|
186
209
|
if (r && r.error) { self.postMessage({ type: 'error', error: String(r.error) }); return }
|
|
187
|
-
self.postMessage({ type: 'done', result: r })
|
|
210
|
+
self.postMessage({ type: 'done', result: withSliceThroughput(r, performance.now() - startedFallback) })
|
|
188
211
|
return
|
|
189
212
|
}
|
|
190
213
|
if (!modPromise) modPromise = loadCore()
|
|
@@ -211,6 +234,8 @@ self.onmessage = async (e) => {
|
|
|
211
234
|
applyFill(Module, d, paintedState)
|
|
212
235
|
}
|
|
213
236
|
else if (paintedState === null) Module.selector_paint(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, d.enforcer)
|
|
237
|
+
else if (strokeFrom(d) && Module.selector_paint_stroke)
|
|
238
|
+
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))
|
|
214
239
|
else Module.selector_paint_shape(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, paintedState, cursorShapeOf(d))
|
|
215
240
|
self.postMessage(paintedReply(Module, d, paintedState)); return
|
|
216
241
|
}
|
|
@@ -223,6 +248,11 @@ self.onmessage = async (e) => {
|
|
|
223
248
|
// change to the same counts, and the viewer's listeners must not need to know which brush produced it.
|
|
224
249
|
if (d.cmd === 'erase') {
|
|
225
250
|
if (FILL_TOOLS.has(d.tool)) applyFill(Module, d, null)
|
|
251
|
+
else if (strokeFrom(d) && Module.selector_erase_stroke)
|
|
252
|
+
Module.selector_erase_stroke(d.facet, d.px, d.py, d.pz, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, cursorShapeOf(d))
|
|
253
|
+
// The eraser gets the cursor shape too, or a shift+drag with the circle brush rubs out the far side of a wall
|
|
254
|
+
// the paint brush never touched. `selector_erase` (no shape) stays the fallback for an older kernel.
|
|
255
|
+
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))
|
|
226
256
|
else Module.selector_erase(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius)
|
|
227
257
|
self.postMessage(paintedReply(Module, d, null)); return
|
|
228
258
|
}
|
|
@@ -279,6 +309,34 @@ self.onmessage = async (e) => {
|
|
|
279
309
|
self.postMessage(reply, transfer); return
|
|
280
310
|
}
|
|
281
311
|
|
|
312
|
+
// fillPreview: what a fill WOULD select, without applying it. Upstream runs this on every mouse move while a
|
|
313
|
+
// fill tool is active and draws the result in a lighter shade (GLGizmoPainterBase.cpp:929) — a preview marks
|
|
314
|
+
// nothing, so it needs no state at all and there is no NONE for a stray boolean to become. `{clear:true}`
|
|
315
|
+
// drops the standing selection, which is what leaving the model under the pointer means.
|
|
316
|
+
if (d.cmd === 'fillPreview') {
|
|
317
|
+
if (!Module.selector_fill_preview) { self.postMessage({ type: 'fillPreview', supported: false, tris: EMPTY_OVERLAY }); return }
|
|
318
|
+
if (d.clear) { Module.selector_fill_preview_clear(); self.postMessage({ type: 'fillPreview', supported: true, tris: EMPTY_OVERLAY }); return }
|
|
319
|
+
const angle = Number.isFinite(d.angle) ? d.angle : 30
|
|
320
|
+
const mode = FILL_PREVIEW_MODE[d.tool] ?? FILL_PREVIEW_MODE.smart
|
|
321
|
+
// Upstream's -1 for the non-propagating case, the same value applyFill sends: one facet has no neighbour to
|
|
322
|
+
// measure an angle against.
|
|
323
|
+
const tris = Module.selector_fill_preview(d.facet, d.hx, d.hy, d.hz, mode === FILL_PREVIEW_MODE.triangle ? -1 : angle, mode)
|
|
324
|
+
self.postMessage({ type: 'fillPreview', supported: true, tris },
|
|
325
|
+
tris?.buffer && tris.byteLength > 0 ? [tris.buffer] : [])
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
// paintMode: the brush-wide options upstream keeps on the gizmo — the overhang restriction and the section
|
|
329
|
+
// plane. Both are set once and read by every stroke, so they are their own command rather than fields on one.
|
|
330
|
+
if (d.cmd === 'paintMode') {
|
|
331
|
+
if (Number.isFinite(d.overhangDeg) && Module.selector_set_overhang_limit) Module.selector_set_overhang_limit(d.overhangDeg)
|
|
332
|
+
if (Module.selector_set_clip_plane) {
|
|
333
|
+
const plane = d.clipPlane
|
|
334
|
+
if (plane) Module.selector_set_clip_plane(plane[0], plane[1], plane[2], plane[3], true)
|
|
335
|
+
else if (d.clipPlane === null) Module.selector_set_clip_plane(0, 0, 1, 0, false)
|
|
336
|
+
}
|
|
337
|
+
self.postMessage({ type: 'paintMode', ok: true }); return
|
|
338
|
+
}
|
|
339
|
+
|
|
282
340
|
if (d.stall) return // stage-30 test hook: simulate a hang -> verifies the main-thread watchdog fires (not set in production)
|
|
283
341
|
// mt (SAB): share the address of the real support progress counter (u32) with main once — even while the kernel is
|
|
284
342
|
// blocked inside C++, the UI thread polls the SAB directly to show support progress. st (non-shared buffer) is unsupported -> not sent.
|
|
@@ -302,11 +360,17 @@ self.onmessage = async (e) => {
|
|
|
302
360
|
self.postMessage({ type: 'layer', z, idx, gcode, paths, widths }, transfer)
|
|
303
361
|
})
|
|
304
362
|
let r
|
|
305
|
-
|
|
363
|
+
// Timed around the kernel call INCLUDING the layer sink, which posts each layer to the main thread from inside
|
|
364
|
+
// it — that transfer is part of what a streamed slice costs and leaving it out would flatter the number.
|
|
365
|
+
const started = performance.now()
|
|
366
|
+
try { r = Module.slice(new Uint8Array(d.stl), paramsText(d.params), onProgress) }
|
|
306
367
|
finally { Module.clear_layer_sink() }
|
|
307
368
|
if (r && r.error) { self.postMessage({ type: 'error', error: String(r.error) }); return }
|
|
308
369
|
// streamed=true -> g-code/layers were already emitted as 'layer' (result holds stats only). batch/MM keep them in result.
|
|
309
|
-
|
|
370
|
+
// withSliceWarnings names what the slice got away with (an off-bed model) on the result itself, and
|
|
371
|
+
// withSliceThroughput how fast it ran — the raw protocol carries both, so createSlicerClient's callers get
|
|
372
|
+
// them without the client having to re-derive anything.
|
|
373
|
+
self.postMessage({ type: 'done', result: withSliceThroughput(withSliceWarnings(r), performance.now() - started) })
|
|
310
374
|
} catch (err) {
|
|
311
375
|
// Includes the WASM abort("memory access out of bounds") — the main thread's OOM ladder decides on re-creation / economy retry.
|
|
312
376
|
self.postMessage({ type: 'error', error: String((err && err.message) || err), ...(err?.code ? { code: err.code } : {}) })
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// How fast the slice actually ran.
|
|
2
|
+
//
|
|
3
|
+
// The kernel already times its own phases (`t_pass1_ms`, `t_surface_ms`, `t_support_ms`, `t_emit_ms` — set
|
|
4
|
+
// unconditionally in finish.cpp), but a phase breakdown is a profiling tool, not an answer to "how fast is this".
|
|
5
|
+
// The wall time around the call is, and only the caller can measure it: it includes the STL crossing the WASM
|
|
6
|
+
// boundary and, in the worker, the layer stream leaving it.
|
|
7
|
+
//
|
|
8
|
+
// `msPerMsegment` is the field to compare runs with, and it is not decoration. The kernel is not deterministic in
|
|
9
|
+
// segment COUNT — the same input has been measured 15% apart between runs — so a raw millisecond figure compares
|
|
10
|
+
// two different amounts of work and reads as a regression that is not there. Normalizing by the work done is what
|
|
11
|
+
// makes two numbers comparable; `layersPerSecond` is the one to show a person, since layers are what a progress
|
|
12
|
+
// bar counts.
|
|
13
|
+
//
|
|
14
|
+
// Everything here is derived. Nothing replaces or renames a `stats` field.
|
|
15
|
+
|
|
16
|
+
// The two technologies time entirely different passes, and only the CONSECUTIVE ones may be summed.
|
|
17
|
+
// FFF: pass1 -> surfaces -> support -> emit (finish.cpp).
|
|
18
|
+
// SLA: contours -> sample -> tree -> raster -> emit (slice_sla.cpp). Its `t_sample_*` and `t_raster_*` keys are
|
|
19
|
+
// nested INSIDE t_sample_ms / t_raster_ms — the C++ says so at each set() — so adding them would double-count.
|
|
20
|
+
// `t_emit_ms` is the one name both use, which is exactly why the FFF list cannot be applied to an SLA result:
|
|
21
|
+
// it would find that single key, sum it alone, and report the emit pass as the whole kernel time.
|
|
22
|
+
const FFF_PHASES = ['t_pass1_ms', 't_surface_ms', 't_support_ms', 't_emit_ms']
|
|
23
|
+
const SLA_PHASES = ['t_contours_ms', 't_sample_ms', 't_tree_ms', 't_raster_ms', 't_emit_ms']
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} [stats] the slice result's `stats`
|
|
27
|
+
* @param {number} wallMs wall time around the slice call, ms
|
|
28
|
+
*/
|
|
29
|
+
export function sliceThroughput(stats, wallMs) {
|
|
30
|
+
const ms = Math.max(0, Number(wallMs) || 0)
|
|
31
|
+
const layers = Number(stats?.layers) || 0
|
|
32
|
+
// SLA counts mask segments here rather than toolpath segments, which is the same thing for this purpose: the
|
|
33
|
+
// amount of geometry the run actually produced, so the normalized figure stays comparable within a technology.
|
|
34
|
+
const segments = Number(stats?.path_segments) || 0
|
|
35
|
+
// The kernel's own total, when it reported its phases — always smaller than `ms`, and the gap is the boundary
|
|
36
|
+
// cost (STL upload, the layer stream, the JS sink). Worth having side by side: a slice that is slow only in
|
|
37
|
+
// the gap is a marshalling problem, not a slicing one.
|
|
38
|
+
const phases = (stats?.sla ? SLA_PHASES : FFF_PHASES)
|
|
39
|
+
.map(key => Number(stats?.[key])).filter(Number.isFinite)
|
|
40
|
+
return {
|
|
41
|
+
ms,
|
|
42
|
+
kernelMs: phases.length ? phases.reduce((sum, value) => sum + value, 0) : null,
|
|
43
|
+
layersPerSecond: ms > 0 ? (layers * 1000) / ms : 0,
|
|
44
|
+
// Null rather than 0 when there is nothing to normalize against: 0 ms/Mseg would read as infinitely fast.
|
|
45
|
+
msPerMsegment: segments > 0 ? ms / (segments / 1e6) : null,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Attaches `throughput` to a slice result in place and returns it. */
|
|
50
|
+
export function withSliceThroughput(result, wallMs) {
|
|
51
|
+
if (result && !result.error) result.throughput = sliceThroughput(result.stats, wallMs)
|
|
52
|
+
return result
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Result-level warnings — the slices that succeed while being wrong.
|
|
2
|
+
//
|
|
3
|
+
// The kernel reports an off-bed model as one flag among twenty stats numbers (`over_bed_model`), which a consumer
|
|
4
|
+
// only finds by already knowing it exists: a bed-centred model handed in without the plate-local subtraction slices
|
|
5
|
+
// at X 234.7–265.4 on a 250mm bed with plausible time and material and no error (examples/DEMOS.md §4.5). All four
|
|
6
|
+
// demos ended up writing the same `if (result.stats.over_bed_model) throw` line. Naming the whole set on the result
|
|
7
|
+
// lets a host surface or log every such condition without knowing which flags the kernel happens to carry.
|
|
8
|
+
//
|
|
9
|
+
// Warnings are strings, and the array is always present so `result.warnings.length` needs no guard. The stats flags
|
|
10
|
+
// stay exactly where they were — this is an additional view of them, not a replacement.
|
|
11
|
+
|
|
12
|
+
/** @param {object} [stats] a slice result's `stats` @returns {string[]} */
|
|
13
|
+
export function sliceWarnings(stats) {
|
|
14
|
+
const warnings = []
|
|
15
|
+
if (stats?.over_bed_model) warnings.push('over_bed_model')
|
|
16
|
+
return warnings
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Attaches `warnings` to a slice result in place (the result is a fresh object per slice) and returns it. */
|
|
20
|
+
export function withSliceWarnings(result) {
|
|
21
|
+
if (result && !result.error) result.warnings = sliceWarnings(result.stats)
|
|
22
|
+
return result
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-slicer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
}
|
package/types/settings.d.ts
CHANGED
|
@@ -19,6 +19,26 @@ export function settingScalar(settings: SlicerSettings | null | undefined, key:
|
|
|
19
19
|
*/
|
|
20
20
|
export function deriveKernelParams(settings: SlicerSettings | null | undefined, opts?: { plate?: number }): Record<string, unknown>
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Clear-then-merge preset application: deletes `keys` (the catalog's own key set) from `settings`, then
|
|
24
|
+
* merges `preset`. A null preset applies nothing and returns the settings unchanged — a failed lookup must
|
|
25
|
+
* not half-clear the previous pick.
|
|
26
|
+
*/
|
|
27
|
+
export function applyPreset(settings: SlicerSettings | null | undefined, preset: Partial<SlicerSettings> | null | undefined, keys: readonly string[] | null | undefined): SlicerSettings
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The schema keys deriveKernelParams consumes — the distinct "From setting" column of engine/PARAMS.md,
|
|
31
|
+
* generated by the same probe that generates the table (so it cannot drift from the real mapping).
|
|
32
|
+
*/
|
|
33
|
+
export const kernelSettingKeys: readonly string[]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The keys of `settings` that deriveKernelParams does NOT read — they are accepted and silently have no
|
|
37
|
+
* effect on an FFF slice. Scope is the FFF derivation only: a key listed here can still be meaningful to
|
|
38
|
+
* the SLA derivation, the viewer, or project metadata. Sorted.
|
|
39
|
+
*/
|
|
40
|
+
export function ignoredKernelSettings(settings: SlicerSettings | null | undefined): string[]
|
|
41
|
+
|
|
22
42
|
/** Which slicing technology the settings map's printer profile declares. Defaults to FFF. */
|
|
23
43
|
export function printerTechnology(settings: SlicerSettings | null | undefined): 'FFF' | 'SLA'
|
|
24
44
|
|