diff --git a/scripts/courseGenerator/Field.lua b/scripts/courseGenerator/Field.lua index f59ddbbf6..3b9902ca5 100644 --- a/scripts/courseGenerator/Field.lua +++ b/scripts/courseGenerator/Field.lua @@ -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 @@ -17,6 +17,7 @@ function Field:init(id, num, boundary) self.islands = {} if boundary then self.boundary:calculateProperties() + self:_smoothBoundary() end end @@ -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() diff --git a/scripts/courseGenerator/FieldworkCourseMultiVehicle.lua b/scripts/courseGenerator/FieldworkCourseMultiVehicle.lua index 54548486e..41635a02d 100644 --- a/scripts/courseGenerator/FieldworkCourseMultiVehicle.lua +++ b/scripts/courseGenerator/FieldworkCourseMultiVehicle.lua @@ -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 diff --git a/scripts/courseGenerator/SplineHelper.lua b/scripts/courseGenerator/SplineHelper.lua index 1b7b0913d..3d31d85fc 100644 --- a/scripts/courseGenerator/SplineHelper.lua +++ b/scripts/courseGenerator/SplineHelper.lua @@ -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 @@ -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) @@ -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 diff --git a/scripts/courseGenerator/test/FieldTest.lua b/scripts/courseGenerator/test/FieldTest.lua index f407a8b9b..c2f0c2184 100644 --- a/scripts/courseGenerator/test/FieldTest.lua +++ b/scripts/courseGenerator/test/FieldTest.lua @@ -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()) diff --git a/scripts/geometry/Polygon.lua b/scripts/geometry/Polygon.lua index 84c8a167b..fe7316de7 100644 --- a/scripts/geometry/Polygon.lua +++ b/scripts/geometry/Polygon.lua @@ -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) diff --git a/scripts/geometry/Polyline.lua b/scripts/geometry/Polyline.lua index 79f8b6aa7..0aacf4595 100644 --- a/scripts/geometry/Polyline.lua +++ b/scripts/geometry/Polyline.lua @@ -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 @@ -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