diff --git a/CLAUDE.md b/CLAUDE.md
index 9c934c97..55624aae 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -593,7 +593,7 @@ turns from the on-screen buttons) with paths built by `YouTurnCreationService`:
- The turn is built from the headland crossing **ahead** of the tractor (`FindTrackHeadlandIntersectionAhead`; one behind only past the last crossing). When no real turn fits, the simple fallback (a semicircle) is seated back along the pass until it keeps `UTurnDistanceFromBoundary` from the fence: to the headland crossing and up to 30 m into the field past it, starting ahead of the tractor (`SimpleFallback`, fork only: renatorgr/AgOpenWeb#130, #135). Only on a straight pass: the fallback runs straight from the tractor, so on a curve it is seated nowhere (as official) and built again as the tractor comes closer; seated to fit 190 m away on a boundary curve, it was never joined and the missed turn was made "from here". It is then used only if every point of it stays in the field, outside obstacles a turn may not drive through (`LeavesField`, #698); otherwise no turn, the creation-failed sound and "No U-turn fits inside the boundary at this end - take over manually". A hard boundary refuses a turn that would start under or behind the tractor.
- A turn tried again with the turn lines moved in (the implement over a hard fence, a landing on a bend, an obstacle in its legs: `CreateTurnPath`'s setback loop, which moves the whole fence in) is seated back from the end of the pass the first try found: walking back from that end, at the moved line's crossing nearest to it (`YouTurnCreationInput.TurnLineEndIndex`, `TurnLineEndOnRetry`; not after a gap crossed). Moved in, the fence's turn line also crossed the pass where the fence bulges in beside it, and the first crossing ahead was taken: on Tapada da Casa an 8 m front boom near the north fence moved the line in 5 m and the turn was made 260 m early, beside the east bulge, landing on the next pass inside its crop and leaving a strip of it unsprayed. Test: `TurnSetbackStaysAtEndTests`. Official since #828; not after a gap crossed is fork only.
- The next pass must have room at this end: at least 20 m of it inside the cultivated area back from the end ahead (`IsPathLandable` / `LandingLength`), not just any piece of it inside the field; otherwise "End of field reached - no room on the next pass at this end" (#712). In this fork, when no pass has room but the current pass goes on inside the field past the headland ahead (a corner the headland clips off it, a gap), there is no turn and AutoSteer drives on across it: "No room to turn at this headland - driving on across it" (`GapAhead` / `DrivesOnAcrossGap`, renatorgr/AgOpenWeb#129; upstream left it out until after the v1.0 freeze). The pass must run on at least 10 m inside past the gap (`MinRunAfterGap`): a sliver or spike of the headland line is not more field.
-- Skip worked passes (the snake order): when the order's next pass has no room at this end, or its turn does not build, the turn goes onto the nearest pass still to do that has room, first on the side the order is going, then on the other side (one turn built per side; a pass at least the skip away before a nearer one; none further than twice the skip's step, else "End of field reached" and the operator decides), never against the side the operator picked with the U-turn arrow (`TurnOntoNearestOpenPass`; AgOpenGPS's `GetNextNotWorkedTrack` only steps outward on the turn's side past worked tracks, so the other side, passes nearer than the skip and the room and build checks are ours); the order is planned again from where it lands. The same nearest pass is tried first when the order's next pass still to do is further than twice the skip's step (`MaxFallbackStep`; the passes in between worked before): the turn went from 17R to 7R along the headland, 80 m, with 16R and 14R open beside the tractor (a tester's field, simulator); with none of them taking a turn, the order's pass is turned onto as before. Fork only. An end where no turn was made is remembered (`_failedEnd`) and not tried again on every cycle until the pass, the direction of travel, the nudge, the order, the field or headland, the skip, the U-turn arrow or a turn setting changes. The order is also planned again when the headland line or the outer boundary changes (`SnakeFieldStamp`), and forgotten with what was found about each pass when the line moves (`ForgetPassStatus`: Reference Line Nudge, an end trimmed; the moved line's pass N is another strip, and the old answers turned the snake past clean passes on Tapada da Casa). In the headland the snake looks for its next turn only heading out of the field (no cultivated ground ahead on the pass): a turn that ends in the headland leaves the tractor facing the field on its new pass, which is not the end of that pass. Fork only: a pass worked on the whole (half covered, `IsPassWorked`) is still to do at the end ahead when less than half of its 20 m inside the cultivated area next to that end is covered (`YouTurnPathingService.IsPassOpenAtThisEnd`, asked in `WorkedPasses` when a turn is planned, once per end): on a tester's field a pass 71 % covered, with its last 100 m at that end left, was turned past.
+- Skip worked passes (the snake order): when the order's next pass has no room at this end, or its turn does not build, the turn goes onto the nearest pass still to do that has room, first on the side the order is going, then on the other side (one turn built per side; a pass at least the skip away before a nearer one; none further than twice the skip's step, else "End of field reached" and the operator decides), never against the side the operator picked with the U-turn arrow (`TurnOntoNearestOpenPass`; AgOpenGPS's `GetNextNotWorkedTrack` only steps outward on the turn's side past worked tracks, so the other side, passes nearer than the skip and the room and build checks are ours); the order is planned again from where it lands. The same nearest pass is tried first when the order's next pass still to do is further than twice the skip's step (`MaxFallbackStep`; the passes in between worked before), from any pass nearer than the order's, either side: the turn went from 17R to 7R along the headland, 80 m, with 16R and 14R open beside the tractor (a tester's field, simulator), and on 21R, with the order's 8R 13 passes over, 15R six over was never looked at and the turn onto 8R did not build, so there was no turn; with none of them taking a turn, the order's pass is turned onto as before. Fork only. An end where no turn was made is remembered (`_failedEnd`) and not tried again on every cycle until the pass, the direction of travel, the nudge, the order, the field or headland, the skip, the U-turn arrow or a turn setting changes. The order is also planned again when the headland line or the outer boundary changes (`SnakeFieldStamp`), and forgotten with what was found about each pass when the line moves (`ForgetPassStatus`: Reference Line Nudge, an end trimmed; the moved line's pass N is another strip, and the old answers turned the snake past clean passes on Tapada da Casa). In the headland the snake looks for its next turn only heading out of the field (no cultivated ground ahead on the pass): a turn that ends in the headland leaves the tractor facing the field on its new pass, which is not the end of that pass. Fork only: a pass worked on the whole (half covered, `IsPassWorked`) is still to do at the end ahead when less than half of its 20 m inside the cultivated area next to that end is covered (`YouTurnPathingService.IsPassOpenAtThisEnd`, asked in `WorkedPasses` when a turn is planned, once per end): on a tester's field a pass 71 % covered, with its last 100 m at that end left, was turned past.
- The end of a pass at a hard outer boundary with no turn planned or driven there ("End of field reached", no turn that fits, U-turns off): as the pivot gets 1 m into the headland from the cultivated area (`HeadlandEntryCheckAfter`), on the line and heading out (the pass leaves the outer boundary within 300 m with no 10 m of field on it first, `YouTurnPathingService.PassLeavesFieldAhead`), AutoSteer goes off with the alarm, "AutoSteer off: end of the pass and no U-turn here. Turn by hand." (`YouTurnStateMachine.EndOfPassWithoutTurn`, step 5b'''' in `GpsPipelineService`; once per entry; fork only). Left on, AutoSteer steered on along the line through the fence (as AgOpenGPS, which only sends geoStop): on Tapada da Casa the simulator drove on 2.8 km. Heading into the field, a headland strip with field past it, or a soft boundary do not count. This stop, the line facing out of the field below and a pass still open at this end measure from the headland line, where the crop ends, not from the turn line the U-turn uses (`TickContext.PassEndLine`, the line the pipeline arms them on: from the turn line a headland more than 10 m wider than the U-turn distance never stopped AutoSteer).
- A turn planned for the end the tractor was heading to is dropped when the tractor turns round on the pass (by hand, or the simulator's turn-around), and the turn at the end ahead is planned (TURNED ROUND in `YouTurnStateMachine.Tick`, `_plannedSameWay`; AgOpenGPS drops a created turn once the pivot leaves the turn area without it, `ResetCreatedYouTurn`). Kept, the headland stop and the fence guard below took it for a turn planned at the other end and left AutoSteer on (Tapada da Casa, simulator). Fork only.
- Only a turn keeps the implement off a hard outer boundary. With none planned or driven, AutoSteer on, going forward, the tractor inside the fence, an end of the implement (half `ActualToolWidth` plus the offset either side of the tool) heading out across the fence, more than 0.5 m out and further than now within 2 s of travel, takes AutoSteer off with the alarm (`HardFenceGuard`, step 5b'''' in `GpsPipelineService`; fork only). AutoSteer joining a line from a corner with the line moved five passes swung an 8 m front boom 0.9 m over the fence. The 2 s are along the curve steered, not straight ahead: the implement's ends turn with the tractor about its turning centre, from the last cycle's steer angle (the one the line asked for when AutoSteer has just gone on; a cycle with no line steered before it waits for the next). Straight ahead leaves a bend: on a boundary curve's first lap (the boom's end on the fence) it took AutoSteer off at a bend with the tractor on the line (`HardFenceGuardPipelineTests`). Joining a line sideways beside the fence is an S that keeps the implement inside (*Stepping onto a line beside a hard fence*). Driving along the fence a little over it, back in, or below walking pace (`MinLookAhead`) does not count.
diff --git a/Shared/AgOpenWeb.Services/YouTurn/YouTurnStateMachine.cs b/Shared/AgOpenWeb.Services/YouTurn/YouTurnStateMachine.cs
index a7aa128a..cb665685 100644
--- a/Shared/AgOpenWeb.Services/YouTurn/YouTurnStateMachine.cs
+++ b/Shared/AgOpenWeb.Services/YouTurn/YouTurnStateMachine.cs
@@ -1273,8 +1273,9 @@ private EndTried EndKey(in TickContext ctx, Models.Track.Track track, GuidanceWo
/// (a hard fence on that side), the ones further that way fit no better. True when one was built
/// and is the planned turn. Ours: AgOpenGPS (CYouTurn.GetNextNotWorkedTrack) only steps outward
/// on the turn's side past worked tracks; the other side, passes nearer than the skip and the
- /// room and build checks are not in it. : asked before the order's
- /// pass, which is further than (only the log says so).
+ /// room and build checks are not in it. Passes up to away are looked
+ /// at, or with (asked before the order's pass, which is further than
+ /// that) every pass nearer than the order's.
///
private bool TurnOntoNearestOpenPass(
in TickContext ctx,
@@ -1294,10 +1295,15 @@ private bool TurnOntoNearestOpenPass(
int skip = ctx.UTurnSkipRows + 1;
int orderSide = orderPass >= current ? 1 : -1;
int[] sides = operatorSide != 0 ? new[] { operatorSide } : new[] { orderSide, -orderSide };
+ // Asked before a far order pass, any pass nearer than it is a shorter way: within twice the
+ // skip only, the order's own pass further over was tried and nearer ones were not (Tapada da
+ // Casa: on 21R the order's 8R was 13 passes over, 15R six over was never looked at, and the
+ // turn onto 8R did not build).
+ int reach = orderPassFar ? Math.Abs(orderPass - current) - 1 : MaxFallbackStep(skip);
foreach (int side in sides)
{
var candidates = seq.Distinct()
- .Where(p => p != orderPass && Math.Sign(p - current) == side && Math.Abs(p - current) <= MaxFallbackStep(skip))
+ .Where(p => p != orderPass && Math.Sign(p - current) == side && Math.Abs(p - current) <= reach)
.OrderBy(p => Math.Abs(p - current) < skip ? 1 : 0)
.ThenBy(p => Math.Abs(p - current));
int looked = 0;
diff --git a/Tests/AgOpenWeb.Services.Tests/YouTurn/SnakeOpenPassTests.cs b/Tests/AgOpenWeb.Services.Tests/YouTurn/SnakeOpenPassTests.cs
index 488fda88..3d3d5d61 100644
--- a/Tests/AgOpenWeb.Services.Tests/YouTurn/SnakeOpenPassTests.cs
+++ b/Tests/AgOpenWeb.Services.Tests/YouTurn/SnakeOpenPassTests.cs
@@ -188,15 +188,54 @@ public void The_order_s_next_pass_far_along_the_headland_waits_for_the_passes_be
}
[Test]
- public void The_order_s_far_pass_is_still_taken_with_none_still_to_do_beside_the_tractor()
+ public void A_pass_still_to_do_nearer_than_the_order_s_far_pass_is_taken_though_more_than_twice_the_skip_over()
{
- // As above with every pass within twice the skip's step either side worked: the order's
- // pass is next, as before, so the field still gets done.
+ // As above with 1 and 3 worked too: the nearest pass still to do is 5, five over, more than
+ // twice the skip's step but nearer than the order's 8. As on a tester's field (simulator):
+ // on 21R the order's 8R was 13 passes over and 15R six over; the search stopped at four
+ // passes, the turn onto 8R did not build, and there was no turn at all.
var (guidance, turn, square) = FarOrderPass();
foreach (int k in new[] { -4, -3, -2, -1, 1, 3 }) _track.MarkPathWorked(k);
var effects = _sm.Tick(Ctx(square, PassX(0), -25, 180), guidance, turn);
+ Assert.That(turn.SnakeSequence!.Take(5), Is.EqualTo(new[] { 0, 2, 4, 6, 8 }), "the scenario: the order goes on east");
+ Assert.Multiple(() =>
+ {
+ Assert.That(turn.TurnPath, Is.Not.Null, $"a turn, not \"{effects.StatusMessage}\"");
+ Assert.That(turn.ReturnPassTargetPath, Is.EqualTo(5), "the nearest pass still to do, not the order's 8");
+ Assert.That(turn.NextTrack!.Points[0].Easting, Is.EqualTo(PassX(5)).Within(0.01));
+ Assert.That(_creation.TurnPathBuilds, Is.EqualTo(1), "only the turn onto pass 5 is built");
+ });
+ }
+
+ [Test]
+ public void A_pass_still_to_do_on_the_other_side_nearer_than_the_order_s_far_pass_is_taken()
+ {
+ // Every pass east up to the order's 8 worked; west, -5 is still to do, five over.
+ var (guidance, turn, square) = FarOrderPass();
+ foreach (int k in new[] { -4, -3, -2, -1, 1, 3, 5, 7 }) _track.MarkPathWorked(k);
+
+ var effects = _sm.Tick(Ctx(square, PassX(0), -25, 180), guidance, turn);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(turn.TurnPath, Is.Not.Null, $"a turn, not \"{effects.StatusMessage}\"");
+ Assert.That(turn.ReturnPassTargetPath, Is.EqualTo(-5), "the nearest pass still to do, not the order's 8");
+ Assert.That(turn.NextTrack!.Points[0].Easting, Is.EqualTo(PassX(-5)).Within(0.01));
+ });
+ }
+
+ [Test]
+ public void The_order_s_far_pass_is_still_taken_with_none_still_to_do_nearer()
+ {
+ // As above with every pass nearer than the order's worked, either side: the order's pass
+ // is next, as before, so the field still gets done.
+ var (guidance, turn, square) = FarOrderPass();
+ foreach (int k in new[] { -7, -6, -5, -4, -3, -2, -1, 1, 3, 5, 7 }) _track.MarkPathWorked(k);
+
+ var effects = _sm.Tick(Ctx(square, PassX(0), -25, 180), guidance, turn);
+
Assert.Multiple(() =>
{
Assert.That(turn.TurnPath, Is.Not.Null, $"a turn, not \"{effects.StatusMessage}\"");