77use super :: * ;
88
99
10- /// Two triangles whose index triples are all distinct, so an unswapped
11- /// pass-through is distinguishable from a correctly reversed winding.
10+ /// Two distinct triangles exercise every index in the frame conversion.
1211const TRIS : [ u32 ; 6 ] = [ 0 , 1 , 2 , 1 , 2 , 3 ] ;
13- /// Per triangle `[a, b, c]` the 2nd/3rd entries swap: `[a, c, b]`.
14- const TRIS_REVERSED : [ u32 ; 6 ] = [ 0 , 2 , 1 , 1 , 3 , 2 ] ;
1512
1613fn cube_corner_positions ( ) -> Vec < f32 > {
1714 vec ! [
@@ -30,35 +27,39 @@ fn yup_swaps_and_negates_the_expected_axes() {
3027 assert_eq ! ( yup_f64( [ 1.0 , 2.0 , 3.0 ] ) , [ 1.0 , 3.0 , -2.0 ] ) ;
3128}
3229
33- /// The module header declares reversed triangle winding a load-bearing part
34- /// of the Z-up→Y-up contract ("mirrors `MeshDataJs::new`, keeps front
35- /// faces"). Nothing pinned it: deleting the swap from BOTH `to_yup_into` and
36- /// `to_yup_in_place` AND from `obj.rs`'s hand-written copy left the whole
37- /// crate suite green (82/82). glTF materials are emitted `doubleSided: true`
38- /// unconditionally, so no renderer-facing assertion can ever fail on
39- /// winding, and the `*_is_byte_identical` tests compare the exporter against
40- /// itself — a bug applied consistently is invisible to them by construction.
41- ///
42- /// So pin each copy DIRECTLY against a literal, rather than against another
43- /// copy: an equivalence test between two implementations of the same
44- /// conversion cannot see a mutation applied symmetrically to both.
45- #[ test]
46- fn to_yup_into_reverses_triangle_winding ( ) {
47- let mut scratch = YUpScratch :: new ( ) ;
48- let positions = cube_corner_positions ( ) ;
49- let normals = vec ! [ 0.0f32 ; positions. len( ) ] ;
50- to_yup_into ( & mut scratch, & positions, & normals, & TRIS , [ 0.0 , 0.0 , 0.0 ] ) ;
51- assert_eq ! ( scratch. indices, TRIS_REVERSED , "streaming path must reverse winding" ) ;
30+ // #4056: an independent geometric invariant catches an incorrect reversal in
31+ // either conversion, even when both implementations make the same mistake.
32+ fn assert_faces_agree_with_normals ( positions : & [ f32 ] , normals : & [ f32 ] , indices : & [ u32 ] ) {
33+ for tri in indices. chunks_exact ( 3 ) {
34+ let p = |i : usize | & positions[ tri[ i] as usize * 3 ..] [ ..3 ] ;
35+ let a = p ( 0 ) ;
36+ let b = p ( 1 ) ;
37+ let c = p ( 2 ) ;
38+ let u = [ b[ 0 ] - a[ 0 ] , b[ 1 ] - a[ 1 ] , b[ 2 ] - a[ 2 ] ] ;
39+ let v = [ c[ 0 ] - a[ 0 ] , c[ 1 ] - a[ 1 ] , c[ 2 ] - a[ 2 ] ] ;
40+ let cross = [ u[ 1 ] * v[ 2 ] - u[ 2 ] * v[ 1 ] , u[ 2 ] * v[ 0 ] - u[ 0 ] * v[ 2 ] , u[ 0 ] * v[ 1 ] - u[ 1 ] * v[ 0 ] ] ;
41+ let n = & normals[ tri[ 0 ] as usize * 3 ..] [ ..3 ] ;
42+ let dot = cross[ 0 ] * n[ 0 ] + cross[ 1 ] * n[ 1 ] + cross[ 2 ] * n[ 2 ] ;
43+ assert ! ( dot > 0.0 , "face must agree with its normal: {dot}" ) ;
44+ }
5245}
5346
5447#[ test]
55- fn to_yup_in_place_reverses_triangle_winding ( ) {
56- let mut positions = cube_corner_positions ( ) ;
57- let mut normals = vec ! [ 0.0f32 ; positions. len( ) ] ;
48+ fn frame_paths_preserve_face_normal_agreement_4056 ( ) {
49+ let positions = cube_corner_positions ( ) ;
50+ let normals = [ 0.0 , 0.0 , 1.0 ] . repeat ( 4 ) ;
51+ assert_faces_agree_with_normals ( & positions, & normals, & TRIS ) ;
52+ let mut scratch = YUpScratch :: new ( ) ;
53+ to_yup_into ( & mut scratch, & positions, & normals, & TRIS , [ 0.0 ; 3 ] ) ;
54+ assert_faces_agree_with_normals ( & scratch. positions , & scratch. normals , & scratch. indices ) ;
55+ assert_eq ! ( scratch. indices, TRIS ) ;
56+
57+ let mut positions = positions;
58+ let mut normals = normals;
5859 let mut indices = TRIS ;
59- let mut origin = [ 0.0f64 ; 3 ] ;
60- to_yup_in_place ( & mut positions, & mut normals, & mut indices, & mut origin ) ;
61- assert_eq ! ( indices, TRIS_REVERSED , "in-place path must reverse winding" ) ;
60+ to_yup_in_place ( & mut positions , & mut normals , & mut indices , & mut [ 0.0 ; 3 ] ) ;
61+ assert_faces_agree_with_normals ( & positions, & normals, & indices) ;
62+ assert_eq ! ( indices, TRIS ) ;
6263}
6364
6465/// The two frame paths are documented as producing identical output; the
@@ -102,31 +103,26 @@ fn to_yup_into_clears_previous_contents_when_reused() {
102103 to_yup_into ( & mut scratch, & positions, & normals, & TRIS , [ 0.0 , 0.0 , 0.0 ] ) ;
103104 assert_eq ! ( scratch. positions. len( ) , positions. len( ) ) ;
104105 assert_eq ! ( scratch. normals. len( ) , normals. len( ) ) ;
105- assert_eq ! ( scratch. indices, TRIS_REVERSED ) ;
106+ assert_eq ! ( scratch. indices, TRIS ) ;
106107}
107108
108109#[ test]
109110fn trailing_partial_triangle_is_left_alone ( ) {
110- // `tri_end` rounds down to a whole triangle; a stray index must not be
111- // swapped into the previous triangle. Kills `% 3` -> `% 2`.
112- //
113- // Both paths compute `tri_end` independently, so both are asserted — the
114- // whole point of this file is that pinning one copy of a duplicated
115- // calculation says nothing about the other.
111+ // A frame rotation leaves every index unchanged, including a partial tail.
116112 let positions = cube_corner_positions ( ) ;
117113 let normals = vec ! [ 0.0f32 ; positions. len( ) ] ;
118114
119115 let mut scratch = YUpScratch :: new ( ) ;
120116 let indices: Vec < u32 > = vec ! [ 0 , 1 , 2 , 3 ] ;
121117 to_yup_into ( & mut scratch, & positions, & normals, & indices, [ 0.0 , 0.0 , 0.0 ] ) ;
122- assert_eq ! ( scratch. indices, vec![ 0 , 2 , 1 , 3 ] , "streaming path" ) ;
118+ assert_eq ! ( scratch. indices, vec![ 0 , 1 , 2 , 3 ] , "streaming path" ) ;
123119
124120 let mut ip = positions. clone ( ) ;
125121 let mut in_ = normals. clone ( ) ;
126122 let mut ii: [ u32 ; 4 ] = [ 0 , 1 , 2 , 3 ] ;
127123 let mut io = [ 0.0f64 ; 3 ] ;
128124 to_yup_in_place ( & mut ip, & mut in_, & mut ii, & mut io) ;
129- assert_eq ! ( ii, [ 0 , 2 , 1 , 3 ] , "in-place path" ) ;
125+ assert_eq ! ( ii, [ 0 , 1 , 2 , 3 ] , "in-place path" ) ;
130126}
131127
132128/// IFC Z is up; glTF Y is up. A rotation about the IFC up-axis has to come out
@@ -201,3 +197,14 @@ fn the_bottom_row_is_left_alone() {
201197 // Column-major: the bottom row is entries 3, 7, 11, 15.
202198 assert_eq ! ( [ y[ 3 ] , y[ 7 ] , y[ 11 ] , y[ 15 ] ] , [ 0.0 , 0.0 , 0.0 , 1.0 ] , "{y:?}" ) ;
203199}
200+
201+ /// #4056: preserving winding during the frame rotation must not erase an
202+ /// actual reflection carried by an element placement.
203+ #[ test]
204+ fn frame_change_preserves_a_reflected_placement_4056 ( ) {
205+ let m = [ -1.0 , 0.0 , 0.0 , 0.0 , 0.0 , 1.0 , 0.0 , 0.0 ,
206+ 0.0 , 0.0 , 1.0 , 0.0 , 0.0 , 0.0 , 0.0 , 1.0 ] ;
207+ let y = yup_matrix4 ( & m) ;
208+ assert_eq ! ( y, m, "an X reflection remains an X reflection after changing up-axis" ) ;
209+ assert_eq ! ( y[ 0 ] * y[ 5 ] * y[ 10 ] , -1.0 ) ;
210+ }
0 commit comments