code wiki / (root) / nx_partloft_lib.nx

nx_partloft_lib.nx source

↩ module page · 1025 lines · 47789 B

1// nx_partloft_lib.nx -- THE LOFTED PART: Infinigen's NurbsPart, replicated in NishiLang from the first byte up 2// (procgen PG59 / anatomy AN19, 2026-09-19). Operator standing order the same day: the estate uses the best-in-class 3// published method as its BASELINE and improves it piece by piece; the ellipsoid-union skull is retired. This file is 4// the first piece. ONE part = a skeleton polyline with a normalised profile at every station, DECOMPOSED into the genes 5// the Infinigen creature system mutates, RECOMPOSED into a grid of NURBS control handles, EVALUATED as a cubic B-spline 6// surface, and WRITTEN as NXMSH2 through the estate's one mesh layout (nx_nxmesh_lib). Read from the primary source, 7// mirrored under knowledge/fetched/ and pinned on procgen.refs: 8// infinigen/assets/creatures/parts/generic_nurbs.py decompose_nurbs_handles / recompose_nurbs_handles / 9// NurbsPart.sample_params (the gene noise STRUCTURE) 10// infinigen/assets/creatures/util/geometry/lofting.py factorize_nurbs_handles, skeleton_to_tangents, 11// compute_profile_verts (frames by rotate_match_directions) 12// infinigen/assets/creatures/util/geometry/nurbs.py geomdl_nurbs: degree (3,3), clamped uniform u knots, cyclic v 13// wrapping three control points, sample_size from face_size 14// infinigen/assets/creatures/util/part_util.py nurbs_to_part: the attachment skeleton is the INTERIOR rows 15// infinigen/assets/creatures/util/creature.py apply_attach_transform: (u, v, rad) surface coordinates 16// CONVENTIONS, THEIRS: a part runs along +x (profiles face +x), y is lateral, z is up; skeleton_yoffs is the lateral 17// drift per segment and thetas the pitch in the xz plane (degrees there, VM_ONE radians here). 18// UNITS, OURS, and there are exactly three: POSITIONS in tenths of a millimetre (u10, NM_UNIT_PER_M, the NXMSH2 19// ruler's unit); FRACTIONS in PL_FQ = 65536 (Q16) -- gene proportions, lateral drifts, station parameters, normalised 20// profiles, surface parameters, the attachment coordinates, the gene variance, and the B-spline knots and basis; and 21// ANGLES in VM_ONE radians (Q12), the estate's one trigonometric unit, for the pitch genes and the yaw. A radius is a 22// position with sixteen fractional bits (Q16 u10), because a profile normalised by a radius rounded to 0.1 mm was 23// measured 0.25 percent off at 20 mm and would be 2.5 percent off on a finger: MEASURED on the first run of the gate, 24// where Q12 proportions truncated to whole u10 per segment lost 0.5 percent of a part's length (995 for 1000). Every 25// division that lands on a stored value rounds to nearest (pl_rdiv); truncation is how that half-percent was lost. 26// No float anywhere: the file's float32 is encoded once, at the door, by nx_nxmesh_lib / nx_vecmath. 27// DECLARED DEVIATIONS (each a rung on procgen, never a silent difference): 28// 1. np.random.normal is Irwin-Hall (twelve uniforms of a 31-bit LCG): same mean and variance, tails end at six 29// sigma. The STRUCTURE of the noise is the source's exactly: one size draw, one length draw, one shared radius 30// draw plus one per station, one per proportion, zero-meaned pitch noise, per-column times per-cell profile noise 31// mirrored across the profile. 32// 2. The attachment ray-cast against a BVH is a parametric search: around the requested station the closed profile 33// is searched for the point nearest the ray (least perpendicular distance, in front of the origin), coarse then 34// bisected, over a one-segment window of u. 35// 3. The attachment skeleton is the clamped cubic B-spline of the interior rows, where the source runs two 36// Catmull-Clark levels over the interior polyline (the limit of that subdivision IS the cubic B-spline). 37// 4. The emitted mesh carries two fan caps so the part is a closed solid for the lattice fusion; the source leaves 38// the tube open and lets remesh close it. Winding is made outward per triangle here; the source runs 39// normals_make_consistent. 40// LIB, no main. license_tier: ORIGINAL (a re-implementation from a read of BSD-3 source; no source bytes copied; the 41// original is an OUTSIDE oracle and never in the build path). No hw writes (Rule 26). 42import "nx_syscalls.nx" 43import "nx_vecmath.nx" 44import "nx_nxmesh_lib.nx" 45import "nx_itoa_lib.nx" 46 47const PL_ONE: i64 = 4096 // VM_ONE spelled once here, the ANGLE and unit-vector unit; the gate asserts the two agree 48const PL_FQ: i64 = 65536 // the FRACTION unit (Q16): genes, parameters, knots, basis 49const PL_FQ2: i64 = 4294967296 // PL_FQ squared: the divisor that takes a Q16 profile times a Q16 radius back to u10 50const PL_FQ_SHIFT: i64 = 16 51const PL_FQ_HALF_SHIFT: i64 = 8 // sqrt(x << 16) << 8 = sqrt(x) << 16: a length with sixteen fractional bits 52const PL_DEG: i64 = 3 // degree_u, degree_v = (3, 3): nurbs.py geomdl_nurbs 53const PL_ORD: i64 = 4 // PL_DEG + 1 nonzero basis functions on any span 54const PL_I64: i64 = 8 55const PL_V3: i64 = 3 56const PL_Q4: i64 = 4 57const PL_PART_BYTES: i64 = 256 // PlPart: 21 words, one page-rounded map 58const PL_MIN_RAD: i64 = 65536 // rads clipped to at least one u10 (Q16): np.clip(rads, 1e-3 m, 1e5 m) 59const PL_MIN_NU: i64 = 4 // a clamped cubic needs four control rows 60const PL_MIN_NV: i64 = 3 // and a periodic cubic three columns 61const PL_GAUSS_N: i64 = 12 // Irwin-Hall count: the variance of a sum of n uniforms is n/12, one at twelve 62const PL_LCG_MUL: i64 = 1103515245 // the ANSI C rand() recurrence, a published constant 63const PL_LCG_ADD: i64 = 12345 64const PL_LCG_MOD: i64 = 2147483648 // 2^31: state times multiplier stays under 2^62 65const PL_LCG_SHIFT: i64 = 16 // the low bits of an LCG are its weakest, discarded 66const PL_LCG_BITS: i64 = 32768 // 2^15 output range after the shift 67// THE GENE NOISE of generic_nurbs.py NurbsPart.sample_params, standard deviations in Q16 of the unit value; each is 68// the source's own figure, spelled in this file's unit (0.1 * 65536 = 6554, 0.15 * 65536 = 9830, 0.07 * 65536 = 4587, 69// 7 degrees = 7 * VM_PI / 180 = 500 in VM_ONE radians). 70const PL_SD_SIZE: i64 = 6554 // sz = N(1, 0.1) 71const PL_SD_LENGTH: i64 = 6554 // length *= sz * N(1, 0.1) 72const PL_SD_RADS: i64 = 6554 // rads *= sz * N(1, 0.1) ... 73const PL_SD_RAD_STATION: i64 = 9830 // ... * N(1, 0.15) per station 74const PL_SD_PROP: i64 = 9830 // proportions *= N(1, 0.15) per segment 75const PL_SD_THETA: i64 = 500 // thetas += N(0, 7 deg) per segment, zero-meaned (VM_ONE radians) 76const PL_SD_PROF_COL: i64 = 4587 // profile noise N(1, 0.07) per column ... 77const PL_SD_PROF_CELL: i64 = 9830 // ... times N(1, 0.15) per cell 78const PL_SEARCH_COARSE: i64 = 64 // attachment search: samples around the ring before the bisection 79const PL_SEARCH_REFINE: i64 = 12 // bisection steps after the coarse pass 80const PL_SEARCH_UHALF: i64 = 2 // u window: this many quarter-segments either side of the station 81const PL_SEARCH_UQUART: i64 = 4 // a segment is split in this many steps for the window 82const PL_FD_STEP: i64 = 256 // finite-difference step for tangents and normals, Q16 (1/256 of the domain) 83const PL_MAGIC_0: i64 = 78 // N 84const PL_MAGIC_1: i64 = 88 // X 85const PL_MAGIC_2: i64 = 77 // M 86const PL_MAGIC_3: i64 = 83 // S 87const PL_MAGIC_4: i64 = 72 // H 88const PL_MAGIC_5: i64 = 50 // 2 89const PL_LAYER_START_OFF: i64 = 16 // within a 24-byte layer record: 16 name bytes, then start u32, then count u32 90const PL_LAYER_COUNT_OFF: i64 = 20 91const PL_OFF_NORMALS: i64 = 36 // byte offset of the three per-vertex normals within a triangle record 92const PL_COL_BONE_R: i64 = 900 // the part's default colour, per-mille: a bone tint a viewer renders as bone 93const PL_COL_BONE_G: i64 = 880 94const PL_COL_BONE_B: i64 = 840 95const PL_MIN_RING: i64 = 3 // a cap fan needs three ring points 96const PL_SAVE_DIGITS: i64 = 16 // bytes reserved per integer on save: sign, 14 digits, separator 97const PL_SAVE_HEADER: i64 = 64 98const PL_MODE644: i64 = 420 99const PL_LOAD_HDR: i64 = 16 // the two-word length record sys_read_file fills 100const PL_FMT_VERSION: i64 = 2 // version 2: Q16 fractions and Q16 radii (version 1 was Q12, never shipped) 101const PL_HEADER_INTS: i64 = 7 // version nu nv root(3) length 102const PL_ASKEL_SPLINE: i64 = 1 // pl_askel took the cubic path (four or more interior rows) 103const PL_ASKEL_LINEAR: i64 = 2 // pl_askel took the linear path (fewer) 104const PL_POS_HUGE: i64 = 1000000000000000000 105 106struct PlPart { nu: i64, nv: i64, root: *i64, length: i64, prop: *i64, theta: *i64, yoff: *i64, rad: *i64, ts: *i64, prof: *i64, skel: *i64, tang: *i64, hnd: *i64, ku: *i64, kv: *i64, ka: *i64, bu: *i64, bv: *i64, bl: *i64, br: *i64 } 107 108func pl_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v } 109// division rounded to nearest, b > 0; the truncating form is how the first run lost half a percent of every length 110func pl_rdiv(a: i64, b: i64) -> i64 { if a >= 0 { return (a + b/2) / b } return (a - b/2) / b } 111func pl_len3(x: i64, y: i64, z: i64) -> i64 { return vm_isqrt(x*x + y*y + z*z) } 112// the same length with sixteen fractional bits (Q16 u10) 113func pl_len3_fx(x: i64, y: i64, z: i64) -> i64 { return vm_isqrt((x*x + y*y + z*z) << PL_FQ_SHIFT) << PL_FQ_HALF_SHIFT } 114// unit vector (Q12) of an integer vector in any unit; a null vector yields zeros and returns 0 115func pl_unit(o: *i64, a: *i64) -> i64 { 116 let l: i64 = pl_len3(a[0], a[1], a[2]) 117 if l == 0 { o[0] = 0; o[1] = 0; o[2] = 0; return 0 } 118 o[0] = pl_rdiv(a[0]*PL_ONE, l); o[1] = pl_rdiv(a[1]*PL_ONE, l); o[2] = pl_rdiv(a[2]*PL_ONE, l) 119 return 1 120} 121// a station's radius in whole u10, rounded 122func pl_rad_u10(p: *PlPart, i: i64) -> i64 { return pl_rdiv(p.rad[i], PL_FQ) } 123 124// ---- the knot vectors, nurbs.py generate_knotvector ------------------------------------------------------------ 125// u: clamped uniform over nu control rows, degree 3: four zeros, nu-4 interior knots k/(nu-3), four ones (Q16). 126// v: uniform unclamped over nv columns, k/(nv+3) for k = 0..nv+3, then the first three intervals wrapped past the end 127// (kv[1..3] + kv[-1] - kv[0]), so a periodic cubic over nv+3 wrapped control columns has nv+7 knots. 128// a: the attachment skeleton's clamped knots over the nu-2 interior rows (filled only when four or more exist). 129func pl_knots(p: *PlPart) -> i64 { 130 let nu: i64 = p.nu 131 let nv: i64 = p.nv 132 var i: i64 = 0 133 while i < nu + PL_ORD { 134 var k: i64 = 0 135 if i >= PL_ORD { k = (i - PL_DEG) * PL_FQ / (nu - PL_DEG) } 136 if i >= nu { k = PL_FQ } 137 p.ku[i] = k 138 i = i + 1 139 } 140 i = 0 141 while i < nv + PL_ORD { 142 p.kv[i] = i * PL_FQ / (nv + PL_DEG) 143 i = i + 1 144 } 145 var w: i64 = 1 146 while w <= PL_DEG { 147 p.kv[nv + PL_DEG + w] = PL_FQ + w * PL_FQ / (nv + PL_DEG) 148 w = w + 1 149 } 150 let na: i64 = nu - 2 151 if na >= PL_ORD { 152 i = 0 153 while i < na + PL_ORD { 154 var ka: i64 = 0 155 if i >= PL_ORD { ka = (i - PL_DEG) * PL_FQ / (na - PL_DEG) } 156 if i >= na { ka = PL_FQ } 157 p.ka[i] = ka 158 i = i + 1 159 } 160 } 161 return 0 162} 163 164// a part with nu stations of nv profile points, every array owned by the part; arrays are page-rounded maps 165func pl_new(nu: i64, nv: i64) -> *PlPart { 166 let p: *PlPart = (sys_mmap(PL_PART_BYTES)) as *PlPart 167 p.nu = nu; p.nv = nv; p.length = 0 168 p.root = sys_mmap(PL_V3*PL_I64) as *i64 169 p.prop = sys_mmap(nu*PL_I64) as *i64 170 p.theta = sys_mmap(nu*PL_I64) as *i64 171 p.yoff = sys_mmap(nu*PL_I64) as *i64 172 p.rad = sys_mmap(nu*PL_I64) as *i64 173 p.ts = sys_mmap(nu*PL_I64) as *i64 174 p.prof = sys_mmap(nu*nv*PL_V3*PL_I64) as *i64 175 p.skel = sys_mmap(nu*PL_V3*PL_I64) as *i64 176 p.tang = sys_mmap(nu*PL_V3*PL_I64) as *i64 177 p.hnd = sys_mmap(nu*nv*PL_V3*PL_I64) as *i64 178 p.ku = sys_mmap((nu+PL_ORD)*PL_I64) as *i64 179 p.kv = sys_mmap((nv+PL_ORD+PL_DEG)*PL_I64) as *i64 180 p.ka = sys_mmap((nu+PL_ORD)*PL_I64) as *i64 181 p.bu = sys_mmap(PL_Q4*PL_I64) as *i64 182 p.bv = sys_mmap(PL_Q4*PL_I64) as *i64 183 p.bl = sys_mmap(PL_Q4*PL_I64) as *i64 184 p.br = sys_mmap(PL_Q4*PL_I64) as *i64 185 if nu >= PL_MIN_NU { if nv >= PL_MIN_NV { pl_knots(p) } } 186 return p 187} 188 189// every gene and every derived array of src into dst (same nu, nv); returns 0, or -1 on a shape mismatch 190func pl_copy(dst: *PlPart, src: *PlPart) -> i64 { 191 if dst.nu != src.nu { return 0 - 1 } 192 if dst.nv != src.nv { return 0 - 1 } 193 let nu: i64 = src.nu 194 let nv: i64 = src.nv 195 var i: i64 = 0 196 while i < PL_V3 { dst.root[i] = src.root[i]; i = i + 1 } 197 dst.length = src.length 198 i = 0 199 while i < nu { dst.prop[i] = src.prop[i]; dst.theta[i] = src.theta[i]; dst.yoff[i] = src.yoff[i]; dst.rad[i] = src.rad[i]; dst.ts[i] = src.ts[i]; i = i + 1 } 200 i = 0 201 while i < nu*PL_V3 { dst.skel[i] = src.skel[i]; dst.tang[i] = src.tang[i]; i = i + 1 } 202 i = 0 203 while i < nu*nv*PL_V3 { dst.prof[i] = src.prof[i]; dst.hnd[i] = src.hnd[i]; i = i + 1 } 204 pl_knots(dst) 205 return 0 206} 207 208// ---- rotation that carries unit vector a onto unit vector b (rotate_match_directions, one pair) ---------------- 209// q = (a x b, 1 + a.b) normalised: the half-angle form needs no arccos and keeps a small angle to 1/4096 rad. 210// A null cross product (parallel OR antiparallel) yields the identity, as the source's |axis| <= 1e-4 branch does. 211func pl_rot_match(q: *i64, a: *i64, b: *i64) -> i64 { 212 let cx: i64 = (a[1]*b[2] - a[2]*b[1]) / PL_ONE 213 let cy: i64 = (a[2]*b[0] - a[0]*b[2]) / PL_ONE 214 let cz: i64 = (a[0]*b[1] - a[1]*b[0]) / PL_ONE 215 if cx*cx + cy*cy + cz*cz == 0 { vm_q_ident(q); return 0 } 216 let d: i64 = (a[0]*b[0] + a[1]*b[1] + a[2]*b[2]) / PL_ONE 217 q[0] = cx; q[1] = cy; q[2] = cz; q[3] = PL_ONE + d 218 vm_q_norm(q, q) 219 return 1 220} 221 222// skeleton_to_tangents: edge directions, averaged with the previous edge at interior stations, unit (Q12). 223// edge is caller scratch of n*3 words. 224func pl_tangents(skel: *i64, n: i64, tang: *i64, edge: *i64) -> i64 { 225 var i: i64 = 0 226 while i < n - 1 { 227 edge[i*PL_V3+0] = skel[(i+1)*PL_V3+0] - skel[i*PL_V3+0] 228 edge[i*PL_V3+1] = skel[(i+1)*PL_V3+1] - skel[i*PL_V3+1] 229 edge[i*PL_V3+2] = skel[(i+1)*PL_V3+2] - skel[i*PL_V3+2] 230 i = i + 1 231 } 232 i = 0 233 while i < n { 234 var ax: i64 = 0 235 var ay: i64 = 0 236 var az: i64 = 0 237 var done: i64 = 0 238 if i == 0 { ax = edge[0]; ay = edge[1]; az = edge[2]; done = 1 } 239 if done == 0 { 240 if i == n - 1 { ax = edge[(n-2)*PL_V3]; ay = edge[(n-2)*PL_V3+1]; az = edge[(n-2)*PL_V3+2]; done = 1 } 241 } 242 if done == 0 { 243 ax = (edge[i*PL_V3] + edge[(i-1)*PL_V3]) / 2 244 ay = (edge[i*PL_V3+1] + edge[(i-1)*PL_V3+1]) / 2 245 az = (edge[i*PL_V3+2] + edge[(i-1)*PL_V3+2]) / 2 246 } 247 let l: i64 = pl_len3(ax, ay, az) 248 if l == 0 { tang[i*PL_V3] = 0; tang[i*PL_V3+1] = 0; tang[i*PL_V3+2] = 0 } else { 249 tang[i*PL_V3] = pl_rdiv(ax*PL_ONE, l); tang[i*PL_V3+1] = pl_rdiv(ay*PL_ONE, l); tang[i*PL_V3+2] = pl_rdiv(az*PL_ONE, l) 250 } 251 i = i + 1 252 } 253 return 0 254} 255 256// ---- DECOMPOSE: handles (nu x nv x 3, u10) -> genes. factorize_nurbs_handles + decompose_nurbs_handles. ------- 257// Returns nu*nv, or a negative code for a shape the cubic surface cannot carry. 258func pl_decompose(hnd: *i64, nu: i64, nv: i64, p: *PlPart) -> i64 { 259 if nu < PL_MIN_NU { return 0 - 1 } 260 if nv < PL_MIN_NV { return 0 - 2 } 261 if p.nu != nu { return 0 - 3 } 262 if p.nv != nv { return 0 - 3 } 263 var k: i64 = 0 264 while k < nu*nv*PL_V3 { p.hnd[k] = hnd[k]; k = k + 1 } 265 var i: i64 = 0 266 while i < nu { 267 var sx: i64 = 0 268 var sy: i64 = 0 269 var sz: i64 = 0 270 var j: i64 = 0 271 while j < nv { sx = sx + hnd[(i*nv+j)*PL_V3]; sy = sy + hnd[(i*nv+j)*PL_V3+1]; sz = sz + hnd[(i*nv+j)*PL_V3+2]; j = j + 1 } 272 p.skel[i*PL_V3] = pl_rdiv(sx, nv); p.skel[i*PL_V3+1] = pl_rdiv(sy, nv); p.skel[i*PL_V3+2] = pl_rdiv(sz, nv) 273 i = i + 1 274 } 275 let edge: *i64 = sys_mmap(nu*PL_V3*PL_I64) as *i64 276 pl_tangents(p.skel, nu, p.tang, edge) 277 let fwd: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 278 fwd[0] = PL_ONE; fwd[1] = 0; fwd[2] = 0 279 let q: *i64 = sys_mmap(PL_Q4*PL_I64) as *i64 280 let t3: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 281 let d: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 282 let r: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 283 let lens: *i64 = sys_mmap(nu*PL_I64) as *i64 284 i = 0 285 while i < nu { 286 t3[0] = p.tang[i*PL_V3]; t3[1] = p.tang[i*PL_V3+1]; t3[2] = p.tang[i*PL_V3+2] 287 pl_rot_match(q, t3, fwd) 288 var acc: i64 = 0 289 var j: i64 = 0 290 while j < nv { 291 d[0] = hnd[(i*nv+j)*PL_V3] - p.skel[i*PL_V3] 292 d[1] = hnd[(i*nv+j)*PL_V3+1] - p.skel[i*PL_V3+1] 293 d[2] = hnd[(i*nv+j)*PL_V3+2] - p.skel[i*PL_V3+2] 294 vm_q_rotate_v3(r, q, d) 295 p.prof[(i*nv+j)*PL_V3] = r[0]; p.prof[(i*nv+j)*PL_V3+1] = r[1]; p.prof[(i*nv+j)*PL_V3+2] = r[2] 296 acc = acc + pl_len3_fx(r[0], r[1], r[2]) 297 j = j + 1 298 } 299 var rad: i64 = pl_rdiv(acc, nv) 300 if rad < PL_MIN_RAD { rad = PL_MIN_RAD } 301 p.rad[i] = rad 302 j = 0 303 while j < nv { 304 var c: i64 = 0 305 while c < PL_V3 { p.prof[(i*nv+j)*PL_V3+c] = pl_rdiv(p.prof[(i*nv+j)*PL_V3+c] * PL_FQ2, rad); c = c + 1 } 306 j = j + 1 307 } 308 i = i + 1 309 } 310 p.root[0] = p.skel[0]; p.root[1] = p.skel[1]; p.root[2] = p.skel[2] 311 var total: i64 = 0 312 i = 0 313 while i < nu - 1 { lens[i] = pl_len3(edge[i*PL_V3], edge[i*PL_V3+1], edge[i*PL_V3+2]); total = total + lens[i]; i = i + 1 } 314 p.length = total 315 i = 0 316 while i < nu - 1 { 317 if total > 0 { p.prop[i] = pl_rdiv(lens[i] * PL_FQ, total) } else { p.prop[i] = 0 } 318 p.theta[i] = vm_atan2(edge[i*PL_V3+2], edge[i*PL_V3]) 319 if lens[i] > 0 { p.yoff[i] = pl_rdiv(edge[i*PL_V3+1] * PL_FQ, lens[i]) } else { p.yoff[i] = 0 } 320 i = i + 1 321 } 322 i = 0 323 while i < nu { p.ts[i] = pl_rdiv(i * PL_FQ, nu - 1); i = i + 1 } 324 pl_knots(p) 325 sys_munmap(edge as *u8, nu*PL_V3*PL_I64); sys_munmap(fwd as *u8, PL_V3*PL_I64); sys_munmap(q as *u8, PL_Q4*PL_I64) 326 sys_munmap(t3 as *u8, PL_V3*PL_I64); sys_munmap(d as *u8, PL_V3*PL_I64); sys_munmap(r as *u8, PL_V3*PL_I64) 327 sys_munmap(lens as *u8, nu*PL_I64) 328 return nu * nv 329} 330 331// ---- RECOMPOSE: genes -> skeleton -> handles. recompose_nurbs_handles + compute_profile_verts. ----------------- 332func pl_recompose(p: *PlPart) -> i64 { 333 let nu: i64 = p.nu 334 let nv: i64 = p.nv 335 if nu < PL_MIN_NU { return 0 - 1 } 336 if nv < PL_MIN_NV { return 0 - 2 } 337 p.skel[0] = p.root[0]; p.skel[1] = p.root[1]; p.skel[2] = p.root[2] 338 var i: i64 = 0 339 while i < nu - 1 { 340 let len: i64 = pl_rdiv(p.length * p.prop[i], PL_FQ) 341 let c: i64 = vm_cos(p.theta[i]) 342 let s: i64 = vm_sin(p.theta[i]) 343 p.skel[(i+1)*PL_V3] = p.skel[i*PL_V3] + pl_rdiv(len * c, PL_ONE) 344 p.skel[(i+1)*PL_V3+1] = p.skel[i*PL_V3+1] + pl_rdiv(len * p.yoff[i], PL_FQ) 345 p.skel[(i+1)*PL_V3+2] = p.skel[i*PL_V3+2] + pl_rdiv(len * s, PL_ONE) 346 i = i + 1 347 } 348 let edge: *i64 = sys_mmap(nu*PL_V3*PL_I64) as *i64 349 pl_tangents(p.skel, nu, p.tang, edge) 350 let fwd: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 351 fwd[0] = PL_ONE; fwd[1] = 0; fwd[2] = 0 352 let q: *i64 = sys_mmap(PL_Q4*PL_I64) as *i64 353 let t3: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 354 let tu: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 355 let pos: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 356 let d: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 357 let r: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 358 i = 0 359 while i < nu { 360 // lerp_sample of the tangents and the skeleton at ts * (nu - 1) 361 let f: i64 = p.ts[i] * (nu - 1) 362 var idx: i64 = f / PL_FQ 363 if idx < 0 { idx = 0 } 364 if idx > nu - 1 { idx = nu - 1 } 365 var rem: i64 = f - idx * PL_FQ 366 if idx >= nu - 1 { rem = 0 } 367 var c: i64 = 0 368 while c < PL_V3 { 369 let a0: i64 = p.tang[idx*PL_V3+c] 370 let s0: i64 = p.skel[idx*PL_V3+c] 371 var a1: i64 = a0 372 var s1: i64 = s0 373 if idx < nu - 1 { a1 = p.tang[(idx+1)*PL_V3+c]; s1 = p.skel[(idx+1)*PL_V3+c] } 374 t3[c] = a0 + pl_rdiv((a1 - a0) * rem, PL_FQ) 375 pos[c] = s0 + pl_rdiv((s1 - s0) * rem, PL_FQ) 376 c = c + 1 377 } 378 pl_unit(tu, t3) 379 pl_rot_match(q, fwd, tu) 380 var j: i64 = 0 381 while j < nv { 382 d[0] = pl_rdiv(p.prof[(i*nv+j)*PL_V3] * p.rad[i], PL_FQ2) 383 d[1] = pl_rdiv(p.prof[(i*nv+j)*PL_V3+1] * p.rad[i], PL_FQ2) 384 d[2] = pl_rdiv(p.prof[(i*nv+j)*PL_V3+2] * p.rad[i], PL_FQ2) 385 vm_q_rotate_v3(r, q, d) 386 p.hnd[(i*nv+j)*PL_V3] = r[0] + pos[0] 387 p.hnd[(i*nv+j)*PL_V3+1] = r[1] + pos[1] 388 p.hnd[(i*nv+j)*PL_V3+2] = r[2] + pos[2] 389 j = j + 1 390 } 391 i = i + 1 392 } 393 pl_knots(p) 394 sys_munmap(edge as *u8, nu*PL_V3*PL_I64); sys_munmap(fwd as *u8, PL_V3*PL_I64); sys_munmap(q as *u8, PL_Q4*PL_I64) 395 sys_munmap(t3 as *u8, PL_V3*PL_I64); sys_munmap(tu as *u8, PL_V3*PL_I64); sys_munmap(pos as *u8, PL_V3*PL_I64) 396 sys_munmap(d as *u8, PL_V3*PL_I64); sys_munmap(r as *u8, PL_V3*PL_I64) 397 return 0 398} 399 400// ---- THE GENES: NurbsPart.sample_params, deviation 1 in the header ---------------------------------------------- 401func pl_lcg(st: *i64) -> i64 { 402 st[0] = (st[0] * PL_LCG_MUL + PL_LCG_ADD) % PL_LCG_MOD 403 return (st[0] >> PL_LCG_SHIFT) % PL_LCG_BITS 404} 405// uniform in [0, PL_FQ) 406func pl_urand(st: *i64) -> i64 { return pl_lcg(st) * PL_FQ / PL_LCG_BITS } 407// standard normal in Q16 by Irwin-Hall: twelve uniforms summed, centred 408func pl_nrand(st: *i64) -> i64 { 409 var s: i64 = 0 410 var k: i64 = 0 411 while k < PL_GAUSS_N { s = s + pl_urand(st); k = k + 1 } 412 return s - PL_GAUSS_N * PL_FQ / 2 413} 414// N(mean, sd * var) with sd and var_ in Q16 (a mean and sd in the caller's unit); a zero deviation returns the mean 415// exactly, so var_ = 0 is the identity by construction 416func pl_normal(st: *i64, mean: i64, sd: i64, var_: i64) -> i64 { 417 let sdv: i64 = sd * var_ / PL_FQ 418 if sdv == 0 { return mean } 419 return mean + pl_rdiv(sdv * pl_nrand(st), PL_FQ) 420} 421// mutate the genes in place; var_ is the source's `var` in Q16 (PL_FQ = 1.0). Deterministic per (seed, var_). 422// The recomposed arrays are NOT refreshed: call pl_recompose after. 423func pl_mutate(p: *PlPart, seed: i64, var_: i64) -> i64 { 424 let nu: i64 = p.nu 425 let nv: i64 = p.nv 426 let st: *i64 = sys_mmap(PL_I64) as *i64 427 var s0: i64 = seed 428 if s0 < 0 { s0 = 0 - s0 } 429 st[0] = s0 % PL_LCG_MOD 430 let sz: i64 = pl_normal(st, PL_FQ, PL_SD_SIZE, var_) 431 p.length = pl_rdiv(pl_rdiv(p.length * sz, PL_FQ) * pl_normal(st, PL_FQ, PL_SD_LENGTH, var_), PL_FQ) 432 let rshared: i64 = pl_normal(st, PL_FQ, PL_SD_RADS, var_) 433 var i: i64 = 0 434 while i < nu { 435 var rr: i64 = pl_rdiv(p.rad[i] * sz, PL_FQ) 436 rr = pl_rdiv(rr * rshared, PL_FQ) 437 rr = pl_rdiv(rr * pl_normal(st, PL_FQ, PL_SD_RAD_STATION, var_), PL_FQ) 438 if rr < PL_MIN_RAD { rr = PL_MIN_RAD } 439 p.rad[i] = rr 440 i = i + 1 441 } 442 i = 0 443 while i < nu - 1 { p.prop[i] = pl_rdiv(p.prop[i] * pl_normal(st, PL_FQ, PL_SD_PROP, var_), PL_FQ); i = i + 1 } 444 let dth: *i64 = sys_mmap(nu*PL_I64) as *i64 445 var sum: i64 = 0 446 i = 0 447 while i < nu - 1 { dth[i] = pl_normal(st, 0, PL_SD_THETA, var_); sum = sum + dth[i]; i = i + 1 } 448 let mean: i64 = sum / (nu - 1) 449 i = 0 450 while i < nu - 1 { p.theta[i] = p.theta[i] + dth[i] - mean; i = i + 1 } 451 let col: *i64 = sys_mmap(nv*PL_I64) as *i64 452 var j: i64 = 0 453 while j < nv { col[j] = pl_normal(st, PL_FQ, PL_SD_PROF_COL, var_); j = j + 1 } 454 let noise: *i64 = sys_mmap(nu*nv*PL_I64) as *i64 455 i = 0 456 while i < nu { 457 j = 0 458 while j < nv { noise[i*nv+j] = pl_rdiv(col[j] * pl_normal(st, PL_FQ, PL_SD_PROF_CELL, var_), PL_FQ); j = j + 1 } 459 i = i + 1 460 } 461 // profile_noise[:, :m//2-1] = profile_noise[:, m//2:-1][:, ::-1]: column j takes column m-2-j for j < m/2-1 462 i = 0 463 while i < nu { 464 j = 0 465 while j < nv/2 - 1 { noise[i*nv+j] = noise[i*nv + (nv - 2 - j)]; j = j + 1 } 466 i = i + 1 467 } 468 i = 0 469 while i < nu { 470 j = 0 471 while j < nv { 472 var c: i64 = 0 473 while c < PL_V3 { p.prof[(i*nv+j)*PL_V3+c] = pl_rdiv(p.prof[(i*nv+j)*PL_V3+c] * noise[i*nv+j], PL_FQ); c = c + 1 } 474 j = j + 1 475 } 476 i = i + 1 477 } 478 sys_munmap(st as *u8, PL_I64); sys_munmap(dth as *u8, nu*PL_I64); sys_munmap(col as *u8, nv*PL_I64) 479 sys_munmap(noise as *u8, nu*nv*PL_I64) 480 return 0 481} 482 483// ---- THE B-SPLINE SURFACE, degree (3,3): geomdl's evaluation in fixed point ---------------------------------- 484// span k with U[k] <= x < U[k+1] over the valid domain U[3] .. U[ncp]; x at or past the end lands on the last span 485func pl_span(U: *i64, ncp: i64, x: i64) -> i64 { 486 if x >= U[ncp] { return ncp - 1 } 487 var k: i64 = PL_DEG 488 while k < ncp - 1 { 489 if x >= U[k+1] { k = k + 1 } else { return k } 490 } 491 return k 492} 493// the four nonzero cubic basis functions at x on span k (Piegl-Tiller A2.2), Q16; left/right are 4-word scratch 494func pl_basis(U: *i64, k: i64, x: i64, N: *i64, left: *i64, right: *i64) -> i64 { 495 N[0] = PL_FQ 496 var j: i64 = 1 497 while j <= PL_DEG { 498 left[j] = x - U[k+1-j] 499 right[j] = U[k+j] - x 500 var saved: i64 = 0 501 var r: i64 = 0 502 while r < j { 503 let den: i64 = right[r+1] + left[j-r] 504 var temp: i64 = 0 505 if den != 0 { temp = N[r] * PL_FQ / den } 506 N[r] = saved + right[r+1] * temp / PL_FQ 507 saved = left[j-r] * temp / PL_FQ 508 r = r + 1 509 } 510 N[j] = saved 511 j = j + 1 512 } 513 return 0 514} 515// S(u, v) in u10 for u, v in [0, PL_FQ]; v maps onto the periodic domain [kv[3], kv[nv+3]], where the curve closes 516func pl_surface(p: *PlPart, u: i64, v: i64, out: *i64) -> i64 { 517 let nu: i64 = p.nu 518 let nv: i64 = p.nv 519 var x: i64 = u 520 if x < 0 { x = 0 } 521 if x > PL_FQ { x = PL_FQ } 522 var vv: i64 = v 523 if vv < 0 { vv = 0 } 524 if vv > PL_FQ { vv = PL_FQ } 525 let vs: i64 = p.kv[PL_DEG] 526 let ve: i64 = p.kv[nv + PL_DEG] 527 let y: i64 = vs + (ve - vs) * vv / PL_FQ 528 let ku: i64 = pl_span(p.ku, nu, x) 529 let kv: i64 = pl_span(p.kv, nv + PL_DEG, y) 530 pl_basis(p.ku, ku, x, p.bu, p.bl, p.br) 531 pl_basis(p.kv, kv, y, p.bv, p.bl, p.br) 532 var c: i64 = 0 533 while c < PL_V3 { 534 var acc: i64 = 0 535 var a: i64 = 0 536 while a < PL_ORD { 537 let row: i64 = ku - PL_DEG + a 538 var b: i64 = 0 539 while b < PL_ORD { 540 let colm: i64 = (kv - PL_DEG + b) % nv 541 acc = acc + p.bu[a] * p.bv[b] / PL_FQ * p.hnd[(row*nv + colm)*PL_V3 + c] 542 b = b + 1 543 } 544 a = a + 1 545 } 546 out[c] = pl_rdiv(acc, PL_FQ) 547 c = c + 1 548 } 549 return 0 550} 551 552// ---- THE ATTACHMENT SKELETON: the interior rows, cubic where four exist (deviation 3), linear otherwise -------- 553// t in [0, PL_FQ]; out = point (u10). Returns PL_ASKEL_SPLINE or PL_ASKEL_LINEAR. 554func pl_askel_point(p: *PlPart, t: i64, out: *i64) -> i64 { 555 let na: i64 = p.nu - 2 556 var tt: i64 = t 557 if tt < 0 { tt = 0 } 558 if tt > PL_FQ { tt = PL_FQ } 559 if na >= PL_ORD { 560 let k: i64 = pl_span(p.ka, na, tt) 561 pl_basis(p.ka, k, tt, p.bu, p.bl, p.br) 562 var c: i64 = 0 563 while c < PL_V3 { 564 var acc: i64 = 0 565 var a: i64 = 0 566 while a < PL_ORD { 567 let row: i64 = 1 + k - PL_DEG + a 568 acc = acc + p.bu[a] * p.skel[row*PL_V3 + c] 569 a = a + 1 570 } 571 out[c] = pl_rdiv(acc, PL_FQ) 572 c = c + 1 573 } 574 return PL_ASKEL_SPLINE 575 } 576 let f: i64 = tt * (na - 1) 577 var idx: i64 = f / PL_FQ 578 if idx > na - 2 { idx = na - 2 } 579 if idx < 0 { idx = 0 } 580 let rem: i64 = f - idx * PL_FQ 581 var c2: i64 = 0 582 while c2 < PL_V3 { 583 let s0: i64 = p.skel[(1+idx)*PL_V3 + c2] 584 let s1: i64 = p.skel[(2+idx)*PL_V3 + c2] 585 out[c2] = s0 + pl_rdiv((s1 - s0) * rem, PL_FQ) 586 c2 = c2 + 1 587 } 588 return PL_ASKEL_LINEAR 589} 590// the attachment skeleton's point and unit tangent (Q12) at t 591func pl_askel(p: *PlPart, t: i64, out: *i64, tan: *i64) -> i64 { 592 let which: i64 = pl_askel_point(p, t, out) 593 let a: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 594 let b: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 595 var t0: i64 = t - PL_FD_STEP 596 var t1: i64 = t + PL_FD_STEP 597 if t0 < 0 { t0 = 0 } 598 if t1 > PL_FQ { t1 = PL_FQ } 599 pl_askel_point(p, t0, a) 600 pl_askel_point(p, t1, b) 601 a[0] = b[0] - a[0]; a[1] = b[1] - a[1]; a[2] = b[2] - a[2] 602 pl_unit(tan, a) 603 sys_munmap(a as *u8, PL_V3*PL_I64); sys_munmap(b as *u8, PL_V3*PL_I64) 604 return which 605} 606 607// the squared perpendicular distance (u10^2) from the ray through o along the unit direction dw (Q12) to the surface 608// point at (u, v); a point behind the origin scores PL_POS_HUGE. Smaller is better: the search minimises it, and the 609// u10 quantisation of the surface bounds the search rather than the cosine's quantisation (a Q12 cosine reads 1.0 for 610// every direction within 22 milliradians -- MEASURED as a 4 u10 lateral miss on the first run of the gate). 611func pl_ray_perp2(p: *PlPart, u: i64, v: i64, o: *i64, dw: *i64, s: *i64) -> i64 { 612 pl_surface(p, u, v, s) 613 let dx: i64 = s[0] - o[0] 614 let dy: i64 = s[1] - o[1] 615 let dz: i64 = s[2] - o[2] 616 let proj: i64 = (dx*dw[0] + dy*dw[1] + dz*dw[2]) / PL_ONE 617 if proj <= 0 { return PL_POS_HUGE } 618 return dx*dx + dy*dy + dz*dz - proj*proj 619} 620// ---- SURFACE COORDINATES -> WORLD: apply_attach_transform's ray, deviation 2 in the header -------------------- 621// t in [0, PL_FQ] along the attachment skeleton; yaw in [-PL_FQ, PL_FQ], a half-turn per unit about the tangent from 622// straight down (-z in the part frame): 0 = down, +1/2 = +y, +1 = up, -1/2 = -y; rad in [0, PL_FQ] from the skeleton 623// (0) to the surface (1). out = location (u10), nrm = outward unit normal at the hit (Q12), tan = unit tangent (Q12), 624// info = [cosine of ray and hit (Q16), u of the hit, v of the hit, skeleton-to-hit distance u10, askel path]. Returns 0. 625func pl_surface_at(p: *PlPart, t: i64, yaw: i64, rad: i64, out: *i64, nrm: *i64, tan: *i64, info: *i64) -> i64 { 626 let nu: i64 = p.nu 627 let o: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 628 let which: i64 = pl_askel(p, t, o, tan) 629 let fwd: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 630 fwd[0] = PL_ONE; fwd[1] = 0; fwd[2] = 0 631 let d0: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 632 let ang: i64 = VM_PI * yaw / PL_FQ 633 d0[0] = 0; d0[1] = vm_sin(ang); d0[2] = 0 - vm_cos(ang) 634 let q: *i64 = sys_mmap(PL_Q4*PL_I64) as *i64 635 pl_rot_match(q, fwd, tan) 636 let dw: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 637 vm_q_rotate_v3(dw, q, d0) 638 let s: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 639 // the station's u on the full surface: interior row 1 is t = 0, interior row nu-2 is t = 1 640 var tt: i64 = t 641 if tt < 0 { tt = 0 } 642 if tt > PL_FQ { tt = PL_FQ } 643 let uc: i64 = (PL_FQ + tt * (nu - 3)) / (nu - 1) 644 let seg: i64 = PL_FQ / (nu - 1) 645 let cell: i64 = PL_FQ / PL_SEARCH_COARSE 646 var best: i64 = PL_POS_HUGE 647 var bu: i64 = uc 648 var bv: i64 = 0 649 var kk: i64 = 0 - PL_SEARCH_UHALF 650 while kk <= PL_SEARCH_UHALF { 651 var uu: i64 = uc + kk * seg / PL_SEARCH_UQUART 652 if uu < 0 { uu = 0 } 653 if uu > PL_FQ { uu = PL_FQ } 654 var m: i64 = 0 655 while m < PL_SEARCH_COARSE { 656 let vv: i64 = m * cell 657 let sc: i64 = pl_ray_perp2(p, uu, vv, o, dw, s) 658 if sc < best { best = sc; bu = uu; bv = vv } 659 m = m + 1 660 } 661 kk = kk + 1 662 } 663 // bisection on v about the best coarse sample: the better of the two neighbouring midpoints, halving each step 664 var lo: i64 = bv - cell 665 var hi: i64 = bv + cell 666 var step: i64 = 0 667 while step < PL_SEARCH_REFINE { 668 let ml: i64 = (lo + bv) / 2 669 let mh: i64 = (bv + hi) / 2 670 var vl: i64 = ml 671 if vl < 0 { vl = vl + PL_FQ } 672 var vh: i64 = mh 673 if vh >= PL_FQ { vh = vh - PL_FQ } 674 let sl: i64 = pl_ray_perp2(p, bu, vl, o, dw, s) 675 let sh: i64 = pl_ray_perp2(p, bu, vh, o, dw, s) 676 var moved: i64 = 0 677 if sl < best { if sl <= sh { best = sl; hi = bv; bv = ml; moved = 1 } } 678 if moved == 0 { if sh < best { best = sh; lo = bv; bv = mh; moved = 1 } } 679 if moved == 0 { lo = ml; hi = mh } 680 step = step + 1 681 } 682 if bv < 0 { bv = bv + PL_FQ } 683 if bv >= PL_FQ { bv = bv - PL_FQ } 684 pl_surface(p, bu, bv, s) 685 let dist: i64 = pl_len3(s[0]-o[0], s[1]-o[1], s[2]-o[2]) 686 var cosq: i64 = 0 687 if dist > 0 { cosq = pl_rdiv(((s[0]-o[0])*dw[0] + (s[1]-o[1])*dw[1] + (s[2]-o[2])*dw[2]) / PL_ONE * PL_FQ, dist) } 688 out[0] = o[0] + pl_rdiv((s[0] - o[0]) * rad, PL_FQ) 689 out[1] = o[1] + pl_rdiv((s[1] - o[1]) * rad, PL_FQ) 690 out[2] = o[2] + pl_rdiv((s[2] - o[2]) * rad, PL_FQ) 691 // the normal: central differences along u and v, crossed, oriented along the ray 692 let su: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 693 let sv: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 694 let tmp: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 695 var ua: i64 = bu - PL_FD_STEP 696 var ub: i64 = bu + PL_FD_STEP 697 if ua < 0 { ua = 0 } 698 if ub > PL_FQ { ub = PL_FQ } 699 pl_surface(p, ua, bv, tmp) 700 pl_surface(p, ub, bv, su) 701 su[0] = su[0] - tmp[0]; su[1] = su[1] - tmp[1]; su[2] = su[2] - tmp[2] 702 var va: i64 = bv - PL_FD_STEP 703 var vb: i64 = bv + PL_FD_STEP 704 if va < 0 { va = va + PL_FQ } 705 if vb >= PL_FQ { vb = vb - PL_FQ } 706 pl_surface(p, bu, va, tmp) 707 pl_surface(p, bu, vb, sv) 708 sv[0] = sv[0] - tmp[0]; sv[1] = sv[1] - tmp[1]; sv[2] = sv[2] - tmp[2] 709 tmp[0] = su[1]*sv[2] - su[2]*sv[1] 710 tmp[1] = su[2]*sv[0] - su[0]*sv[2] 711 tmp[2] = su[0]*sv[1] - su[1]*sv[0] 712 pl_unit(nrm, tmp) 713 if nrm[0]*dw[0] + nrm[1]*dw[1] + nrm[2]*dw[2] < 0 { nrm[0] = 0 - nrm[0]; nrm[1] = 0 - nrm[1]; nrm[2] = 0 - nrm[2] } 714 info[0] = cosq; info[1] = bu; info[2] = bv; info[3] = dist; info[4] = which 715 sys_munmap(o as *u8, PL_V3*PL_I64); sys_munmap(fwd as *u8, PL_V3*PL_I64); sys_munmap(d0 as *u8, PL_V3*PL_I64) 716 sys_munmap(q as *u8, PL_Q4*PL_I64); sys_munmap(dw as *u8, PL_V3*PL_I64); sys_munmap(s as *u8, PL_V3*PL_I64) 717 sys_munmap(su as *u8, PL_V3*PL_I64); sys_munmap(sv as *u8, PL_V3*PL_I64); sys_munmap(tmp as *u8, PL_V3*PL_I64) 718 return 0 719} 720 721// ---- THE MESH: nurbs.py nurbs(..., method='geomdl', face_size) --------------------------------------------- 722// out = [ulength, vlength, sample_size, ring_points, ntris]. ulength is the longest u-polyline over the columns, 723// vlength the longest OPEN v-polyline over the rows (np.diff does not wrap), sample_size = L / face + 1 for both 724// parameters (one delta), ring_points = sample_size - 1 (the closing duplicate dropped), ntris counts the tube's two 725// triangles per quad plus the two cap fans. Returns ntris, or -1 when the face size leaves fewer than three ring 726// points (a cap fan cannot exist) -- a NAMED refusal, never a silent floor. 727func pl_mesh_counts(p: *PlPart, face: i64, out: *i64) -> i64 { 728 let nu: i64 = p.nu 729 let nv: i64 = p.nv 730 var ul: i64 = 0 731 var j: i64 = 0 732 while j < nv { 733 var acc: i64 = 0 734 var i: i64 = 0 735 while i < nu - 1 { 736 let a: i64 = (i*nv+j)*PL_V3 737 let b: i64 = ((i+1)*nv+j)*PL_V3 738 acc = acc + pl_len3(p.hnd[b]-p.hnd[a], p.hnd[b+1]-p.hnd[a+1], p.hnd[b+2]-p.hnd[a+2]) 739 i = i + 1 740 } 741 if acc > ul { ul = acc } 742 j = j + 1 743 } 744 var vl: i64 = 0 745 var i2: i64 = 0 746 while i2 < nu { 747 var acc2: i64 = 0 748 var j2: i64 = 0 749 while j2 < nv - 1 { 750 let a: i64 = (i2*nv+j2)*PL_V3 751 let b: i64 = (i2*nv+j2+1)*PL_V3 752 acc2 = acc2 + pl_len3(p.hnd[b]-p.hnd[a], p.hnd[b+1]-p.hnd[a+1], p.hnd[b+2]-p.hnd[a+2]) 753 j2 = j2 + 1 754 } 755 if acc2 > vl { vl = acc2 } 756 i2 = i2 + 1 757 } 758 var L: i64 = ul 759 if vl > L { L = vl } 760 var f: i64 = face 761 if f < 1 { f = 1 } 762 let ss: i64 = L / f + 1 763 let m: i64 = ss - 1 764 out[0] = ul; out[1] = vl; out[2] = ss; out[3] = m 765 if m < PL_MIN_RING { out[4] = 0 - 1; return 0 - 1 } 766 let nt: i64 = 2 * (ss - 1) * m + 2 * m 767 out[4] = nt 768 return nt 769} 770// one triangle into the NXMSH2 buffer: winding flipped so the face normal points along `outward` 771func pl_put_tri(B: *u8, tb: i64, t: i64, V: *i64, ia: i64, ib: i64, ic: i64, ox: i64, oy: i64, oz: i64, xyz9: *i64) -> i64 { 772 var b: i64 = ib 773 var c: i64 = ic 774 let e1x: i64 = V[ib*PL_V3] - V[ia*PL_V3] 775 let e1y: i64 = V[ib*PL_V3+1] - V[ia*PL_V3+1] 776 let e1z: i64 = V[ib*PL_V3+2] - V[ia*PL_V3+2] 777 let e2x: i64 = V[ic*PL_V3] - V[ia*PL_V3] 778 let e2y: i64 = V[ic*PL_V3+1] - V[ia*PL_V3+1] 779 let e2z: i64 = V[ic*PL_V3+2] - V[ia*PL_V3+2] 780 var nx: i64 = e1y*e2z - e1z*e2y 781 var ny: i64 = e1z*e2x - e1x*e2z 782 var nz: i64 = e1x*e2y - e1y*e2x 783 if nx*ox + ny*oy + nz*oz < 0 { b = ic; c = ib; nx = 0 - nx; ny = 0 - ny; nz = 0 - nz } 784 xyz9[0] = V[ia*PL_V3]; xyz9[1] = V[ia*PL_V3+1]; xyz9[2] = V[ia*PL_V3+2] 785 xyz9[3] = V[b*PL_V3]; xyz9[4] = V[b*PL_V3+1]; xyz9[5] = V[b*PL_V3+2] 786 xyz9[6] = V[c*PL_V3]; xyz9[7] = V[c*PL_V3+1]; xyz9[8] = V[c*PL_V3+2] 787 nm_put_tri(B, tb, t, xyz9) 788 let l: i64 = pl_len3(nx, ny, nz) 789 var qx: i64 = 0 790 var qy: i64 = 0 791 var qz: i64 = 0 792 if l > 0 { qx = pl_rdiv(nx*PL_ONE, l); qy = pl_rdiv(ny*PL_ONE, l); qz = pl_rdiv(nz*PL_ONE, l) } 793 var v: i64 = 0 794 while v < PL_V3 { 795 let o: i64 = tb + t*NM_TRI_REC + PL_OFF_NORMALS + v*NM_VERT_STRIDE 796 nm_put_u32(B, o, vm_int_to_f32(qx, PL_ONE)) 797 nm_put_u32(B, o + NM_F32_BYTES, vm_int_to_f32(qy, PL_ONE)) 798 nm_put_u32(B, o + NM_F32_BYTES*2, vm_int_to_f32(qz, PL_ONE)) 799 v = v + 1 800 } 801 nm_col_put(B, tb, t, PL_COL_BONE_R, PL_COL_BONE_G, PL_COL_BONE_B) 802 return 0 803} 804// write the part as a one-layer NXMSH2 at `path`; rep = [ntris, bytes, sample_size, ring_points]. Returns ntris, or 805// -1 when the face size is refused by pl_mesh_counts, -2 when the file cannot be opened. 806func pl_mesh_write(p: *PlPart, face: i64, path: *u8, rep: *i64) -> i64 { 807 let cnt: *i64 = sys_mmap(PL_I64*8) as *i64 808 let nt: i64 = pl_mesh_counts(p, face, cnt) 809 if nt < 0 { rep[0] = 0 - 1; rep[1] = 0; rep[2] = cnt[2]; rep[3] = cnt[3]; return 0 - 1 } 810 let ss: i64 = cnt[2] 811 let m: i64 = cnt[3] 812 let V: *i64 = sys_mmap(ss*m*PL_V3*PL_I64) as *i64 813 let cen: *i64 = sys_mmap(ss*PL_V3*PL_I64) as *i64 814 let s: *i64 = sys_mmap(PL_V3*PL_I64) as *i64 815 var i: i64 = 0 816 while i < ss { 817 let u: i64 = i * PL_FQ / (ss - 1) 818 var cx: i64 = 0 819 var cy: i64 = 0 820 var cz: i64 = 0 821 var j: i64 = 0 822 while j < m { 823 let v: i64 = j * PL_FQ / m 824 pl_surface(p, u, v, s) 825 V[(i*m+j)*PL_V3] = s[0]; V[(i*m+j)*PL_V3+1] = s[1]; V[(i*m+j)*PL_V3+2] = s[2] 826 cx = cx + s[0]; cy = cy + s[1]; cz = cz + s[2] 827 j = j + 1 828 } 829 cen[i*PL_V3] = pl_rdiv(cx, m); cen[i*PL_V3+1] = pl_rdiv(cy, m); cen[i*PL_V3+2] = pl_rdiv(cz, m) 830 i = i + 1 831 } 832 let bytes: i64 = nm_file_bytes(1, nt) 833 let B: *u8 = sys_mmap(bytes + PL_SAVE_HEADER) 834 B[0] = PL_MAGIC_0 as u8; B[1] = PL_MAGIC_1 as u8; B[2] = PL_MAGIC_2 as u8; B[3] = PL_MAGIC_3 as u8 835 B[4] = PL_MAGIC_4 as u8; B[5] = PL_MAGIC_5 as u8; B[6] = 0 as u8; B[7] = 0 as u8 836 nm_put_u32(B, NM_OFF_NLAYERS, 1) 837 nm_put_u32(B, NM_OFF_NTRIS, nt) 838 B[NM_HDR] = 112 as u8; B[NM_HDR+1] = 97 as u8; B[NM_HDR+2] = 114 as u8; B[NM_HDR+3] = 116 as u8 // "part" 839 nm_put_u32(B, NM_HDR + PL_LAYER_START_OFF, 0) 840 nm_put_u32(B, NM_HDR + PL_LAYER_COUNT_OFF, nt) 841 let tb: i64 = nm_tri_base(B) 842 let xyz9: *i64 = sys_mmap(9*PL_I64) as *i64 843 // the tube: quads (i,j) (i,j+1) (i+1,j+1) (i+1,j), each split at its first diagonal, cyclic in j 844 var t: i64 = 0 845 i = 0 846 while i < ss - 1 { 847 var j: i64 = 0 848 while j < m { 849 let jn: i64 = (j + 1) % m 850 let a: i64 = i*m + j 851 let b: i64 = i*m + jn 852 let c: i64 = (i+1)*m + jn 853 let d: i64 = (i+1)*m + j 854 // outward for this quad: from the ring centre at row i to vertex a 855 let ox: i64 = V[a*PL_V3] - cen[i*PL_V3] 856 let oy: i64 = V[a*PL_V3+1] - cen[i*PL_V3+1] 857 let oz: i64 = V[a*PL_V3+2] - cen[i*PL_V3+2] 858 pl_put_tri(B, tb, t, V, a, b, c, ox, oy, oz, xyz9) 859 t = t + 1 860 pl_put_tri(B, tb, t, V, a, c, d, ox, oy, oz, xyz9) 861 t = t + 1 862 j = j + 1 863 } 864 i = i + 1 865 } 866 // the caps: fans from the ring centre, outward along the tube's own axis (deviation 4) 867 let CV: *i64 = sys_mmap((m + 1)*PL_V3*PL_I64) as *i64 868 var e: i64 = 0 869 while e < 2 { 870 var row: i64 = 0 871 var ox: i64 = cen[0] - cen[PL_V3] 872 var oy: i64 = cen[1] - cen[PL_V3+1] 873 var oz: i64 = cen[2] - cen[PL_V3+2] 874 if e == 1 { 875 row = ss - 1 876 ox = cen[(ss-1)*PL_V3] - cen[(ss-2)*PL_V3] 877 oy = cen[(ss-1)*PL_V3+1] - cen[(ss-2)*PL_V3+1] 878 oz = cen[(ss-1)*PL_V3+2] - cen[(ss-2)*PL_V3+2] 879 } 880 var j: i64 = 0 881 while j < m { 882 CV[j*PL_V3] = V[(row*m+j)*PL_V3]; CV[j*PL_V3+1] = V[(row*m+j)*PL_V3+1]; CV[j*PL_V3+2] = V[(row*m+j)*PL_V3+2] 883 j = j + 1 884 } 885 CV[m*PL_V3] = cen[row*PL_V3]; CV[m*PL_V3+1] = cen[row*PL_V3+1]; CV[m*PL_V3+2] = cen[row*PL_V3+2] 886 j = 0 887 while j < m { 888 pl_put_tri(B, tb, t, CV, m, j, (j + 1) % m, ox, oy, oz, xyz9) 889 t = t + 1 890 j = j + 1 891 } 892 e = e + 1 893 } 894 let fd: i64 = sys_openat_wr(path, PL_MODE644) 895 if fd < 0 { rep[0] = 0 - 2; rep[1] = bytes; rep[2] = ss; rep[3] = m; return 0 - 2 } 896 sys_write(fd, B, bytes) 897 sys_close(fd) 898 rep[0] = nt; rep[1] = bytes; rep[2] = ss; rep[3] = m 899 sys_munmap(V as *u8, ss*m*PL_V3*PL_I64); sys_munmap(cen as *u8, ss*PL_V3*PL_I64); sys_munmap(s as *u8, PL_V3*PL_I64) 900 sys_munmap(B, bytes + PL_SAVE_HEADER); sys_munmap(xyz9 as *u8, 9*PL_I64); sys_munmap(CV as *u8, (m + 1)*PL_V3*PL_I64) 901 sys_munmap(cnt as *u8, PL_I64*8) 902 return nt 903} 904 905// ---- THE PART FILE: the genes as whitespace-separated integers, so a part is DATA a board can carry ------------- 906// NXPART <version>\n <nu> <nv>\n <root x y z>\n <length>\n <prop nu-1>\n <theta nu-1>\n <yoff nu-1>\n <rad nu>\n 907// <ts nu>\n <prof nu*nv*3>\n -- every value in this file's units (u10, Q16 fractions, Q16 u10 radii, VM_ONE radians). 908func pl_save_ints(fb: *u8, fo: i64, A: *i64, n: i64) -> i64 { 909 var o: i64 = fo 910 var i: i64 = 0 911 while i < n { 912 o = nxi_buf(fb, o, A[i]) 913 fb[o] = 32 as u8 914 o = o + 1 915 i = i + 1 916 } 917 fb[o] = 10 as u8 918 return o + 1 919} 920func pl_save(path: *u8, p: *PlPart) -> i64 { 921 let nu: i64 = p.nu 922 let nv: i64 = p.nv 923 let nints: i64 = PL_HEADER_INTS + 3*(nu-1) + 2*nu + nu*nv*PL_V3 924 let cap: i64 = nints*PL_SAVE_DIGITS + PL_SAVE_HEADER 925 let fb: *u8 = sys_mmap(cap) 926 var fo: i64 = 0 927 fb[0] = 78 as u8; fb[1] = 88 as u8; fb[2] = 80 as u8; fb[3] = 65 as u8; fb[4] = 82 as u8; fb[5] = 84 as u8; fb[6] = 32 as u8 928 fo = 7 929 fo = nxi_buf(fb, fo, PL_FMT_VERSION) 930 fb[fo] = 10 as u8 931 fo = fo + 1 932 let hdr: *i64 = sys_mmap(PL_I64*2) as *i64 933 hdr[0] = nu; hdr[1] = nv 934 fo = pl_save_ints(fb, fo, hdr, 2) 935 fo = pl_save_ints(fb, fo, p.root, PL_V3) 936 hdr[0] = p.length 937 fo = pl_save_ints(fb, fo, hdr, 1) 938 fo = pl_save_ints(fb, fo, p.prop, nu - 1) 939 fo = pl_save_ints(fb, fo, p.theta, nu - 1) 940 fo = pl_save_ints(fb, fo, p.yoff, nu - 1) 941 fo = pl_save_ints(fb, fo, p.rad, nu) 942 fo = pl_save_ints(fb, fo, p.ts, nu) 943 fo = pl_save_ints(fb, fo, p.prof, nu*nv*PL_V3) 944 let fd: i64 = sys_openat_wr(path, PL_MODE644) 945 if fd < 0 { return 0 - 1 } 946 sys_write(fd, fb, fo) 947 sys_close(fd) 948 sys_munmap(fb, cap); sys_munmap(hdr as *u8, PL_I64*2) 949 return fo 950} 951// the next integer at or after pos[0]; returns 1 and advances, or 0 at the end of the buffer 952func pl_tok(buf: *u8, n: i64, pos: *i64, out: *i64) -> i64 { 953 var i: i64 = pos[0] 954 while i < n { 955 var c: i64 = buf[i] as i64 956 var neg: i64 = 0 957 if c == 45 { 958 neg = 1 959 i = i + 1 960 if i < n { c = buf[i] as i64 } else { c = 0 } 961 } 962 var isd: i64 = 0 963 if c >= 48 { if c <= 57 { isd = 1 } } 964 if isd == 1 { 965 var v: i64 = 0 966 var run: i64 = 1 967 while run == 1 { 968 v = v*10 + (c - 48) 969 i = i + 1 970 if i < n { c = buf[i] as i64 } else { c = 0 } 971 var still: i64 = 0 972 if c >= 48 { if c <= 57 { still = 1 } } 973 if still == 0 { run = 0 } 974 } 975 if neg == 1 { v = 0 - v } 976 out[0] = v 977 pos[0] = i 978 return 1 979 } 980 i = i + 1 981 } 982 pos[0] = i 983 return 0 984} 985func pl_load_ints(buf: *u8, n: i64, pos: *i64, A: *i64, cnt: i64, tok: *i64) -> i64 { 986 var i: i64 = 0 987 while i < cnt { 988 if pl_tok(buf, n, pos, tok) == 0 { return 0 - 1 } 989 A[i] = tok[0] 990 i = i + 1 991 } 992 return cnt 993} 994// load a part file; returns the part, recomposed and ready to evaluate, or a null pointer when the file is absent, 995// carries another version, or ends before its declared count 996func pl_load(path: *u8) -> *PlPart { 997 let ln: *i64 = sys_mmap(PL_LOAD_HDR) as *i64 998 let buf: *u8 = sys_read_file(path, ln) 999 if (buf as i64) == 0 { return 0 as *PlPart } 1000 let n: i64 = ln[0] 1001 let pos: *i64 = sys_mmap(PL_I64) as *i64 1002 let tok: *i64 = sys_mmap(PL_I64) as *i64 1003 pos[0] = 0 1004 if pl_tok(buf, n, pos, tok) == 0 { return 0 as *PlPart } 1005 if tok[0] != PL_FMT_VERSION { return 0 as *PlPart } 1006 if pl_tok(buf, n, pos, tok) == 0 { return 0 as *PlPart } 1007 let nu: i64 = tok[0] 1008 if pl_tok(buf, n, pos, tok) == 0 { return 0 as *PlPart } 1009 let nv: i64 = tok[0] 1010 if nu < PL_MIN_NU { return 0 as *PlPart } 1011 if nv < PL_MIN_NV { return 0 as *PlPart } 1012 let p: *PlPart = pl_new(nu, nv) 1013 if pl_load_ints(buf, n, pos, p.root, PL_V3, tok) < 0 { return 0 as *PlPart } 1014 if pl_tok(buf, n, pos, tok) == 0 { return 0 as *PlPart } 1015 p.length = tok[0] 1016 if pl_load_ints(buf, n, pos, p.prop, nu - 1, tok) < 0 { return 0 as *PlPart } 1017 if pl_load_ints(buf, n, pos, p.theta, nu - 1, tok) < 0 { return 0 as *PlPart } 1018 if pl_load_ints(buf, n, pos, p.yoff, nu - 1, tok) < 0 { return 0 as *PlPart } 1019 if pl_load_ints(buf, n, pos, p.rad, nu, tok) < 0 { return 0 as *PlPart } 1020 if pl_load_ints(buf, n, pos, p.ts, nu, tok) < 0 { return 0 as *PlPart } 1021 if pl_load_ints(buf, n, pos, p.prof, nu*nv*PL_V3, tok) < 0 { return 0 as *PlPart } 1022 pl_recompose(p) 1023 sys_munmap(ln as *u8, PL_LOAD_HDR); sys_munmap(pos as *u8, PL_I64); sys_munmap(tok as *u8, PL_I64) 1024 return p 1025}