three-slicer 0.1.5 → 0.1.7
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/README.md +256 -21
- package/components/README.md +2 -2
- package/data/README.md +2 -2
- package/data/filaments.js +1 -1
- package/data/preset-keys.json +1 -0
- package/data/processes.js +1 -1
- package/engine/README.md +261 -5
- package/engine/index.d.ts +74 -5
- package/engine/index.js +9 -4
- package/engine/src/client.js +136 -0
- package/engine/src/data.js +3 -0
- package/engine/src/preset_file.js +99 -0
- package/engine/src/settings.js +120 -3
- package/engine/src/slicer.worker.js +30 -3
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/package.json +20 -10
- package/types/client.d.ts +74 -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.d.ts +90 -2
- package/types/viewer.d.ts +57 -1
- package/types/worker.d.ts +87 -0
- package/viewer/README.md +162 -3
- package/viewer/dist/Viewport.js +3350 -2273
- package/viewer/styles.css +31 -3
- package/types/gen_settings_types.mjs +0 -70
|
@@ -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)
|
package/engine/src/settings.js
CHANGED
|
@@ -22,6 +22,43 @@ const KERNEL_PATTERNS = ['rectilinear', 'grid', 'triangles', 'zigzag', 'gyroid',
|
|
|
22
22
|
'honeycomb', '3dhoneycomb', 'crosshatch', 'concentric']
|
|
23
23
|
const KERNEL_SEAMS = ['nearest', 'aligned', 'back', 'random']
|
|
24
24
|
|
|
25
|
+
// ---- Pass-through kernel parameters -----------------------------------------------------------------------
|
|
26
|
+
// The kernel reads 159 parameters (see the reference table in engine/README.md, generated from its own reader).
|
|
27
|
+
// These are the ones whose schema key carries the SAME name, so the mapping is the identity and the only real
|
|
28
|
+
// decision is WHEN to send them. They are sent only when the settings map actually holds the key — never filled
|
|
29
|
+
// in from the schema default — because the two defaults disagree for several of them:
|
|
30
|
+
// independent_support_layer_height is true in the schema and false in the kernel, printable_height is 100 there
|
|
31
|
+
// and 250 here. Filling one in would silently reslice every existing caller's model. This is the same omission
|
|
32
|
+
// rule the per-feature widths, the support tools and the prime tower keys already follow, and it is what keeps
|
|
33
|
+
// deriveKernelParams({}) producing exactly the parameter set it produced before these keys were mapped.
|
|
34
|
+
const PASSTHROUGH_NUM = [
|
|
35
|
+
// Real (organic) tree support shape. Upstream keeps an _organic spelling and a suffix-less one for most of
|
|
36
|
+
// these, and the kernel accepts both — preferring _organic — so both are passed as written rather than
|
|
37
|
+
// reconciled here. Before this, none of the organic tree support could be tuned from a settings map at all.
|
|
38
|
+
'tree_support_branch_angle', 'tree_support_branch_angle_organic',
|
|
39
|
+
'tree_support_branch_diameter', 'tree_support_branch_diameter_organic',
|
|
40
|
+
'tree_support_branch_distance', 'tree_support_branch_distance_organic',
|
|
41
|
+
'tree_support_branch_diameter_angle', 'tree_support_angle_slow', 'tree_support_tip_diameter',
|
|
42
|
+
'tree_support_top_rate', 'tree_support_wall_count',
|
|
43
|
+
'support_object_first_layer_gap',
|
|
44
|
+
// The tree support's build-volume ceiling. Distinct from bed_height above — that one is the kernel's own
|
|
45
|
+
// off-the-bed check — but both come from this single schema key, because upstream has one option for both jobs.
|
|
46
|
+
'printable_height',
|
|
47
|
+
// Upstream's *_filament_id family: a 1-based filament index per feature, 0 meaning "whatever tool the region
|
|
48
|
+
// already prints with". slice_mm.cpp honours the wall and sparse-infill ids and reports the rest in the G-code
|
|
49
|
+
// rather than silently dropping them.
|
|
50
|
+
'outer_wall_filament_id', 'inner_wall_filament_id', 'sparse_infill_filament_id',
|
|
51
|
+
'top_surface_filament_id', 'bottom_surface_filament_id', 'internal_solid_filament_id',
|
|
52
|
+
]
|
|
53
|
+
const PASSTHROUGH_BOOL = ['independent_support_layer_height']
|
|
54
|
+
// Per-filament vectors, one entry per filament, passed positionally like every other per-extruder array.
|
|
55
|
+
// filament_map is which physical extruder each filament sits in; density and cost turn extruded millimetres into
|
|
56
|
+
// the grams and currency the G-code footer reports.
|
|
57
|
+
const PASSTHROUGH_NUM_VECTOR = ['filament_map', 'filament_density', 'filament_cost']
|
|
58
|
+
// Material identity. The kernel needs the type for the two decisions upstream makes by material name (PETG's
|
|
59
|
+
// extra unretract, TPU on the first layer) and the settings id for the footer.
|
|
60
|
+
const PASSTHROUGH_STR_VECTOR = ['filament_type', 'filament_settings_id']
|
|
61
|
+
|
|
25
62
|
// Machine limits (the "Motion ability" printer page) -> kernel time-estimate parameters.
|
|
26
63
|
// Declarative on purpose: the kernel collapses X/Y into one axis, so the mapping cannot be a plain pass-through,
|
|
27
64
|
// but adding a limit stays a data row instead of code. The fallback is the kernel's own default, so an unedited
|
|
@@ -217,9 +254,47 @@ export function normalizeProjectSettings(raw) {
|
|
|
217
254
|
return { settings, applied, skipped }
|
|
218
255
|
}
|
|
219
256
|
|
|
257
|
+
// The exact inverse, for WRITING a project_settings.config: every value back to the string shape upstream
|
|
258
|
+
// serializes (bool -> "1"/"0", point -> "XxY", a points group -> "X1xY1,X2xY2"), because that is what every other
|
|
259
|
+
// slicer's reader — and normalizeProjectSettings above — expects to coerce FROM. Writing the JS types raw would
|
|
260
|
+
// hand OrcaSlicer a bool it reads with its own string parser and this package a round-trip that only works here.
|
|
261
|
+
// Keys the schema does not define are dropped for the same reason normalize drops them: they could not be typed.
|
|
262
|
+
const pointToText = (point) => (Array.isArray(point) ? `${point[0]}x${point[1]}` : String(point))
|
|
263
|
+
|
|
264
|
+
function serializeScalar(type, value) {
|
|
265
|
+
if (value == null) return ''
|
|
266
|
+
switch (type) {
|
|
267
|
+
case 'coBool': case 'coBools':
|
|
268
|
+
return value ? '1' : '0'
|
|
269
|
+
case 'coPoint': case 'coPoints':
|
|
270
|
+
return pointToText(value)
|
|
271
|
+
case 'coPointsGroups':
|
|
272
|
+
return Array.isArray(value) ? value.map(pointToText).join(',') : String(value)
|
|
273
|
+
default:
|
|
274
|
+
return String(value) // numbers, percents, strings, enums — String() is upstream's own spelling for all of them
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A settings map (real JS types) -> the all-strings object a 3mf's `project_settings.config` stores. */
|
|
279
|
+
export function serializeProjectSettings(settings) {
|
|
280
|
+
const out = {}
|
|
281
|
+
for (const [key, value] of Object.entries(settings || {})) {
|
|
282
|
+
const type = schema[key]?.type
|
|
283
|
+
if (!type) continue
|
|
284
|
+
// A coPoint's value IS an array ([x, y]) — the generic array path would serialize its two numbers separately.
|
|
285
|
+
out[key] = (type !== 'coPoint' && Array.isArray(value))
|
|
286
|
+
? value.map(entry => serializeScalar(type, entry))
|
|
287
|
+
: serializeScalar(type, value)
|
|
288
|
+
}
|
|
289
|
+
return out
|
|
290
|
+
}
|
|
291
|
+
|
|
220
292
|
|
|
221
293
|
// Right-panel settings values -> kernel parameters (derived from schema keys)
|
|
222
|
-
|
|
294
|
+
// opts.plate: which plate's entry to take from per-plate array options (wipe_tower_x/y — upstream coFloats,
|
|
295
|
+
// one entry per plate). Defaults to 0, so every existing caller reads exactly what it always did.
|
|
296
|
+
export function deriveKernelParams(settings, opts) {
|
|
297
|
+
const plate = opts?.plate ?? 0
|
|
223
298
|
const S = k => settingScalar(settings, k)
|
|
224
299
|
const num = (k, d) => { const v = Number(S(k)); return Number.isFinite(v) ? v : d }
|
|
225
300
|
const bool = (k, d) => { const v = settingRaw(settings, k); return typeof v === 'boolean' ? v : (Array.isArray(v) ? !!v[0] : (v == null ? d : !!v)) }
|
|
@@ -283,7 +358,9 @@ export function deriveKernelParams(settings) {
|
|
|
283
358
|
// A hole (a material that overrides nothing) is filled with the value tool 0 resolved to, because the kernel
|
|
284
359
|
// reads the array positionally and cannot tell "absent" from "0".
|
|
285
360
|
// Prime tower keys, omitted when unset so the kernel/viewer defaults stay in charge (same rule as supportTools).
|
|
286
|
-
// wipe_tower_x/y are coFloats upstream — one entry per plate —
|
|
361
|
+
// wipe_tower_x/y are coFloats upstream — one entry per plate — indexed by opts.plate. A hole (null entry) means
|
|
362
|
+
// nobody chose a position for THAT plate, so the automatic placement stands there; a scalar applies to every
|
|
363
|
+
// plate (the pre-array form, kept readable so old saved settings keep meaning what they meant).
|
|
287
364
|
const towerSettings = {}
|
|
288
365
|
{
|
|
289
366
|
const width = num('prime_tower_width', 0)
|
|
@@ -292,7 +369,9 @@ export function deriveKernelParams(settings) {
|
|
|
292
369
|
// a coordinate off the front of a 200mm bed. Taking it as "the user chose a position" put the tower outside
|
|
293
370
|
// the plate and disabled the automatic placement entirely — measured as a tower at z=-127.5 on a 200mm bed.
|
|
294
371
|
// Absent from the map means nobody chose one, and the placement beside the model stands.
|
|
295
|
-
const chosen = (key) => { const
|
|
372
|
+
const chosen = (key) => { const raw = settings?.[key]
|
|
373
|
+
const v = Array.isArray(raw) ? raw[plate] : raw
|
|
374
|
+
const n = Number(v)
|
|
296
375
|
return (v != null && v !== '' && Number.isFinite(n)) ? n : null }
|
|
297
376
|
const towerX = chosen('wipe_tower_x'), towerY = chosen('wipe_tower_y')
|
|
298
377
|
if (towerX != null) towerSettings.prime_tower_x = towerX
|
|
@@ -311,6 +390,38 @@ export function deriveKernelParams(settings) {
|
|
|
311
390
|
if (prime > 0) towerSettings.prime_volume = prime
|
|
312
391
|
}
|
|
313
392
|
|
|
393
|
+
// Pass-through parameters (see PASSTHROUGH_* above). `present` reads the settings MAP, not settingRaw, because
|
|
394
|
+
// settingRaw's schema fallback is exactly what must not happen here — an unedited key has to stay absent.
|
|
395
|
+
const passthrough = {}
|
|
396
|
+
{
|
|
397
|
+
const present = (key) => settings != null && key in settings && settings[key] != null && settings[key] !== ''
|
|
398
|
+
for (const key of PASSTHROUGH_NUM) {
|
|
399
|
+
if (!present(key)) continue
|
|
400
|
+
const value = Number(S(key))
|
|
401
|
+
if (Number.isFinite(value)) passthrough[key] = value
|
|
402
|
+
}
|
|
403
|
+
for (const key of PASSTHROUGH_BOOL) if (present(key)) passthrough[key] = bool(key, false)
|
|
404
|
+
for (const key of PASSTHROUGH_NUM_VECTOR) {
|
|
405
|
+
if (!present(key)) continue
|
|
406
|
+
const raw = settings[key]
|
|
407
|
+
passthrough[key] = (Array.isArray(raw) ? raw : [raw]).map(entry => { const n = Number(entry); return Number.isFinite(n) ? n : 0 })
|
|
408
|
+
}
|
|
409
|
+
for (const key of PASSTHROUGH_STR_VECTOR) {
|
|
410
|
+
if (!present(key)) continue
|
|
411
|
+
const raw = settings[key]
|
|
412
|
+
passthrough[key] = (Array.isArray(raw) ? raw : [raw]).map(entry => String(entry ?? ''))
|
|
413
|
+
}
|
|
414
|
+
// support_line_width is coFloatOrPercent, but the kernel reads it with the plain number reader rather than the
|
|
415
|
+
// percent-aware jwidth_raw the per-feature widths get. A "120%" would reach strtod as a quoted string and
|
|
416
|
+
// parse to 0 == auto — the setting silently ignored. Resolved here against the nozzle, which is the same
|
|
417
|
+
// thing jwidth_raw does for the widths that do get the percent-aware reader.
|
|
418
|
+
if (present('support_line_width')) {
|
|
419
|
+
const text = String(settingScalar(settings, 'support_line_width')).trim()
|
|
420
|
+
const width = text.endsWith('%') ? (parseFloat(text) / 100) * num('nozzle_diameter', 0.4) : Number(text)
|
|
421
|
+
if (Number.isFinite(width) && width > 0) passthrough.support_line_width = width
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
314
425
|
const perExtruder = {}
|
|
315
426
|
for (const [param, key, scalar] of [
|
|
316
427
|
['extruder_nozzle_temp', 'nozzle_temperature', num('nozzle_temperature', 200)],
|
|
@@ -332,6 +443,7 @@ export function deriveKernelParams(settings) {
|
|
|
332
443
|
return {
|
|
333
444
|
...widths,
|
|
334
445
|
...machine,
|
|
446
|
+
...passthrough, // present only for keys the settings map actually holds (see PASSTHROUGH_* above)
|
|
335
447
|
layer_height: num('layer_height', 0.2),
|
|
336
448
|
first_layer_height: num('initial_layer_print_height', 0.2),
|
|
337
449
|
line_width,
|
|
@@ -421,3 +533,8 @@ export function deriveKernelParams(settings) {
|
|
|
421
533
|
// support_density has no matching schema key, so the kernel default (0.15) is used; scarf_length likewise uses the kernel default (10mm)
|
|
422
534
|
}
|
|
423
535
|
}
|
|
536
|
+
|
|
537
|
+
// ---- Preset files (the .json OrcaSlicer imports/exports) ----------------------------------------------------
|
|
538
|
+
// Re-exported here rather than given their own subpath: they are settings-map transforms like the 3mf project
|
|
539
|
+
// codecs above, and a consumer that has one hand on `settings` should not need a second import path for them.
|
|
540
|
+
export { writePresetFile, readPresetFile, presetFileText, presetOptionKeys } from './preset_file.js'
|
|
@@ -11,17 +11,24 @@
|
|
|
11
11
|
// Automatic multithreaded kernel selection: with crossOriginIsolated (sites serving COOP/COEP) it uses mt (-pthread, PASS1
|
|
12
12
|
// layers in parallel — measured 2.2x), otherwise st (zero-config). The dynamic import makes bundlers emit both as chunks,
|
|
13
13
|
// but only one is loaded at runtime. On mt init failure (SAB blocked, …) it falls back to st.
|
|
14
|
+
// Which kernel loaded is worth saying once — mt vs st is a 2x difference and the fallback is silent otherwise.
|
|
15
|
+
// A host embedding the viewer in its own product can switch it off (`<Viewport features={{ logs: false }} />`,
|
|
16
|
+
// which sets `quiet` on the messages it sends): this is a separate module instance from the viewer's log.js and
|
|
17
|
+
// cannot read a flag set over there.
|
|
18
|
+
let quiet = false
|
|
19
|
+
const say = (level, ...args) => { if (!quiet) console[level](...args) }
|
|
20
|
+
|
|
14
21
|
const loadCore = async () => {
|
|
15
22
|
const isolated = typeof crossOriginIsolated !== 'undefined' && crossOriginIsolated
|
|
16
23
|
if (isolated) {
|
|
17
24
|
try {
|
|
18
25
|
const M = await (await import('./slicer_core.mt.js')).default()
|
|
19
|
-
|
|
26
|
+
say('info', '[slicer.worker] core: mt (threads)')
|
|
20
27
|
return M
|
|
21
|
-
} catch (e) {
|
|
28
|
+
} catch (e) { say('warn', '[slicer.worker] mt load failed — falling back to st:', e) }
|
|
22
29
|
}
|
|
23
30
|
const M = await (await import('./slicer_core.js')).default()
|
|
24
|
-
|
|
31
|
+
say('info', '[slicer.worker] core: st')
|
|
25
32
|
return M
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -108,7 +115,16 @@ const paintedReply = (Module, message, paintedState) => {
|
|
|
108
115
|
|
|
109
116
|
self.onmessage = async (e) => {
|
|
110
117
|
const d = e.data
|
|
118
|
+
// Read before the kernel loads: loadCore() below is what prints, so a `quiet` arriving with the first message
|
|
119
|
+
// (the warmup) has to take effect before that call, not after it.
|
|
120
|
+
if (d.quiet !== undefined) quiet = !!d.quiet
|
|
111
121
|
try {
|
|
122
|
+
// Asking for the painting must not be what LOADS the kernel. Every other command needs it, but if this worker
|
|
123
|
+
// has never loaded it then selector_prepare has never run either, so there is no painting to report — and
|
|
124
|
+
// making a save wait on a 4MB WASM load before the first slice is seconds of nothing, for an empty answer.
|
|
125
|
+
if (d.cmd === 'exportPaint' && !modPromise) {
|
|
126
|
+
self.postMessage({ type: 'paintExport', supported: false, facets: [], hex: '' }); return
|
|
127
|
+
}
|
|
112
128
|
if (!modPromise) modPromise = loadCore()
|
|
113
129
|
const Module = await modPromise
|
|
114
130
|
// Warmup: only load the kernel (+ spawn the mt pthread pool) ahead of time — removes the perceived load on the first slice
|
|
@@ -160,6 +176,17 @@ self.onmessage = async (e) => {
|
|
|
160
176
|
reply.applied = applied
|
|
161
177
|
self.postMessage(reply); return
|
|
162
178
|
}
|
|
179
|
+
// exportPaint: the reverse of importPaint — every marked facet's split tree as {facets, hex} in the CURRENT
|
|
180
|
+
// selector's numbering, the same parallel-array pairing importPaint takes (hex is one newline-joined blob).
|
|
181
|
+
// The caller (3mf save) rebases the facet indices back onto per-object numbering; this worker cannot, because
|
|
182
|
+
// which objects were merged in what order is the viewer's knowledge. A kernel built before the binding existed
|
|
183
|
+
// reports supported:false rather than throwing — the save then falls back to the paint it imported.
|
|
184
|
+
if (d.cmd === 'exportPaint') {
|
|
185
|
+
const exported = Module.selector_export_paint ? Module.selector_export_paint() : null
|
|
186
|
+
self.postMessage({ type: 'paintExport', supported: !!Module.selector_export_paint,
|
|
187
|
+
facets: exported?.facets ?? [], hex: exported?.hex ?? '' })
|
|
188
|
+
return
|
|
189
|
+
}
|
|
163
190
|
// clear wipes every state at once, so each requested count is 0 by construction — no need to ask the kernel back.
|
|
164
191
|
if (d.cmd === 'clear') {
|
|
165
192
|
Module.selector_clear()
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-slicer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Browser/WASM 3D-printing slicer reverse-engineered from OrcaSlicer: slicing engine (Arachne walls, tree/grid supports, multi-material), React viewer with GPU toolpath preview, and schema-driven settings UI.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"types": "./types/worker.d.ts",
|
|
22
22
|
"default": "./engine/src/slicer.worker.js"
|
|
23
23
|
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./types/client.d.ts",
|
|
26
|
+
"default": "./engine/src/client.js"
|
|
27
|
+
},
|
|
24
28
|
"./wasm": {
|
|
25
29
|
"types": "./types/wasm.d.ts",
|
|
26
30
|
"default": "./engine/src/slicer_core.js"
|
|
@@ -56,10 +60,12 @@
|
|
|
56
60
|
"./data/*": {
|
|
57
61
|
"types": "./types/data/*.d.ts",
|
|
58
62
|
"default": "./data/*"
|
|
59
|
-
}
|
|
63
|
+
},
|
|
64
|
+
"./package.json": "./package.json"
|
|
60
65
|
},
|
|
66
|
+
"sideEffects": false,
|
|
61
67
|
"scripts": {
|
|
62
|
-
"build": "node types/gen_settings_types.mjs && vite build --config components/vite.config.js && vite build --config viewer/vite.config.js && cp viewer/src/make_worker.js viewer/dist/make_worker.js",
|
|
68
|
+
"build": "node types/gen_settings_types.mjs && node types/gen_kernel_params.mjs && vite build --config components/vite.config.js && vite build --config viewer/vite.config.js && cp viewer/src/make_worker.js viewer/dist/make_worker.js",
|
|
63
69
|
"check": "bash pack_check.sh"
|
|
64
70
|
},
|
|
65
71
|
"peerDependencies": {
|
|
@@ -67,6 +73,11 @@
|
|
|
67
73
|
"react-dom": ">=18",
|
|
68
74
|
"three": "^0.160.0"
|
|
69
75
|
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"react": { "optional": true },
|
|
78
|
+
"react-dom": { "optional": true },
|
|
79
|
+
"three": { "optional": true }
|
|
80
|
+
},
|
|
70
81
|
"devDependencies": {
|
|
71
82
|
"typescript": "^5.6.0",
|
|
72
83
|
"vite": "^5.4.11"
|
|
@@ -75,17 +86,16 @@
|
|
|
75
86
|
"engine",
|
|
76
87
|
"!engine/**/.omc",
|
|
77
88
|
"types",
|
|
89
|
+
"!**/test_*.mjs",
|
|
90
|
+
"!types/gen_*.mjs",
|
|
78
91
|
"components/dist",
|
|
79
92
|
"viewer/dist",
|
|
80
93
|
"viewer/styles.css",
|
|
81
94
|
"components/styles.css",
|
|
82
|
-
"data
|
|
83
|
-
"data/
|
|
84
|
-
"data
|
|
85
|
-
"data/
|
|
86
|
-
"data/printers.json",
|
|
87
|
-
"data/processes.js",
|
|
88
|
-
"data/filaments.js",
|
|
95
|
+
"data",
|
|
96
|
+
"!data/config-schema-builddump.json",
|
|
97
|
+
"!data/**/.omc",
|
|
98
|
+
"!data/printers/**",
|
|
89
99
|
"README.md",
|
|
90
100
|
"LICENSE.txt"
|
|
91
101
|
],
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// three-slicer/client — promise wrapper over the worker protocol in three-slicer/worker.
|
|
2
|
+
import type { SliceLayer, SliceResult } from '../engine/index.d.ts'
|
|
3
|
+
import type { BrushArgs, PaintState, PaintTool } from './worker.d.ts'
|
|
4
|
+
|
|
5
|
+
export type { BrushArgs, PaintState, PaintTool }
|
|
6
|
+
|
|
7
|
+
export interface ClientSliceCallbacks {
|
|
8
|
+
onProgress?: (done: number, total: number) => void
|
|
9
|
+
/** Takes the layers yourself. With it set, the result carries stats only and nothing is buffered here. */
|
|
10
|
+
onLayer?: (layer: SliceLayer) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Painted facet counts. `counts` is present only when the call named states. */
|
|
14
|
+
export interface PaintCounts {
|
|
15
|
+
/** State 1 — the support enforcer, and Extruder 1 */
|
|
16
|
+
enforcer: number
|
|
17
|
+
/** State 2 — the support blocker, and Extruder 2 */
|
|
18
|
+
blocker: number
|
|
19
|
+
counts?: Record<number, number>
|
|
20
|
+
/** Facets an importPaint actually applied */
|
|
21
|
+
applied?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PaintOverlays {
|
|
25
|
+
enforcer: Float32Array
|
|
26
|
+
blocker: Float32Array
|
|
27
|
+
overlays?: Record<number, Float32Array>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SlicerClient {
|
|
31
|
+
/** The worker being driven — for a host that also wants to listen in. */
|
|
32
|
+
worker: Worker
|
|
33
|
+
|
|
34
|
+
/** Load the kernel ahead of the first slice. */
|
|
35
|
+
warmup(): Promise<void>
|
|
36
|
+
|
|
37
|
+
/** `params` may be an object; it is stringified for you, which the raw protocol does not do. */
|
|
38
|
+
slice(stl: ArrayBuffer | Uint8Array, params: object | string, cb?: ClientSliceCallbacks): Promise<SliceResult>
|
|
39
|
+
|
|
40
|
+
/** Register a mesh for painting. `keepPaint` carries existing marks across a move; `kept` says whether they survived. */
|
|
41
|
+
prepare(stl: ArrayBuffer | Uint8Array, opts?: { keepPaint?: boolean }): Promise<{ facets: number; kept: boolean }>
|
|
42
|
+
|
|
43
|
+
paint(args: BrushArgs & {
|
|
44
|
+
state?: PaintState; enforcer?: boolean; tool?: PaintTool
|
|
45
|
+
angle?: number; cursor?: 'sphere' | 'circle'; states?: PaintState[]
|
|
46
|
+
}): Promise<PaintCounts>
|
|
47
|
+
|
|
48
|
+
/** Back to the default extruder. A separate call, not `paint` with state 0 — see three-slicer/worker. */
|
|
49
|
+
erase(args: BrushArgs & { tool?: PaintTool; angle?: number; states?: PaintState[] }): Promise<PaintCounts>
|
|
50
|
+
|
|
51
|
+
clear(states?: PaintState[]): Promise<PaintCounts>
|
|
52
|
+
overlay(states?: PaintState[]): Promise<PaintOverlays>
|
|
53
|
+
|
|
54
|
+
/** Load a 3mf's painting. Replaces every mark, so run it directly after `prepare`. */
|
|
55
|
+
importPaint(facets: Int32Array | number[], hex: string, states?: PaintState[]): Promise<PaintCounts>
|
|
56
|
+
exportPaint(): Promise<{ supported: boolean; facets: number[]; hex: string }>
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Stop the running slice. Works while the worker is blocked in WASM, but the flag lives in a SharedArrayBuffer,
|
|
60
|
+
* so it exists only on the multithreaded kernel — a cross-origin-isolated page. Returns false when there is no
|
|
61
|
+
* flag to write; terminate the worker instead.
|
|
62
|
+
*/
|
|
63
|
+
cancel(): boolean
|
|
64
|
+
|
|
65
|
+
/** Kill the worker and reject everything still in flight. */
|
|
66
|
+
terminate(): void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param worker an existing worker to drive. Omitted, one is created — browser only, since Node has no global
|
|
71
|
+
* `Worker`; in Node use `createSlicer()` from the root entry instead.
|
|
72
|
+
*/
|
|
73
|
+
export function createSlicerClient(worker?: Worker): SlicerClient
|
|
74
|
+
export default createSlicerClient
|
package/types/components.d.ts
CHANGED
|
@@ -13,6 +13,16 @@ export interface SettingsPanelProps {
|
|
|
13
13
|
embedded?: boolean
|
|
14
14
|
/** Replace schema-driven widgets per key (e.g. `{ printable_area: MyBedEditor }`) */
|
|
15
15
|
customWidgets?: Record<string, React.ComponentType<any>>
|
|
16
|
+
/**
|
|
17
|
+
* Pins the panel to one builder (optionally one page) and drops the search/group/page/mode chrome, so the form
|
|
18
|
+
* can be folded into another card. This is how `<Viewport/>`'s `motionPanel` slot is meant to be filled:
|
|
19
|
+
*
|
|
20
|
+
* ```jsx
|
|
21
|
+
* <SettingsPanel settings={settings} setSettings={setSettings} embedded
|
|
22
|
+
* only={{ builder: 'TabPrinter::build_kinematics_page' }} />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
only?: { builder: string; page?: string }
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
declare const SettingsPanel: React.FC<SettingsPanelProps>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// preset-keys.json — which option keys belong to which preset type, extracted from upstream's Preset.cpp
|
|
2
|
+
// (`s_Preset_print_options`, `s_Preset_filament_options`, and printer_options() = printer + machine limits +
|
|
3
|
+
// the per-extruder keys from PrintConfig.cpp).
|
|
4
|
+
//
|
|
5
|
+
// Distinct from printers.json's `keys`, which is the 21 columns the kernel reads: that is what a picker needs,
|
|
6
|
+
// this is what writing a preset FILE another slicer will accept needs. Keys the config schema does not define are
|
|
7
|
+
// dropped at extraction — every consumer coerces by schema type, so an untyped key could not be handled anyway.
|
|
8
|
+
// Read it through `presetOptionKeys()` in three-slicer/settings rather than indexing this by hand.
|
|
9
|
+
|
|
10
|
+
export interface PresetKeyLists {
|
|
11
|
+
/** Machine preset options — upstream's `Preset::printer_options()` */
|
|
12
|
+
printer: string[]
|
|
13
|
+
/** Print (process) preset options */
|
|
14
|
+
process: string[]
|
|
15
|
+
/** Filament preset options */
|
|
16
|
+
filament: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare const presetKeys: PresetKeyLists
|
|
20
|
+
export default presetKeys
|
|
@@ -7,8 +7,8 @@
|
|
|
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]` */
|
|
11
|
-
export type PrinterEntry = [string, number, string]
|
|
10
|
+
/** `[nozzle diameter, index into sets, model name without the nozzle suffix, default process preset]` */
|
|
11
|
+
export type PrinterEntry = [string, number, string, string]
|
|
12
12
|
|
|
13
13
|
export interface PrinterData {
|
|
14
14
|
/** Option keys, in column order — the same keys the settings panel edits */
|
package/types/data.d.ts
CHANGED
|
@@ -5,10 +5,12 @@ import type { UIPage } from './data/ui-tree.json.d.ts'
|
|
|
5
5
|
import type { ToggleGroup } from './data/toggle-rules.json.d.ts'
|
|
6
6
|
import type { InvalidationRule } from './data/invalidation-map.json.d.ts'
|
|
7
7
|
import type { PrinterData, PrinterRow, PrinterEntry } from './data/printers.json.d.ts'
|
|
8
|
+
import type { PresetKeyLists } from './data/preset-keys.json.d.ts'
|
|
8
9
|
import type { ProcessData } from './data/processes.js.d.ts'
|
|
9
10
|
import type { FilamentData, FilamentRow, FilamentEntry } from './data/filaments.js.d.ts'
|
|
10
11
|
|
|
11
12
|
export type { ConfigOption, UIPage, ToggleGroup, InvalidationRule, PrinterData, PrinterRow, PrinterEntry }
|
|
13
|
+
export type { PresetKeyLists }
|
|
12
14
|
export type { ProcessData, FilamentData, FilamentRow, FilamentEntry }
|
|
13
15
|
|
|
14
16
|
/** The 907 option definitions */
|
|
@@ -21,6 +23,8 @@ export const toggleRules: Record<string, ToggleGroup>
|
|
|
21
23
|
export const invalidationMap: Record<string, InvalidationRule[]>
|
|
22
24
|
/** Machine limits per printer, from the upstream vendor profiles */
|
|
23
25
|
export const printers: PrinterData
|
|
26
|
+
/** Which option keys belong to which preset type — read it through `presetOptionKeys()` */
|
|
27
|
+
export const presetKeys: PresetKeyLists
|
|
24
28
|
// The two large artifacts load on demand — read them through processPresets()/filamentPresets()
|
|
25
29
|
// in three-slicer/settings rather than decoding the column layout here.
|
|
26
30
|
export function loadProcesses(): Promise<ProcessData>
|