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.
@@ -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
@@ -16,7 +17,7 @@ export function settingScalar(settings: SlicerSettings | null | undefined, key:
16
17
  * ponytail: 53 returned keys, so Record instead of listing each — kernel params are passed straight to slice(),
17
18
  * not an API consumers read field by field. Expand it when field access is actually needed.
18
19
  */
19
- export function deriveKernelParams(settings: SlicerSettings | null | undefined): Record<string, unknown>
20
+ export function deriveKernelParams(settings: SlicerSettings | null | undefined, opts?: { plate?: number }): Record<string, unknown>
20
21
 
21
22
  /**
22
23
  * A 3mf project's raw `Metadata/project_settings.config` -> a settings map this API accepts.
@@ -30,6 +31,93 @@ export function normalizeProjectSettings(raw: Record<string, unknown> | null | u
30
31
  skipped: string[]
31
32
  }
32
33
 
34
+ /**
35
+ * The inverse, for WRITING a 3mf's `Metadata/project_settings.config`: every value back to the string shape
36
+ * upstream serializes (a bool as "1"/"0", a point as "XxY", a points group as "X1xY1,X2xY2"), so another slicer
37
+ * reads what it expects. Keys the config schema does not define are dropped.
38
+ */
39
+ export function serializeProjectSettings(settings: SlicerSettings | null | undefined): Record<string, string | string[]>
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
+
33
121
  // ---- Lazily loaded presets --------------------------------------------------
34
122
  // Both facades hide the column layout of the artifact they wrap; the promise is created once and reused.
35
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 =
package/types/worker.d.ts CHANGED
@@ -1,3 +1,90 @@
1
1
  // three-slicer/worker — the slice worker script. Not a module you import, but the target of
2
2
  // `new Worker(new URL('three-slicer/worker', import.meta.url), { type: 'module' })`.
3
+ //
4
+ // The protocol below is the raw message contract. `createSlicerClient()` from `three-slicer/client` wraps it in
5
+ // promises and is the easier way in; these types are here for hosts driving the worker directly, and because the
6
+ // shape has traps that only a written-down contract makes visible — see the slice request and `erase`.
7
+ import type { SliceResult } from '../engine/index.d.ts'
8
+
9
+ /** Brush geometry shared by paint and erase: the hit facet, the hit point, the camera, the radius. */
10
+ export interface BrushArgs {
11
+ facet: number
12
+ hx: number; hy: number; hz: number
13
+ cx: number; cy: number; cz: number
14
+ radius: number
15
+ }
16
+
17
+ /**
18
+ * Painting state, following OrcaSlicer's own `EnforcerBlockerType`: one enum serving two jobs.
19
+ * `1` is both the support ENFORCER and Extruder 1, `2` is both the BLOCKER and Extruder 2, `3..16` are
20
+ * Extruders 3 to 16. `0` (NONE) is deliberately not a value here — see the `erase` command.
21
+ */
22
+ export type PaintState = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16
23
+
24
+ /** Selection tools that are not the radius brush. All three take the mesh's own topology instead of a radius. */
25
+ export type PaintTool = 'smart' | 'bucket' | 'triangle'
26
+
27
+ export type SlicerRequest =
28
+ /** Load the kernel (and spawn the mt thread pool) ahead of the first slice. Reply: `warm`. */
29
+ | { cmd: 'warmup' }
30
+ /**
31
+ * Register a mesh for painting. `keepPaint` says this is the same model in a new place, so the marks carry
32
+ * over — the reply reports whether they actually did. Reply: `prepared`.
33
+ */
34
+ | { cmd: 'prepare'; stl: ArrayBuffer; keepPaint?: boolean }
35
+ /**
36
+ * Paint. With `state` it addresses any extruder; with `enforcer` it stays on the original boolean pair, where
37
+ * `false` means BLOCKER. `states` asks for those states' counts back. Reply: `painted`.
38
+ */
39
+ | (BrushArgs & { cmd: 'paint'; state?: PaintState; enforcer?: boolean; tool?: PaintTool
40
+ angle?: number; cursor?: 'sphere' | 'circle'; states?: PaintState[] })
41
+ /**
42
+ * Return the brushed facets to the default extruder. Its OWN command rather than `{cmd:'paint', state:0}`:
43
+ * embind coerces a JS `false` to the int `0` == NONE, and the legacy blocker brush sends exactly that `false`,
44
+ * so admitting `0` on the state path would let a stray boolean erase silently. Reply: `painted`.
45
+ */
46
+ | (BrushArgs & { cmd: 'erase'; tool?: PaintTool; angle?: number; states?: PaintState[] })
47
+ /**
48
+ * Load painting out of a 3mf. REPLACES every mark (the kernel's deserialize resets first), so it belongs
49
+ * immediately after a `prepare` and nowhere else. Reply: `painted`, with `applied`.
50
+ */
51
+ | { cmd: 'importPaint'; facets: Int32Array | number[]; hex: string; states?: PaintState[] }
52
+ /** Every marked facet's split tree, in the CURRENT selector's numbering. Reply: `paintExport`. */
53
+ | { cmd: 'exportPaint' }
54
+ /** Wipe every state at once. Reply: `painted`, all zero. */
55
+ | { cmd: 'clear'; states?: PaintState[] }
56
+ /** The painted overlay triangles. `enf`/`blk` always; `states` adds those states. Reply: `overlay`. */
57
+ | { cmd: 'overlay'; states?: PaintState[] }
58
+ /**
59
+ * Slice — and note it carries **no `cmd`**: slicing is the worker's default action, and leaving the field off
60
+ * is what selects it. `params` must be a JSON **string**; the worker hands it straight to the kernel, which
61
+ * parses JSON text. (`createSlicer().slice()` on the direct handle also accepts an object and stringifies it.)
62
+ * Replies: `progress` and `layer` while it runs, then `done` — or `error`.
63
+ */
64
+ | { stl: ArrayBuffer; params: string }
65
+
66
+ export type SlicerResponse =
67
+ | { type: 'warm' }
68
+ | { type: 'prepared'; facets: number; kept: boolean }
69
+ /**
70
+ * `enf`/`blk` are the counts for states 1 and 2 and are always present, so a listener predating the
71
+ * state-addressed protocol keeps working. `counts` appears only when the request named `states`.
72
+ */
73
+ | { type: 'painted'; enf: number; blk: number; counts?: Record<number, number>; applied?: number }
74
+ /** `supported: false` from a kernel built before the export binding existed — it does not throw. */
75
+ | { type: 'paintExport'; supported: boolean; facets: number[]; hex: string }
76
+ | { type: 'overlay'; enf: Float32Array; blk: Float32Array; overlays?: Record<number, Float32Array> }
77
+ /**
78
+ * Sent once, unprompted, by the multithreaded kernel only: the address of the support-progress counter and of
79
+ * the cancel flag, inside a SharedArrayBuffer. Writing 1 to the cancel flag stops a slice the worker is
80
+ * currently blocked inside. A single-threaded kernel has no shared buffer and never sends this.
81
+ */
82
+ | { type: 'supsab'; buf: SharedArrayBuffer; ptr: number; cancelPtr: number }
83
+ | { type: 'progress'; done: number; total: number }
84
+ /** One sliced layer, its typed arrays transferred rather than copied. */
85
+ | { type: 'layer'; z: number; idx: number; gcode: string; paths: Float32Array; widths: Float32Array }
86
+ /** `result.stats.streamed` says whether the G-code came through `layer` messages or is on the result. */
87
+ | { type: 'done'; result: SliceResult }
88
+ | { type: 'error'; error: string }
89
+
3
90
  export {}
package/viewer/README.md CHANGED
@@ -2,23 +2,42 @@
2
2
 
3
3
  3D slicer viewer as a React component: three.js viewport (orbit/transform gizmos), model import (STL/OBJ/3MF/AMF/PLY + pluggable formats such as STEP, multi-object, drag & drop), Web Worker slicing via `three-slicer`, support and material painting, per-extruder filament presets, multi-plate, and a GPU-instanced volumetric toolpath preview ported from OrcaSlicer's libvgcode (millions of segments in a single draw path, per-feature colors, layer range slider, G-code export).
4
4
 
5
+ ![The Prepare tab — drag-and-drop viewport with the printer, filament and process cards in the right sidebar](../../slice-sidebar.png)
6
+
5
7
  ```bash
6
- npm i three-slicer/viewer react react-dom three@^0.160.0
8
+ npm i three-slicer react react-dom three # the three are optional peers; the viewer needs all of them
7
9
  ```
8
10
 
9
11
  ```jsx
10
12
  import { useState } from 'react'
11
13
  import Viewport from 'three-slicer/viewer'
12
- import 'three-slicer/viewer/styles.css'
13
14
 
14
15
  function App() {
15
16
  const [settings, setSettings] = useState({}) // OrcaSlicer schema keys; sparse — defaults fill the rest
16
- return <Viewport settings={settings} setSettings={setSettings} />
17
+ // The container matters — see Layout below.
18
+ return (
19
+ <div style={{ position: 'relative', width: '100%', height: '100vh' }}>
20
+ <Viewport settings={settings} setSettings={setSettings} />
21
+ </div>
22
+ )
17
23
  }
18
24
  ```
19
25
 
26
+ Styles are injected into the component's Shadow DOM root — nothing to import, and host CSS cannot collide.
27
+
28
+ ## Layout
29
+
30
+ **Give it a positioned, sized container.** The shadow host is `display: contents`, so it contributes no box of its
31
+ own, and the shell inside is `position: absolute; inset: 0`. It therefore fills the nearest *positioned* ancestor —
32
+ `position: relative` (or absolute/fixed) with a real height. Drop it into a plain static `<div>` and it escapes to
33
+ the initial containing block instead, which usually looks like the viewer covering the whole page.
34
+
35
+ There is no width or height prop: the component always fills that container, and resizes with it.
36
+
20
37
  Slice parameters are derived from `settings` via `three-slicer`'s schema mapping — pair it with `three-slicer/components`' `<SettingsPanel/>` sharing the same state for a full slicer UI.
21
38
 
39
+ A `.3mf` written by a slicer (OrcaSlicer/BambuStudio save, MakerWorld download) imports as a **project**: plate layout, project settings, and support/material painting are restored, not just the meshes. Where a facet carries both paint kinds, material paint wins and the dropped support paint is reported.
40
+
22
41
  Props: `settings`, `setSettings`, and three optional React-node slots rendered in the right sidebar —
23
42
  `processPanel` (the process card), `motionPanel` (folded into the printer card) and `filamentPanel`
24
43
  (folded into the filament card, next to the material picker).
@@ -58,6 +77,55 @@ Every panel can be switched off, and the values the component owns can be seeded
58
77
  second while slicing.
59
78
  - **`onSliced`** — `{plate, stats, gcode}` when a slice is cached. Switching plate tabs does not re-fire it.
60
79
 
80
+ ## Keyboard and mouse
81
+
82
+ Which keys are live depends on the tab, and every one is ignored while an input has focus. Letter shortcuts match
83
+ the **physical** key, so they keep working under a non-Latin keyboard layout — with an IME on, the Korean layout
84
+ reports `'ㅁ'` for M, and matching on the character would silently kill every letter shortcut while leaving the
85
+ arrows and Delete working.
86
+
87
+ | Both tabs | |
88
+ | --- | --- |
89
+ | `Ctrl/⌘ + R` | Slice the current plate |
90
+ | `Ctrl/⌘ + C` / `V` / `X` | Copy · paste · cut the selection |
91
+ | `Ctrl/⌘ + K` or `Ctrl/⌘ + D` | Duplicate |
92
+ | `Ctrl/⌘ + A` | Select all |
93
+ | `Ctrl/⌘ + Z` | Undo — bound to the component root, so it never reaches your app's own Ctrl+Z |
94
+ | `Ctrl/⌘ + Shift + Z` or `Ctrl/⌘ + Y` | Redo |
95
+
96
+ | Prepare | |
97
+ | --- | --- |
98
+ | `M` or `G` · `R` · `S` | Move · rotate · scale gizmo |
99
+ | `Arrow keys` | Nudge 10mm — `Shift` for 1mm |
100
+ | `PageUp` / `PageDown` | Rotate ±45° |
101
+ | `Delete` / `Backspace` | Remove the selection |
102
+ | `Z` · `B` | Zoom to all · zoom to bed |
103
+ | `Esc` | Cancel the current tool |
104
+ | `?` | Shortcut help overlay |
105
+
106
+ | Preview | |
107
+ | --- | --- |
108
+ | `↑` / `↓` | Step one layer — `Shift` for ten |
109
+ | `L` · `T` | Single-layer view · travel moves |
110
+ | `Z` · `B` | Zoom to all · zoom to bed |
111
+ | `Esc` | Back to Prepare |
112
+
113
+ Mouse selection follows upstream's rules: a plain click **replaces** the selection, `Ctrl/⌘ + click` adds or
114
+ removes, a plain click on something already selected **keeps** the set (which is what makes dragging several
115
+ objects work), `Shift + drag` is a box select, and clicking empty space clears. Alt is not a de-select, in
116
+ upstream or here.
117
+
118
+ ## Undo and redo
119
+
120
+ The history covers the viewport's **own** state: object transforms, additions, removals, per-object extruder and
121
+ visibility. Fifty steps, with repeats of the same action inside half a second folded into one — so a held arrow key
122
+ undoes as a single move rather than forty.
123
+
124
+ What it deliberately does not cover, and why the boundary sits there: `settings` is **yours**, passed in as a prop,
125
+ so nothing inside the component can put it back. Painting is outside it too (restoring a step would need a kernel
126
+ re-`prepare` for every entry), as are the prime tower and the plate count, which write host settings. If you want
127
+ those undoable, undo them on your side of the prop.
128
+
61
129
  ## Materials and painting
62
130
 
63
131
  The filament card holds **one material per extruder**: add an extruder, pick its colour, and pick its filament preset from the vendor catalog (`filamentPresets()` in `three-slicer/settings` — the shortlist is the printer model's own recommendation, the full list everything compatible). Selecting a row aims the material picker and the settings form at that extruder; the colours feed the object meshes, the prime tower and the preview's **Filament** view type, which colours every segment by the extruder that printed it.
@@ -71,6 +139,97 @@ Two limits to expect while using it:
71
139
 
72
140
  If you consume `three-slicer/viewer/toolpath` directly, note that the stride-8 role field `paths[k+3]` now carries the printing extruder alongside the role as `role + tool * 16`. Mask it — `& 15` for the role, `>>> 4` for the tool. Streams produced before this encoding are entirely below 16 and decode to their own role with tool 0, so older data still reads correctly.
73
141
 
142
+ ## Turning behaviour off
143
+
144
+ `panels` controls what is drawn; `features` controls what the component *does* outside its own box. Same rule —
145
+ opt-out only, everything on unless you say `false`, an unknown key stays on.
146
+
147
+ ```jsx
148
+ <Viewport
149
+ gcode={text}
150
+ panels={{ sidebar: false, topBar: false }}
151
+ features={{ warmup: false, shortcuts: false, logs: false }}
152
+ />
153
+ ```
154
+
155
+ | Key | Off means |
156
+ | --- | --- |
157
+ | `shortcuts` | No keyboard bindings. They are installed on `window`, so while the viewer is mounted they fire wherever focus is (inputs excepted) — and `Ctrl+C` preventDefaults, which takes a host page's own copy. Turn them off when your app has its own. |
158
+ | `warmup` | The WASM kernel is not loaded on mount. Nothing is downloaded or compiled until something actually slices — which for a `gcode`-only viewer is never, so this is several megabytes and a thread pool you stop paying for. |
159
+ | `drop` | No drag and drop onto the canvas. |
160
+ | `filePicker` | The file dialog never opens, from any of the buttons that open it. |
161
+ | `contextMenu` | No right-click menu. The browser's own menu stays suppressed over the canvas either way — OrbitControls preventDefaults `contextmenu` itself. |
162
+ | `logs` | No console output, from the component or from the slice worker. |
163
+
164
+ ### Read-only panels
165
+
166
+ A sidebar panel can also be shown but not editable — the shape you want when the host picks the printer, process
167
+ and filament itself and does not want the user changing them. It is a third value on the same `panels` key:
168
+
169
+ ```jsx
170
+ <Viewport
171
+ settings={presetSettings}
172
+ panels={{ printerCard: 'readonly', filamentCard: 'readonly', processCard: 'readonly' }}
173
+ />
174
+ ```
175
+
176
+ Lockable: `sidebar`, `printerCard`, `filamentCard`, `objectList`, `towerCard`, `previewControls`, `processCard`,
177
+ `sliceBar`. `sidebar: 'readonly'` locks the whole column at once; the per-card keys are for mixing, such as a
178
+ locked printer card above a live object list. Every other panel takes `true`/`false` only — a `'readonly'` that
179
+ silently did nothing on half the keys would be worse than not offering it.
180
+
181
+ The panel is drawn normally and keeps showing real values; it just cannot be clicked, typed into or tabbed to, and
182
+ that includes any node you passed into it (`motionPanel`, `filamentPanel`, `processPanel`). Implemented with the
183
+ `inert` attribute, so it needs Chrome 102+, Safari 15.5+ or Firefox 112+.
184
+
185
+ **It locks the UI path, not the value.** `settings` is still your prop, and other paths inside the viewer still
186
+ write it — importing a 3mf project applies that project's settings whether or not the cards are locked.
187
+
188
+ ### Printer preset files
189
+
190
+ The printer card carries **Load** and **Save**. Save writes an OrcaSlicer preset: a machine `.json` on its own, or
191
+ an `.orca_printer` bundle when the settings map also names a process or filament preset (the cards record those as
192
+ `printer_settings_id` / `print_settings_id` / `filament_settings_id`, so nothing extra has to be tracked). Load
193
+ accepts the same set upstream's dialog does — `.json`, `.orca_printer`, `.orca_bundle`, `.orca_filament`, `.zip` —
194
+ and applies every preset it finds, clearing that type's keys first.
195
+
196
+ `inherits` is followed through the shipped printer catalog, so a vendor file loads with its bed intact; when the
197
+ parent is unknown the file's own values are still applied and the notice says so. Codec details and the reason
198
+ that matters are in the [package README](../README.md#preset-files).
199
+
200
+ Load opens a file dialog, so it respects `features.filePicker`; Save goes through `onExport` like every other
201
+ download.
202
+
203
+ ### Taking the saves
204
+
205
+ The save buttons are inside the component, so `onExport` is the only way to send a file somewhere other than the
206
+ browser's download folder. It sees the 3mf project, the STL and each plate's G-code. Return truthy to say you
207
+ handled it:
208
+
209
+ ```jsx
210
+ <Viewport onExport={(file, filename) => { uploadToServer(file, filename); return true }} />
211
+ ```
212
+
213
+ Return nothing and the download happens as well, which is useful for logging what was saved without changing it.
214
+
215
+ ## What the host cannot drive
216
+
217
+ The component owns its scene, and the props are the whole interface — there is no ref, no imperative handle, and no
218
+ `models` prop. Worth knowing before you design around it:
219
+
220
+ - **Models enter through the UI only** — the file dialog or drag and drop. A host cannot hand a mesh in, and cannot
221
+ remove or transform one. What it *can* do is watch: the `objects` event reports `{id, name, extruder, visible}`
222
+ for every object as the set changes.
223
+ - **Slicing is triggered from the UI or by `defaultAutoSlice`**, which unlike the in-app toggle also performs the
224
+ first slice — that is what makes a panel-less embed able to slice at all. The result arrives on `onSliced`.
225
+ - **`gcode` is one-way**: pass G-code text and it is drawn on the selected plate instead of a slice result, and
226
+ auto re-slice leaves that plate alone while it is set.
227
+ - Everything else the component owns — camera, selection, plate count, paint state — is reported through `onEvent`
228
+ and settable only by the user.
229
+
230
+ If you need programmatic control over the scene, build on the subpath exports (`three-slicer/viewer/toolpath`,
231
+ `/loaders`, `/gcode`) and `three-slicer/client` rather than on `<Viewport/>`.
232
+
74
233
  ## Multithreaded slicing (automatic)
75
234
 
76
235
  The engine ships two WASM kernels: single-threaded (default) and `-pthread` multithreaded (~2× wall-clock on layer-parallel wall generation, measured 12× on the wall pass itself). The worker picks the mt kernel automatically when the page is [cross-origin isolated](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated) — i.e. served with: