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.
@@ -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
@@ -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
- 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,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.6",
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>
@@ -1,6 +1,7 @@
1
1
  // three-slicer/settings
2
2
  import type { SlicerSettings, SettingKey, Point } from './settings-keys.d.ts'
3
- export type { SlicerSettings, SettingKey, Point }
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 =