three-slicer 0.1.6 → 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 +253 -21
- package/data/filaments.js +1 -1
- package/data/preset-keys.json +1 -0
- package/data/processes.js +1 -1
- package/engine/README.md +259 -3
- 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 +75 -0
- package/engine/src/slicer.worker.js +13 -3
- 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 +82 -1
- package/types/viewer.d.ts +57 -1
- package/types/worker.d.ts +87 -0
- package/viewer/README.md +156 -2
- package/viewer/dist/Viewport.js +2937 -2757
- package/viewer/styles.css +5 -0
- 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
|
|
@@ -353,6 +390,38 @@ export function deriveKernelParams(settings, opts) {
|
|
|
353
390
|
if (prime > 0) towerSettings.prime_volume = prime
|
|
354
391
|
}
|
|
355
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
|
+
|
|
356
425
|
const perExtruder = {}
|
|
357
426
|
for (const [param, key, scalar] of [
|
|
358
427
|
['extruder_nozzle_temp', 'nozzle_temperature', num('nozzle_temperature', 200)],
|
|
@@ -374,6 +443,7 @@ export function deriveKernelParams(settings, opts) {
|
|
|
374
443
|
return {
|
|
375
444
|
...widths,
|
|
376
445
|
...machine,
|
|
446
|
+
...passthrough, // present only for keys the settings map actually holds (see PASSTHROUGH_* above)
|
|
377
447
|
layer_height: num('layer_height', 0.2),
|
|
378
448
|
first_layer_height: num('initial_layer_print_height', 0.2),
|
|
379
449
|
line_width,
|
|
@@ -463,3 +533,8 @@ export function deriveKernelParams(settings, opts) {
|
|
|
463
533
|
// support_density has no matching schema key, so the kernel default (0.15) is used; scarf_length likewise uses the kernel default (10mm)
|
|
464
534
|
}
|
|
465
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,6 +115,9 @@ 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 {
|
|
112
122
|
// Asking for the painting must not be what LOADS the kernel. Every other command needs it, but if this worker
|
|
113
123
|
// has never loaded it then selector_prepare has never run either, so there is no painting to report — and
|
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>
|
package/types/settings.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// three-slicer/settings
|
|
2
2
|
import type { SlicerSettings, SettingKey, Point } from './settings-keys.d.ts'
|
|
3
|
-
|
|
3
|
+
import type { PrinterEntry } from './data/printers.json.d.ts'
|
|
4
|
+
export type { SlicerSettings, SettingKey, Point, PrinterEntry }
|
|
4
5
|
|
|
5
6
|
/** The config-schema default value (undefined for unknown keys) */
|
|
6
7
|
export function schemaDefault(key: string): unknown
|
|
@@ -37,6 +38,86 @@ export function normalizeProjectSettings(raw: Record<string, unknown> | null | u
|
|
|
37
38
|
*/
|
|
38
39
|
export function serializeProjectSettings(settings: SlicerSettings | null | undefined): Record<string, string | string[]>
|
|
39
40
|
|
|
41
|
+
// ---- Preset files -----------------------------------------------------------
|
|
42
|
+
// The `.json` OrcaSlicer's "Config files" dialog imports and its "Export preset" writes. One file, one preset,
|
|
43
|
+
// one type. The value encoding is the same all-strings shape `project_settings.config` uses — same writer
|
|
44
|
+
// upstream (ConfigBase::save_to_json) — so these reuse the coercion above rather than repeating it.
|
|
45
|
+
|
|
46
|
+
/** Upstream's own `type` field: machine = printer, process = print settings, filament = material. */
|
|
47
|
+
export type PresetType = 'machine' | 'process' | 'filament'
|
|
48
|
+
|
|
49
|
+
/** The option keys that belong in a preset of this type, from upstream's `Preset::printer_options()` and friends. */
|
|
50
|
+
export function presetOptionKeys(type: PresetType): string[]
|
|
51
|
+
|
|
52
|
+
export interface WritePresetOptions {
|
|
53
|
+
/** Defaults to `'machine'`. */
|
|
54
|
+
type?: PresetType
|
|
55
|
+
/** Goes in the file's `name` field, and is what an importer shows. Defaults to `'Custom'`. */
|
|
56
|
+
name?: string
|
|
57
|
+
/**
|
|
58
|
+
* `true` (the default) writes every option of that type, filling unset ones from the config-schema default, so
|
|
59
|
+
* the file stands on its own. `false` writes only what the settings map holds — a diff-shaped file, readable
|
|
60
|
+
* only next to whatever it was derived from.
|
|
61
|
+
*/
|
|
62
|
+
complete?: boolean
|
|
63
|
+
version?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A settings map -> the object a preset `.json` holds. Written **without `inherits`**: upstream saves a derived
|
|
68
|
+
* preset as the diff against its parent, which only means something beside the vendor database it came from.
|
|
69
|
+
*/
|
|
70
|
+
export function writePresetFile(settings: SlicerSettings | null | undefined, options?: WritePresetOptions): Record<string, unknown>
|
|
71
|
+
|
|
72
|
+
/** The same, already stringified — what an export actually writes to disk. */
|
|
73
|
+
export function presetFileText(settings: SlicerSettings | null | undefined, options?: WritePresetOptions): string
|
|
74
|
+
|
|
75
|
+
export interface ReadPresetResult {
|
|
76
|
+
/** The parent's values with the file's own on top, since the file is the diff. */
|
|
77
|
+
settings: SlicerSettings
|
|
78
|
+
type: PresetType
|
|
79
|
+
name: string
|
|
80
|
+
/** The parent named by the file, or `null`. */
|
|
81
|
+
inherits: string | null
|
|
82
|
+
/**
|
|
83
|
+
* Set to the parent's name when it was named but could not be resolved. The file's own values are still
|
|
84
|
+
* applied — a vendor preset without its parent still carries most of itself, and the caller decides whether
|
|
85
|
+
* that is good enough. Watch for it: the Bambu X1C machine file never states its own bed, so read alone it
|
|
86
|
+
* yields a printer with no `printable_area` at all.
|
|
87
|
+
*/
|
|
88
|
+
missingParent: string | null
|
|
89
|
+
applied: number
|
|
90
|
+
skipped: string[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A parsed preset `.json` -> a settings map. Pass `resolveParent` to follow `inherits`; `printerSettings` covers
|
|
95
|
+
* the machine case for the keys this package carries.
|
|
96
|
+
*/
|
|
97
|
+
export function readPresetFile(raw: Record<string, unknown> | null | undefined,
|
|
98
|
+
options?: { resolveParent?: (name: string, type: PresetType) => SlicerSettings | null }): ReadPresetResult
|
|
99
|
+
|
|
100
|
+
// ---- Printer profiles -------------------------------------------------------
|
|
101
|
+
// printers.json is column-oriented; these hide that layout so no consumer decodes it by hand.
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Every option key a printer profile can set. Delete these from the settings map before applying a different
|
|
105
|
+
* printer — a profile only carries the keys it sets, so without the clear the previous machine's values survive.
|
|
106
|
+
*/
|
|
107
|
+
export const printerKeys: string[]
|
|
108
|
+
|
|
109
|
+
/** Vendor -> profile name -> entry, straight from the data. For building a picker. */
|
|
110
|
+
export const printersByVendor: Record<string, Record<string, PrinterEntry>>
|
|
111
|
+
|
|
112
|
+
/** The settings a printer profile applies, ready to merge. `null` when the name is unknown. */
|
|
113
|
+
export function printerSettings(profileName: string): SlicerSettings | null
|
|
114
|
+
|
|
115
|
+
/** The vendor's recommended process preset for that printer, or `''` when the profile names none. */
|
|
116
|
+
export function printerDefaultPreset(profileName: string): string
|
|
117
|
+
|
|
118
|
+
/** The schema keys the kernel's machine limits are read from — for UI that shows which ones are in play. */
|
|
119
|
+
export const machineLimitKeys: string[]
|
|
120
|
+
|
|
40
121
|
// ---- Lazily loaded presets --------------------------------------------------
|
|
41
122
|
// Both facades hide the column layout of the artifact they wrap; the promise is created once and reused.
|
|
42
123
|
|
package/types/viewer.d.ts
CHANGED
|
@@ -21,8 +21,32 @@ export interface ViewportProps {
|
|
|
21
21
|
/**
|
|
22
22
|
* Per-panel visibility. Every panel is visible unless its key is explicitly `false`, so a host only opts out —
|
|
23
23
|
* and a panel added in a later version does not disappear for hosts that listed the ones they wanted.
|
|
24
|
+
*
|
|
25
|
+
* A {@link LockablePanel} also accepts `'readonly'`: the panel is drawn but nothing inside it can be clicked,
|
|
26
|
+
* typed into or tabbed to — including any node the host passed into it. That is the shape a host wants when it
|
|
27
|
+
* presets the printer, process and filament itself and does not want the user changing them.
|
|
28
|
+
*
|
|
29
|
+
* Note what it locks: the UI path, not the value. `settings` is still the host's own prop, and other paths
|
|
30
|
+
* inside the viewer still write it — importing a 3mf project applies that project's settings either way.
|
|
24
31
|
*/
|
|
25
|
-
panels?: Partial<Record<ViewportPanel, boolean>>
|
|
32
|
+
panels?: Partial<Record<ViewportPanel, boolean>> & Partial<Record<LockablePanel, boolean | 'readonly'>>
|
|
33
|
+
/**
|
|
34
|
+
* Behaviour switches, following the same opt-out rule as {@link ViewportProps.panels}: everything is on unless
|
|
35
|
+
* its key is explicitly `false`. These are the things the component does *outside* its own box — take the page's
|
|
36
|
+
* keyboard, load the WASM kernel, write to the console — which a host embedding it in a larger app may already
|
|
37
|
+
* be doing itself. See {@link ViewportFeature}.
|
|
38
|
+
*/
|
|
39
|
+
features?: Partial<Record<ViewportFeature, boolean>>
|
|
40
|
+
/**
|
|
41
|
+
* Intercepts every file the viewer would otherwise hand to the browser as a download — the 3mf project, the STL,
|
|
42
|
+
* and each plate's G-code. Return anything truthy to say it is handled and suppress the download; return nothing
|
|
43
|
+
* to let it proceed as well. This is the only way in: the save buttons live inside the component.
|
|
44
|
+
*
|
|
45
|
+
* ```jsx
|
|
46
|
+
* <Viewport onExport={(blob, filename) => { upload(blob, filename); return true }} />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
onExport?: (file: Blob, filename: string) => boolean | void
|
|
26
50
|
/**
|
|
27
51
|
* G-code text to render instead of a slice result. Parsed into the same layer stream the kernel produces and
|
|
28
52
|
* shown on the selected plate; the kernel is never started. While it is set, auto re-slice leaves that plate alone.
|
|
@@ -41,10 +65,42 @@ export interface ViewportProps {
|
|
|
41
65
|
onSliced?: (result: { plate: number; stats: Record<string, unknown>; gcode: string }) => void
|
|
42
66
|
}
|
|
43
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Behaviour keys accepted by {@link ViewportProps.features}. All default to enabled.
|
|
70
|
+
*
|
|
71
|
+
* - `shortcuts` — the keyboard bindings. They are installed on `window`, so while the viewer is mounted they fire
|
|
72
|
+
* wherever focus is (except inside inputs) and `Ctrl+C` in particular preventDefaults. Turn them off when the
|
|
73
|
+
* host page has its own.
|
|
74
|
+
* - `warmup` — loading the WASM kernel on mount, ahead of the first slice. Off, nothing is downloaded or compiled
|
|
75
|
+
* until something actually slices — which for a `gcode`-only viewer is never.
|
|
76
|
+
* - `drop` — drag and drop onto the canvas.
|
|
77
|
+
* - `filePicker` — the file dialog, from every button that opens it.
|
|
78
|
+
* - `contextMenu` — the right-click menu. Note that the browser's own menu stays suppressed over the canvas either
|
|
79
|
+
* way: OrbitControls preventDefaults `contextmenu` itself.
|
|
80
|
+
* - `logs` — console diagnostics, from the component and from the slice worker.
|
|
81
|
+
*/
|
|
82
|
+
export type ViewportFeature = 'shortcuts' | 'warmup' | 'drop' | 'filePicker' | 'contextMenu' | 'logs'
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The panels that accept `'readonly'` as well as `true`/`false` — the right column and the cards in it. Everything
|
|
86
|
+
* else takes a boolean only: locking the gizmo rail or the plate tabs is what `features` and `panels: false` are
|
|
87
|
+
* for, and pretending otherwise would mean a `'readonly'` that silently does nothing on half the keys.
|
|
88
|
+
*
|
|
89
|
+
* `sidebar: 'readonly'` locks the whole column in one go, so the per-card keys are for mixing — a locked printer
|
|
90
|
+
* card above a live object list, say.
|
|
91
|
+
*/
|
|
92
|
+
export type LockablePanel =
|
|
93
|
+
| 'sidebar' | 'printerCard' | 'filamentCard' | 'objectList' | 'towerCard'
|
|
94
|
+
| 'previewControls' | 'processCard' | 'sliceBar'
|
|
95
|
+
|
|
44
96
|
/** Panel keys accepted by {@link ViewportProps.panels}. `sidebar: false` hides the whole right column at once. */
|
|
45
97
|
export type ViewportPanel =
|
|
46
98
|
| 'topBar' | 'gizmoRail' | 'objectToolbar' | 'paintPanel' | 'statsCard' | 'plateBar' | 'emptyHint' | 'status'
|
|
47
99
|
| 'sidebar' | 'printerCard' | 'filamentCard' | 'objectList' | 'previewControls' | 'processCard' | 'sliceBar'
|
|
100
|
+
/** The over-the-bed warning, shown in Prepare when something leaves the printable area. */
|
|
101
|
+
| 'bedWarn'
|
|
102
|
+
/** The prime tower card. Only rendered anyway once a second filament exists. */
|
|
103
|
+
| 'towerCard'
|
|
48
104
|
|
|
49
105
|
/** Value changes reported through {@link ViewportProps.onEvent}. Not fired for the initial values. */
|
|
50
106
|
export type ViewportEvent =
|