code wiki / (root) / nx_bed_mesh.nx

nx_bed_mesh.nx source

↩ module page · 207 lines · 7866 B

1// nx_bed_mesh.nx -- slicer-side per-XY Z compensation from a probed 2// bed-mesh model. 3// 4// ===================================================================== 5// EXCEED axis (research baseline) 6// ===================================================================== 7// 8// Industry treatment of bed warp (as of 2026-05): 9// 10// Klipper: BED_MESH_PROFILE_LOAD + Z_TILT_ADJUST -- mesh is 11// probed by firmware, Z-correction applied at GCODE 12// EXECUTION time. Slicer is bed-naive. 13// Marlin: M420 S1 -- runtime correction, same model. 14// RepRapFirmware: M557 + bed-mesh compensation, runtime. 15// OrcaSlicer 2.3.2: single-number "First Layer Print Height 16// Compensation" -- one global offset, not per-XY. 17// PrusaSlicer 2.7: mesh-leveling AWARE (knows the printer does it) 18// but emits FLAT first-layer G-code; relies on 19// firmware to compensate. 20// Bambu Studio: no slicer-side mesh compensation. 21// 22// All four major slicers EMIT FLAT G-code for the first layer and 23// rely on firmware-side Z correction. This breaks for: 24// - Firmware without mesh leveling (older boards, custom builds) 25// - G-code archived / shared / printed on a different machine 26// - Printers with mesh data the operator wants embedded for 27// reproducibility 28// 29// This file ships slicer-side bilinear-interp per-XY Z compensation 30// baked DIRECTLY into the Z coordinates of the emitted G-code, with 31// layer-tapered fade-out so the model still meets nominal Z by the 32// time bed warp stops mattering (typically 1-2 mm above bed). 33// 34// ===================================================================== 35// 36// license_tier: ORIGINAL 37 38import "nx_syscalls.nx" 39const NX_MAGIC_16384: i64 = 16384 40 41// ===== verdicts ==================================================== 42 43const NX_BED_MESH_OK: i64 = 0 44const NX_BED_MESH_ERR_BAD_INPUT: i64 = 1 45const NX_BED_MESH_ERR_BAD_INDEX: i64 = 2 46 47// ===== struct ====================================================== 48// 49// z_deltas is a flat n_x * n_y array indexed as [iy * n_x + ix]. 50// Q14 throughout (1 unit = 1/16384 mm). Typical bed warp magnitude: 51// ±300 μm = ±0.3 mm = ±4915 Q14. 52 53struct NxBedMesh { 54 n_x: i64, 55 n_y: i64, 56 x_min_q14: i64, 57 x_max_q14: i64, 58 y_min_q14: i64, 59 y_max_q14: i64, 60 z_deltas: *i64, 61 taper_layers: i64, 62} 63 64const NX_BED_MESH_BYTES: i64 = 64 65 66// Default taper: 10 layers at typical 0.2 mm layer height = 2 mm of 67// compensation fade-out. Matches Klipper's default `fade_end = 2.0` 68// (mesh-leveling literature consensus). 69const NX_BED_MESH_TAPER_LAYERS: i64 = 10 70 71// ===== construction ================================================ 72 73func nx_bed_mesh_new(n_x: i64, n_y: i64, 74 x_min_q14: i64, x_max_q14: i64, 75 y_min_q14: i64, y_max_q14: i64, 76 taper_layers: i64) -> *NxBedMesh { 77 if n_x < 2 { return 0 as *NxBedMesh } 78 if n_y < 2 { return 0 as *NxBedMesh } 79 if x_max_q14 <= x_min_q14 { return 0 as *NxBedMesh } 80 if y_max_q14 <= y_min_q14 { return 0 as *NxBedMesh } 81 if taper_layers <= 0 { return 0 as *NxBedMesh } 82 83 let m: *NxBedMesh = (sys_mmap(NX_BED_MESH_BYTES)) as *NxBedMesh 84 m.n_x = n_x 85 m.n_y = n_y 86 m.x_min_q14 = x_min_q14 87 m.x_max_q14 = x_max_q14 88 m.y_min_q14 = y_min_q14 89 m.y_max_q14 = y_max_q14 90 m.z_deltas = (sys_mmap(n_x * n_y * 8)) as *i64 91 m.taper_layers = taper_layers 92 93 // Zero-init z_deltas (flat bed default). 94 var i: i64 = 0 95 let total: i64 = n_x * n_y 96 while i < total { 97 m.z_deltas[i] = 0 98 i = i + 1 99 } 100 return m 101} 102 103func nx_bed_mesh_set_point(m: *NxBedMesh, ix: i64, iy: i64, 104 dz_q14: i64) -> i64 { 105 if (m as i64) == 0 { return NX_BED_MESH_ERR_BAD_INPUT } 106 if ix < 0 { return NX_BED_MESH_ERR_BAD_INDEX } 107 if iy < 0 { return NX_BED_MESH_ERR_BAD_INDEX } 108 if ix >= m.n_x { return NX_BED_MESH_ERR_BAD_INDEX } 109 if iy >= m.n_y { return NX_BED_MESH_ERR_BAD_INDEX } 110 m.z_deltas[iy * m.n_x + ix] = dz_q14 111 return NX_BED_MESH_OK 112} 113 114// ===== bilinear Z lookup =========================================== 115// 116// Returns the Z compensation (Q14 mm) at the given (x, y) by bilinear 117// interpolation over the four nearest grid corners. Points outside 118// the mesh return the nearest-edge value (NOT extrapolated -- mesh 119// data outside the probed region is undefined; we cap to the edge). 120 121func nx_bed_mesh_z_offset(m: *NxBedMesh, x_q14: i64, y_q14: i64) -> i64 { 122 if (m as i64) == 0 { return 0 } 123 124 // Map (x, y) to fractional grid index (gx, gy) in Q14. 125 let span_x: i64 = m.x_max_q14 - m.x_min_q14 126 let span_y: i64 = m.y_max_q14 - m.y_min_q14 127 let nx_m1: i64 = m.n_x - 1 128 let ny_m1: i64 = m.n_y - 1 129 130 // gx = (x - x_min) * (n_x - 1) / span_x (Q14) 131 let dx: i64 = x_q14 - m.x_min_q14 132 var gx_q14: i64 = (dx * nx_m1) / (span_x / NX_MAGIC_16384) 133 let dy: i64 = y_q14 - m.y_min_q14 134 var gy_q14: i64 = (dy * ny_m1) / (span_y / NX_MAGIC_16384) 135 136 // Clamp to [0, (n_x-1) * Q14] / [0, (n_y-1) * Q14] 137 let gx_max: i64 = nx_m1 * NX_MAGIC_16384 138 let gy_max: i64 = ny_m1 * NX_MAGIC_16384 139 if gx_q14 < 0 { gx_q14 = 0 } 140 if gy_q14 < 0 { gy_q14 = 0 } 141 if gx_q14 > gx_max { gx_q14 = gx_max } 142 if gy_q14 > gy_max { gy_q14 = gy_max } 143 144 let ix0: i64 = gx_q14 / NX_MAGIC_16384 145 let iy0: i64 = gy_q14 / NX_MAGIC_16384 146 var ix1: i64 = ix0 + 1 147 var iy1: i64 = iy0 + 1 148 if ix1 > nx_m1 { ix1 = nx_m1 } 149 if iy1 > ny_m1 { iy1 = ny_m1 } 150 151 // Fractional parts in Q14 (0..16384). 152 let fx_q14: i64 = gx_q14 - ix0 * NX_MAGIC_16384 153 let fy_q14: i64 = gy_q14 - iy0 * NX_MAGIC_16384 154 let inv_fx_q14: i64 = NX_MAGIC_16384 - fx_q14 155 let inv_fy_q14: i64 = NX_MAGIC_16384 - fy_q14 156 157 let z00: i64 = m.z_deltas[iy0 * m.n_x + ix0] 158 let z10: i64 = m.z_deltas[iy0 * m.n_x + ix1] 159 let z01: i64 = m.z_deltas[iy1 * m.n_x + ix0] 160 let z11: i64 = m.z_deltas[iy1 * m.n_x + ix1] 161 162 // Bilinear: z = (1-fx)(1-fy) z00 + fx (1-fy) z10 + 163 // (1-fx) fy z01 + fx fy z11 164 // Q14 multiplication: (a_q14 * b_q14) / 16384 = (a*b)_q14 165 let w00_num: i64 = inv_fx_q14 * inv_fy_q14 166 let w00: i64 = w00_num / NX_MAGIC_16384 167 let w10_num: i64 = fx_q14 * inv_fy_q14 168 let w10: i64 = w10_num / NX_MAGIC_16384 169 let w01_num: i64 = inv_fx_q14 * fy_q14 170 let w01: i64 = w01_num / NX_MAGIC_16384 171 let w11_num: i64 = fx_q14 * fy_q14 172 let w11: i64 = w11_num / NX_MAGIC_16384 173 174 let c00_num: i64 = w00 * z00 175 let c00: i64 = c00_num / NX_MAGIC_16384 176 let c10_num: i64 = w10 * z10 177 let c10: i64 = c10_num / NX_MAGIC_16384 178 let c01_num: i64 = w01 * z01 179 let c01: i64 = c01_num / NX_MAGIC_16384 180 let c11_num: i64 = w11 * z11 181 let c11: i64 = c11_num / NX_MAGIC_16384 182 183 let sum_a: i64 = c00 + c10 184 let sum_b: i64 = c01 + c11 185 return sum_a + sum_b 186} 187 188// ===== layer-tapered compensation ================================= 189// 190// Linearly fades compensation from FULL at layer 0 to ZERO at 191// layer_idx == taper_layers. Above taper_layers, returns 0 192// (model meets nominal Z above the fade region). 193// 194// Returns Q14 Z compensation to be ADDED to the emitter's z_q14. 195 196func nx_bed_mesh_z_at_layer(m: *NxBedMesh, x_q14: i64, y_q14: i64, 197 layer_idx: i64) -> i64 { 198 if (m as i64) == 0 { return 0 } 199 if layer_idx < 0 { return 0 } 200 if layer_idx >= m.taper_layers { return 0 } 201 202 let raw_dz: i64 = nx_bed_mesh_z_offset(m, x_q14, y_q14) 203 let remaining: i64 = m.taper_layers - layer_idx 204 let scaled_num: i64 = raw_dz * remaining 205 let dz: i64 = scaled_num / m.taper_layers 206 return dz 207}