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..74de273d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1370,6 +1370,41 @@ 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**, 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. + + 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..f9234678 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,316 @@ 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); - 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"; + 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]); + } + } + + 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; +} + +/// 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. +/// +/// 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); + 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 + } + 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; +} + +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); + } + 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"; + + // 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"; + } +} + +// 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 +1341,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) {