code wiki / (root) / nx_uvunwrap_gate.nx

nx_uvunwrap_gate.nx source

↩ module page · 510 lines · 32240 B

1// nx_uvunwrap_gate.nx -- the gate for nx_uvunwrap_lib (/compare/dcc DC5). 2// 3// THE HAZARD THIS GATE IS SHAPED AGAINST. A parameteriser is unusually easy to fake, because the two 4// obvious ways to fail both score well on the obvious metrics: 5// * MAP EVERY VERTEX TO ONE POINT and every triangle is a perfect similarity of nothing. Angle and area 6// distortion computed over non-degenerate triangles are zero because there are no non-degenerate 7// triangles left to average. The tooth that kills it is chart AREA, and it is planted and bitten here. 8// * FOLD THE CHART OVER ITSELF and the per-triangle metrics stay small while the atlas is unusable. The 9// tooth that kills that one is the flipped-triangle count, and the unseamed cylinder below is a real 10// mesh that actually produces it rather than a synthetic case. 11// So the load-bearing structure is: a POSITIVE CONTROL that must come back at essentially zero, a 12// DISCRIMINATION pair that must come back different, and a planted defect for every refusal class. 13// 14// WHY A DEVELOPABLE SURFACE IS THE RIGHT POSITIVE CONTROL, AND WHY THIS IS A HARD CLAIM RATHER THAN A 15// SOFT ONE. A surface with zero Gaussian curvature is EXACTLY parameterisable -- an isometry into the 16// plane exists, so the true minimum of the conformal energy is zero and any distortion measured on one is 17// a bug in the solver, not a property of the input. Three of the fixtures below are developable for three 18// different reasons (a plane, six planes cut apart, a piecewise-flat prism wall with no interior vertex) 19// and all three must read at or under the tolerance. 20// 21// WHY THE CURVED FIXTURE IS A PYRAMID AND NOT THE CYLINDER THE BRIEF SUGGESTED. A cylinder WALL is 22// developable -- it unrolls with no distortion at all -- so a cylinder reading non-zero would be reporting 23// its own TOPOLOGY (an uncut annulus cannot embed in the plane) rather than curvature, and reading that as 24// a curvature measurement would be measuring a different subject than the one being named. The pyramid's 25// apex is a genuine cone point: four faces meet there with a total angle of 282 degrees against the 360 a 26// plane would need, a deficit of 78 degrees that no cutting can remove. It CANNOT be flattened, so its 27// distortion must be non-zero, and this gate prints the number rather than asserting a bound it invented. 28// The cylinder still appears -- in both states -- because it is the only fixture that proves a seam does 29// anything: the same 16 triangles refuse as a fold uncut, and unwrap cleanly with one edge cut. 30// 31// COMPOSES nx_meshvalid_lib rather than carrying a second validity checker, per the brief and per the 32// duplicate-ruler law. 100% sovereign. No hardware writes (Rule 26). license_tier: ORIGINAL expect_exit: 0 33import "nx_syscalls.nx" 34import "nx_gate_verdict.nx" 35import "nx_meshvalid_lib.nx" 36import "nx_uvunwrap_lib.nx" 37 38// ---- fixture dimensions. Integers chosen so every fixture is exactly representable; none of them is a 39// threshold and none of them is read by the library. ---------------------------------------------- 40const UG_S: i64 = 1000 // flat quad side 41const UG_CS: i64 = 400 // cube side 42const UG_PA: i64 = 100 // pyramid base half-width 43const UG_PH: i64 = 100 // pyramid height 44const UG_R1: i64 = 100 // octagon short radius component 45const UG_R2: i64 = 241 // octagon long radius component, round(100 * (1 + sqrt(2))) 46const UG_CH: i64 = 200 // cylinder height 47const UG_NSEG: i64 = 8 // cylinder segments 48const UG_QUAD_VERTS: i64 = 4 // the flat quad's four corners 49const UG_QUAD_TRIS: i64 = 2 // and the two triangles they split into 50const UG_PYR_VERTS: i64 = 5 // the pyramid's apex plus its four base corners 51const UG_PYR_TRIS: i64 = 4 // and its four side faces 52const UG_CUBE_FACES: i64 = 6 53const UG_CUBE_TRIS_PER_FACE: i64 = 2 54const UG_CUBE_CORNERS_PER_FACE: i64 = 4 55 56// THE TOLERANCE FOR A DEVELOPABLE SURFACE, DERIVED RATHER THAN CHOSEN. Two quantisations stand between 57// an exact parameterisation and the number this gate reads. (1) The emitted coordinates are Q16 integers 58// inside a tile whose inner width is at most 65534, so each vertex carries up to one unit of truncation; 59// against the roughly 46000-unit edges of the flat quad that is 43 parts per million, and both distortion 60// measures are first order in it, so about 0.05 permil. (2) The solver stops at UV_CONV_EPS = 32 working 61// units of a 2^20 span, another 0.06 permil at the same order. Five permil is therefore about twenty-five 62// times the combined floor -- loose enough that rounding can never redden it, tight enough that a real 63// solver defect cannot hide under it. The measured values are PRINTED, so the actual margin is on the 64// record every run rather than being taken on trust from this comment. 65const UG_FLAT_TOL_PERMIL: i64 = 5 66// The pyramid must clear the developable tolerance by a real margin, not by one permil of noise. Ten 67// times is the separation asserted; the true measured value is printed beside it. 68const UG_CURVED_FLOOR: i64 = 50 69 70func ug_eq(a: i64, b: i64) -> i64 { if a == b { return 1 } return 0 } 71func ug_ne(a: i64, b: i64) -> i64 { if a != b { return 1 } return 0 } 72func ug_gt(a: i64, b: i64) -> i64 { if a > b { return 1 } return 0 } 73func ug_le(a: i64, b: i64) -> i64 { if a <= b { return 1 } return 0 } 74func ug_pn(label: *u8, v: i64) -> i64 { gv_puts(label); gv_num(v); gv_puts("\n" as *u8); return 0 } 75 76// THE STRUCTURAL INVARIANT UNDER EVERY DISTORTION NUMBER THIS GATE READS. A triangle must name three 77// DISTINCT uv classes. Two of its corners sharing a class is not a poorly-solved triangle, it is one 78// unknown standing where three belong, so its image is collapsed before any solver runs and every metric 79// taken on it is measuring the corner relabel rather than the parameterisation. Counted, not flagged, so 80// the number is on the record beside the verdict. 81func ug_class_collisions(m: *i64, nt: i64) -> i64 { 82 var t: i64 = 0 83 var bad: i64 = 0 84 while t < nt { 85 let a: i64 = uv_corner_class(m, t, 0) 86 let b: i64 = uv_corner_class(m, t, 1) 87 let c: i64 = uv_corner_class(m, t, 2) 88 var hit: i64 = 0 89 if a == b { hit = 1 } 90 if b == c { hit = 1 } 91 if a == c { hit = 1 } 92 bad = bad + hit 93 t = t + 1 94 } 95 return bad 96} 97 98// ---- fixtures ---------------------------------------------------------------------------------- 99// A flat quad: two coplanar triangles, consistently wound. Developable by inspection -- it is already 100// in a plane -- so the exact answer is a similarity and the exact distortion is zero. 101func ug_quad() -> *i64 { 102 let m: *i64 = uv_new(UG_QUAD_VERTS, UG_QUAD_TRIS) 103 if (m as i64) == 0 { return m } 104 uv_set_vert(m, 0, 0, 0, 0) 105 uv_set_vert(m, 1, UG_S, 0, 0) 106 uv_set_vert(m, 2, UG_S, UG_S, 0) 107 uv_set_vert(m, 3, 0, UG_S, 0) 108 uv_set_tri(m, 0, 0, 1, 2) 109 uv_set_tri(m, 1, 0, 2, 3) 110 return m 111} 112 113// A closed cube, every face wound outward, each face split on a diagonal that is NOT a cube edge. With 114// all twelve cube edges seamed the six faces become six charts; with none seamed it is one chart, and 115// that pair is what makes the chart count a measurement of the seam logic rather than of the mesh. 116func ug_cube(seams: i64) -> *i64 { 117 let m: *i64 = uv_new(8, 12) 118 if (m as i64) == 0 { return m } 119 uv_set_vert(m, 0, 0, 0, 0) 120 uv_set_vert(m, 1, UG_CS, 0, 0) 121 uv_set_vert(m, 2, UG_CS, UG_CS, 0) 122 uv_set_vert(m, 3, 0, UG_CS, 0) 123 uv_set_vert(m, 4, 0, 0, UG_CS) 124 uv_set_vert(m, 5, UG_CS, 0, UG_CS) 125 uv_set_vert(m, 6, UG_CS, UG_CS, UG_CS) 126 uv_set_vert(m, 7, 0, UG_CS, UG_CS) 127 uv_set_tri(m, 0, 0, 3, 2) 128 uv_set_tri(m, 1, 0, 2, 1) 129 uv_set_tri(m, 2, 4, 5, 6) 130 uv_set_tri(m, 3, 4, 6, 7) 131 uv_set_tri(m, 4, 0, 1, 5) 132 uv_set_tri(m, 5, 0, 5, 4) 133 uv_set_tri(m, 6, 3, 7, 6) 134 uv_set_tri(m, 7, 3, 6, 2) 135 uv_set_tri(m, 8, 0, 4, 7) 136 uv_set_tri(m, 9, 0, 7, 3) 137 uv_set_tri(m, 10, 1, 2, 6) 138 uv_set_tri(m, 11, 1, 6, 5) 139 if seams == 1 { 140 uv_seam_set(m, 0, 1) 141 uv_seam_set(m, 1, 2) 142 uv_seam_set(m, 2, 3) 143 uv_seam_set(m, 3, 0) 144 uv_seam_set(m, 4, 5) 145 uv_seam_set(m, 5, 6) 146 uv_seam_set(m, 6, 7) 147 uv_seam_set(m, 7, 4) 148 uv_seam_set(m, 0, 4) 149 uv_seam_set(m, 1, 5) 150 uv_seam_set(m, 2, 6) 151 uv_seam_set(m, 3, 7) 152 } 153 return m 154} 155 156// A square pyramid's four side faces. The apex is an INTERIOR vertex of the chart with an angle sum of 157// 4 * 70.53 = 282.1 degrees, a deficit of 77.9 -- a cone point, so this surface has no isometry into the 158// plane and its distortion is a property of the geometry rather than of the solver. 159func ug_pyramid() -> *i64 { 160 let m: *i64 = uv_new(UG_PYR_VERTS, UG_PYR_TRIS) 161 if (m as i64) == 0 { return m } 162 uv_set_vert(m, 0, 0, 0, UG_PH) 163 uv_set_vert(m, 1, 0 - UG_PA, 0 - UG_PA, 0) 164 uv_set_vert(m, 2, UG_PA, 0 - UG_PA, 0) 165 uv_set_vert(m, 3, UG_PA, UG_PA, 0) 166 uv_set_vert(m, 4, 0 - UG_PA, UG_PA, 0) 167 uv_set_tri(m, 0, 0, 1, 2) 168 uv_set_tri(m, 1, 0, 2, 3) 169 uv_set_tri(m, 2, 0, 3, 4) 170 uv_set_tri(m, 3, 0, 4, 1) 171 return m 172} 173 174func ug_oct_x(i: i64) -> i64 { 175 if i == 0 { return UG_R2 } 176 if i == 1 { return UG_R1 } 177 if i == 2 { return 0 - UG_R1 } 178 if i == 3 { return 0 - UG_R2 } 179 if i == 4 { return 0 - UG_R2 } 180 if i == 5 { return 0 - UG_R1 } 181 if i == 6 { return UG_R1 } 182 return UG_R2 183} 184func ug_oct_y(i: i64) -> i64 { 185 if i == 0 { return UG_R1 } 186 if i == 1 { return UG_R2 } 187 if i == 2 { return UG_R2 } 188 if i == 3 { return UG_R1 } 189 if i == 4 { return 0 - UG_R1 } 190 if i == 5 { return 0 - UG_R2 } 191 if i == 6 { return 0 - UG_R2 } 192 return 0 - UG_R1 193} 194 195// An octagonal prism WALL: two rings, sixteen triangles, open top and bottom. Every vertex lies on a 196// boundary ring, so there is no interior vertex and no angle sum to violate -- the wall is piecewise flat 197// and therefore DEVELOPABLE. It still cannot be laid out uncut, because an annulus does not embed in the 198// plane. One seam along a single vertical edge turns it into a disk. Same sixteen triangles, one call 199// different, opposite outcomes: that is the seam capability measured rather than asserted. 200func ug_cyl(seam: i64) -> *i64 { 201 let m: *i64 = uv_new(UG_NSEG * 2, UG_NSEG * 2) 202 if (m as i64) == 0 { return m } 203 var i: i64 = 0 204 while i < UG_NSEG { 205 uv_set_vert(m, i, ug_oct_x(i), ug_oct_y(i), 0) 206 uv_set_vert(m, UG_NSEG + i, ug_oct_x(i), ug_oct_y(i), UG_CH) 207 i = i + 1 208 } 209 i = 0 210 while i < UG_NSEG { 211 let j: i64 = (i + 1) % UG_NSEG 212 uv_set_tri(m, i * 2 + 0, i, j, UG_NSEG + j) 213 uv_set_tri(m, i * 2 + 1, i, UG_NSEG + j, UG_NSEG + i) 214 i = i + 1 215 } 216 if seam == 1 { uv_seam_set(m, 0, UG_NSEG) } 217 return m 218} 219 220// Three triangles sharing one edge. The defect is the edge, nothing else: the vertices are distinct, no 221// triangle is degenerate and every index is in range, so anything this fixture refuses on it refuses on 222// the non-manifold edge specifically. 223func ug_nonmanifold() -> *i64 { 224 let m: *i64 = uv_new(5, 3) 225 if (m as i64) == 0 { return m } 226 uv_set_vert(m, 0, 0, 0, 0) 227 uv_set_vert(m, 1, UG_S, 0, 0) 228 uv_set_vert(m, 2, 0, UG_S, 0) 229 uv_set_vert(m, 3, 0, 0, UG_S) 230 uv_set_vert(m, 4, 0, 0, 0 - UG_S) 231 uv_set_tri(m, 0, 0, 1, 2) 232 uv_set_tri(m, 1, 0, 1, 3) 233 uv_set_tri(m, 2, 0, 1, 4) 234 return m 235} 236 237func main(argc: i64, argv: *i64) -> i64 { 238 let ctr: *i64 = gv_ctr() 239 gv_head("nx_uvunwrap_gate -- a general UV unwrap for an arbitrary mesh, and the referee that can see a fold" as *u8) 240 241 // ================ THE POSITIVE CONTROL ================ 242 // A developable surface is EXACTLY parameterisable, so any real distortion here is a solver bug. If 243 // this section is not clean, nothing below it means anything. 244 let quad: *i64 = ug_quad() 245 gv_check("fixture-allocated" as *u8, ug_ne(quad as i64, 0), ctr) 246 gv_check("verdict-before-anything-is-run-is-NOT-RUN" as *u8, ug_eq(uv_verdict(quad), UV_E_NOTRUN), ctr) 247 gv_check("relaxing-before-charts-exist-refuses-by-stage" as *u8, ug_eq(uv_conformal(quad), UV_E_STAGE), ctr) 248 let qrc: i64 = uv_unwrap(quad) 249 ug_pn(" quad verdict code = " as *u8, qrc) 250 ug_pn(" quad charts = " as *u8, uv_chart_count(quad)) 251 ug_pn(" quad uv vertices = " as *u8, uv_uvvert_count(quad)) 252 ug_pn(" quad sweeps = " as *u8, uv_iters(quad)) 253 ug_pn(" quad last max move = " as *u8, uv_maxmove(quad)) 254 ug_pn(" quad ANGLE mean permil = " as *u8, uv_angle_mean(quad)) 255 ug_pn(" quad ANGLE max permil = " as *u8, uv_angle_max(quad)) 256 ug_pn(" quad AREA mean permil = " as *u8, uv_area_mean(quad)) 257 ug_pn(" quad AREA max permil = " as *u8, uv_area_max(quad)) 258 ug_pn(" quad chart uv area = " as *u8, uv_chart_uv_area(quad, 0)) 259 // THE CORNER-TO-CLASS MAP ITSELF, PRINTED. It is the one product of the chart stage that every later 260 // stage reads and none of them can repair, so a defect in it surfaces as a solver defect and sends the 261 // reader to the wrong function. On a mesh with no seams it must be a bijection onto the mesh vertices. 262 var qc: i64 = 0 263 while qc < UG_QUAD_TRIS * UV_TRI { 264 ug_pn(" quad corner class = " as *u8, uv_corner_class(quad, qc / UV_TRI, qc % UV_TRI)) 265 qc = qc + 1 266 } 267 ug_pn(" quad class collisions = " as *u8, ug_class_collisions(quad, UG_QUAD_TRIS)) 268 gv_check("flat-quad-unwraps-clean" as *u8, ug_eq(qrc, UV_OK), ctr) 269 gv_check("flat-quad-is-one-chart" as *u8, ug_eq(uv_chart_count(quad), 1), ctr) 270 gv_check("flat-quad-has-four-uv-vertices-not-six-corners" as *u8, ug_eq(uv_uvvert_count(quad), 4), ctr) 271 gv_check("flat-quad-ANGLE-distortion-at-or-near-zero" as *u8, ug_le(uv_angle_max(quad), UG_FLAT_TOL_PERMIL), ctr) 272 gv_check("flat-quad-AREA-distortion-at-or-near-zero" as *u8, ug_le(uv_area_max(quad), UG_FLAT_TOL_PERMIL), ctr) 273 gv_check("flat-quad-has-no-flipped-triangle" as *u8, ug_eq(uv_flipped(quad), 0), ctr) 274 gv_check("flat-quad-converged-inside-its-derived-budget" as *u8, ug_le(uv_maxmove(quad), UV_CONV_EPS), ctr) 275 gv_check("flat-quad-needed-no-singular-vertex-skip" as *u8, ug_eq(uv_singular_sweeps(quad), 0), ctr) 276 // ANTI-VACUITY, the tooth the do-nothing unwrapper cannot pass: a real parameterisation covers area. 277 gv_check("flat-quad-chart-area-is-non-degenerate" as *u8, ug_gt(uv_chart_uv_area(quad, 0), 0), ctr) 278 gv_check("flat-quad-has-no-degenerate-uv-triangle" as *u8, ug_eq(uv_uvdegen(quad), 0), ctr) 279 // THE STRUCTURAL TOOTH UNDERNEATH THE DISTORTION ONES. Every number above is measured on the corner 280 // classes; if two corners of a triangle share one, the triangle was collapsed before the solver was 281 // ever asked, and the distortion figures are then a reading of the relabel wearing the solver's name. 282 gv_check("no-triangle-of-the-flat-quad-names-one-uv-class-twice" as *u8, ug_eq(ug_class_collisions(quad, UG_QUAD_TRIS), 0), ctr) 283 // the emitted coordinates must actually be in the unit square the wire format promises 284 var inside: i64 = 1 285 var t: i64 = 0 286 while t < 2 { 287 var k: i64 = 0 288 while k < 3 { 289 if uv_corner_u(quad, t, k) < 0 { inside = 0 } 290 if uv_corner_v(quad, t, k) < 0 { inside = 0 } 291 if uv_corner_u(quad, t, k) > UV_Q16M { inside = 0 } 292 if uv_corner_v(quad, t, k) > UV_Q16M { inside = 0 } 293 k = k + 1 294 } 295 t = t + 1 296 } 297 gv_check("every-emitted-coordinate-lies-in-the-unit-square" as *u8, inside, ctr) 298 299 // ================ SEAMS AND CHART COUNT ================ 300 let cube0: *i64 = ug_cube(0) 301 let crc0: i64 = uv_chart_build(cube0) 302 gv_check("an-unseamed-closed-cube-is-ONE-chart" as *u8, ug_eq(uv_chart_count(cube0), 1), ctr) 303 gv_check("the-unseamed-cube-reads-CLOSED-through-the-composed-referee" as *u8, ug_eq(uv_mv_closed(cube0), 1), ctr) 304 gv_check("chart-build-on-a-valid-closed-mesh-succeeds" as *u8, ug_eq(crc0, UV_OK), ctr) 305 306 let cube: *i64 = ug_cube(1) 307 let curc: i64 = uv_unwrap(cube) 308 ug_pn(" cube verdict code = " as *u8, curc) 309 ug_pn(" cube seams declared = " as *u8, uv_seam_count(cube)) 310 ug_pn(" cube charts = " as *u8, uv_chart_count(cube)) 311 ug_pn(" cube uv vertices = " as *u8, uv_uvvert_count(cube)) 312 ug_pn(" cube atlas grid = " as *u8, uv_grid(cube)) 313 ug_pn(" cube ANGLE max permil = " as *u8, uv_angle_max(cube)) 314 ug_pn(" cube AREA max permil = " as *u8, uv_area_max(cube)) 315 ug_pn(" cube chart overlaps = " as *u8, uv_overlaps(cube)) 316 gv_check("a-cube-seamed-on-all-twelve-edges-unwraps-clean" as *u8, ug_eq(curc, UV_OK), ctr) 317 gv_check("twelve-seams-were-recorded-not-deduplicated-away" as *u8, ug_eq(uv_seam_count(cube), 12), ctr) 318 gv_check("the-seamed-cube-produces-EXACTLY-SIX-charts" as *u8, ug_eq(uv_chart_count(cube), UG_CUBE_FACES), ctr) 319 // ASSERT THE FIXTURE REACHED THE CONDITION, not only that the total came out right: six charts of two 320 // triangles each is a different fact from six charts totalling twelve triangles. 321 var faces_ok: i64 = 1 322 var c: i64 = 0 323 while c < uv_chart_count(cube) { 324 if uv_chart_tris(cube, c) != UG_CUBE_TRIS_PER_FACE { faces_ok = 0 } 325 c = c + 1 326 } 327 gv_check("every-one-of-the-six-charts-carries-exactly-two-triangles" as *u8, faces_ok, ctr) 328 gv_check("seams-split-the-shared-corners-into-six-times-four-uv-vertices" as *u8, ug_eq(uv_uvvert_count(cube), UG_CUBE_FACES * UG_CUBE_CORNERS_PER_FACE), ctr) 329 gv_check("the-atlas-grid-is-ceil-sqrt-of-the-chart-count" as *u8, ug_eq(uv_grid(cube), 3), ctr) 330 // each cube face is a flat square, so the developable claim applies to all six of them too 331 gv_check("each-cube-face-is-developable-and-reads-at-or-near-zero" as *u8, ug_le(uv_angle_max(cube), UG_FLAT_TOL_PERMIL), ctr) 332 gv_check("cube-faces-carry-no-area-distortion-either" as *u8, ug_le(uv_area_max(cube), UG_FLAT_TOL_PERMIL), ctr) 333 gv_check("six-charts-in-a-three-by-three-atlas-do-not-overlap" as *u8, ug_eq(uv_overlaps(cube), 0), ctr) 334 gv_check("seaming-changes-the-chart-count-one-to-six" as *u8, ug_ne(uv_chart_count(cube0), uv_chart_count(cube)), ctr) 335 ug_pn(" cube class collisions = " as *u8, ug_class_collisions(cube, UG_CUBE_FACES * UG_CUBE_TRIS_PER_FACE)) 336 gv_check("no-cube-triangle-names-one-uv-class-twice" as *u8, ug_eq(ug_class_collisions(cube, UG_CUBE_FACES * UG_CUBE_TRIS_PER_FACE), 0), ctr) 337 338 // ================ THE CURVED CONTROL: HONEST, NOT SILENT ================ 339 let pyr: *i64 = ug_pyramid() 340 let prc: i64 = uv_unwrap(pyr) 341 ug_pn(" pyramid verdict code = " as *u8, prc) 342 ug_pn(" pyramid charts = " as *u8, uv_chart_count(pyr)) 343 ug_pn(" pyramid sweeps = " as *u8, uv_iters(pyr)) 344 ug_pn(" pyramid last max move = " as *u8, uv_maxmove(pyr)) 345 ug_pn(" pyramid ANGLE mean prm = " as *u8, uv_angle_mean(pyr)) 346 ug_pn(" pyramid ANGLE max prm = " as *u8, uv_angle_max(pyr)) 347 ug_pn(" pyramid AREA mean prm = " as *u8, uv_area_mean(pyr)) 348 ug_pn(" pyramid AREA max prm = " as *u8, uv_area_max(pyr)) 349 ug_pn(" pyramid flipped tris = " as *u8, uv_flipped(pyr)) 350 ug_pn(" pyramid chart uv area = " as *u8, uv_chart_uv_area(pyr, 0)) 351 gv_check("a-cone-point-fixture-still-produces-a-map-rather-than-refusing" as *u8, ug_eq(prc, UV_OK), ctr) 352 gv_check("the-cone-point-is-interior-so-the-chart-holds-all-four-faces" as *u8, ug_eq(uv_chart_tris(pyr, 0), 4), ctr) 353 gv_check("a-cone-point-REPORTS-its-distortion-instead-of-reading-zero" as *u8, ug_gt(uv_angle_max(pyr), UG_CURVED_FLOOR), ctr) 354 // THE DISCRIMINATION TOOTH. Either number alone proves nothing -- a metric stuck at zero passes the 355 // developable cases and a metric stuck high passes this one. Only the two together show the referee 356 // is measuring the surface rather than emitting a constant. 357 gv_check("the-referee-SEPARATES-a-cone-point-from-a-plane" as *u8, ug_gt(uv_angle_max(pyr), uv_angle_max(quad) * 10), ctr) 358 gv_check("the-curved-chart-is-still-non-degenerate-in-area" as *u8, ug_gt(uv_chart_uv_area(pyr, 0), 0), ctr) 359 gv_check("the-curved-chart-is-not-folded" as *u8, ug_eq(uv_flipped(pyr), 0), ctr) 360 ug_pn(" pyramid class collis. = " as *u8, ug_class_collisions(pyr, UG_PYR_TRIS)) 361 gv_check("no-pyramid-triangle-names-one-uv-class-twice" as *u8, ug_eq(ug_class_collisions(pyr, UG_PYR_TRIS), 0), ctr) 362 363 // ================ THE SEAM, PROVED ON ONE MESH IN TWO STATES ================ 364 let cyl0: *i64 = ug_cyl(0) 365 let y0: i64 = uv_unwrap(cyl0) 366 ug_pn(" cylinder UNCUT verdict = " as *u8, y0) 367 ug_pn(" cylinder UNCUT charts = " as *u8, uv_chart_count(cyl0)) 368 ug_pn(" cylinder UNCUT uv vts = " as *u8, uv_uvvert_count(cyl0)) 369 ug_pn(" cylinder UNCUT flipped = " as *u8, uv_flipped(cyl0)) 370 ug_pn(" cylinder UNCUT ang max = " as *u8, uv_angle_max(cyl0)) 371 let cyl1: *i64 = ug_cyl(1) 372 let y1: i64 = uv_unwrap(cyl1) 373 ug_pn(" cylinder CUT verdict = " as *u8, y1) 374 ug_pn(" cylinder CUT charts = " as *u8, uv_chart_count(cyl1)) 375 ug_pn(" cylinder CUT uv vts = " as *u8, uv_uvvert_count(cyl1)) 376 ug_pn(" cylinder CUT flipped = " as *u8, uv_flipped(cyl1)) 377 ug_pn(" cylinder CUT sweeps = " as *u8, uv_iters(cyl1)) 378 ug_pn(" cylinder CUT ang max = " as *u8, uv_angle_max(cyl1)) 379 ug_pn(" cylinder CUT area max= " as *u8, uv_area_max(cyl1)) 380 gv_check("one-seam-does-NOT-split-the-ring-into-two-charts" as *u8, ug_eq(uv_chart_count(cyl1), 1), ctr) 381 gv_check("one-seam-DOES-split-two-vertices-into-four-uv-vertices" as *u8, ug_eq(uv_uvvert_count(cyl1), uv_uvvert_count(cyl0) + 2), ctr) 382 gv_check("the-cut-cylinder-wall-unwraps-clean" as *u8, ug_eq(y1, UV_OK), ctr) 383 gv_check("the-cut-wall-is-developable-and-reads-at-or-near-zero" as *u8, ug_le(uv_angle_max(cyl1), UG_FLAT_TOL_PERMIL), ctr) 384 gv_check("the-cut-wall-carries-no-area-distortion-either" as *u8, ug_le(uv_area_max(cyl1), UG_FLAT_TOL_PERMIL), ctr) 385 gv_check("the-cut-wall-has-no-flipped-triangle" as *u8, ug_eq(uv_flipped(cyl1), 0), ctr) 386 ug_pn(" cut wall class collis. = " as *u8, ug_class_collisions(cyl1, UG_NSEG * 2)) 387 gv_check("no-triangle-of-the-cut-wall-names-one-uv-class-twice" as *u8, ug_eq(ug_class_collisions(cyl1, UG_NSEG * 2), 0), ctr) 388 // THE BITE: the identical sixteen triangles must be refused uncut and accepted cut. A parameteriser 389 // that accepted the annulus would be emitting an atlas that cannot be painted, silently. 390 gv_bite("neg-control-uncut-annulus-is-refused" as *u8, ug_ne(y0, UV_OK), ug_ne(y1, UV_OK), ctr) 391 392 // ================ NEGATIVE CONTROLS, one planted defect each ================ 393 // NON-MANIFOLD EDGE, adjudicated by the composed referee rather than by a second checker here. 394 let nm: *i64 = ug_nonmanifold() 395 let nrc: i64 = uv_unwrap(nm) 396 ug_pn(" non-manifold verdict = " as *u8, nrc) 397 ug_pn(" non-manifold mv code = " as *u8, uv_mv_verdict(nm)) 398 gv_bite("neg-control-non-manifold-edge" as *u8, ug_eq(nrc, UV_E_TOPOLOGY), ug_eq(qrc, UV_E_TOPOLOGY), ctr) 399 gv_check("the-topology-refusal-came-from-the-composed-meshvalid-referee" as *u8, ug_eq(uv_mv_verdict(nm), MV_E_NONMAN), ctr) 400 gv_check("a-refused-mesh-emits-no-charts-rather-than-partial-ones" as *u8, ug_eq(uv_chart_count(nm), 0), ctr) 401 // A BOUNDARY EDGE IS NOT A DEFECT AND MUST NOT BE TREATED AS ONE. Reading meshvalid's whole-mesh 402 // verdict instead of the four conjuncts that actually block a parameterisation would refuse every 403 // open surface -- which is every surface worth unwrapping. This is the tooth that pins that choice. 404 gv_check("an-OPEN-surface-is-accepted-though-the-referee-calls-it-not-closed" as *u8, ug_eq(uv_mv_closed(quad), 0), ctr) 405 gv_check("and-the-open-surface-still-unwrapped-clean" as *u8, ug_eq(qrc, UV_OK), ctr) 406 407 // NO PINS. Reachable through the chart index: a chart that does not exist has no vertices, so it can 408 // supply no pins, and the solver refuses by name instead of relaxing an empty system. Stated narrowly 409 // on purpose -- on a mesh that PASSES validation every real chart has three distinct vertices, so the 410 // whole-pipeline route to this refusal is unreachable and this tooth does not pretend otherwise. 411 let pins: *i64 = sys_mmap(2 * 8) as *i64 412 let pgood: i64 = uv_pins_for(cube, 0, pins) 413 let pbad: i64 = uv_pins_for(cube, uv_chart_count(cube), pins) 414 ug_pn(" pins on a real chart = " as *u8, pgood) 415 ug_pn(" pins on an empty chart = " as *u8, pbad) 416 gv_bite("neg-control-chart-with-no-pinnable-vertices" as *u8, ug_eq(pbad, 0), ug_eq(pgood, 0), ctr) 417 gv_check("a-real-chart-establishes-exactly-the-two-LSCM-pins" as *u8, ug_eq(pgood, 2), ctr) 418 gv_check("the-two-pins-are-distinct-uv-vertices" as *u8, ug_ne(pins[0], pins[1]), ctr) 419 420 // OVERLAPPING CHARTS. Tile ownership makes a genuine overlap unreachable, so one is PLANTED -- a 421 // detector that has only ever been asked an impossible question has not been shown to fire. 422 let cubeo: *i64 = ug_cube(1) 423 uv_unwrap(cubeo) 424 let clean_ov: i64 = uv_overlaps(cubeo) 425 uv_force_box(cubeo, 1, uv_chart_box(cubeo, 0, 0), uv_chart_box(cubeo, 0, 1), uv_chart_box(cubeo, 0, 2), uv_chart_box(cubeo, 0, 3)) 426 let orc: i64 = uv_overlap_check(cubeo) 427 ug_pn(" overlaps before plant = " as *u8, clean_ov) 428 ug_pn(" overlaps after plant = " as *u8, uv_overlaps(cubeo)) 429 gv_bite("neg-control-overlapping-charts" as *u8, ug_eq(orc, UV_E_OVERLAP), ug_eq(curc, UV_E_OVERLAP), ctr) 430 gv_check("the-planted-overlap-is-counted-not-just-flagged" as *u8, ug_gt(uv_overlaps(cubeo), clean_ov), ctr) 431 432 // THE COLLAPSED MAP. This is the anti-vacuity control in its executable form: plant the do-nothing 433 // unwrapper's output over a real chart and require the referee to name it degenerate. 434 let coll: *i64 = ug_quad() 435 uv_unwrap(coll) 436 let area_before: i64 = uv_chart_uv_area(coll, 0) 437 var ct: i64 = 0 438 while ct < 2 { 439 var ck: i64 = 0 440 while ck < 3 { 441 uv_force_corner(coll, ct, ck, UV_Q16 / 2, UV_Q16 / 2) 442 ck = ck + 1 443 } 444 ct = ct + 1 445 } 446 let drc: i64 = uv_distortion(coll) 447 ug_pn(" chart area before = " as *u8, area_before) 448 ug_pn(" chart area collapsed = " as *u8, uv_chart_uv_area(coll, 0)) 449 ug_pn(" degenerate uv tris = " as *u8, uv_uvdegen(coll)) 450 gv_bite("neg-control-every-vertex-mapped-to-one-point" as *u8, ug_eq(drc, UV_E_UVDEGEN), ug_eq(qrc, UV_E_UVDEGEN), ctr) 451 gv_check("the-collapsed-map-is-named-degenerate-not-scored-perfect" as *u8, ug_eq(uv_uvdegen(coll), 2), ctr) 452 gv_check("the-collapsed-chart-covers-ZERO-area" as *u8, ug_eq(uv_chart_uv_area(coll, 0), 0), ctr) 453 gv_check("and-the-same-chart-covered-real-area-before-the-plant" as *u8, ug_gt(area_before, 0), ctr) 454 455 // ================ THE TEXC BRIDGE, AND ITS DECLARED LOSS ================ 456 let tw: i64 = uv_texc_words(quad) 457 let qout: *i64 = sys_mmap(uv_texc_words(quad) * 8) as *i64 458 let qloss: i64 = uv_emit_texc(quad, qout) 459 let cout: *i64 = sys_mmap(uv_texc_words(cube) * 8) as *i64 460 let closs: i64 = uv_emit_texc(cube, cout) 461 ug_pn(" texc words for quad = " as *u8, tw) 462 ug_pn(" texc corners lost quad = " as *u8, qloss) 463 ug_pn(" texc corners lost cube = " as *u8, closs) 464 gv_check("the-texc-payload-is-the-header-plus-three-words-per-vertex" as *u8, ug_eq(tw, 4 + 4 * 3), ctr) 465 gv_check("the-texc-header-carries-the-vertex-count" as *u8, ug_eq(qout[0], 4), ctr) 466 gv_check("the-texc-header-carries-the-stride-the-bake-path-expects" as *u8, ug_eq(qout[1], 3), ctr) 467 gv_check("the-texc-header-carries-the-atlas-grid" as *u8, ug_eq(qout[2], uv_grid(quad)), ctr) 468 // THE HONEST HALF. A per-vertex wire format has one slot per vertex and a seam exists precisely to 469 // need two, so this conversion MUST lose coordinates on a seamed mesh. The defect would be reporting 470 // zero; naming the count is the bridge. 471 gv_bite("neg-control-per-vertex-texc-cannot-carry-a-seam" as *u8, ug_gt(closs, 0), ug_gt(qloss, 0), ctr) 472 gv_check("an-unseamed-mesh-crosses-the-bridge-with-no-loss-at-all" as *u8, ug_eq(qloss, 0), ctr) 473 gv_check("the-loss-is-readable-back-off-the-mesh-not-only-returned" as *u8, ug_eq(uv_texc_loss(cube), closs), ctr) 474 475 // ================ THE INSTRUMENT ITSELF ================ 476 // THE REFUSAL THE RIGID UNFOLD USED TO THROW AWAY. uvi_place_third refuses when the edge it is handed 477 // is not an edge of the triangle it has been asked to place; the walk marked that triangle seen 478 // anyway, so the vertex was never written and the chart collapsed in silence where a named refusal 479 // belonged. BITTEN DIRECTLY, because a mesh that passes validation cannot produce the malformed call 480 // from the pipeline, and a guard that has only ever been asked a well-formed question has not been 481 // shown to fire. The positive half is the SAME function on the real shared edge, which must stay quiet. 482 let pth: *i64 = ug_quad() 483 uv_unwrap(pth) 484 let bad_place: i64 = uvi_place_third(pth, 0, 1, 1, 3) 485 let good_place: i64 = uvi_place_third(pth, 0, 1, 2, 0) 486 ug_pn(" place-third bad edge = " as *u8, bad_place) 487 ug_pn(" place-third real edge = " as *u8, good_place) 488 gv_bite("neg-control-place-third-refuses-an-edge-the-triangle-does-not-carry" as *u8, ug_eq(bad_place, UV_E_ARGS), ug_eq(good_place, UV_E_ARGS), ctr) 489 // TWO CAUSES MUST NOT SHARE ONE NUMBER. An unfold that leaves a vertex unplaced and a chart whose two 490 // pins coincide are different faults with different remedies; while they shared UV_E_NOPIN the first 491 // read as the second and the investigation went to the wrong function. 492 gv_check("an-unplaced-vertex-refuses-under-a-code-of-its-own-not-the-no-pins-one" as *u8, ug_ne(UV_E_UNPLACED, UV_E_NOPIN), ctr) 493 gv_check("and-that-code-carries-a-name-of-its-own-too" as *u8, ug_ne(uv_err_name(UV_E_UNPLACED) as i64, uv_err_name(UV_E_NOPIN) as i64), ctr) 494 gv_check("every-refusal-class-carries-a-distinct-name" as *u8, ug_ne(uv_err_name(UV_E_FOLD) as i64, uv_err_name(UV_E_OVERLAP) as i64), ctr) 495 gv_check("a-refusal-code-with-no-class-still-gets-a-name" as *u8, ug_ne(uv_err_name(0 - 999) as i64, 0), ctr) 496 gv_check("an-out-of-range-chart-index-refuses-rather-than-reading-out-of-bounds" as *u8, ug_eq(uv_chart_tris(quad, uv_chart_count(quad)), 0 - 1), ctr) 497 gv_check("a-mesh-too-small-to-hold-a-triangle-is-refused-at-construction" as *u8, ug_eq(uv_new(2, 1) as i64, 0), ctr) 498 gv_check("a-mesh-with-no-triangles-is-refused-at-construction" as *u8, ug_eq(uv_new(4, 0) as i64, 0), ctr) 499 gv_check("a-seam-on-a-vertex-with-itself-is-refused" as *u8, ug_eq(uv_seam_set(ug_quad(), 1, 1), UV_E_ARGS), ctr) 500 gv_check("a-seam-declared-after-charts-are-built-is-refused-by-stage" as *u8, ug_eq(uv_seam_set(quad, 0, 1), UV_E_STAGE), ctr) 501 // THE STRUCTURAL INVARIANT BEHIND A REAL BUG IN THE LIBRARY THIS ONE COMPOSES. nx_meshvalid shipped a 502 // header one word too short; its counters aliased the vertex array and silently moved a corner, and 503 // the only symptom was one detector that could never fire. Stated arithmetically here so a future 504 // slot addition to nx_uvunwrap_lib cannot reintroduce the same aliasing in silence. 505 gv_check("the-header-outranks-every-array-offset-slot" as *u8, ug_gt(UV_HDR, UV_O_ROOT), ctr) 506 gv_check("the-working-span-resolves-the-wire-format-more-finely-than-the-convergence-bar" as *u8, ug_gt(UV_PIN_SPAN / UV_Q16, 1), ctr) 507 gv_check("the-sweep-budget-scales-with-the-chart-rather-than-being-fixed" as *u8, ug_gt(UV_ITER_PER_UVV, 0), ctr) 508 509 return gv_verdict("nx_uvunwrap_gate" as *u8, ctr, "the estate could unwrap exactly one shape of mesh before this -- a rigged humanoid -- and every rival shows distortion in a checker overlay for a person to judge; this one takes an arbitrary mesh, cuts it on declared seams, and REFUSES a fold instead of shipping an atlas nobody can paint" as *u8) 510}