three-slicer 0.2.3 → 0.2.5

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/viewer.d.ts CHANGED
@@ -3,12 +3,42 @@ import type * as React from 'react'
3
3
  import type { SlicerSettings } from './settings-keys.d.ts'
4
4
 
5
5
  export interface ViewportProps {
6
- /** Sparse settings map. Defaults to `{}`. */
6
+ /**
7
+ * Sparse settings map. Defaults to `{}`. Two viewer knobs ride in it beside the schema keys: `sla_antialias`
8
+ * (SL1 mask MSAA, 1|2|4) and `slice_workers` — how many plates an all-plates run slices at once (0/absent =
9
+ * Auto: half the cores on the threaded kernel, one per core on the single-threaded one; the in-app select
10
+ * writes it). Neither reaches a 3mf.
11
+ */
7
12
  settings?: SlicerSettings
8
13
  /** React setState shape. Defaults to a no-op. */
9
14
  setSettings?: React.Dispatch<React.SetStateAction<SlicerSettings>>
10
- /** Left process-panel slot — usually `<SettingsPanel embedded/>`. Defaults to null. */
15
+ /**
16
+ * Per-plate overrides: `{[plateIndex]: sparse map}`, merged over `settings` for that plate's slice only —
17
+ * a key's absence means "follow the global value" (delete the key to clear an override). Plate identity is
18
+ * the plate index; deleting the last plate truncates its entry. Editing one plate's override invalidates only
19
+ * that plate's cached result. Any key can be overridden: `printer_technology` routes that plate to the
20
+ * FFF or SLA slicer (an SLA plate's grid cell is sized to its resin display), and a bed override
21
+ * (`printable_area`/`printable_height`) resizes that plate's own grid cell. What a 3mf cannot represent —
22
+ * a mixed-technology or mixed-bed project — is refused at project export with a typed error
23
+ * (`error.code` `'UNSUPPORTED_MIXED_TECH_3MF'` / `'UNSUPPORTED_MIXED_BED_3MF'`); per-plate G-code/.sl1
24
+ * exports always work. Defaults to `{}`.
25
+ */
26
+ plateSettings?: Record<number, SlicerSettings>
27
+ /** React setState shape for {@link ViewportProps.plateSettings}. Defaults to a no-op. */
28
+ setPlateSettings?: React.Dispatch<React.SetStateAction<Record<number, SlicerSettings>>>
29
+ /**
30
+ * Left process-panel slot — usually `<SettingsPanel embedded/>`. Defaults to null.
31
+ *
32
+ * Pass a FUNCTION to opt into the per-plate scope toggle (shown with more than one plate): in plate scope
33
+ * the panel receives the selected plate's EFFECTIVE map and a setter that writes only the changed keys into
34
+ * {@link ViewportProps.plateSettings}; `meta.overriddenKeys`/`meta.onRevertKey` drive the override badges
35
+ * (`<SettingsPanel overriddenKeys={...} onRevertKey={...}/>`). In global scope the pair is simply
36
+ * `settings`/`setSettings`. A plain node keeps the global-only binding.
37
+ */
11
38
  processPanel?: React.ReactNode
39
+ | ((settings: SlicerSettings, setSettings: React.Dispatch<React.SetStateAction<SlicerSettings>>,
40
+ meta: { scope: 'global' | 'plate', selectedPlate: number, overriddenKeys: string[],
41
+ onRevertKey: ((key: string) => void) | null }) => React.ReactNode)
12
42
  /** Motion-limits editor, folded into the printer card — usually `<SettingsPanel only={{builder:'TabPrinter::build_kinematics_page'}}/>`. */
13
43
  motionPanel?: React.ReactNode
14
44
  /**
@@ -99,6 +129,15 @@ export interface ViewportProps {
99
129
  plate: number; stats: Record<string, unknown>; gcode: string
100
130
  throughput?: { ms: number; kernelMs: number | null; layersPerSecond: number; msPerMsegment: number | null }
101
131
  }) => void
132
+ /**
133
+ * An all-plates run as it moves: the run map on every change, `null` when none is on. `workers.pool` is how
134
+ * many slice at once, `workers.kernel` which kernel loaded ('mt' threads / 'st'); each plate carries its state
135
+ * and its own progress (0..1) and layers/second. Fired several times a second while a run is on.
136
+ */
137
+ onSliceRun?: (run: {
138
+ workers: { pool: number; active: number; kernel: 'mt' | 'st' | null }
139
+ plates: Record<number, { state: 'queued' | 'busy' | 'done' | 'failed'; progress: number; rate: number }>
140
+ } | null) => void
102
141
  }
103
142
 
104
143
  /**
package/types/worker.d.ts CHANGED
@@ -13,6 +13,14 @@ export interface BrushArgs {
13
13
  hx: number; hy: number; hz: number
14
14
  cx: number; cy: number; cz: number
15
15
  radius: number
16
+ /**
17
+ * The PREVIOUS sample of this stroke. Present, the kernel paints the capsule swept from there to `(hx,hy,hz)`
18
+ * instead of a ball at the hit — which is what upstream does between two consecutive mouse positions
19
+ * (`DoublePointCursor`, GLGizmoPainterBase.cpp:878). A pointer stream is sampled, not continuous, so without it
20
+ * a fast drag lands as a row of separate blobs. Omit on the first sample of a stroke; a kernel built before the
21
+ * capsule existed ignores it and paints the ball, so it is safe to always send.
22
+ */
23
+ px?: number; py?: number; pz?: number
16
24
  }
17
25
 
18
26
  /**
@@ -56,6 +64,23 @@ export type SlicerRequest =
56
64
  | { cmd: 'clear'; states?: PaintState[] }
57
65
  /** The painted overlay triangles. `enf`/`blk` always; `states` adds those states. Reply: `overlay`. */
58
66
  | { cmd: 'overlay'; states?: PaintState[] }
67
+
68
+ /**
69
+ * What a fill tool WOULD select under the cursor, without marking anything — upstream draws this on every mouse
70
+ * move while a fill is active (GLGizmoPainterBase.cpp:929) so a click is aimed rather than tried. No state
71
+ * argument at all: a preview marks nothing, so there is no NONE for a stray boolean to become.
72
+ * `{clear:true}` drops the standing selection. Reply: `fillPreview`.
73
+ */
74
+ | { cmd: 'fillPreview'; facet?: number; hx?: number; hy?: number; hz?: number
75
+ tool?: PaintTool; angle?: number; clear?: boolean }
76
+
77
+ /**
78
+ * Brush-wide options, set once and read by every stroke and fill — upstream keeps both on the gizmo.
79
+ * `overhangDeg` restricts painting to facets overhanging by more than that angle (`0` = off, the default);
80
+ * `clipPlane` is the section plane `[nx, ny, nz, offset]` in KERNEL coords that every cursor clips against
81
+ * (`null` turns it off), which is the only way a brush reaches an interior surface. Reply: `paintMode`.
82
+ */
83
+ | { cmd: 'paintMode'; overhangDeg?: number; clipPlane?: [number, number, number, number] | null }
59
84
  /**
60
85
  * Legacy single-object SLA (resin) slice. `params` accepts a JSON string or an object (this path parses either). Same reply stream as
61
86
  * a slice: `progress` and `layer` while it runs, then `done` (stats only; layers arrived as `layer`) — or `error`.
@@ -73,7 +98,8 @@ export type SlicerRequest =
73
98
  | { stl: ArrayBuffer; params: string | object }
74
99
 
75
100
  export type SlicerResponse =
76
- | { type: 'warm' }
101
+ /** `kernel` says which variant loaded: 'mt' under crossOriginIsolated, 'st' otherwise (or after an mt load failure). */
102
+ | { type: 'warm'; kernel: 'mt' | 'st' | null }
77
103
  | { type: 'prepared'; facets: number; kept: boolean }
78
104
  /**
79
105
  * `enf`/`blk` are the counts for states 1 and 2 and are always present, so a listener predating the
@@ -83,6 +109,9 @@ export type SlicerResponse =
83
109
  /** `supported: false` from a kernel built before the export binding existed — it does not throw. */
84
110
  | { type: 'paintExport'; supported: boolean; facets: number[]; hex: string }
85
111
  | { type: 'overlay'; enf: Float32Array; blk: Float32Array; overlays?: Record<number, Float32Array> }
112
+ /** `supported` false on a kernel built before the preview existed; `tris` is flat x,y,z, 3 vertices per triangle. */
113
+ | { type: 'fillPreview'; supported: boolean; tris: Float32Array }
114
+ | { type: 'paintMode'; ok: boolean }
86
115
  /**
87
116
  * Sent once, unprompted, by the multithreaded kernel only: the address of the support-progress counter and of
88
117
  * the cancel flag, inside a SharedArrayBuffer. Writing 1 to the cancel flag stops a slice the worker is
package/viewer/README.md CHANGED
@@ -130,6 +130,8 @@ Every panel can be switched off, and the values the component owns can be seeded
130
130
  - **`onSliced`** — `{plate, stats, gcode, throughput}` when a slice is cached. Switching plate tabs does not
131
131
  re-fire it. `throughput` sits beside `stats` rather than inside it, because that is where the engine puts it:
132
132
  it is measured around the call, not reported by the kernel.
133
+ - **`onSliceRun`** — an all-plates run as it moves: `{workers: {pool, active, kernel}, plates: {[i]: {state,
134
+ progress, rate}}}` on every change, `null` between runs. For a host drawing its own per-plate progress.
133
135
 
134
136
  `sliceRate` is live throughput — layers finished per second over a 250ms window, `0` between slices, and shown in
135
137
  the slice bar as well. When it appears depends on the technology, because the two publish per-layer news at
@@ -180,6 +182,26 @@ arrows and Delete working.
180
182
  | `Z` · `B` | Zoom to all · zoom to bed |
181
183
  | `Esc` | Back to Prepare |
182
184
 
185
+ While a brush is open these take over the same letters — the object shortcuts come back the moment it closes.
186
+
187
+ | Painting | |
188
+ | --- | --- |
189
+ | `C` · `S` | Brush shape: circle (the visible surface) · sphere (reaches through a thin wall) |
190
+ | `F` · `B` · `T` | Smart fill · bucket fill · single triangle |
191
+ | `V` · `H` | Lock the stroke to a vertical · horizontal line — press again to release |
192
+ | `1`…`9` | Paint with that filament (material brush only) |
193
+ | `Shift + drag` | Erase instead of paint |
194
+ | Drag off the model | Rotate the camera — a press that misses the model is not the brush's, so orbiting never means closing it |
195
+ | `Ctrl/⌘ + wheel` | Brush radius, or the fill angle for a fill tool — the bare wheel stays the camera zoom |
196
+ | `Alt + wheel` | Scrub the section plane, which is what lets a brush reach a surface inside the model |
197
+ | `Esc` | Close the brush, and put back whatever the section plane cut away |
198
+
199
+ The brush draws itself while it is open — a ring for the circle cursor, a translucent ball for the sphere — and a
200
+ fill tool shades what a click would flood before you click it. A drag paints the **capsule** swept between samples
201
+ rather than one ball per sample, so a fast stroke is continuous instead of a row of blobs. A press that misses the
202
+ model falls through to the camera, so the view can be turned mid-paint without closing the brush. The support brush also
203
+ offers *on overhangs only*, which restricts every stroke to facets steeper than the support threshold.
204
+
183
205
  Mouse selection follows upstream's rules: a plain click **replaces** the selection, `Ctrl/⌘ + click` adds or
184
206
  removes, a plain click on something already selected **keeps** the set (which is what makes dragging several
185
207
  objects work), `Shift + drag` is a box select, and clicking empty space clears. Alt is not a de-select, in
@@ -321,7 +343,53 @@ Cross-Origin-Opener-Policy: same-origin
321
343
  Cross-Origin-Embedder-Policy: require-corp
322
344
  ```
323
345
 
324
- No headers → falls back to the single-threaded kernel. Nothing else to change.
346
+ No headers → falls back to the single-threaded kernel. Nothing else to change. Which one loaded is shown as a
347
+ badge (`mt`/`st`) beside the Workers select in the slice menu, and the `warmup()` client call resolves with it —
348
+ the two are a measured 9.8x apart on a 3M-facet model, which is too much to leave to the console.
349
+
350
+ ### Slicing several plates at once
351
+
352
+ **All plates** in the slice menu drains the plates through a pool of workers instead of one after another. The
353
+ **Workers** select beside it sets the pool: *Auto* is half the cores on the threaded kernel and one per core on the
354
+ single-threaded one, capped at the plate count and by model size (below); the select offers every value up to
355
+ that memory cap and nothing past it, because past it the tab itself dies (measured: five workers on a 143MB
356
+ model), which nothing in the page can catch. The plate tabs show each plate's state (queued, slicing with its own percentage, done, failed)
357
+ independently of which one is selected, and the slice bar lists them on one row. Cancel stops every worker. A worker that dies mid-run (memory, most
358
+ often) is dropped and its plate is re-run alone once the others finish, so a count that was too high for the
359
+ model costs time rather than results; the notice says which plates that happened to.
360
+
361
+ The numbers behind Auto (node harness, 3M-facet model, 15 cores, 30 plates, wall time against one worker):
362
+
363
+ | kernel | preset | 3 workers | 5 | 10 | 15 |
364
+ |---|---|---|---|---|---|
365
+ | mt | classic walls, no support | 1.10x | 1.15x | 1.15x | 1.14x |
366
+ | mt | arachne + tree support | 2.08x | 2.60x | 3.16x | 3.31x |
367
+ | st | classic walls, no support | — | — | — | 7.2x (14 workers) |
368
+
369
+ The gain is the kernel's serial share: with classic walls one worker already keeps ~77% of the cores busy, with
370
+ arachne (whose emission pass runs serial) and tree support far less, and the extra workers fill that. More workers
371
+ than cores gained nothing (17 = 15), and no count ever made a run slower — what a worker costs is memory: each
372
+ holds its own copy of the model and its own wasm heap, measured 2.5GB for one, 8.8GB for five and 11.6GB for
373
+ fifteen on that model. That is why Auto stops at half the cores and why the select goes higher only by hand.
374
+ Three things were measured NOT to matter and are deliberately absent: giving each worker a thread budget, slicing
375
+ the largest plate first, and whether the count divides the plate count. The value is `slice_workers` in the
376
+ settings map (0 = Auto), a viewer knob like `sla_antialias`, and it does not reach a 3mf.
377
+
378
+ The browser is tighter than that harness, because every worker's wasm heap lives in the one renderer process that
379
+ already holds each plate's STL buffer and three.js geometry. Measured in Chromium on the same 143MB-STL model
380
+ over nine plates (classic preset, 15 cores):
381
+
382
+ | workers | wall | Chromium RSS peak |
383
+ |---|---|---|
384
+ | 1 | 17.8s | 10.4GB |
385
+ | 2 | 15.7s (1.13x) | 13.7GB |
386
+ | 3 | crashed the tab once; 18.7s once | 10.2GB |
387
+ | 8 (Auto by cores) | crashed the tab | 11.6GB |
388
+
389
+ So Auto is also capped by model size — a worker's heap is taken as 12x the STL it slices (node measured ~1.7GB
390
+ for 143MB) against a 4GB pool budget, which gives 2 for that model and does not bind below ~40MB per plate. A
391
+ manual count is capped the same way. What the browser has NOT measured yet is
392
+ the arachne + tree-support preset, where the harness gains were largest.
325
393
 
326
394
  ## Bundler notes
327
395