nx_csg_scene.nx source
↩ module page · 264 lines · 10583 B
1// nx_csg_scene.nx -- N-PRIMITIVE CSG. nx_csg_extract is hardwired to TWO primitives
2// (a op b); real parts need many features folded into ONE watertight solid -- a toilet-
3// cleaner head is plate MINUS pocket MINUS slots UNION neck UNION socket MINUS bore.
4//
5// This GROWS the capability by COMPOSING what already exists, adding no new geometry math:
6// * the boolean algebra is the EXISTING nx_csg_combine_k (min/max + smooth-min fillet)
7// * the surface mesher is the EXISTING marching-tetrahedra core (mt_tet, imported)
8// * per-primitive ROTATION (about an arbitrary pivot) is the EXISTING nx_trig (cos/sin),
9// applied by inverse-rotating the query point -- nx_sdf stays untouched
10// * the only new thing is a LEFT-FOLD of the field over a list of (prim, op, fillet-k):
11// field = combine_k(op[1], eval(p0), eval(p1), k[1]) ... combine_k(op[n-1], ., eval(p_{n-1}), k[n-1])
12// prim[0] is the seed (its op/k ignored).
13//
14// Robust by construction: SDF booleans can never fail/self-intersect the way B-rep does,
15// and the marching grid is shared so the result is watertight by the same canonical-edge
16// argument as nx_csg_extract. Output is a pipeline-native Q14-mm NxMesh -> nx_stl_write_mesh
17// -> slicer -> G-code, exactly like every other sovereign solid.
18//
19// NOTE (tech-debt, logged not hidden): the grid+marching loop below is copied from
20// nx_csg_extract with only the 2-prim field swapped for the scene field. The clean
21// refactor is a field-eval callback in nx_csg_extract; deferred until NishiLang fn-ptr
22// support is confirmed. license_tier: ORIGINAL
23
24import "nx_syscalls.nx"
25import "nx_mesh.nx"
26import "nx_sdf.nx"
27import "nx_csg.nx"
28import "nx_trig.nx"
29const NX_MAGIC_1024: i64 = 1024
30const NX_MAGIC_65536: i64 = 65536
31
32// rotation axes (v1 implements Y -- the hook tilt; X/Z are easy follow-ons)
33const NX_ROT_NONE: i64 = 0
34const NX_ROT_X: i64 = 1
35const NX_ROT_Y: i64 = 2
36const NX_ROT_Z: i64 = 3
37
38// A scene applies its primitives left-to-right. prim[0] seeds the field; for i>=1 the
39// field so far is combined with eval(prim[i]) under ops[i] (fillet radius ks[i], 0=sharp).
40// Each prim may carry a rotation: axis rax[i] about pivot (rpx,rpy,rpz)[i] by rang[i]
41// (Q10 turn; 1024 = full circle). rax=0 -> axis-aligned (no rotation).
42struct NxCsgScene {
43 n: i64,
44 cap: i64,
45 prims: *i64, // n entries, each an (NxSdfPrim*) stored as i64
46 ops: *i64, // n entries (ops[0] unused)
47 ks: *i64, // n entries (ks[0] unused); 0 = sharp boolean, >0 = fillet radius (Q14)
48 rax: *i64, // rotation axis (NX_ROT_*)
49 rang: *i64, // rotation angle (Q10 turn)
50 rpx: *i64, // pivot x (Q14)
51 rpy: *i64, // pivot y (Q14)
52 rpz: *i64, // pivot z (Q14)
53}
54
55func nx_csg_scene_new(cap: i64) -> *NxCsgScene {
56 let s: *NxCsgScene = (sys_mmap(80)) as *NxCsgScene
57 s.n = 0
58 s.cap = cap
59 s.prims = (sys_mmap(cap * 8)) as *i64
60 s.ops = (sys_mmap(cap * 8)) as *i64
61 s.ks = (sys_mmap(cap * 8)) as *i64
62 s.rax = (sys_mmap(cap * 8)) as *i64
63 s.rang = (sys_mmap(cap * 8)) as *i64
64 s.rpx = (sys_mmap(cap * 8)) as *i64
65 s.rpy = (sys_mmap(cap * 8)) as *i64
66 s.rpz = (sys_mmap(cap * 8)) as *i64
67 return s
68}
69
70// add an axis-aligned primitive; returns its index, or -1 if full.
71func nx_csg_scene_add(s: *NxCsgScene, p: *NxSdfPrim, op: i64, k: i64) -> i64 {
72 if s.n >= s.cap { return 0 - 1 }
73 let i: i64 = s.n
74 s.prims[i] = p as i64
75 s.ops[i] = op
76 s.ks[i] = k
77 s.rax[i] = NX_ROT_NONE
78 s.rang[i] = 0
79 s.rpx[i] = 0
80 s.rpy[i] = 0
81 s.rpz[i] = 0
82 s.n = i + 1
83 return i
84}
85
86// add a ROTATED primitive (axis about pivot, angle Q10 turn); returns its index or -1.
87func nx_csg_scene_add_rot(s: *NxCsgScene, p: *NxSdfPrim, op: i64, k: i64,
88 axis: i64, ang_q10: i64, px: i64, py: i64, pz: i64) -> i64 {
89 let i: i64 = nx_csg_scene_add(s, p, op, k)
90 if i < 0 { return i }
91 s.rax[i] = axis
92 s.rang[i] = ang_q10
93 s.rpx[i] = px
94 s.rpy[i] = py
95 s.rpz[i] = pz
96 return i
97}
98
99// signed distance of primitive i at world (x,y,z), applying its rotation if any.
100// Rotating the SHAPE by +ang = inverse-rotating the query point about the pivot.
101func nx_csg_scene_eval_i(s: *NxCsgScene, i: i64, x: i64, y: i64, z: i64) -> i64 {
102 let p: *NxSdfPrim = (s.prims[i]) as *NxSdfPrim
103 let ax: i64 = s.rax[i]
104 if ax == NX_ROT_NONE { return nx_sdf_eval(p, x, y, z) }
105 let c: i64 = nx_cos_turn_q10(s.rang[i])
106 let sn: i64 = nx_sin_turn_q10(s.rang[i])
107 if ax == NX_ROT_Y {
108 let dx: i64 = x - s.rpx[i]
109 let dz: i64 = z - s.rpz[i]
110 let qx: i64 = (dx * c + dz * sn) / NX_MAGIC_1024
111 let qz: i64 = (0 - dx * sn + dz * c) / NX_MAGIC_1024
112 return nx_sdf_eval(p, s.rpx[i] + qx, y, s.rpz[i] + qz)
113 }
114 if ax == NX_ROT_X {
115 let dy: i64 = y - s.rpy[i]
116 let dz: i64 = z - s.rpz[i]
117 let qy: i64 = (dy * c + dz * sn) / NX_MAGIC_1024
118 let qz: i64 = (0 - dy * sn + dz * c) / NX_MAGIC_1024
119 return nx_sdf_eval(p, x, s.rpy[i] + qy, s.rpz[i] + qz)
120 }
121 // NX_ROT_Z
122 let dx2: i64 = x - s.rpx[i]
123 let dy2: i64 = y - s.rpy[i]
124 let qx2: i64 = (dx2 * c + dy2 * sn) / NX_MAGIC_1024
125 let qy2: i64 = (0 - dx2 * sn + dy2 * c) / NX_MAGIC_1024
126 return nx_sdf_eval(p, s.rpx[i] + qx2, s.rpy[i] + qy2, z)
127}
128
129// fold the scene field at (x,y,z).
130func nx_csg_scene_field(s: *NxCsgScene, x: i64, y: i64, z: i64) -> i64 {
131 if s.n <= 0 { return 1 } // empty -> everywhere outside
132 var d: i64 = nx_csg_scene_eval_i(s, 0, x, y, z)
133 var i: i64 = 1
134 while i < s.n {
135 let di: i64 = nx_csg_scene_eval_i(s, i, x, y, z)
136 d = nx_csg_combine_k(s.ops[i], d, di, s.ks[i])
137 i = i + 1
138 }
139 return d
140}
141
142// inside-sample count over a coarse grid (debug / feature-presence proofs).
143func nx_csg_scene_count_inside(s: *NxCsgScene,
144 lox: i64, loy: i64, loz: i64,
145 hix: i64, hiy: i64, hiz: i64, step: i64) -> i64 {
146 var c: i64 = 0
147 var z: i64 = loz
148 while z <= hiz {
149 var y: i64 = loy
150 while y <= hiy {
151 var x: i64 = lox
152 while x <= hix {
153 if nx_csg_scene_field(s, x, y, z) < 0 { c = c + 1 }
154 x = x + step
155 }
156 y = y + step
157 }
158 z = z + step
159 }
160 return c
161}
162
163// Extract the composed scene over the box [lo,hi] at grid resolution res into an NxMesh.
164// Mirrors nx_csg_extract's proven grid + 6-tet (Kuhn) marching loop; the ONLY change is
165// the field is the N-primitive scene fold instead of a 2-primitive boolean.
166func nx_csg_extract_scene(s: *NxCsgScene,
167 lox: i64, loy: i64, loz: i64,
168 hix: i64, hiy: i64, hiz: i64, res: i64) -> *NxMesh {
169 let np: i64 = res + 1
170 let field: *i64 = (sys_mmap(np * np * np * 8)) as *i64
171 var k: i64 = 0
172 while k <= res {
173 let z: i64 = loz + (hiz - loz) * k / res
174 var j: i64 = 0
175 while j <= res {
176 let y: i64 = loy + (hiy - loy) * j / res
177 var ii: i64 = 0
178 while ii <= res {
179 let x: i64 = lox + (hix - lox) * ii / res
180 field[ii + j*np + k*np*np] = nx_csg_scene_field(s, x, y, z)
181 ii = ii + 1
182 }
183 j = j + 1
184 }
185 k = k + 1
186 }
187
188 // 6-tet (Kuhn) decomposition along cube diagonal 0-7 (identical to nx_csg_extract)
189 let tets: *i64 = (sys_mmap(24 * 8)) as *i64
190 tets[0]=0;tets[1]=7;tets[2]=1;tets[3]=3; tets[4]=0;tets[5]=7;tets[6]=3;tets[7]=2
191 tets[8]=0;tets[9]=7;tets[10]=2;tets[11]=6; tets[12]=0;tets[13]=7;tets[14]=6;tets[15]=4
192 tets[16]=0;tets[17]=7;tets[18]=4;tets[19]=5; tets[20]=0;tets[21]=7;tets[22]=5;tets[23]=1
193
194 // generous tri cap (bigger constant than nx_csg_extract: multi-feature parts have
195 // more surface area; a too-small cap would silently DROP tris and break watertightness).
196 let cap: i64 = 48 * res * res + NX_MAGIC_65536
197 let tribuf: *i64 = (sys_mmap(cap * 9 * 8)) as *i64
198 let ntri: *i64 = (sys_mmap(8)) as *i64
199 ntri[0] = 0
200
201 let cp: *i64 = (sys_mmap(8 * 3 * 8)) as *i64
202 let cd: *i64 = (sys_mmap(8 * 8)) as *i64
203 let P: *i64 = (sys_mmap(12 * 8)) as *i64
204 let D: *i64 = (sys_mmap(4 * 8)) as *i64
205 let ins: *i64 = (sys_mmap(4 * 8)) as *i64
206 let outs: *i64 = (sys_mmap(4 * 8)) as *i64
207 let q0: *i64 = (sys_mmap(24)) as *i64
208 let q1: *i64 = (sys_mmap(24)) as *i64
209 let q2: *i64 = (sys_mmap(24)) as *i64
210 let q3: *i64 = (sys_mmap(24)) as *i64
211
212 var ck: i64 = 0
213 while ck < res {
214 var cj: i64 = 0
215 while cj < res {
216 var ci: i64 = 0
217 while ci < res {
218 var c: i64 = 0
219 while c < 8 {
220 let gi: i64 = ci + (c & 1)
221 let gj: i64 = cj + ((c >> 1) & 1)
222 let gk: i64 = ck + ((c >> 2) & 1)
223 cp[c*3+0] = lox + (hix - lox) * gi / res
224 cp[c*3+1] = loy + (hiy - loy) * gj / res
225 cp[c*3+2] = loz + (hiz - loz) * gk / res
226 cd[c] = field[gi + gj*np + gk*np*np]
227 c = c + 1
228 }
229 var t: i64 = 0
230 while t < 6 {
231 var v: i64 = 0
232 while v < 4 {
233 let corner: i64 = tets[t*4+v]
234 P[v*3+0] = cp[corner*3+0]
235 P[v*3+1] = cp[corner*3+1]
236 P[v*3+2] = cp[corner*3+2]
237 D[v] = cd[corner]
238 v = v + 1
239 }
240 mt_tet(tribuf, ntri, cap, P, D, ins, outs, q0, q1, q2, q3)
241 t = t + 1
242 }
243 ci = ci + 1
244 }
245 cj = cj + 1
246 }
247 ck = ck + 1
248 }
249
250 let nt: i64 = ntri[0]
251 if nt <= 0 { return 0 as *NxMesh }
252 let m: *NxMesh = nx_mesh_alloc(nt * 3, nt, 0)
253 if (m as i64) == 0 { return m }
254 var t2: i64 = 0
255 while t2 < nt {
256 let base: i64 = t2 * 9
257 nx_mesh_set_vertex(m, t2*3+0, tribuf[base+0], tribuf[base+1], tribuf[base+2], 0)
258 nx_mesh_set_vertex(m, t2*3+1, tribuf[base+3], tribuf[base+4], tribuf[base+5], 0)
259 nx_mesh_set_vertex(m, t2*3+2, tribuf[base+6], tribuf[base+7], tribuf[base+8], 0)
260 nx_mesh_set_triangle(m, t2, t2*3+0, t2*3+1, t2*3+2)
261 t2 = t2 + 1
262 }
263 return m
264}