nx_obj_import.nx source
↩ module page · 405 lines · 16515 B
1// nx_obj_import.nx -- sovereign Wavefront OBJ READER. The missing half of nx_obj_export: the
2// ecosystem could WRITE meshes and could not READ them, so no externally-authored geometry could
3// ever enter the pipeline. This is the ingest gate for any licensed/CC scan or sculpt.
4//
5// ★BUILT FOR REAL-WORLD OBJ, NOT JUST OUR OWN EXPORT. Files from Blender/scanners/Commons differ
6// from what nx_obj_export emits in three ways that each break a naive parser:
7// 1. FLOATING-POINT vertex coords ("v 0.123456 -1.5 2.0e-1") -- our exporter writes integers.
8// Parsed into fx1024 fixed point, including exponent notation.
9// 2. NEGATIVE face indices (-1 = most recent vertex), the relative form in the OBJ spec.
10// 3. N-GONS (quads are everywhere in DCC exports) -- fan-triangulated, not rejected.
11// Also tolerates v/vt/vn face forms (a, a/b, a//c, a/b/c), CRLF, and unknown directives.
12//
13// verts land in fx1024 units, matching nx_obj_export/nx_meshgen so the mesh stack composes.
14// usage as a lib: oi_read(path, vbuf, fbuf, vcap, fcap, counts) -> 0 ok, negative on error
15// counts[0]=nv counts[1]=nf
16// license_tier: ORIGINAL
17import "nx_syscalls.nx"
18
19const OI_FX: i64 = 1024
20
21func oi_is_digit(c: i64) -> i64 { if c < 48 { return 0 } if c > 57 { return 0 } return 1 }
22// advance p past spaces/tabs
23func oi_ws(buf: *u8, n: i64, p: *i64) -> i64 {
24 var i: i64 = p[0]
25 var d: i64 = 0
26 while d == 0 {
27 if i >= n { d = 1 } else {
28 if buf[i] == (32 as u8) { i = i + 1 } else { if buf[i] == (9 as u8) { i = i + 1 } else { d = 1 } }
29 }
30 }
31 p[0] = i
32 return i
33}
34// advance p to the start of the next line
35func oi_eol(buf: *u8, n: i64, p: *i64) -> i64 {
36 var i: i64 = p[0]
37 var d: i64 = 0
38 while d == 0 {
39 if i >= n { d = 1 } else { if buf[i] == (10 as u8) { i = i + 1; d = 1 } else { i = i + 1 } }
40 }
41 p[0] = i
42 return i
43}
44// parse a decimal (optionally signed, optionally fractional, optional e-exponent) into fx1024.
45// Returns the fixed-point value; sets ok[0]=1 if any digits were consumed.
46func oi_fx(buf: *u8, n: i64, p: *i64, ok: *i64) -> i64 {
47 ok[0] = 0
48 oi_ws(buf, n, p)
49 var i: i64 = p[0]
50 var neg: i64 = 0
51 if i < n { if buf[i] == (45 as u8) { neg = 1; i = i + 1 } else { if buf[i] == (43 as u8) { i = i + 1 } } }
52 var ip: i64 = 0
53 var got: i64 = 0
54 var d1: i64 = 0
55 while d1 == 0 {
56 if i >= n { d1 = 1 } else {
57 if oi_is_digit(buf[i] as i64) == 1 { ip = ip * 10 + ((buf[i] as i64) - 48); i = i + 1; got = 1 } else { d1 = 1 }
58 }
59 }
60 var fx: i64 = ip * OI_FX
61 // fractional part: accumulate up to 6 digits then scale once (avoids per-digit rounding drift)
62 if i < n {
63 if buf[i] == (46 as u8) {
64 i = i + 1
65 var fr: i64 = 0
66 var scale: i64 = 1
67 var nd: i64 = 0
68 var d2: i64 = 0
69 while d2 == 0 {
70 if i >= n { d2 = 1 } else {
71 if oi_is_digit(buf[i] as i64) == 1 {
72 if nd < 6 { fr = fr * 10 + ((buf[i] as i64) - 48); scale = scale * 10; nd = nd + 1 }
73 i = i + 1; got = 1
74 } else { d2 = 1 }
75 }
76 }
77 if scale > 1 { fx = fx + (fr * OI_FX) / scale }
78 }
79 }
80 // exponent: scanners and Blender both emit 1.0e-3 forms
81 if i < n {
82 if buf[i] == (101 as u8) { i = i + 1 } else { if buf[i] == (69 as u8) { i = i + 1 } }
83 }
84 if i > p[0] {
85 var eneg: i64 = 0
86 var j: i64 = i
87 var hase: i64 = 0
88 if j < n { if buf[j] == (45 as u8) { eneg = 1; j = j + 1; hase = 1 } else { if buf[j] == (43 as u8) { j = j + 1; hase = 1 } } }
89 var ev: i64 = 0
90 var d3: i64 = 0
91 while d3 == 0 {
92 if j >= n { d3 = 1 } else {
93 if oi_is_digit(buf[j] as i64) == 1 { ev = ev * 10 + ((buf[j] as i64) - 48); j = j + 1; hase = 1 } else { d3 = 1 }
94 }
95 }
96 if hase == 1 {
97 if ev > 12 { ev = 12 }
98 var k: i64 = 0
99 while k < ev { if eneg == 1 { fx = fx / 10 } else { fx = fx * 10 } k = k + 1 }
100 i = j
101 }
102 }
103 p[0] = i
104 if got == 1 { ok[0] = 1 }
105 if neg == 1 { return 0 - fx }
106 return fx
107}
108// parse one face vertex reference: forms a | a/b | a//c | a/b/c. Returns the POSITION index
109// (still 1-based or negative); consumes any /vt/vn that follows.
110func oi_faceref(buf: *u8, n: i64, p: *i64, ok: *i64) -> i64 {
111 ok[0] = 0
112 oi_ws(buf, n, p)
113 var i: i64 = p[0]
114 var neg: i64 = 0
115 if i < n { if buf[i] == (45 as u8) { neg = 1; i = i + 1 } }
116 var v: i64 = 0
117 var got: i64 = 0
118 var d: i64 = 0
119 while d == 0 {
120 if i >= n { d = 1 } else {
121 if oi_is_digit(buf[i] as i64) == 1 { v = v * 10 + ((buf[i] as i64) - 48); i = i + 1; got = 1 } else { d = 1 }
122 }
123 }
124 // swallow /vt/vn
125 var d2: i64 = 0
126 while d2 == 0 {
127 if i >= n { d2 = 1 } else {
128 if buf[i] == (47 as u8) { i = i + 1 } else {
129 if oi_is_digit(buf[i] as i64) == 1 { i = i + 1 } else {
130 if buf[i] == (45 as u8) { i = i + 1 } else { d2 = 1 }
131 }
132 }
133 }
134 }
135 p[0] = i
136 if got == 0 { return 0 }
137 ok[0] = 1
138 if neg == 1 { return 0 - v }
139 return v
140}
141
142// Read an OBJ file into vbuf (nv*3 fx1024) and fbuf (nf*3 zero-based indices).
143// Returns 0 on success; -1 unreadable, -2 empty, -3 vertex overflow, -4 face overflow,
144// -5 a face referenced a vertex that does not exist.
145func oi_read(path: *u8, vbuf: *i64, fbuf: *i64, vcap: i64, fcap: i64, counts: *i64) -> i64 {
146 counts[0] = 0
147 counts[1] = 0
148 let cl: *i64 = sys_mmap(16) as *i64
149 cl[0] = 0
150 let buf: *u8 = sys_read_file(path, cl)
151 let n: i64 = cl[0]
152 if (buf as i64) == 0 { return 0 - 1 }
153 if n <= 0 { return 0 - 2 }
154 let p: *i64 = sys_mmap(16) as *i64
155 let ok: *i64 = sys_mmap(16) as *i64
156 let poly: *i64 = sys_mmap(64 * 8) as *i64
157 p[0] = 0
158 var nv: i64 = 0
159 var nf: i64 = 0
160 var rc: i64 = 0
161 var done: i64 = 0
162 while done == 0 {
163 if p[0] >= n { done = 1 } else {
164 oi_ws(buf, n, p)
165 let s: i64 = p[0]
166 if s >= n { done = 1 } else {
167 let c0: i64 = buf[s] as i64
168 var handled: i64 = 0
169 if c0 == 118 {
170 // 'v' -- but only bare "v ", not vt / vn / vp
171 let c1: i64 = buf[s+1] as i64
172 if c1 == 32 { handled = 1 } else { if c1 == 9 { handled = 1 } }
173 if handled == 1 {
174 p[0] = s + 1
175 let x: i64 = oi_fx(buf, n, p, ok)
176 let y: i64 = oi_fx(buf, n, p, ok)
177 let z: i64 = oi_fx(buf, n, p, ok)
178 if nv >= vcap { rc = 0 - 3; done = 1 } else {
179 vbuf[nv*3] = x; vbuf[nv*3+1] = y; vbuf[nv*3+2] = z
180 nv = nv + 1
181 }
182 }
183 }
184 if c0 == 102 {
185 // 'f' -- collect every vertex ref on the line, then FAN-TRIANGULATE
186 handled = 1
187 p[0] = s + 1
188 var np: i64 = 0
189 var d4: i64 = 0
190 while d4 == 0 {
191 if p[0] >= n { d4 = 1 } else {
192 if buf[p[0]] == (10 as u8) { d4 = 1 } else {
193 let r: i64 = oi_faceref(buf, n, p, ok)
194 if ok[0] == 0 { d4 = 1 } else {
195 var idx: i64 = r
196 if idx < 0 { idx = nv + idx } else { idx = idx - 1 }
197 if idx < 0 { rc = 0 - 5; d4 = 1 } else {
198 if idx >= nv { rc = 0 - 5; d4 = 1 } else {
199 if np < 64 { poly[np] = idx; np = np + 1 }
200 }
201 }
202 }
203 }
204 }
205 }
206 if rc == 0 {
207 var k: i64 = 1
208 while k + 1 < np {
209 if nf >= fcap { rc = 0 - 4; k = np } else {
210 fbuf[nf*3] = poly[0]; fbuf[nf*3+1] = poly[k]; fbuf[nf*3+2] = poly[k+1]
211 nf = nf + 1
212 k = k + 1
213 }
214 }
215 }
216 if rc != 0 { done = 1 }
217 }
218 if done == 0 { oi_eol(buf, n, p) }
219 }
220 }
221 }
222 sys_munmap(p as *u8, 16)
223 sys_munmap(ok as *u8, 16)
224 sys_munmap(poly as *u8, 64 * 8)
225 if rc != 0 { return rc }
226 counts[0] = nv
227 counts[1] = nf
228 if nv == 0 { return 0 - 2 }
229 return 0
230}
231
232// Source-space exact decimal geometry. Legacy oi_read remains fx1024.
233// Decimal exponent is representation metadata, never a physical-unit assertion.
234const OIE_MAX:i64=9223372036854775807
235const OIE_MIN:i64=0-9223372036854775807-1
236const OIE_SYNTAX:i64=-6
237const OIE_RANGE:i64=-7
238const OIE_BUFFER:i64=-8
239const OIE_TRIANGLES_ONLY:i64=0
240const OIE_FAN_UNQUALIFIED:i64=1
241struct OiExactView {
242 nv:i64,
243 nt:i64,
244 exponent10:i64,
245 nonzero:i64,
246 errorOffset:i64,
247 status:i64,
248 sourceBytes:i64,
249 unitState:i64,
250 basisState:i64,
251 sourceFaces:i64,
252 polygonFaces:i64,
253 fanTriangles:i64,
254 triangulation:i64
255}
256func oi_exact_reset(o:*OiExactView,n:i64)->i64 {
257 o.nv=0;o.nt=0;o.exponent10=0;o.nonzero=0;o.errorOffset=0;o.status=0;o.sourceBytes=n
258 o.unitState=0;o.basisState=0;o.sourceFaces=0;o.polygonFaces=0;o.fanTriangles=0;o.triangulation=OIE_TRIANGLES_ONLY;return 0
259}
260// Full token grammar: signed decimal with optional exponent. Output is normalized
261// signed significand/exponent; all arithmetic refuses nonrepresentable values.
262func oi_decimal_exact(s:*u8,n:i64,value:*i64,power:*i64)->i64 {
263 if (s as i64)<=0||(value as i64)<=0||(power as i64)<=0||n<=0{return OIE_SYNTAX}
264 var i:i64=0;var neg:i64=0
265 if s[0]==45{neg=1;i=1}else{if s[0]==43{i=1}}
266 if i>=n{return OIE_SYNTAX}
267 let start:i64=i;var dot:i64=0;var digits:i64=0;var fraction:i64=0;var last:i64=-1;var trailing:i64=0
268 while i<n{
269 let c:i64=s[i] as i64
270 if c==101||c==69{break}
271 if c==46{if dot==1{return OIE_SYNTAX};dot=1}else{
272 if oi_is_digit(c)==0{return OIE_SYNTAX}
273 digits=digits+1;if dot==1{fraction=fraction+1}
274 if c!=48{last=i;trailing=0}else{trailing=trailing+1}
275 };i=i+1
276 }
277 if digits==0{return OIE_SYNTAX}
278 let mend:i64=i;var ex:i64=0;var eneg:i64=0
279 if i<n{
280 i=i+1;if i>=n{return OIE_SYNTAX}
281 if s[i]==45{eneg=1;i=i+1}else{if s[i]==43{i=i+1}}
282 if i>=n{return OIE_SYNTAX}
283 while i<n{
284 let d:i64=(s[i] as i64)-48;if d<0||d>9{return OIE_SYNTAX}
285 if ex>(OIE_MAX-d)/10{return OIE_RANGE};ex=ex*10+d;i=i+1
286 }
287 }
288 if last<0{value[0]=0;power[0]=0;return 0}
289 if eneg==1{ex=0-ex}
290 // -fraction + trailing lies within the token length, so the subtraction is safe.
291 let adjust:i64=trailing-fraction
292 if adjust>0{if ex>OIE_MAX-adjust{return OIE_RANGE}}else{if ex<OIE_MIN-adjust{return OIE_RANGE}}
293 ex=ex+adjust
294 // Negative accumulation admits the full signed-i64 range, including MIN.
295 var a:i64=0;var limit:i64=0-OIE_MAX;if neg==1{limit=OIE_MIN}
296 i=start
297 while i<=last{
298 let c:i64=s[i] as i64
299 if c!=46{
300 let d:i64=c-48
301 if a<limit/10{return OIE_RANGE}
302 a=a*10;if a<limit+d{return OIE_RANGE};a=a-d
303 };i=i+1
304 }
305 if neg==0{a=0-a};value[0]=a;power[0]=ex;return 0
306}
307func oi_exact_scaled(v:i64,ex:i64,target:i64,out:*i64)->i64 {
308 if v==0{out[0]=0;return 0};if ex<target{return OIE_RANGE}
309 var a:i64=v;var e:i64=ex
310 // No subtraction of remote exponents; overflow stops after representable digits.
311 while e>target{
312 if a>OIE_MAX/10||a<OIE_MIN/10{return OIE_RANGE}
313 a=a*10;e=e-1
314 };out[0]=a;return 0
315}
316func oi_exact_space(c:i64)->i64 {if c==32||c==9||c==13{return 1};return 0}
317func oi_exact_token(b:*u8,end:i64,p:*i64,len:*i64)->i64 {
318 var i:i64=p[0];while i<end{if oi_exact_space(b[i] as i64)==0{break};i=i+1}
319 let start:i64=i
320 while i<end{if oi_exact_space(b[i] as i64)==1{break};if b[i]==35{break};i=i+1}
321 p[0]=i;len[0]=i-start;return start
322}
323// Face references share source ordering with oi_read. Checked indices replace
324// unchecked integer accumulation only in this new strict boundary.
325func oi_exact_ref(s:*u8,n:i64,nv:i64,out:*i64)->i64 {
326 if n<=0{return OIE_SYNTAX};var i:i64=0;var neg:i64=0
327 if s[0]==45{neg=1;i=1}else{if s[0]==43{i=1}}
328 var a:i64=0;var digits:i64=0
329 while i<n{
330 let c:i64=s[i] as i64;if c==47{break}
331 if oi_is_digit(c)==0{return OIE_SYNTAX};let d:i64=c-48
332 if a>(OIE_MAX-d)/10{return OIE_RANGE};a=a*10+d;digits=digits+1;i=i+1
333 }
334 if digits==0||a==0{return OIE_SYNTAX}
335 var index:i64=a-1;if neg==1{if a>nv{return -5};index=nv-a}
336 if index<0||index>=nv{return -5}
337 var fields:i64=0
338 while i<n{
339 if s[i]!=47{return OIE_SYNTAX};fields=fields+1;if fields>2{return OIE_SYNTAX};i=i+1
340 var count:i64=0;var signedField:i64=0
341 if i<n{if s[i]==45||s[i]==43{signedField=1;i=i+1}}
342 while i<n{if s[i]==47{break};if oi_is_digit(s[i] as i64)==0{return OIE_SYNTAX};count=count+1;i=i+1}
343 // Empty vt is valid only in v//vn. Empty final component is malformed.
344 if count==0{if signedField!=0{return OIE_SYNTAX};if i>=n{return OIE_SYNTAX};if fields!=1{return OIE_SYNTAX}}
345 }
346 out[0]=index;return 0
347}
348func oi_exact_scan(b:*u8,n:i64,v:*i64,f:*i64,o:*OiExactView,emit:i64,vcap:i64,fcap:i64)->i64 {
349 var pos:i64=0;var nv:i64=0;var nt:i64=0;var faces:i64=0;var polygons:i64=0;var fan:i64=0
350 while pos<n{
351 var end:i64=pos;while end<n{if b[end]==10{break};end=end+1}
352 var p:i64=pos;var len:i64=0
353 let tag:i64=oi_exact_token(b,end,&p,&len)
354 if len==1{
355 if b[tag]==118{
356 var axis:i64=0
357 while axis<3{
358 let start:i64=oi_exact_token(b,end,&p,&len);o.errorOffset=start
359 if len<=0{return OIE_SYNTAX}
360 var val:i64=0;var ex:i64=0
361 let rc:i64=oi_decimal_exact((b as i64+start) as *u8,len,&val,&ex);if rc!=0{return rc}
362 if emit==0{if val!=0{if o.nonzero==0||ex<o.exponent10{o.exponent10=ex};o.nonzero=1}}else{
363 var scaled:i64=0;let sr:i64=oi_exact_scaled(val,ex,o.exponent10,&scaled);if sr!=0{return sr}
364 if nv>=vcap{return -3};v[nv*3+axis]=scaled
365 };axis=axis+1
366 }
367 oi_exact_token(b,end,&p,&len);if len!=0{return OIE_SYNTAX}
368 if nv>=OIE_MAX/(3*__size_of(i64)){return OIE_RANGE};nv=nv+1
369 }else{if b[tag]==102{
370 var count:i64=0;var first:i64=0;var previous:i64=0
371 while p<end{
372 let start:i64=oi_exact_token(b,end,&p,&len);if len==0{break};o.errorOffset=start
373 var index:i64=0;let rc:i64=oi_exact_ref((b as i64+start) as *u8,len,nv,&index);if rc!=0{return rc}
374 if count==0{first=index}else{if count>=2{
375 if nt>=OIE_MAX/(3*__size_of(i64)){return OIE_RANGE}
376 if emit==1{if nt>=fcap{return -4};f[nt*3]=first;f[nt*3+1]=previous;f[nt*3+2]=index};nt=nt+1
377 }};previous=index;count=count+1
378 }
379 if count<3{return OIE_SYNTAX};faces=faces+1
380 if count>3{polygons=polygons+1;fan=fan+count-2}
381 }}
382 }
383 pos=end;if pos<n{pos=pos+1}
384 }
385 if emit==0{o.nv=nv;o.nt=nt;o.sourceFaces=faces;o.polygonFaces=polygons;o.fanTriangles=fan;if polygons>0{o.triangulation=OIE_FAN_UNQUALIFIED}}else{if nv!=o.nv||nt!=o.nt||faces!=o.sourceFaces||polygons!=o.polygonFaces||fan!=o.fanTriangles{return OIE_SYNTAX}};if nv==0{return -2};return 0
386}
387// Borrowed source bytes are kept unchanged. Counts are sizing information until status==0.
388// Partial caller buffers on failure are invalid and must not be consumed.
389func oi_read_exact_bytes(b:*u8,n:i64,v:*i64,f:*i64,vcap:i64,fcap:i64,o:*OiExactView)->i64 {
390 if (o as i64)<=0{return OIE_BUFFER};oi_exact_reset(o,n)
391 if (b as i64)<=0||n<=0||vcap<0||fcap<0{o.status=OIE_BUFFER;return o.status}
392 var rc:i64=oi_exact_scan(b,n,v,f,o,0,vcap,fcap)
393 if rc==0{
394 if o.nv>vcap{rc=-3}else{if o.nt>fcap{rc=-4}else{
395 if (v as i64)<=0{rc=OIE_BUFFER}else{if o.nt>0&&(f as i64)<=0{rc=OIE_BUFFER}else{rc=oi_exact_scan(b,n,v,f,o,1,vcap,fcap)}}
396 }}
397 }
398 o.status=rc;return rc
399}
400func oi_read_exact(path:*u8,v:*i64,f:*i64,vcap:i64,fcap:i64,o:*OiExactView)->i64 {
401 if (o as i64)<=0{return OIE_BUFFER};oi_exact_reset(o,0)
402 var n:i64=0;let b:*u8=sys_map_file(path,&n)
403 if (b as i64)==0{o.status=-1;return -1}
404 let rc:i64=oi_read_exact_bytes(b,n,v,f,vcap,fcap,o);sys_munmap_direct(b,n);return rc
405}