three-slicer 0.2.3 → 0.2.4
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/engine/src/client.js +9 -0
- package/engine/src/slicer.worker.js +45 -0
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.js +0 -0
- package/package.json +1 -1
- package/types/.omc/state/sessions/02e5ddf5-4378-46e1-8fca-e90412c30804/pre-tool-advisory-throttle.json +10 -0
- package/types/worker.d.ts +28 -0
- package/viewer/README.md +20 -0
- package/viewer/dist/Viewport.js +3639 -3274
- package/viewer/styles.css +4 -0
package/engine/src/client.js
CHANGED
|
@@ -21,6 +21,7 @@ export { SLA_CAPABILITIES, SLA_JOB_VERSION, SlaRequestError } from './sla_reques
|
|
|
21
21
|
const REPLY_OF = {
|
|
22
22
|
warmup: 'warm', prepare: 'prepared', paint: 'painted', erase: 'painted', clear: 'painted',
|
|
23
23
|
importPaint: 'painted', exportPaint: 'paintExport', overlay: 'overlay', slice: 'done', sla: 'done',
|
|
24
|
+
fillPreview: 'fillPreview', paintMode: 'paintMode',
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const asBuffer = (data) => (data instanceof ArrayBuffer ? data : data?.buffer ?? data)
|
|
@@ -143,6 +144,14 @@ export function createSlicerClient(worker = new Worker(new URL('./slicer.worker.
|
|
|
143
144
|
overlay: (states) => send({ cmd: 'overlay', ...(states ? { states } : {}) })
|
|
144
145
|
.then(reply => ({ enforcer: reply.enf, blocker: reply.blk, overlays: reply.overlays })),
|
|
145
146
|
|
|
147
|
+
/** What a fill would select under `{tool, facet, hx, hy, hz, angle}`, without applying it. `{clear:true}`
|
|
148
|
+
* drops the standing selection. `supported` is false on a kernel built before the preview existed. */
|
|
149
|
+
fillPreview: (args) => send({ cmd: 'fillPreview', ...args })
|
|
150
|
+
.then(reply => ({ supported: reply.supported, triangles: reply.tris })),
|
|
151
|
+
/** Brush-wide options: `{overhangDeg}` restricts every stroke to overhangs (0 = off), `{clipPlane:[nx,ny,nz,d]}`
|
|
152
|
+
* sets the section plane every cursor clips against (null turns it off). */
|
|
153
|
+
paintMode: (args) => send({ cmd: 'paintMode', ...args }).then(() => undefined),
|
|
154
|
+
|
|
146
155
|
/** Load a 3mf's painting. Replaces every mark, so run it directly after `prepare`. */
|
|
147
156
|
importPaint: (facets, hex, states) => brush('importPaint', { facets, hex, ...(states ? { states } : {}) }),
|
|
148
157
|
exportPaint: () => send({ cmd: 'exportPaint' })
|
|
@@ -95,6 +95,16 @@ const cursorShapeOf = (message) => (message.cursor === 'circle' ? 1 : 0)
|
|
|
95
95
|
// a radius or a camera: what they select comes from the mesh's own topology, which is why they are a branch rather
|
|
96
96
|
// than more optional fields on the brush call. `state === null` means erase (the entry points with no state
|
|
97
97
|
// argument), matching how the brush splits paint from erase.
|
|
98
|
+
// The previous sample of the current stroke, when the message carries one. Upstream paints the CAPSULE between two
|
|
99
|
+
// consecutive mouse positions (DoublePointCursor, GLGizmoPainterBase.cpp:878) — a pointer stream is sampled, not
|
|
100
|
+
// continuous, so a ball per sample leaves a fast drag as a row of blobs with gaps between them. Optional on the
|
|
101
|
+
// wire and feature-detected on the kernel, so a caller or a build without it keeps painting single points.
|
|
102
|
+
const strokeFrom = (message) =>
|
|
103
|
+
Number.isFinite(message.px) && Number.isFinite(message.py) && Number.isFinite(message.pz)
|
|
104
|
+
|
|
105
|
+
// The fill modes the kernel's fill_preview takes (selector_bridge.h FILL_*), by the tool name the protocol uses.
|
|
106
|
+
const FILL_PREVIEW_MODE = { smart: 0, bucket: 1, triangle: 2 }
|
|
107
|
+
|
|
98
108
|
const FILL_TOOLS = new Set(['smart', 'bucket', 'triangle'])
|
|
99
109
|
const applyFill = (Module, message, state) => {
|
|
100
110
|
const angle = Number.isFinite(message.angle) ? message.angle : 30
|
|
@@ -224,6 +234,8 @@ self.onmessage = async (e) => {
|
|
|
224
234
|
applyFill(Module, d, paintedState)
|
|
225
235
|
}
|
|
226
236
|
else if (paintedState === null) Module.selector_paint(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, d.enforcer)
|
|
237
|
+
else if (strokeFrom(d) && Module.selector_paint_stroke)
|
|
238
|
+
Module.selector_paint_stroke(d.facet, d.px, d.py, d.pz, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, paintedState, cursorShapeOf(d))
|
|
227
239
|
else Module.selector_paint_shape(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, paintedState, cursorShapeOf(d))
|
|
228
240
|
self.postMessage(paintedReply(Module, d, paintedState)); return
|
|
229
241
|
}
|
|
@@ -236,6 +248,11 @@ self.onmessage = async (e) => {
|
|
|
236
248
|
// change to the same counts, and the viewer's listeners must not need to know which brush produced it.
|
|
237
249
|
if (d.cmd === 'erase') {
|
|
238
250
|
if (FILL_TOOLS.has(d.tool)) applyFill(Module, d, null)
|
|
251
|
+
else if (strokeFrom(d) && Module.selector_erase_stroke)
|
|
252
|
+
Module.selector_erase_stroke(d.facet, d.px, d.py, d.pz, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, cursorShapeOf(d))
|
|
253
|
+
// The eraser gets the cursor shape too, or a shift+drag with the circle brush rubs out the far side of a wall
|
|
254
|
+
// the paint brush never touched. `selector_erase` (no shape) stays the fallback for an older kernel.
|
|
255
|
+
else if (Module.selector_erase_shape) Module.selector_erase_shape(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius, cursorShapeOf(d))
|
|
239
256
|
else Module.selector_erase(d.facet, d.hx, d.hy, d.hz, d.cx, d.cy, d.cz, d.radius)
|
|
240
257
|
self.postMessage(paintedReply(Module, d, null)); return
|
|
241
258
|
}
|
|
@@ -292,6 +309,34 @@ self.onmessage = async (e) => {
|
|
|
292
309
|
self.postMessage(reply, transfer); return
|
|
293
310
|
}
|
|
294
311
|
|
|
312
|
+
// fillPreview: what a fill WOULD select, without applying it. Upstream runs this on every mouse move while a
|
|
313
|
+
// fill tool is active and draws the result in a lighter shade (GLGizmoPainterBase.cpp:929) — a preview marks
|
|
314
|
+
// nothing, so it needs no state at all and there is no NONE for a stray boolean to become. `{clear:true}`
|
|
315
|
+
// drops the standing selection, which is what leaving the model under the pointer means.
|
|
316
|
+
if (d.cmd === 'fillPreview') {
|
|
317
|
+
if (!Module.selector_fill_preview) { self.postMessage({ type: 'fillPreview', supported: false, tris: EMPTY_OVERLAY }); return }
|
|
318
|
+
if (d.clear) { Module.selector_fill_preview_clear(); self.postMessage({ type: 'fillPreview', supported: true, tris: EMPTY_OVERLAY }); return }
|
|
319
|
+
const angle = Number.isFinite(d.angle) ? d.angle : 30
|
|
320
|
+
const mode = FILL_PREVIEW_MODE[d.tool] ?? FILL_PREVIEW_MODE.smart
|
|
321
|
+
// Upstream's -1 for the non-propagating case, the same value applyFill sends: one facet has no neighbour to
|
|
322
|
+
// measure an angle against.
|
|
323
|
+
const tris = Module.selector_fill_preview(d.facet, d.hx, d.hy, d.hz, mode === FILL_PREVIEW_MODE.triangle ? -1 : angle, mode)
|
|
324
|
+
self.postMessage({ type: 'fillPreview', supported: true, tris },
|
|
325
|
+
tris?.buffer && tris.byteLength > 0 ? [tris.buffer] : [])
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
// paintMode: the brush-wide options upstream keeps on the gizmo — the overhang restriction and the section
|
|
329
|
+
// plane. Both are set once and read by every stroke, so they are their own command rather than fields on one.
|
|
330
|
+
if (d.cmd === 'paintMode') {
|
|
331
|
+
if (Number.isFinite(d.overhangDeg) && Module.selector_set_overhang_limit) Module.selector_set_overhang_limit(d.overhangDeg)
|
|
332
|
+
if (Module.selector_set_clip_plane) {
|
|
333
|
+
const plane = d.clipPlane
|
|
334
|
+
if (plane) Module.selector_set_clip_plane(plane[0], plane[1], plane[2], plane[3], true)
|
|
335
|
+
else if (d.clipPlane === null) Module.selector_set_clip_plane(0, 0, 1, 0, false)
|
|
336
|
+
}
|
|
337
|
+
self.postMessage({ type: 'paintMode', ok: true }); return
|
|
338
|
+
}
|
|
339
|
+
|
|
295
340
|
if (d.stall) return // stage-30 test hook: simulate a hang -> verifies the main-thread watchdog fires (not set in production)
|
|
296
341
|
// mt (SAB): share the address of the real support progress counter (u32) with main once — even while the kernel is
|
|
297
342
|
// blocked inside C++, the UI thread polls the SAB directly to show support progress. st (non-shared buffer) is unsupported -> not sent.
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-slicer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Browser/WASM 3D-printing slicer reverse-engineered from OrcaSlicer: slicing engine (Arachne walls, tree/grid supports, multi-material), SLA resin slicing with PrusaSlicer's ported support/pad chain and .sl1 export, React viewer with GPU toolpath preview, and schema-driven settings UI.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"entries": {
|
|
4
|
+
"79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
|
|
5
|
+
"last_emitted_at_ms": 1788437194779,
|
|
6
|
+
"message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"updated_at": "2026-09-03T12:06:34.779Z"
|
|
10
|
+
}
|
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`.
|
|
@@ -83,6 +108,9 @@ export type SlicerResponse =
|
|
|
83
108
|
/** `supported: false` from a kernel built before the export binding existed — it does not throw. */
|
|
84
109
|
| { type: 'paintExport'; supported: boolean; facets: number[]; hex: string }
|
|
85
110
|
| { type: 'overlay'; enf: Float32Array; blk: Float32Array; overlays?: Record<number, Float32Array> }
|
|
111
|
+
/** `supported` false on a kernel built before the preview existed; `tris` is flat x,y,z, 3 vertices per triangle. */
|
|
112
|
+
| { type: 'fillPreview'; supported: boolean; tris: Float32Array }
|
|
113
|
+
| { type: 'paintMode'; ok: boolean }
|
|
86
114
|
/**
|
|
87
115
|
* Sent once, unprompted, by the multithreaded kernel only: the address of the support-progress counter and of
|
|
88
116
|
* the cancel flag, inside a SharedArrayBuffer. Writing 1 to the cancel flag stops a slice the worker is
|
package/viewer/README.md
CHANGED
|
@@ -180,6 +180,26 @@ arrows and Delete working.
|
|
|
180
180
|
| `Z` · `B` | Zoom to all · zoom to bed |
|
|
181
181
|
| `Esc` | Back to Prepare |
|
|
182
182
|
|
|
183
|
+
While a brush is open these take over the same letters — the object shortcuts come back the moment it closes.
|
|
184
|
+
|
|
185
|
+
| Painting | |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `C` · `S` | Brush shape: circle (the visible surface) · sphere (reaches through a thin wall) |
|
|
188
|
+
| `F` · `B` · `T` | Smart fill · bucket fill · single triangle |
|
|
189
|
+
| `V` · `H` | Lock the stroke to a vertical · horizontal line — press again to release |
|
|
190
|
+
| `1`…`9` | Paint with that filament (material brush only) |
|
|
191
|
+
| `Shift + drag` | Erase instead of paint |
|
|
192
|
+
| Drag off the model | Rotate the camera — a press that misses the model is not the brush's, so orbiting never means closing it |
|
|
193
|
+
| `Ctrl/⌘ + wheel` | Brush radius, or the fill angle for a fill tool — the bare wheel stays the camera zoom |
|
|
194
|
+
| `Alt + wheel` | Scrub the section plane, which is what lets a brush reach a surface inside the model |
|
|
195
|
+
| `Esc` | Close the brush, and put back whatever the section plane cut away |
|
|
196
|
+
|
|
197
|
+
The brush draws itself while it is open — a ring for the circle cursor, a translucent ball for the sphere — and a
|
|
198
|
+
fill tool shades what a click would flood before you click it. A drag paints the **capsule** swept between samples
|
|
199
|
+
rather than one ball per sample, so a fast stroke is continuous instead of a row of blobs. A press that misses the
|
|
200
|
+
model falls through to the camera, so the view can be turned mid-paint without closing the brush. The support brush also
|
|
201
|
+
offers *on overhangs only*, which restricts every stroke to facets steeper than the support threshold.
|
|
202
|
+
|
|
183
203
|
Mouse selection follows upstream's rules: a plain click **replaces** the selection, `Ctrl/⌘ + click` adds or
|
|
184
204
|
removes, a plain click on something already selected **keeps** the set (which is what makes dragging several
|
|
185
205
|
objects work), `Shift + drag` is a box select, and clicking empty space clears. Alt is not a de-select, in
|