code wiki / (root) / nx_csg.nx

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}