From b6e815163d6b5bd38c342491504eded4f0183cd9 Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya Date: Sun, 2 Aug 2026 04:26:42 -0700 Subject: [PATCH 1/3] fix(mesh): pave the arc a road curving through a junction actually describes (#356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A junction splits its through road into two arms, and the corner solver extrapolated each arm's facing edge as an infinite straight ray from a single end station. Two collinear edges never meet, so `parallel_edges` caught the straight case and paved a corridor; on a curve the rays met at a fabricated apex metres outside the pavement and the fillet built there paved out to it. Measured at r = 30 m: the apex sat 7.18 m past the true edge, and the 15 m fillet dipped 4.08 m past it. The curved case is now detected the way the straight one always was. When two arms' facing edges share an osculating circle they are one pavement edge running through the junction, so there is no corner to fillet and the mesher paves the arc corridor the edge describes. The arc is anchored through BOTH face corners rather than to either arm's osculating circle: the arms are clothoid-fitted, so those circles differ by centimetres and anchoring to one leaves a sliver at the other. The corridor's inward offset is signed, because "inward" is toward the arc centre only for a convex through-arm. A straight arm has zero edge curvature, so the solve stays exactly the line-line one it has always been. All seven straight-approach fixtures mesh bit-identically, verified by a before/after differential of every fixture and pinned from here on by a golden generated from the pre-fix solver. `junction_corners()` already drops solves it cannot fillet, so a through pair simply reports no corner — as a straight through-road always did — and the Corner tool needs no change. An authored radius on such a pair is ignored, exactly as it always was for a straight one. The reproduction is no longer disabled: `r30_sidewalk_inside` joins the quality matrix, and new gates assert the floor lands on the pavement edge the curve describes and that no corner is reported between a road and itself. Worth recording: without sidewalks the spike was there too and every gate passed it, because a spike is a convex corner and the fillet gate exempts those. Only the sidewalk band wrapped it into a concave needle the matrix could see — which is why the new gates are geometric rather than quality metrics. Closes #356 --- .gitattributes | 7 + CHANGELOG.md | 30 + core/src/mesh/junction_corner_detail.cpp | 54 + core/src/mesh/junction_corner_detail.hpp | 26 + core/src/mesh/junction_surface.cpp | 38 + .../data/junction/straight_floor_golden.txt | 1599 +++++++++++++++++ core/tests/test_t_junction_quality.cpp | 295 ++- 7 files changed, 1987 insertions(+), 62 deletions(-) create mode 100644 core/tests/data/junction/straight_floor_golden.txt diff --git a/.gitattributes b/.gitattributes index 6012ddb4..7cc55a72 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,10 @@ editor/resources/help/help.css text eol=lf # test reads in binary, so without this the Windows runner's autocrlf checkout # fails the gate on line endings alone — which is exactly how it first failed. tests/esmini/*.xosc text eol=lf + +# Third instance (#356): the straight-approach junction-floor golden is what the +# mesher emits, compared line by line by TJunctionStraightIdentity +# .StraightApproachFloorsMatchTheGolden. The generator writes '\n' and the test +# reads in binary, so without this the Windows runner's autocrlf checkout would +# fail the gate on line endings alone. +core/tests/data/junction/straight_floor_golden.txt text eol=lf diff --git a/CHANGELOG.md b/CHANGELOG.md index ae1a8105..519c47d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1370,6 +1370,36 @@ Current version on `main`: **0.0.1**. about silently never matched. ### Fixed +- **A road curving through a junction no longer paves a spike of pavement that + belongs to no road** ([#356](https://github.com/Robomous/RoadMaker/issues/356)). + A junction splits its through road into two arms, and the corner solver + extrapolated each arm's facing edge as an infinite **straight ray** from a + single end station. Two collinear edges never meet, so straight roads were + always fine — but on a curve the rays met at a fabricated apex metres outside + the pavement, and the fillet built there paved out to it. For a 30 m radius + the apex sat **7.18 m** past the true edge and the fillet dipped 4.08 m past + it. + + The curved case is now detected the way the straight one always was: when the + two arms' facing edges share an osculating circle they are one pavement edge + running through the junction, so there is no corner to fillet and the mesher + paves the arc corridor the edge itself describes. The corridor is anchored to + an arc through **both** face corners rather than to either arm's osculating + circle — the arms are clothoid-fitted, so those two circles differ by + centimetres, and anchoring to one leaves a sliver at the other. The floor's + southernmost point now lands on the true pavement edge to the millimetre. + + A straight arm has zero edge curvature, so the solve stays exactly the + line-line one it has always been: all seven straight-approach fixtures mesh + **bit-identically**, pinned from here on by a committed golden. `#356`'s + reproduction is no longer disabled — it is a matrix case, and three new + geometric gates assert the floor stays on the pavement the curve describes and + that no corner is reported between a road and itself. + + Worth recording, because it is why this survived so long: without sidewalks + the spike was there too and **every gate passed it**, since a spike is a convex + corner and the fillet gate exempts those. Only the sidewalk band wrapped it + into a concave needle the matrix could see. - **Virtual-junction linkage and connection internals round-trip** ([#537](https://github.com/Robomous/RoadMaker/issues/537)). Three holes, all in modeled scopes that #453's preservation sweep deliberately does not cover: diff --git a/core/src/mesh/junction_corner_detail.cpp b/core/src/mesh/junction_corner_detail.cpp index cc70b11b..155a216d 100644 --- a/core/src/mesh/junction_corner_detail.cpp +++ b/core/src/mesh/junction_corner_detail.cpp @@ -164,6 +164,20 @@ std::vector corner_faces(const RoadNetwork& network, const Junction& // frame is flipped relative to the entering direction, so its road-left // corner is the entering-right one. const bool flipped = arm.contact == ContactPoint::Start; + // True pavement-edge curvature at the face. The centerline curvature is + // left-positive along +s; an edge offset t metres left of it rides a + // radius (1/kappa - t), and traversing the arm backwards (a Start contact + // enters the junction along -s) negates the signed curvature. + const double kappa_c = road->plan_view.evaluate(station).curvature; + const auto edge_kappa = [&](double t) { + const double denom = 1.0 - (kappa_c * t); + if (std::abs(denom) < tol::kLength) { + return 0.0; + } + return sign * kappa_c / denom; + }; + const double k_front = edge_kappa(offsets.front()); + const double k_back = edge_kappa(offsets.back()); faces.push_back(CornerFace{ .left = flipped ? std::array{right[0], right[1]} : std::array{left[0], left[1]}, @@ -172,6 +186,8 @@ std::vector corner_faces(const RoadNetwork& network, const Junction& .ix = ix, .iy = iy, .arm = arm, + .kappa_left = flipped ? k_back : k_front, + .kappa_right = flipped ? k_front : k_back, }); } if (faces.size() < 2) { @@ -254,6 +270,44 @@ CornerSolution solve_corner(const RoadNetwork& network, solution.parallel_edges = true; return solution; } + // A curved road split by the junction presents its two halves as two arms + // whose facing edges are one continuous pavement edge. Straight rays from + // the two faces meet at a fabricated apex metres outside the pavement, and + // filleting it paves a spike that belongs to no road (#356). Detect it the + // way `parallel_edges` detects the straight case — by the edges being the + // same curve — and report the arc instead of a corner. + if (std::abs(a.kappa_right) > kThroughEdgeMinCurvature && + std::abs(b.kappa_left) > kThroughEdgeMinCurvature) { + // Osculating circle of each edge: centre one signed radius to the left. + const double ra = 1.0 / a.kappa_right; + const double rb = 1.0 / b.kappa_left; + const std::array ca{pa[0] - (ra * a.iy), pa[1] + (ra * a.ix)}; + const std::array cb{pb[0] - (rb * b.iy), pb[1] + (rb * b.ix)}; + // Traversed in opposing directions, one continuous edge has curvatures of + // OPPOSITE sign, so compare |r| and the centres. + if (std::hypot(ca[0] - cb[0], ca[1] - cb[1]) < kThroughEdgeTolerance && + std::abs(std::abs(ra) - std::abs(rb)) < kThroughEdgeTolerance) { + // Pave the arc THROUGH both face corners rather than either osculating + // circle. The arms are clothoid-fitted, so their two osculating circles + // differ by centimetres; an arc anchored to one of them misses the other + // arm's face and leaves exactly the sliver this fix exists to remove. + // Tangent to A's edge at pa and passing through pb: + // centre = pa + t * normal(A), t = |chord|^2 / (2 * normal . chord) + const std::array normal{-a.iy, a.ix}; + const std::array chord{pb[0] - pa[0], pb[1] - pa[1]}; + const double denom = 2.0 * ((normal[0] * chord[0]) + (normal[1] * chord[1])); + if (std::abs(denom) > tol::kLength) { + const double t = ((chord[0] * chord[0]) + (chord[1] * chord[1])) / denom; + solution.through_edge = true; + solution.arc_center = {pa[0] + (t * normal[0]), pa[1] + (t * normal[1])}; + // `normal` is the interior side (the same one the straight strips use), + // so the sign of t already says which side of the edge the pavement is + // on: keep it rather than take the magnitude. + solution.arc_radius = t; + return solution; + } + } + } // Edge-line intersection pa + ta·A_dir = pb + tb·B_dir. const double dx = pb[0] - pa[0]; const double dy = pb[1] - pa[1]; diff --git a/core/src/mesh/junction_corner_detail.hpp b/core/src/mesh/junction_corner_detail.hpp index 1b0e91c9..e55b022c 100644 --- a/core/src/mesh/junction_corner_detail.hpp +++ b/core/src/mesh/junction_corner_detail.hpp @@ -70,6 +70,15 @@ inline constexpr double kMinFilletRadius = 0.05; /// Smallest authored tangent-leg setback [m]. inline constexpr double kMinFilletExtent = 0.05; +/// Below this curvature [1/m] an arm's edge is treated as straight, so the +/// straight-arm corner solve stays exactly the line-line one it always was +/// (2 km radius — far beyond any junction-scale corner). +inline constexpr double kThroughEdgeMinCurvature = 5e-4; + +/// How closely two arms' facing edges must share an osculating circle [m] to +/// count as one pavement edge running through the junction. +inline constexpr double kThroughEdgeTolerance = 1.5; + /// Connecting roads of this junction, in connection order, de-duplicated. [[nodiscard]] std::vector connecting_roads(const Junction& junction); @@ -105,6 +114,11 @@ struct CornerFace { double ix = 0.0; double iy = 0.0; RoadEnd arm; + /// Signed plan curvature [1/m] of the `left`/`right` pavement edge at the + /// face, traversed INTO the junction (left-positive). Zero for a straight + /// arm, which is what makes the straight-arm corner solve bit-identical. + double kappa_left = 0.0; + double kappa_right = 0.0; }; /// The arm faces of `junction`, INTO the junction, sorted CCW around their @@ -131,6 +145,18 @@ struct CornerSolution { bool parallel_edges = false; bool corner_exists = false; + /// The two faces' edges are the SAME pavement edge curving through the + /// junction (a curved road split by it) — the curved analogue of + /// `parallel_edges`. There is no corner to fillet; the mesher paves the + /// arc corridor described by `arc_center` / `arc_radius`. + bool through_edge = false; + std::array arc_center{}; + /// SIGNED radius [m] of that arc: positive when the centre lies on the + /// pavement side of the edge (a convex through-arm), negative when the + /// pavement is on the far side (a concave one). The magnitude is the radius; + /// the sign is which way "one strip width inward" points. + double arc_radius = 0.0; + /// Edge-line intersection: A's right edge meeting B's left edge. std::array corner{}; diff --git a/core/src/mesh/junction_surface.cpp b/core/src/mesh/junction_surface.cpp index b86d6cc4..dc63afdf 100644 --- a/core/src/mesh/junction_surface.cpp +++ b/core/src/mesh/junction_surface.cpp @@ -171,6 +171,44 @@ void append_corner_fillets(const RoadNetwork& network, } continue; } + if (solution.through_edge) { + // One pavement edge curving through the junction: pave the corridor the + // edge itself describes, sampled at the fillet sagitta, rather than the + // straight-ray corner it has no business having (#356). + const std::array& c = solution.arc_center; + const double r = std::abs(solution.arc_radius); + const double ang_a = std::atan2(pa[1] - c[1], pa[0] - c[0]); + double ang_b = std::atan2(pb[1] - c[1], pb[0] - c[0]); + while (ang_b - ang_a > std::numbers::pi) { + ang_b -= 2.0 * std::numbers::pi; + } + while (ang_a - ang_b > std::numbers::pi) { + ang_b += 2.0 * std::numbers::pi; + } + const double sweep = ang_b - ang_a; + const double step = + 2.0 * + std::acos(std::clamp(1.0 - (junction_corner_detail::kFilletArcSagitta / r), 0.0, 1.0)); + const int steps = + std::max(4, static_cast(std::ceil(std::abs(sweep) / std::max(step, 1e-3)))); + // One strip width INWARD, exactly as the straight strips run. Inward is + // toward the arc centre only for a convex through-arm; for a concave one + // (the arms meeting on the inside of the curve) the pavement is on the + // far side, so the strip has to widen the radius instead. The sign of + // `arc_radius` carries that. + const double r_in = std::max(r - std::copysign(kEdgeStripWidth, solution.arc_radius), 0.0); + Clipper2Lib::PathD corridor; + for (int k = 0; k <= steps; ++k) { + const double ang = ang_a + (sweep * static_cast(k) / steps); + corridor.emplace_back(c[0] + (r * std::cos(ang)), c[1] + (r * std::sin(ang))); + } + for (int k = steps; k >= 0; --k) { + const double ang = ang_a + (sweep * static_cast(k) / steps); + corridor.emplace_back(c[0] + (r_in * std::cos(ang)), c[1] + (r_in * std::sin(ang))); + } + push_ccw(std::move(corridor)); + continue; + } if (!solution.valid) { continue; } diff --git a/core/tests/data/junction/straight_floor_golden.txt b/core/tests/data/junction/straight_floor_golden.txt new file mode 100644 index 00000000..1d8b7e88 --- /dev/null +++ b/core/tests/data/junction/straight_floor_golden.txt @@ -0,0 +1,1599 @@ +# perp floor0 +v -4.0229034423828125 -6.4698734283447266 0 +v -2.5999889373779297 -4.5999917984008789 0 +v -4.3247222900390625 -6.125584602355957 0 +v 1.4000110626220703 -0.59999179840087891 0 +v -0.59998893737792969 -0.59999179840087891 0 +v 1.4000110626220703 -2.5999917984008789 0 +v 3.4000110626220703 -2.5999917984008789 0 +v 3.4000110626220703 -0.59999179840087891 0 +v 1.4000110626220703 -6.5999917984008789 0 +v 2.9999930858612061 -8.5999882221221924 0 +v 3.4000110626220703 -6.5999917984008789 0 +v -6.5999889373779297 -0.59999179840087891 0 +v -6.4499926567077637 3.2999870777130127 0 +v -8.5999870300292969 1.1999952793121338 0 +v 8.5999999999999943 -5.0999999999999996 0 +v 8.5999813079833984 -2.9999911785125732 0 +v 7.6932001113891602 -5.2195758819580078 0 +v 5.4000110626220703 -2.5999917984008789 0 +v 5.4000110626220703 -0.59999179840087891 0 +v 4.2999887466430664 3.2999835014343262 0 +v -4.5999889373779297 -2.5999917984008789 0 +v -2.5999889373779297 -2.5999917984008789 0 +v -4.5999889373779297 -0.59999179840087891 0 +v -8.5999851226806641 -0.89999723434448242 0 +v -3.814697265625e-06 3.2999849319458008 0 +v -2.1500000953674316 3.2999856472015381 0 +v -4.5999889373779297 -4.5999917984008789 0 +v 5.4000110626220703 -4.5999917984008789 0 +v 7.2611923217773438 -5.3662233352661133 0 +v -6.5999889373779297 -2.5999917984008789 0 +v -5.4600248336791992 -5.3667087554931641 0 +v -5.0494918823242188 -5.5692501068115234 0 +v -3.7686395645141602 -6.8505487442016602 0 +v -3.5667085647583008 -7.2600250244140625 0 +v -2.5999889373779297 -6.5999917984008789 0 +v -0.59998893737792969 -4.5999917984008789 0 +v -0.59998893737792969 -6.5999917984008789 0 +v -2.5999889373779297 -0.59999179840087891 0 +v -0.59998893737792969 -2.5999917984008789 0 +v -4.2999963760375977 3.2999863624572754 0 +v -3.3000000000000025 -8.5999999999999961 0 +v -1.1999986171722412 -8.5999906063079834 0 +v -3.419245719909668 -7.6944198608398438 0 +v 5.1000000000000014 -8.6000000000000014 0 +v 5.2195758819580078 -7.6932001113891602 0 +v 6.8505878448486328 -5.5686197280883789 0 +v 6.4698371887207031 -5.8229284286499023 0 +v 1.4000110626220703 -4.5999917984008789 0 +v 3.4000110626220703 -4.5999917984008789 0 +v 6.4499850273132324 3.2999827861785889 0 +v 8.5999813079833984 1.19999098777771 0 +v 8.5999999999999943 3.2999999999999998 0 +v -5.8938684463500977 -5.2193737030029297 0 +v -8.5999832153320312 -2.9999897480010986 0 +v -8.5999999999999943 -5.0999999999999996 0 +v -6.8006515502929688 -5.1000003814697266 0 +v -4.6698732376098633 -5.8229026794433594 0 +v -8.5999999999999943 3.2999999999999998 0 +v 2.1499924659729004 3.2999842166900635 0 +v 0.89999723434448242 -8.5999894142150879 0 +v 5.3666896820068359 -7.2600650787353516 0 +v 6.1256179809570312 -6.1246929168701172 0 +v 5.8236713409423828 -6.4688692092895508 0 +v 5.5692739486694336 -6.8494548797607422 0 +v 8.5999813079833984 -0.90000009536743164 0 +f 0 1 2 +f 3 4 5 +f 5 6 7 +f 8 9 10 +f 11 12 13 +f 14 15 16 +f 7 6 17 +f 7 18 19 +f 20 21 22 +f 13 23 11 +f 24 25 4 +f 11 22 12 +f 1 26 2 +f 27 17 6 +f 4 3 24 +f 28 15 27 +f 27 15 17 +f 11 29 22 +f 30 31 26 +f 32 33 34 +f 1 0 34 +f 35 36 8 +f 37 38 4 +f 1 21 26 +f 39 12 22 +f 22 37 39 +f 40 41 42 +f 9 43 44 +f 45 27 46 +f 47 5 35 +f 48 8 10 +f 19 18 49 +f 7 17 18 +f 49 50 51 +f 26 29 52 +f 53 54 55 +f 31 56 26 +f 20 22 29 +f 30 26 52 +f 42 41 34 +f 33 42 34 +f 32 34 0 +f 21 1 35 +f 1 36 35 +f 53 29 23 +f 11 23 29 +f 53 55 29 +f 57 13 12 +f 21 37 22 +f 37 21 38 +f 35 38 21 +f 6 5 48 +f 25 39 37 +f 58 24 3 +f 37 4 25 +f 41 59 36 +f 9 8 59 +f 48 10 27 +f 9 44 10 +f 60 10 44 +f 61 27 62 +f 10 62 27 +f 10 60 63 +f 27 61 46 +f 64 17 15 +f 5 38 35 +f 38 5 4 +f 47 48 5 +f 48 27 6 +f 58 7 19 +f 64 18 17 +f 64 50 18 +f 16 15 28 +f 50 49 18 +f 55 52 29 +f 56 2 26 +f 36 34 41 +f 36 1 34 +f 20 26 21 +f 20 29 26 +f 36 59 8 +f 62 10 63 +f 27 45 28 +f 47 8 48 +f 47 35 8 +f 3 7 58 +f 3 5 7 +# deg45 floor0 +v -13.455129623413086 -5.5496816635131836 0 +v -11.485264778137207 -4.5559654235839844 0 +v -13.742633819580078 -5.3574819564819336 0 +v -3.9682877858479819 -11.180785497029623 0 +v -3.485264778137207 -8.5559654235839844 0 +v -5.485264778137207 -10.555965423583984 0 +v 10.712017377217609 -5.1000054677327471 0 +v 13.098640124003092 -5.0999933878580732 0 +v 12.514735221862793 -2.5559654235839844 0 +v 10.514735221862793 -0.55596542358398438 0 +v 12.514735221862793 -0.55596542358398438 0 +v -7.485264778137207 -12.555965423583984 0 +v -5.6558949152628584 -12.868375460306803 0 +v -11.485264778137207 -0.55596542358398438 0 +v -13.420562998453775 3.2999820709228516 0 +v -13.485264778137207 -0.55596542358398438 0 +v -13.485264778137207 -2.5559654235839844 0 +v -7.485264778137207 -8.5559654235839844 0 +v -7.485264778137207 -10.555965423583984 0 +v 0.51473522186279297 -4.5559654235839844 0 +v -1.485264778137207 -6.5559654235839844 0 +v 0.56536960601806641 -7.2634449005126953 0 +v 2.514735221862793 -4.5559654235839844 0 +v 0.51473522186279297 -2.5559654235839844 0 +v -12.883953094482422 -8.053471565246582 0 +v -13.283199846221432 -8.6162950903902278 0 +v -11.485264778137207 -8.5559654235839844 0 +v -14.059408187866211 -5.2156658172607422 0 +v 4.514735221862793 -0.55596542358398438 0 +v 4.514735221862793 -2.5559654235839844 0 +v 6.514735221862793 -0.55596542358398438 0 +v 4.514735221862793 -4.5559654235839844 0 +v 3.8634347915649414 -5.7791061401367188 0 +v -5.485264778137207 -0.55596542358398438 0 +v -7.485264778137207 -0.55596542358398438 0 +v -5.485264778137207 -2.5559654235839844 0 +v -3.485264778137207 -2.5559654235839844 0 +v -3.485264778137207 -0.55596542358398438 0 +v -2.2806806564331055 -9.4931955337524414 0 +v -0.92473220825195312 -8.2919836044311523 0 +v 2.514735221862793 -2.5559654235839844 0 +v 2.514735221862793 -0.55596542358398438 0 +v 0.51473522186279297 -0.55596542358398438 0 +v 15.485262632369995 -2.9999904632568359 0 +v 13.420560137430826 3.2999820709228516 0 +v 15.485262155532837 1.1999912261962891 0 +v -11.798269987106323 -10.101212501525879 0 +v -9.485264778137207 -0.55596542358398438 0 +v -9.485264778137207 -2.5559654235839844 0 +v -9.485264778137207 -4.5559654235839844 0 +v -9.485264778137207 -10.555965423583984 0 +v -9.485264778137207 -8.5559654235839844 0 +v -11.485264778137207 -6.5559654235839844 0 +v -5.485264778137207 -4.5559654235839844 0 +v -5.485264778137207 -6.5559654235839844 0 +v -3.485264778137207 -4.5559654235839844 0 +v -15.485263347625732 -0.90000009536743164 0 +v -7.485264778137207 -4.5559654235839844 0 +v -7.485264778137207 -2.5559654235839844 0 +v 11.355858357747396 3.2999820709228516 0 +v 6.514735221862793 -2.5559654235839844 0 +v 8.514735221862793 -2.5559654235839844 0 +v -1.485264778137207 -4.5559654235839844 0 +v 3.0069084167480469 -6.0746555328369141 0 +v 2.1692285537719727 -6.4216337203979492 0 +v -11.355861218770347 3.2999820709228516 0 +v -1.485264778137207 -0.55596542358398438 0 +v -1.485264778137207 -2.5559654235839844 0 +v -5.1617558797200509 3.2999820709228516 0 +v 10.514735221862793 -2.5559654235839844 0 +v 5.1617530186971035 3.2999820709228516 0 +v -10.313347339630127 -11.586130142211914 0 +v -12.692941665649414 -7.3908357620239258 0 +v -12.760236740112305 -7.7291994094848633 0 +v -11.485264778137207 -2.5559654235839844 0 +v -12.731491088867188 -6.7018728256225586 0 +v -12.683161735534668 -7.0451478958129883 0 +v -8.8284246921539307 -13.071047782897949 0 +v -7.485264778137207 -6.5559654235839844 0 +v -5.485264778137207 -8.5559654235839844 0 +v -15.485262632369995 -2.9999911785125732 0 +v -15.485281374238568 -5.0999999999999996 0 +v -14.738866806030273 -5.1001405715942383 0 +v -13.203314781188965 -5.7879352569580078 0 +v -12.995427131652832 -6.064300537109375 0 +v -9.485264778137207 -6.5559654235839844 0 +v -9.2911594390869148 3.2999820709228516 0 +v -7.2264576594034828 3.2999820709228516 0 +v -7.3435028842544341 -14.555992052357226 0 +v -3.485264778137207 -6.5559654235839844 0 +v -3.097054100036619 3.2999820709228516 0 +v -1.0323523203531906 3.2999820709228516 0 +v 7.2264547983805336 3.2999820709228516 0 +v 1.0323494593302414 3.2999820709228516 0 +v 8.514735221862793 -0.55596542358398438 0 +v 6.5180902481079102 -5.2093391418457031 0 +v 8.3253946304321289 -5.1000175476074219 0 +v 15.485262393951416 -0.89999961853027344 0 +v 9.2911565780639656 3.2999820709228516 0 +v -12.836437225341797 -6.3723459243774414 0 +v -15.48526406288147 1.19999098777771 0 +v -15.485281374238564 3.2999999999999989 0 +v -0.19551372528076172 -7.7554149627685547 0 +v 1.354578971862793 -6.8183097839355469 0 +v 5.6215686798095703 -5.34576416015625 0 +v 4.7356729507446289 -5.5359516143798828 0 +v 3.0970512390136715 3.2999820709228516 0 +v 15.485281374238568 -5.0999999999999996 0 +v 15.485281374238568 3.2999999999999998 0 +f 0 1 2 +f 3 4 5 +f 6 7 8 +f 9 8 10 +f 5 11 12 +f 13 14 15 +f 13 15 16 +f 17 18 5 +f 19 20 21 +f 22 23 19 +f 24 25 26 +f 27 2 16 +f 28 29 30 +f 31 22 32 +f 33 34 35 +f 36 37 35 +f 38 39 4 +f 40 41 23 +f 42 23 41 +f 7 43 8 +f 44 10 45 +f 46 26 25 +f 47 13 48 +f 49 48 1 +f 18 50 11 +f 26 51 52 +f 17 50 18 +f 53 54 55 +f 56 16 15 +f 57 35 58 +f 57 58 49 +f 44 59 10 +f 58 35 34 +f 60 61 30 +f 20 4 39 +f 62 55 20 +f 63 22 64 +f 14 13 65 +f 66 67 42 +f 33 37 68 +f 23 42 67 +f 69 6 8 +f 28 30 70 +f 46 71 50 +f 72 73 26 +f 74 1 48 +f 75 76 52 +f 71 77 50 +f 77 11 50 +f 18 11 5 +f 51 26 50 +f 78 17 54 +f 79 17 5 +f 78 54 57 +f 80 81 82 +f 47 65 13 +f 83 84 1 +f 85 51 78 +f 58 48 49 +f 58 34 48 +f 47 34 86 +f 33 87 34 +f 88 12 11 +f 62 67 55 +f 89 55 54 +f 29 31 60 +f 36 67 37 +f 23 67 19 +f 90 66 91 +f 92 70 30 +f 93 91 42 +f 42 41 93 +f 63 32 22 +f 94 92 30 +f 95 96 60 +f 43 97 8 +f 61 60 96 +f 94 9 98 +f 94 30 61 +f 69 8 9 +f 8 97 10 +f 73 24 26 +f 76 72 52 +f 50 26 46 +f 16 2 1 +f 52 1 84 +f 75 52 99 +f 99 52 84 +f 26 52 72 +f 85 78 49 +f 77 88 11 +f 12 3 5 +f 79 5 4 +f 51 17 78 +f 51 50 17 +f 79 54 17 +f 89 54 4 +f 85 52 51 +f 85 49 52 +f 80 82 16 +f 16 82 27 +f 74 48 13 +f 80 16 56 +f 100 56 15 +f 14 100 15 +f 14 101 100 +f 83 1 0 +f 49 1 52 +f 74 16 1 +f 74 13 16 +f 53 57 54 +f 57 49 78 +f 53 35 57 +f 53 55 35 +f 87 86 34 +f 47 48 34 +f 86 65 47 +f 68 87 33 +f 33 35 37 +f 79 4 54 +f 38 4 3 +f 21 20 102 +f 20 39 102 +f 22 19 64 +f 19 21 103 +f 89 20 55 +f 89 4 20 +f 62 19 67 +f 62 20 19 +f 103 64 19 +f 104 31 105 +f 36 55 67 +f 36 35 55 +f 66 37 67 +f 66 90 37 +f 90 68 37 +f 42 91 66 +f 40 22 31 +f 40 23 22 +f 40 29 41 +f 40 31 29 +f 106 93 41 +f 31 32 105 +f 31 104 60 +f 107 43 7 +f 61 96 69 +f 95 60 104 +f 60 30 29 +f 6 69 96 +f 69 9 61 +f 28 41 29 +f 28 106 41 +f 70 106 28 +f 59 98 9 +f 94 61 9 +f 98 92 94 +f 9 10 59 +f 97 45 10 +f 44 45 108 +# deg135 floor0 +v 0.51473522186279297 -3.2831869125366211 0 +v 2.514735221862793 -5.2831869125366211 0 +v 2.514735221862793 -3.2831869125366211 0 +v 4.514735221862793 -5.2831869125366211 0 +v 6.514735221862793 -5.2831869125366211 0 +v 4.514735221862793 -3.2831869125366211 0 +v -7.2264576594034828 3.299982770284017 0 +v -9.2911594390869148 3.2999828338623045 0 +v -7.485264778137207 0.71681308746337891 0 +v 0.51473522186279297 -5.2831869125366211 0 +v 0.51473522186279297 -1.2831869125366211 0 +v 2.514735221862793 -1.2831869125366211 0 +v 0.51473522186279297 0.71681308746337891 0 +v -13.420562998453775 3.29998296101888 0 +v -11.485264778137207 0.71681308746337891 0 +v -11.355861218770347 3.2999828974405925 0 +v -1.485264778137207 0.71681308746337891 0 +v -3.485264778137207 0.71681308746337891 0 +v -1.485264778137207 -1.2831869125366211 0 +v -1.485264778137207 -3.2831869125366211 0 +v 6.514735221862793 -3.2831869125366211 0 +v 6.514735221862793 -7.2831869125366211 0 +v 4.514735221862793 -7.2831869125366211 0 +v 14.224413871765137 -6.043787956237793 0 +v 14.331413269042969 -5.7853679656982422 0 +v 12.514735221862793 -5.2831869125366211 0 +v 8.514735221862793 -5.2831869125366211 0 +v -7.485264778137207 -3.2831869125366211 0 +v -5.485264778137207 -3.2831869125366211 0 +v -7.485264778137207 -1.2831869125366211 0 +v -11.485264778137207 -3.2831869125366211 0 +v -11.485264778137207 -1.2831869125366211 0 +v -13.485264778137207 -3.2831869125366211 0 +v -5.485264778137207 0.71681308746337891 0 +v -5.1617558797200509 3.299982706705729 0 +v 8.514735221862793 -7.2831869125366211 0 +v 8.514735221862793 -9.2831869125366211 0 +v 4.514735221862793 -1.2831869125366211 0 +v 12.514735221862793 -1.2831869125366211 0 +v 12.514735221862793 0.71681308746337891 0 +v 10.514735221862793 -1.2831869125366211 0 +v 6.9002088486008795 -11.681434044836321 0 +v 8.514735221862793 -11.283186912536621 0 +v 6.514735221862793 -9.2831869125366211 0 +v 10.514735221862793 -3.2831869125366211 0 +v 8.514735221862793 -3.2831869125366211 0 +v -9.485264778137207 0.71681308746337891 0 +v 10.514735221862793 -5.2831869125366211 0 +v 14.939069747924805 -5.2190141677856445 0 +v 12.514735221862793 -3.2831869125366211 0 +v 14.698625564575195 -5.3615818023681641 0 +v -10.632535934448242 -5.0999999046325684 0 +v -13.05889892578125 -5.0999910831451416 0 +v -5.7798099517822266 -5.1000175476074219 0 +v -9.485264778137207 -1.2831869125366211 0 +v -13.485264778137207 -1.2831869125366211 0 +v -15.485262632369995 -2.9999909400939941 0 +v 1.7891129367291081 -8.0016641994705857 0 +v 0.41188050328511627 -7.2342834392485305 0 +v -3.485264778137207 -1.2831869125366211 0 +v -3.097054100036619 3.2999826431274415 0 +v 4.514735221862793 0.71681308746337891 0 +v -1.0323523203531906 3.2999825795491535 0 +v 1.0323494593302414 3.299982515970866 0 +v 10.514735221862793 -7.2831869125366211 0 +v 10.10121488571167 -11.79826545715332 0 +v 11.586134910583496 -10.31334400177002 0 +v 10.514735221862793 -9.2831869125366211 0 +v 15.485264778137207 -2.9999947547912598 0 +v 14.174453735351562 -6.3215312957763672 0 +v 6.514735221862793 0.71681308746337891 0 +v 6.514735221862793 -1.2831869125366211 0 +v 8.514735221862793 -1.2831869125366211 0 +v 15.485262870788574 1.1999897956848145 0 +v 13.420560137430826 3.2999821345011395 0 +v 2.514735221862793 0.71681308746337891 0 +v 3.0970512390136715 3.299982452392578 0 +v -2.4416021842060882 -5.8945491070301372 0 +v -0.99936433579679851 -6.5314104698908366 0 +v -15.485281374238568 -5.0999999999999996 0 +v -9.485264778137207 -3.2831869125366211 0 +v -8.2061729431152344 -5.1000087261199951 0 +v -13.485264778137207 0.71681308746337891 0 +v -15.48526406288147 1.1999917030334473 0 +v -5.485264778137207 -1.2831869125366211 0 +v -3.485264778137207 -3.2831869125366211 0 +v 5.1617530186971035 3.2999823888142905 0 +v 4.429833158132169 -9.7232472619481545 0 +v 12.514735221862793 -7.2831869125366211 0 +v 14.555992052357226 -7.3435028842544341 0 +v 14.254188537597656 -6.8737411499023438 0 +v 13.071054935455322 -8.8284225463867188 0 +v 14.492236137390137 -5.5534791946411133 0 +v 8.514735221862793 0.71681308746337891 0 +v 10.514735221862793 0.71681308746337891 0 +v 11.355858357747396 3.2999821980794271 0 +v 7.2264547983805336 3.2999823252360025 0 +v 15.485281374238568 -5.0999999999999996 0 +v -4.3283510208129883 -5.1881341934204102 0 +v -15.485263347625732 -0.89999961853027344 0 +v -15.485281374238575 3.2999999999999861 0 +v 3.1293863386817709 -8.8319109187699016 0 +v 8.6162950903902136 -13.283199846221429 0 +v 5.6876710541143822 -10.673766190912788 0 +v 14.184475898742676 -6.6010494232177734 0 +v 9.2911565780639656 3.299982261657715 0 +v 15.485263824462891 -0.90000247955322266 0 +v 15.485281374238568 3.2999999999999998 0 +f 0 1 2 +f 3 4 5 +f 6 7 8 +f 1 0 9 +f 10 11 12 +f 2 1 5 +f 13 14 15 +f 16 17 18 +f 19 0 18 +f 20 5 4 +f 21 4 22 +f 23 24 25 +f 4 21 26 +f 27 28 29 +f 30 31 32 +f 33 17 34 +f 35 21 36 +f 2 37 11 +f 38 39 40 +f 41 42 43 +f 44 40 45 +f 7 15 46 +f 44 45 47 +f 48 49 50 +f 51 30 52 +f 28 27 53 +f 54 46 31 +f 14 46 15 +f 32 55 56 +f 46 8 7 +f 57 1 58 +f 17 33 59 +f 17 16 60 +f 6 33 34 +f 37 61 11 +f 62 12 63 +f 5 20 37 +f 35 36 64 +f 65 66 67 +f 48 68 49 +f 23 25 69 +f 61 37 70 +f 71 72 70 +f 39 73 74 +f 75 61 76 +f 55 32 31 +f 77 78 19 +f 32 52 30 +f 56 79 32 +f 79 52 32 +f 27 80 81 +f 8 46 29 +f 82 13 83 +f 14 31 46 +f 84 33 29 +f 62 60 16 +f 81 53 27 +f 21 22 43 +f 1 57 22 +f 0 19 9 +f 84 59 33 +f 71 20 72 +f 37 2 5 +f 59 19 18 +f 59 28 85 +f 75 11 61 +f 2 11 0 +f 76 61 86 +f 36 42 67 +f 87 43 22 +f 36 43 42 +f 72 45 40 +f 26 64 47 +f 88 89 90 +f 91 89 88 +f 49 68 38 +f 24 92 25 +f 45 26 47 +f 45 4 26 +f 40 93 72 +f 94 39 95 +f 70 86 61 +f 70 72 93 +f 86 70 96 +f 97 68 48 +f 95 39 74 +f 94 40 39 +f 85 28 98 +f 51 81 80 +f 99 56 55 +f 30 80 31 +f 30 51 80 +f 54 80 27 +f 54 31 80 +f 54 29 46 +f 54 27 29 +f 83 99 82 +f 55 31 82 +f 82 99 55 +f 100 83 13 +f 14 82 31 +f 14 13 82 +f 8 33 6 +f 8 29 33 +f 78 9 19 +f 53 98 28 +f 101 87 22 +f 3 22 4 +f 101 22 57 +f 77 85 98 +f 19 85 77 +f 19 59 85 +f 58 9 78 +f 58 1 9 +f 84 28 59 +f 84 29 28 +f 10 12 18 +f 59 18 17 +f 60 34 17 +f 16 12 62 +f 16 18 12 +f 10 0 11 +f 10 18 0 +f 75 12 11 +f 12 75 63 +f 76 63 75 +f 102 42 41 +f 103 41 43 +f 36 21 43 +f 103 43 87 +f 65 42 102 +f 3 1 22 +f 3 5 1 +f 35 26 21 +f 35 64 26 +f 91 88 67 +f 64 36 67 +f 65 67 42 +f 64 67 88 +f 67 66 91 +f 104 88 90 +f 64 88 47 +f 47 88 25 +f 88 104 69 +f 88 69 25 +f 92 50 25 +f 20 45 72 +f 20 4 45 +f 71 37 20 +f 71 70 37 +f 44 49 40 +f 44 47 49 +f 94 93 40 +f 93 94 105 +f 93 96 70 +f 96 93 105 +f 95 105 94 +f 49 25 50 +f 49 47 25 +f 106 38 68 +f 38 40 49 +f 106 39 38 +f 106 73 39 +f 107 74 73 +# asymmetric floor0 +v -7.0000000000000009 3.2999999999999985 0 +v -6.9999837875366211 1.1999924182891846 0 +v -4.6666571299235038 3.2999836603800454 0 +v -3.7229032516479492 -6.4698734283447266 0 +v -2.9999847412109375 -4.5999870300292969 0 +v -4.0247220993041992 -6.125584602355957 0 +v -4.7494916915893555 -5.5692501068115234 0 +v -5.1600246429443359 -5.3667087554931641 0 +v 1.0000152587890625 -0.59998703002929688 0 +v -0.9999847412109375 -0.59998703002929688 0 +v 1.0000152587890625 -2.5999870300292969 0 +v -0.9999847412109375 -4.5999870300292969 0 +v -0.9999847412109375 -6.5999870300292969 0 +v -2.9999847412109375 -2.5999870300292969 0 +v -0.9999847412109375 -2.5999870300292969 0 +v -2.9999847412109375 -0.59998703002929688 0 +v 3.0000152587890625 -0.59998703002929688 0 +v 2.3333257039388027 3.299982706705729 0 +v -6.9999828338623047 -0.89999914169311523 0 +v -4.9999847412109375 -2.5999870300292969 0 +v -4.9999847412109375 -0.59998703002929688 0 +v 5.5931997299194336 -5.2195758819580078 0 +v 6.9999816417694092 -2.9999904632568359 0 +v 3.0000152587890625 -2.5999870300292969 0 +v 7 -5.0999999999999996 0 +v 6.5006084442138672 -5.1000003814697266 0 +v 3.2666902542114258 -7.2600650787353516 0 +v 1.0000152587890625 -6.5999870300292969 0 +v 3.1191310882568359 -7.6951093673706055 0 +v -3.4686403274536133 -6.8505487442016602 0 +v 1.0000152587890625 -4.5999870300292969 0 +v -6.9999818801879883 -2.999990701675415 0 +v -5.5938682556152344 -5.2193737030029297 0 +v -3.2667093276977539 -7.2600250244140625 0 +v -6.5006523132324219 -5.1000003814697266 0 +v -2.3333295186360665 3.2999833424886069 0 +v 1.972919307900415e-15 -8.6000000000000014 0 +v 5.1611919403076172 -5.3662233352661133 0 +v 3.0000152587890625 -4.5999870300292969 0 +v 4.7505884170532227 -5.5686197280883789 0 +v 3.7236709594726562 -6.4688692092895508 0 +v 4.0256175994873047 -6.1246929168701172 0 +v 6.9999814033508301 -0.89999961853027344 0 +v 6.999981164932251 1.1999912261962891 0 +v 4.6666533152262373 3.2999823888142905 0 +v -4.369873046875 -5.8229026794433594 0 +v -7 -5.0999999999999996 0 +v -2.9999999999999982 -8.6000000000000014 0 +v -3.1192455291748047 -7.6944198608398438 0 +v -1.9073486328125e-06 3.299983024597168 0 +v 3.0000000000000018 -8.6000000000000014 0 +v 3.469273567199707 -6.8494548797607422 0 +v 4.3698368072509766 -5.8229284286499023 0 +v 7 3.2999999999999998 0 +f 0 1 2 +f 3 4 5 +f 6 4 7 +f 8 9 10 +f 11 4 12 +f 13 14 15 +f 16 17 8 +f 18 19 20 +f 21 22 23 +f 24 22 25 +f 26 27 28 +f 12 4 29 +f 13 15 19 +f 30 10 11 +f 25 22 21 +f 15 20 19 +f 31 32 19 +f 33 12 29 +f 31 34 32 +f 14 10 9 +f 15 14 9 +f 15 2 20 +f 2 15 35 +f 12 36 27 +f 37 38 39 +f 38 40 41 +f 21 23 37 +f 30 11 27 +f 23 10 38 +f 42 43 16 +f 43 44 16 +f 17 16 44 +f 4 6 45 +f 19 7 4 +f 19 32 7 +f 31 46 34 +f 12 47 36 +f 12 33 48 +f 12 48 47 +f 3 29 4 +f 11 14 4 +f 11 10 14 +f 1 20 2 +f 18 31 19 +f 13 4 14 +f 13 19 4 +f 8 49 9 +f 15 9 35 +f 1 18 20 +f 49 35 9 +f 36 50 27 +f 40 38 51 +f 12 27 11 +f 27 50 28 +f 27 51 38 +f 27 26 51 +f 52 38 41 +f 38 52 39 +f 38 37 23 +f 30 38 10 +f 30 27 38 +f 23 16 10 +f 16 23 42 +f 22 42 23 +f 49 8 17 +f 8 10 16 +f 53 44 43 +f 5 4 45 +# start_contact floor0 +v -4.5999889373779297 -4.5999870300292969 0 +v -5.5686197280883789 -6.8505878448486328 0 +v -2.5999889373779297 -6.5999870300292969 0 +v -6.5999889373779297 -2.5999870300292969 0 +v -4.5999889373779297 -2.5999870300292969 0 +v -6.5999889373779297 -0.59998703002929688 0 +v -2.5999889373779297 -0.59998703002929688 0 +v -2.5999889373779297 -2.5999870300292969 0 +v -0.59998893737792969 -2.5999870300292969 0 +v -0.59998893737792969 -4.5999870300292969 0 +v 1.4000110626220703 -4.5999870300292969 0 +v -2.5999889373779297 -4.5999870300292969 0 +v -6.4499926567077637 3.2999870777130127 0 +v -8.5999884605407715 1.1999936103820801 0 +v 1.4000110626220703 -6.5999870300292969 0 +v 3.7692737579345703 -6.8494548797607422 0 +v 3.4000110626220703 -4.5999870300292969 0 +v -5.2195758819580078 -7.6932001113891602 0 +v -2.9999902248382568 -8.5999820232391357 0 +v 5.4000110626220703 -0.59998703002929688 0 +v 3.4000110626220703 -2.5999870300292969 0 +v 5.4000110626220703 -2.5999870300292969 0 +v -8.5999875068664551 -2.9999947547912598 0 +v -8.5999999999999943 -5.0999999999999996 0 +v -7.6932001113891602 -5.2195758819580078 0 +v -5.3662233352661133 -7.2611923217773438 0 +v -4.2999963760375977 3.2999863624572754 0 +v -4.5999889373779297 -0.59998703002929688 0 +v 1.4000110626220703 -2.5999870300292969 0 +v 1.4000110626220703 -0.59998703002929688 0 +v 2.1499924659729004 3.2999842166900635 0 +v 3.4000110626220703 -0.59998703002929688 0 +v 4.2999887466430664 3.2999835014343262 0 +v -3.814697265625e-06 3.2999849319458008 0 +v 8.5999817848205566 -0.89999961853027344 0 +v 8.5999815464019775 1.1999912261962891 0 +v -6.8494548797607422 -5.5692739486694336 0 +v -6.4688692092895508 -5.8236713409423828 0 +v -5.0999999999999996 -8.5999999999999996 0 +v -0.59998893737792969 -6.5999870300292969 0 +v -0.89999723434448242 -8.5999836921691895 0 +v 3.5666904449462891 -7.2600650787353516 0 +v 5.0505876541137695 -5.5686197280883789 0 +v 4.6698369979858398 -5.8229284286499023 0 +v 4.0236711502075195 -6.4688692092895508 0 +v 8.5999820232391357 -2.9999904632568359 0 +v 6.4499850273132324 3.2999827861785889 0 +v 8.5999999999999943 3.2999999999999998 0 +v 6.8006076812744141 -5.1000003814697266 0 +v -5.8229284286499023 -6.4698371887207031 0 +v -6.1246929168701172 -6.1256179809570312 0 +v -8.5999999999999854 3.3000000000000043 0 +v -7.2600650787353516 -5.3666896820068359 0 +v 1.199995756149292 -8.5999853610992432 0 +v -8.5999879837036133 -0.90000057220458984 0 +v -2.1500000953674316 3.2999856472015381 0 +v 3.4195756912231445 -7.6932001113891602 0 +v 3.2999999999999998 -8.5999999999999996 0 +v 5.4611921310424805 -5.3662233352661133 0 +v 4.325617790222168 -6.1246929168701172 0 +v 5.8931999206542969 -5.2195758819580078 0 +v -0.59998893737792969 -0.59998703002929688 0 +v 8.5999999999999943 -5.0999999999999996 0 +f 0 1 2 +f 3 4 5 +f 6 7 8 +f 8 9 10 +f 11 0 2 +f 5 12 13 +f 14 15 16 +f 17 18 2 +f 19 20 21 +f 22 23 24 +f 25 2 1 +f 26 12 27 +f 28 29 8 +f 5 27 12 +f 30 31 32 +f 33 29 30 +f 34 35 19 +f 0 36 37 +f 38 18 17 +f 10 39 14 +f 0 3 36 +f 27 5 4 +f 4 3 0 +f 11 7 0 +f 29 31 30 +f 9 39 10 +f 40 2 18 +f 15 14 41 +f 42 16 43 +f 44 16 15 +f 34 21 45 +f 31 20 19 +f 28 8 10 +f 46 35 47 +f 48 45 21 +f 32 19 46 +f 49 0 50 +f 0 49 1 +f 51 13 12 +f 36 3 52 +f 3 24 52 +f 25 17 2 +f 9 2 39 +f 39 53 14 +f 40 39 2 +f 22 3 54 +f 5 54 3 +f 13 54 5 +f 22 24 3 +f 4 7 6 +f 4 0 7 +f 9 8 7 +f 31 29 20 +f 27 6 26 +f 27 4 6 +f 55 26 6 +f 14 53 56 +f 10 14 16 +f 40 53 39 +f 57 56 53 +f 58 21 16 +f 20 10 16 +f 16 44 59 +f 20 16 21 +f 16 42 58 +f 60 48 21 +f 61 6 8 +f 33 55 61 +f 60 21 58 +f 19 21 34 +f 6 61 55 +f 19 32 31 +f 62 45 48 +f 35 46 19 +f 0 37 50 +f 11 9 7 +f 11 2 9 +f 41 14 56 +f 43 16 59 +f 28 20 29 +f 28 10 20 +f 61 29 33 +f 61 8 29 +# graded floor0 +v -4.0229034423828125 -6.4698734283447266 2.977959643065184 +v -2.5999889373779297 -4.5999917984008789 2.9610751710224914 +v -4.3247222900390625 -6.125584602355957 2.9667144159584105 +v 1.4000110626220703 -0.59999179840087891 3.0437618130190058 +v -0.59998893737792969 -0.59999179840087891 2.9819755732888087 +v 1.4000110626220703 -2.5999917984008789 3.0388912564145567 +v 3.4000110626220703 -2.5999917984008789 3.0910987479347733 +v 3.4000110626220703 -0.59999179840087891 3.1044533393390705 +v 1.4000110626220703 -6.5999917984008789 3.0145040083414139 +v 2.9999930858612061 -8.5999882221221924 3 +v 3.4000110626220703 -6.5999917984008789 3.0305246143337237 +v -6.5999889373779297 -0.59999179840087891 2.7995428965713725 +v -6.4499926567077637 3.2999870777130127 2.7944454061779687 +v -8.5999870300292969 1.1999952793121338 2.742 +v 8.5999999999999943 -5.0999999999999996 3.2579999999999996 +v 8.5999813079833984 -2.9999911785125732 3.2579999999999996 +v 7.6932001113891602 -5.2195758819580078 3.2579999999999996 +v 5.4000110626220703 -2.5999917984008789 3.1522708100122649 +v 5.4000110626220703 -0.59999179840087891 3.1713316501117155 +v 4.2999887466430664 3.2999835014343262 3.1079999999999997 +v -4.5999889373779297 -2.5999917984008789 2.8873785080054142 +v -2.5999889373779297 -2.5999917984008789 2.9381589083146773 +v -4.5999889373779297 -0.59999179840087891 2.8633163315937078 +v -8.5999851226806641 -0.89999723434448242 2.742 +v -3.814697265625e-06 3.2999849319458008 2.9579999999999997 +v -2.1500000953674316 3.2999856472015381 2.9579999999999997 +v -4.5999889373779297 -4.5999917984008789 2.9264061401087291 +v 5.4000110626220703 -4.5999917984008789 3.1291353022036374 +v 7.2611923217773438 -5.3662233352661133 3.2579999999999996 +v -6.5999889373779297 -2.5999917984008789 2.8216326520045429 +v -5.4600248336791992 -5.3667087554931641 2.9216581815391356 +v -5.0494918823242188 -5.5692501068115234 2.9384098831684673 +v -3.7686395645141602 -6.8505487442016602 2.9870617746106012 +v -3.5667085647583008 -7.2600250244140625 2.9870617746106012 +v -2.5999889373779297 -6.5999917984008789 2.9851753029553958 +v -0.59998893737792969 -4.5999917984008789 2.9903954197297455 +v -0.59998893737792969 -6.5999917984008789 2.9981974779756104 +v -2.5999889373779297 -0.59999179840087891 2.9270894486067061 +v -0.59998893737792969 -2.5999917984008789 2.9873522762955624 +v -4.2999963760375977 3.2999863624572754 2.8180574021256146 +v -3.3000000000000025 -8.5999999999999961 3 +v -1.1999986171722412 -8.5999906063079834 3 +v -3.419245719909668 -7.6944198608398438 2.9938670085677428 +v 5.1000000000000014 -8.6000000000000014 3 +v 5.2195758819580078 -7.6932001113891602 3 +v 6.8505878448486328 -5.5686197280883789 3.1371490399632793 +v 6.4698371887207031 -5.8229284286499023 3.0966214075438785 +v 1.4000110626220703 -4.5999917984008789 3.0290406250557762 +v 3.4000110626220703 -4.5999917984008789 3.0699454975954557 +v 6.4499850273132324 3.2999827861785889 3.2089885093950983 +v 8.5999813079833984 1.19999098777771 3.2579999999999996 +v 8.5999999999999943 3.2999999999999998 3.2579999999999996 +v -5.8938684463500977 -5.2193737030029297 2.9006452420576498 +v -8.5999832153320312 -2.9999897480010986 2.742 +v -8.5999999999999943 -5.0999999999999996 2.742 +v -6.8006515502929688 -5.1000003814697266 2.8597713187642015 +v -4.6698732376098633 -5.8229026794433594 2.9534798953171988 +v -8.5999999999999943 3.2999999999999998 2.742 +v 2.1499924659729004 3.2999842166900635 3.1079999999999997 +v 0.89999723434448242 -8.5999894142150879 3 +v 5.3666896820068359 -7.2600650787353516 3 +v 6.1256179809570312 -6.1246929168701172 3.0783418184608644 +v 5.8236713409423828 -6.4688692092895508 3.0593256199201249 +v 5.5692739486694336 -6.8494548797607422 3.0315498569862904 +v 8.5999813079833984 -0.90000009536743164 3.2579999999999996 +f 0 1 2 +f 3 4 5 +f 5 6 7 +f 8 9 10 +f 11 12 13 +f 14 15 16 +f 7 6 17 +f 7 18 19 +f 20 21 22 +f 13 23 11 +f 24 25 4 +f 11 22 12 +f 1 26 2 +f 27 17 6 +f 4 3 24 +f 28 15 27 +f 27 15 17 +f 11 29 22 +f 30 31 26 +f 32 33 34 +f 1 0 34 +f 35 36 8 +f 37 38 4 +f 1 21 26 +f 39 12 22 +f 22 37 39 +f 40 41 42 +f 9 43 44 +f 45 27 46 +f 47 5 35 +f 48 8 10 +f 19 18 49 +f 7 17 18 +f 49 50 51 +f 26 29 52 +f 53 54 55 +f 31 56 26 +f 20 22 29 +f 30 26 52 +f 42 41 34 +f 33 42 34 +f 32 34 0 +f 21 1 35 +f 1 36 35 +f 53 29 23 +f 11 23 29 +f 53 55 29 +f 57 13 12 +f 21 37 22 +f 37 21 38 +f 35 38 21 +f 6 5 48 +f 25 39 37 +f 58 24 3 +f 37 4 25 +f 41 59 36 +f 9 8 59 +f 48 10 27 +f 9 44 10 +f 60 10 44 +f 61 27 62 +f 10 62 27 +f 10 60 63 +f 27 61 46 +f 64 17 15 +f 5 38 35 +f 38 5 4 +f 47 48 5 +f 48 27 6 +f 58 7 19 +f 64 18 17 +f 64 50 18 +f 16 15 28 +f 50 49 18 +f 55 52 29 +f 56 2 26 +f 36 34 41 +f 36 1 34 +f 20 26 21 +f 20 29 26 +f 36 59 8 +f 62 10 63 +f 27 45 28 +f 47 8 48 +f 47 35 8 +f 3 7 58 +f 3 5 7 +# multilane floor0 +v 1.6000175476074219 -10.899990081787109 0 +v -0.39998245239257812 -10.899990081787109 0 +v -0.39998245239257812 -12.899990081787109 0 +v 12.399981238625266 3.1090868169611152 0 +v 9.6000175476074219 5.1000099182128906 0 +v 9.6000175476074219 3.1000099182128906 0 +v 9.6000175476074219 -0.89999008178710938 0 +v 9.6000175476074219 1.1000099182128906 0 +v 7.6000175476074219 -0.89999008178710938 0 +v 1.6000175476074219 -12.899990081787109 0 +v 0.89999914169311523 -14.899988651275635 0 +v -4.3999824523925781 5.1000099182128906 0 +v -6.3999824523925781 5.1000099182128906 0 +v -4.3999824523925781 3.1000099182128906 0 +v -2.3999824523925781 3.1000099182128906 0 +v -2.3999824523925781 5.1000099182128906 0 +v -4.3999824523925781 -6.8999900817871094 0 +v -2.3999824523925781 -6.8999900817871094 0 +v -4.3999824523925781 -4.8999900817871094 0 +v -6.3999824523925781 9.1000099182128906 0 +v -8.3999824523925781 9.1000099182128906 0 +v -6.3999824523925781 7.1000099182128906 0 +v -0.39998245239257812 -6.8999900817871094 0 +v -0.39998245239257812 -4.8999900817871094 0 +v -12.400000000000006 -11.4 0 +v -10.399982452392578 -8.8999900817871094 0 +v -12.399980718439275 -9.3272585435347111 0 +v -8.3999824523925781 1.1000099182128906 0 +v -6.3999824523925781 -0.89999008178710938 0 +v -6.3999824523925781 1.1000099182128906 0 +v 7.6000175476074219 -2.8999900817871094 0 +v 9.6000175476074219 -2.8999900817871094 0 +v 5.6000175476074219 5.1000099182128906 0 +v 5.6000175476074219 7.1000099182128906 0 +v 3.6000175476074219 5.1000099182128906 0 +v -5.8938684463500977 -11.519373893737793 0 +v -4.3999824523925781 -10.899990081787109 0 +v -6.3999824523925781 -8.8999900817871094 0 +v -2.3999824523925781 -10.899990081787109 0 +v -4.3999824523925781 -8.8999900817871094 0 +v -6.3999824523925781 -2.8999900817871094 0 +v -8.3999824523925781 -2.8999900817871094 0 +v -8.3999824523925781 -4.8999900817871094 0 +v 1.6000175476074219 -8.8999900817871094 0 +v 9.6000175476074219 -4.8999900817871094 0 +v 7.6000175476074219 -4.8999900817871094 0 +v 9.6000175476074219 -6.8999900817871094 0 +v 1.6000175476074219 -6.8999900817871094 0 +v 3.6000175476074219 -6.8999900817871094 0 +v -10.399982452392578 -4.8999900817871094 0 +v -10.399982452392578 -6.8999900817871094 0 +v -2.3999824523925781 -2.8999900817871094 0 +v -2.3999824523925781 -4.8999900817871094 0 +v -0.39998245239257812 -2.8999900817871094 0 +v -12.399981412020596 -1.0363629081032482 0 +v -12.399981238625266 -3.1090868169611152 0 +v -10.399982452392578 -2.8999900817871094 0 +v 1.6000175476074219 3.1000099182128906 0 +v 3.6000175476074219 3.1000099182128906 0 +v 1.6000175476074219 5.1000099182128906 0 +v 5.6000175476074219 1.1000099182128906 0 +v 7.6000175476074219 3.1000099182128906 0 +v 5.6000175476074219 3.1000099182128906 0 +v 9.6000175476074219 7.1000099182128906 0 +v 12.399981065229936 5.1818107258189805 0 +v 2.9999940395355225 -14.899987936019897 0 +v 5.1000000000000014 -14.899999999999999 0 +v 5.2195758819580078 -13.993200302124023 0 +v -2.3999824523925781 -12.899990081787109 0 +v -12.399981585415928 1.0363610007546171 0 +v -10.399982452392578 -0.89999008178710938 0 +v -10.399982452392578 1.1000099182128906 0 +v -12.399980891834606 -7.2545346346768476 0 +v -12.399981065229936 -5.1818107258189805 0 +v -4.3999824523925781 -2.8999900817871094 0 +v -4.3999824523925781 -0.89999008178710938 0 +v -6.3999824523925781 -6.8999900817871094 0 +v -0.39998245239257812 -8.8999900817871094 0 +v 1.6000175476074219 1.1000099182128906 0 +v -0.39998245239257812 1.1000099182128906 0 +v 1.6000175476074219 -0.89999008178710938 0 +v 3.6000175476074219 -0.89999008178710938 0 +v 5.6000175476074219 -0.89999008178710938 0 +v 3.6000175476074219 1.1000099182128906 0 +v 7.6000175476074219 -6.8999900817871094 0 +v 5.6000175476074219 -6.8999900817871094 0 +v -8.3999824523925781 -0.89999008178710938 0 +v -6.3999824523925781 3.1000099182128906 0 +v -8.3999824523925781 3.1000099182128906 0 +v -6.1999917030334473 11.399981021881104 0 +v -4.3999824523925781 9.1000099182128906 0 +v -4.1333281199137346 11.399981180826822 0 +v -2.3999824523925781 1.1000099182128906 0 +v -0.39998245239257812 3.1000099182128906 0 +v -10.399982452392578 5.1000099182128906 0 +v -8.3999824523925781 5.1000099182128906 0 +v -10.399982452392578 7.1000099182128906 0 +v 7.6000175476074219 9.1000099182128906 0 +v 5.6000175476074219 9.1000099182128906 0 +v 3.6000175476074219 9.1000099182128906 0 +v 4.1333262125651054 11.399981816609701 0 +v -0.39998245239257812 5.1000099182128906 0 +v 7.6000175476074219 5.1000099182128906 0 +v -6.8006515502929688 -11.399999618530273 0 +v -4.6698732376098633 -12.122902870178223 0 +v -4.3247222900390625 -12.42558479309082 0 +v -8.3999824523925781 -6.8999900817871094 0 +v -9.600316047668457 -11.399991035461426 0 +v -2.3999824523925781 -8.8999900817871094 0 +v -6.3999824523925781 -4.8999900817871094 0 +v 3.6000175476074219 -8.8999900817871094 0 +v 3.6000175476074219 -10.899990081787109 0 +v 6.8505878448486328 -11.868619918823242 0 +v 5.6000175476074219 -10.899990081787109 0 +v 6.4698371887207031 -12.122927665710449 0 +v 3.6000175476074219 -12.899990081787109 0 +v 5.8236713409423828 -12.768869400024414 0 +v 7.6000175476074219 -8.8999900817871094 0 +v 8.6006078720092773 -11.399999618530273 0 +v 9.6000175476074219 -8.8999900817871094 0 +v 3.6000175476074219 -4.8999900817871094 0 +v 1.6000175476074219 -4.8999900817871094 0 +v 1.6000175476074219 -2.8999900817871094 0 +v 5.6000175476074219 -8.8999900817871094 0 +v 5.6000175476074219 -4.8999900817871094 0 +v 12.399981932206588 -5.1818088184703486 0 +v 12.399982105601918 -7.2545327273282139 0 +v 12.399981758811258 -3.1090849096124824 0 +v -12.399982278997248 9.32725663618608 0 +v -12.399982105601918 7.2545327273282139 0 +v -10.399982452392578 9.1000099182128906 0 +v -4.3999824523925781 1.1000099182128906 0 +v -2.3999824523925781 -0.89999008178710938 0 +v -0.39998245239257812 -0.89999008178710938 0 +v -10.399982452392578 3.1000099182128906 0 +v -8.3999824523925781 7.1000099182128906 0 +v -2.3999824523925781 9.1000099182128906 0 +v -4.3999824523925781 7.1000099182128906 0 +v 1.6000175476074219 7.1000099182128906 0 +v -0.39998245239257812 7.1000099182128906 0 +v 2.0666626294453927 11.399981657663981 0 +v 3.6000175476074219 -2.8999900817871094 0 +v 5.6000175476074219 -2.8999900817871094 0 +v 1.6000175476074219 9.1000099182128906 0 +v -0.39998245239257812 9.1000099182128906 0 +v -9.5367431640625e-07 11.399981498718262 0 +v 3.6000175476074219 7.1000099182128906 0 +v 12.399981585415928 -1.0363610007546171 0 +v 12.399981412020596 1.0363629081032482 0 +v 10.333316961924236 11.399982293446859 0 +v 9.6000175476074219 9.1000099182128906 0 +v 12.399980718439275 9.3272585435347111 0 +v 6.1999897956848145 11.39998197555542 0 +v 8.2666533788045253 11.399982134501139 0 +v -8.3999824523925781 -8.8999900817871094 0 +v -5.4600248336791992 -11.666708946228027 0 +v -4.0229034423828125 -12.769872665405273 0 +v -3.5667085647583008 -13.560025215148926 0 +v -3.7686395645141602 -13.150548934936523 0 +v -1.199995756149292 -14.899989366531372 0 +v 6.1256179809570312 -12.42469310760498 0 +v 7.6951093673706055 -11.519130706787109 0 +v 7.2611923217773438 -11.66622257232666 0 +v 12.399982278997248 -9.32725663618608 0 +v 12.400000000000006 -11.4 0 +v -12.399981758811258 3.1090849096124824 0 +v -2.3999824523925781 7.1000099182128906 0 +v -12.399981932206588 5.1818088184703486 0 +v -8.2666552861531599 11.399980862935385 0 +v -10.333318869272865 11.399980703989664 0 +v -2.0666645367940273 11.399981339772543 0 +v 7.6000175476074219 7.1000099182128906 0 +v 7.6000175476074219 1.1000099182128906 0 +v 12.400000000000006 11.4 0 +v -5.0494918823242188 -11.869250297546387 0 +v -3.419245719909668 -13.994420051574707 0 +v -3.2999999999999918 -14.900000000000004 0 +v 5.5692739486694336 -13.149455070495605 0 +v 5.3666896820068359 -13.560065269470215 0 +v -12.400000000000006 11.4 0 +v 12.399980891834606 7.2545346346768476 0 +f 0 1 2 +f 3 4 5 +f 6 7 8 +f 9 2 10 +f 11 12 13 +f 14 15 13 +f 16 17 18 +f 19 20 21 +f 22 23 17 +f 24 25 26 +f 27 28 29 +f 30 31 8 +f 32 33 34 +f 35 36 37 +f 38 39 36 +f 40 41 42 +f 0 43 1 +f 44 45 46 +f 47 43 48 +f 49 50 42 +f 51 52 53 +f 54 55 56 +f 57 58 59 +f 60 61 62 +f 63 4 64 +f 65 66 67 +f 2 38 68 +f 69 70 71 +f 72 50 73 +f 28 74 75 +f 16 18 76 +f 47 77 43 +f 78 79 80 +f 81 82 83 +f 84 45 85 +f 86 70 41 +f 87 88 29 +f 89 90 91 +f 14 92 93 +f 94 95 96 +f 97 98 33 +f 99 98 100 +f 34 59 58 +f 57 83 58 +f 101 93 59 +f 32 102 33 +f 4 63 102 +f 35 37 103 +f 39 37 36 +f 1 38 2 +f 104 105 36 +f 106 42 50 +f 107 25 24 +f 27 88 71 +f 52 17 23 +f 108 77 17 +f 109 40 42 +f 23 53 52 +f 110 43 111 +f 112 113 114 +f 113 115 116 +f 117 118 119 +f 120 121 48 +f 122 53 121 +f 123 85 110 +f 124 120 85 +f 125 46 126 +f 31 44 127 +f 86 27 70 +f 128 129 130 +f 131 75 92 +f 132 133 92 +f 14 13 92 +f 87 12 88 +f 88 95 134 +f 130 96 135 +f 91 90 136 +f 21 137 90 +f 138 139 59 +f 140 99 100 +f 141 120 142 +f 120 141 121 +f 82 8 60 +f 7 3 5 +f 138 143 139 +f 144 143 145 +f 98 99 146 +f 99 140 143 +f 147 148 6 +f 61 4 102 +f 3 64 4 +f 149 150 151 +f 152 97 153 +f 154 25 107 +f 154 50 25 +f 155 36 35 +f 108 17 39 +f 76 106 37 +f 68 38 156 +f 9 0 2 +f 157 68 158 +f 2 68 159 +f 1 77 38 +f 1 43 77 +f 76 42 106 +f 56 41 70 +f 54 56 70 +f 42 41 49 +f 40 28 41 +f 16 39 17 +f 16 37 39 +f 121 23 47 +f 53 23 121 +f 40 74 28 +f 74 40 18 +f 51 53 133 +f 133 53 80 +f 159 10 2 +f 0 111 43 +f 111 0 115 +f 111 115 113 +f 160 113 116 +f 161 117 162 +f 110 111 113 +f 163 119 164 +f 118 164 119 +f 22 47 23 +f 47 48 121 +f 81 83 80 +f 60 62 83 +f 48 85 120 +f 123 117 85 +f 124 142 120 +f 30 8 142 +f 44 46 125 +f 84 46 45 +f 119 126 46 +f 44 31 45 +f 31 147 6 +f 125 127 44 +f 86 28 27 +f 86 41 28 +f 94 165 134 +f 95 88 12 +f 131 92 13 +f 27 29 88 +f 14 93 15 +f 79 93 92 +f 21 12 137 +f 95 12 135 +f 166 136 137 +f 101 59 139 +f 94 96 167 +f 130 135 20 +f 95 135 96 +f 130 129 96 +f 89 168 19 +f 130 20 169 +f 11 137 12 +f 90 137 136 +f 20 19 168 +f 166 139 136 +f 99 143 146 +f 144 136 139 +f 91 136 170 +f 93 79 57 +f 122 80 53 +f 78 80 83 +f 57 59 93 +f 142 82 141 +f 142 8 82 +f 62 58 83 +f 34 58 62 +f 143 144 139 +f 144 170 136 +f 97 152 98 +f 171 63 97 +f 146 33 98 +f 97 150 153 +f 5 61 7 +f 172 7 61 +f 171 97 33 +f 61 102 62 +f 63 150 97 +f 173 149 151 +f 72 26 25 +f 154 103 37 +f 154 107 103 +f 174 104 36 +f 174 36 155 +f 38 36 105 +f 68 175 159 +f 68 157 175 +f 105 156 38 +f 159 175 176 +f 158 68 156 +f 108 38 77 +f 108 39 38 +f 72 25 50 +f 49 73 50 +f 55 73 49 +f 106 154 37 +f 106 50 154 +f 56 49 41 +f 56 55 49 +f 109 42 76 +f 76 37 16 +f 52 18 17 +f 52 74 18 +f 109 18 40 +f 109 76 18 +f 51 74 52 +f 74 51 75 +f 10 65 9 +f 65 67 115 +f 9 115 0 +f 115 9 65 +f 177 115 178 +f 115 67 178 +f 177 116 115 +f 113 160 114 +f 113 162 117 +f 113 112 162 +f 161 118 117 +f 163 126 119 +f 46 117 119 +f 22 77 47 +f 22 17 77 +f 110 48 43 +f 48 110 85 +f 122 141 80 +f 122 121 141 +f 123 113 117 +f 123 110 113 +f 124 45 142 +f 124 85 45 +f 84 117 46 +f 84 85 117 +f 30 45 31 +f 30 142 45 +f 69 54 70 +f 165 69 71 +f 27 71 70 +f 165 71 134 +f 71 88 134 +f 167 165 94 +f 132 92 75 +f 28 75 29 +f 131 29 75 +f 131 13 29 +f 132 51 133 +f 132 75 51 +f 87 13 12 +f 87 29 13 +f 11 15 137 +f 11 13 15 +f 96 129 167 +f 94 134 95 +f 179 128 169 +f 168 169 20 +f 130 169 128 +f 21 135 12 +f 135 21 20 +f 19 90 89 +f 19 21 90 +f 166 15 139 +f 166 137 15 +f 79 133 80 +f 79 92 133 +f 78 57 79 +f 78 83 57 +f 101 15 93 +f 101 139 15 +f 81 141 82 +f 81 80 141 +f 172 61 60 +f 60 83 82 +f 138 146 143 +f 138 59 146 +f 34 146 59 +f 146 34 33 +f 170 144 145 +f 140 145 143 +f 32 62 102 +f 32 34 62 +f 152 100 98 +f 172 8 7 +f 172 60 8 +f 6 8 31 +f 6 148 7 +f 127 147 31 +f 5 4 61 +f 148 3 7 +f 171 102 63 +f 171 33 102 +f 180 150 63 +f 63 64 180 +f 149 153 150 +f 180 151 150 diff --git a/core/tests/test_t_junction_quality.cpp b/core/tests/test_t_junction_quality.cpp index 1d7e5c91..d13f1fea 100644 --- a/core/tests/test_t_junction_quality.cpp +++ b/core/tests/test_t_junction_quality.cpp @@ -41,6 +41,7 @@ #include "roadmaker/edit/operations.hpp" #include "roadmaker/geometry/poly3.hpp" #include "roadmaker/io/gltf_exporter.hpp" +#include "roadmaker/mesh/junction_corners.hpp" #include "roadmaker/mesh/junction_surface_spans.hpp" #include "roadmaker/mesh/mesh_builder.hpp" #include "roadmaker/road/authoring.hpp" @@ -68,6 +69,7 @@ #include #include #include +#include #include #include #include @@ -1009,54 +1011,228 @@ TEST_P(TJunctionQuality, SurfaceNormalsAreSmoothAndWelded) { } } -// Standing reproduction of issue #356 — a curved approach meeting a junction of -// sidewalked roads, attached on the CONCAVE side. DISABLED because it fails: -// it is the bug, not a regression guard, and it is enabled by whoever fixes it. +// --- Issue #356: a road curving THROUGH a junction --------------------------- // -// Run it with (no line continuation — a trailing backslash would make this a -// multi-line comment, which GCC rejects under -Werror=comment): -// roadmaker_core_tests --gtest_also_run_disabled_tests -// --gtest_filter='*DISABLED_CurvedInsideApproachWithSidewalks*' +// A road split by a junction presents its two halves as two arms whose facing +// edges are ONE continuous pavement edge. Extrapolating each as an infinite +// straight ray from its single end station makes them meet at an apex metres +// outside the pavement, and filleting that apex paved a spike belonging to no +// road. Straight arms are the case that always worked, because two collinear +// edges never meet (`parallel_edges`); the curved case is its exact analogue +// and was the bug. // -// Measured on 7ac3c75, against the same curve WITHOUT sidewalks in parentheses: +// The failure was measured on `9e9bce2` for r = 30 m attached on the concave +// side with sidewalks, against the same curve WITHOUT sidewalks in parentheses: // // worst triangle angle 0.51 deg (6.62 deg) // sliver triangles 4 (0), budget 2 // max concave turn 88.2 deg (passes), gate < 20 deg // -// The 88 degree re-entrant corner is the visible artifact. `corner_faces` -// samples each arm at ONE end station and treats its outer edge as an infinite -// straight ray (junction_corner_detail.cpp:141-197); `solve_corner` intersects -// those rays (:250-265). On a curved arm the true pavement edge peels away from -// its own tangent, so the rays meet at a fabricated apex metres outside the -// pavement and `append_corner_fillets` (junction_surface.cpp:135-209) paves a -// wedge all the way out to it. Both curved cases show that wedge in the -// viewport; adding the sidewalk band wraps it and turns the far end into a -// needle sharp enough to trip the gates. +// Note what the no-sidewalk column says: the spike was there too and every gate +// passed it, because a spike is a CONVEX corner and the fillet gate exempts +// those. The sidewalk band wrapped the spike, pulled its far end into a needle +// and turned the artifact concave, which is the only reason the matrix could +// see it at all. That is why the gates below are geometric — they pin the +// pavement to the curve rather than waiting for a sliver to appear. + +struct ThroughEdgeCase { + std::string_view name; + double radius; + LaneProfile profile; +}; + +class TJunctionCurvedThrough : public ::testing::TestWithParam {}; + +// The junction floor must stay on the pavement the curve actually describes. +// `setup_curved` centres the target's arc at (0, radius) with the branch on the +// concave side, so the floor's southernmost point IS the through-edge at its +// extreme: it must land on the outer pavement edge, at y = -half_width. +// +// This is the assertion that fails hardest on the old solver. The straight rays +// met at a fabricated apex 7.18 m beyond the true edge, and the fillet it +// carried dipped to y = -8.88 against a true -4.80. The bound below is -4.85. // -// Two things the 2026-07-22 investigation comment on #356 says are NOT true -// here, both re-checked while writing this: -// - It does reproduce in the headless harness. That pass looked at -// watertightness and floor-vs-arm plan overlap, which still hold -// (boundary_crossings = 0). The fillet and sliver gates are what expose it. -// - `FloorCarriesDrivingMaterial` does NOT need relaxing for a sidewalked -// profile. `build_one_junction_floor` keeps the bands in JunctionFloor -// ::details and leaves .mesh as the carriageway, so it passes unchanged -// (mesh_builder.cpp:1039-1046). That comment predates #402 / PR #441. -TEST(TJunctionQualityRepro, DISABLED_CurvedInsideApproachWithSidewalksFailsTheFilletGate) { - Tee tee = attach(setup_curved(30.0, true, LaneProfile::urban_sidewalk())); +// The second bound is the opposite failure: a corridor that under-covers would +// leave the floor short of the pavement and open a gap at the mouth. +TEST_P(TJunctionCurvedThrough, FloorStaysOnTheTruePavementEdge) { + const ThroughEdgeCase& through = GetParam(); + Tee tee = attach(setup_curved(through.radius, true, through.profile)); const NetworkMesh mesh = roadmaker::build_network_mesh(tee.network); ASSERT_FALSE(mesh.junction_floors.empty()); - const QualityMetrics metrics = compute_metrics(tee, mesh); - EXPECT_LE(metrics.slivers, kSliverBudget) << format_metrics(metrics); - EXPECT_GE(metrics.min_angle_deg, kMinTriangleAngleDeg) << format_metrics(metrics); + double half_width = 0.0; + for (const LaneSpec& lane : through.profile.right) { + half_width += lane.width; + } + ASSERT_GT(half_width, 0.0); + + double southernmost = std::numeric_limits::max(); + for (const roadmaker::JunctionFloor& floor : mesh.junction_floors) { + for (std::size_t i = 0; i + 2 < floor.mesh.positions.size(); i += 3) { + southernmost = std::min(southernmost, floor.mesh.positions[i + 1]); + } + } - const std::vector> loop = - ccw_boundary_loop(mesh.junction_floors.front().mesh); - ASSERT_GE(loop.size(), 8U); - EXPECT_LT(std::ranges::max(concave_turns_deg(loop)), 20.0) - << "re-entrant corner without a fillet arc"; + EXPECT_GE(southernmost, -half_width - 0.05) + << "junction floor paves " << (-half_width - southernmost) + << " m beyond the pavement edge the curve describes — fabricated corner apex"; + EXPECT_LE(southernmost, -half_width + 0.25) + << "junction floor stops short of the pavement edge — gap at the mouth"; +} + +// The corner that is not there: a curved road running through the junction must +// report no corner between its own two halves, exactly as a straight one does. +// `junction_corners()` already drops the pairs it cannot fillet, so the Corner +// tool needs no change — but it MUST agree with the floor, or the tool would +// offer a handle sitting on pavement that no longer exists (issue #225). +TEST_P(TJunctionCurvedThrough, NoCornerIsReportedForTheRoadRunningThrough) { + const ThroughEdgeCase& through = GetParam(); + // Both orientations: `inside` puts the branch on the concave side of the + // curve, so the through-edge is the convex one, and `outside` is its mirror. + // The detection has to hold for both — the two differ in the sign of the + // edge curvature, which is exactly what the solve keys on. + for (const bool inside : {true, false}) { + Tee tee = attach(setup_curved(through.radius, inside, through.profile)); + const std::vector corners = + roadmaker::junction_corners(tee.network, tee.junction); + const roadmaker::Junction* junction = tee.network.junction(tee.junction); + ASSERT_NE(junction, nullptr); + ASSERT_EQ(junction->arms.size(), 3U); + + // A tee has three arm pairs; the two halves of the split target are the + // pair that is NOT the branch, so exactly two corners survive. + EXPECT_EQ(corners.size(), 2U) + << "inside=" << inside + << ": the road running through the junction still reports a corner " + "between its own two halves"; + for (const roadmaker::JunctionCornerInfo& corner : corners) { + EXPECT_NE(corner.arm_a.road, corner.arm_b.road) + << "inside=" << inside << ": a corner was reported between two ends of the same road"; + } + } +} + +INSTANTIATE_TEST_SUITE_P( + Through, + TJunctionCurvedThrough, + ::testing::Values(ThroughEdgeCase{"r30_sidewalk", 30.0, LaneProfile::urban_sidewalk()}, + ThroughEdgeCase{"r30_rural", 30.0, LaneProfile::two_lane_rural()}, + ThroughEdgeCase{"r100_rural", 100.0, LaneProfile::two_lane_rural()}), + [](const ::testing::TestParamInfo& through) { + return std::string(through.param.name); + }); + +// --- Straight-approach identity (#356 acceptance) ---------------------------- +// +// The through-edge corridor may only ever fire on a CURVED arm: a straight one +// has zero edge curvature, so the corner solve stays exactly the line-line one +// it has always been and every straight-approach junction must mesh unchanged. +// The golden below was generated on `9e9bce2` — BEFORE the fix — and the fix +// reproduces all seven cases exactly. +// +// Positions compare to 1e-6 m rather than bitwise: this is ONE committed file +// checked on three platforms, and clothoid/Clipper2/CDT arithmetic is not +// required to agree to the last ULP across them. A micron is three orders of +// magnitude below kBoundarySimplify, so nothing a corner change can do hides +// under it. Triangle indices compare exactly, so any change in triangulation +// fails loudly. + +/// The fixtures whose arms are all straight — the ones the corner solve must +/// treat identically forever. +const std::vector& straight_cases() { + static const std::vector cases{ + TeeCase{"perp", setup_perp}, + TeeCase{"deg45", setup_deg45}, + TeeCase{"deg135", setup_deg135}, + TeeCase{"asymmetric", setup_asymmetric}, + TeeCase{"start_contact", setup_start_contact}, + TeeCase{"graded", setup_graded}, + TeeCase{"multilane", setup_multilane}, + }; + return cases; +} + +std::filesystem::path straight_golden_path() { + return std::filesystem::path(RM_CORE_TESTS_DIR) / "data" / "junction" / + "straight_floor_golden.txt"; +} + +/// Serialises every straight fixture's junction floors, one line per vertex and +/// per triangle. `{:.17g}` round-trips a double exactly, so the file records +/// what was generated rather than a rounding of it. +std::string straight_floor_dump() { + std::string out; + for (const TeeCase& straight : straight_cases()) { + Tee tee = attach(straight.setup()); + const NetworkMesh mesh = roadmaker::build_network_mesh(tee.network); + for (std::size_t f = 0; f < mesh.junction_floors.size(); ++f) { + const SubMesh& floor = mesh.junction_floors[f].mesh; + fmt::format_to(std::back_inserter(out), "# {} floor{}\n", straight.name, f); + for (std::size_t i = 0; i + 2 < floor.positions.size(); i += 3) { + fmt::format_to(std::back_inserter(out), + "v {:.17g} {:.17g} {:.17g}\n", + floor.positions[i], + floor.positions[i + 1], + floor.positions[i + 2]); + } + for (std::size_t i = 0; i + 2 < floor.indices.size(); i += 3) { + fmt::format_to(std::back_inserter(out), + "f {} {} {}\n", + floor.indices[i], + floor.indices[i + 1], + floor.indices[i + 2]); + } + } + } + return out; +} + +TEST(TJunctionStraightIdentity, StraightApproachFloorsMatchTheGolden) { + std::ifstream golden(straight_golden_path(), std::ios::binary); + ASSERT_TRUE(golden.is_open()) << "missing golden " << straight_golden_path(); + std::vector expected; + for (std::string line; std::getline(golden, line);) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); // the file is pinned eol=lf, but never trust a checkout + } + expected.push_back(line); + } + + std::vector actual; + { + std::istringstream stream(straight_floor_dump()); + for (std::string line; std::getline(stream, line);) { + actual.push_back(line); + } + } + ASSERT_EQ(actual.size(), expected.size()) << "straight-approach floor topology changed"; + + for (std::size_t i = 0; i < expected.size(); ++i) { + if (expected[i].starts_with("v ")) { + double ex = 0.0, ey = 0.0, ez = 0.0, ax = 0.0, ay = 0.0, az = 0.0; + std::istringstream(expected[i].substr(2)) >> ex >> ey >> ez; + ASSERT_TRUE(actual[i].starts_with("v ")) << "line " << i; + std::istringstream(actual[i].substr(2)) >> ax >> ay >> az; + EXPECT_NEAR(ax, ex, 1e-6) << "line " << i; + EXPECT_NEAR(ay, ey, 1e-6) << "line " << i; + EXPECT_NEAR(az, ez, 1e-6) << "line " << i; + } else { + // '# case' headers and 'f a b c' triangles must match to the character. + EXPECT_EQ(actual[i], expected[i]) << "line " << i; + } + } +} + +// Regenerates the straight-approach golden. Run manually, and only with a +// reviewed reason — a diff here means straight junctions moved: +// roadmaker_core_tests --gtest_also_run_disabled_tests +// --gtest_filter='*DISABLED_WriteStraightFloorGolden' +TEST(TJunctionQualityTools, DISABLED_WriteStraightFloorGolden) { + const std::string dump = straight_floor_dump(); + std::ofstream out(straight_golden_path(), std::ios::binary); + ASSERT_TRUE(out.is_open()); + out.write(dump.data(), static_cast(dump.size())); + ASSERT_TRUE(out.good()); } // Regenerates the committed tee golden (assets/samples + fuzz corpus per the @@ -1077,32 +1253,27 @@ TEST(TJunctionQualityTools, DISABLED_WriteTeeSample) { INSTANTIATE_TEST_SUITE_P( Matrix, TJunctionQuality, - ::testing::Values(TeeCase{"perp", setup_perp}, - TeeCase{"deg45", setup_deg45}, - TeeCase{"deg135", setup_deg135}, - TeeCase{"r100_outside", [] { return setup_curved(100.0, false); }}, - TeeCase{"r100_inside", [] { return setup_curved(100.0, true); }}, - TeeCase{"r30_outside", [] { return setup_curved(30.0, false); }}, - TeeCase{"r30_inside", [] { return setup_curved(30.0, true); }}, - // Issue #356: a curved approach meeting a junction of - // SIDEWALKED roads — the scenario the field report shows - // and the one thing this matrix never covered. Only the - // convex side is in the matrix; the concave side does not - // pass today, and is the DISABLED reproduction below. - // - // NOTE this case clears the sliver budget EXACTLY (2 of 2) - // with a 1.54 deg worst triangle, against 6.62 deg for the - // same curve without sidewalks. It is deliberately left - // that tight: it is the early-warning sibling of the - // reproduction, and anything that costs it one more sliver - // should fail loudly rather than drift. - TeeCase{ - "r30_sidewalk_outside", - [] { return setup_curved(30.0, false, LaneProfile::urban_sidewalk()); }}, - TeeCase{"asymmetric", setup_asymmetric}, - TeeCase{"start_contact", setup_start_contact}, - TeeCase{"graded", setup_graded}, - TeeCase{"multilane", setup_multilane}), + ::testing::Values( + TeeCase{"perp", setup_perp}, + TeeCase{"deg45", setup_deg45}, + TeeCase{"deg135", setup_deg135}, + TeeCase{"r100_outside", [] { return setup_curved(100.0, false); }}, + TeeCase{"r100_inside", [] { return setup_curved(100.0, true); }}, + TeeCase{"r30_outside", [] { return setup_curved(30.0, false); }}, + TeeCase{"r30_inside", [] { return setup_curved(30.0, true); }}, + // Issue #356: a curved approach meeting a junction of + // SIDEWALKED roads — the scenario the field report shows + // and the one thing this matrix never covered. BOTH sides + // run now; the concave one was the reproduction until the + // through-edge corridor replaced the fabricated apex. + TeeCase{"r30_sidewalk_outside", + [] { return setup_curved(30.0, false, LaneProfile::urban_sidewalk()); }}, + TeeCase{"r30_sidewalk_inside", + [] { return setup_curved(30.0, true, LaneProfile::urban_sidewalk()); }}, + TeeCase{"asymmetric", setup_asymmetric}, + TeeCase{"start_contact", setup_start_contact}, + TeeCase{"graded", setup_graded}, + TeeCase{"multilane", setup_multilane}), // `tee_case`, not `info` — GCC -Wshadow flags the gtest macro's internal // parameter of the same name. [](const ::testing::TestParamInfo& tee_case) { From 8bdeb93ae38190744d491a3a2146dd3747197a70 Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya Date: Sun, 2 Aug 2026 04:42:10 -0700 Subject: [PATCH 2/3] test(mesh): compare the straight-approach golden canonically, not line by line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate failed on macOS arm64 while passing on x86_64 Linux, Windows and macOS — with the SAME vertices in a different sequence. Mesh vertex order is not stable across architectures and never was a kernel guarantee: the determinism tests pin it only within a single run. A golden that compares the dump line by line is therefore asserting something the mesher does not promise. Resolve each index triple to coordinates, sort the corners within a triangle and the triangles within a case, and compare that. Geometry and connectivity — what actually must not move — stay pinned; vertex numbering no longer is. The committed golden is unchanged, since it already encodes the same triangles. Re-verified by sabotage: kFootprintWeld 0.01 -> 0.012 still fails the gate. --- CHANGELOG.md | 7 +- core/tests/test_t_junction_quality.cpp | 102 ++++++++++++++++++------- 2 files changed, 82 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 519c47d6..74de273d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1391,7 +1391,12 @@ Current version on `main`: **0.0.1**. A straight arm has zero edge curvature, so the solve stays exactly the line-line one it has always been: all seven straight-approach fixtures mesh - **bit-identically**, pinned from here on by a committed golden. `#356`'s + **bit-identically**, measured by a before/after differential of every fixture. + The committed golden that guards it from here on compares geometry and + connectivity *canonically* rather than line by line — mesh vertex ORDER turns + out to differ between x86_64 and arm64, and it has never been a kernel + guarantee (the determinism tests pin it only within a single run), so a golden + asserting it would be asserting something the mesher does not promise. `#356`'s reproduction is no longer disabled — it is a matrix case, and three new geometric gates assert the floor stays on the pavement the curve describes and that no corner is reported between a road and itself. diff --git a/core/tests/test_t_junction_quality.cpp b/core/tests/test_t_junction_quality.cpp index d13f1fea..cbd32528 100644 --- a/core/tests/test_t_junction_quality.cpp +++ b/core/tests/test_t_junction_quality.cpp @@ -1187,38 +1187,88 @@ std::string straight_floor_dump() { return out; } -TEST(TJunctionStraightIdentity, StraightApproachFloorsMatchTheGolden) { - std::ifstream golden(straight_golden_path(), std::ios::binary); - ASSERT_TRUE(golden.is_open()) << "missing golden " << straight_golden_path(); - std::vector expected; - for (std::string line; std::getline(golden, line);) { +/// One triangle as nine coordinates, its three corners sorted so the same +/// triangle canonicalises identically however it was wound or indexed. +using CanonTriangle = std::array; + +/// Parses a dump into `case name -> canonical triangle list`, resolving each +/// index triple to coordinates and sorting so the result is invariant to +/// vertex numbering and triangle order. +/// +/// It has to be: the mesh's vertex ORDER is not stable across architectures. +/// The first version of this gate compared the dump line by line and failed on +/// macOS arm64 while passing on x86_64 Linux, Windows and macOS — with the +/// same vertices in a different sequence. Vertex order has never been a kernel +/// guarantee (the determinism tests only pin it within one run), so a golden +/// that pins it is asserting something the mesher does not promise. Geometry +/// and connectivity are what must not move, and both are pinned here. +std::map> parse_floor_dump(const std::string& text) { + std::map> out; + std::istringstream stream(text); + std::string key; + std::vector> verts; + for (std::string line; std::getline(stream, line);) { if (!line.empty() && line.back() == '\r') { line.pop_back(); // the file is pinned eol=lf, but never trust a checkout } - expected.push_back(line); + if (line.starts_with("# ")) { + key = line.substr(2); + verts.clear(); + } else if (line.starts_with("v ")) { + std::array p{}; + std::istringstream(line.substr(2)) >> p[0] >> p[1] >> p[2]; + verts.push_back(p); + } else if (line.starts_with("f ")) { + std::array idx{}; + std::istringstream(line.substr(2)) >> idx[0] >> idx[1] >> idx[2]; + if (idx[0] >= verts.size() || idx[1] >= verts.size() || idx[2] >= verts.size()) { + continue; + } + std::array, 3> corners{verts[idx[0]], verts[idx[1]], verts[idx[2]]}; + std::ranges::sort(corners); + CanonTriangle tri{}; + for (std::size_t c = 0; c < 3; ++c) { + for (std::size_t k = 0; k < 3; ++k) { + tri[(c * 3) + k] = corners[c][k]; + } + } + out[key].push_back(tri); + } } + for (auto& [name, tris] : out) { + std::ranges::sort(tris); + } + return out; +} - std::vector actual; - { - std::istringstream stream(straight_floor_dump()); - for (std::string line; std::getline(stream, line);) { - actual.push_back(line); - } +TEST(TJunctionStraightIdentity, StraightApproachFloorsMatchTheGolden) { + std::ifstream golden(straight_golden_path(), std::ios::binary); + ASSERT_TRUE(golden.is_open()) << "missing golden " << straight_golden_path(); + const std::string golden_text{std::istreambuf_iterator(golden), + std::istreambuf_iterator()}; + + const std::map> expected = parse_floor_dump(golden_text); + const std::map> actual = + parse_floor_dump(straight_floor_dump()); + ASSERT_FALSE(expected.empty()); + + std::vector expected_keys; + std::vector actual_keys; + for (const auto& [name, tris] : expected) { + expected_keys.push_back(name); } - ASSERT_EQ(actual.size(), expected.size()) << "straight-approach floor topology changed"; - - for (std::size_t i = 0; i < expected.size(); ++i) { - if (expected[i].starts_with("v ")) { - double ex = 0.0, ey = 0.0, ez = 0.0, ax = 0.0, ay = 0.0, az = 0.0; - std::istringstream(expected[i].substr(2)) >> ex >> ey >> ez; - ASSERT_TRUE(actual[i].starts_with("v ")) << "line " << i; - std::istringstream(actual[i].substr(2)) >> ax >> ay >> az; - EXPECT_NEAR(ax, ex, 1e-6) << "line " << i; - EXPECT_NEAR(ay, ey, 1e-6) << "line " << i; - EXPECT_NEAR(az, ez, 1e-6) << "line " << i; - } else { - // '# case' headers and 'f a b c' triangles must match to the character. - EXPECT_EQ(actual[i], expected[i]) << "line " << i; + for (const auto& [name, tris] : actual) { + actual_keys.push_back(name); + } + ASSERT_EQ(actual_keys, expected_keys) << "a straight fixture gained or lost a junction floor"; + + for (const auto& [name, want] : expected) { + const std::vector& got = actual.at(name); + ASSERT_EQ(got.size(), want.size()) << name << ": triangle count changed"; + for (std::size_t t = 0; t < want.size(); ++t) { + for (std::size_t k = 0; k < 9; ++k) { + EXPECT_NEAR(got[t][k], want[t][k], 1e-6) << name << ": triangle " << t << " coord " << k; + } } } } From 832c91800546b3d10031f65d647071853a61cf8e Mon Sep 17 00:00:00 2001 From: Jesus Armando Anaya Date: Sun, 2 Aug 2026 05:09:35 -0700 Subject: [PATCH 3/3] test(mesh): match golden triangles with a tolerance instead of sorting them Sorting canonical triangles and comparing the lists pairwise is not robust: floor coordinates carry float32-precision noise (~2e-6 m at these magnitudes, one float ULP), and a single ULP is enough to swap two neighbours in the sort. Every later triangle then reports as a metres-wide mismatch, which is exactly how this read on the Windows runner -- one true 1.9e-6 difference followed by a cascade of bogus 8 m ones. Match the two triangle multisets with a tolerance instead. 1e-4 m is 20x above the float noise and 50x below kBoundarySimplify, so it cannot hide a real change. Sorting still canonicalises WITHIN a triangle, where the corners are metres apart and no rounding can reorder them. Re-verified by sabotage: kFootprintWeld 0.01 -> 0.012 leaves 25 of 68 golden triangles in `asymmetric` with no counterpart. --- core/tests/test_t_junction_quality.cpp | 44 ++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/core/tests/test_t_junction_quality.cpp b/core/tests/test_t_junction_quality.cpp index cbd32528..f9234678 100644 --- a/core/tests/test_t_junction_quality.cpp +++ b/core/tests/test_t_junction_quality.cpp @@ -1202,6 +1202,11 @@ using CanonTriangle = std::array; /// guarantee (the determinism tests only pin it within one run), so a golden /// that pins it is asserting something the mesher does not promise. Geometry /// and connectivity are what must not move, and both are pinned here. +/// +/// Sorting canonicalises WITHIN a triangle (corners are metres apart, so no +/// rounding can reorder them). Across triangles the caller matches with a +/// tolerance instead — see the comment there for why sorting fails at that +/// level. std::map> parse_floor_dump(const std::string& text) { std::map> out; std::istringstream stream(text); @@ -1265,11 +1270,44 @@ TEST(TJunctionStraightIdentity, StraightApproachFloorsMatchTheGolden) { for (const auto& [name, want] : expected) { const std::vector& got = actual.at(name); ASSERT_EQ(got.size(), want.size()) << name << ": triangle count changed"; - for (std::size_t t = 0; t < want.size(); ++t) { - for (std::size_t k = 0; k < 9; ++k) { - EXPECT_NEAR(got[t][k], want[t][k], 1e-6) << name << ": triangle " << t << " coord " << k; + + // Match as a multiset with a tolerance rather than comparing the sorted + // lists pairwise. Floor coordinates carry float32-precision noise — about + // 2e-6 m at these magnitudes, one float ULP — and a single ULP is enough to + // swap two neighbours in the sort, which then reports every later triangle + // as a metres-wide mismatch. Matching is immune to that; sorting is not. + // kTol is 20x above that noise and 50x below kBoundarySimplify, so it + // cannot hide a real change (the kFootprintWeld sabotage moves 2e-3). + constexpr double kTol = 1e-4; + std::vector taken(got.size(), false); + std::size_t unmatched = 0; + for (const CanonTriangle& expect : want) { + bool found = false; + for (std::size_t j = 0; j < got.size() && !found; ++j) { + if (taken[j]) { + continue; + } + bool same = true; + for (std::size_t k = 0; k < 9 && same; ++k) { + same = std::abs(got[j][k] - expect[k]) <= kTol; + } + if (same) { + taken[j] = true; + found = true; + } + } + if (!found) { + ++unmatched; + if (unmatched <= 3) { + ADD_FAILURE() << name << ": no triangle within " << kTol << " m of the golden's (" + << expect[0] << ", " << expect[1] << ") (" << expect[3] << ", " << expect[4] + << ") (" << expect[6] << ", " << expect[7] << ")"; + } } } + EXPECT_EQ(unmatched, 0U) << name << ": " << unmatched << " of " << want.size() + << " golden triangles have no counterpart — straight-approach " + "junction geometry moved"; } }