three-slicer 0.3.4 → 0.3.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/CHANGELOG.md +19 -0
- package/engine/PARAMS.md +1 -1
- package/engine/src/contour_front_gpu.js +5 -5
- package/engine/src/contour_front_shaders.js +1 -1
- package/engine/src/slicer.worker.js +39 -16
- package/engine/src/slicer_core.js +0 -0
- package/engine/src/slicer_core.mt.wasm +0 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- The real wipe tower (`wipe_tower_real: true`) read past the end of its filament table on a tool change to or from
|
|
8
|
+
T2 or above (#69). The kernel registered filaments for tools 0 and 1 only, so the third tool's wipe moves took
|
|
9
|
+
their feedrate from whatever memory followed: `F0` or a value that changed from one slice to the next, and the
|
|
10
|
+
toolchange comment named no material. Every tool the change involves is now registered. On a three-extruder
|
|
11
|
+
slice the wipe moves went from 114 `F0` lines to none, and three runs give the same G-code. Each block now has the
|
|
12
|
+
shape the two-extruder tower always had. The default tower (the ring) and two-extruder slices are unchanged.
|
|
13
|
+
`wipe_tower_real` still defaults to `false`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- The kernel's own C++ and its tests were rewritten for readability: abbreviated names written out, one statement
|
|
18
|
+
per line, no ternaries, and one shared module for the test meshes. The kernel's output is unchanged, checked with
|
|
19
|
+
the golden G-code dump and a per-call hash of every kernel result in the test suite. Parameter keys, `stats`
|
|
20
|
+
fields and the worker protocol are unchanged.
|
|
21
|
+
|
|
3
22
|
## 0.3.4 — 2026-10-11
|
|
4
23
|
|
|
5
24
|
### Added
|
package/engine/PARAMS.md
CHANGED
|
@@ -230,7 +230,7 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
|
|
|
230
230
|
| `extruder_retract_speed` | `?` | `[]` | `filament_retraction_speed` | undefined |
|
|
231
231
|
| `extruder_z_hop` | `?` | `[]` | `filament_z_hop` | undefined |
|
|
232
232
|
| `extrusion_rate_slope_segment_length` | `number` | `1.0` | — | segment split length for the real PE (mm) |
|
|
233
|
-
| `filament_max_volumetric_speed` | `number[]` | `[]` | `filament_max_volumetric_speed` | mm³/s, per filament; empty = no cap (
|
|
233
|
+
| `filament_max_volumetric_speed` | `number[]` | `[]` | `filament_max_volumetric_speed` | mm³/s, per filament; empty = no cap (GcodeWriter::capped_feed) |
|
|
234
234
|
| `filter_out_gap_fill` | `number` | `0.0` | `filter_out_gap_fill` | filter_out_gap_fill (mm): gap fill shorter than this is dropped |
|
|
235
235
|
| `first_layer_bed_temp` | `number` | `-1.0` | `before_layer_change_gcode`, `change_filament_gcode`, `filament_end_gcode`, `filament_start_gcode`, `file_start_gcode`, `hot_plate_temp_initial_layer`, `layer_change_gcode`, `machine_end_gcode`, `machine_start_gcode`, `time_lapse_gcode` | the plate type's _initial_layer bed option (<0 = none sent) |
|
|
236
236
|
| `first_layer_flow_ratio` | `?` | — | `first_layer_flow_ratio` | undefined |
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// between the cut and the union.
|
|
4
4
|
//
|
|
5
5
|
// What runs here (WGSL: contour_front_shaders.js), every layer at once:
|
|
6
|
-
// cut per triangle, the layers it spans (exact z compares), each crossing in double-float, oriented as
|
|
6
|
+
// cut per triangle, the layers it spans (exact z compares), each crossing in double-float, oriented as intersect_triangle_with_plane
|
|
7
7
|
// cancel segments coinciding exactly in opposite directions removed in pairs (cancel_coincident), by a hash on place
|
|
8
|
-
// chain each end continues into the smallest unused segment starting at its 1e-3 mm key (
|
|
8
|
+
// chain each end continues into the smallest unused segment starting at its 1e-3 mm key (chain_polygons' greedy rule)
|
|
9
9
|
// loops pointer jumping for each loop's head and each segment's rank; tails that lead into a cycle stay out
|
|
10
|
-
// layout loops sorted by (layer, head), laid out as
|
|
10
|
+
// layout loops sorted by (layer, head), laid out as chain_polygons' n + 1 points, write_polygon's duplicates dropped
|
|
11
11
|
// tables layer origins and grids, polygon ranges, the cell-list size
|
|
12
12
|
//
|
|
13
13
|
// Measured (design report gpu-full-port-trial-2026-10-01.md section 3): 3.0M triangles, 420 layers in 30 ms (the kernel's
|
|
@@ -205,10 +205,10 @@ export async function makeContourFront(device) {
|
|
|
205
205
|
clear(loopStart, (headCount + 1) * 4)
|
|
206
206
|
run(LOOPS_SHADER, 'lengths', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), headCount)
|
|
207
207
|
scan(loopStart, headCount + 1)
|
|
208
|
-
const [slotCount] = await readWords(loopStart, 1, headCount) // points of the closed loops,
|
|
208
|
+
const [slotCount] = await readWords(loopStart, 1, headCount) // points of the closed loops, chain_polygons' n + 1 each
|
|
209
209
|
run(LOOPS_SHADER, 'place', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), segmentCount)
|
|
210
210
|
|
|
211
|
-
// the points write_polygon keeps, the polygons
|
|
211
|
+
// the points write_polygon keeps, the polygons chain_polygons keeps, the layer boxes
|
|
212
212
|
const kept = buffer('kept', (slotCount + 1) * 4), polygonKept = buffer('polygonKept', (headCount + 1) * 4)
|
|
213
213
|
const layerBox = buffer('layerBox', layerCount * 32), slotPolygon = buffer('slotPolygon', slotCount * 4)
|
|
214
214
|
const boxStart = new Int32Array(layerCount * 8)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// WGSL of the GPU front of PASS1 (contour_front_gpu.js):
|
|
1
|
+
// WGSL of the GPU front of PASS1 (contour_front_gpu.js): intersect_triangle_with_plane -> chain_polygons -> cancel_coincident ->
|
|
2
2
|
// build_gpu_input, every layer at once. Ported from the measured experiment (.omc/plans/gpu-work/fullport/gpu_slice.mjs,
|
|
3
3
|
// design report gpu-full-port-trial-2026-10-01.md section 3); edited only with a measurement beside it, as
|
|
4
4
|
// contour_gpu_shaders.js.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Stage 30 (OOM tolerance): set_layer_sink lets the kernel emit layers as it produces them; each is transferred to main
|
|
7
7
|
// immediately (Float32Array buffers moved -> the worker copy is released at once) and the kernel frees that layer buffer
|
|
8
|
-
// from the heap. The resident result (the full
|
|
8
|
+
// from the heap. The resident result (the full GcodeWriter::gcode + the full layersArray) disappears, so the WASM heap peak drops sharply
|
|
9
9
|
// -> large models avoid OOM. The final 'done' carries stats only.
|
|
10
10
|
// In economy mode (params.economy) only G-code chunks are emitted, without toolpaths (finishes without a preview).
|
|
11
11
|
// The MM / real-PE paths fall back to batch mode.
|
|
@@ -27,7 +27,10 @@ import { FILL_TOOLS } from 'three-slicer-viewer/paint'
|
|
|
27
27
|
// and createSlicerClient both take an object and stringify it for you. One object, two shapes, and the difference
|
|
28
28
|
// showed up only at the boundary. The worker now accepts either; the SLA branch always did, which is what made
|
|
29
29
|
// the split arbitrary rather than principled.
|
|
30
|
-
const paramsText = (params) =>
|
|
30
|
+
const paramsText = (params) => {
|
|
31
|
+
if (typeof params === 'string') return params
|
|
32
|
+
return JSON.stringify(params ?? {})
|
|
33
|
+
}
|
|
31
34
|
|
|
32
35
|
let quiet = false
|
|
33
36
|
const say = (level, ...args) => { if (!quiet) console[level](...args) }
|
|
@@ -120,7 +123,8 @@ const validatedPaintStates = (states) => {
|
|
|
120
123
|
// flow (viewer use_slicer.js) reads those two fields and nothing else.
|
|
121
124
|
const reportedPaintStates = (message, paintedState) => {
|
|
122
125
|
if (Array.isArray(message.states)) return validatedPaintStates(message.states)
|
|
123
|
-
|
|
126
|
+
if (paintedState === null) return null
|
|
127
|
+
return [paintedState]
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
// The state a paint message asks for: an explicit `state` wins, otherwise null meaning "use the legacy boolean path".
|
|
@@ -133,7 +137,10 @@ const explicitPaintState = (message) => {
|
|
|
133
137
|
|
|
134
138
|
// Brush cursor shape (selector_bridge.h CURSOR_*). Sent as a name because the message is a protocol a host writes
|
|
135
139
|
// by hand; the integer stays on this side of it. Anything unknown is the sphere the brush has always used.
|
|
136
|
-
const cursorShapeOf = (message) =>
|
|
140
|
+
const cursorShapeOf = (message) => {
|
|
141
|
+
if (message.cursor === 'circle') return 1
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
137
144
|
|
|
138
145
|
// The tools that are not the radius brush: upstream's Smart fill and Bucket fill, plus its single-triangle POINTER
|
|
139
146
|
// cursor — which is literally bucket fill with propagation off, so the three share one dispatch. None of them takes
|
|
@@ -150,8 +157,15 @@ const strokeFrom = (message) =>
|
|
|
150
157
|
// The fill modes the kernel's fill_preview takes (selector_bridge.h FILL_*), by the tool name the protocol uses.
|
|
151
158
|
const FILL_PREVIEW_MODE = { smart: 0, bucket: 1, triangle: 2 }
|
|
152
159
|
|
|
160
|
+
// A fill's angle limit in degrees, 30 when the message does not carry one. applyFill and the fill preview both
|
|
161
|
+
// read it here, so the preview selects what the fill will.
|
|
162
|
+
const fillAngleOf = (message) => {
|
|
163
|
+
if (Number.isFinite(message.angle)) return message.angle
|
|
164
|
+
return 30
|
|
165
|
+
}
|
|
166
|
+
|
|
153
167
|
const applyFill = (Module, message, state) => {
|
|
154
|
-
const angle =
|
|
168
|
+
const angle = fillAngleOf(message)
|
|
155
169
|
if (message.tool === 'smart') {
|
|
156
170
|
if (state === null) Module.selector_seed_fill_erase(message.facet, message.hx, message.hy, message.hz, angle)
|
|
157
171
|
else Module.selector_seed_fill(message.facet, message.hx, message.hy, message.hz, angle, state)
|
|
@@ -160,7 +174,8 @@ const applyFill = (Module, message, state) => {
|
|
|
160
174
|
// Upstream passes -1 as the angle when it is not propagating (GLGizmoPainterBase.cpp ~861): with a single facet
|
|
161
175
|
// there is no neighbour to measure an angle against, and -1 is how that call says "no angle limit".
|
|
162
176
|
const propagate = message.tool === 'bucket'
|
|
163
|
-
|
|
177
|
+
let bucketAngle = -1
|
|
178
|
+
if (propagate) bucketAngle = angle
|
|
164
179
|
if (state === null) Module.selector_bucket_fill_erase(message.facet, message.hx, message.hy, message.hz, bucketAngle, propagate)
|
|
165
180
|
else Module.selector_bucket_fill(message.facet, message.hx, message.hy, message.hz, bucketAngle, propagate, state)
|
|
166
181
|
}
|
|
@@ -276,7 +291,9 @@ const handleMessage = async (e) => {
|
|
|
276
291
|
// `keepPaint` says the mesh is the same model in a new place, so the marks carry over — the reply reports whether
|
|
277
292
|
// they actually did, since a face-count change makes the kernel fall back to a clean registration.
|
|
278
293
|
if (d.cmd === 'prepare') {
|
|
279
|
-
|
|
294
|
+
let kept = false
|
|
295
|
+
if (d.keepPaint) kept = Module.selector_reprepare(new Uint8Array(d.stl))
|
|
296
|
+
else Module.selector_prepare(new Uint8Array(d.stl))
|
|
280
297
|
reply({ type: 'prepared', facets: Module.selector_facet_count(), kept })
|
|
281
298
|
return
|
|
282
299
|
}
|
|
@@ -321,7 +338,8 @@ const handleMessage = async (e) => {
|
|
|
321
338
|
// A kernel built before this binding existed simply has no selector_import_paint; the reply says applied:0
|
|
322
339
|
// rather than throwing, because losing a 3mf's paint must not also lose its geometry.
|
|
323
340
|
if (d.cmd === 'importPaint') {
|
|
324
|
-
|
|
341
|
+
let applied = 0
|
|
342
|
+
if (Module.selector_import_paint) applied = Module.selector_import_paint(d.facets, d.hex ?? '')
|
|
325
343
|
const importedReply = paintedReply(Module, d, null)
|
|
326
344
|
importedReply.applied = applied
|
|
327
345
|
reply(importedReply); return
|
|
@@ -332,7 +350,8 @@ const handleMessage = async (e) => {
|
|
|
332
350
|
// which objects were merged in what order is the viewer's knowledge. A kernel built before the binding existed
|
|
333
351
|
// reports supported:false rather than throwing — the save then falls back to the paint it imported.
|
|
334
352
|
if (d.cmd === 'exportPaint') {
|
|
335
|
-
|
|
353
|
+
let exported = null
|
|
354
|
+
if (Module.selector_export_paint) exported = Module.selector_export_paint()
|
|
336
355
|
reply({ type: 'paintExport', supported: !!Module.selector_export_paint,
|
|
337
356
|
facets: exported?.facets ?? [], hex: exported?.hex ?? '' })
|
|
338
357
|
return
|
|
@@ -358,9 +377,11 @@ const handleMessage = async (e) => {
|
|
|
358
377
|
// enf/blk stay unconditional for the listener that predates the per-state map. When the request names states,
|
|
359
378
|
// they are the ONLY ones read, so building the other two would be pure waste on the hot path.
|
|
360
379
|
const wanted = states && !states.includes(PAINT_STATE_ENFORCER) && !states.includes(PAINT_STATE_BLOCKER)
|
|
361
|
-
const overlayReply = { type: 'overlay',
|
|
362
|
-
|
|
363
|
-
|
|
380
|
+
const overlayReply = { type: 'overlay', enf: EMPTY_OVERLAY, blk: EMPTY_OVERLAY }
|
|
381
|
+
if (!wanted) {
|
|
382
|
+
overlayReply.enf = Module.selector_overlay(true)
|
|
383
|
+
overlayReply.blk = Module.selector_overlay(false)
|
|
384
|
+
}
|
|
364
385
|
if (states) overlayReply.overlays = Object.fromEntries(states.map(state => [state, Module.selector_overlay_state(state)]))
|
|
365
386
|
const transfer = [overlayReply.enf, overlayReply.blk, ...Object.values(overlayReply.overlays ?? {})]
|
|
366
387
|
.filter(a => a?.buffer && a.byteLength > 0).map(a => a.buffer)
|
|
@@ -374,13 +395,15 @@ const handleMessage = async (e) => {
|
|
|
374
395
|
if (d.cmd === 'fillPreview') {
|
|
375
396
|
if (!Module.selector_fill_preview) { reply({ type: 'fillPreview', supported: false, tris: EMPTY_OVERLAY }); return }
|
|
376
397
|
if (d.clear) { Module.selector_fill_preview_clear(); reply({ type: 'fillPreview', supported: true, tris: EMPTY_OVERLAY }); return }
|
|
377
|
-
const angle = Number.isFinite(d.angle) ? d.angle : 30
|
|
378
398
|
const mode = FILL_PREVIEW_MODE[d.tool] ?? FILL_PREVIEW_MODE.smart
|
|
379
399
|
// Upstream's -1 for the non-propagating case, the same value applyFill sends: one facet has no neighbour to
|
|
380
400
|
// measure an angle against.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
401
|
+
let previewAngle = fillAngleOf(d)
|
|
402
|
+
if (mode === FILL_PREVIEW_MODE.triangle) previewAngle = -1
|
|
403
|
+
const previewTriangles = Module.selector_fill_preview(d.facet, d.hx, d.hy, d.hz, previewAngle, mode)
|
|
404
|
+
const transfer = []
|
|
405
|
+
if (previewTriangles?.buffer && previewTriangles.byteLength > 0) transfer.push(previewTriangles.buffer)
|
|
406
|
+
reply({ type: 'fillPreview', supported: true, tris: previewTriangles }, transfer)
|
|
384
407
|
return
|
|
385
408
|
}
|
|
386
409
|
// paintMode: the brush-wide options upstream keeps on the gizmo — the overhang restriction and the section
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-slicer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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",
|
|
@@ -134,6 +134,6 @@
|
|
|
134
134
|
"clipping"
|
|
135
135
|
],
|
|
136
136
|
"dependencies": {
|
|
137
|
-
"three-slicer-viewer": "0.3.
|
|
137
|
+
"three-slicer-viewer": "0.3.5"
|
|
138
138
|
}
|
|
139
139
|
}
|