three-slicer 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -5,7 +5,7 @@
5
5
  ![The Prepare tab — drag-and-drop viewport with the printer, filament and process cards in the right sidebar](../../slice-sidebar.png)
6
6
 
7
7
  ```bash
8
- npm i three-slicer # react, react-dom and three are peer dependencies — npm installs them for you
8
+ npm i three-slicer react react-dom three # the three are optional peers; the viewer needs all of them
9
9
  ```
10
10
 
11
11
  ```jsx
@@ -14,12 +14,26 @@ import Viewport from 'three-slicer/viewer'
14
14
 
15
15
  function App() {
16
16
  const [settings, setSettings] = useState({}) // OrcaSlicer schema keys; sparse — defaults fill the rest
17
- 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
+ )
18
23
  }
19
24
  ```
20
25
 
21
26
  Styles are injected into the component's Shadow DOM root — nothing to import, and host CSS cannot collide.
22
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
+
23
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.
24
38
 
25
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.
@@ -63,6 +77,55 @@ Every panel can be switched off, and the values the component owns can be seeded
63
77
  second while slicing.
64
78
  - **`onSliced`** — `{plate, stats, gcode}` when a slice is cached. Switching plate tabs does not re-fire it.
65
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
+
66
129
  ## Materials and painting
67
130
 
68
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.
@@ -76,6 +139,97 @@ Two limits to expect while using it:
76
139
 
77
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.
78
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
+
79
233
  ## Multithreaded slicing (automatic)
80
234
 
81
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: