code wiki / _hdl_build / nx_fab_slice.nx
nx_fab_slice.nx source
↩ module page · 155 lines · 6774 B
1// nx_fab_slice.nx -- L0 FAB SUBSTRATE, rung 1 of the electronics-fab arc (hardware layer up):
2// binary STL in -> fixed-point micrometer geometry -> z-plane slice -> G-code out. This is the team's
3// first machine-DRIVER capability (charter layer-0: deterministic + a machine driver -> real outcome):
4// G-code is the wire format every FDM printer motion controller (Marlin/Klipper class) consumes.
5// LAWS HONORED: struct-free (NxDirent law), integer-only fixed point (1 unit = 1 micrometer, so 1.000mm
6// = 1000), no variable shifts (pow2 by loop), exact-integer KATs in nx_fab_slice_test.nx.
7// HONEST SCOPE: slices ONE triangle per call + emits a single polyline; contour chaining across a full
8// mesh, perimeter offsetting, infill, and the C-oracle differential are the documented NEXT rungs --
9// flagged, not faked. license_tier: ORIGINAL
10import "nx_syscalls.nx"
11const FAB_MAGIC_65536: i64 = 65536
12const FAB_MAGIC_16777216: i64 = 16777216
13const FAB_MAGIC_2147483648: i64 = 2147483648
14const FAB_MAGIC_8388608: i64 = 8388608
15
16// f32 decode verdicts/clamps (structured, not magic ints buried in logic)
17const FAB_UM_BAD: i64 = -888888888888888 // NaN/Inf input -> caller must reject (defensive at the boundary)
18const FAB_UM_MAX: i64 = 888888888888888 // clamp for absurd-magnitude floats (no silent overflow)
19
20// 2^k by loop -- the no-variable-shift law
21func fab_pow2(k: i64) -> i64 { var p: i64 = 1; var i: i64 = 0; while i < k { p = p * 2; i = i + 1 } return p }
22
23// little-endian u32 read (STL is LE by spec)
24func fab_u32le(b: *u8, off: i64) -> i64 {
25 var v: i64 = b[off] as i64
26 v = v + (b[off+1] as i64) * 256
27 v = v + (b[off+2] as i64) * FAB_MAGIC_65536
28 v = v + (b[off+3] as i64) * FAB_MAGIC_16777216
29 return v
30}
31
32// signed division, round-to-nearest, half away from zero -- the slice interpolator's exactness rule
33func fab_div_round(num: i64, den: i64) -> i64 {
34 var n: i64 = num; var d: i64 = den; var sg: i64 = 1
35 if n < 0 { n = 0 - n; sg = 0 - sg }
36 if d < 0 { d = 0 - d; sg = 0 - sg }
37 let q: i64 = (n + d/2) / d
38 return sg * q
39}
40
41// IEEE-754 binary32 bit pattern -> micrometers (value_mm * 1000), round-to-nearest.
42// Integer-only: value = (-1)^s * (m|2^23) * 2^(e-150); um = that * 1000.
43func ff32_to_um(bits: i64) -> i64 {
44 let s: i64 = (bits / FAB_MAGIC_2147483648) % 2
45 let e: i64 = (bits / FAB_MAGIC_8388608) % 256
46 let m: i64 = bits % FAB_MAGIC_8388608
47 if e == 255 { return FAB_UM_BAD } // NaN/Inf -> reject at the boundary
48 if e == 0 { return 0 } // zero + denormals (< 1.2e-38 mm) -> 0 um
49 let p1000: i64 = (m + FAB_MAGIC_8388608) * 1000 // (mantissa with implicit bit) * scale; < 2^34
50 let sh: i64 = e - 150
51 var v: i64 = 0
52 if sh >= 0 {
53 if sh > 28 { v = FAB_UM_MAX } // p1000*2^sh would pass 2^62 -> clamp, never wrap
54 if sh <= 28 { v = p1000 * fab_pow2(sh) }
55 }
56 if sh < 0 {
57 let k: i64 = 0 - sh
58 if k > 40 { v = 0 } // below half a micrometer for any mantissa
59 if k <= 40 { let d: i64 = fab_pow2(k); v = (p1000 + d/2) / d }
60 }
61 if s == 1 { v = 0 - v }
62 return v
63}
64
65// ---- binary STL (80-byte header, u32 tri count, 50 bytes/tri: 12B normal + 3 verts * 12B + u16 attr) ----
66
67func fstl_tri_count(b: *u8) -> i64 { return fab_u32le(b, 80) }
68
69// vertex coordinate in micrometers: triangle t, vertex v (0..2), axis ax (0=x,1=y,2=z)
70func fstl_vert_um(b: *u8, t: i64, v: i64, ax: i64) -> i64 {
71 let off: i64 = 84 + t*50 + 12 + v*12 + ax*4
72 return ff32_to_um(fab_u32le(b, off))
73}
74
75// load triangle t's 9 coordinates (x0 y0 z0 x1 y1 z1 x2 y2 z2, micrometers) into out
76func fstl_tri_um(b: *u8, t: i64, out: *i64) -> i64 {
77 var v: i64 = 0
78 while v < 3 {
79 var ax: i64 = 0
80 while ax < 3 { out[v*3+ax] = fstl_vert_um(b, t, v, ax); ax = ax + 1 }
81 v = v + 1
82 }
83 return 9
84}
85
86// ---- slice: triangle x z-plane -> 0 or 2 intersection points ----
87// tri = 9 coords as above; zc = cut height (um); out receives x,y pairs (um).
88// Edge-crossing convention is HALF-OPEN (za <= zc < zb or zb <= zc < za) so a vertex exactly on the
89// plane is counted on exactly one of its two edges -- no double-hit, no miss. A face lying flat in the
90// plane yields 0 (degenerate by convention). Returns the number of points written (0 or 2).
91func fslice_tri(tri: *i64, zc: i64, out: *i64) -> i64 {
92 var np: i64 = 0
93 var i: i64 = 0
94 while i < 3 {
95 let j: i64 = (i + 1) % 3
96 let za: i64 = tri[i*3+2]
97 let zb: i64 = tri[j*3+2]
98 var cross: i64 = 0
99 if za <= zc { if zb > zc { cross = 1 } }
100 if zb <= zc { if za > zc { cross = 1 } }
101 if cross == 1 {
102 let xa: i64 = tri[i*3]; let ya: i64 = tri[i*3+1]
103 let xb: i64 = tri[j*3]; let yb: i64 = tri[j*3+1]
104 out[np*2] = xa + fab_div_round((xb - xa) * (zc - za), zb - za)
105 out[np*2+1] = ya + fab_div_round((yb - ya) * (zc - za), zb - za)
106 np = np + 1
107 }
108 i = i + 1
109 }
110 return np
111}
112
113// ---- G-code emit: polyline of n points (um pairs) -> "G0 X..mm Y..mm" then "G1 ..." moves ----
114
115func fgc_puts(out: *u8, off: i64, s: *u8) -> i64 {
116 var i: i64 = 0
117 while s[i] != (0 as u8) { out[off+i] = s[i]; i = i + 1 }
118 return off + i
119}
120
121// micrometers -> "mmm.fff" decimal millimeters (3 fixed decimals = full um precision, the G-code idiom)
122func fgc_putd(out: *u8, o: i64, vin: i64) -> i64 {
123 var off: i64 = o
124 var v: i64 = vin
125 if v < 0 { out[off] = 45 as u8; off = off + 1; v = 0 - v }
126 let mm: i64 = v / 1000
127 let fr: i64 = v % 1000
128 let t: *u8 = sys_mmap(24); var k: i64 = 0; var m: i64 = mm
129 if m == 0 { t[0] = 48 as u8; k = 1 }
130 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
131 var i: i64 = 0
132 while i < k { out[off] = t[k-1-i]; off = off + 1; i = i + 1 }
133 out[off] = 46 as u8; off = off + 1
134 out[off] = (48 + (fr / 100) % 10) as u8; off = off + 1
135 out[off] = (48 + (fr / 10) % 10) as u8; off = off + 1
136 out[off] = (48 + fr % 10) as u8; off = off + 1
137 return off
138}
139
140// emit the polyline; returns byte length written. First point = travel (G0), rest = moves (G1).
141// Extrusion (E) and feed (F) words are the toolpath rung's job -- geometry first, honestly scoped.
142func fgcode_emit(pts: *i64, n: i64, out: *u8) -> i64 {
143 var off: i64 = 0
144 var i: i64 = 0
145 while i < n {
146 if i == 0 { off = fgc_puts(out, off, "G0 X" as *u8) }
147 if i > 0 { off = fgc_puts(out, off, "G1 X" as *u8) }
148 off = fgc_putd(out, off, pts[i*2])
149 off = fgc_puts(out, off, " Y" as *u8)
150 off = fgc_putd(out, off, pts[i*2+1])
151 out[off] = 10 as u8; off = off + 1
152 i = i + 1
153 }
154 return off
155}