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/engine/README.md CHANGED
@@ -37,7 +37,7 @@ Off-main-thread (browser): `new Worker(engineWorkerURL(), { type: 'module' })`
37
37
 
38
38
  Two levels — use whichever fits:
39
39
 
40
- 1. **Kernel params (this package's native contract)** — a flat JSON object of ~90 keys (`layer_height`, `wall_loops`, `infill_density` 0–1, `sparse_infill_pattern`, `enable_support`, `wall_generator: 'classic' | 'arachne'`, …). See `src/settings.js` for the full shape.
40
+ 1. **Kernel params (this package's native contract)** — a flat JSON object (`layer_height`, `wall_loops`, `infill_density` 0–1, `sparse_infill_pattern`, `enable_support`, `wall_generator: 'classic' | 'arachne'`, …). Every accepted key is listed under [Kernel parameter reference](#kernel-parameter-reference) below.
41
41
  2. **OrcaSlicer settings map** — a sparse `{schemaKey: value}` map using original OrcaSlicer option keys (923 defined in `three-slicer/data`); unset keys fall back to schema defaults:
42
42
 
43
43
  ```js
@@ -45,7 +45,253 @@ import { deriveKernelParams } from 'three-slicer/settings'
45
45
  slicer.slice(stl, deriveKernelParams({ layer_height: 0.25, sparse_infill_density: 15 }))
46
46
  ```
47
47
 
48
- **Known limit:** `deriveKernelParams` maps a curated whitelist of 92 schema keys to kernel params. Editing other schema keys has no slicing effect (they exist for UI/metadata). Scalar options use their first element only; the filament options listed under *Materials* below are the exception — those keep every extruder's entry.
48
+ **Known limit:** `deriveKernelParams` maps a curated subset of schema keys to kernel params. Editing other schema keys has no slicing effect (they exist for UI/metadata). Scalar options use their first element only; the filament options listed under *Materials* below are the exception — those keep every extruder's entry. Kernel parameters with no schema key behind them are reachable only by writing the params object yourself — the reference table marks which those are.
49
+
50
+ ## Kernel parameter reference
51
+
52
+ Generated from the kernel's own reader (`params.cpp`) — it is the definition of the accepted key set, so this table
53
+ cannot drift from what `slice()` actually honours. Regenerate with `node types/gen_kernel_params.mjs`.
54
+
55
+ <!-- KERNEL-PARAMS:BEGIN — generated by types/gen_kernel_params.mjs, do not edit by hand -->
56
+
57
+ The kernel accepts **159 parameters**. `deriveKernelParams()` produces 131 of them from the
58
+ OrcaSlicer settings map; the remaining 28 have no schema key behind them and can only be set by writing
59
+ the params object directly — those rows show `—` in the *From setting* column.
60
+
61
+ A parameter left out of the object keeps the default listed here, so sending a partial object is normal and is
62
+ what `deriveKernelParams()` itself does.
63
+
64
+ *From setting* names the schema key the value is read from, which is often **not** the same name and not the
65
+ same unit — `first_layer_height` comes from `initial_layer_print_height`, and `infill_density` is
66
+ `sparse_infill_density` divided by 100 (a settings map holding `20` produces `0.2` here).
67
+
68
+ ### Geometry and walls
69
+
70
+ | Kernel param | Type | Default | From setting | Notes |
71
+ | --- | --- | --- | --- | --- |
72
+ | `auto_center` | `boolean` | `false` | — | stage 28: true = realign the combined bbox to the origin (stage-3 legacy). false (default) = trust the viewer coordinates (no realignment, only Z seating) -> the toolpath overlaps the on-screen model exactly. Upstream = only the plate origin offset (GCode.cpp:932). |
73
+ | `first_layer_height` | `number` | `0.2` | `initial_layer_print_height` | |
74
+ | `gcode_resolution` | `number` | `0.01` | `resolution` | resolution (mm): path simplification tolerance. Upstream PrintConfig default 0.01 |
75
+ | `initial_layer_line_width` | `number` | `0` | `initial_layer_line_width` | |
76
+ | `inner_wall_line_width` | `number` | `0` | `inner_wall_line_width` | |
77
+ | `internal_solid_infill_line_width` | `number` | `0` | `internal_solid_infill_line_width` | |
78
+ | `layer_height` | `number` | `0.2` | `layer_height` | |
79
+ | `line_width` | `number` | `0.42` | `line_width` | |
80
+ | `outer_wall_line_width` | `number` | `0` | `outer_wall_line_width` | |
81
+ | `sparse_infill_line_width` | `number` | `0` | `sparse_infill_line_width` | |
82
+ | `spiral_mode` | `boolean` | `false` | `spiral_mode` | spiral_mode (vase) |
83
+ | `support_line_width` | `number` | `0.0` | `support_line_width` | support_line_width (0=auto; the real tree support extrusion width) |
84
+ | `top_surface_line_width` | `number` | `0` | `top_surface_line_width` | |
85
+ | `wall_generator` | `string` | `'classic'` | `wall_generator` | classic\|arachne (arachne = the real ported WallToolPaths) |
86
+ | `wall_loops` | `number` | `2` | `wall_loops` | |
87
+
88
+ ### Infill
89
+
90
+ | Kernel param | Type | Default | From setting | Notes |
91
+ | --- | --- | --- | --- | --- |
92
+ | `bottom_shell_layers` | `number` | `3` | `bottom_shell_layers` | top_shell_layers/bottom_shell_layers |
93
+ | `infill_angle` | `number` | `45.0` | `infill_direction` | infill_direction |
94
+ | `infill_density` | `number` | `0.15` | `sparse_infill_density` | |
95
+ | `ironing_flow` | `number` | `10.0` | `ironing_flow` | ironing flow (%) |
96
+ | `ironing_spacing` | `number` | `0.1` | `ironing_spacing` | ironing line spacing (mm) |
97
+ | `ironing_speed` | `number` | `30.0` | `ironing_speed` | ironing speed (mm/s) |
98
+ | `ironing_type` | `string` | `'none'` | `ironing_type` | none\|top\|topmost\|solid (the top family = on) |
99
+ | `sparse_infill_filament_id` | `number` | `0` | `sparse_infill_filament_id` | |
100
+ | `sparse_infill_pattern` | `string` | `'rectilinear'` | `sparse_infill_pattern` | rectilinear\|grid\|triangles\|zigzag\|gyroid |
101
+ | `top_shell_layers` | `number` | `4` | `top_shell_layers` | top_shell_layers/bottom_shell_layers |
102
+
103
+ ### Speeds, flow and temperature
104
+
105
+ | Kernel param | Type | Default | From setting | Notes |
106
+ | --- | --- | --- | --- | --- |
107
+ | `bed_temp` | `number` | `60` | `hot_plate_temp` | |
108
+ | `bridge_speed` | `number` | `25.0` | `bridge_speed` | bridge_speed[0] (slowdown for unsupported bottoms) |
109
+ | `close_fan_the_first_x_layers` | `number` | `1` | `close_fan_the_first_x_layers` | close_fan_the_first_x_layers |
110
+ | `enable_pressure_advance` | `boolean` | `false` | `enable_pressure_advance` | enable_pressure_advance[0] |
111
+ | `fan_speed` | `number` | `100.0` | `fan_max_speed` | fan_speed (%) |
112
+ | `filament_diameter` | `number` | `1.75` | `filament_diameter` | |
113
+ | `first_layer_speed` | `number` | `20` | `initial_layer_speed` | |
114
+ | `flow_ratio` | `number` | `1.0` | `filament_flow_ratio` | |
115
+ | `full_fan_speed_layer` | `number` | `3` | `full_fan_speed_layer` | full_fan_speed_layer |
116
+ | `machine_accel_print` | `number` | `5000` | `default_acceleration` | mm/s² |
117
+ | `machine_accel_retract` | `number` | `5000` | `machine_max_acceleration_retracting` | mm/s² |
118
+ | `machine_accel_travel` | `number` | `5000` | `travel_acceleration` | mm/s² |
119
+ | `machine_jerk_e` | `number` | `2.5` | `machine_max_jerk_e` | mm/s |
120
+ | `machine_jerk_xy` | `number` | `9.0` | `machine_max_jerk_x` | mm/s |
121
+ | `machine_jerk_z` | `number` | `0.4` | `machine_max_jerk_z` | mm/s |
122
+ | `machine_max_accel_e` | `number` | `5000` | `machine_max_acceleration_e` | mm/s² (machine_max_acceleration_*) — per-axis ceiling, distinct from the accel_* feedrates above |
123
+ | `machine_max_accel_xy` | `number` | `5000` | `machine_max_acceleration_x` | mm/s² (machine_max_acceleration_*) — per-axis ceiling, distinct from the accel_* feedrates above |
124
+ | `machine_max_accel_z` | `number` | `500` | `machine_max_acceleration_z` | mm/s² (machine_max_acceleration_*) — per-axis ceiling, distinct from the accel_* feedrates above |
125
+ | `machine_max_speed_e` | `number` | `30` | `machine_max_speed_e` | mm/s |
126
+ | `machine_max_speed_xy` | `number` | `500` | `machine_max_speed_x` | mm/s |
127
+ | `machine_max_speed_z` | `number` | `12` | `machine_max_speed_z` | mm/s |
128
+ | `max_volumetric_extrusion_rate_slope` | `number` | `0.0` | `max_volumetric_extrusion_rate_slope` | PE-lite flow change rate limit (mm³/s², 0=off) |
129
+ | `nozzle_diameter` | `number` | `0.4` | `nozzle_diameter` | |
130
+ | `nozzle_temp` | `number` | `210` | `nozzle_temperature` | |
131
+ | `pressure_advance` | `number` | `0.02` | `pressure_advance` | pressure_advance[0] |
132
+ | `print_speed` | `number` | `60` | `outer_wall_speed` | |
133
+ | `slow_down_layer_time` | `number` | `8.0` | `slow_down_layer_time` | slow_down_layer_time (s) |
134
+ | `travel_speed` | `number` | `150` | `travel_speed` | |
135
+
136
+ ### Retraction and seams
137
+
138
+ | Kernel param | Type | Default | From setting | Notes |
139
+ | --- | --- | --- | --- | --- |
140
+ | `retract_length` | `number` | `0.8` | `filament_retraction_length`, `retraction_length` | retraction_length/speed[0], z_hop[0] |
141
+ | `retract_speed` | `number` | `30.0` | `filament_retraction_speed`, `retraction_speed` | retraction_length/speed[0], z_hop[0] |
142
+ | `retraction_minimum_travel` | `number` | `2.0` | `filament_retraction_minimum_travel`, `retraction_minimum_travel` | retraction_minimum_travel (mm): minimum travel that triggers a retraction |
143
+ | `scarf_length` | `number` | `10.0` | — | length of the scarf z/flow ramp (mm) |
144
+ | `seam_position` | `string` | `'back'` | `seam_position` | nearest\|aligned\|back\|random |
145
+ | `seam_slope_type` | `string` | `'none'` | `seam_slope_type` | none\|external\|all -> scarf seam (external/all = on) |
146
+ | `z_hop` | `number` | `0.4` | `filament_z_hop`, `z_hop` | retraction_length/speed[0], z_hop[0] |
147
+
148
+ ### Adhesion (skirt, brim, raft)
149
+
150
+ | Kernel param | Type | Default | From setting | Notes |
151
+ | --- | --- | --- | --- | --- |
152
+ | `brim_object_gap` | `number` | `0.0` | `brim_object_gap` | brim_object_gap (mm): gap between brim and object |
153
+ | `brim_width` | `number` | `0.0` | `brim_width` | skirt_distance / brim_width |
154
+ | `raft_contact_distance` | `number` | `0.1` | `raft_contact_distance` | raft_contact_distance (mm) |
155
+ | `raft_expansion` | `number` | `1.5` | `raft_expansion` | raft_expansion (mm). Upstream default 1.5 (previously hardcoded to 3.0) |
156
+ | `raft_first_layer_height` | `number` | `0.30` | — | raft first layer height (mm) |
157
+ | `raft_layers` | `number` | `0` | `raft_layers` | raft_layers |
158
+ | `skirt_distance` | `number` | `2.0` | `skirt_distance` | skirt_distance / brim_width |
159
+ | `skirt_height` | `number` | `1` | `skirt_height` | skirt_height: how many layers the skirt is drawn on |
160
+ | `skirt_loops` | `number` | `1` | `skirt_loops` | skirt_loops |
161
+
162
+ ### Support
163
+
164
+ | Kernel param | Type | Default | From setting | Notes |
165
+ | --- | --- | --- | --- | --- |
166
+ | `bridge_no_support` | `boolean` | `false` | `bridge_no_support` | bridge_no_support: no support under bridge regions |
167
+ | `enable_support` | `boolean` | `false` | `enable_support` | enable_support |
168
+ | `support_angle` | `number` | `0.0` | `support_angle` | support_angle: base angle of the support body (°). Upstream SupportParameters::base_angle |
169
+ | `support_auto` | `boolean` | `true` | — | stage 20: true = automatic overhang detection, false = manual (painted enforcers only) |
170
+ | `support_base_pattern` | `string` | `'default'` | `support_base_pattern` | support_base_pattern: default\|rectilinear\|rectilinear-grid\|honeycomb\|... |
171
+ | `support_base_pattern_spacing` | `number` | `2.5` | `support_base_pattern_spacing` | support_base_pattern_spacing (mm). Determines spacing together with density |
172
+ | `support_bottom_z_distance` | `number` | `0.2` | `support_bottom_z_distance` | stage 32: z gap under support resting on a model top surface (default 0.2 = equivalent to today's 1 layer -> default behavior and golden unchanged) |
173
+ | `support_density` | `number` | `0.15` | — | density of the support body |
174
+ | `support_expansion` | `number` | `0.0` | `support_expansion` | support_expansion (mm): expands the overhang region |
175
+ | `support_filament` | `number` | `0` | `support_filament` | filament for the support base (and, upstream, the raft) |
176
+ | `support_grid_snap` | `boolean` | `true` | — | equivalent of the upstream SupportGridPattern (default behavior of the grid style) |
177
+ | `support_interface_bottom_layers` | `number` | `0` | `support_interface_bottom_layers` | support_interface_bottom_layers (0 = none) |
178
+ | `support_interface_filament` | `number` | `0` | `support_interface_filament` | filament for the support interface |
179
+ | `support_interface_pattern` | `string` | `'auto'` | `support_interface_pattern` | support_interface_pattern: auto\|rectilinear\|concentric\|rectilinear_interlaced\|grid |
180
+ | `support_interface_spacing` | `number` | `0.5` | `support_interface_spacing` | support_interface_spacing (mm, 0=solid). Upstream default 0.5 |
181
+ | `support_interface_top_layers` | `number` | `2` | `support_interface_top_layers` | support_interface_top_layers |
182
+ | `support_object_first_layer_gap` | `number` | `0.2` | `support_object_first_layer_gap` | support_object_first_layer_gap (mm) |
183
+ | `support_on_build_plate_only` | `boolean` | `false` | `support_on_build_plate_only` | support_on_build_plate_only: only support reaching the bed |
184
+ | `support_overhang_min_area` | `number` | `0.0` | — | minimum overhang area (mm², 0 = auto w²). Replaces the morphological opening filter |
185
+ | `support_remove_small_overhang` | `boolean` | `true` | `support_remove_small_overhang` | support_remove_small_overhang (upstream default true) |
186
+ | `support_style` | `string` | `'grid'` | `support_style` | grid\|tree_lite |
187
+ | `support_threshold_angle` | `number` | `30.0` | `support_threshold_angle` | support_threshold_angle |
188
+ | `support_threshold_overlap` | `number` | `0.5` | `support_threshold_overlap` | support_threshold_overlap: the overlap criterion when θ=0 (as a fraction of extrusion width) |
189
+ | `support_top_z_distance` | `number` | `0.2` | `support_top_z_distance` | support_top_z_distance |
190
+ | `support_xy_distance` | `number` | `0.35` | `support_object_xy_distance` | support_object_xy_distance |
191
+ | `tree_lite_min_radius` | `number` | `1.5` | — | tree_lite taper constants (our own approximation — no matching upstream key) |
192
+ | `tree_lite_shrink` | `number` | `0.5` | — | tree_lite taper constants (our own approximation — no matching upstream key) |
193
+ | `tree_style` | `string` | `'organic'` | — | organic\|slim\|strong\|hybrid (upstream support_style smsTree*) |
194
+ | `tree_support_angle_slow` | `number` | `25.0` | `tree_support_angle_slow` | tree_support_angle_slow (deg) |
195
+ | `tree_support_branch_angle` | `number` | `40.0` | `tree_support_branch_angle` | fallback spelling — `tree_support_branch_angle_organic` wins when both are sent |
196
+ | `tree_support_branch_angle_organic` | `number` | `40.0` | `tree_support_branch_angle_organic` | tree_support_branch_angle_organic (deg) |
197
+ | `tree_support_branch_diameter` | `number` | `2.0` | `tree_support_branch_diameter` | fallback spelling — `tree_support_branch_diameter_organic` wins when both are sent |
198
+ | `tree_support_branch_diameter_angle` | `number` | `5.0` | `tree_support_branch_diameter_angle` | tree_support_branch_diameter_angle (deg) |
199
+ | `tree_support_branch_diameter_organic` | `number` | `2.0` | `tree_support_branch_diameter_organic` | tree_support_branch_diameter_organic (mm) |
200
+ | `tree_support_branch_distance` | `number` | `1.0` | `tree_support_branch_distance` | fallback spelling — `tree_support_branch_distance_organic` wins when both are sent |
201
+ | `tree_support_branch_distance_organic` | `number` | `1.0` | `tree_support_branch_distance_organic` | tree_support_branch_distance_organic (mm) |
202
+ | `tree_support_tip_diameter` | `number` | `0.8` | `tree_support_tip_diameter` | tree_support_tip_diameter (mm) |
203
+ | `tree_support_top_rate` | `number` | `30.0` | `tree_support_top_rate` | tree_support_top_rate (%) |
204
+ | `tree_support_wall_count` | `number` | `0` | `tree_support_wall_count` | tree_support_wall_count (organic applies max(1,·) internally) |
205
+
206
+ ### Multi-material and prime tower
207
+
208
+ | Kernel param | Type | Default | From setting | Notes |
209
+ | --- | --- | --- | --- | --- |
210
+ | `bottom_surface_filament_id` | `number` | `0` | `bottom_surface_filament_id` | |
211
+ | `enable_prime_tower` | `boolean` | `true` | `enable_prime_tower` | |
212
+ | `filament_cost` | `number[]` | `[]` | `filament_cost` | currency per kg, per filament |
213
+ | `filament_density` | `number[]` | `[]` | `filament_density` | g/cm3, per filament |
214
+ | `filament_map` | `number[]` | `[]` | `filament_map` | |
215
+ | `filament_settings_id` | `vector<std::string>` | `[]` | `filament_settings_id` | |
216
+ | `filament_type` | `vector<std::string>` | `[]` | `filament_type` | |
217
+ | `inner_wall_filament_id` | `number` | `0` | `inner_wall_filament_id` | |
218
+ | `internal_solid_filament_id` | `number` | `0` | `internal_solid_filament_id` | |
219
+ | `outer_wall_filament_id` | `number` | `0` | `outer_wall_filament_id` | |
220
+ | `top_surface_filament_id` | `number` | `0` | `top_surface_filament_id` | |
221
+ | `wipe_tower_real` | `boolean` | `false` | — | stage 12: use the real WipeTower.generate() on MM changes instead of the stage-6 square ring |
222
+
223
+ ### Bed and machine G-code
224
+
225
+ | Kernel param | Type | Default | From setting | Notes |
226
+ | --- | --- | --- | --- | --- |
227
+ | `bed_depth` | `number` | `256.0` | `printable_area` | bed size (offset = bed/2) |
228
+ | `bed_height` | `number` | `0.0` | `printable_height` | printable_height (mm). 0 = no ceiling (backwards compatible) |
229
+ | `bed_width` | `number` | `256.0` | `printable_area` | bed size (offset = bed/2) |
230
+ | `enable_arc_fitting` | `boolean` | `false` | `enable_arc_fitting` | enable_arc_fitting |
231
+ | `machine_end_gcode` | `string` | — | `machine_end_gcode` | printer profile custom G-code. Empty = the mini-kernel's own preamble/footer only |
232
+ | `machine_start_gcode` | `string` | — | `machine_start_gcode` | printer profile custom G-code. Empty = the mini-kernel's own preamble/footer only |
233
+ | `printable_height` | `number` | `250.0` | `printable_height` | printable_height (mm) — the tree BuildVolume height |
234
+
235
+ ### Engine, streaming and diagnostics
236
+
237
+ | Kernel param | Type | Default | From setting | Notes |
238
+ | --- | --- | --- | --- | --- |
239
+ | `arachne_dump` | `boolean` | `false` | — | temporary diagnostic: dump the PASS1 arachne input polygons to stderr |
240
+ | `economy` | `boolean` | `false` | — | |
241
+ | `emit_pe_tags` | `boolean` | `false` | — | emit PE tags on extrusion runs (enabled automatically when the real PE is used). Default false (backwards compatible) |
242
+ | `extruder_count` | `number` | `1` | — | multi-material: how many extruders are used (1\|2) |
243
+ | `extruder_filament_diameter` | `number[]` | `[]` | `filament_diameter` | |
244
+ | `extruder_flow_ratio` | `number[]` | `[]` | `filament_flow_ratio` | |
245
+ | `extruder_nozzle_temp` | `number[]` | `[]` | `nozzle_temperature` | |
246
+ | `extruder_retract_length` | `?` | `[]` | `filament_retraction_length` | undefined |
247
+ | `extruder_retract_speed` | `?` | `[]` | `filament_retraction_speed` | undefined |
248
+ | `extruder_z_hop` | `?` | `[]` | `filament_z_hop` | undefined |
249
+ | `extrusion_rate_slope_segment_length` | `number` | `1.0` | — | segment split length for the real PE (mm) |
250
+ | `flush_into_infill` | `boolean` | `false` | `flush_into_infill` | |
251
+ | `flush_multiplier` | `number` | `1.0` | `flush_multiplier` | upstream scales the whole table by this |
252
+ | `flush_volumes_matrix` | `number[]` | `[]` | `flush_volumes_matrix` | |
253
+ | `gcode_config_block` | `boolean` | `false` | — | ; CONFIG_BLOCK_START … the parameters this slice ran with … END |
254
+ | `gcode_stats_block` | `boolean` | `false` | — | ; filament used [mm]/[cm3]/[g], cost, total filament change |
255
+ | `independent_support_layer_height` | `boolean` | `false` | `independent_support_layer_height` | false by default because of the kernel z grid constraint (gap quantized to layers) |
256
+ | `keep_stages` | `boolean` | `false` | — | keep the stages cached after slicing (skips the early release — a memory trade-off) |
257
+ | `mm_group_split` | `number` | `0` | — | triangle group boundary index ([0,split)=T0, [split,N)=T1) |
258
+ | `mm_group_splits` | `number[]` | `[]` | — | |
259
+ | `mm_group_tools` | `number[]` | `[]` | — | |
260
+ | `pe_external_perimeter_only` | `boolean` | `false` | — | real PE: smooth outer walls only |
261
+ | `pe_lite` | `boolean` | `true` | — | true = the effective PE-lite (default), false = the ported real PE (tagged g-code) |
262
+ | `pe_strip_tags` | `boolean` | `true` | — | strip the tags from the final output after real PE post-processing |
263
+ | `prime_tower_ring_size` | `number` | `15.0` | — | side length of the fallback square ring (mm) |
264
+ | `prime_tower_width` | `number` | `30.0` | `prime_tower_width` | width of the real WipeTower (mm). Separate from the square ring width (15). |
265
+ | `prime_tower_x` | `number` | `10.0` | `wipe_tower_x` | |
266
+ | `prime_tower_y` | `number` | `10.0` | `wipe_tower_y` | |
267
+ | `prime_volume` | `number` | `0.0` | `prime_volume` | extra volume primed on every change, on top of the pair's |
268
+ | `reduce_crossing_wall` | `boolean` | `false` | `reduce_crossing_wall` | wall-avoiding travel |
269
+ | `reuse_stages` | `number` | `0` | — | |
270
+ | `time_engine` | `string` | `'full'` | — | |
271
+
272
+ <!-- KERNEL-PARAMS:END -->
273
+
274
+ ### The parameters no setting reaches
275
+
276
+ A `—` in *From setting* means there is no OrcaSlicer option behind that parameter, so it exists only if you put it
277
+ in the params object yourself. They are not one kind of thing, and the difference matters before you reach for one:
278
+
279
+ - **Host runtime flags** — `economy`, `keep_stages`, `reuse_stages`, `time_engine`, `wipe_tower_real`,
280
+ `gcode_stats_block`, `gcode_config_block`. Decisions the *caller* makes about a particular run, not properties of
281
+ the print. `economy` is the last rung of an out-of-memory retry: it streams G-code and skips the preview
282
+ toolpaths and the time estimate, so a model that cannot finish any other way still finishes.
283
+ - **Values the viewer computes** — `extruder_count`, `mm_group_split`, `mm_group_splits`, `mm_group_tools`,
284
+ `auto_center`. These come from the scene (how many extruders are loaded, which triangle ranges belong to which
285
+ tool), so there is nothing for a settings map to hold. `<Viewport/>` fills them in at slice time.
286
+ - **Kernel-only tuning** — `support_density`, `support_auto`, `support_grid_snap`, `support_overhang_min_area`,
287
+ `tree_style`, `tree_lite_shrink`, `tree_lite_min_radius`, `scarf_length`, `prime_tower_ring_size`,
288
+ `raft_first_layer_height`, `extrusion_rate_slope_segment_length`. Real knobs with no upstream option of the same
289
+ name — some are this kernel's own approximations (the `tree_lite_*` taper), others are upstream behaviour that
290
+ upstream never exposed as a setting.
291
+ - **Diagnostics** — `arachne_dump`, `pe_lite`, `pe_strip_tags`, `pe_external_perimeter_only`, `emit_pe_tags`. Left
292
+ at their defaults unless you are working on the kernel itself. `pe_lite` in particular defaults to `true` because
293
+ the real pressure equalizer only acts on G-code carrying OrcaSlicer's `;_EXTRUDE_SET_SPEED` tags, which this
294
+ kernel emits only when asked.
49
295
 
50
296
  ## Materials and multi-material
51
297
 
@@ -57,7 +303,17 @@ import { filamentPresets, deriveKernelParams } from 'three-slicer/settings'
57
303
  const filaments = await filamentPresets() // lazy — the artifact loads on first call
58
304
  filaments.listFor('Bambu Lab X1 Carbon 0.4 nozzle') // [{name, type, vendor}, …]
59
305
  filaments.recommendedFor('Bambu Lab X1 Carbon 0.4 nozzle') // the vendor's shortlist, filtered to the compatible set
60
- Object.assign(settings, filaments.settingsFor('Bambu PLA Basic @BBL X1C'))
306
+
307
+ // Applying one: CLEAR the preset's key set first. A preset carries only the keys it sets, so a plain merge leaves
308
+ // behind whatever the previous material set and this one does not — a PLA pick after an ABS pick keeps ABS's
309
+ // chamber temperature. `keys` is the exact set to clear, and the filament and process key sets are disjoint, so
310
+ // clearing one never disturbs the other.
311
+ function applyPreset(settings, preset, keys) {
312
+ const next = { ...settings }
313
+ for (const key of keys) delete next[key]
314
+ return Object.assign(next, preset)
315
+ }
316
+ settings = applyPreset(settings, filaments.settingsFor('Bambu PLA Basic @BBL X1C'), filaments.keys)
61
317
  ```
62
318
 
63
319
  **One material per extruder.** Upstream stores every filament option as one entry per extruder, so a multi-material settings map writes each extruder's material at its own index:
package/engine/index.d.ts CHANGED
@@ -6,12 +6,80 @@ export interface SliceCallbacks {
6
6
  onProgress?: (done: number, total: number) => void
7
7
  onLayer?: (layer: SliceLayer) => void
8
8
  }
9
+ /** The estimator's machine limits, as they were actually applied (`stats.machine_limits`). */
10
+ export interface SliceMachineLimits {
11
+ max_speed_xy: number; max_speed_z: number; max_speed_e: number
12
+ max_accel_xy: number; max_accel_z: number; max_accel_e: number
13
+ jerk_xy: number; jerk_z: number; jerk_e: number
14
+ accel_print: number; accel_travel: number; accel_retract: number
15
+ }
16
+
9
17
  export interface SliceStats {
10
- layers: number; model_layers: number; raft_layers: number;
11
- path_segments: number; filament_mm: number; time_estimate: number;
12
- streamed?: boolean; economy?: boolean; [k: string]: unknown
18
+ /** Emitted layers, raft included — the length of `result.layers` */
19
+ layers: number
20
+ /** Layers belonging to the model itself */
21
+ model_layers: number
22
+ raft_layers: number
23
+ path_segments: number
24
+ /** Filament consumed, mm of input filament */
25
+ filament_mm: number
26
+ /** Wall-crossing travels — the cross-check for `reduce_crossing_wall` */
27
+ wall_crossings: number
28
+
29
+ /** Seconds. `0` in economy mode, which skips the estimate */
30
+ time_estimate: number
31
+ first_layer_time: number
32
+ time_extrude: number
33
+ time_travel: number
34
+ /** Filament total the estimator parsed back out of the G-code — compare against `filament_mm` */
35
+ time_filament_mm: number
36
+ time_moves: number
37
+ /** Seconds per layer index */
38
+ layer_times: Record<string, number>
39
+ /** Seconds per extrusion role, keyed by the role number (the low nibble of `paths[k+3]`) */
40
+ role_times: Record<string, number>
41
+ /** Which estimator ran: the ported GCodeProcessor, the transcription, or neither */
42
+ time_engine: 'full' | 'transcribed' | 'fallback' | string
43
+ machine_limits: SliceMachineLimits
44
+
45
+ /** True when anything printed leaves the bed — support, skirt, brim and raft included */
46
+ over_bed: boolean
47
+ /** True when the MODEL itself is off the bed, not just what was printed around it */
48
+ over_bed_model: boolean
49
+ /** How far past the bed the toolpaths reach, mm. `0` = inside */
50
+ over_bed_x: number; over_bed_y: number
51
+ /** Emitted top layer past `printable_height`, mm */
52
+ over_bed_z: number
53
+
54
+ /** True when G-code and layers were delivered through `onLayer` and are NOT on the result */
55
+ streamed: boolean
56
+ /** True when it finished in economy mode: no preview toolpaths and no time estimate */
57
+ economy: boolean
58
+
59
+ // ---- multi-material only (absent from a single-material slice) ----
60
+ /** Indexed by tool number; sums to `filament_mm` */
61
+ filament_mm_by_tool?: number[]
62
+ /** The prime/wipe tower's share — already counted inside the per-tool figures above */
63
+ filament_mm_purge?: number
64
+ filament_mm_purge_by_tool?: number[]
65
+
66
+ // ---- phase timings, present when the kernel was built with them ----
67
+ t_pass1_ms?: number; t_surface_ms?: number; t_support_ms?: number; t_emit_ms?: number
68
+ /** The JS boundary's share during emit */
69
+ t_flush_ms?: number
70
+
71
+ [k: string]: unknown
72
+ }
73
+
74
+ export interface SliceResult {
75
+ /** Absent when `onLayer` was set — assemble it from the callback instead */
76
+ gcode?: string
77
+ stats: SliceStats
78
+ /** Absent when `onLayer` was set */
79
+ layers?: unknown[]
80
+ /** Set instead of a result when the slice failed or was cancelled (`'canceled'`) */
81
+ error?: string
13
82
  }
14
- export interface SliceResult { gcode?: string; stats: SliceStats; layers?: unknown[]; error?: string }
15
83
  export interface PaintArgs {
16
84
  facet: number; hx: number; hy: number; hz: number;
17
85
  cx: number; cy: number; cz: number; radius: number; enforcer: boolean
@@ -21,7 +89,8 @@ export interface Slicer {
21
89
  paintPrepare(stl: ArrayBuffer | Uint8Array): number
22
90
  paint(args: PaintArgs): { enf: number; blk: number }
23
91
  paintClear(): void
24
- overlay(enforcer: boolean): number[]
92
+ /** The painted overlay triangles for one state, as the kernel's own detached copy. */
93
+ overlay(enforcer: boolean): Float32Array
25
94
  heapSize(): number
26
95
  module: unknown
27
96
  dispose(): void
package/engine/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  // three-slicer — browser/WASM 3D-slicing SDK (reverse-engineered from OrcaSlicer).
2
- // Public API. Node-safe (imports only the pure-JS WASM factory; no JSON so it loads in plain Node ESM).
2
+ // Public API. Node-safe (loads only the pure-JS WASM factory; no JSON so it loads in plain Node ESM).
3
3
  // Schema-driven helpers (deriveKernelParams etc.) live in the "three-slicer/settings" subpath because
4
4
  // they import JSON — use them in a bundler (vite) or with Node JSON import attributes.
5
- import createModule from './src/slicer_core.js'
6
5
 
7
6
  const u8 = (b) => (b instanceof Uint8Array ? b : new Uint8Array(b))
8
7
 
@@ -21,7 +20,10 @@ export const engineWorkerURL = () => new URL('./src/slicer.worker.js', import.me
21
20
  // paintPrepare/paint/paintClear/overlay: manual support enforcer/blocker painting (TriangleSelector).
22
21
  // heapSize(): current WASM heap bytes (peak, monotonic). module: escape hatch. dispose(): drop the module.
23
22
  export async function createSlicer() {
24
- const M = await createModule()
23
+ // Loaded here rather than at module scope so the 3.5MB emscripten glue is a separate chunk: a host that imports
24
+ // this module only for engineWorkerURL() — the recommended browser path, where the kernel runs in the worker —
25
+ // used to pull the whole thing into its bundle for a function that returns a URL.
26
+ const M = await (await import('./src/slicer_core.js')).default()
25
27
  return {
26
28
  slice(stl, params, { onProgress, onLayer } = {}) {
27
29
  const p = typeof params === 'string' ? params : JSON.stringify(params || {})
@@ -35,7 +37,10 @@ export async function createSlicer() {
35
37
  return { enf: M.selector_painted_count(true), blk: M.selector_painted_count(false) }
36
38
  },
37
39
  paintClear() { M.selector_clear() },
38
- overlay(enforcer) { return Array.from(M.selector_overlay(!!enforcer)) },
40
+ // Returned as the kernel's own Float32Array. It is already a detached copy (bindings.cpp to_f32 slices the
41
+ // memory view), and the Array.from() this used to do cost 1.1ms of a measured 3.6ms stroke on a 40k-triangle
42
+ // mesh — the same conversion the worker dropped for the same reason (slicer.worker.js, 'overlay').
43
+ overlay(enforcer) { return M.selector_overlay(!!enforcer) },
39
44
  heapSize() { return M.heap_size() },
40
45
  module: M,
41
46
  dispose() { /* emscripten module is GC'd when dropped; no explicit teardown required */ },
@@ -0,0 +1,136 @@
1
+ // three-slicer/client — a promise wrapper over the slice worker's message protocol.
2
+ //
3
+ // Why it exists: the worker is the recommended way to slice in a browser (the kernel blocks its thread for
4
+ // seconds), but driving it meant hand-writing a `cmd` string protocol, assembling the streamed layers, knowing
5
+ // that a slice request is the one message with NO cmd and that its params must already be a JSON string, and
6
+ // reaching into a SharedArrayBuffer at an offset the worker posts once to cancel anything. All of that lived in
7
+ // the viewer and was not published.
8
+ //
9
+ // What it deliberately does NOT do: the viewer's progress weighting (its 15/5/40/40 phase budget is measured
10
+ // against ITS models) and its out-of-memory retry ladder (re-slicing simplified, then in economy mode). Those are
11
+ // UI policy — a host wanting them should own them, and lifting them here would make this a copy of the viewer.
12
+ //
13
+ // Requests are answered in order. The worker's onmessage handler processes one message at a time and each command
14
+ // replies exactly once, so a FIFO of pending requests is enough to match a reply to its caller — the protocol
15
+ // carries no request ids and does not need them.
16
+ import { engineWorkerURL } from '../index.js'
17
+
18
+ // Which reply type ends which command. A slice has no cmd (that is what selects slicing) and ends on 'done'.
19
+ const REPLY_OF = {
20
+ warmup: 'warm', prepare: 'prepared', paint: 'painted', erase: 'painted', clear: 'painted',
21
+ importPaint: 'painted', exportPaint: 'paintExport', overlay: 'overlay', slice: 'done',
22
+ }
23
+
24
+ const asBuffer = (data) => (data instanceof ArrayBuffer ? data : data?.buffer ?? data)
25
+
26
+ /**
27
+ * @param {Worker} [worker] an existing worker to drive. Omit to create one — which only works in a browser,
28
+ * since Node has no global Worker; a Node caller should use createSlicer() from the root entry instead.
29
+ */
30
+ export function createSlicerClient(worker = new Worker(engineWorkerURL(), { type: 'module' })) {
31
+ const pending = [] // FIFO of {expect, resolve, reject, onProgress, onLayer, chunks, layers}
32
+ let cancelFlag = null // Uint32Array over the worker's SharedArrayBuffer — mt kernel only
33
+ let closed = false
34
+
35
+ worker.addEventListener('message', ({ data }) => {
36
+ // Unprompted and not a reply to anything: the addresses of the progress counter and the cancel flag.
37
+ if (data.type === 'supsab') {
38
+ try { if (data.cancelPtr) cancelFlag = new Uint32Array(data.buf, data.cancelPtr, 1) } catch { /* not shared */ }
39
+ return
40
+ }
41
+ const head = pending[0]
42
+ if (!head) return
43
+ if (data.type === 'progress') { head.onProgress?.(data.done, data.total); return }
44
+ if (data.type === 'layer') {
45
+ head.onLayer?.({ z: data.z, idx: data.idx, gcode: data.gcode, paths: data.paths, widths: data.widths })
46
+ // Accumulated only when the caller did not take the layers itself — otherwise a streamed slice would hold
47
+ // the whole model in memory here, which is the one thing streaming exists to avoid.
48
+ if (!head.onLayer) { if (data.gcode) head.chunks.push(data.gcode); head.layers.push(data) }
49
+ return
50
+ }
51
+ pending.shift()
52
+ if (data.type === 'error') { head.reject(new Error(data.error)); return }
53
+ if (data.type !== head.expect) { head.reject(new Error(`expected '${head.expect}', got '${data.type}'`)); return }
54
+ // What the layer messages accumulated, if this caller did not take them itself.
55
+ if (data.type === 'done' && head.layers.length) data.assembled = { gcode: head.chunks.join(''), layers: head.layers }
56
+ head.resolve(data)
57
+ })
58
+ worker.addEventListener('error', (event) => {
59
+ const head = pending.shift()
60
+ head?.reject(new Error(event.message || 'worker failed'))
61
+ })
62
+
63
+ const send = (message, { transfer = [], onProgress, onLayer } = {}) => new Promise((resolve, reject) => {
64
+ if (closed) { reject(new Error('slicer client terminated')); return }
65
+ pending.push({ expect: REPLY_OF[message.cmd ?? 'slice'], resolve, reject, onProgress, onLayer, chunks: [], layers: [] })
66
+ worker.postMessage(message, transfer)
67
+ })
68
+
69
+ const brush = (cmd, args) => send({ ...args, cmd }).then(reply => ({
70
+ enforcer: reply.enf, blocker: reply.blk, counts: reply.counts, applied: reply.applied,
71
+ }))
72
+
73
+ return {
74
+ worker,
75
+
76
+ /** Load the kernel ahead of the first slice, so the first one is not also a 4MB download. */
77
+ warmup: () => send({ cmd: 'warmup' }).then(() => undefined),
78
+
79
+ /**
80
+ * Slice. `params` may be an object — it is stringified here, which the raw protocol does not do for you.
81
+ * With `onLayer` the layers are yours and the result carries stats only; without it they are assembled and
82
+ * the result looks like the direct handle's.
83
+ */
84
+ async slice(stl, params, { onProgress, onLayer } = {}) {
85
+ const buffer = asBuffer(stl)
86
+ const entry = { stl: buffer, params: typeof params === 'string' ? params : JSON.stringify(params ?? {}) }
87
+ const reply = await send(entry, { transfer: [buffer], onProgress, onLayer })
88
+ const result = reply.result ?? {}
89
+ // A streamed slice returns stats only, with the content delivered as `layer` messages. Unless the caller
90
+ // took those itself, hand back the same shape a batch slice produces.
91
+ if (!reply.assembled) return result
92
+ return { ...result, gcode: reply.assembled.gcode, layers: reply.assembled.layers }
93
+ },
94
+
95
+ /** Register a mesh for painting. `keepPaint` carries existing marks across a move. */
96
+ prepare: (stl, { keepPaint = false } = {}) => {
97
+ const buffer = asBuffer(stl)
98
+ return send({ cmd: 'prepare', stl: buffer, keepPaint }, { transfer: [buffer] })
99
+ .then(reply => ({ facets: reply.facets, kept: reply.kept }))
100
+ },
101
+
102
+ /** Paint. `{state: 1..16}` addresses any extruder; `{enforcer: false}` is the legacy blocker brush. */
103
+ paint: (args) => brush('paint', args),
104
+ /** Return the brushed facets to the default extruder. Not `paint` with state 0 — see types/worker.d.ts. */
105
+ erase: (args) => brush('erase', args),
106
+ clear: (states) => brush('clear', states ? { states } : {}),
107
+
108
+ overlay: (states) => send({ cmd: 'overlay', ...(states ? { states } : {}) })
109
+ .then(reply => ({ enforcer: reply.enf, blocker: reply.blk, overlays: reply.overlays })),
110
+
111
+ /** Load a 3mf's painting. Replaces every mark, so run it directly after `prepare`. */
112
+ importPaint: (facets, hex, states) => brush('importPaint', { facets, hex, ...(states ? { states } : {}) }),
113
+ exportPaint: () => send({ cmd: 'exportPaint' })
114
+ .then(reply => ({ supported: reply.supported, facets: reply.facets, hex: reply.hex })),
115
+
116
+ /**
117
+ * Stop the running slice. The kernel polls this flag from inside its C++ loop, so it works while the worker
118
+ * is blocked in WASM — but the flag lives in a SharedArrayBuffer, so it exists only on the multithreaded
119
+ * kernel, i.e. on a cross-origin-isolated page. Returns false when there is nothing to write to; terminate
120
+ * the worker instead.
121
+ */
122
+ cancel() {
123
+ if (!cancelFlag) return false
124
+ try { Atomics.store(cancelFlag, 0, 1) } catch { cancelFlag[0] = 1 }
125
+ return true
126
+ },
127
+
128
+ terminate() {
129
+ closed = true
130
+ while (pending.length) pending.shift().reject(new Error('slicer client terminated'))
131
+ worker.terminate()
132
+ },
133
+ }
134
+ }
135
+
136
+ export default createSlicerClient
@@ -11,6 +11,9 @@ export { default as uiTree } from 'three-slicer/data/ui-tree.json' with { type:
11
11
  export { default as toggleRules } from 'three-slicer/data/toggle-rules.json' with { type: 'json' }
12
12
  export { default as invalidationMap } from 'three-slicer/data/invalidation-map.json' with { type: 'json' }
13
13
  export { default as printers } from 'three-slicer/data/printers.json' with { type: 'json' }
14
+ // Which option keys belong to which preset type, straight from upstream's Preset.cpp. Small (17KB) and needed by
15
+ // anything that writes or reads a preset FILE, so it is static rather than lazy like the two big catalogs.
16
+ export { default as presetKeys } from 'three-slicer/data/preset-keys.json' with { type: 'json' }
14
17
  // processes is the largest artifact (~800 KB) and is only needed once a printer is picked, so it stays a dynamic
15
18
  // import. It is generated as a JS module rather than JSON precisely because of the attribute problem above, in its
16
19
  // other direction: a dynamic JSON import needs the attribute in Node, and that same attribute makes browsers