code wiki / (root) / nx_supports.nx

nx_supports.nx source

↩ module page · 357 lines · 13809 B

1// nx_supports.nx -- overhang detection for FDM 3D printing. 2// 3// v1 algorithm: for each vertex of the CURRENT layer's polygon, 4// check whether that vertex is supported by the PREVIOUS layer's 5// polygon (geometric expansion of prev by overhang_tolerance to 6// account for the maximum unsupported overhang angle). Unsupported 7// vertices need support pillars from below. 8// 9// Per cardinal NISHI_3D_PRINT_ROADMAP §2.1 axis 1 (physics-aware 10// supports): v1 is conservative -- detects only vertex-position 11// overhangs. v2 will add yield-load model + tree pathfinding + 12// branching topology, ALL composing v1's detection output. 13// 14// Overhang tolerance: 15// For a 45° safe overhang at layer height h, a vertex can extend 16// h * tan(45°) = h sideways without support. Caller passes the 17// tolerance directly (typically equal to layer_height_q14). 18// 19// Composes nx_polygon (offset for the "supported envelope" of prev 20// + point-in-polygon for membership test). 21// 22// First-layer handling: if prev_polygon is NULL (a 0 pointer cast) 23// the layer is treated as bed-supported -- no overhang verts. 24// 25// Christus-specific motivation: the outstretched arms appear in a 26// layer where they have no support in the layer below. Without 27// supports they bridge across air and sag. This primitive detects 28// where the arms start so v2 can generate pillars. 29// 30// license_tier: ORIGINAL 31 32import "nx_syscalls.nx" 33import "nx_polygon.nx" 34import "nx_abs.nx" 35import "nx_machine_graph.nx" 36import "nx_material_profile.nx" 37import "nx_gcode_emit.nx" 38import "nx_pillar_physics.nx" 39 40// ===== verdicts =================================================== 41 42const NX_SUPPORTS_OK: i64 = 0 43const NX_SUPPORTS_ERR_BAD_INPUT: i64 = 1 44const NX_SUPPORTS_ERR_CAPACITY: i64 = 2 45 46func nx_supports_verdict_name(v: i64) -> *u8 { 47 if v == NX_SUPPORTS_OK { return "OK" } 48 if v == NX_SUPPORTS_ERR_BAD_INPUT { return "BAD_INPUT" } 49 if v == NX_SUPPORTS_ERR_CAPACITY { return "CAPACITY" } 50 return "UNKNOWN" 51} 52 53// ===== support-point list ========================================= 54 55struct NxSupportPoints { 56 xs: *i64, 57 ys: *i64, 58 n: i64, 59 capacity: i64, 60} 61 62const NX_SUPPORTS_BYTES: i64 = 32 63 64func nx_supports_new(capacity: i64) -> *NxSupportPoints { 65 let sp: *NxSupportPoints = (sys_mmap(NX_SUPPORTS_BYTES)) as *NxSupportPoints 66 sp.xs = (sys_mmap(capacity * 8)) as *i64 67 sp.ys = (sys_mmap(capacity * 8)) as *i64 68 sp.n = 0 69 sp.capacity = capacity 70 return sp 71} 72 73func nx_supports_add(sp: *NxSupportPoints, x: i64, y: i64) -> i64 { 74 if sp.n >= sp.capacity { return -1 } 75 sp.xs[sp.n] = x 76 sp.ys[sp.n] = y 77 sp.n = sp.n + 1 78 return 0 79} 80 81// ===== overhang detection ========================================== 82// 83// For each vertex of `cur`, test whether it falls inside `prev` 84// expanded outward by `overhang_tol_q14`. Vertices outside the 85// expanded prev are appended to `sp` as overhang points. 86// 87// If `prev` is NULL (first layer), no overhang reported. 88// 89// Returns NX_SUPPORTS_OK on success, or an error verdict. 90 91func nx_supports_layer_overhang(prev: *NxPolygon, cur: *NxPolygon, 92 overhang_tol_q14: i64, 93 sp: *NxSupportPoints) -> i64 { 94 if (cur as i64) == 0 { return NX_SUPPORTS_ERR_BAD_INPUT } 95 if cur.n_verts < 3 { return NX_SUPPORTS_ERR_BAD_INPUT } 96 if (sp as i64) == 0 { return NX_SUPPORTS_ERR_BAD_INPUT } 97 98 // First layer: bed-supported by definition. 99 if (prev as i64) == 0 { return NX_SUPPORTS_OK } 100 if prev.n_verts < 3 { return NX_SUPPORTS_OK } 101 102 // Expand the previous layer by overhang_tol. The expanded 103 // polygon is the "supported envelope" -- anything inside it is 104 // OK; anything outside needs a pillar. 105 var expanded: *NxPolygon = prev 106 if overhang_tol_q14 > 0 { 107 expanded = nx_polygon_offset(prev, overhang_tol_q14) 108 if (expanded as i64) == 0 { return NX_SUPPORTS_ERR_BAD_INPUT } 109 } 110 111 // Test each vertex of cur against the supported envelope. 112 var i: i64 = 0 113 while i < cur.n_verts { 114 let vx: i64 = nx_polygon_get_x(cur, i) 115 let vy: i64 = nx_polygon_get_y(cur, i) 116 if nx_polygon_point_inside(expanded, vx, vy) == 0 { 117 if nx_supports_add(sp, vx, vy) != 0 { 118 return NX_SUPPORTS_ERR_CAPACITY 119 } 120 } 121 i = i + 1 122 } 123 return NX_SUPPORTS_OK 124} 125 126// ===== aggregate overhang count =================================== 127// 128// Quick scalar metric: total number of overhang vertices summed 129// across a sequence of consecutive layers. Useful for printability 130// scoring (no overhangs = no supports needed = simpler print). 131// 132// Caller passes polygon[0..n_layers]; polygons[0] is the bed layer 133// (NULL prev). Returns the aggregate count. 134 135func nx_supports_total_overhang(polygons: *u8, n_layers: i64, 136 overhang_tol_q14: i64) -> i64 { 137 if n_layers <= 0 { return 0 } 138 let sp: *NxSupportPoints = nx_supports_new(4096) 139 var total: i64 = 0 140 141 var li: i64 = 0 142 while li < n_layers { 143 var prev_poly: *NxPolygon = 0 as *NxPolygon 144 if li > 0 { 145 let prev_pp: *i64 = ((polygons as i64) + (li - 1) * 8) as *i64 146 prev_poly = prev_pp[0] as *NxPolygon 147 } 148 let cur_pp: *i64 = ((polygons as i64) + li * 8) as *i64 149 let cur_poly: *NxPolygon = cur_pp[0] as *NxPolygon 150 151 let n_before: i64 = sp.n 152 nx_supports_layer_overhang(prev_poly, cur_poly, 153 overhang_tol_q14, sp) 154 let n_added: i64 = sp.n - n_before 155 total = total + n_added 156 // Reset for next layer (don't accumulate vertex history in v1). 157 sp.n = 0 158 li = li + 1 159 } 160 return total 161} 162 163// ===== v2: pillar plan + G-code emission ========================== 164// 165// A NxSupportPillar is a vertical column of support material printed 166// from the bed up to (just below) the overhang vertex it supports. 167// Industry standard: small square cross-section (~2 mm edge) so the 168// pillar is mechanically rigid but easy to snap off after the print. 169// 170// NxSupportPlan aggregates pillars across the whole print. Build the 171// plan ONCE during slice setup (walk every layer's overhang detection, 172// dedup by XY proximity, extend top_z_q14 to highest layer needing 173// support). Then EMIT it per layer during the slice pipeline pass. 174// 175// v2 first iteration emits a single perimeter square at each pillar's 176// XY on every layer where pillar.top_z_q14 >= layer_z. v2.1 will add: 177// - small Z-gap between pillar top and overhang (easy removal) 178// - sparse zigzag infill inside pillars (less material) 179// - tree topology (branches that consolidate as they descend) 180 181struct NxSupportPillar { 182 x: i64, 183 y: i64, 184 top_z_q14: i64, 185 footprint_q14: i64, // per-pillar footprint (v2.1 physics-aware 186 // sizing); falls back to plan.footprint_q14 187 // when nx_support_plan_add_pillar is used. 188} 189 190const NX_SUPPORT_PILLAR_BYTES: i64 = 32 191 192struct NxSupportPlan { 193 pillars: *NxSupportPillar, 194 n: i64, 195 capacity: i64, 196 footprint_q14: i64, 197 spacing_q14: i64, 198} 199 200const NX_SUPPORT_PLAN_BYTES: i64 = 40 201 202func nx_support_plan_new(capacity: i64, 203 footprint_q14: i64, 204 spacing_q14: i64) -> *NxSupportPlan { 205 let plan: *NxSupportPlan = (sys_mmap(NX_SUPPORT_PLAN_BYTES)) as *NxSupportPlan 206 let buf: *NxSupportPillar = (sys_mmap(capacity * NX_SUPPORT_PILLAR_BYTES)) as *NxSupportPillar 207 plan.pillars = buf 208 plan.n = 0 209 plan.capacity = capacity 210 plan.footprint_q14 = footprint_q14 211 plan.spacing_q14 = spacing_q14 212 return plan 213} 214 215// Returns 0 if a new pillar was created, 1 if merged into an existing 216// pillar (top_z extended), or -1 if capacity exceeded. 217func nx_support_plan_add_pillar(plan: *NxSupportPlan, 218 x: i64, y: i64, top_z_q14: i64) -> i64 { 219 if (plan as i64) == 0 { return -1 } 220 let tol: i64 = plan.spacing_q14 221 222 // Dedup-by-proximity: any existing pillar within tol on both axes 223 // absorbs this request by extending its top_z to max(old, new). 224 var i: i64 = 0 225 while i < plan.n { 226 let pp: *NxSupportPillar = (((plan.pillars as i64) + i * NX_SUPPORT_PILLAR_BYTES)) as *NxSupportPillar 227 let dx_pos: i64 = x - pp.x 228 let dx: i64 = nx_abs(dx_pos) 229 let dy_pos: i64 = y - pp.y 230 let dy: i64 = nx_abs(dy_pos) 231 if dx <= tol { 232 if dy <= tol { 233 if top_z_q14 > pp.top_z_q14 { pp.top_z_q14 = top_z_q14 } 234 return 1 235 } 236 } 237 i = i + 1 238 } 239 240 if plan.n >= plan.capacity { return -1 } 241 let np: *NxSupportPillar = (((plan.pillars as i64) + plan.n * NX_SUPPORT_PILLAR_BYTES)) as *NxSupportPillar 242 np.x = x 243 np.y = y 244 np.top_z_q14 = top_z_q14 245 np.footprint_q14 = plan.footprint_q14 // default: plan-level 246 plan.n = plan.n + 1 247 return 0 248} 249 250// ===== physics-aware pillar add =================================== 251// 252// EXCEED axis: composes nx_pillar_footprint_for_load to size THIS 253// pillar's footprint from the cantilever moment it bears. Industry 254// (Orca/Bambu/Cura/Prusa) sizes every pillar identically regardless 255// of load -- under-engineering heavy cantilevers, over-engineering 256// light ones. This primitive scales pillar cross-section so the 257// pillar can ACTUALLY hold up what's above it. 258// 259// Inputs (Q14): 260// x, y, top_z_q14: pillar location + height (same as basic add) 261// local_mass_g_q14: mass of the LOCAL cantilever above this pillar 262// (caller slices the overhang into per-pillar 263// contributions; typically overhang_area_local × 264// n_layers_above × layer_thickness × material.density) 265// arm_mm_q14: horizontal distance from pillar centerline to 266// the local cantilever centroid 267// material: NxMaterialProfile (reads tensile_yield_mpa_q14) 268// 269// Returns 0 = new pillar, 1 = merged into existing (footprint of the 270// existing pillar grows to MAX of old and new physics-computed), 271// -1 = capacity exhausted. 272// 273// Merge policy: when a heavier load lands at an already-existing XY, 274// extend BOTH top_z (to highest layer) AND footprint (to thickest 275// required) -- the merged pillar must support every contribution. 276 277func nx_support_plan_add_pillar_physics(plan: *NxSupportPlan, 278 x: i64, y: i64, top_z_q14: i64, 279 local_mass_g_q14: i64, 280 arm_mm_q14: i64, 281 material: *NxMaterialProfile) -> i64 { 282 if (plan as i64) == 0 { return -1 } 283 284 let foot_q14: i64 = nx_pillar_footprint_for_load(local_mass_g_q14, 285 arm_mm_q14, 286 material) 287 let tol: i64 = plan.spacing_q14 288 289 var i: i64 = 0 290 while i < plan.n { 291 let pp: *NxSupportPillar = (((plan.pillars as i64) + i * NX_SUPPORT_PILLAR_BYTES)) as *NxSupportPillar 292 let dx_pos: i64 = x - pp.x 293 let dx: i64 = nx_abs(dx_pos) 294 let dy_pos: i64 = y - pp.y 295 let dy: i64 = nx_abs(dy_pos) 296 if dx <= tol { 297 if dy <= tol { 298 if top_z_q14 > pp.top_z_q14 { pp.top_z_q14 = top_z_q14 } 299 if foot_q14 > pp.footprint_q14 { pp.footprint_q14 = foot_q14 } 300 return 1 301 } 302 } 303 i = i + 1 304 } 305 306 if plan.n >= plan.capacity { return -1 } 307 let np: *NxSupportPillar = (((plan.pillars as i64) + plan.n * NX_SUPPORT_PILLAR_BYTES)) as *NxSupportPillar 308 np.x = x 309 np.y = y 310 np.top_z_q14 = top_z_q14 311 np.footprint_q14 = foot_q14 312 plan.n = plan.n + 1 313 return 0 314} 315 316// Emit a small square perimeter at each pillar XY whose top_z_q14 317// reaches or exceeds the current layer Z. Composes nx_polygon_make_square 318// + nx_gemit_polygon -- the SAME perimeter emission path the model 319// uses, so the support prints with identical material flow + accel. 320// 321// Returns the number of pillars emitted on this layer. 322func nx_support_plan_emit_layer(plan: *NxSupportPlan, 323 e: *NxGcodeEmitter, 324 z_q14: i64) -> i64 { 325 if (plan as i64) == 0 { return 0 } 326 if (e as i64) == 0 { return 0 } 327 if plan.n == 0 { return 0 } 328 329 let mat: *NxMaterialProfile = e.material 330 let speed: i64 = mat.print_speed_mms 331 let travel: i64 = mat.travel_speed_mms 332 333 nx_gemit_cstr(e, ";SUPPORT_START\n") 334 335 var emitted: i64 = 0 336 var i: i64 = 0 337 while i < plan.n { 338 let pp: *NxSupportPillar = (((plan.pillars as i64) + i * NX_SUPPORT_PILLAR_BYTES)) as *NxSupportPillar 339 if pp.top_z_q14 >= z_q14 { 340 // v2 EXCEED axis: each pillar uses its OWN footprint 341 // (load-proportional when added via _add_pillar_physics; 342 // plan default otherwise). Industry uses single global value. 343 let half: i64 = pp.footprint_q14 / 2 344 let x_min: i64 = pp.x - half 345 let y_min: i64 = pp.y - half 346 let x_max: i64 = pp.x + half 347 let y_max: i64 = pp.y + half 348 let sq: *NxPolygon = nx_polygon_make_square(x_min, y_min, x_max, y_max) 349 nx_gemit_polygon(e, sq, z_q14, speed, travel) 350 emitted = emitted + 1 351 } 352 i = i + 1 353 } 354 355 nx_gemit_cstr(e, ";SUPPORT_END\n") 356 return emitted 357}