Skip to content

Commit

Permalink
Merge pull request #26 from Courseplay/18-rows-almost-parallel-to-the…
Browse files Browse the repository at this point in the history
…-headland-and-crossing-it-multiple-times-result-in-extra-blocks

18 rows almost parallel to the headland and crossing it multiple times result in extra blocks
  • Loading branch information
pvaiko authored Jan 23, 2024
2 parents eb7cf8d + 8e88627 commit e3b99a1
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 71 deletions.
20 changes: 12 additions & 8 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,18 @@ jobs:
- name: Run unit tests
run: |
cd CourseGenerator/test
lua WrapAroundIndexTest.lua
lua LineSegmentTest.lua
lua VertexTest.lua
lua SliderTest.lua
lua TransformTest.lua
lua PolylineTest.lua
lua PolygonTest.lua
lua HeadlandConnectorTest.lua
lua BlockSequencerTest.lua
lua CacheMapTest.lua
lua FieldTest.lua
lua FieldworkCourseTest.lua
lua HeadlandConnectorTest.lua
lua LineSegmentTest.lua
lua PolygonTest.lua
lua PolylineTest.lua
lua SliderTest.lua
lua RowPatternTest.lua
lua RowTest.lua
lua SliderTest.lua
lua TransformTest.lua
lua VertexTest.lua
lua WrapAroundIndexTest.lua
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Courseplay_FS22
*.log
*.log
.idea
4 changes: 2 additions & 2 deletions CourseGenerator/Center.lua
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ function Center:_findBestRowAngle()
-- sin(a - longestEdgeDirection) will be 0 when angle is the closest.
local notLongestEdgePenalty = 5 * math.abs(math.sin(cg.Math.getDeltaAngle(math.rad(a), longestEdgeDirection)))
local score = 6 * #blocks + #rows + smallBlockPenalty + notLongestEdgePenalty
self.logger:debug(' %dº - rows: %d blocks: %d small block penalty: %.1f not longest edge penalty: %.1f score: %.3f',
self.logger:trace(' %dº - rows: %d blocks: %d small block penalty: %.1f not longest edge penalty: %.1f score: %.3f',
a, #rows, #blocks, smallBlockPenalty, notLongestEdgePenalty, score)
if score < minScore then
minScore = score
Expand Down Expand Up @@ -400,7 +400,7 @@ function Center:_splitIntoBlocks(rows, headland)
local blockId = 1

for i, row in ipairs(rows) do
local sections = row:split(headland, self.bigIslands)
local sections = row:split(headland, self.bigIslands, false, self.context.enableSmallOverlapsWithHeadland)
self.logger:trace('Row %d has %d section(s)', i, #sections)
-- first check if there is a block which overlaps with more than one section
-- if that's the case, close the open blocks. This forces the creation of new blocks
Expand Down
14 changes: 12 additions & 2 deletions CourseGenerator/FieldworkContext.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function FieldworkContext:init(field, workingWidth, turningRadius, nHeadlands)
self.rowAngle = 0
self.evenRowDistribution = false
self.useBaselineEdge = false
self.enableSmallOverlapsWithHeadland = false
self.logger = Logger('FieldworkContext')
self.errors = {}
end
Expand All @@ -37,8 +38,8 @@ function FieldworkContext:log()
self.workingWidth, self.turningRadius, self.nHeadlands, self.nHeadlandsWithRoundCorners, self.headlandClockwise)
self.logger:debug('field corner radius: %.1f, sharpen corners: %s, bypass islands: %s, headlands around islands %d, island headland cw %s',
self.fieldCornerRadius, self.sharpenCorners, self.bypassIslands, self.nIslandHeadlands, self.islandHeadlandClockwise)
self.logger:debug('row pattern: %s, row angle auto: %s, %.1fº, even row distribution: %s, use baseline edge: %s',
self.rowPattern, self.autoRowAngle, math.deg(self.rowAngle), self.evenRowDistribution, self.useBaselineEdge)
self.logger:debug('row pattern: %s, row angle auto: %s, %.1fº, even row distribution: %s, use baseline edge: %s, small overlaps: %s',
self.rowPattern, self.autoRowAngle, math.deg(self.rowAngle), self.evenRowDistribution, self.useBaselineEdge, self.enableSmallOverlapsWithHeadland)
end

function FieldworkContext:addError(logger, ...)
Expand Down Expand Up @@ -172,5 +173,14 @@ function FieldworkContext:setRowPattern(rowPattern)
return self
end

---@param enableSmallOverlaps boolean|nil if true, and the row is almost parallel to the boundary and crosses it
--- multiple times (for instance a slightly zigzagging headland), do not split the row unless it is getting too
--- far from the boundary (it is like a smart version of onlyFirstAndLastInterSections, but significantly will slow
--- down the generation)
function FieldworkContext:setEnableSmallOverlapsWithHeadland(enableSmallOverlaps)
self.enableSmallOverlapsWithHeadland = enableSmallOverlaps
return self
end

---@class cg.FieldworkContext
cg.FieldworkContext = FieldworkContext
2 changes: 1 addition & 1 deletion CourseGenerator/FieldworkCourse.lua
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ end

---@return cg.Polyline
function FieldworkCourse:getCenterPath()
return self.center:getPath()
return self.center and self.center:getPath() or cg.Polyline()
end

------------------------------------------------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion CourseGenerator/FieldworkCourseTwoSided.lua
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ function FieldworkCourseTwoSided:_trim(row, middleHeadlandRow)
end
-- where is the longer part?
local is = intersections[1]
local lengthFromIntersectionToEnd = row:getLength(is.ixA)
local lengthFromIntersectionToEnd = row:getLengthBetween(is.ixA)
if lengthFromIntersectionToEnd < row:getLength() / 2 then
-- shorter part towards the end
row:cutEndAtIx(is.ixA)
Expand Down
56 changes: 42 additions & 14 deletions CourseGenerator/Geometry/Polyline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,15 @@ function Polyline:vertices(from, to)
end

--- edge iterator
---@param startIx number|nil start the iteration at the edge starting at the startIx vertex (or at the first)
---@param endIx number|nil the last edge to return is the one starting at endIx (or the last vertex)
---@return number, cg.LineSegment, cg.Vertex
function Polyline:edges(startIx)
function Polyline:edges(startIx, endIx)
local i = startIx and startIx - 1 or 0
local last = endIx or #self
return function()
i = i + 1
if i >= #self then
if i >= last then
return nil, nil, nil
else
return i, self[i]:getExitEdge() or cg.LineSegment.fromVectors(self[i], self[i + 1]), self[i]
Expand Down Expand Up @@ -166,24 +169,27 @@ function Polyline:reverse()
return self
end

function Polyline:getLength(startIx)
function Polyline:getLength()
-- we cache the full length, and if it exists, return it
if not self.length or startIx then
if not self.length then
-- otherwise calculate length
local length = 0
for _, e in self:edges(startIx) do
for _, e in self:edges() do
length = length + e:getLength()
end
if startIx then
return length
else
-- full length was requested, cache it
self.length = length
end
self.length = length
end
return self.length
end

function Polyline:getLengthBetween(startIx, endIx)
local length = 0
for _, e in self:edges(startIx, endIx) do
length = length + e:getLength()
end
return length
end

---@return number index of the first vertex which is at least d distance from ix
---(can be nil if the end of line reached before d)
function Polyline:moveForward(ix, d)
Expand Down Expand Up @@ -255,6 +261,9 @@ end
--- Cut all vertices from the first vertex up to but not including ix, shortening the polyline at the start
---@param ix number
function Polyline:cutStartAtIx(ix)
if ix >= #self then
return
end
ix = math.min(ix - 1, #self - 2)
for _ = 1, ix do
table.remove(self, 1)
Expand All @@ -279,7 +288,7 @@ function Polyline:trimAtFirstIntersection(other)
return
end
-- where is the longer part?
local lengthFromIntersectionToEnd = self:getLength(intersections[1].ixA)
local lengthFromIntersectionToEnd = self:getLengthBetween(intersections[1].ixA)
if lengthFromIntersectionToEnd < self:getLength() / 2 then
-- shorter part towards the end
self:cutEndAtIx(intersections[1].ixA)
Expand Down Expand Up @@ -498,7 +507,7 @@ function Polyline:ensureMinimumRadius(r, makeCorners)
totalMoved)
cg.addDebugPoint(entry:getBase())
cg.addDebugPoint(exit:getBase())
self[currentIx].isCorner = true
cg.addDebugPoint(self[currentIx])
end
else
adjustedCornerVertices = makeArc(entry, exit)
Expand Down Expand Up @@ -666,7 +675,7 @@ end
--- Private functions
------------------------------------------------------------------------------------------------------------------------

--- Get all intersections with other, in the order we would meet them traversing self in the given direction
--- Get all intersections with other, in the order we would meet them traversing self in the increasing index direction
---@param other cg.Polyline
---@param startIx number index to start looking for intersections with other
---@param userData any user data to add to the Intersection objects (to later identify them)
Expand Down Expand Up @@ -817,6 +826,25 @@ function Polyline:_createOffset(result, offsetVector, minEdgeLength, preserveCor
end


------ Cut a polyline at is1 and is2, keeping the section between the two. is1 and is2 becomes the start and
--- end of the cut polyline.
---@param is1 cg.Intersection
---@param is2 cg.Intersection
---@param section cg.Polyline|nil an optional, initialized object that will become the section
---@return cg.Polyline
function Polyline:_cutAtIntersections(is1, is2, section)
section = section or cg.Polyline()
section:append(is1.is)
local src = is1.ixA + 1
while src < is2.ixA do
section:append(self[src])
src = src + 1
end
section:append(is2.is)
section:calculateProperties()
return section
end

function Polyline:__tostring()
local result = ''
for i, v in ipairs(self) do
Expand Down
9 changes: 8 additions & 1 deletion CourseGenerator/Geometry/Slider.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ local Slider = CpObject(cg.LineSegment)
---@param d number distance from polyline[ix] for the initial position
function Slider:init(polyline, ix, d)
self.polyline = polyline
self._isAtEnd = false
self:set(ix, 0)
self:move(d)
self:move(d or 0)
end

function Slider:set(ix, d)
Expand All @@ -29,15 +30,19 @@ end

--- Move d distance along the polyline. Will not move past the ends.
---@param d number distance to move
---@return boolean true if the move was successful, false if the end of the polyline reached before moving the
--- distance required
function Slider:move(d)
local dRemaining = math.abs(d)
local ix, offset = self.ix, self.d
local endReached = false

local function forward()
local exitEdge = self:vertex(ix):getExitEdge()
if not exitEdge then
-- reached the end of polyline
offset = 0
endReached = true
return false
end
local dToEdgeEnd = exitEdge:getLength() - offset
Expand All @@ -57,6 +62,7 @@ function Slider:move(d)
if not entryEdge then
-- reached the start of polyline
dRemaining = 0
endReached = true
return false
end
if dRemaining > offset then
Expand All @@ -76,6 +82,7 @@ function Slider:move(d)

end
self:set(ix, offset)
return not endReached
end

---@class cg.Slider : cg.LineSegment
Expand Down
Loading

0 comments on commit e3b99a1

Please sign in to comment.