nx_csg.nx source
↩ module page · 245 lines · 10833 B
1// nx_csg.nx -- CSG solid -> watertight mesh via marching tetrahedra over an SDF.
2//
3// Samples a two-primitive boolean field (nx_sdf) on a grid, splits every cube into
4// 6 tetrahedra (Kuhn subdivision along the 0-7 diagonal -> shared faces split
5// identically, no cracks), and emits the zero-isosurface. Edge crossings are
6// computed in a CANONICAL endpoint order so a grid edge shared by two tets yields
7// the IDENTICAL point (watertight, the same fix the slicer needed). Triangle
8// winding is forced outward via an inside-reference test. Output is a pipeline-
9// native Q14-mm NxMesh -> flows to nx_stl_write_mesh -> slicer, like every other
10// solid. Robust by construction: SDF booleans never fail the way B-rep does.
11// license_tier: ORIGINAL
12
13import "nx_syscalls.nx"
14import "nx_mesh.nx"
15import "nx_sdf.nx"
16
17// canonical edge crossing: order endpoints by (z,y,x) so both tets sharing this
18// grid edge interpolate the SAME point. out[0..2] = crossing (Q14).
19func mt_cross(out: *i64, P: *i64, i: i64, j: i64, D: *i64) -> i64 {
20 var xi: i64 = P[i*3+0]; var yi: i64 = P[i*3+1]; var zi: i64 = P[i*3+2]; var di: i64 = D[i]
21 var xj: i64 = P[j*3+0]; var yj: i64 = P[j*3+1]; var zj: i64 = P[j*3+2]; var dj: i64 = D[j]
22 var swap: i64 = 0
23 if zj < zi { swap = 1 }
24 if zj == zi { if yj < yi { swap = 1 } }
25 if zj == zi { if yj == yi { if xj < xi { swap = 1 } } }
26 if swap == 1 {
27 let tx: i64 = xi; let ty: i64 = yi; let tz: i64 = zi; let td: i64 = di
28 xi = xj; yi = yj; zi = zj; di = dj
29 xj = tx; yj = ty; zj = tz; dj = td
30 }
31 let den: i64 = di - dj
32 out[0] = xi + (xj - xi) * di / den
33 out[1] = yi + (yj - yi) * di / den
34 out[2] = zi + (zj - zi) * di / den
35 return 0
36}
37
38// append a triangle, flipping winding so its normal points AWAY from (rx,ry,rz)
39// (a point inside the solid) -> outward-facing.
40func mt_emit(tribuf: *i64, ntri: *i64, cap: i64, p0: *i64, p1: *i64, p2: *i64,
41 rx: i64, ry: i64, rz: i64) -> i64 {
42 if ntri[0] >= cap { return 0 }
43 let ux: i64 = p1[0]-p0[0]; let uy: i64 = p1[1]-p0[1]; let uz: i64 = p1[2]-p0[2]
44 let vx: i64 = p2[0]-p0[0]; let vy: i64 = p2[1]-p0[1]; let vz: i64 = p2[2]-p0[2]
45 let nx2: i64 = uy*vz - uz*vy
46 let ny: i64 = uz*vx - ux*vz
47 let nz: i64 = ux*vy - uy*vx
48 let cxx: i64 = (p0[0]+p1[0]+p2[0])/3
49 let cyy: i64 = (p0[1]+p1[1]+p2[1])/3
50 let czz: i64 = (p0[2]+p1[2]+p2[2])/3
51 let dot: i64 = nx2*(cxx-rx) + ny*(cyy-ry) + nz*(czz-rz)
52 let base: i64 = ntri[0]*9
53 tribuf[base+0]=p0[0]; tribuf[base+1]=p0[1]; tribuf[base+2]=p0[2]
54 if dot >= 0 {
55 tribuf[base+3]=p1[0]; tribuf[base+4]=p1[1]; tribuf[base+5]=p1[2]
56 tribuf[base+6]=p2[0]; tribuf[base+7]=p2[1]; tribuf[base+8]=p2[2]
57 } else {
58 tribuf[base+3]=p2[0]; tribuf[base+4]=p2[1]; tribuf[base+5]=p2[2]
59 tribuf[base+6]=p1[0]; tribuf[base+7]=p1[1]; tribuf[base+8]=p1[2]
60 }
61 ntri[0] = ntri[0] + 1
62 return 0
63}
64
65// march one tetrahedron (P = 4 corners flat [12], D = 4 field values). ins/outs/
66// c0..c3 are caller-owned scratch (avoids per-tet mmap storms).
67func mt_tet(tribuf: *i64, ntri: *i64, cap: i64, P: *i64, D: *i64,
68 ins: *i64, outs: *i64, c0: *i64, c1: *i64, c2: *i64, c3: *i64) -> i64 {
69 var ni: i64 = 0; var no: i64 = 0
70 var i: i64 = 0
71 while i < 4 {
72 if D[i] < 0 { ins[ni] = i; ni = ni + 1 } else { outs[no] = i; no = no + 1 }
73 i = i + 1
74 }
75 if ni == 0 { return 0 }
76 if ni == 4 { return 0 }
77 if ni == 1 {
78 let a: i64 = ins[0]
79 mt_cross(c0, P, a, outs[0], D); mt_cross(c1, P, a, outs[1], D); mt_cross(c2, P, a, outs[2], D)
80 mt_emit(tribuf, ntri, cap, c0, c1, c2, P[a*3+0], P[a*3+1], P[a*3+2])
81 }
82 if ni == 3 {
83 let d: i64 = outs[0]
84 mt_cross(c0, P, d, ins[0], D); mt_cross(c1, P, d, ins[1], D); mt_cross(c2, P, d, ins[2], D)
85 let rx: i64 = (P[ins[0]*3+0]+P[ins[1]*3+0]+P[ins[2]*3+0])/3
86 let ry: i64 = (P[ins[0]*3+1]+P[ins[1]*3+1]+P[ins[2]*3+1])/3
87 let rz: i64 = (P[ins[0]*3+2]+P[ins[1]*3+2]+P[ins[2]*3+2])/3
88 mt_emit(tribuf, ntri, cap, c0, c1, c2, rx, ry, rz)
89 }
90 if ni == 2 {
91 let a: i64 = ins[0]; let b: i64 = ins[1]; let cc: i64 = outs[0]; let dd: i64 = outs[1]
92 mt_cross(c0, P, a, cc, D) // a-c
93 mt_cross(c1, P, b, cc, D) // b-c
94 mt_cross(c2, P, b, dd, D) // b-d
95 mt_cross(c3, P, a, dd, D) // a-d
96 let rx: i64 = (P[a*3+0]+P[b*3+0])/2
97 let ry: i64 = (P[a*3+1]+P[b*3+1])/2
98 let rz: i64 = (P[a*3+2]+P[b*3+2])/2
99 mt_emit(tribuf, ntri, cap, c0, c1, c2, rx, ry, rz) // ac, bc, bd
100 mt_emit(tribuf, ntri, cap, c0, c2, c3, rx, ry, rz) // ac, bd, ad
101 }
102 return 0
103}
104
105// Extract the boolean (a op b) solid over the box [lo,hi] at grid resolution res.
106// k_q14 > 0 rounds the junction (fillet); k_q14 = 0 is a sharp boolean.
107func nx_csg_extract(a: *NxSdfPrim, b: *NxSdfPrim, op: i64,
108 lox: i64, loy: i64, loz: i64,
109 hix: i64, hiy: i64, hiz: i64, res: i64, k_q14: i64) -> *NxMesh {
110 let np: i64 = res + 1
111 let field: *i64 = (sys_mmap(np * np * np * 8)) as *i64
112 var k: i64 = 0
113 while k <= res {
114 let z: i64 = loz + (hiz - loz) * k / res
115 var j: i64 = 0
116 while j <= res {
117 let y: i64 = loy + (hiy - loy) * j / res
118 var ii: i64 = 0
119 while ii <= res {
120 let x: i64 = lox + (hix - lox) * ii / res
121 field[ii + j*np + k*np*np] = nx_csg_field_k(a, b, op, k_q14, x, y, z)
122 ii = ii + 1
123 }
124 j = j + 1
125 }
126 k = k + 1
127 }
128
129 // 6-tet (Kuhn) decomposition along cube diagonal 0-7
130 let tets: *i64 = (sys_mmap(24 * 8)) as *i64
131 tets[0]=0;tets[1]=7;tets[2]=1;tets[3]=3; tets[4]=0;tets[5]=7;tets[6]=3;tets[7]=2
132 tets[8]=0;tets[9]=7;tets[10]=2;tets[11]=6; tets[12]=0;tets[13]=7;tets[14]=6;tets[15]=4
133 tets[16]=0;tets[17]=7;tets[18]=4;tets[19]=5; tets[20]=0;tets[21]=7;tets[22]=5;tets[23]=1
134
135 let cap: i64 = 48 * res * res + 4096
136 let tribuf: *i64 = (sys_mmap(cap * 9 * 8)) as *i64
137 let ntri: *i64 = (sys_mmap(8)) as *i64
138 ntri[0] = 0
139
140 // scratch (allocate once)
141 let cp: *i64 = (sys_mmap(8 * 3 * 8)) as *i64 // 8 corner positions
142 let cd: *i64 = (sys_mmap(8 * 8)) as *i64 // 8 corner field values
143 let P: *i64 = (sys_mmap(12 * 8)) as *i64
144 let D: *i64 = (sys_mmap(4 * 8)) as *i64
145 let ins: *i64 = (sys_mmap(4 * 8)) as *i64
146 let outs: *i64 = (sys_mmap(4 * 8)) as *i64
147 let q0: *i64 = (sys_mmap(24)) as *i64
148 let q1: *i64 = (sys_mmap(24)) as *i64
149 let q2: *i64 = (sys_mmap(24)) as *i64
150 let q3: *i64 = (sys_mmap(24)) as *i64
151
152 var ck: i64 = 0
153 while ck < res {
154 var cj: i64 = 0
155 while cj < res {
156 var ci: i64 = 0
157 while ci < res {
158 // gather the cube's 8 corners
159 var c: i64 = 0
160 while c < 8 {
161 let gi: i64 = ci + (c & 1)
162 let gj: i64 = cj + ((c >> 1) & 1)
163 let gk: i64 = ck + ((c >> 2) & 1)
164 cp[c*3+0] = lox + (hix - lox) * gi / res
165 cp[c*3+1] = loy + (hiy - loy) * gj / res
166 cp[c*3+2] = loz + (hiz - loz) * gk / res
167 cd[c] = field[gi + gj*np + gk*np*np]
168 c = c + 1
169 }
170 // march the 6 tets
171 var t: i64 = 0
172 while t < 6 {
173 var v: i64 = 0
174 while v < 4 {
175 let corner: i64 = tets[t*4+v]
176 P[v*3+0] = cp[corner*3+0]
177 P[v*3+1] = cp[corner*3+1]
178 P[v*3+2] = cp[corner*3+2]
179 D[v] = cd[corner]
180 v = v + 1
181 }
182 mt_tet(tribuf, ntri, cap, P, D, ins, outs, q0, q1, q2, q3)
183 t = t + 1
184 }
185 ci = ci + 1
186 }
187 cj = cj + 1
188 }
189 ck = ck + 1
190 }
191
192 let nt: i64 = ntri[0]
193 if nt <= 0 { return 0 as *NxMesh }
194 let m: *NxMesh = nx_mesh_alloc(nt * 3, nt, 0)
195 if (m as i64) == 0 { return m }
196 var t2: i64 = 0
197 while t2 < nt {
198 let base: i64 = t2 * 9
199 nx_mesh_set_vertex(m, t2*3+0, tribuf[base+0], tribuf[base+1], tribuf[base+2], 0)
200 nx_mesh_set_vertex(m, t2*3+1, tribuf[base+3], tribuf[base+4], tribuf[base+5], 0)
201 nx_mesh_set_vertex(m, t2*3+2, tribuf[base+6], tribuf[base+7], tribuf[base+8], 0)
202 nx_mesh_set_triangle(m, t2, t2*3+0, t2*3+1, t2*3+2)
203 t2 = t2 + 1
204 }
205 return m
206}
207
208// convenience: box (half hx,hy,hz at origin) MINUS a Z-cylinder (radius r) through it.
209func nx_csg_box_minus_cyl(hx: i64, hy: i64, hz: i64, r: i64, res: i64) -> *NxMesh {
210 let box: *NxSdfPrim = nx_sdf_make(NX_SDF_BOX, 0, 0, 0, hx, hy, hz)
211 let cyl: *NxSdfPrim = nx_sdf_make(NX_SDF_CYL, 0, 0, 0, r, hz + 16384, 0) // taller than box
212 let pad: i64 = 16384 // 1mm padding so the surface isn't clipped at the grid edge
213 return nx_csg_extract(box, cyl, NX_CSG_DIFF,
214 0 - hx - pad, 0 - hy - pad, 0 - hz - pad,
215 hx + pad, hy + pad, hz + pad, res, 0)
216}
217
218// convenience: a rounded box of overall half-extent H with corner radius r.
219// (Face centres reach +/-H; edges/corners are rounded by r.)
220func nx_csg_roundbox_mesh(H: i64, r: i64, res: i64) -> *NxMesh {
221 let rb: *NxSdfPrim = nx_sdf_make4(NX_SDF_ROUNDBOX, 0, 0, 0, H - r, H - r, H - r, r)
222 let e: i64 = H + 16384
223 return nx_csg_extract(rb, rb, NX_CSG_UNION, 0 - e, 0 - e, 0 - e, e, e, e, res, 0)
224}
225
226// convenience: a single torus (major R, minor r) meshed via marching tetrahedra.
227// (union with itself is idempotent, so we reuse the two-primitive extractor.)
228func nx_csg_torus_mesh(R: i64, r: i64, res: i64) -> *NxMesh {
229 let t: *NxSdfPrim = nx_sdf_make(NX_SDF_TORUS, 0, 0, 0, R, r, 0)
230 let ext: i64 = R + r + 16384
231 let zext: i64 = r + 16384
232 return nx_csg_extract(t, t, NX_CSG_UNION, 0 - ext, 0 - ext, 0 - zext, ext, ext, zext, res, 0)
233}
234
235// convenience: smooth (filleted) union of two spheres of radius r, centres at
236// x = +/- sep on the X axis, blended over fillet radius k_q14. Demonstrates the
237// SDF fillet -- the spheres merge with a smooth neck no B-rep edge-surgery needed.
238func nx_csg_two_sphere_fillet(r: i64, sep: i64, k: i64, res: i64) -> *NxMesh {
239 let sa: *NxSdfPrim = nx_sdf_make(NX_SDF_SPHERE, 0 - sep, 0, 0, r, 0, 0)
240 let sb: *NxSdfPrim = nx_sdf_make(NX_SDF_SPHERE, sep, 0, 0, r, 0, 0)
241 let pad: i64 = k + 16384
242 let ex: i64 = sep + r + pad
243 let ey: i64 = r + pad
244 return nx_csg_extract(sa, sb, NX_CSG_UNION, 0 - ex, 0 - ey, 0 - ey, ex, ey, ey, res, k)
245}