nx_uvunwrap_lib.nx source
↩ module page · 1642 lines · 75241 B
1// nx_uvunwrap_lib.nx -- A GENERAL UV UNWRAP FOR AN ARBITRARY MESH, WITH A DISTORTION REFEREE (/compare/dcc DC5).
2//
3// WHAT WAS ABSENT, MEASURED NOT ASSERTED. nx_capsearch over 6,892 organs (1,375 tools + 5,517 libs,
4// corpus_complete=1) returned NO general parameterisation: the estate's only unwrap is ntx_apply in
5// nx_nxa_texc_lib, a bone-anchored CYLINDRICAL map that reads SKEL parents and SKIN weights and refuses
6// outright without them ("no SKIN section -- refusing to fabricate bone ownership"). That is the right
7// design for a humanoid NXA and it is unreachable for everything else: a sculpt, a boolean result, a
8// terrain patch, a retopologised prop has no skeleton to anchor to, so it could not be unwrapped AT ALL,
9// and texture painting was blocked for every non-humanoid asset in the estate.
10//
11// OUTPUT IS WIRE-COMPATIBLE WITH THE INCUMBENT BAKE PATH ON PURPOSE. uv_emit_texc writes exactly the TEXC
12// payload nx_nxa_texbake_lib already consumes -- header [nv][stride 3][atlas grid g][reserved 0] then per
13// vertex [u Q16][v Q16][chart], charts owning disjoint tiles of a g x g grid with a one-unit gutter, the
14// same shape and the same fixed-point scale ntx_apply emits. So a mesh unwrapped here bakes through the
15// existing sampler with nothing else changed.
16// BUT THE BRIDGE IS LOSSY AND IT SAYS SO. TEXC is PER VERTEX and a seam is exactly the place where one
17// mesh vertex needs TWO texture coordinates, so the incumbent wire format cannot represent a seam at
18// all. This library solves per CORNER (a mesh vertex split by seams into several UV vertices, which is
19// what makes a cut actually cut), and uv_emit_texc COUNTS the vertices whose extra coordinates the
20// per-vertex form drops, into UV_H_TEXCLOSS. A lossy conversion that reports zero loss would be the
21// defect; one that names its loss is a bridge.
22//
23// THE HONEST ALGORITHM. A least-squares conformal map (Levy et al., LSCM) needs a linear solve, and there
24// is no linear solver in this estate and no float type to build one on. So this is an ITERATIVE relaxation
25// toward the same stationary condition, and every part of that choice is declared rather than hidden:
26// * The energy is the LSCM energy in its Dirichlet-minus-area form, E = sum_edges W|dU|^2 - 2 sum_t A_t.
27// THAT SECOND TERM IS THE WHOLE REASON THIS DOES NOT COLLAPSE. A pure harmonic (Dirichlet-only)
28// relaxation with a free boundary and two pinned vertices has its minimum at the degenerate map that
29// puts every vertex on the segment between the pins -- a fold, scoring zero by every naive measure.
30// The -2*area term rewards area and makes the non-degenerate map the minimiser instead.
31// * Setting dE/dU_p = 0 gives a closed-form per-vertex update, so this is a damped Jacobi sweep over
32// the exact stationary condition rather than an invented heuristic. Interior vertices see a pure
33// cotangent-Laplacian average (their one-ring area terms telescope to zero); the area term acts only
34// on the boundary, pushing it outward. That is LSCM, iterated.
35// * TWO PINNED VERTICES PER CHART, the standard LSCM constraint: the conformal energy is invariant to
36// translation, rotation and scale, so two pins remove exactly the null space and no more.
37// * The iteration bound is DERIVED FROM THE CHART, the convergence criterion is DECLARED, and a chart
38// that reaches neither is REFUSED BY NAME (UV_E_NOCONVERGE) rather than emitted folded.
39//
40// THE REFEREE IS THE POINT. uv_distortion reports angle (quasi-conformal dilatation) and area distortion
41// in permil, mean and max, per chart and overall, measured on the COORDINATES ACTUALLY EMITTED -- after
42// atlas placement, not on some intermediate the caller never sees. Without it a fold is invisible: every
43// rival DCC shows a checker pattern and asks a person to look. A developable surface is exactly
44// parameterisable, so it must read at or near zero, and that is this file's own positive control.
45//
46// COMPOSES THE INCUMBENTS, ADDS NO SECOND RULER: mesh validity is nx_meshvalid_lib (mv_check / mv_count /
47// mv_manifold) -- there is no second topology checker here; integer square root and the fixed-point unit
48// are nx_vecmath (vm_isqrt, VM_ONE); the hash capacity rule and mixing function are meshvalid's mv_cap_for
49// and mv_mix rather than a second copy.
50//
51// ALL INTEGER, so every result is bit-exact and reproducible. No hardware writes (Rule 26).
52// license_tier: ORIGINAL
53import "nx_syscalls.nx"
54import "nx_vecmath.nx"
55import "nx_meshvalid_lib.nx"
56
57// ---- header slots ------------------------------------------------------------------------------
58const UV_H_NV: i64 = 0
59const UV_H_NT: i64 = 1
60const UV_H_ECAP: i64 = 2
61const UV_H_SCAP: i64 = 3
62const UV_H_NC: i64 = 4
63const UV_H_NUV: i64 = 5
64const UV_H_ERR: i64 = 6
65const UV_H_STAGE: i64 = 7
66const UV_H_GRID: i64 = 8
67const UV_H_NSEAM: i64 = 9
68const UV_H_ITERS: i64 = 10
69const UV_H_MAXMOVE: i64 = 11
70const UV_H_FLIP: i64 = 12
71const UV_H_SING: i64 = 13
72const UV_H_UVDEGEN: i64 = 14
73const UV_H_NOCONV: i64 = 15
74const UV_H_ANGMEAN: i64 = 16
75const UV_H_ANGMAX: i64 = 17
76const UV_H_AREAMEAN: i64 = 18
77const UV_H_AREAMAX: i64 = 19
78const UV_H_OVERLAP: i64 = 20
79const UV_H_TEXCLOSS: i64 = 21
80const UV_H_MVERR: i64 = 22
81const UV_H_MVCLOSED: i64 = 23
82const UV_H_NCORN: i64 = 24
83// ---- array offsets, computed ONCE by uv_layout and stored, never recomputed by a caller ----------
84const UV_O_VERT: i64 = 25
85const UV_O_TRI: i64 = 26
86const UV_O_ETAB: i64 = 27
87const UV_O_STAB: i64 = 28
88const UV_O_TCHART: i64 = 29
89const UV_O_STACK: i64 = 30
90const UV_O_CSTART: i64 = 31
91const UV_O_CORDER: i64 = 32
92const UV_O_UVF: i64 = 33
93const UV_O_UVSRC: i64 = 34
94const UV_O_UVCH: i64 = 35
95const UV_O_WU: i64 = 36
96const UV_O_WV: i64 = 37
97const UV_O_ACCU: i64 = 38
98const UV_O_ACCV: i64 = 39
99const UV_O_ACCW: i64 = 40
100const UV_O_ACCAU: i64 = 41
101const UV_O_ACCAV: i64 = 42
102const UV_O_CUV: i64 = 43
103const UV_O_TANG: i64 = 44
104const UV_O_TRAT: i64 = 45
105const UV_O_TSAR: i64 = 46
106const UV_O_CBOX: i64 = 47
107const UV_O_CCNT: i64 = 48
108const UV_O_CDIST: i64 = 49
109const UV_O_LIST: i64 = 50
110const UV_O_STAMP: i64 = 51
111const UV_O_TSEEN: i64 = 52
112const UV_O_PLACED: i64 = 53
113const UV_O_ROOT: i64 = 54
114// THE HEADER MUST OUTRANK EVERY SLOT IT USES. nx_meshvalid shipped with a header one word too short and
115// its first-offender slots silently ALIASED vertex 0, moving a corner; every topology class still passed
116// and the only symptom was one detector that could never fire. Highest slot in use here is 53, so 64
117// leaves ten words of headroom AND a structural tooth in the gate asserts the relation arithmetically.
118const UV_HDR: i64 = 64
119const UV_I64: i64 = 8
120const UV_TRI: i64 = 3
121const UV_EREC: i64 = 4 // edge record: [lo, hi, tri0+1, tri1+1]
122const UV_SREC: i64 = 3 // seam record: [lo, hi, used]
123// ...AND THE SLOT NAMES FOR BOTH, because a layout that lives only in the comment above gets hand-counted
124// at every access, which is the same shape as writing the width twice. The UV_ED_/UV_SM_ prefixes avoid
125// the UV_E_ refusal-code space deliberately: two constant families one character apart is its own hazard.
126const UV_ED_LO: i64 = 0
127const UV_ED_HI: i64 = 1
128const UV_ED_T0: i64 = 2 // first triangle, stored +1 so that 0 reads as EMPTY
129const UV_ED_T1: i64 = 3 // second triangle, same +1 bias
130const UV_SM_LO: i64 = 0
131const UV_SM_HI: i64 = 1
132const UV_SM_USED: i64 = 2
133const UV_ACCN: i64 = 5 // accU accV accW accAU accAV
134
135// THREE MORE RECORDS, WHOSE WIDTHS THE LAYOUT USED TO HAND-WRITE. UV_EREC and UV_SREC above set the
136// convention and these three were left as bare widths -- written once in uv_layout, again in
137// uv_words_used, and again at every accessor: four independent copies of one shape. A width written
138// beside a record is a second copy of that record's layout, and the two drift silently, because widening
139// the record leaves every bare + 2 / + 3 still compiling and still reading a word, just the wrong one,
140// with no diagnostic anywhere. The field names below are the single source of truth for the slot order.
141const UV_CUVREC: i64 = 2 // per-corner uv pair: [u, v]
142const UV_BOXREC: i64 = 4 // per-chart uv bounds: [u0, v0, u1, v1]
143const UV_DISTREC: i64 = 4 // per-chart distortion: [amean, amax, dmean, dmax]
144const UV_P_U: i64 = 0
145const UV_P_V: i64 = 1
146const UV_B_U0: i64 = 0
147const UV_B_V0: i64 = 1
148const UV_B_U1: i64 = 2
149const UV_B_V1: i64 = 3
150const UV_D_AMEAN: i64 = 0
151const UV_D_AMAX: i64 = 1
152const UV_D_DMEAN: i64 = 2
153const UV_D_DMAX: i64 = 3
154
155// THE TEXC WIRE HEADER, WHOSE WIDTH WAS HAND-WRITTEN AT EVERY SLOT THAT FOLLOWS IT. Four words --
156// [nv][stride][grid][reserved] -- counted once as a size in uv_texc_words and then again as a bare + 4
157// payload base at each of the eight accesses in uv_emit_texc. Widen the header and every one of those
158// eight still compiles, still reads a word, and addresses the wrong one.
159const UV_X_HDR: i64 = 4
160const UV_X_NV: i64 = 0
161const UV_X_STRIDE: i64 = 1
162const UV_X_GRID: i64 = 2
163const UV_X_RSVD: i64 = 3
164const UV_X_U: i64 = 0 // per-vertex payload slots, relative to that vertex's record base
165const UV_X_V: i64 = 1
166const UV_X_CH: i64 = 2
167
168// ---- pipeline stages ---------------------------------------------------------------------------
169const UV_ST_NEW: i64 = 0
170const UV_ST_CHARTS: i64 = 1
171const UV_ST_SOLVED: i64 = 2
172const UV_ST_MEASURED: i64 = 3
173
174// ---- refusal codes. EVERY ONE IS ITS OWN CLASS: a compound refusal that will not name its failing
175// conjunct sends the reader to guess, and the reader always guesses the alarming one. -------------
176const UV_OK: i64 = 0
177const UV_E_ARGS: i64 = 0 - 1
178const UV_E_TOPOLOGY: i64 = 0 - 2
179const UV_E_NOPIN: i64 = 0 - 3
180const UV_E_NOCONVERGE: i64 = 0 - 4
181const UV_E_FOLD: i64 = 0 - 5
182const UV_E_UVDEGEN: i64 = 0 - 6
183const UV_E_OVERLAP: i64 = 0 - 7
184const UV_E_STAGE: i64 = 0 - 8
185const UV_E_SPAN: i64 = 0 - 9
186const UV_E_NOTRUN: i64 = 0 - 10
187// A CODE OF ITS OWN, DELIBERATELY NOT UV_E_NOPIN. The rigid unfold leaving a chart vertex unplaced and a
188// chart whose two pins land on one point are different faults with different remedies, and while they
189// shared one number the first was unreadable from outside: the symptom said "this chart has no pins" about
190// a chart whose pins were fine. Two causes behind one code is what makes a defect expensive to find.
191const UV_E_UNPLACED: i64 = 0 - 11
192
193// ---- fixed point, every constant carrying the derivation that produced it -----------------------
194// The TEXC wire scale. NOT a free choice: nx_nxa_texc_lib emits UVs as Q16 over the unit square
195// (NT_Q16 65536, NT_Q16M 65535) and nx_nxa_texbake_lib samples them at that scale, so adopting any
196// other scale would make this file a converter instead of a producer.
197const UV_Q16: i64 = 65536
198const UV_Q16M: i64 = 65535
199// The gutter, in Q16 units, left on every side of a chart's atlas tile. One unit is the smallest
200// non-zero separation the wire format can express, and it is what makes bilinear sampling at a tile
201// edge read the chart rather than its neighbour -- the same one-unit inset ntx_apply uses.
202const UV_GUTTER: i64 = 1
203// Weight scale for the cotangents. DERIVED: nx_vecmath VM_ONE is 4096 and is already the estate's
204// fixed-point unit (nx_itrig emits fx4096, nx_vmd decodes rotation at 4096). A second scale here would
205// make this file the place two scales meet.
206const UV_W_Q: i64 = 4096
207// The working span the solver relaxes in, and the u-coordinate the second pin is fixed at. DERIVED from
208// the output resolution: the final tile inner width is at most UV_Q16 - 2 = 65534, so a working span of
209// 2^20 gives at least 16 working units per emitted Q16 unit even for a single chart, i.e. the rounding
210// into the wire format costs strictly less than the convergence criterion below. Making it larger only
211// eats headroom in the sweep arithmetic; smaller and quantisation would dominate the measured distortion.
212const UV_PIN_SPAN: i64 = 1048576
213// The initial-guess space, before the similarity that lands the pins. Kept at 2^16 so that the similarity
214// multiply (guess coordinate * guess delta * UV_PIN_SPAN) stays inside i64: 2^16 * 2^17 * 2^20 = 2^53.
215const UV_GUESS_SPAN: i64 = 65536
216// Cotangent clamp. cot(1 degree) = 57.29, so a triangle whose angles are all at least one degree never
217// reaches 64; a triangle with an angle below one degree is a sliver whose weight would outrank a
218// well-formed neighbour's by three orders of magnitude and dominate the whole system. Clamping at 64
219// bounds the conditioning without touching any triangle a caller would consider usable.
220const UV_COT_CLAMP: i64 = 64
221// Convergence criterion, in working units, on the largest per-vertex move of a whole sweep (measured as
222// |du| + |dv|, the L1 move, which bounds the Euclidean one). DERIVED: 32 working units is 2 emitted Q16
223// units at the single-chart tile size, and integer division truncation makes a plus-or-minus one limit
224// cycle inevitable on each of two coordinates, so 2 emitted units is the finest criterion that can
225// actually be reached rather than orbited. At a 4096-square atlas -- the size the bake path uses -- one
226// texel is 16 Q16 units, so this is an eighth of a texel.
227const UV_CONV_EPS: i64 = 32
228// Sweep budget. DERIVED, not picked: damped Jacobi on a Laplacian-like operator over a two-dimensional
229// patch of n vertices has spectral radius about 1 - c/n, so cutting the initial error (bounded by
230// UV_PIN_SPAN) down to UV_CONV_EPS takes roughly n * ln(UV_PIN_SPAN / UV_CONV_EPS) = n * 15 sweeps.
231// 32 per vertex is a little over twice that. It is a BUDGET, not a promise: a chart that exhausts it is
232// refused by name, never emitted.
233const UV_ITER_PER_UVV: i64 = 32
234// Floor on that budget so a chart of three vertices still gets a real relaxation rather than two sweeps.
235const UV_ITER_MIN: i64 = 64
236// Damping. DERIVED: undamped Jacobi on this operator has iteration-matrix eigenvalues 1 - lambda with
237// lambda in (0,2], so the top of the spectrum oscillates at magnitude near 1. Damping by one half maps
238// them to 1 - lambda/2 in [0,1), which is the standard damped-Jacobi smoother, and no mode can amplify.
239// It is a divisor rather than a multiplier because integer division toward zero then makes the fixed
240// point exactly reachable: a move of one unit damps to zero and the sweep stops instead of orbiting.
241const UV_OMEGA_DEN: i64 = 2
242// Reduction bound for the first-fundamental-form triple (a,b,c) before the dilatation is computed. The
243// dilatation is a RATIO of singular values and is therefore invariant to a common scale on a,b,c, so
244// reducing costs nothing but overflow headroom. DERIVED: with a,b,c at or below 2^20, the products a*c
245// and b*b reach 2^40 and the permil form below reaches 2^22 * 10^6, all far inside i64.
246const UV_ABC_MAX: i64 = 1048576
247// Bound on any one component of the Ss / St vectors BEFORE they are squared. DERIVED: the sum of three
248// squares must stay inside i64, so a component is bounded by sqrt((2^63 - 1) / 3) = 1.75e9, and 1e9 keeps
249// a 1.75x margin. Reducing Ss and St by a COMMON divisor costs nothing for the same reason the (a,b,c)
250// reduction does: the dilatation is a ratio of singular values and is blind to a common scale.
251const UV_SS_MAX: i64 = 1000000000
252// Guard on the solved chart extent before atlas placement, so the placement multiply cannot overflow.
253// DERIVED: the product (chart span * tile inner width) must stay inside i64; at the largest possible
254// tile (65534) that bounds the span at 1.4e14, and 1e11 keeps three orders of headroom while remaining
255// five orders larger than any converged chart, whose span is on the order of UV_PIN_SPAN.
256const UV_SDEN_MAX: i64 = 100000000000
257const UV_PERMIL: i64 = 1000
258const UV_MIN_PINS: i64 = 2
259
260func uv_err_name(e: i64) -> *u8 {
261 if e == UV_OK { return "UNWRAPPED" as *u8 }
262 if e == UV_E_ARGS { return "REFUSED-BAD-ARGUMENTS" as *u8 }
263 if e == UV_E_TOPOLOGY { return "REFUSED-MESH-TOPOLOGY" as *u8 }
264 if e == UV_E_NOPIN { return "REFUSED-CHART-HAS-NO-TWO-PINS" as *u8 }
265 if e == UV_E_NOCONVERGE { return "REFUSED-CHART-DID-NOT-CONVERGE" as *u8 }
266 if e == UV_E_FOLD { return "REFUSED-FOLDED-CHART" as *u8 }
267 if e == UV_E_UVDEGEN { return "REFUSED-DEGENERATE-CHART-AREA" as *u8 }
268 if e == UV_E_OVERLAP { return "REFUSED-OVERLAPPING-CHARTS" as *u8 }
269 if e == UV_E_STAGE { return "REFUSED-PIPELINE-STAGE" as *u8 }
270 if e == UV_E_SPAN { return "REFUSED-CHART-EXTENT-UNPLACEABLE" as *u8 }
271 if e == UV_E_NOTRUN { return "REFUSED-UNWRAP-NOT-RUN" as *u8 }
272 if e == UV_E_UNPLACED { return "REFUSED-UNFOLD-LEFT-A-VERTEX-UNPLACED" as *u8 }
273 return "REFUSED-UNCLASSIFIED" as *u8
274}
275
276// ---- layout ------------------------------------------------------------------------------------
277// THE LAYOUT ARITHMETIC EXISTS EXACTLY ONCE. A second copy written to compute the allocation size is the
278// duplicate-ruler defect in its purest form -- two expressions that must agree, with nothing making them.
279// So uv_new runs the REAL layout over a scratch header to learn the size, then runs it again over the
280// real arena. One function, one answer, and they cannot drift.
281func uv_layout(h: *i64) -> i64 {
282 let nv: i64 = h[UV_H_NV]
283 let nt: i64 = h[UV_H_NT]
284 let nk: i64 = nt * UV_TRI
285 h[UV_H_NCORN] = nk
286 var o: i64 = UV_HDR
287 h[UV_O_VERT] = o; o = o + nv * UV_TRI
288 h[UV_O_TRI] = o; o = o + nt * UV_TRI
289 h[UV_O_ETAB] = o; o = o + h[UV_H_ECAP] * UV_EREC
290 h[UV_O_STAB] = o; o = o + h[UV_H_SCAP] * UV_SREC
291 h[UV_O_TCHART] = o; o = o + nt
292 h[UV_O_STACK] = o; o = o + nt
293 h[UV_O_CSTART] = o; o = o + nt + 1
294 h[UV_O_CORDER] = o; o = o + nt
295 h[UV_O_UVF] = o; o = o + nk
296 h[UV_O_UVSRC] = o; o = o + nk
297 h[UV_O_UVCH] = o; o = o + nk
298 h[UV_O_WU] = o; o = o + nk
299 h[UV_O_WV] = o; o = o + nk
300 h[UV_O_ACCU] = o; o = o + nk
301 h[UV_O_ACCV] = o; o = o + nk
302 h[UV_O_ACCW] = o; o = o + nk
303 h[UV_O_ACCAU] = o; o = o + nk
304 h[UV_O_ACCAV] = o; o = o + nk
305 h[UV_O_LIST] = o; o = o + nk
306 // STAMP serves two different key spaces at two different times: class ids in [0,nuv) while charts are
307 // built and relaxed, and mesh vertex ids in [0,nv) in the TEXC bridge. It is sized for BOTH rather than
308 // for whichever looks larger, because a mesh may legally carry vertices no triangle references, and a
309 // shared scratch array sized for one of its two users is an aliasing bug waiting for that input.
310 h[UV_O_STAMP] = o; o = o + nk + nv
311 h[UV_O_TSEEN] = o; o = o + nt
312 h[UV_O_PLACED] = o; o = o + nk
313 // THE UNION-FIND ROOTS, RESOLVED ONCE AND KEPT. They cannot be resolved on demand out of UVF, because
314 // the relabel that consumes them REWRITES UVF -- one array cannot be a parent forest and the answer
315 // read off it at the same time.
316 h[UV_O_ROOT] = o; o = o + nk
317 h[UV_O_CUV] = o; o = o + nk * UV_CUVREC
318 h[UV_O_TANG] = o; o = o + nt
319 h[UV_O_TRAT] = o; o = o + nt
320 h[UV_O_TSAR] = o; o = o + nt
321 h[UV_O_CBOX] = o; o = o + nt * UV_BOXREC
322 h[UV_O_CCNT] = o; o = o + nt
323 h[UV_O_CDIST] = o; o = o + nt * UV_DISTREC
324 return o
325}
326
327func uv_new(nv: i64, nt: i64) -> *i64 {
328 if nv < UV_TRI { return 0 as *i64 }
329 if nt <= 0 { return 0 as *i64 }
330 let scratch: *i64 = sys_mmap(UV_HDR * UV_I64) as *i64
331 scratch[UV_H_NV] = nv
332 scratch[UV_H_NT] = nt
333 // capacity DERIVED from the element count by the incumbent rule (meshvalid mv_cap_for: next power of
334 // two at or above four times the count, load factor at or below a quarter), so neither table has a
335 // size to outgrow and neither can silently fill. An edge count is bounded by 3 per triangle, and a
336 // seam is an edge, so both tables take the same derivation.
337 scratch[UV_H_ECAP] = mv_cap_for(nt * UV_TRI)
338 scratch[UV_H_SCAP] = mv_cap_for(nt * UV_TRI)
339 let words: i64 = uv_layout(scratch)
340 let m: *i64 = sys_mmap(words * UV_I64) as *i64
341 if (m as i64) == 0 { return m }
342 m[UV_H_NV] = nv
343 m[UV_H_NT] = nt
344 m[UV_H_ECAP] = scratch[UV_H_ECAP]
345 m[UV_H_SCAP] = scratch[UV_H_SCAP]
346 uv_layout(m)
347 m[UV_H_ERR] = UV_E_NOTRUN
348 m[UV_H_STAGE] = UV_ST_NEW
349 m[UV_H_MVERR] = MV_E_NOTRUN
350 m[UV_H_MVCLOSED] = 0 - 1
351 return m
352}
353
354func uv_words_used(m: *i64) -> i64 { return m[UV_O_CDIST] + m[UV_H_NT] * UV_DISTREC }
355
356func uv_set_vert(m: *i64, i: i64, x: i64, y: i64, z: i64) -> i64 {
357 if i < 0 { return UV_E_ARGS }
358 if i >= m[UV_H_NV] { return UV_E_ARGS }
359 let o: i64 = m[UV_O_VERT] + i * UV_TRI
360 m[o + 0] = x
361 m[o + 1] = y
362 m[o + 2] = z
363 return UV_OK
364}
365
366func uv_set_tri(m: *i64, i: i64, a: i64, b: i64, c: i64) -> i64 {
367 if i < 0 { return UV_E_ARGS }
368 if i >= m[UV_H_NT] { return UV_E_ARGS }
369 let o: i64 = m[UV_O_TRI] + i * UV_TRI
370 m[o + 0] = a
371 m[o + 1] = b
372 m[o + 2] = c
373 return UV_OK
374}
375
376func uv_vx(m: *i64, v: i64) -> i64 { return m[m[UV_O_VERT] + v * UV_TRI + 0] }
377func uv_vy(m: *i64, v: i64) -> i64 { return m[m[UV_O_VERT] + v * UV_TRI + 1] }
378func uv_vz(m: *i64, v: i64) -> i64 { return m[m[UV_O_VERT] + v * UV_TRI + 2] }
379func uv_ti(m: *i64, t: i64, k: i64) -> i64 { return m[m[UV_O_TRI] + t * UV_TRI + k] }
380
381// ---- (1) SEAMS AND CHARTS ----------------------------------------------------------------------
382// A seam is an undirected vertex pair. It does two distinct things and both matter: it blocks the flood
383// fill, so triangles on either side land in different charts when that is the only path between them;
384// and it blocks the corner merge, so a vertex ON the seam becomes two UV vertices. WITHOUT THE SECOND
385// HALF A SEAM DOES NOTHING USEFUL ON A CLOSED LOOP -- cutting a cylinder along one edge leaves the
386// triangles still connected the long way round, so the chart is unchanged, and only splitting the
387// vertices actually turns the annulus into a disk. That case is a gate fixture, not a hypothetical.
388func uv_seam_set(m: *i64, a: i64, b: i64) -> i64 {
389 if m[UV_H_STAGE] != UV_ST_NEW { m[UV_H_ERR] = UV_E_STAGE; return UV_E_STAGE }
390 if a < 0 { return UV_E_ARGS }
391 if b < 0 { return UV_E_ARGS }
392 if a >= m[UV_H_NV] { return UV_E_ARGS }
393 if b >= m[UV_H_NV] { return UV_E_ARGS }
394 if a == b { return UV_E_ARGS }
395 var lo: i64 = a
396 var hi: i64 = b
397 if lo > hi { lo = b; hi = a }
398 let cap: i64 = m[UV_H_SCAP]
399 let base: i64 = m[UV_O_STAB]
400 var s: i64 = mv_mix(lo, hi, cap)
401 var guard: i64 = 0
402 while guard < cap {
403 let o: i64 = base + s * UV_SREC
404 if m[o + UV_SM_USED] == 0 {
405 m[o + UV_SM_LO] = lo
406 m[o + UV_SM_HI] = hi
407 m[o + UV_SM_USED] = 1
408 m[UV_H_NSEAM] = m[UV_H_NSEAM] + 1
409 return UV_OK
410 }
411 if m[o + UV_SM_LO] == lo { if m[o + UV_SM_HI] == hi { return UV_OK } }
412 s = s + 1
413 if s >= cap { s = 0 }
414 guard = guard + 1
415 }
416 return UV_E_ARGS
417}
418
419func uv_is_seam(m: *i64, a: i64, b: i64) -> i64 {
420 var lo: i64 = a
421 var hi: i64 = b
422 if lo > hi { lo = b; hi = a }
423 let cap: i64 = m[UV_H_SCAP]
424 let base: i64 = m[UV_O_STAB]
425 var s: i64 = mv_mix(lo, hi, cap)
426 var guard: i64 = 0
427 while guard < cap {
428 let o: i64 = base + s * UV_SREC
429 if m[o + UV_SM_USED] == 0 { return 0 }
430 if m[o + UV_SM_LO] == lo { if m[o + UV_SM_HI] == hi { return 1 } }
431 s = s + 1
432 if s >= cap { s = 0 }
433 guard = guard + 1
434 }
435 return 0
436}
437
438// edge slot, inserting on first sight. Records the (at most two) triangles that use the edge; a third
439// user is a non-manifold edge, which meshvalid has already refused before this runs.
440func uvi_edge_slot(m: *i64, a: i64, b: i64, t: i64) -> i64 {
441 var lo: i64 = a
442 var hi: i64 = b
443 if lo > hi { lo = b; hi = a }
444 let cap: i64 = m[UV_H_ECAP]
445 let base: i64 = m[UV_O_ETAB]
446 var s: i64 = mv_mix(lo, hi, cap)
447 var guard: i64 = 0
448 while guard < cap {
449 let o: i64 = base + s * UV_EREC
450 if m[o + UV_ED_T0] == 0 {
451 m[o + UV_ED_LO] = lo
452 m[o + UV_ED_HI] = hi
453 m[o + UV_ED_T0] = t + 1
454 return s
455 }
456 if m[o + UV_ED_LO] == lo { if m[o + UV_ED_HI] == hi {
457 if m[o + UV_ED_T1] == 0 { m[o + UV_ED_T1] = t + 1 }
458 return s
459 } }
460 s = s + 1
461 if s >= cap { s = 0 }
462 guard = guard + 1
463 }
464 return 0 - 1
465}
466
467func uvi_edge_find(m: *i64, a: i64, b: i64) -> i64 {
468 var lo: i64 = a
469 var hi: i64 = b
470 if lo > hi { lo = b; hi = a }
471 let cap: i64 = m[UV_H_ECAP]
472 let base: i64 = m[UV_O_ETAB]
473 var s: i64 = mv_mix(lo, hi, cap)
474 var guard: i64 = 0
475 while guard < cap {
476 let o: i64 = base + s * UV_EREC
477 if m[o + UV_ED_T0] == 0 { return 0 - 1 }
478 if m[o + UV_ED_LO] == lo { if m[o + UV_ED_HI] == hi { return s } }
479 s = s + 1
480 if s >= cap { s = 0 }
481 guard = guard + 1
482 }
483 return 0 - 1
484}
485
486func uvi_find(f: *i64, base: i64, x: i64) -> i64 {
487 var r: i64 = x
488 while f[base + r] != r { f[base + r] = f[base + f[base + r]]; r = f[base + r] }
489 return r
490}
491
492func uvi_union(f: *i64, base: i64, a: i64, b: i64) -> i64 {
493 let ra: i64 = uvi_find(f, base, a)
494 let rb: i64 = uvi_find(f, base, b)
495 if ra == rb { return 0 }
496 if ra < rb { f[base + rb] = ra } else { f[base + ra] = rb }
497 return 1
498}
499
500// which corner of triangle t names vertex v, or -1
501func uvi_corner_of(m: *i64, t: i64, v: i64) -> i64 {
502 if uv_ti(m, t, 0) == v { return 0 }
503 if uv_ti(m, t, 1) == v { return 1 }
504 if uv_ti(m, t, 2) == v { return 2 }
505 return 0 - 1
506}
507
508// COMPOSE THE INCUMBENT REFEREE, DO NOT WRITE A SECOND ONE. nx_meshvalid_lib already names every mesh
509// defect class with a count and a first offender, so validity here is one call into it.
510// THE WHOLE-MESH VERDICT IS THE WRONG CONJUNCT AND USING IT WOULD BE A BUG. mv_manifold demands a
511// CLOSED surface, and every chart worth unwrapping is open -- a flat quad, a terrain patch and a cut
512// cylinder all have boundary edges by definition, so mv_manifold is 0 for all of them and reading it
513// as the gate would refuse every legitimate input. What actually blocks a parameterisation is a
514// non-manifold edge (no consistent neighbourhood), an out-of-range index, a degenerate triangle (no
515// defined normal) and inconsistent winding (no coherent orientation to preserve). Those four counters
516// are read; MV_C_BOUND is deliberately NOT, and mv_manifold is recorded as a reported fact instead.
517func uvi_validate(m: *i64) -> i64 {
518 let nv: i64 = m[UV_H_NV]
519 let nt: i64 = m[UV_H_NT]
520 let mv: *i64 = mv_new(nv, nt)
521 if (mv as i64) == 0 { return UV_E_ARGS }
522 var i: i64 = 0
523 while i < nv {
524 mv_set_vert(mv, i, uv_vx(m, i), uv_vy(m, i), uv_vz(m, i))
525 i = i + 1
526 }
527 var t: i64 = 0
528 while t < nt {
529 mv_set_tri(mv, t, uv_ti(m, t, 0), uv_ti(m, t, 1), uv_ti(m, t, 2))
530 t = t + 1
531 }
532 let rc: i64 = mv_check(mv)
533 m[UV_H_MVERR] = rc
534 m[UV_H_MVCLOSED] = mv_manifold(mv)
535 if mv_count(mv, MV_C_INDEX) > 0 { return UV_E_TOPOLOGY }
536 if mv_count(mv, MV_C_DEGEN) > 0 { return UV_E_TOPOLOGY }
537 if mv_count(mv, MV_C_NONMAN) > 0 { return UV_E_TOPOLOGY }
538 if mv_count(mv, MV_C_WIND) > 0 { return UV_E_TOPOLOGY }
539 return UV_OK
540}
541
542func uv_chart_build(m: *i64) -> i64 {
543 let vrc: i64 = uvi_validate(m)
544 if vrc != UV_OK { m[UV_H_ERR] = vrc; return vrc }
545 let nt: i64 = m[UV_H_NT]
546 let nk: i64 = m[UV_H_NCORN]
547 let tch: i64 = m[UV_O_TCHART]
548 let stk: i64 = m[UV_O_STACK]
549 let uvf: i64 = m[UV_O_UVF]
550
551 // edge table from the triangles
552 var t: i64 = 0
553 while t < nt {
554 var k: i64 = 0
555 while k < UV_TRI {
556 let a: i64 = uv_ti(m, t, k)
557 let b: i64 = uv_ti(m, t, (k + 1) % UV_TRI)
558 if uvi_edge_slot(m, a, b, t) < 0 { m[UV_H_ERR] = UV_E_ARGS; return UV_E_ARGS }
559 k = k + 1
560 }
561 t = t + 1
562 }
563
564 // flood fill triangles across shared NON-SEAM edges
565 t = 0
566 while t < nt { m[tch + t] = 0 - 1; t = t + 1 }
567 var nc: i64 = 0
568 var seed: i64 = 0
569 while seed < nt {
570 if m[tch + seed] < 0 {
571 var sp: i64 = 0
572 m[stk + sp] = seed
573 sp = sp + 1
574 m[tch + seed] = nc
575 while sp > 0 {
576 sp = sp - 1
577 let cur: i64 = m[stk + sp]
578 var k2: i64 = 0
579 while k2 < UV_TRI {
580 let a: i64 = uv_ti(m, cur, k2)
581 let b: i64 = uv_ti(m, cur, (k2 + 1) % UV_TRI)
582 if uv_is_seam(m, a, b) == 0 {
583 let es: i64 = uvi_edge_find(m, a, b)
584 if es >= 0 {
585 let eo: i64 = m[UV_O_ETAB] + es * UV_EREC
586 var w: i64 = 2
587 while w < UV_EREC {
588 let nb: i64 = m[eo + w] - 1
589 if nb >= 0 { if m[tch + nb] < 0 {
590 m[tch + nb] = nc
591 m[stk + sp] = nb
592 sp = sp + 1
593 } }
594 w = w + 1
595 }
596 }
597 }
598 k2 = k2 + 1
599 }
600 }
601 nc = nc + 1
602 }
603 seed = seed + 1
604 }
605 m[UV_H_NC] = nc
606
607 // UV VERTICES: union corners across shared NON-SEAM edges. Two corners naming the same mesh vertex
608 // are the same texture coordinate only if a path of non-seam edges joins them WITHIN the chart. This
609 // is what makes a cut cut, and it also handles a pinch vertex (one mesh vertex whose two triangle
610 // fans meet only at that point) correctly and for free -- the two fans simply never merge.
611 var kk: i64 = 0
612 while kk < nk { m[uvf + kk] = kk; kk = kk + 1 }
613 t = 0
614 while t < nt {
615 var k3: i64 = 0
616 while k3 < UV_TRI {
617 let a: i64 = uv_ti(m, t, k3)
618 let b: i64 = uv_ti(m, t, (k3 + 1) % UV_TRI)
619 if uv_is_seam(m, a, b) == 0 {
620 let es2: i64 = uvi_edge_find(m, a, b)
621 if es2 >= 0 {
622 let eo2: i64 = m[UV_O_ETAB] + es2 * UV_EREC
623 let t0: i64 = m[eo2 + UV_ED_T0] - 1
624 let t1: i64 = m[eo2 + UV_ED_T1] - 1
625 if t1 >= 0 { if t0 >= 0 {
626 let ca0: i64 = uvi_corner_of(m, t0, a)
627 let ca1: i64 = uvi_corner_of(m, t1, a)
628 let cb0: i64 = uvi_corner_of(m, t0, b)
629 let cb1: i64 = uvi_corner_of(m, t1, b)
630 if ca0 >= 0 { if ca1 >= 0 { uvi_union(m, uvf, t0 * UV_TRI + ca0, t1 * UV_TRI + ca1) } }
631 if cb0 >= 0 { if cb1 >= 0 { uvi_union(m, uvf, t0 * UV_TRI + cb0, t1 * UV_TRI + cb1) } }
632 } }
633 }
634 }
635 k3 = k3 + 1
636 }
637 t = t + 1
638 }
639 // relabel roots to dense class ids 0..nuv-1, and record each class's mesh vertex and chart
640 let uvsrc: i64 = m[UV_O_UVSRC]
641 let uvch: i64 = m[UV_O_UVCH]
642 let stamp: i64 = m[UV_O_STAMP]
643 let root: i64 = m[UV_O_ROOT]
644 // RESOLVE EVERY ROOT BEFORE REWRITING A SINGLE PARENT, AND KEEP THE TWO IN ARRAYS OF THEIR OWN. UVF is
645 // the union-find parent forest AND the corner-to-class map, and this relabel turns it from the first
646 // into the second IN PLACE. A find() run after the first corner has been rewritten therefore walks a
647 // CLASS ID as though it were a parent index -- and the old code hid the class behind a 0-2-cls
648 // encoding, which makes it NEGATIVE, so the walk indexed BEFORE the array and both read and wrote the
649 // CSTART and CORDER slots that sit underneath it.
650 // MEASURED ON THE FLAT QUAD, this file's own positive control, before the fix: corner 4 resolved to
651 // class 0 instead of class 2, so triangle 1 carried two corners in one class and could only ever emit
652 // a collapsed image; mesh vertex 0 lost its class entirely, which is why uv_emit_texc reported ONE
653 // lost corner on an UNSEAMED mesh -- arithmetically impossible when the map is the bijection an
654 // unseamed mesh guarantees; and UVSRC was corrupted underneath the solver, so a surface that has an
655 // exact isometry into the plane read 987 permil of angle distortion and refused UV_E_UVDEGEN.
656 // TWO JOBS SHARING ONE ARRAY IS THE WHOLE DEFECT. The extra nk words are the price of them not being
657 // able to interfere, and the relabel below never reads a slot it has already written.
658 kk = 0
659 while kk < nk { m[root + kk] = uvi_find(m, uvf, kk); kk = kk + 1 }
660 kk = 0
661 while kk < nk { m[stamp + kk] = 0 - 1; kk = kk + 1 }
662 var nuv: i64 = 0
663 kk = 0
664 while kk < nk {
665 let r: i64 = m[root + kk]
666 if m[stamp + r] < 0 { m[stamp + r] = nuv; nuv = nuv + 1 }
667 kk = kk + 1
668 }
669 kk = 0
670 while kk < nk {
671 let cls2: i64 = m[stamp + m[root + kk]]
672 m[uvf + kk] = cls2
673 m[uvsrc + cls2] = uv_ti(m, kk / UV_TRI, kk % UV_TRI)
674 m[uvch + cls2] = m[tch + kk / UV_TRI]
675 kk = kk + 1
676 }
677 m[UV_H_NUV] = nuv
678
679 // CSR of triangles by chart, plus the per-chart triangle count
680 let cst: i64 = m[UV_O_CSTART]
681 let cord: i64 = m[UV_O_CORDER]
682 let ccnt: i64 = m[UV_O_CCNT]
683 var c: i64 = 0
684 while c < nc + 1 { m[cst + c] = 0; c = c + 1 }
685 t = 0
686 while t < nt { m[cst + m[tch + t] + 1] = m[cst + m[tch + t] + 1] + 1; t = t + 1 }
687 c = 0
688 while c < nc { m[ccnt + c] = m[cst + c + 1]; c = c + 1 }
689 c = 0
690 while c < nc { m[cst + c + 1] = m[cst + c + 1] + m[cst + c]; c = c + 1 }
691 let fill: i64 = m[UV_O_LIST]
692 c = 0
693 while c < nc { m[fill + c] = m[cst + c]; c = c + 1 }
694 t = 0
695 while t < nt {
696 let cc: i64 = m[tch + t]
697 m[cord + m[fill + cc]] = t
698 m[fill + cc] = m[fill + cc] + 1
699 t = t + 1
700 }
701 m[UV_H_STAGE] = UV_ST_CHARTS
702 m[UV_H_ERR] = UV_OK
703 return UV_OK
704}
705
706func uv_chart_count(m: *i64) -> i64 { return m[UV_H_NC] }
707func uv_uvvert_count(m: *i64) -> i64 { return m[UV_H_NUV] }
708func uv_seam_count(m: *i64) -> i64 { return m[UV_H_NSEAM] }
709func uv_chart_of_tri(m: *i64, t: i64) -> i64 {
710 if t < 0 { return 0 - 1 }
711 if t >= m[UV_H_NT] { return 0 - 1 }
712 if m[UV_H_STAGE] < UV_ST_CHARTS { return 0 - 1 }
713 return m[m[UV_O_TCHART] + t]
714}
715func uv_chart_tris(m: *i64, c: i64) -> i64 {
716 if c < 0 { return 0 - 1 }
717 if c >= m[UV_H_NC] { return 0 - 1 }
718 return m[m[UV_O_CCNT] + c]
719}
720
721// ---- (2) CONFORMAL RELAXATION ------------------------------------------------------------------
722// collect the distinct UV vertices of chart c into the LIST array; returns how many.
723func uvi_chart_classes(m: *i64, c: i64) -> i64 {
724 if c < 0 { return 0 }
725 if c >= m[UV_H_NC] { return 0 }
726 let stamp: i64 = m[UV_O_STAMP]
727 let list: i64 = m[UV_O_LIST]
728 let uvf: i64 = m[UV_O_UVF]
729 var i: i64 = 0
730 while i < m[UV_H_NUV] { m[stamp + i] = 0; i = i + 1 }
731 var n: i64 = 0
732 var p: i64 = m[m[UV_O_CSTART] + c]
733 let e: i64 = m[m[UV_O_CSTART] + c + 1]
734 while p < e {
735 let t: i64 = m[m[UV_O_CORDER] + p]
736 var k: i64 = 0
737 while k < UV_TRI {
738 let cls: i64 = m[uvf + t * UV_TRI + k]
739 if m[stamp + cls] == 0 {
740 m[stamp + cls] = 1
741 m[list + n] = cls
742 n = n + 1
743 }
744 k = k + 1
745 }
746 p = p + 1
747 }
748 return n
749}
750
751func uvi_d2(m: *i64, va: i64, vb: i64) -> i64 {
752 let dx: i64 = uv_vx(m, va) - uv_vx(m, vb)
753 let dy: i64 = uv_vy(m, va) - uv_vy(m, vb)
754 let dz: i64 = uv_vz(m, va) - uv_vz(m, vb)
755 return dx * dx + dy * dy + dz * dz
756}
757
758// TWO PINS, the standard LSCM constraint. Chosen as the chart's approximate 3D diameter by the usual
759// two-pass walk (farthest from an arbitrary seed, then farthest from that) -- an APPROXIMATION of the
760// diameter, not the diameter, and named as one. It only has to produce two well-separated vertices; a
761// worse pair costs conditioning, never correctness, because the energy itself is what is minimised.
762// Returns the number of pins established: 2, or 0 when the chart cannot supply two distinct positions.
763func uv_pins_for(m: *i64, c: i64, out: *i64) -> i64 {
764 let n: i64 = uvi_chart_classes(m, c)
765 if n < UV_MIN_PINS { return 0 }
766 let list: i64 = m[UV_O_LIST]
767 let uvsrc: i64 = m[UV_O_UVSRC]
768 let a: i64 = m[list + 0]
769 var best: i64 = 0 - 1
770 var i: i64 = 0
771 var pa: i64 = a
772 while i < n {
773 let d2: i64 = uvi_d2(m, m[uvsrc + m[list + i]], m[uvsrc + a])
774 if d2 > best { best = d2; pa = m[list + i] }
775 i = i + 1
776 }
777 i = 0
778 best = 0 - 1
779 var pb: i64 = pa
780 while i < n {
781 let d3: i64 = uvi_d2(m, m[uvsrc + m[list + i]], m[uvsrc + pa])
782 if d3 > best { best = d3; pb = m[list + i] }
783 i = i + 1
784 }
785 if best <= 0 { return 0 }
786 if pb == pa { return 0 }
787 out[0] = pa
788 out[1] = pb
789 return UV_MIN_PINS
790}
791
792// cotangent of the angle at vertex vk in the triangle (vi, vj, vk), in UV_W_Q scale, clamped.
793func uvi_cotq(m: *i64, vi: i64, vj: i64, vk: i64) -> i64 {
794 let ux: i64 = uv_vx(m, vi) - uv_vx(m, vk)
795 let uy: i64 = uv_vy(m, vi) - uv_vy(m, vk)
796 let uz: i64 = uv_vz(m, vi) - uv_vz(m, vk)
797 let wx: i64 = uv_vx(m, vj) - uv_vx(m, vk)
798 let wy: i64 = uv_vy(m, vj) - uv_vy(m, vk)
799 let wz: i64 = uv_vz(m, vj) - uv_vz(m, vk)
800 let dot: i64 = ux * wx + uy * wy + uz * wz
801 let cx: i64 = uy * wz - uz * wy
802 let cy: i64 = uz * wx - ux * wz
803 let cz: i64 = ux * wy - uy * wx
804 let cr: i64 = vm_isqrt(cx * cx + cy * cy + cz * cz)
805 let cmax: i64 = UV_COT_CLAMP * UV_W_Q
806 if cr <= 0 { return cmax }
807 var q: i64 = dot * UV_W_Q / cr
808 if q > cmax { q = cmax }
809 if q < 0 - cmax { q = 0 - cmax }
810 return q
811}
812
813// Fixed-point scale the rigid unfold works in, before the guess is normalised. DERIVED: it is UV_W_Q,
814// the estate's unit, reused rather than a fresh scale; at a mesh edge of 200 units a length becomes 8.2e5
815// and the law-of-cosines numerator below reaches 2e12, four million times inside i64.
816const UV_UNF_Q: i64 = 4096
817
818// squared 3D edge length lifted into the unfold's fixed point, in one square root rather than two.
819func uvi_len_q(m: *i64, va: i64, vb: i64) -> i64 {
820 return vm_isqrt(uvi_d2(m, va, vb) * UV_UNF_Q * UV_UNF_Q)
821}
822
823// Place the one vertex of triangle tn that its already-placed neighbour tp does not share, at its exact
824// 3D edge lengths from the two shared ones -- the law of cosines, done once per triangle.
825// THE SIDE MATTERS AND IS NOT A COIN FLIP: the new vertex goes on the OPPOSITE side of the shared edge
826// from the parent's own third vertex. Choosing the same side folds the chart back over the triangle it
827// just came from, which is a fold the distortion metric would then honestly report -- a self-inflicted
828// failure that would read exactly like the mesh being unwrappable.
829// A vertex already placed by another path KEEPS its first position: on a developable surface every path
830// agrees, and on one that is not, the disagreement IS the curvature, and it is the relaxation's job to
831// distribute it rather than this function's job to hide it.
832func uvi_place_third(m: *i64, tp: i64, tn: i64, va: i64, vb: i64) -> i64 {
833 let uvf: i64 = m[UV_O_UVF]
834 let uvsrc: i64 = m[UV_O_UVSRC]
835 let wu: i64 = m[UV_O_WU]
836 let wv: i64 = m[UV_O_WV]
837 let pl: i64 = m[UV_O_PLACED]
838 let ka: i64 = uvi_corner_of(m, tn, va)
839 let kb: i64 = uvi_corner_of(m, tn, vb)
840 if ka < 0 { return UV_E_ARGS }
841 if kb < 0 { return UV_E_ARGS }
842 let kc: i64 = UV_TRI - ka - kb // corner indices are 0,1,2 and two are known, so the third is 3 - a - b
843 let ca: i64 = m[uvf + tn * UV_TRI + ka]
844 let cb: i64 = m[uvf + tn * UV_TRI + kb]
845 let cc: i64 = m[uvf + tn * UV_TRI + kc]
846 if m[pl + cc] == 1 { return UV_OK }
847 let vc: i64 = m[uvsrc + cc]
848 let ax: i64 = m[wu + ca]
849 let ay: i64 = m[wv + ca]
850 let dx: i64 = m[wu + cb] - ax
851 let dy: i64 = m[wv + cb] - ay
852 let lab: i64 = vm_isqrt(dx * dx + dy * dy)
853 if lab <= 0 { return UV_E_ARGS }
854 let lac2: i64 = uvi_d2(m, va, vc) * UV_UNF_Q * UV_UNF_Q
855 let lbc2: i64 = uvi_d2(m, vb, vc) * UV_UNF_Q * UV_UNF_Q
856 let xq: i64 = (lab * lab + lac2 - lbc2) / (2 * lab)
857 var yq2: i64 = lac2 - xq * xq
858 if yq2 < 0 { yq2 = 0 } // rounding only; the triangle inequality guarantees the sign
859 let yq: i64 = vm_isqrt(yq2)
860 let pk: i64 = UV_TRI - uvi_corner_of(m, tp, va) - uvi_corner_of(m, tp, vb)
861 let pc: i64 = m[uvf + tp * UV_TRI + pk]
862 let cr: i64 = dx * (m[wv + pc] - ay) - dy * (m[wu + pc] - ax)
863 var sgn: i64 = 1
864 if cr > 0 { sgn = 0 - 1 }
865 m[wu + cc] = ax + dx * xq / lab - dy * yq * sgn / lab
866 m[wv + cc] = ay + dy * xq / lab + dx * yq * sgn / lab
867 m[pl + cc] = 1
868 return UV_OK
869}
870
871// THE RIGID UNFOLD. Walks the chart triangle by triangle from a seed, laying each one flat at its exact
872// 3D edge lengths. EXACT for any developable surface -- a flat quad, a cube face, a cut cylinder wall --
873// and path-dependent for one that is not, which is the honest behaviour: a cone point cannot be flattened
874// and something has to give.
875func uvi_unfold(m: *i64, c: i64, n: i64) -> i64 {
876 let list: i64 = m[UV_O_LIST]
877 let uvf: i64 = m[UV_O_UVF]
878 let uvsrc: i64 = m[UV_O_UVSRC]
879 let wu: i64 = m[UV_O_WU]
880 let wv: i64 = m[UV_O_WV]
881 let pl: i64 = m[UV_O_PLACED]
882 let ts: i64 = m[UV_O_TSEEN]
883 let stk: i64 = m[UV_O_STACK]
884 let cord: i64 = m[UV_O_CORDER]
885 var i: i64 = 0
886 while i < n { m[pl + m[list + i]] = 0; i = i + 1 }
887 let p0: i64 = m[m[UV_O_CSTART] + c]
888 let p1: i64 = m[m[UV_O_CSTART] + c + 1]
889 var p: i64 = p0
890 while p < p1 { m[ts + m[cord + p]] = 0; p = p + 1 }
891 if p1 <= p0 { return UV_E_NOPIN }
892 let seed: i64 = m[cord + p0]
893 let a0: i64 = m[uvf + seed * UV_TRI + 0]
894 let b0: i64 = m[uvf + seed * UV_TRI + 1]
895 let c0: i64 = m[uvf + seed * UV_TRI + 2]
896 let lab: i64 = uvi_len_q(m, m[uvsrc + a0], m[uvsrc + b0])
897 if lab <= 0 { return UV_E_NOPIN }
898 let lac2: i64 = uvi_d2(m, m[uvsrc + a0], m[uvsrc + c0]) * UV_UNF_Q * UV_UNF_Q
899 let lbc2: i64 = uvi_d2(m, m[uvsrc + b0], m[uvsrc + c0]) * UV_UNF_Q * UV_UNF_Q
900 let x0: i64 = (lab * lab + lac2 - lbc2) / (2 * lab)
901 var y0s: i64 = lac2 - x0 * x0
902 if y0s < 0 { y0s = 0 }
903 m[wu + a0] = 0
904 m[wv + a0] = 0
905 m[pl + a0] = 1
906 m[wu + b0] = lab
907 m[wv + b0] = 0
908 m[pl + b0] = 1
909 m[wu + c0] = x0
910 m[wv + c0] = vm_isqrt(y0s)
911 m[pl + c0] = 1
912 m[ts + seed] = 1
913 var sp: i64 = 0
914 m[stk + sp] = seed
915 sp = sp + 1
916 while sp > 0 {
917 sp = sp - 1
918 let t: i64 = m[stk + sp]
919 var k: i64 = 0
920 while k < UV_TRI {
921 let va: i64 = uv_ti(m, t, k)
922 let vb: i64 = uv_ti(m, t, (k + 1) % UV_TRI)
923 if uv_is_seam(m, va, vb) == 0 {
924 let es: i64 = uvi_edge_find(m, va, vb)
925 if es >= 0 {
926 let eo: i64 = m[UV_O_ETAB] + es * UV_EREC
927 var w: i64 = 2
928 while w < UV_EREC {
929 let nb: i64 = m[eo + w] - 1
930 if nb >= 0 { if nb != t { if m[ts + nb] == 0 {
931 // THE RETURN CODE IS THE POINT. Marking the triangle seen after a REFUSED
932 // placement is what turns a named refusal into a silent collapse: the vertex
933 // is never written, the walk never comes back for it because the triangle now
934 // reads as done, and the origin it was left holding is indistinguishable
935 // downstream from a coordinate the unfold actually chose.
936 let prc: i64 = uvi_place_third(m, t, nb, va, vb)
937 if prc != UV_OK { return prc }
938 m[ts + nb] = 1
939 m[stk + sp] = nb
940 sp = sp + 1
941 } } }
942 w = w + 1
943 }
944 }
945 }
946 k = k + 1
947 }
948 }
949 // THE WALK HAS TO ANSWER FOR THE WHOLE CHART, NOT FOR THE PART OF IT THAT WENT WELL. A class the walk
950 // never placed keeps whatever its slot held -- zero on a fresh arena -- and every step after this one
951 // reads that origin as a position the unfold chose: the similarity below can take it as a pin, the
952 // relaxation relaxes toward it, and the referee measures distortion against it. Refused under a code
953 // of its own so the reason travels with the refusal instead of arriving as somebody else's.
954 i = 0
955 var unplaced: i64 = 0
956 while i < n {
957 if m[pl + m[list + i]] == 0 { unplaced = unplaced + 1 }
958 i = i + 1
959 }
960 if unplaced > 0 { return UV_E_UNPLACED }
961 return UV_OK
962}
963
964// The initial guess: the rigid unfold, normalised uniformly into the guess space, then the 2D similarity
965// that lands pin A on the origin and pin B on (UV_PIN_SPAN, 0).
966// WHY AN UNFOLD AND NOT A PROJECTION. The obvious cheap guess is to drop the chart onto the two coordinate
967// axes it spreads along, and it is wrong in a way that matters: a cylinder wall projects onto a RING, two
968// vertices deep, and no local descent recovers the unrolled strip from there. The seam capability would
969// have shipped with a solver unable to use it, and the false refusal that followed would have read as the
970// mesh's fault rather than the guess's.
971// EVERY STEP HERE IS A SIMILARITY OR AN ISOMETRY, WHICH IS EXACTLY WHAT A CONFORMAL MAP IS ALLOWED TO BE,
972// so on a developable surface the guess is already the answer and the relaxation confirms it rather than
973// degrading it. That is what lets the positive control below be an exact claim instead of a hopeful one.
974func uvi_initial(m: *i64, c: i64, n: i64, pa: i64, pb: i64) -> i64 {
975 let list: i64 = m[UV_O_LIST]
976 let wu: i64 = m[UV_O_WU]
977 let wv: i64 = m[UV_O_WV]
978 let urc: i64 = uvi_unfold(m, c, n)
979 if urc != UV_OK { return urc }
980 var u0: i64 = 0
981 var u1: i64 = 0
982 var v0: i64 = 0
983 var v1: i64 = 0
984 var i: i64 = 0
985 while i < n {
986 let z: i64 = m[list + i]
987 if i == 0 { u0 = m[wu + z]; u1 = u0; v0 = m[wv + z]; v1 = v0 }
988 if m[wu + z] < u0 { u0 = m[wu + z] }
989 if m[wu + z] > u1 { u1 = m[wu + z] }
990 if m[wv + z] < v0 { v0 = m[wv + z] }
991 if m[wv + z] > v1 { v1 = m[wv + z] }
992 i = i + 1
993 }
994 // UNIFORM normalisation into the guess space -- one divisor on both axes. A per-axis fit is a shear,
995 // and a shear is precisely what the unfold just went to the trouble of not doing.
996 var sden: i64 = u1 - u0
997 if v1 - v0 > sden { sden = v1 - v0 }
998 if sden < 1 { sden = 1 }
999 if sden > UV_SDEN_MAX { return UV_E_SPAN }
1000 i = 0
1001 while i < n {
1002 let cls: i64 = m[list + i]
1003 m[wu + cls] = (m[wu + cls] - u0) * UV_GUESS_SPAN / sden
1004 m[wv + cls] = (m[wv + cls] - v0) * UV_GUESS_SPAN / sden
1005 i = i + 1
1006 }
1007 let gax: i64 = m[wu + pa]
1008 let gay: i64 = m[wv + pa]
1009 let dx: i64 = m[wu + pb] - gax
1010 let dy: i64 = m[wv + pb] - gay
1011 let nd: i64 = dx * dx + dy * dy
1012 // the two pins projecting onto the same guess point leaves the similarity undefined. REFUSE by name:
1013 // inventing a rotation here would silently choose the fold this file exists to prevent.
1014 if nd <= 0 { return UV_E_NOPIN }
1015 i = 0
1016 while i < n {
1017 let cls2: i64 = m[list + i]
1018 let px: i64 = m[wu + cls2] - gax
1019 let py: i64 = m[wv + cls2] - gay
1020 m[wu + cls2] = (px * dx + py * dy) * UV_PIN_SPAN / nd
1021 m[wv + cls2] = (py * dx - px * dy) * UV_PIN_SPAN / nd
1022 i = i + 1
1023 }
1024 return UV_OK
1025}
1026
1027// ONE CHART. Damped Jacobi over the exact LSCM stationary condition, pins held fixed.
1028func uvi_solve_chart(m: *i64, c: i64) -> i64 {
1029 let pins: *i64 = sys_mmap(UV_MIN_PINS * UV_I64) as *i64
1030 if uv_pins_for(m, c, pins) < UV_MIN_PINS { return UV_E_NOPIN }
1031 let pa: i64 = pins[0]
1032 let pb: i64 = pins[1]
1033 let n: i64 = uvi_chart_classes(m, c)
1034 let irc: i64 = uvi_initial(m, c, n, pa, pb)
1035 if irc != UV_OK { return irc }
1036 let list: i64 = m[UV_O_LIST]
1037 let uvf: i64 = m[UV_O_UVF]
1038 let uvsrc: i64 = m[UV_O_UVSRC]
1039 let wu: i64 = m[UV_O_WU]
1040 let wv: i64 = m[UV_O_WV]
1041 let au: i64 = m[UV_O_ACCU]
1042 let av: i64 = m[UV_O_ACCV]
1043 let aw: i64 = m[UV_O_ACCW]
1044 let aau: i64 = m[UV_O_ACCAU]
1045 let aav: i64 = m[UV_O_ACCAV]
1046 let p0: i64 = m[m[UV_O_CSTART] + c]
1047 let p1: i64 = m[m[UV_O_CSTART] + c + 1]
1048 var cap: i64 = n * UV_ITER_PER_UVV
1049 if cap < UV_ITER_MIN { cap = UV_ITER_MIN }
1050 var it: i64 = 0
1051 var used: i64 = 0
1052 var done: i64 = 0
1053 var maxmove: i64 = 0
1054 // THE LOOP CURSOR IS NOT THE ANSWER. Leaving early by writing the cap into the cursor would erase the
1055 // very number this function has to report, so the sweep count is carried in a counter of its own and
1056 // the cursor is free to be used as the exit sentinel.
1057 while it < cap {
1058 if done == 0 {
1059 var i: i64 = 0
1060 while i < n {
1061 let z: i64 = m[list + i]
1062 m[au + z] = 0
1063 m[av + z] = 0
1064 m[aw + z] = 0
1065 m[aau + z] = 0
1066 m[aav + z] = 0
1067 i = i + 1
1068 }
1069 var p: i64 = p0
1070 while p < p1 {
1071 let t: i64 = m[m[UV_O_CORDER] + p]
1072 let ci: i64 = m[uvf + t * UV_TRI + 0]
1073 let cj: i64 = m[uvf + t * UV_TRI + 1]
1074 let ck: i64 = m[uvf + t * UV_TRI + 2]
1075 let vi: i64 = m[uvsrc + ci]
1076 let vj: i64 = m[uvsrc + cj]
1077 let vk: i64 = m[uvsrc + ck]
1078 // weight on edge (i,j) is the cotangent at k, and so round the triangle
1079 let wk: i64 = uvi_cotq(m, vi, vj, vk)
1080 let wi: i64 = uvi_cotq(m, vj, vk, vi)
1081 let wj: i64 = uvi_cotq(m, vk, vi, vj)
1082 m[aw + ci] = m[aw + ci] + wk + wj
1083 m[aw + cj] = m[aw + cj] + wk + wi
1084 m[aw + ck] = m[aw + ck] + wi + wj
1085 m[au + ci] = m[au + ci] + wk * m[wu + cj] + wj * m[wu + ck]
1086 m[au + cj] = m[au + cj] + wk * m[wu + ci] + wi * m[wu + ck]
1087 m[au + ck] = m[au + ck] + wi * m[wu + cj] + wj * m[wu + ci]
1088 m[av + ci] = m[av + ci] + wk * m[wv + cj] + wj * m[wv + ck]
1089 m[av + cj] = m[av + cj] + wk * m[wv + ci] + wi * m[wv + ck]
1090 m[av + ck] = m[av + ck] + wi * m[wv + cj] + wj * m[wv + ci]
1091 // the area gradient: for corner i the neighbours in winding order are j (next) and k (prev)
1092 m[aau + ci] = m[aau + ci] + m[wv + cj] - m[wv + ck]
1093 m[aav + ci] = m[aav + ci] + m[wu + ck] - m[wu + cj]
1094 m[aau + cj] = m[aau + cj] + m[wv + ck] - m[wv + ci]
1095 m[aav + cj] = m[aav + cj] + m[wu + ci] - m[wu + ck]
1096 m[aau + ck] = m[aau + ck] + m[wv + ci] - m[wv + cj]
1097 m[aav + ck] = m[aav + ck] + m[wu + cj] - m[wu + ci]
1098 p = p + 1
1099 }
1100 maxmove = 0
1101 i = 0
1102 while i < n {
1103 let q: i64 = m[list + i]
1104 var skip: i64 = 0
1105 if q == pa { skip = 1 }
1106 if q == pb { skip = 1 }
1107 if m[aw + q] <= 0 { skip = 1; m[UV_H_SING] = m[UV_H_SING] + 1 }
1108 if skip == 0 {
1109 let den: i64 = m[aw + q]
1110 let tu: i64 = (m[au + q] + UV_W_Q * m[aau + q]) / den
1111 let tv: i64 = (m[av + q] + UV_W_Q * m[aav + q]) / den
1112 let du: i64 = (tu - m[wu + q]) / UV_OMEGA_DEN
1113 let dv: i64 = (tv - m[wv + q]) / UV_OMEGA_DEN
1114 m[wu + q] = m[wu + q] + du
1115 m[wv + q] = m[wv + q] + dv
1116 let mvd: i64 = vm_abs(du) + vm_abs(dv)
1117 if mvd > maxmove { maxmove = mvd }
1118 }
1119 i = i + 1
1120 }
1121 used = used + 1
1122 if maxmove <= UV_CONV_EPS { done = 1; it = cap }
1123 }
1124 it = it + 1
1125 }
1126 if used > m[UV_H_ITERS] { m[UV_H_ITERS] = used }
1127 if maxmove > m[UV_H_MAXMOVE] { m[UV_H_MAXMOVE] = maxmove }
1128 if done == 0 {
1129 m[UV_H_NOCONV] = m[UV_H_NOCONV] + 1
1130 return UV_E_NOCONVERGE
1131 }
1132 return UV_OK
1133}
1134
1135// ceil(sqrt(nc)): the atlas grid, exactly the rule ntx_apply uses so both producers tile alike.
1136func uv_grid_for(nc: i64) -> i64 {
1137 var g: i64 = 1
1138 while g * g < nc { g = g + 1 }
1139 return g
1140}
1141
1142// place every solved chart into its own tile of a g x g atlas, UNIFORMLY SCALED. A per-axis fit would
1143// squash the chart and INTRODUCE exactly the angle distortion the solver just spent its budget removing,
1144// so the same divisor is used on both axes and only the tile's spare margin is wasted.
1145func uvi_atlas(m: *i64) -> i64 {
1146 let nc: i64 = m[UV_H_NC]
1147 let g: i64 = uv_grid_for(nc)
1148 m[UV_H_GRID] = g
1149 let tw: i64 = UV_Q16 / g
1150 let inner: i64 = tw - 2 * UV_GUTTER
1151 if inner < 1 { return UV_E_SPAN }
1152 let wu: i64 = m[UV_O_WU]
1153 let wv: i64 = m[UV_O_WV]
1154 let list: i64 = m[UV_O_LIST]
1155 let cbox: i64 = m[UV_O_CBOX]
1156 var c: i64 = 0
1157 while c < nc {
1158 let n: i64 = uvi_chart_classes(m, c)
1159 var u0: i64 = 0
1160 var u1: i64 = 0
1161 var v0: i64 = 0
1162 var v1: i64 = 0
1163 var i: i64 = 0
1164 while i < n {
1165 let z: i64 = m[list + i]
1166 if i == 0 { u0 = m[wu + z]; u1 = u0; v0 = m[wv + z]; v1 = v0 }
1167 if m[wu + z] < u0 { u0 = m[wu + z] }
1168 if m[wu + z] > u1 { u1 = m[wu + z] }
1169 if m[wv + z] < v0 { v0 = m[wv + z] }
1170 if m[wv + z] > v1 { v1 = m[wv + z] }
1171 i = i + 1
1172 }
1173 var sden: i64 = u1 - u0
1174 if v1 - v0 > sden { sden = v1 - v0 }
1175 if sden < 1 { sden = 1 }
1176 if sden > UV_SDEN_MAX { m[UV_H_ERR] = UV_E_SPAN; return UV_E_SPAN }
1177 let tx: i64 = c % g
1178 let ty: i64 = c / g
1179 i = 0
1180 while i < n {
1181 let z2: i64 = m[list + i]
1182 m[wu + z2] = tx * tw + UV_GUTTER + (m[wu + z2] - u0) * inner / sden
1183 m[wv + z2] = ty * tw + UV_GUTTER + (m[wv + z2] - v0) * inner / sden
1184 i = i + 1
1185 }
1186 // chart box in EMITTED coordinates, which is what uv_overlap_check adjudicates
1187 i = 0
1188 while i < n {
1189 let z3: i64 = m[list + i]
1190 if i == 0 {
1191 m[cbox + c * UV_BOXREC + UV_B_U0] = m[wu + z3]
1192 m[cbox + c * UV_BOXREC + UV_B_V0] = m[wv + z3]
1193 m[cbox + c * UV_BOXREC + UV_B_U1] = m[wu + z3]
1194 m[cbox + c * UV_BOXREC + UV_B_V1] = m[wv + z3]
1195 }
1196 if m[wu + z3] < m[cbox + c * UV_BOXREC + UV_B_U0] { m[cbox + c * UV_BOXREC + UV_B_U0] = m[wu + z3] }
1197 if m[wv + z3] < m[cbox + c * UV_BOXREC + UV_B_V0] { m[cbox + c * UV_BOXREC + UV_B_V0] = m[wv + z3] }
1198 if m[wu + z3] > m[cbox + c * UV_BOXREC + UV_B_U1] { m[cbox + c * UV_BOXREC + UV_B_U1] = m[wu + z3] }
1199 if m[wv + z3] > m[cbox + c * UV_BOXREC + UV_B_V1] { m[cbox + c * UV_BOXREC + UV_B_V1] = m[wv + z3] }
1200 i = i + 1
1201 }
1202 c = c + 1
1203 }
1204 // publish per-corner coordinates, the actual product of this library
1205 let cuv: i64 = m[UV_O_CUV]
1206 let uvf: i64 = m[UV_O_UVF]
1207 var k: i64 = 0
1208 while k < m[UV_H_NCORN] {
1209 let cls: i64 = m[uvf + k]
1210 m[cuv + k * UV_CUVREC + UV_P_U] = m[wu + cls]
1211 m[cuv + k * UV_CUVREC + UV_P_V] = m[wv + cls]
1212 k = k + 1
1213 }
1214 return UV_OK
1215}
1216
1217func uv_conformal(m: *i64) -> i64 {
1218 if m[UV_H_STAGE] < UV_ST_CHARTS { m[UV_H_ERR] = UV_E_STAGE; return UV_E_STAGE }
1219 m[UV_H_NOCONV] = 0
1220 m[UV_H_SING] = 0
1221 m[UV_H_ITERS] = 0
1222 m[UV_H_MAXMOVE] = 0
1223 let nc: i64 = m[UV_H_NC]
1224 var c: i64 = 0
1225 while c < nc {
1226 let rc: i64 = uvi_solve_chart(m, c)
1227 if rc != UV_OK { m[UV_H_ERR] = rc; return rc }
1228 c = c + 1
1229 }
1230 let arc: i64 = uvi_atlas(m)
1231 if arc != UV_OK { m[UV_H_ERR] = arc; return arc }
1232 m[UV_H_STAGE] = UV_ST_SOLVED
1233 m[UV_H_ERR] = UV_OK
1234 return UV_OK
1235}
1236
1237func uv_iters(m: *i64) -> i64 { return m[UV_H_ITERS] }
1238func uv_maxmove(m: *i64) -> i64 { return m[UV_H_MAXMOVE] }
1239// NAMED FOR WHAT IT COUNTS, NOT FOR WHAT IT SOUNDS LIKE: this is vertex-SWEEPS skipped because the
1240// incident cotangent sum was non-positive, so one persistently obtuse vertex contributes once per sweep
1241// rather than once. Calling it a vertex count would overstate the population by the sweep budget.
1242func uv_singular_sweeps(m: *i64) -> i64 { return m[UV_H_SING] }
1243func uv_grid(m: *i64) -> i64 { return m[UV_H_GRID] }
1244func uv_corner_u(m: *i64, t: i64, k: i64) -> i64 { return m[m[UV_O_CUV] + (t * UV_TRI + k) * UV_CUVREC + UV_P_U] }
1245func uv_corner_v(m: *i64, t: i64, k: i64) -> i64 { return m[m[UV_O_CUV] + (t * UV_TRI + k) * UV_CUVREC + UV_P_V] }
1246func uv_corner_class(m: *i64, t: i64, k: i64) -> i64 { return m[m[UV_O_UVF] + t * UV_TRI + k] }
1247
1248// ---- (3) THE DISTORTION REFEREE ----------------------------------------------------------------
1249// Measured on the EMITTED coordinates, after atlas placement -- a metric taken on an intermediate the
1250// caller never receives would be measuring a different subject than the one it names.
1251//
1252// ANGLE. The quasi-conformal dilatation k = (smax - smin) / (smax + smin) of the affine map each triangle
1253// induces, in permil. Zero exactly when the map is a similarity on that triangle, i.e. conformal. The two
1254// singular values come from the first fundamental form of the UV-to-3D map, following the standard
1255// Sander construction, so NO LOCAL FLATTENING OF THE TRIANGLE IS NEEDED and nothing but dot products of
1256// 3D edge vectors is involved. Because k is a RATIO of singular values it is invariant to a common scale
1257// on (a,b,c), which is what lets the triple be reduced into overflow-safe range for free.
1258//
1259// AREA. The ratio of emitted area to 3D area per triangle, then each triangle's deviation from its own
1260// CHART's mean ratio, in permil. Deliberately relative to the chart rather than absolute: a chart is
1261// scaled to fit its atlas tile, so its absolute scale carries no information and an absolute ratio would
1262// report the tile size as distortion.
1263func uvi_tri_metrics(m: *i64, t: i64) -> i64 {
1264 let vi: i64 = uv_ti(m, t, 0)
1265 let vj: i64 = uv_ti(m, t, 1)
1266 let vk: i64 = uv_ti(m, t, 2)
1267 // 3D edge vectors from corner 0. Working relative to a corner rather than absolutely keeps every
1268 // product below bounded by the EDGE length instead of the coordinate magnitude.
1269 let jx: i64 = uv_vx(m, vj) - uv_vx(m, vi)
1270 let jy: i64 = uv_vy(m, vj) - uv_vy(m, vi)
1271 let jz: i64 = uv_vz(m, vj) - uv_vz(m, vi)
1272 let kx: i64 = uv_vx(m, vk) - uv_vx(m, vi)
1273 let ky: i64 = uv_vy(m, vk) - uv_vy(m, vi)
1274 let kz: i64 = uv_vz(m, vk) - uv_vz(m, vi)
1275 let su: i64 = uv_corner_u(m, t, 0)
1276 let sv: i64 = uv_corner_v(m, t, 0)
1277 let ju: i64 = uv_corner_u(m, t, 1) - su
1278 let jv: i64 = uv_corner_v(m, t, 1) - sv
1279 let ku: i64 = uv_corner_u(m, t, 2) - su
1280 let kv: i64 = uv_corner_v(m, t, 2) - sv
1281 let tsar: i64 = m[UV_O_TSAR]
1282 let tang: i64 = m[UV_O_TANG]
1283 let trat: i64 = m[UV_O_TRAT]
1284 let twoa: i64 = ju * kv - ku * jv // twice the SIGNED emitted area
1285 m[tsar + t] = twoa
1286 // twice the 3D area, exact cross product then one integer square root
1287 let cx: i64 = jy * kz - jz * ky
1288 let cy: i64 = jz * kx - jx * kz
1289 let cz: i64 = jx * ky - jy * kx
1290 let a3: i64 = vm_isqrt(cx * cx + cy * cy + cz * cz)
1291 if twoa == 0 {
1292 // A COLLAPSED TRIANGLE IS NOT A ZERO-DISTORTION TRIANGLE. Scoring it zero is precisely how a
1293 // do-nothing unwrapper that maps every vertex to one point would score perfectly, so it is its
1294 // own class and it is counted, never averaged in.
1295 m[UV_H_UVDEGEN] = m[UV_H_UVDEGEN] + 1
1296 m[tang + t] = 0 - 1
1297 m[trat + t] = 0 - 1
1298 return UV_E_UVDEGEN
1299 }
1300 if a3 <= 0 {
1301 m[UV_H_UVDEGEN] = m[UV_H_UVDEGEN] + 1
1302 m[tang + t] = 0 - 1
1303 m[trat + t] = 0 - 1
1304 return UV_E_UVDEGEN
1305 }
1306 var tw2: i64 = twoa
1307 if tw2 < 0 { tw2 = 0 - tw2 }
1308 m[trat + t] = tw2 * UV_W_Q / a3
1309 // Ss and St, translated to corner 0 so the magnitudes are edge-sized
1310 var ssx: i64 = jx * kv - kx * jv
1311 var ssy: i64 = jy * kv - ky * jv
1312 var ssz: i64 = jz * kv - kz * jv
1313 var stx: i64 = kx * ju - jx * ku
1314 var sty: i64 = ky * ju - jy * ku
1315 var stz: i64 = kz * ju - jz * ku
1316 // REDUCE BEFORE SQUARING, never after: a component past UV_SS_MAX overflows the very sum that would
1317 // have reported it as too large, so the check has to run while the number is still readable.
1318 var smx: i64 = vm_abs(ssx)
1319 if vm_abs(ssy) > smx { smx = vm_abs(ssy) }
1320 if vm_abs(ssz) > smx { smx = vm_abs(ssz) }
1321 if vm_abs(stx) > smx { smx = vm_abs(stx) }
1322 if vm_abs(sty) > smx { smx = vm_abs(sty) }
1323 if vm_abs(stz) > smx { smx = vm_abs(stz) }
1324 if smx > UV_SS_MAX {
1325 let sd: i64 = smx / UV_SS_MAX + 1
1326 ssx = ssx / sd
1327 ssy = ssy / sd
1328 ssz = ssz / sd
1329 stx = stx / sd
1330 sty = sty / sd
1331 stz = stz / sd
1332 }
1333 var fa: i64 = ssx * ssx + ssy * ssy + ssz * ssz
1334 var fb: i64 = ssx * stx + ssy * sty + ssz * stz
1335 var fc: i64 = stx * stx + sty * sty + stz * stz
1336 // reduce by a common divisor -- free, because the dilatation is scale-invariant
1337 var mx: i64 = fa
1338 if fc > mx { mx = fc }
1339 let fbA: i64 = vm_abs(fb)
1340 if fbA > mx { mx = fbA }
1341 if mx > UV_ABC_MAX {
1342 let d: i64 = mx / UV_ABC_MAX + 1
1343 fa = fa / d
1344 fb = fb / d
1345 fc = fc / d
1346 }
1347 var det: i64 = fa * fc - fb * fb
1348 if det < 0 { det = 0 } // Cauchy-Schwarz guarantees this; the clamp is against rounding
1349 let gdet: i64 = vm_isqrt(det)
1350 let sp2: i64 = fa + fc + 2 * gdet // (smax + smin)^2
1351 var sm2: i64 = fa + fc - 2 * gdet // (smax - smin)^2
1352 if sm2 < 0 { sm2 = 0 }
1353 if sp2 <= 0 {
1354 m[UV_H_UVDEGEN] = m[UV_H_UVDEGEN] + 1
1355 m[tang + t] = 0 - 1
1356 return UV_E_UVDEGEN
1357 }
1358 m[tang + t] = vm_isqrt(sm2 * UV_PERMIL * UV_PERMIL / sp2)
1359 return UV_OK
1360}
1361
1362func uv_distortion(m: *i64) -> i64 {
1363 if m[UV_H_STAGE] < UV_ST_SOLVED { m[UV_H_ERR] = UV_E_STAGE; return UV_E_STAGE }
1364 let nt: i64 = m[UV_H_NT]
1365 let nc: i64 = m[UV_H_NC]
1366 m[UV_H_UVDEGEN] = 0
1367 m[UV_H_FLIP] = 0
1368 let tang: i64 = m[UV_O_TANG]
1369 let trat: i64 = m[UV_O_TRAT]
1370 let tsar: i64 = m[UV_O_TSAR]
1371 let cdist: i64 = m[UV_O_CDIST]
1372 var t: i64 = 0
1373 var bad: i64 = UV_OK
1374 while t < nt {
1375 let rc: i64 = uvi_tri_metrics(m, t)
1376 if rc != UV_OK { bad = rc }
1377 t = t + 1
1378 }
1379 // FOLD: a triangle whose emitted winding opposes its chart's majority. The chart's own signed-area
1380 // sum decides the majority, so an entirely mirrored chart is NOT reported as folded -- a global
1381 // reflection is a legitimate parameterisation, a local one is a fold.
1382 var c: i64 = 0
1383 while c < nc {
1384 var tot: i64 = 0
1385 var p: i64 = m[m[UV_O_CSTART] + c]
1386 let pe: i64 = m[m[UV_O_CSTART] + c + 1]
1387 while p < pe { tot = tot + m[tsar + m[m[UV_O_CORDER] + p]]; p = p + 1 }
1388 p = m[m[UV_O_CSTART] + c]
1389 while p < pe {
1390 let sa: i64 = m[tsar + m[m[UV_O_CORDER] + p]]
1391 if tot >= 0 { if sa < 0 { m[UV_H_FLIP] = m[UV_H_FLIP] + 1 } }
1392 if tot < 0 { if sa > 0 { m[UV_H_FLIP] = m[UV_H_FLIP] + 1 } }
1393 p = p + 1
1394 }
1395 c = c + 1
1396 }
1397 // per-chart angle mean/max, and area deviation from the chart's own mean ratio
1398 c = 0
1399 while c < nc {
1400 let p0: i64 = m[m[UV_O_CSTART] + c]
1401 let p1: i64 = m[m[UV_O_CSTART] + c + 1]
1402 var cnt: i64 = 0
1403 var asum: i64 = 0
1404 var amax: i64 = 0
1405 var rsum: i64 = 0
1406 var p2: i64 = p0
1407 while p2 < p1 {
1408 let tt: i64 = m[m[UV_O_CORDER] + p2]
1409 if m[tang + tt] >= 0 {
1410 cnt = cnt + 1
1411 asum = asum + m[tang + tt]
1412 if m[tang + tt] > amax { amax = m[tang + tt] }
1413 rsum = rsum + m[trat + tt]
1414 }
1415 p2 = p2 + 1
1416 }
1417 var amean: i64 = 0
1418 var rmean: i64 = 0
1419 if cnt > 0 { amean = asum / cnt; rmean = rsum / cnt }
1420 var dsum: i64 = 0
1421 var dmax: i64 = 0
1422 if rmean > 0 {
1423 p2 = p0
1424 while p2 < p1 {
1425 let tt2: i64 = m[m[UV_O_CORDER] + p2]
1426 if m[trat + tt2] >= 0 {
1427 let dev: i64 = vm_abs(m[trat + tt2] - rmean) * UV_PERMIL / rmean
1428 dsum = dsum + dev
1429 if dev > dmax { dmax = dev }
1430 }
1431 p2 = p2 + 1
1432 }
1433 }
1434 var dmean: i64 = 0
1435 if cnt > 0 { dmean = dsum / cnt }
1436 m[cdist + c * UV_DISTREC + UV_D_AMEAN] = amean
1437 m[cdist + c * UV_DISTREC + UV_D_AMAX] = amax
1438 m[cdist + c * UV_DISTREC + UV_D_DMEAN] = dmean
1439 m[cdist + c * UV_DISTREC + UV_D_DMAX] = dmax
1440 c = c + 1
1441 }
1442 // whole-mesh rollup: mean of the per-chart means weighted by triangle count, worst of the maxima
1443 var wa: i64 = 0
1444 var wd: i64 = 0
1445 var wn: i64 = 0
1446 var ma: i64 = 0
1447 var md: i64 = 0
1448 c = 0
1449 while c < nc {
1450 let k: i64 = m[m[UV_O_CCNT] + c]
1451 wa = wa + m[cdist + c * UV_DISTREC + UV_D_AMEAN] * k
1452 wd = wd + m[cdist + c * UV_DISTREC + UV_D_DMEAN] * k
1453 wn = wn + k
1454 if m[cdist + c * UV_DISTREC + UV_D_AMAX] > ma { ma = m[cdist + c * UV_DISTREC + UV_D_AMAX] }
1455 if m[cdist + c * UV_DISTREC + UV_D_DMAX] > md { md = m[cdist + c * UV_DISTREC + UV_D_DMAX] }
1456 c = c + 1
1457 }
1458 if wn > 0 { m[UV_H_ANGMEAN] = wa / wn; m[UV_H_AREAMEAN] = wd / wn }
1459 m[UV_H_ANGMAX] = ma
1460 m[UV_H_AREAMAX] = md
1461 m[UV_H_STAGE] = UV_ST_MEASURED
1462 if bad != UV_OK { m[UV_H_ERR] = bad; return bad }
1463 m[UV_H_ERR] = UV_OK
1464 return UV_OK
1465}
1466
1467func uv_angle_mean(m: *i64) -> i64 { return m[UV_H_ANGMEAN] }
1468func uv_angle_max(m: *i64) -> i64 { return m[UV_H_ANGMAX] }
1469func uv_area_mean(m: *i64) -> i64 { return m[UV_H_AREAMEAN] }
1470func uv_area_max(m: *i64) -> i64 { return m[UV_H_AREAMAX] }
1471func uv_flipped(m: *i64) -> i64 { return m[UV_H_FLIP] }
1472func uv_uvdegen(m: *i64) -> i64 { return m[UV_H_UVDEGEN] }
1473func uv_chart_angle_mean(m: *i64, c: i64) -> i64 {
1474 if c < 0 { return 0 - 1 }
1475 if c >= m[UV_H_NC] { return 0 - 1 }
1476 return m[m[UV_O_CDIST] + c * UV_DISTREC + UV_D_AMEAN]
1477}
1478func uv_chart_angle_max(m: *i64, c: i64) -> i64 {
1479 if c < 0 { return 0 - 1 }
1480 if c >= m[UV_H_NC] { return 0 - 1 }
1481 return m[m[UV_O_CDIST] + c * UV_DISTREC + UV_D_AMAX]
1482}
1483func uv_chart_area_mean(m: *i64, c: i64) -> i64 {
1484 if c < 0 { return 0 - 1 }
1485 if c >= m[UV_H_NC] { return 0 - 1 }
1486 return m[m[UV_O_CDIST] + c * UV_DISTREC + UV_D_DMEAN]
1487}
1488func uv_chart_area_max(m: *i64, c: i64) -> i64 {
1489 if c < 0 { return 0 - 1 }
1490 if c >= m[UV_H_NC] { return 0 - 1 }
1491 return m[m[UV_O_CDIST] + c * UV_DISTREC + UV_D_DMAX]
1492}
1493// THE ANTI-VACUITY MEASUREMENT. Summed unsigned emitted area of a chart, in squared Q16 units. An
1494// unwrapper that maps everything to one point reads exactly zero here while scoring zero on any
1495// distortion metric that averages only over non-degenerate triangles -- so this is the number that
1496// separates a perfect parameterisation from no parameterisation at all.
1497func uv_chart_uv_area(m: *i64, c: i64) -> i64 {
1498 if c < 0 { return 0 - 1 }
1499 if c >= m[UV_H_NC] { return 0 - 1 }
1500 if m[UV_H_STAGE] < UV_ST_MEASURED { return 0 - 1 }
1501 var s: i64 = 0
1502 var p: i64 = m[m[UV_O_CSTART] + c]
1503 let pe: i64 = m[m[UV_O_CSTART] + c + 1]
1504 while p < pe {
1505 s = s + vm_abs(m[m[UV_O_TSAR] + m[m[UV_O_CORDER] + p]])
1506 p = p + 1
1507 }
1508 return s / 2
1509}
1510
1511// ---- (4) OVERLAP -------------------------------------------------------------------------------
1512// Two questions, kept apart because they have different remedies and different strengths of proof.
1513// BETWEEN CHARTS the answer is exact: every chart owns one tile of the grid and the boxes are compared
1514// in emitted coordinates, so an overlap is a real overlap and REFUSED. Tile ownership makes this
1515// impossible by construction, which is exactly why the gate has to PLANT one to show the detector
1516// fires -- a check that has only ever been asked an impossible question has not been shown to work.
1517// WITHIN A CHART the honest answer is narrower and it is stated rather than dressed up: the flipped-
1518// triangle count is a NECESSARY condition for local injectivity, NOT a sufficient one for global
1519// injectivity. A chart can in principle wrap onto itself with every triangle correctly oriented, and
1520// this does not detect that. It detects the fold class a relaxation actually produces, and a caller
1521// who needs the global guarantee needs a pairwise test this file does not claim to have.
1522func uv_overlap_check(m: *i64) -> i64 {
1523 if m[UV_H_STAGE] < UV_ST_MEASURED { m[UV_H_ERR] = UV_E_STAGE; return UV_E_STAGE }
1524 let nc: i64 = m[UV_H_NC]
1525 let cbox: i64 = m[UV_O_CBOX]
1526 m[UV_H_OVERLAP] = 0
1527 var a: i64 = 0
1528 while a < nc {
1529 var b: i64 = a + 1
1530 while b < nc {
1531 var hit: i64 = 1
1532 if m[cbox + a * UV_BOXREC + UV_B_U1] < m[cbox + b * UV_BOXREC + UV_B_U0] { hit = 0 }
1533 if m[cbox + b * UV_BOXREC + UV_B_U1] < m[cbox + a * UV_BOXREC + UV_B_U0] { hit = 0 }
1534 if m[cbox + a * UV_BOXREC + UV_B_V1] < m[cbox + b * UV_BOXREC + UV_B_V0] { hit = 0 }
1535 if m[cbox + b * UV_BOXREC + UV_B_V1] < m[cbox + a * UV_BOXREC + UV_B_V0] { hit = 0 }
1536 if hit == 1 { m[UV_H_OVERLAP] = m[UV_H_OVERLAP] + 1 }
1537 b = b + 1
1538 }
1539 a = a + 1
1540 }
1541 if m[UV_H_FLIP] > 0 { m[UV_H_ERR] = UV_E_FOLD; return UV_E_FOLD }
1542 if m[UV_H_OVERLAP] > 0 { m[UV_H_ERR] = UV_E_OVERLAP; return UV_E_OVERLAP }
1543 m[UV_H_ERR] = UV_OK
1544 return UV_OK
1545}
1546
1547func uv_overlaps(m: *i64) -> i64 { return m[UV_H_OVERLAP] }
1548func uv_chart_box(m: *i64, c: i64, k: i64) -> i64 {
1549 if c < 0 { return 0 - 1 }
1550 if c >= m[UV_H_NC] { return 0 - 1 }
1551 if k < 0 { return 0 - 1 }
1552 if k >= UV_BOXREC { return 0 - 1 }
1553 return m[m[UV_O_CBOX] + c * UV_BOXREC + k]
1554}
1555// EXISTS FOR THE NEGATIVE CONTROL AND FOR NOTHING ELSE, and is named so no reader mistakes it for part
1556// of the pipeline. Tile ownership makes a genuine cross-chart overlap unreachable, so the only way to
1557// show uv_overlap_check can fire is to plant one; a detector that has never been given a positive case
1558// is indistinguishable from a detector that cannot fire at all.
1559func uv_force_box(m: *i64, c: i64, u0: i64, v0: i64, u1: i64, v1: i64) -> i64 {
1560 if c < 0 { return UV_E_ARGS }
1561 if c >= m[UV_H_NC] { return UV_E_ARGS }
1562 m[m[UV_O_CBOX] + c * UV_BOXREC + UV_B_U0] = u0
1563 m[m[UV_O_CBOX] + c * UV_BOXREC + UV_B_V0] = v0
1564 m[m[UV_O_CBOX] + c * UV_BOXREC + UV_B_U1] = u1
1565 m[m[UV_O_CBOX] + c * UV_BOXREC + UV_B_V1] = v1
1566 return UV_OK
1567}
1568
1569// EXISTS FOR THE ANTI-VACUITY CONTROL AND FOR NOTHING ELSE, and named so no reader mistakes it for part
1570// of the pipeline. The failure this file most needs to be unable to hide is the do-nothing unwrapper that
1571// maps every vertex to one point: it has zero angle distortion and zero area distortion by any metric that
1572// averages over non-degenerate triangles, because it HAS no non-degenerate triangles. No solver in here
1573// produces that map, so the only way to show the degeneracy detector fires is to plant it.
1574func uv_force_corner(m: *i64, t: i64, k: i64, u: i64, v: i64) -> i64 {
1575 if t < 0 { return UV_E_ARGS }
1576 if t >= m[UV_H_NT] { return UV_E_ARGS }
1577 if k < 0 { return UV_E_ARGS }
1578 if k >= UV_TRI { return UV_E_ARGS }
1579 m[m[UV_O_CUV] + (t * UV_TRI + k) * UV_CUVREC + UV_P_U] = u
1580 m[m[UV_O_CUV] + (t * UV_TRI + k) * UV_CUVREC + UV_P_V] = v
1581 return UV_OK
1582}
1583
1584// ---- the whole pipeline, in order --------------------------------------------------------------
1585func uv_unwrap(m: *i64) -> i64 {
1586 let a: i64 = uv_chart_build(m)
1587 if a != UV_OK { return a }
1588 let b: i64 = uv_conformal(m)
1589 if b != UV_OK { return b }
1590 let c: i64 = uv_distortion(m)
1591 if c != UV_OK { return c }
1592 return uv_overlap_check(m)
1593}
1594
1595func uv_verdict(m: *i64) -> i64 { return m[UV_H_ERR] }
1596func uv_stage(m: *i64) -> i64 { return m[UV_H_STAGE] }
1597func uv_mv_verdict(m: *i64) -> i64 { return m[UV_H_MVERR] }
1598func uv_mv_closed(m: *i64) -> i64 { return m[UV_H_MVCLOSED] }
1599
1600// ---- the TEXC bridge ---------------------------------------------------------------------------
1601// Writes the payload nx_nxa_texbake_lib already reads: [nv][stride 3][grid g][0] then per vertex
1602// [u Q16][v Q16][chart]. RETURNS THE NUMBER OF TRIANGLE CORNERS WHOSE TEXTURE COORDINATE THE PER-VERTEX
1603// FORM CANNOT CARRY -- corners, counted where they occur, NOT vertices, because naming it a vertex count
1604// would overstate one number and understate the other. Every seam vertex produces some, since a
1605// per-vertex wire format has exactly one slot per vertex and a seam exists to need two. That loss is the
1606// honest cost of the bridge and it is counted rather than hidden: a caller baking a seamed mesh through
1607// the incumbent path needs to know before the bleed shows up in a render.
1608func uv_texc_words(m: *i64) -> i64 { return UV_X_HDR + m[UV_H_NV] * UV_TRI }
1609
1610func uv_emit_texc(m: *i64, out: *i64) -> i64 {
1611 if m[UV_H_STAGE] < UV_ST_SOLVED { return UV_E_STAGE }
1612 let nv: i64 = m[UV_H_NV]
1613 let nt: i64 = m[UV_H_NT]
1614 out[UV_X_NV] = nv
1615 out[UV_X_STRIDE] = UV_TRI
1616 out[UV_X_GRID] = m[UV_H_GRID]
1617 out[UV_X_RSVD] = 0
1618 var i: i64 = 0
1619 while i < nv { out[UV_X_HDR + i * UV_TRI + UV_X_CH] = 0 - 1; i = i + 1 }
1620 var loss: i64 = 0
1621 var t: i64 = 0
1622 while t < nt {
1623 var k: i64 = 0
1624 while k < UV_TRI {
1625 let v: i64 = uv_ti(m, t, k)
1626 let cls: i64 = uv_corner_class(m, t, k)
1627 if out[UV_X_HDR + v * UV_TRI + UV_X_CH] < 0 {
1628 out[UV_X_HDR + v * UV_TRI + UV_X_U] = uv_corner_u(m, t, k)
1629 out[UV_X_HDR + v * UV_TRI + UV_X_V] = uv_corner_v(m, t, k)
1630 out[UV_X_HDR + v * UV_TRI + UV_X_CH] = m[m[UV_O_UVCH] + cls]
1631 m[m[UV_O_STAMP] + m[UV_H_NCORN] + v] = cls
1632 } else {
1633 if m[m[UV_O_STAMP] + m[UV_H_NCORN] + v] != cls { loss = loss + 1 }
1634 }
1635 k = k + 1
1636 }
1637 t = t + 1
1638 }
1639 m[UV_H_TEXCLOSS] = loss
1640 return loss
1641}
1642func uv_texc_loss(m: *i64) -> i64 { return m[UV_H_TEXCLOSS] }