nx_gcode_emit.nx source
↩ module page · 738 lines · 29324 B
1// nx_gcode_emit.nx -- compose every P0-P2 primitive into actual
2// Klipper-flavour G-code text. This is the closing-the-loop
3// primitive: NxSliceContours + NxMachineGraph + NxMaterialProfile
4// + nx_gcode_extrude + nx_q14_format -> .gcode file bytes that
5// Klipper/Moonraker can execute.
6//
7// Per cardinal NISHI_3D_PRINT_ROADMAP §2.5: every emitted G-code
8// file carries its provenance manifest as a comment-line header
9// (composes nx_gcode_manifest). G-code comments are universally
10// ignored by firmware -- zero behaviour impact, clean EXCEED axis
11// vs every SOTA slicer (none of which embed source provenance).
12//
13// G-code structure emitted (subset for v1; richer post-processing
14// queued):
15//
16// ; <manifest header lines if caller supplies one>
17// M104 S<print_temp> ; hotend target (no wait)
18// M140 S<bed_temp> ; bed target (no wait)
19// M190 S<bed_first_layer> ; wait bed
20// M109 S<hotend_first> ; wait hotend
21// G28 ; home all
22// G92 E0 ; reset extruder
23// G1 Z<safe_z> F300
24// ; --- per layer ---
25// ;LAYER:<n>
26//
27// M106 S<fan_pct> ; fan (post first-layer)
28// ; --- per polygon ---
29// G0 X<x> Y<y> Z<z> F<travel>
30// G1 X<x> Y<y> E<acc> F<print>
31//
32//
33// ; --- end ---
34// M104 S0
35// M140 S0
36// G1 Z<top>
37// M84
38//
39// Coordinate convention: Q14 fixed-point inputs throughout, emitted
40// as decimal text via nx_q14_to_decimal (3 fractional digits ->
41// 0.001 mm = 16.4 Q14 units precision, well above the formatter
42// truncation error).
43//
44// license_tier: ORIGINAL
45
46import "nx_syscalls.nx"
47import "nx_abs.nx"
48import "nx_polygon.nx"
49import "nx_slice_contour.nx"
50import "nx_machine_graph.nx"
51import "nx_material_profile.nx"
52import "nx_gcode_extrude.nx"
53import "nx_gcode_manifest.nx"
54import "nx_q14_format.nx"
55import "nx_bed_mesh.nx"
56const NX_MAGIC_16384: i64 = 16384
57
58const NX_GEMIT_Q14: i64 = 16384
59const NX_GEMIT_SAFE_Z_Q14: i64 = 81920 // 5mm = 5 * Q14
60const NX_GEMIT_END_Z_Q14: i64 = 1638400 // 100mm raise at end
61
62// ===== verdicts ====================================================
63
64const NX_GEMIT_OK: i64 = 0
65const NX_GEMIT_ERR_CAPACITY: i64 = 1
66const NX_GEMIT_ERR_BAD_MACHINE: i64 = 2
67const NX_GEMIT_ERR_BAD_MATERIAL: i64 = 3
68const NX_GEMIT_N_VERDICTS: i64 = 4
69
70func nx_gemit_verdict_name(v: i64) -> *u8 {
71 if v == NX_GEMIT_OK { return "OK" }
72 if v == NX_GEMIT_ERR_CAPACITY { return "CAPACITY" }
73 if v == NX_GEMIT_ERR_BAD_MACHINE { return "BAD_MACHINE" }
74 if v == NX_GEMIT_ERR_BAD_MATERIAL { return "BAD_MATERIAL" }
75 return "UNKNOWN"
76}
77
78// ===== struct ======================================================
79
80struct NxGcodeEmitter {
81 buf: *u8,
82 len: i64,
83 cap: i64,
84 machine: *NxMachineGraph,
85 material: *NxMaterialProfile,
86 layer_height_q14: i64,
87 line_width_q14: i64,
88 filament_xsect_q14: i64,
89 E_acc_q14: i64,
90 verdict: i64,
91 n_perimeters: i64, // shell loops per contour (default 2)
92 z_hop_q14: i64, // travel Z-hop amount; 0 = disabled
93 cur_layer_z_q14: i64, // current layer Z (tracked for hop)
94 arc_fit_enabled: i64, // 0 = G1 only; 1 = detect arcs -> G2/G3
95 arc_tol_q14: i64, // tolerance for arc fit (default ~410 = 0.025mm)
96 n_brim_loops: i64, // first-layer adhesion ring loops; 0 = disabled
97}
98
99const NX_GEMIT_BYTES: i64 = 128
100
101// ===== construction ===============================================
102
103func nx_gemit_new(machine: *NxMachineGraph, material: *NxMaterialProfile,
104 layer_height_q14: i64, line_width_q14: i64,
105 buf_capacity: i64) -> *NxGcodeEmitter {
106 let e: *NxGcodeEmitter = (sys_mmap(NX_GEMIT_BYTES)) as *NxGcodeEmitter
107 e.buf = sys_mmap(buf_capacity)
108 e.len = 0
109 e.cap = buf_capacity
110 e.machine = machine
111 e.material = material
112 e.layer_height_q14 = layer_height_q14
113 e.line_width_q14 = line_width_q14
114 e.filament_xsect_q14 = nx_gcex_filament_xsect_q14(machine.filament_dia_um)
115 e.E_acc_q14 = 0
116 e.verdict = NX_GEMIT_OK
117 e.n_perimeters = 2 // safe default: outer + 1 inner shell
118 e.z_hop_q14 = 0 // default disabled (operator opt-in)
119 e.cur_layer_z_q14 = 0
120 e.arc_fit_enabled = 0 // default off; operator enables for curves
121 e.arc_tol_q14 = 410 // ~0.025mm; industry-standard arc-fit tol
122 e.n_brim_loops = 0 // default off; operator enables for tall/narrow/ASA
123 return e
124}
125
126// Operator-facing brim toggle. Brim = first-layer adhesion ring of
127// N concentric loops printed OUTSIDE the model perimeter, fused to
128// the print, snapped off after. Industry typical 5-10 loops for
129// tall narrow models or ABS/ASA-class materials. Default disabled
130// to avoid brim-removal overhead for non-adhesion-critical prints.
131func nx_gemit_set_brim(e: *NxGcodeEmitter, n_loops: i64) -> i64 {
132 if n_loops < 0 { return -1 }
133 if n_loops > 30 { return -1 } // sanity ceiling
134 e.n_brim_loops = n_loops
135 return 0
136}
137
138// Operator-facing arc-fit toggle. Enable for curved prints to win
139// ~30-50% G-code file-size + ~10% real print time (smoother motion).
140// Disable for known-straight prints (cubes, brackets) -- saves the
141// detection-overhead per polygon.
142func nx_gemit_set_arc_fitting(e: *NxGcodeEmitter, enabled: i64,
143 tol_q14: i64) -> i64 {
144 if enabled != 0 { if enabled != 1 { return -1 } }
145 if tol_q14 < 0 { return -1 }
146 if tol_q14 > NX_MAGIC_16384 { return -1 } // 1mm tolerance is insane ceiling
147 e.arc_fit_enabled = enabled
148 if tol_q14 > 0 { e.arc_tol_q14 = tol_q14 }
149 return 0
150}
151
152// Operator-facing override for Z-hop on travels. Industry typical
153// 0.2-0.4 mm (3277-6554 Q14). 0 disables. Default disabled to
154// avoid the hop-overhead when not needed; operator enables for
155// surface-finish-critical or tall-skinny prints.
156func nx_gemit_set_z_hop(e: *NxGcodeEmitter, hop_q14: i64) -> i64 {
157 if hop_q14 < 0 { return -1 }
158 if hop_q14 > NX_MAGIC_16384 { return -1 } // sanity ceiling: 1mm
159 e.z_hop_q14 = hop_q14
160 return 0
161}
162
163// Operator-facing override for shell count. Industry typical: 3.
164// Substrate default: 2 (safe minimum vs single-perimeter weakness).
165func nx_gemit_set_perimeters(e: *NxGcodeEmitter, n: i64) -> i64 {
166 if n < 1 { return -1 }
167 if n > 10 { return -1 } // sanity ceiling
168 e.n_perimeters = n
169 return 0
170}
171
172// ===== base writers ===============================================
173
174func nx_gemit_byte(e: *NxGcodeEmitter, b: i64) -> i64 {
175 if e.len >= e.cap {
176 e.verdict = NX_GEMIT_ERR_CAPACITY
177 return -1
178 }
179 e.buf[e.len] = b & 0xff
180 e.len = e.len + 1
181 return 0
182}
183
184func nx_gemit_cstr(e: *NxGcodeEmitter, s: *u8) -> i64 {
185 var i: i64 = 0
186 while s[i] != 0 {
187 if nx_gemit_byte(e, s[i] as i64) != 0 { return -1 }
188 i = i + 1
189 }
190 return 0
191}
192
193func nx_gemit_bytes(e: *NxGcodeEmitter, src: *u8, n: i64) -> i64 {
194 var i: i64 = 0
195 while i < n {
196 if nx_gemit_byte(e, src[i] as i64) != 0 { return -1 }
197 i = i + 1
198 }
199 return 0
200}
201
202func nx_gemit_int(e: *NxGcodeEmitter, value: i64) -> i64 {
203 let scratch: *u8 = sys_mmap(32)
204 let n: i64 = nx_int_to_decimal(value, scratch, 32)
205 if n < 0 { return -1 }
206 return nx_gemit_bytes(e, scratch, n)
207}
208
209func nx_gemit_q14(e: *NxGcodeEmitter, value_q14: i64, frac_digits: i64) -> i64 {
210 let scratch: *u8 = sys_mmap(48)
211 let n: i64 = nx_q14_to_decimal(value_q14, scratch, 48, frac_digits)
212 if n < 0 { return -1 }
213 return nx_gemit_bytes(e, scratch, n)
214}
215
216// ===== provenance header ==========================================
217//
218// Splices a pre-built (and finalized) NxGcodeManifest into the
219// emitter at current cursor. Caller is responsible for finalising
220// the manifest before calling this.
221
222func nx_gemit_provenance(e: *NxGcodeEmitter, m: *NxGcodeManifest) -> i64 {
223 if (m as i64) == 0 { return 0 }
224 if m.finalized != 1 { return -1 }
225 return nx_gemit_bytes(e, m.buf, m.len)
226}
227
228// ===== preamble ===================================================
229
230func nx_gemit_preamble(e: *NxGcodeEmitter) -> i64 {
231 let mat: *NxMaterialProfile = e.material
232 // Heater targets (no wait)
233 nx_gemit_cstr(e, "M104 S")
234 nx_gemit_int(e, mat.hotend_first_layer_c)
235 nx_gemit_cstr(e, "\nM140 S")
236 nx_gemit_int(e, mat.bed_first_layer_c)
237 // Wait bed first, then hotend (warming bed while hotend climbs)
238 nx_gemit_cstr(e, "\nM190 S")
239 nx_gemit_int(e, mat.bed_first_layer_c)
240 nx_gemit_cstr(e, "\nM109 S")
241 nx_gemit_int(e, mat.hotend_first_layer_c)
242 // Home all axes
243 nx_gemit_cstr(e, "\nG28")
244 // Klipper bed-mesh auto-calibrate (closes #1 failure cause:
245 // bed-not-level + nozzle-gap-wrong per 99-stability roadmap).
246 // Safe to call: Qidi X-Max 3 has has_auto_bed_mesh=1 per
247 // nx_machine_graph; the macro is a no-op on printers without.
248 if e.machine.has_auto_bed_mesh == 1 {
249 // 2026 Klipper native: ADAPTIVE=1 probes only the print
250 // bounding box, denser mesh, 15-20 min saved per print on
251 // 300mm-class beds (per Klipper 2026 release notes).
252 nx_gemit_cstr(e, "\nBED_MESH_CALIBRATE ADAPTIVE=1")
253 }
254 // Reset extruder + safe Z + EXPLICIT fan off for first layer
255 // (defensive: ensures fan is OFF for layer 0 regardless of any
256 // residual state from a previous print -- composes with the
257 // material.fan_disable_first_n_layers logic in nx_gemit_layer).
258 nx_gemit_cstr(e, "\nG92 E0\nM106 S0\nG1 Z")
259 nx_gemit_q14(e, NX_GEMIT_SAFE_Z_Q14, 3)
260 nx_gemit_cstr(e, " F300\n")
261 return e.verdict
262}
263
264// Skirt is moved below extrude_to (forward-ref forbidden in NishiLang).
265
266// ===== travel + extrude moves =====================================
267
268// Retract: pull filament back by material.retract_distance_um before
269// a travel. Emits a single G1 E<E_acc - dist> at retract speed.
270// E_acc is NOT updated -- unretract returns to the same position.
271func nx_gemit_retract(e: *NxGcodeEmitter) -> i64 {
272 let mat: *NxMaterialProfile = e.material
273 if mat.retract_distance_um <= 0 { return 0 } // no retraction configured
274 // Convert µm -> Q14 mm: dist_um * Q14_ONE / 1000
275 let dist_q14: i64 = mat.retract_distance_um * NX_GEMIT_Q14 / 1000
276 let target_e: i64 = e.E_acc_q14 - dist_q14
277 let feedrate: i64 = mat.retract_speed_mms * 60
278 nx_gemit_cstr(e, "G1 E")
279 nx_gemit_q14(e, target_e, 5)
280 nx_gemit_cstr(e, " F")
281 nx_gemit_int(e, feedrate)
282 nx_gemit_byte(e, 10)
283 return e.verdict
284}
285
286// Unretract: push filament forward to current E_acc after travel.
287func nx_gemit_unretract(e: *NxGcodeEmitter) -> i64 {
288 let mat: *NxMaterialProfile = e.material
289 if mat.retract_distance_um <= 0 { return 0 }
290 let feedrate: i64 = mat.retract_speed_mms * 60
291 nx_gemit_cstr(e, "G1 E")
292 nx_gemit_q14(e, e.E_acc_q14, 5)
293 nx_gemit_cstr(e, " F")
294 nx_gemit_int(e, feedrate)
295 nx_gemit_byte(e, 10)
296 return e.verdict
297}
298
299// Travel: retract + optional Z-hop + G0 + Z-lower + unretract.
300// Z-hop lifts the nozzle by e.z_hop_q14 BEFORE the travel and
301// lowers AFTER arrival -- prevents nozzle from scraping across
302// previously-printed parts. Skipped if e.z_hop_q14 == 0.
303func nx_gemit_travel_to(e: *NxGcodeEmitter, x_q14: i64, y_q14: i64,
304 z_q14: i64, speed_mms: i64) -> i64 {
305 nx_gemit_retract(e)
306 let feedrate: i64 = speed_mms * 60 // mm/min for G-code
307 // Z-hop: lift before travel. Tracked layer Z is z_q14 + hop.
308 if e.z_hop_q14 > 0 {
309 let hopped_z: i64 = z_q14 + e.z_hop_q14
310 nx_gemit_cstr(e, "G1 Z")
311 nx_gemit_q14(e, hopped_z, 3)
312 nx_gemit_cstr(e, " F")
313 nx_gemit_int(e, feedrate)
314 nx_gemit_byte(e, 10)
315 }
316 // Travel itself: G0 at travel speed. If Z-hop was applied, the
317 // G0 stays at the hopped Z (lower happens below).
318 nx_gemit_cstr(e, "G0 X")
319 nx_gemit_q14(e, x_q14, 3)
320 nx_gemit_cstr(e, " Y")
321 nx_gemit_q14(e, y_q14, 3)
322 nx_gemit_cstr(e, " Z")
323 if e.z_hop_q14 > 0 {
324 nx_gemit_q14(e, z_q14 + e.z_hop_q14, 3)
325 }
326 if e.z_hop_q14 == 0 {
327 nx_gemit_q14(e, z_q14, 3)
328 }
329 nx_gemit_cstr(e, " F")
330 nx_gemit_int(e, feedrate)
331 nx_gemit_byte(e, 10)
332 // Lower Z back after travel.
333 if e.z_hop_q14 > 0 {
334 nx_gemit_cstr(e, "G1 Z")
335 nx_gemit_q14(e, z_q14, 3)
336 nx_gemit_cstr(e, " F")
337 nx_gemit_int(e, feedrate)
338 nx_gemit_byte(e, 10)
339 }
340 e.cur_layer_z_q14 = z_q14
341 nx_gemit_unretract(e)
342 return e.verdict
343}
344
345func nx_gemit_extrude_to(e: *NxGcodeEmitter, from_x: i64, from_y: i64,
346 to_x: i64, to_y: i64, speed_mms: i64) -> i64 {
347 let E_delta: i64 = nx_gcex_E_delta_for_move(from_x, from_y, to_x, to_y,
348 e.line_width_q14, e.layer_height_q14,
349 e.filament_xsect_q14)
350 e.E_acc_q14 = e.E_acc_q14 + E_delta
351 let feedrate: i64 = speed_mms * 60
352 nx_gemit_cstr(e, "G1 X")
353 nx_gemit_q14(e, to_x, 3)
354 nx_gemit_cstr(e, " Y")
355 nx_gemit_q14(e, to_y, 3)
356 nx_gemit_cstr(e, " E")
357 nx_gemit_q14(e, e.E_acc_q14, 5)
358 nx_gemit_cstr(e, " F")
359 nx_gemit_int(e, feedrate)
360 nx_gemit_byte(e, 10)
361 return e.verdict
362}
363
364// ===== skirt (first-layer adhesion preflight) =====================
365//
366// Per NISHI_3D_PRINT_STABILITY_99_ROADMAP §1.1: skirt is the #1
367// substrate-side mitigation for first-layer adhesion failures.
368// - primes the nozzle (consistent flow before real print starts)
369// - lets operator + camera observe first-layer adhesion BEFORE
370// committing to the real print -- abort early if skirt won't
371// stick
372//
373// v1: single rectangular loop at bbox + offset. Multi-loop +
374// polygon-shape-following skirt queued for v2.
375
376func nx_gemit_skirt(e: *NxGcodeEmitter,
377 bbox_min_x_q14: i64, bbox_min_y_q14: i64,
378 bbox_max_x_q14: i64, bbox_max_y_q14: i64,
379 z_q14: i64, offset_q14: i64) -> i64 {
380 let mat: *NxMaterialProfile = e.material
381 let speed: i64 = mat.first_layer_speed_mms
382 let travel: i64 = mat.travel_speed_mms
383
384 let sx0: i64 = bbox_min_x_q14 - offset_q14
385 let sy0: i64 = bbox_min_y_q14 - offset_q14
386 let sx1: i64 = bbox_max_x_q14 + offset_q14
387 let sy1: i64 = bbox_max_y_q14 + offset_q14
388
389 nx_gemit_cstr(e, ";SKIRT_START\n")
390 nx_gemit_travel_to(e, sx0, sy0, z_q14, travel)
391 nx_gemit_extrude_to(e, sx0, sy0, sx1, sy0, speed)
392 nx_gemit_extrude_to(e, sx1, sy0, sx1, sy1, speed)
393 nx_gemit_extrude_to(e, sx1, sy1, sx0, sy1, speed)
394 nx_gemit_extrude_to(e, sx0, sy1, sx0, sy0, speed)
395 nx_gemit_cstr(e, ";SKIRT_END\n")
396 return e.verdict
397}
398
399// ===== G2/G3 arc emit =============================================
400//
401// Emits a single arc command from current position to (end_x, end_y)
402// with center at offset (I, J) from current. winding > 0 -> G3
403// (counter-clockwise), winding < 0 -> G2 (clockwise). E_delta
404// computed by caller (chord-length approximation suffices for v1;
405// 1% under-extrusion vs true arc length, operator tunes via material
406// flow_ratio if it matters).
407//
408// I and J are offsets from CURRENT position to circle center,
409// per RS-274 convention.
410
411func nx_gemit_arc(e: *NxGcodeEmitter,
412 end_x_q14: i64, end_y_q14: i64,
413 i_offset_q14: i64, j_offset_q14: i64,
414 winding: i64,
415 e_delta_q14: i64, speed_mms: i64) -> i64 {
416 e.E_acc_q14 = e.E_acc_q14 + e_delta_q14
417 let feedrate: i64 = speed_mms * 60
418 if winding < 0 { nx_gemit_cstr(e, "G2 X") }
419 if winding >= 0 { nx_gemit_cstr(e, "G3 X") }
420 nx_gemit_q14(e, end_x_q14, 3)
421 nx_gemit_cstr(e, " Y")
422 nx_gemit_q14(e, end_y_q14, 3)
423 nx_gemit_cstr(e, " I")
424 nx_gemit_q14(e, i_offset_q14, 3)
425 nx_gemit_cstr(e, " J")
426 nx_gemit_q14(e, j_offset_q14, 3)
427 nx_gemit_cstr(e, " E")
428 nx_gemit_q14(e, e.E_acc_q14, 5)
429 nx_gemit_cstr(e, " F")
430 nx_gemit_int(e, feedrate)
431 nx_gemit_byte(e, 10)
432 return e.verdict
433}
434
435// ===== single-polygon perimeter (with optional arc fitting) =======
436//
437// Travels to vertex 0, then walks the polygon vertices. If
438// arc_fit_enabled, greedy-detects runs of 4+ vertices that fit a
439// circular arc within arc_tol_q14, emits single G2/G3 per run.
440// Falls back to G1 individual moves for non-arc segments.
441//
442// Arc-fit overhead: O(n²) worst case (re-fitting per vertex);
443// acceptable for typical contours (~50-200 verts/layer). Disabled
444// by default (operator opt-in for curved prints).
445
446func nx_gemit_polygon(e: *NxGcodeEmitter, p: *NxPolygon,
447 z_q14: i64, speed_mms: i64,
448 travel_speed_mms: i64) -> i64 {
449 if p.n_verts < 3 { return 0 }
450 let v0x: i64 = nx_polygon_get_x(p, 0)
451 let v0y: i64 = nx_polygon_get_y(p, 0)
452 nx_gemit_travel_to(e, v0x, v0y, z_q14, travel_speed_mms)
453
454 var prev_x: i64 = v0x
455 var prev_y: i64 = v0y
456 let cx_out: *i64 = (sys_mmap(8)) as *i64
457 let cy_out: *i64 = (sys_mmap(8)) as *i64
458 let rsq_out: *i64 = (sys_mmap(8)) as *i64
459
460 var i: i64 = 1
461 while i < p.n_verts {
462 var arc_consumed: i64 = 0
463 // Try arc fit starting at (prev, verts[i], verts[i+1])
464 // requires 3 verts ahead from prev: verts[i], verts[i+1],
465 // and at least one more to make 4 total (min for arc emit).
466 if e.arc_fit_enabled == 1 {
467 if i + 2 < p.n_verts {
468 let v1x: i64 = nx_polygon_get_x(p, i)
469 let v1y: i64 = nx_polygon_get_y(p, i)
470 let v2x: i64 = nx_polygon_get_x(p, i + 1)
471 let v2y: i64 = nx_polygon_get_y(p, i + 1)
472 let ok: i64 = nx_polygon_3point_circle(prev_x, prev_y,
473 v1x, v1y,
474 v2x, v2y,
475 cx_out, cy_out, rsq_out)
476 if ok == 1 {
477 // Try to extend: how many additional verts fit?
478 var j: i64 = i + 2
479 var ext: i64 = 0
480 while j < p.n_verts {
481 let cand_x: i64 = nx_polygon_get_x(p, j)
482 let cand_y: i64 = nx_polygon_get_y(p, j)
483 if nx_polygon_point_on_circle(cx_out[0], cy_out[0],
484 rsq_out[0],
485 cand_x, cand_y,
486 e.arc_tol_q14) == 1 {
487 ext = ext + 1
488 j = j + 1
489 }
490 if nx_polygon_point_on_circle(cx_out[0], cy_out[0],
491 rsq_out[0],
492 cand_x, cand_y,
493 e.arc_tol_q14) == 0 {
494 j = p.n_verts // exit loop
495 }
496 }
497 // Emit arc if we got 4+ verts (prev + i + i+1 + extensions)
498 if ext >= 1 {
499 let arc_end_idx: i64 = i + 1 + ext
500 let arc_end_x: i64 = nx_polygon_get_x(p, arc_end_idx)
501 let arc_end_y: i64 = nx_polygon_get_y(p, arc_end_idx)
502 let i_off: i64 = cx_out[0] - prev_x
503 let j_off: i64 = cy_out[0] - prev_y
504 let winding: i64 = nx_polygon_arc_winding(cx_out[0], cy_out[0],
505 prev_x, prev_y,
506 v1x, v1y)
507 // E_delta via chord-length approximation (1% under-extrude vs arc)
508 let e_delta: i64 = nx_gcex_E_delta_for_move(prev_x, prev_y,
509 arc_end_x, arc_end_y,
510 e.line_width_q14,
511 e.layer_height_q14,
512 e.filament_xsect_q14)
513 nx_gemit_arc(e, arc_end_x, arc_end_y, i_off, j_off,
514 winding, e_delta, speed_mms)
515 prev_x = arc_end_x
516 prev_y = arc_end_y
517 i = arc_end_idx + 1
518 arc_consumed = 1
519 }
520 }
521 }
522 }
523 if arc_consumed == 0 {
524 let vx: i64 = nx_polygon_get_x(p, i)
525 let vy: i64 = nx_polygon_get_y(p, i)
526 nx_gemit_extrude_to(e, prev_x, prev_y, vx, vy, speed_mms)
527 prev_x = vx
528 prev_y = vy
529 i = i + 1
530 }
531 }
532 // Close back to vertex 0
533 nx_gemit_extrude_to(e, prev_x, prev_y, v0x, v0y, speed_mms)
534 return e.verdict
535}
536
537// ===== brim (first-layer adhesion ring) ===========================
538//
539// Emits e.n_brim_loops concentric loops printed OUTWARD from the
540// model contour at first-layer Z. Loops are printed largest-first
541// (outside-in) so the innermost brim loop fuses with the model
542// perimeter when the per-layer emit starts.
543//
544// Industry typical: 5-10 brim loops for tall narrow models or
545// ABS/ASA-class materials that warp + lift. Operator snaps brim
546// off after print.
547
548func nx_gemit_brim(e: *NxGcodeEmitter, contour: *NxPolygon,
549 z_q14: i64) -> i64 {
550 if e.n_brim_loops <= 0 { return 0 }
551 if (contour as i64) == 0 { return 0 }
552 if contour.n_verts < 3 { return 0 }
553 let mat: *NxMaterialProfile = e.material
554 let speed: i64 = mat.first_layer_speed_mms
555 let travel: i64 = mat.travel_speed_mms
556
557 nx_gemit_cstr(e, ";BRIM_START\n")
558 // Print outside-in: i = n_brim_loops down to 1. Each loop is
559 // the contour offset outward by (i * line_width_q14).
560 var i: i64 = e.n_brim_loops
561 while i >= 1 {
562 let offset_d: i64 = i * e.line_width_q14
563 let loop_poly: *NxPolygon = nx_polygon_offset(contour, offset_d)
564 if (loop_poly as i64) != 0 {
565 if loop_poly.n_verts >= 3 {
566 nx_gemit_polygon(e, loop_poly, z_q14, speed, travel)
567 }
568 }
569 i = i - 1
570 }
571 nx_gemit_cstr(e, ";BRIM_END\n")
572 return e.verdict
573}
574
575// ===== per-layer ==================================================
576
577func nx_gemit_layer(e: *NxGcodeEmitter, contours: *NxSliceContours,
578 layer_idx: i64) -> i64 {
579 let mat: *NxMaterialProfile = e.material
580 let z_q14: i64 = (layer_idx + 1) * e.layer_height_q14
581
582 // Layer comment marker (operator + tooling can parse this).
583 nx_gemit_cstr(e, ";LAYER:")
584 nx_gemit_int(e, layer_idx)
585 nx_gemit_byte(e, 10)
586
587 // Fan: disabled for first N layers per material profile.
588 if layer_idx >= mat.fan_disable_first_n_layers {
589 let fan_byte: i64 = (mat.fan_speed_normal_pct * 255) / 100
590 nx_gemit_cstr(e, "M106 S")
591 nx_gemit_int(e, fan_byte)
592 nx_gemit_byte(e, 10)
593 }
594
595 // Speeds: first-layer slower than normal.
596 var speed: i64 = mat.print_speed_mms
597 if layer_idx == 0 { speed = mat.first_layer_speed_mms }
598 let travel: i64 = mat.travel_speed_mms
599
600 var pi: i64 = 0
601 while pi < contours.n_polys {
602 let poly: *NxPolygon = nx_slice_contours_get(contours, pi)
603 if (poly as i64) != 0 {
604 // Multi-perimeter: emit n_perimeters inward-offset loops.
605 // Stops early if the inset polygon collapses below a
606 // viable area threshold (~ 4 × line_width² in Q28).
607 var cur: *NxPolygon = poly
608 var perim_i: i64 = 0
609 var done: i64 = 0
610 while done == 0 {
611 if perim_i >= e.n_perimeters { done = 1 }
612 if done == 0 {
613 if cur.n_verts < 3 { done = 1 }
614 }
615 if done == 0 {
616 // Degeneracy guard: signed 2*area in Q28. If
617 // below ~ (line_width)² × 4 (a few-bead-width
618 // floor), the inset has collapsed; stop.
619 let area2: i64 = nx_polygon_signed_2area_q28(cur)
620 let area_abs: i64 = nx_abs(area2)
621 let line_sq: i64 = e.line_width_q14 * e.line_width_q14
622 if area_abs < line_sq * 4 { done = 1 }
623 }
624 if done == 0 {
625 nx_gemit_polygon(e, cur, z_q14, speed, travel)
626 perim_i = perim_i + 1
627 if perim_i < e.n_perimeters {
628 // Inset for next perimeter: negative offset
629 // shrinks polygon by line_width inward.
630 let next_poly: *NxPolygon = nx_polygon_offset(cur, 0 - e.line_width_q14)
631 if (next_poly as i64) == 0 { done = 1 }
632 if done == 0 { cur = next_poly }
633 }
634 }
635 }
636 }
637 pi = pi + 1
638 }
639 return e.verdict
640}
641
642// ===== postamble ==================================================
643
644func nx_gemit_postamble(e: *NxGcodeEmitter) -> i64 {
645 nx_gemit_cstr(e, "M104 S0\nM140 S0\nM106 S0\nG1 Z")
646 nx_gemit_q14(e, NX_GEMIT_END_Z_Q14, 3)
647 nx_gemit_cstr(e, " F600\nM84\n")
648 return e.verdict
649}
650
651// ===== bed-mesh-compensated extrude (EXCEED axis) ===================
652//
653// Subdivides a single extrusion move into N segments based on
654// max_segment_q14 length, then emits G1 X Y Z E F per segment with
655// Z computed from the bed mesh + layer taper at each segment endpoint.
656//
657// Industry baseline (every major slicer): emits ONE G1 X Y E F per
658// extrusion move with the layer Z fixed at travel-to time -- relies
659// on firmware-side bed mesh compensation to apply per-XY Z deltas at
660// execution time. This breaks for boards without mesh leveling
661// (older controllers, custom builds), G-code archived for later
662// playback, or print farms where G-code moves between machines with
663// different mesh profiles.
664//
665// EXCEED: this function bakes per-XY Z directly into the emitted Z
666// coordinates so the file is fully self-contained. Works on any
667// firmware including bare Marlin without M420 support.
668
669const NX_GEMIT_DEFAULT_BEDMESH_SEG_Q14: i64 = 81920 // 5.0 mm in Q14
670
671func nx_gemit_extrude_to_bedmesh(e: *NxGcodeEmitter,
672 bed_mesh: *NxBedMesh,
673 from_x: i64, from_y: i64,
674 to_x: i64, to_y: i64,
675 z_base_q14: i64,
676 layer_idx: i64,
677 max_segment_q14: i64,
678 speed_mms: i64) -> i64 {
679 if (e as i64) == 0 { return 0 }
680 if (bed_mesh as i64) == 0 { return 0 }
681
682 // Segment count: ceil(max(|dx|, |dy|) / max_segment).
683 let dx_raw: i64 = to_x - from_x
684 let dy_raw: i64 = to_y - from_y
685 let adx: i64 = nx_abs(dx_raw)
686 let ady: i64 = nx_abs(dy_raw)
687 var span: i64 = adx
688 if ady > span { span = ady }
689 if span == 0 { return e.verdict } // no-op move
690
691 var n_segs: i64 = 1
692 if max_segment_q14 > 0 {
693 n_segs = span / max_segment_q14
694 if span - n_segs * max_segment_q14 > 0 { n_segs = n_segs + 1 }
695 if n_segs < 1 { n_segs = 1 }
696 }
697
698 let feedrate: i64 = speed_mms * 60
699 var prev_x: i64 = from_x
700 var prev_y: i64 = from_y
701 var seg_i: i64 = 1
702 while seg_i <= n_segs {
703 // Segment endpoint via linear interpolation in Q14.
704 let scaled_dx: i64 = dx_raw * seg_i
705 let scaled_dy: i64 = dy_raw * seg_i
706 let inc_x: i64 = scaled_dx / n_segs
707 let inc_y: i64 = scaled_dy / n_segs
708 let seg_x: i64 = from_x + inc_x
709 let seg_y: i64 = from_y + inc_y
710
711 // Z at this XY: base + bed-mesh layer-tapered offset.
712 let dz: i64 = nx_bed_mesh_z_at_layer(bed_mesh, seg_x, seg_y, layer_idx)
713 let seg_z: i64 = z_base_q14 + dz
714
715 // Extrusion volume for THIS segment only.
716 let E_delta: i64 = nx_gcex_E_delta_for_move(prev_x, prev_y, seg_x, seg_y,
717 e.line_width_q14, e.layer_height_q14,
718 e.filament_xsect_q14)
719 e.E_acc_q14 = e.E_acc_q14 + E_delta
720
721 nx_gemit_cstr(e, "G1 X")
722 nx_gemit_q14(e, seg_x, 3)
723 nx_gemit_cstr(e, " Y")
724 nx_gemit_q14(e, seg_y, 3)
725 nx_gemit_cstr(e, " Z")
726 nx_gemit_q14(e, seg_z, 3)
727 nx_gemit_cstr(e, " E")
728 nx_gemit_q14(e, e.E_acc_q14, 5)
729 nx_gemit_cstr(e, " F")
730 nx_gemit_int(e, feedrate)
731 nx_gemit_byte(e, 10)
732
733 prev_x = seg_x
734 prev_y = seg_y
735 seg_i = seg_i + 1
736 }
737 return e.verdict
738}