three-slicer 0.3.1 → 0.3.3

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/PARAMS.md CHANGED
@@ -8,8 +8,8 @@ cannot drift from what `slice()` actually honours. Regenerate with `node types/g
8
8
 
9
9
  <!-- KERNEL-PARAMS:BEGIN — generated by types/gen_kernel_params.mjs, do not edit by hand -->
10
10
 
11
- The kernel accepts **159 parameters**. `deriveKernelParams()` produces 131 of them from the
12
- OrcaSlicer settings map; the remaining 28 have no schema key behind them and can only be set by writing
11
+ The kernel accepts **189 parameters**. `deriveKernelParams()` produces 160 of them from the
12
+ OrcaSlicer settings map; the remaining 29 have no schema key behind them and can only be set by writing
13
13
  the params object directly — those rows show `—` in the *From setting* column.
14
14
 
15
15
  A parameter left out of the object keeps the default listed here, so sending a partial object is normal and is
@@ -46,11 +46,13 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
46
46
  | `bottom_shell_layers` | `number` | `3` | `bottom_shell_layers` | top_shell_layers/bottom_shell_layers |
47
47
  | `infill_angle` | `number` | `45.0` | `infill_direction` | infill_direction |
48
48
  | `infill_density` | `number` | `0.15` | `sparse_infill_density` | |
49
+ | `infill_wall_overlap` | `number` | `15.0` | `infill_wall_overlap` | infill_wall_overlap (percent) |
49
50
  | `ironing_flow` | `number` | `10.0` | `ironing_flow` | ironing flow (%) |
50
51
  | `ironing_spacing` | `number` | `0.1` | `ironing_spacing` | ironing line spacing (mm) |
51
52
  | `ironing_speed` | `number` | `30.0` | `ironing_speed` | ironing speed (mm/s) |
52
53
  | `ironing_type` | `string` | `'none'` | `ironing_type` | none\|top\|topmost\|solid (the top family = on) |
53
54
  | `sparse_infill_filament_id` | `number` | `0` | `sparse_infill_filament_id` | |
55
+ | `sparse_infill_flow_ratio` | `number` | `1.0` | `sparse_infill_flow_ratio` | |
54
56
  | `sparse_infill_pattern` | `string` | `'rectilinear'` | `sparse_infill_pattern` | rectilinear\|grid\|triangles\|zigzag\|gyroid |
55
57
  | `top_shell_layers` | `number` | `4` | `top_shell_layers` | top_shell_layers/bottom_shell_layers |
56
58
 
@@ -103,6 +105,7 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
103
105
 
104
106
  | Kernel param | Type | Default | From setting | Notes |
105
107
  | --- | --- | --- | --- | --- |
108
+ | `brim_flow_ratio` | `?` | — | `brim_flow_ratio` | undefined |
106
109
  | `brim_object_gap` | `number` | `0.0` | `brim_object_gap` | brim_object_gap (mm): gap between brim and object |
107
110
  | `brim_width` | `number` | `0.0` | `brim_width` | skirt_distance / brim_width |
108
111
  | `raft_contact_distance` | `number` | `0.1` | `raft_contact_distance` | raft_contact_distance (mm) |
@@ -127,9 +130,11 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
127
130
  | `support_density` | `number` | `0.15` | — | density of the support body |
128
131
  | `support_expansion` | `number` | `0.0` | `support_expansion` | support_expansion (mm): expands the overhang region |
129
132
  | `support_filament` | `number` | `0` | `support_filament` | filament for the support base (and, upstream, the raft) |
133
+ | `support_flow_ratio` | `?` | — | `support_flow_ratio` | undefined |
130
134
  | `support_grid_snap` | `boolean` | `true` | — | equivalent of the upstream SupportGridPattern (default behavior of the grid style) |
131
135
  | `support_interface_bottom_layers` | `number` | `0` | `support_interface_bottom_layers` | support_interface_bottom_layers (0 = none) |
132
136
  | `support_interface_filament` | `number` | `0` | `support_interface_filament` | filament for the support interface |
137
+ | `support_interface_flow_ratio` | `?` | — | `support_interface_flow_ratio` | undefined |
133
138
  | `support_interface_pattern` | `string` | `'auto'` | `support_interface_pattern` | support_interface_pattern: auto\|rectilinear\|concentric\|rectilinear_interlaced\|grid |
134
139
  | `support_interface_spacing` | `number` | `0.5` | `support_interface_spacing` | support_interface_spacing (mm, 0=solid). Upstream default 0.5 |
135
140
  | `support_interface_top_layers` | `number` | `2` | `support_interface_top_layers` | support_interface_top_layers |
@@ -144,7 +149,7 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
144
149
  | `support_xy_distance` | `number` | `0.35` | `support_object_xy_distance` | support_object_xy_distance |
145
150
  | `tree_lite_min_radius` | `number` | `1.5` | — | tree_lite taper constants (our own approximation — no matching upstream key) |
146
151
  | `tree_lite_shrink` | `number` | `0.5` | — | tree_lite taper constants (our own approximation — no matching upstream key) |
147
- | `tree_style` | `string` | `'organic'` | — | organic\|slim\|strong\|hybrid (upstream support_style smsTree*) |
152
+ | `tree_style` | `string` | `'organic'` | `support_style` | organic\|slim\|strong\|hybrid (upstream support_style smsTree*) |
148
153
  | `tree_support_angle_slow` | `number` | `25.0` | `tree_support_angle_slow` | tree_support_angle_slow (deg) |
149
154
  | `tree_support_branch_angle` | `number` | `40.0` | `tree_support_branch_angle` | fallback spelling — `tree_support_branch_angle_organic` wins when both are sent |
150
155
  | `tree_support_branch_angle_organic` | `number` | `40.0` | `tree_support_branch_angle_organic` | tree_support_branch_angle_organic (deg) |
@@ -190,7 +195,11 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
190
195
 
191
196
  | Kernel param | Type | Default | From setting | Notes |
192
197
  | --- | --- | --- | --- | --- |
198
+ | `alternate_extra_wall` | `boolean` | `false` | `alternate_extra_wall` | alternate_extra_wall |
193
199
  | `arachne_dump` | `boolean` | `false` | — | temporary diagnostic: dump the PASS1 arachne input polygons to stderr |
200
+ | `bottom_solid_infill_flow_ratio` | `number` | `1.0` | `bottom_solid_infill_flow_ratio` | |
201
+ | `bridge_flow` | `number` | `1.0` | `bridge_flow` | |
202
+ | `detect_thin_wall` | `boolean` | `false` | `detect_thin_wall` | detect_thin_wall: medial-axis thin walls where one loop does not fit |
194
203
  | `economy` | `boolean` | `false` | — | |
195
204
  | `emit_pe_tags` | `boolean` | `false` | — | emit PE tags on extrusion runs (enabled automatically when the real PE is used). Default false (backwards compatible) |
196
205
  | `extruder_count` | `number` | `1` | — | multi-material: how many extruders are used (1\|2) |
@@ -201,27 +210,48 @@ same unit — `first_layer_height` comes from `initial_layer_print_height`, and
201
210
  | `extruder_retract_speed` | `?` | `[]` | `filament_retraction_speed` | undefined |
202
211
  | `extruder_z_hop` | `?` | `[]` | `filament_z_hop` | undefined |
203
212
  | `extrusion_rate_slope_segment_length` | `number` | `1.0` | — | segment split length for the real PE (mm) |
213
+ | `filament_max_volumetric_speed` | `number[]` | `[]` | `filament_max_volumetric_speed` | mm³/s, per filament; empty = no cap (GW::capped_feed) |
214
+ | `filter_out_gap_fill` | `number` | `0.0` | `filter_out_gap_fill` | filter_out_gap_fill (mm): gap fill shorter than this is dropped |
215
+ | `first_layer_flow_ratio` | `?` | — | `first_layer_flow_ratio` | undefined |
204
216
  | `flush_into_infill` | `boolean` | `false` | `flush_into_infill` | |
205
217
  | `flush_multiplier` | `number` | `1.0` | `flush_multiplier` | upstream scales the whole table by this |
206
218
  | `flush_volumes_matrix` | `number[]` | `[]` | `flush_volumes_matrix` | |
219
+ | `gap_fill_flow_ratio` | `?` | — | `gap_fill_flow_ratio` | undefined |
220
+ | `gap_infill_speed` | `number[]` | `[]` | `gap_infill_speed` | gap_infill_speed (mm/s, per extruder): a first entry of 0 turns gap fill off (upstream has_gap_fill); empty = the schema's 30 |
207
221
  | `gcode_config_block` | `boolean` | `false` | — | ; CONFIG_BLOCK_START … the parameters this slice ran with … END |
222
+ | `gcode_role_tags` | `boolean` | `false` | — | ;TYPE:<role> on every role change (upstream GCodeProcessor tag) |
208
223
  | `gcode_stats_block` | `boolean` | `false` | — | ; filament used [mm]/[cm3]/[g], cost, total filament change |
209
224
  | `independent_support_layer_height` | `boolean` | `false` | `independent_support_layer_height` | false by default because of the kernel z grid constraint (gap quantized to layers) |
225
+ | `inner_wall_flow_ratio` | `number` | `1.0` | `inner_wall_flow_ratio` | |
226
+ | `internal_solid_infill_flow_ratio` | `?` | — | `internal_solid_infill_flow_ratio` | undefined |
210
227
  | `keep_stages` | `boolean` | `false` | — | keep the stages cached after slicing (skips the early release — a memory trade-off) |
211
228
  | `mm_group_split` | `number` | `0` | — | triangle group boundary index ([0,split)=T0, [split,N)=T1) |
212
229
  | `mm_group_splits` | `number[]` | `[]` | — | |
213
230
  | `mm_group_tools` | `number[]` | `[]` | — | |
231
+ | `only_one_wall_first_layer` | `boolean` | `false` | `only_one_wall_first_layer` | only_one_wall_first_layer |
232
+ | `outer_wall_flow_ratio` | `number` | `1.0` | `outer_wall_flow_ratio` | |
214
233
  | `pe_external_perimeter_only` | `boolean` | `false` | — | real PE: smooth outer walls only |
215
234
  | `pe_lite` | `boolean` | `true` | — | true = the effective PE-lite (default), false = the ported real PE (tagged g-code) |
216
235
  | `pe_strip_tags` | `boolean` | `true` | — | strip the tags from the final output after real PE post-processing |
236
+ | `placeholder_config` | `string` | — | `filament_end_gcode`, `machine_end_gcode`, `machine_start_gcode` | |
237
+ | `precise_outer_wall` | `boolean` | `true` | `precise_outer_wall` | precise_outer_wall |
217
238
  | `prime_tower_ring_size` | `number` | `15.0` | — | side length of the fallback square ring (mm) |
218
239
  | `prime_tower_width` | `number` | `30.0` | `prime_tower_width` | width of the real WipeTower (mm). Separate from the square ring width (15). |
219
240
  | `prime_tower_x` | `number` | `10.0` | `wipe_tower_x` | |
220
241
  | `prime_tower_y` | `number` | `10.0` | `wipe_tower_y` | |
221
242
  | `prime_volume` | `number` | `0.0` | `prime_volume` | extra volume primed on every change, on top of the pair's |
243
+ | `print_flow_ratio` | `number` | `1.0` | `print_flow_ratio` | |
222
244
  | `reduce_crossing_wall` | `boolean` | `false` | `reduce_crossing_wall` | wall-avoiding travel |
223
245
  | `reuse_stages` | `number` | `0` | — | |
246
+ | `scarf_joint_flow_ratio` | `?` | — | `scarf_joint_flow_ratio` | undefined |
247
+ | `set_other_flow_ratios` | `boolean` | `false` | `set_other_flow_ratios` | |
248
+ | `single_tool` | `number` | `0` | — | |
249
+ | `thick_bridges` | `boolean` | `false` | `thick_bridges` | |
224
250
  | `time_engine` | `string` | `'full'` | — | |
251
+ | `top_bottom_infill_wall_overlap` | `number` | `25.0` | `top_bottom_infill_wall_overlap` | top_bottom_infill_wall_overlap (percent, first and topmost layer) |
252
+ | `top_solid_infill_flow_ratio` | `number` | `1.0` | `top_solid_infill_flow_ratio` | |
253
+ | `wall_direction` | `string` | `'ccw'` | `wall_direction` | wall_direction (ccw \| cw) |
254
+ | `wall_sequence` | `string` | `'inner wall/outer wall'` | `wall_sequence` | wall_sequence (inner wall/outer wall \| outer wall/inner wall \| inner-outer-inner wall) |
225
255
 
226
256
  <!-- KERNEL-PARAMS:END -->
227
257
 
@@ -231,14 +261,14 @@ A `—` in *From setting* means there is no OrcaSlicer option behind that parame
231
261
  in the params object yourself. They are not one kind of thing, and the difference matters before you reach for one:
232
262
 
233
263
  - **Host runtime flags** — `economy`, `keep_stages`, `reuse_stages`, `time_engine`, `wipe_tower_real`,
234
- `gcode_stats_block`, `gcode_config_block`. Decisions the *caller* makes about a particular run, not properties of
264
+ `gcode_stats_block`, `gcode_config_block`, `gcode_role_tags`. Decisions the *caller* makes about a particular run, not properties of
235
265
  the print. `economy` is the last rung of an out-of-memory retry: it streams G-code and skips the preview
236
266
  toolpaths and the time estimate, so a model that cannot finish any other way still finishes.
237
267
  - **Values the viewer computes** — `extruder_count`, `mm_group_split`, `mm_group_splits`, `mm_group_tools`,
238
- `auto_center`. These come from the scene (how many extruders are loaded, which triangle ranges belong to which
239
- tool), so there is nothing for a settings map to hold. `<Viewport/>` fills them in at slice time.
268
+ `single_tool`, `auto_center`. These come from the scene (how many extruders are loaded, which triangle ranges belong
269
+ to which tool, the one filament a plate's objects all print with), so there is nothing for a settings map to hold. `<Viewport/>` fills them in at slice time.
240
270
  - **Kernel-only tuning** — `support_density`, `support_auto`, `support_grid_snap`, `support_overhang_min_area`,
241
- `tree_style`, `tree_lite_shrink`, `tree_lite_min_radius`, `scarf_length`, `prime_tower_ring_size`,
271
+ `tree_lite_shrink`, `tree_lite_min_radius`, `scarf_length`, `prime_tower_ring_size`,
242
272
  `raft_first_layer_height`, `extrusion_rate_slope_segment_length`. Real knobs with no upstream option of the same
243
273
  name — some are this kernel's own approximations (the `tree_lite_*` taper), others are upstream behaviour that
244
274
  upstream never exposed as a setting.
@@ -0,0 +1,291 @@
1
+ // The front of PASS1 on the GPU: the seated triangles and the layer planes in (the kernel's MESH phase,
2
+ // wasm-core/contour_phase.h), contour_gpu.js's union input out, in GPU buffers, so the contours never leave the GPU
3
+ // between the cut and the union.
4
+ //
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 tri_plane
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 (chain_polys' greedy rule)
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 chain_polys' n + 1 points, write_polygon's duplicates dropped
11
+ // tables layer origins and grids, polygon ranges, the cell-list size
12
+ //
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
14
+ // own capture: 410 ms st, 71 ms mt); 0.24 % of the edges differ from the kernel's by its double rounding a shared
15
+ // mesh-edge crossing to 1e-6 mm two ways, which the 48-bit double-float does not reproduce.
16
+ //
17
+ // A layer with a chain that does not close is marked (openLayers): the kernel cuts, chains and unions that layer itself
18
+ // from the mesh it handed over (contour_phase::assemble), so the slice never depends on the GPU closing every loop.
19
+ import {
20
+ CUT_SHADER, CANCEL_SHADER, CHAIN_SHADER, JUMP_SHADER, CYCLES_SHADER, LOOPS_SHADER, OUTPUT_SHADER, WRITE_SHADER,
21
+ TABLES_SHADER, ENTRIES_SHADER, OPEN_LAYERS_SHADER,
22
+ } from './contour_front_shaders.js'
23
+ import { SCAN_SHADER } from './contour_gpu_shaders.js'
24
+
25
+ // Numeric WebGPU constants: the named globals exist in a browser worker but not on a bare node globalThis.
26
+ const BUFFER = { MAP_READ: 0x1, COPY_SRC: 0x4, COPY_DST: 0x8, UNIFORM: 0x40, STORAGE: 0x80 }
27
+ const SHADER_STAGE_COMPUTE = 0x4
28
+ const MAP_MODE_READ = 0x1
29
+ const WORKGROUP_SIZE = 256
30
+ const MAX_WORKGROUPS_PER_DIMENSION = 65535
31
+ const CELL_SIZE = 2_000_000 // the union's grid cell in kernel units; contour_gpu.js and contour_phase.cpp use the same
32
+ const INT32_MAX = 0x7fffffff
33
+ const INT32_MIN = -0x80000000
34
+
35
+ const smallestPowerOfTwo = (count) => 2 ** Math.ceil(Math.log2(Math.max(2, count)))
36
+
37
+ /**
38
+ * @param {GPUDevice} device the device contour_gpu.js runs on (its buffers feed the union directly)
39
+ * @returns {Promise<{ cut(triangles: Float32Array, planes: Float64Array): Promise<object>, dispose(): void }>}
40
+ */
41
+ export async function makeContourFront(device) {
42
+ const BINDING_TYPE = { u: 'uniform', r: 'read-only-storage', w: 'storage' }
43
+ const layouts = new Map(), modules = new Map(), pipelines = new Map(), buffers = new Map()
44
+ let encoder = null, transient = []
45
+
46
+ const layoutOf = (kinds) => {
47
+ if (!layouts.has(kinds)) layouts.set(kinds, device.createBindGroupLayout({ entries: [...kinds].map((kind, binding) => (
48
+ { binding, visibility: SHADER_STAGE_COMPUTE, buffer: { type: BINDING_TYPE[kind] } })) }))
49
+ return layouts.get(kinds)
50
+ }
51
+ const pipelineOf = (code, entryPoint, kinds) => {
52
+ const key = `${entryPoint}\u0000${kinds}\u0000${code}`
53
+ if (!pipelines.has(key)) {
54
+ if (!modules.has(code)) modules.set(code, device.createShaderModule({ code }))
55
+ pipelines.set(key, device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [layoutOf(kinds)] }),
56
+ compute: { module: modules.get(code), entryPoint } }))
57
+ }
58
+ return pipelines.get(key)
59
+ }
60
+ // compile every shader up front, so a WGSL error surfaces as a refusal instead of an empty result
61
+ for (const code of [CUT_SHADER, CANCEL_SHADER, CHAIN_SHADER, JUMP_SHADER, CYCLES_SHADER, LOOPS_SHADER, OUTPUT_SHADER, WRITE_SHADER, TABLES_SHADER, ENTRIES_SHADER, OPEN_LAYERS_SHADER, SCAN_SHADER]) {
62
+ const module = device.createShaderModule({ code })
63
+ modules.set(code, module)
64
+ const failure = (await module.getCompilationInfo()).messages.find(message => message.type === 'error')
65
+ if (failure) throw new Error(`contour front shader: line ${failure.lineNum}: ${failure.message}`)
66
+ }
67
+
68
+ // a storage buffer kept across slices by name; a regrown one is destroyed after the submit that last used it
69
+ const buffer = (name, bytes) => {
70
+ const size = Math.max(16, Math.ceil(bytes / 16) * 16)
71
+ const kept = buffers.get(name)
72
+ if (kept && kept.size >= size) return kept
73
+ if (kept) transient.push(kept)
74
+ const made = device.createBuffer({ size, usage: BUFFER.STORAGE | BUFFER.COPY_SRC | BUFFER.COPY_DST })
75
+ buffers.set(name, made)
76
+ return made
77
+ }
78
+ const encoderOf = () => { if (!encoder) encoder = device.createCommandEncoder(); return encoder }
79
+ const uniform = (values) => {
80
+ const made = device.createBuffer({ size: Math.max(16, values.length * 4), usage: BUFFER.UNIFORM | BUFFER.COPY_DST })
81
+ device.queue.writeBuffer(made, 0, new Uint32Array(values))
82
+ transient.push(made)
83
+ return made
84
+ }
85
+ const zero = device.createBuffer({ size: 16, usage: BUFFER.UNIFORM | BUFFER.COPY_DST }) // the double-float's `dfZero`
86
+ device.queue.writeBuffer(zero, 0, new Uint32Array(4))
87
+ const run = (code, entryPoint, kinds, bound, invocations) => {
88
+ if (invocations <= 0) return
89
+ let groupsX = Math.ceil(invocations / WORKGROUP_SIZE), groupsY = 1
90
+ if (groupsX > MAX_WORKGROUPS_PER_DIMENSION) { groupsY = Math.ceil(groupsX / MAX_WORKGROUPS_PER_DIMENSION); groupsX = MAX_WORKGROUPS_PER_DIMENSION }
91
+ const group = device.createBindGroup({ layout: layoutOf(kinds), entries: bound.map((target, binding) => ({ binding, resource: { buffer: target } })) })
92
+ const pass = encoderOf().beginComputePass()
93
+ pass.setPipeline(pipelineOf(code, entryPoint, kinds)); pass.setBindGroup(0, group); pass.dispatchWorkgroups(groupsX, groupsY); pass.end()
94
+ }
95
+ const clear = (target, bytes) => encoderOf().clearBuffer(target, 0, Math.ceil(bytes / 4) * 4)
96
+ const copy = (source, sourceOffset, target, targetOffset, bytes) => encoderOf().copyBufferToBuffer(source, sourceOffset, target, targetOffset, bytes)
97
+ const submit = async () => {
98
+ if (encoder) { device.queue.submit([encoder.finish()]); encoder = null }
99
+ await device.queue.onSubmittedWorkDone()
100
+ for (const item of transient) item.destroy()
101
+ transient = []
102
+ }
103
+ // `wordCount` 32-bit words from `offsetWords` on, copied out of the staging buffer
104
+ const readWords = async (source, wordCount, offsetWords = 0, Type = Uint32Array) => {
105
+ const bytes = Math.max(4, wordCount * 4)
106
+ const staging = device.createBuffer({ size: Math.ceil(bytes / 16) * 16, usage: BUFFER.MAP_READ | BUFFER.COPY_DST })
107
+ encoderOf().copyBufferToBuffer(source, offsetWords * 4, staging, 0, Math.ceil(bytes / 4) * 4)
108
+ await submit()
109
+ await staging.mapAsync(MAP_MODE_READ)
110
+ const words = new Type(staging.getMappedRange(0, wordCount * 4).slice(0))
111
+ staging.unmap(); staging.destroy()
112
+ return words
113
+ }
114
+ // in-place exclusive scan of `count` u32 (contour_gpu_shaders.js' scan)
115
+ const scan = (data, count, depth = 0) => {
116
+ const blocks = Math.ceil(count / WORKGROUP_SIZE)
117
+ const sums = buffer(`scan.${depth}`, (blocks + 1) * 4)
118
+ const parameters = uniform([count, 0, 0, 0])
119
+ run(SCAN_SHADER, 'scanBlock', 'uww', [parameters, data, sums], count)
120
+ if (blocks === 1) return
121
+ scan(sums, blocks, depth + 1)
122
+ run(SCAN_SHADER, 'addOff', 'uww', [parameters, data, sums], count)
123
+ }
124
+
125
+ // layers, triangles -> segments (with their chain keys), cancelled pairs, successors
126
+ async function segmentsOf(triangles, planes) {
127
+ const triangleCount = triangles.length / 9, layerCount = planes.length
128
+ const planePairs = new Float32Array(layerCount * 2) // each plane's z as hi + lo, for exact compares with f32 vertices
129
+ for (let layer = 0; layer < layerCount; layer++) {
130
+ const high = Math.fround(planes[layer])
131
+ planePairs[layer * 2] = high; planePairs[layer * 2 + 1] = Math.fround(planes[layer] - high)
132
+ }
133
+ const triangleBuffer = buffer('triangles', triangles.byteLength), planeBuffer = buffer('planes', planePairs.byteLength)
134
+ device.queue.writeBuffer(triangleBuffer, 0, triangles); device.queue.writeBuffer(planeBuffer, 0, planePairs)
135
+ const counts = buffer('counts', (triangleCount + 1) * 4)
136
+ clear(counts, (triangleCount + 1) * 4)
137
+ const unused = [1, 2, 3].map(index => buffer(`unused${index}`, 16))
138
+ run(CUT_SHADER, 'main', 'urrwwwwu', [uniform([triangleCount, layerCount, 0, 0]), triangleBuffer, planeBuffer, counts, ...unused, zero], triangleCount)
139
+ scan(counts, triangleCount + 1)
140
+ const [segmentCount] = await readWords(counts, 1, triangleCount) // the segment count sizes everything after
141
+ const segmentPoints = buffer('segmentPoints', segmentCount * 16), segmentKeys = buffer('segmentKeys', segmentCount * 16)
142
+ const segmentLayer = buffer('segmentLayer', segmentCount * 4)
143
+ run(CUT_SHADER, 'main', 'urrwwwwu', [uniform([triangleCount, layerCount, 1, 0]), triangleBuffer, planeBuffer, counts, segmentPoints, segmentKeys, segmentLayer, zero], triangleCount)
144
+
145
+ const tableSize = smallestPowerOfTwo(segmentCount * 2), mask = tableSize - 1
146
+ const dead = buffer('dead', segmentCount * 4)
147
+ const owner = buffer('owner', tableSize * 4), headA = buffer('headA', tableSize * 4), headB = buffer('headB', tableSize * 4)
148
+ const nextA = buffer('nextA', segmentCount * 4), nextB = buffer('nextB', segmentCount * 4)
149
+ clear(dead, segmentCount * 4); clear(owner, tableSize * 4); clear(headA, tableSize * 4); clear(headB, tableSize * 4)
150
+ const cancelBound = [uniform([segmentCount, mask, 0, 0]), segmentPoints, segmentLayer, owner, headA, headB, nextA, dead]
151
+ run(CANCEL_SHADER, 'insert', 'urrwwwww', cancelBound, segmentCount)
152
+ run(CANCEL_SHADER, 'pair', 'urrwwwww', cancelBound, tableSize)
153
+ clear(owner, tableSize * 4); clear(headA, tableSize * 4); clear(headB, tableSize * 4)
154
+ const successor = buffer('successor', (segmentCount + 2) * 4)
155
+ clear(successor, (segmentCount + 2) * 4)
156
+ const chainBound = [uniform([segmentCount, mask, 0, 0]), segmentKeys, segmentLayer, dead, owner, headA, headB, nextA, nextB, successor]
157
+ run(CHAIN_SHADER, 'insertStarts', 'urrrwwwwww', chainBound, segmentCount)
158
+ run(CHAIN_SHADER, 'insertEnds', 'urrrwwwwww', chainBound, segmentCount)
159
+ run(CHAIN_SHADER, 'pairEnds', 'urrrwwwwww', chainBound, tableSize)
160
+ return { layerCount, segmentCount, segmentPoints, segmentLayer, successor, dead }
161
+ }
162
+
163
+ // segments with successors -> the union's input (contour_gpu.js' layout), still on the GPU
164
+ async function loopsOf(segments) {
165
+ const { layerCount, segmentCount, segmentPoints, segmentLayer, successor, dead } = segments
166
+ // labels (each segment's loop head), then ranks (its distance to the loop's last segment); ping-pong buffers
167
+ const rounds = Math.ceil(Math.log2(Math.max(2, segmentCount))) + 1
168
+ const valueA = buffer('valueA', segmentCount * 4), nextOfA = buffer('nextOfA', segmentCount * 4)
169
+ const valueB = buffer('valueB', segmentCount * 4), nextOfB = buffer('nextOfB', segmentCount * 4)
170
+ const label = buffer('label', segmentCount * 4), rank = buffer('rank', segmentCount * 4), reach = buffer('reach', segmentCount * 4)
171
+ const jump = (mode, input, output) => run(JUMP_SHADER, 'main', 'urrwwwwrr',
172
+ [uniform([segmentCount, mode, 0, 0]), successor, dead, input[0], input[1], output[0], output[1], label, reach], segmentCount)
173
+ const jumpRounds = (mode, finalValue, finalNext) => {
174
+ let from = [valueA, nextOfA], to = [valueB, nextOfB]
175
+ for (let round = 0; round < rounds; round++) { jump(mode, from, to); [from, to] = [to, from] }
176
+ copy(from[0], 0, finalValue, 0, segmentCount * 4)
177
+ if (finalNext) copy(from[1], 0, finalNext, 0, segmentCount * 4)
178
+ }
179
+ jump(0, [valueB, nextOfB], [valueA, nextOfA]); jumpRounds(1, label, reach)
180
+ const hit = buffer('hit', segmentCount * 4)
181
+ clear(hit, segmentCount * 4)
182
+ run(CYCLES_SHADER, 'main', 'uww', [uniform([segmentCount, 0, 0, 0]), reach, hit], segmentCount)
183
+ run(CYCLES_SHADER, 'main', 'uww', [uniform([segmentCount, 1, 0, 0]), reach, hit], segmentCount)
184
+ const openLayer = buffer('openLayer', layerCount * 4)
185
+ clear(openLayer, layerCount * 4)
186
+ run(OPEN_LAYERS_SHADER, 'main', 'urrrw', [uniform([segmentCount, 0, 0, 0]), dead, reach, segmentLayer, openLayer], segmentCount)
187
+ jump(2, [valueB, nextOfB], [valueA, nextOfA]); jumpRounds(3, rank)
188
+
189
+ // heads, sorted by (layer, head) with a bitonic sort
190
+ const headFlag = buffer('headFlag', (segmentCount + 1) * 4)
191
+ clear(headFlag, (segmentCount + 1) * 4)
192
+ const loopBound = (parameters, heads, polygonOf, loopStart, order) => [parameters, successor, label, rank, segmentLayer, headFlag, heads, polygonOf, loopStart, order, reach]
193
+ const unused = [4, 5, 6, 7].map(index => buffer(`unused${index}`, 16))
194
+ run(LOOPS_SHADER, 'flagHeads', 'urrrrwwwwwr', loopBound(uniform([segmentCount, 0, layerCount, 0]), ...unused), segmentCount)
195
+ scan(headFlag, segmentCount + 1)
196
+ const [headCount] = await readWords(headFlag, 1, segmentCount) // the loop count sizes the sort
197
+ const padded = smallestPowerOfTwo(headCount)
198
+ const heads = buffer('heads', padded * 8), polygonOf = buffer('polygonOf', segmentCount * 4)
199
+ const loopStart = buffer('loopStart', (headCount + 1) * 4), order = buffer('order', (segmentCount + headCount) * 4)
200
+ const bound = (parameters) => loopBound(parameters, heads, polygonOf, loopStart, order)
201
+ run(LOOPS_SHADER, 'gatherHeads', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, 0])), segmentCount)
202
+ run(LOOPS_SHADER, 'padHeads', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), padded - headCount)
203
+ for (let block = 2; block <= padded; block *= 2) for (let distance = block / 2; distance >= 1; distance /= 2)
204
+ run(LOOPS_SHADER, 'bitonic', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, distance, block])), padded)
205
+ clear(loopStart, (headCount + 1) * 4)
206
+ run(LOOPS_SHADER, 'lengths', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), headCount)
207
+ scan(loopStart, headCount + 1)
208
+ const [slotCount] = await readWords(loopStart, 1, headCount) // points of the closed loops, chain_polys' n + 1 each
209
+ run(LOOPS_SHADER, 'place', 'urrrrwwwwwr', bound(uniform([segmentCount, padded, layerCount, headCount])), segmentCount)
210
+
211
+ // the points write_polygon keeps, the polygons chain_polys keeps, the layer boxes
212
+ const kept = buffer('kept', (slotCount + 1) * 4), polygonKept = buffer('polygonKept', (headCount + 1) * 4)
213
+ const layerBox = buffer('layerBox', layerCount * 32), slotPolygon = buffer('slotPolygon', slotCount * 4)
214
+ const boxStart = new Int32Array(layerCount * 8)
215
+ for (let layer = 0; layer < layerCount; layer++) boxStart.set([INT32_MAX, INT32_MAX, INT32_MIN, INT32_MIN], layer * 8)
216
+ device.queue.writeBuffer(layerBox, 0, boxStart)
217
+ clear(kept, (slotCount + 1) * 4)
218
+ const outputBound = [uniform([slotCount, headCount, layerCount, 0]), order, segmentPoints, segmentLayer, successor, loopStart, kept, polygonKept, layerBox, slotPolygon]
219
+ run(OUTPUT_SHADER, 'slotPolygons', 'urrrrrwwww', outputBound, headCount)
220
+ run(OUTPUT_SHADER, 'keep', 'urrrrrwwww', outputBound, slotCount)
221
+ run(OUTPUT_SHADER, 'polygonCounts', 'urrrrrwwww', outputBound, headCount)
222
+ run(OUTPUT_SHADER, 'keepValid', 'urrrrrwwww', outputBound, slotCount)
223
+ const polygonValidAt = buffer('polygonValidAt', (headCount + 1) * 4)
224
+ clear(polygonValidAt, (headCount + 1) * 4)
225
+ copy(polygonKept, 0, polygonValidAt, 0, headCount * 4)
226
+ scan(polygonValidAt, headCount + 1)
227
+ const keptAt = buffer('keptAt', (slotCount + 1) * 4)
228
+ clear(keptAt, (slotCount + 1) * 4)
229
+ copy(kept, 0, keptAt, 0, slotCount * 4)
230
+ scan(keptAt, slotCount + 1)
231
+ const [pointCount] = await readWords(keptAt, 1, slotCount)
232
+ const [polygonCount] = await readWords(polygonValidAt, 1, headCount)
233
+ const points = buffer('points', pointCount * 8), layerStart = buffer('layerStart', (layerCount + 1) * 4)
234
+ clear(layerStart, (layerCount + 1) * 4)
235
+ run(WRITE_SHADER, 'main', 'urrrrrrww', [uniform([slotCount, headCount, layerCount, 0]), order, segmentPoints, segmentLayer, kept, keptAt, layerBox, points, layerStart], slotCount)
236
+ scan(layerStart, layerCount + 1)
237
+ const layerInfo = buffer('layerInfo', (layerCount + 1) * 16), polygonInfo = buffer('polygonInfo', polygonCount * 20 + 16)
238
+ const tableBound = [uniform([slotCount, headCount, layerCount, CELL_SIZE]), layerBox, layerStart, layerInfo, polygonValidAt, loopStart, keptAt, order, segmentLayer, polygonInfo]
239
+ run(TABLES_SHADER, 'cells', 'urwwrrrrrw', tableBound, 1)
240
+ run(TABLES_SHADER, 'polygons', 'urwwrrrrrw', tableBound, headCount)
241
+ const entries = buffer('entries', 16)
242
+ clear(entries, 16)
243
+ run(ENTRIES_SHADER, 'main', 'urrw', [uniform([polygonCount, CELL_SIZE, 0, 0]), polygonInfo, points, entries], polygonCount)
244
+ const [cellCount] = await readWords(layerInfo, 1, layerCount * 4)
245
+ const [cellEntryCount] = await readWords(entries, 1)
246
+ const boxes = await readWords(layerBox, layerCount * 8, 0, Int32Array)
247
+ const openLayers = await readWords(openLayer, layerCount)
248
+ // the origin the kernel adds back to every piece; a layer with no loop has none, and the kernel's own is 0 there
249
+ const origins = new Int32Array(layerCount * 2)
250
+ for (let layer = 0; layer < layerCount; layer++) {
251
+ if (boxes[layer * 8 + 2] < boxes[layer * 8]) continue
252
+ origins[layer * 2] = boxes[layer * 8]; origins[layer * 2 + 1] = boxes[layer * 8 + 1]
253
+ }
254
+ return { layerCount, segmentCount: pointCount, polygonCount, cellCount, cellEntryCount, headCount, slotCount, origins, openLayers,
255
+ points, layerInfo, polygonInfo }
256
+ }
257
+
258
+ /**
259
+ * The union input of every layer of a mesh.
260
+ * @param {Float32Array} triangles 9 floats per triangle, seated (the kernel's contour_mesh().tris)
261
+ * @param {Float64Array} planes the z of every layer plane (contour_mesh().planes)
262
+ * @returns {Promise<object>} counts, `origins` (min x, min y per layer, kernel units), `openLayers` (1 per layer with a
263
+ * segment left out of every closed loop: that layer's input is incomplete and the kernel rebuilds it), `openSegments`
264
+ * and `input`, contour_gpu.js' union input with its arrays as GPU buffers
265
+ */
266
+ const cut = async (triangles, planes) => {
267
+ const started = performance.now()
268
+ const segments = await segmentsOf(triangles, planes)
269
+ const [matcherOverflow] = await readWords(segments.successor, 1, segments.segmentCount)
270
+ const deadFlags = await readWords(segments.dead, segments.segmentCount)
271
+ let cancelled = 0
272
+ for (const flag of deadFlags) cancelled += flag
273
+ const laid = await loopsOf(segments)
274
+ const loopSegments = laid.slotCount - laid.headCount
275
+ return {
276
+ ms: performance.now() - started, triangleCount: triangles.length / 9, rawSegments: segments.segmentCount, cancelled,
277
+ openSegments: segments.segmentCount - cancelled - loopSegments, matcherOverflow, origins: laid.origins, openLayers: laid.openLayers,
278
+ input: { layerCount: laid.layerCount, segmentCount: laid.segmentCount, polygonCount: laid.polygonCount, cellCount: laid.cellCount,
279
+ cellEntryCount: laid.cellEntryCount, points: { gpuBuffer: laid.points }, layerInfo: { gpuBuffer: laid.layerInfo },
280
+ polyInfo: { gpuBuffer: laid.polygonInfo }, polyLayer: { gpuBuffer: laid.polygonInfo, offset: laid.polygonCount * 16 } },
281
+ }
282
+ }
283
+ const dispose = () => {
284
+ for (const kept of buffers.values()) kept.destroy()
285
+ buffers.clear()
286
+ for (const item of transient) item.destroy()
287
+ transient = []
288
+ zero.destroy()
289
+ }
290
+ return { cut, dispose }
291
+ }