code wiki / (root) / nx_gcode_emit.nx

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}