Skip to content

Commit

Permalink
Merge pull request #3518 from Courseplay/3510-featuresmoother-headlan…
Browse files Browse the repository at this point in the history
…d-generation

feat: smooth zigzagged field boundaries a bit
  • Loading branch information
Tensuko authored Oct 11, 2024
2 parents 146fc8f + 61d5151 commit cb62981
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 28 deletions.
11 changes: 10 additions & 1 deletion scripts/courseGenerator/Field.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ CourseGenerator.Field = Field

---@param id string unique ID for this field for logging
---@param num number field number as shown in game
---@param boundary Polygon|nil boundary of the field
---@param boundary [{x, y}]|nil boundary of the field
function Field:init(id, num, boundary)
self.id = id
self.num = num
Expand All @@ -17,6 +17,7 @@ function Field:init(id, num, boundary)
self.islands = {}
if boundary then
self.boundary:calculateProperties()
self:_smoothBoundary()
end
end

Expand Down Expand Up @@ -66,10 +67,18 @@ function Field.loadSavedFields(fileName)
for _, f in pairs(fields) do
f:getBoundary():calculateProperties()
f:setupIslands()
f:_smoothBoundary()
end
return fields
end

--- Smooth out zigzags in the boundary. Zigzags are created when the FieldScanner is running at high resolution
--- bigger than the game field pixel size
function Field:_smoothBoundary()
CourseGenerator.SplineHelper.smooth(self:getBoundary(), 2, 1, #self:getBoundary(), math.rad(5), math.rad(45))
self:getBoundary():ensureMinimumEdgeLength(CourseGenerator.cMaxEdgeLength, math.rad(30))
end

--- Center of the field (centroid)
---@return Vector
function Field:getCenter()
Expand Down
7 changes: 5 additions & 2 deletions scripts/courseGenerator/FieldworkCourseMultiVehicle.lua
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,11 @@ function FieldworkCourseMultiVehicle:_offsetConnectingPath(path, ix, offsetVecto
section:append(path[i])
i = i + 1
until i > #path or not path[i]:getAttributes():isOnConnectingPath()
local offsetConnectingPath = _generateOffsetSection(section, offsetVector)
_appendOffsetSection(section, offsetConnectingPath, offsetPath)
if #section > 1 then
-- connecting paths with a single vertex can be ignored
local offsetConnectingPath = _generateOffsetSection(section, offsetVector)
_appendOffsetSection(section, offsetConnectingPath, offsetPath)
end
return i
end

Expand Down
33 changes: 21 additions & 12 deletions scripts/courseGenerator/SplineHelper.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ local SplineHelper = {}
---@param from number start index
---@param to number end index (may be less than from, to wrap around a polygon's end
---@param s number tuck factor
local function tuck(p, from, to, s)
local function tuck(p, from, to, s, minAngle, maxAngle)
for _, cv, pv, nv in p:vertices(from, to) do
if pv and cv and nv then
if not cv.isCorner and cv.dA and math.abs(cv.dA) > CourseGenerator.cMinSmoothingAngle then
if not cv.isCorner and cv.dA and math.abs(cv.dA) > (minAngle or CourseGenerator.cMinSmoothingAngle)
and math.abs(cv.dA) < (maxAngle or math.huge) then
local m = (pv + nv) / 2
local cm = m - cv
cv.x, cv.y = cv.x + s * cm.x, cv.y + s * cm.y
Expand All @@ -19,7 +20,7 @@ local function tuck(p, from, to, s)
end

--- Add a vertex between existing ones
local function refine(p, from, to)
local function refine(p, from, to, minAngle, maxAngle)
-- iterate through the existing table but do not insert the
-- new points, only remember the index where they would end up
-- (as we do not want to modify the table while iterating)
Expand All @@ -28,33 +29,41 @@ local function refine(p, from, to)
for i, cv, _, nv in p:vertices(from, to) do
-- initialize ix to the first value of i
if nv and cv then
if not cv.isCorner and cv.dA and math.abs(cv.dA) > CourseGenerator.cMinSmoothingAngle then
if not cv.isCorner and cv.dA and math.abs(cv.dA) > (minAngle or CourseGenerator.cMinSmoothingAngle)
and math.abs(cv.dA) < (maxAngle or math.huge) then
local m = (nv + cv) / 2
local newVertex = cv:clone()
newVertex.x, newVertex.y = m.x, m.y
ix = ix + 1
table.insert(verticesToInsert, {ix = ix, vertex = newVertex})
table.insert(verticesToInsert, { ix = ix, vertex = newVertex })
end
end
ix = ix + 1
end
for _, v in ipairs(verticesToInsert) do
table.insert(p, v.ix, v.vertex )
table.insert(p, v.ix, v.vertex)
end
p:calculateProperties(from, to + #verticesToInsert)
end

---@return number the index where
function SplineHelper.smooth(p, order, from, to)
---@param p Polyline
---@param order number how many times smooth should be called, more calls will result in a smoother curve
---@param from number start index
---@param to number end index (may be less than from, to wrap around a polygon's end
---@param minAngle number|nil ignore vertices where the delta angle is less than this, default is CourseGenerator.cMinSmoothingAngle
---@param maxAngle number|nil ignore vertices where the delta angle is greater than this, default is math.huge
---@return number the index where the smoothing ended, that is, the new value of 'to' after smoothing inserted
--- new vertices
function SplineHelper.smooth(p, order, from, to, minAngle, maxAngle)
if (order <= 0) then
return
else
local origSize = #p
refine(p, from, to)
refine(p, from, to, minAngle, maxAngle)
to = to + #p - origSize
tuck(p, from, to, 0.5)
tuck(p, from, to, -0.15)
SplineHelper.smooth(p, order - 1, from, to)
tuck(p, from, to, 0.5, minAngle, maxAngle)
tuck(p, from, to, -0.15, minAngle, maxAngle)
SplineHelper.smooth(p, order - 1, from, to, minAngle, maxAngle)
end
return to
end
Expand Down
14 changes: 7 additions & 7 deletions scripts/courseGenerator/test/FieldTest.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ lu.EPS = 0.01
function testField()
local fields = CourseGenerator.Field.loadSavedFields('fields/Coldborough.xml')
lu.assertEquals(#fields, 9)
lu.assertEquals(#fields[8].boundary, 135)
lu.assertEquals(#fields[8].boundary, 90)
local field = fields[8]
local center = field:getCenter()
lu.assertAlmostEquals(center.x, 380.8, 0.1)
lu.assertAlmostEquals(center.y, 31.14, 0.1)
lu.assertAlmostEquals(center.x, 381.41, 0.1)
lu.assertAlmostEquals(center.y, 31.3, 0.1)
local x1, y1, x2, y2 = field:getBoundingBox()
lu.assertAlmostEquals(x1, 307.15)
lu.assertAlmostEquals(y1, -80.84)
lu.assertAlmostEquals(x2, 452.84)
lu.assertAlmostEquals(y2, 157.33)
lu.assertAlmostEquals(x1, 307.18)
lu.assertAlmostEquals(y1, -80.66)
lu.assertAlmostEquals(x2, 452.82)
lu.assertAlmostEquals(y2, 157.16)
end
os.exit(lu.LuaUnit.run())
6 changes: 3 additions & 3 deletions scripts/geometry/Polygon.lua
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,9 @@ function Polygon:getLongestEdgeDirection()
return self.longestEdgeDirection
end

function Polygon:ensureMinimumEdgeLength(minimumLength)
Polyline.ensureMinimumEdgeLength(self, minimumLength)
if (self[1] - self[#self]):length() < minimumLength then
function Polygon:ensureMinimumEdgeLength(minimumLength, maxDeltaAngle)
Polyline.ensureMinimumEdgeLength(self, minimumLength, maxDeltaAngle)
if (self[1] - self[#self]):length() < minimumLength and self:_canRemoveVertex(#self, maxDeltaAngle) then
table.remove(self, #self)
end
self:calculateProperties(#self - 1)
Expand Down
19 changes: 16 additions & 3 deletions scripts/geometry/Polyline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ function Polyline:removeGlitches()
local i = 1
while i < #self do
local dA = self:at(i).dA
if dA and dA > math.pi - 0.2 then
if dA and math.abs(dA) > math.pi - 0.2 then
table.remove(self, i)
else
i = i + 1
Expand All @@ -329,11 +329,24 @@ function Polyline:removeGlitches()
self:calculateProperties()
end

function Polyline:_canRemoveVertex(i, maxDeltaAngle)
if not maxDeltaAngle then
return true
else
-- only remove vertices which aren't around a corner
return math.abs(self:at(i + 1).dA) < maxDeltaAngle and math.abs(self:at(i).dA) < maxDeltaAngle
end
end


--- If two vertices are closer than minimumLength, replace them with one between.
function Polyline:ensureMinimumEdgeLength(minimumLength)
---@param minimumLength number After this operation, no two vertices will be closer than minimumLength
---@param maxDeltaAngle number|nil when specified, vertices where the delta angle is bigger than this are not removed,
--- thus, corners are preserved
function Polyline:ensureMinimumEdgeLength(minimumLength, maxDeltaAngle)
local i = 1
while i < #self do
if (self:at(i + 1) - self:at(i)):length() < minimumLength then
if (self:at(i + 1) - self:at(i)):length() < minimumLength and self:_canRemoveVertex(i, maxDeltaAngle) then
table.remove(self, i + 1)
else
i = i + 1
Expand Down

0 comments on commit cb62981

Please sign in to comment.