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.
@@ -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)
@@ -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
- export function deriveKernelParams(settings) {
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 — so the scalar reduction takes the first set one.
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 v = settings?.[key]; const n = Number(Array.isArray(v) ? v[0] : v)
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
- console.info('[slicer.worker] core: mt (threads)')
26
+ say('info', '[slicer.worker] core: mt (threads)')
20
27
  return M
21
- } catch (e) { console.warn('[slicer.worker] mt load failed — falling back to st:', 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
- console.info('[slicer.worker] core: st')
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.5",
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/config-schema.json",
83
- "data/ui-tree.json",
84
- "data/toggle-rules.json",
85
- "data/invalidation-map.json",
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
@@ -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>