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}