nx_slice_auto_supports.nx source
↩ module page · 158 lines · 6925 B
1// nx_slice_auto_supports.nx -- auto-build a physics-aware support
2// plan from a real mesh.
3//
4// =====================================================================
5// SUPERIOR CAPABILITY (per the 2026-05-20 cardinal):
6// Industry slicers auto-detect supports but ALL emit constant-diameter
7// pillars regardless of cantilever load. This primitive composes:
8//
9// mesh -> bvh -> per-layer slice -> overhang vertex detect ->
10// load-proportional pillar add (physics-aware sizing) -> NxSupportPlan
11//
12// The capability that industry doesn't have: auto-detected supports
13// with load-AWARE per-pillar sizing. The integrated workflow IS the
14// superior capability -- not just a math primitive.
15// =====================================================================
16//
17// license_tier: ORIGINAL
18
19import "nx_syscalls.nx"
20import "nx_polygon.nx"
21import "nx_mesh.nx"
22import "nx_mesh_print_check.nx"
23import "nx_bvh.nx"
24import "nx_slice_plane.nx"
25import "nx_slice_contour.nx"
26import "nx_supports.nx"
27import "nx_material_profile.nx"
28import "nx_pillar_physics.nx"
29
30// Default local-load proxy for each detected overhang vertex.
31// v1 uses constant 1 g·mm moment per vertex; substrate caller can
32// override by walking the plan and updating per-pillar load with
33// geometry-derived values. The proximity-dedup of the plan ensures
34// repeated overhangs at the same XY across layers extend top_z to
35// the highest needing layer.
36const NX_AUTO_SUPPORT_DEFAULT_MASS_Q14: i64 = 16384 // 1 g
37const NX_AUTO_SUPPORT_DEFAULT_ARM_Q14: i64 = 32768 // 2 mm
38
39const NX_AUTO_SUPPORT_OK: i64 = 0
40const NX_AUTO_SUPPORT_ERR_INPUT: i64 = 1
41
42// Builds a physics-aware support plan by walking every layer Z step
43// of the mesh, detecting overhang vertices via nx_supports_layer_overhang,
44// and adding them to a NxSupportPlan via load-proportional sizing.
45//
46// Returns 0-ptr on bad input. Otherwise returns a fully-populated
47// NxSupportPlan that can be passed to nx_support_plan_emit_layer in
48// the main slice pipeline.
49
50func nx_slice_pipe_build_supports(mesh: *NxMesh,
51 bvh: *NxBvh,
52 material: *NxMaterialProfile,
53 layer_height_q14: i64,
54 overhang_tol_q14: i64,
55 footprint_q14: i64,
56 spacing_q14: i64) -> *NxSupportPlan {
57 if (mesh as i64) == 0 { return 0 as *NxSupportPlan }
58 if mesh.n_tris <= 0 { return 0 as *NxSupportPlan }
59 if (bvh as i64) == 0 { return 0 as *NxSupportPlan }
60 if (material as i64) == 0 { return 0 as *NxSupportPlan }
61 if layer_height_q14 <= 0 { return 0 as *NxSupportPlan }
62
63 let bbox: *NxMeshBBox = nx_mesh_bbox_compute(mesh)
64 if (bbox as i64) == 0 { return 0 as *NxSupportPlan }
65 if bbox.valid != 1 { return 0 as *NxSupportPlan }
66
67 // Plan capacity estimate: at most ~16 overhang verts per layer
68 // (typical model has a handful). Clamp to sane bounds.
69 let bbox_z_span: i64 = bbox.max_z - bbox.min_z
70 let n_layers_est: i64 = bbox_z_span / layer_height_q14
71 var plan_cap: i64 = n_layers_est * 4
72 if plan_cap < 32 { plan_cap = 32 }
73 if plan_cap > 4096 { plan_cap = 4096 }
74
75 let plan: *NxSupportPlan = nx_support_plan_new(plan_cap, footprint_q14, spacing_q14)
76 if (plan as i64) == 0 { return 0 as *NxSupportPlan }
77
78 let sp: *NxSupportPoints = nx_supports_new(1024)
79
80 // Walk layers from bottom to top.
81 //
82 // v1.1 (2026-05-20): cache last_nonempty_prev across coplanar-
83 // slice gaps. When a slice plane is coplanar with a mesh face
84 // (e.g., column-top and cap-bottom at the same Z in a mushroom),
85 // nx_slice_plane produces zero segments and contour-build
86 // returns zero contours. v1 treated this as "prev_contours is
87 // empty -> cur is bed-supported" which incorrectly missed real
88 // overhangs at the layer just past the coplanar slice.
89 //
90 // Fix: track last_nonempty_prev separately; use it for overhang
91 // comparison when prev_contours is empty but we have established
92 // history above the bed. (Documented in mushroom-overhang test
93 // commit prior to this one.)
94 var prev_contours: *NxSliceContours = 0 as *NxSliceContours
95 var last_nonempty_prev: *NxSliceContours = 0 as *NxSliceContours
96 var layer_idx: i64 = 0
97 var z_q14: i64 = bbox.min_z + layer_height_q14
98 while z_q14 <= bbox.max_z {
99 let soup: *NxSliceSoup = nx_slice_plane(mesh, bvh, z_q14)
100 let cur_contours: *NxSliceContours = nx_slice_contour_build(soup)
101
102 // Choose effective_prev: prefer current prev_contours; fall
103 // back to last_nonempty_prev when prev was empty (coplanar
104 // slice gap).
105 var effective_prev: *NxSliceContours = prev_contours
106 if (effective_prev as i64) != 0 {
107 if effective_prev.n_polys == 0 {
108 effective_prev = last_nonempty_prev
109 }
110 }
111 else {
112 effective_prev = last_nonempty_prev
113 }
114
115 if (effective_prev as i64) != 0 {
116 if effective_prev.n_polys > 0 {
117 // v1 pairing: match contours by index (single-island
118 // correct; multi-island best-effort). v2 centroid-
119 // tracking queued.
120 var ci: i64 = 0
121 while ci < cur_contours.n_polys {
122 let cur_poly: *NxPolygon = nx_slice_contours_get(cur_contours, ci)
123 var prev_poly: *NxPolygon = 0 as *NxPolygon
124 if ci < effective_prev.n_polys {
125 prev_poly = nx_slice_contours_get(effective_prev, ci)
126 }
127 if (cur_poly as i64) != 0 {
128 let n_before: i64 = sp.n
129 nx_supports_layer_overhang(prev_poly, cur_poly,
130 overhang_tol_q14, sp)
131 var vi: i64 = n_before
132 while vi < sp.n {
133 nx_support_plan_add_pillar_physics(plan,
134 sp.xs[vi], sp.ys[vi], z_q14,
135 NX_AUTO_SUPPORT_DEFAULT_MASS_Q14,
136 NX_AUTO_SUPPORT_DEFAULT_ARM_Q14,
137 material)
138 vi = vi + 1
139 }
140 }
141 ci = ci + 1
142 }
143 }
144 }
145
146 // Update last_nonempty_prev ONLY if cur has content.
147 if (cur_contours as i64) != 0 {
148 if cur_contours.n_polys > 0 {
149 last_nonempty_prev = cur_contours
150 }
151 }
152 prev_contours = cur_contours
153 layer_idx = layer_idx + 1
154 z_q14 = bbox.min_z + (layer_idx + 1) * layer_height_q14
155 }
156
157 return plan
158}