diff --git a/game/love_src/lib/slick/cache.lua b/game/love_src/lib/slick/cache.lua new file mode 100644 index 0000000..a61914d --- /dev/null +++ b/game/love_src/lib/slick/cache.lua @@ -0,0 +1,22 @@ +local delaunay = require("slick.geometry.triangulation.delaunay") + +--- @class slick.cache +--- @field triangulator slick.geometry.triangulation.delaunay +local cache = {} +local metatable = { __index = cache } + +--- @param options slick.options +function cache.new(options) + if options.sharedCache then + return options.sharedCache + end + + return setmetatable({ + triangulator = delaunay.new({ + epsilon = options.epsilon, + debug = options.debug + }) + }, metatable) +end + +return cache diff --git a/game/love_src/lib/slick/collision/box.lua b/game/love_src/lib/slick/collision/box.lua new file mode 100644 index 0000000..e88fd0e --- /dev/null +++ b/game/love_src/lib/slick/collision/box.lua @@ -0,0 +1,50 @@ +local commonShape = require("slick.collision.commonShape") +local transform = require("slick.geometry.transform") + +--- @class slick.collision.box: slick.collision.commonShape +local box = setmetatable({}, { __index = commonShape }) +local metatable = { __index = box } + +--- @param entity slick.entity | slick.cache | nil +--- @param x number +--- @param y number +--- @param w number +--- @param h number +--- @return slick.collision.box +function box.new(entity, x, y, w, h) + local result = setmetatable(commonShape.new(entity), metatable) + + --- @cast result slick.collision.box + result:init(x, y, w, h) + return result +end + +--- @param x number +--- @param y number +--- @param w number +--- @param h number +function box:init(x, y, w, h) + commonShape.init(self) + + self:addPoints( + x, y, + x + w, y, + x + w, y + h, + x, y + h) + + self:addNormal(0, 1) + self:addNormal(-1, 0) + self:addNormal(0, -1) + self:addNormal(1, 0) + + self:transform(transform.IDENTITY) + + assert(self.vertexCount == 4, "box must have 4 points") + assert(self.normalCount == 4, "box must have 4 normals") +end + +function box:inside(p) + return p.x >= self.bounds:left() and p.x <= self.bounds:right() and p.y >= self.bounds:top() and p.y <= self.bounds:bottom() +end + +return box diff --git a/game/love_src/lib/slick/collision/commonShape.lua b/game/love_src/lib/slick/collision/commonShape.lua new file mode 100644 index 0000000..484c305 --- /dev/null +++ b/game/love_src/lib/slick/collision/commonShape.lua @@ -0,0 +1,288 @@ +local point = require("slick.geometry.point") +local rectangle = require("slick.geometry.rectangle") +local segment = require("slick.geometry.segment") +local slickmath = require("slick.util.slickmath") +local slicktable = require("slick.util.slicktable") + +--- @class slick.collision.commonShape +--- @field tag any +--- @field entity slick.entity | slick.cache | nil +--- @field vertexCount number +--- @field normalCount number +--- @field center slick.geometry.point +--- @field vertices slick.geometry.point[] +--- @field bounds slick.geometry.rectangle +--- @field private preTransformedVertices slick.geometry.point[] +--- @field normals slick.geometry.point[] +--- @field private preTransformedNormals slick.geometry.point[] +local commonShape = {} +local metatable = { __index = commonShape } + +--- @param e slick.entity | slick.cache | nil +--- @return slick.collision.commonShape +function commonShape.new(e) + return setmetatable({ + entity = e, + vertexCount = 0, + normalCount = 0, + center = point.new(), + bounds = rectangle.new(), + vertices = {}, + preTransformedVertices = {}, + normals = {}, + preTransformedNormals = {} + }, metatable) +end + +function commonShape:init() + self.vertexCount = 0 + self.normalCount = 0 +end + +function commonShape:makeClockwise() + -- Line segments don't have a winding. + -- And points nor empty polygons do either. + if self.vertexCount < 3 then + return + end + + local winding + for i = 1, self.vertexCount do + local j = slickmath.wrap(i, 1, self.vertexCount) + local k = slickmath.wrap(j, 1, self.vertexCount) + + local side = slickmath.direction( + self.preTransformedVertices[i], + self.preTransformedVertices[j], + self.preTransformedVertices[k]) + + if side ~= 0 then + winding = side + break + end + end + + if not winding then + return + end + + if winding <= 0 then + return + end + + local i = self.vertexCount + local j = 1 + + while i > j do + self.preTransformedVertices[i], self.preTransformedVertices[j] = self.preTransformedVertices[j], self.preTransformedVertices[i] + + i = i - 1 + j = j + 1 + end +end + +--- @protected +--- @param x1 number? +--- @param y1 number? +--- @param ... number? +function commonShape:addPoints(x1, y1, ...) + if not (x1 and y1) then + self:makeClockwise() + return + end + + self.vertexCount = self.vertexCount + 1 + local p = self.preTransformedVertices[self.vertexCount] + if not p then + p = point.new() + table.insert(self.preTransformedVertices, p) + end + p:init(x1, y1) + + self:addPoints(...) +end + +--- @protected +--- @param x number +--- @param y number +function commonShape:addNormal(x, y) + assert(not (x == 0 and y == 0)) + + self.normalCount = self.normalCount + 1 + + local normal = self.preTransformedNormals[self.normalCount] + if not normal then + normal = point.new() + self.preTransformedNormals[self.normalCount] = normal + end + + normal:init(x, y) + normal:normalize(normal) + + return normal +end + +--- @param p slick.geometry.point +--- @return boolean +function commonShape:inside(p) + local winding + for i = 1, self.vertexCount do + local side = slickmath.direction(self.vertices[i], self.vertices[i % self.vertexCount + 1], p) + + -- Point is collinear with edge. + -- We consider this inside. + if side == 0 then + return true + end + + if winding and side ~= winding then + return false + end + + if not winding and side ~= 0 then + winding = side + end + end + + return true +end + +local _cachedNormal = point.new() +function commonShape:buildNormals() + local direction = slickmath.direction(self.preTransformedVertices[1], self.preTransformedVertices[2], self.preTransformedVertices[3]) + assert(direction ~= 0, "polygon is degenerate") + if direction < 0 then + slicktable.reverse(self.preTransformedVertices) + end + + + for i = 1, self.vertexCount do + local j = i % self.vertexCount + 1 + + local p1 = self.preTransformedVertices[i] + local p2 = self.preTransformedVertices[j] + + p1:direction(p2, _cachedNormal) + + local n = self:addNormal(_cachedNormal.x, _cachedNormal.y) + n:right(n) + end +end + +--- @param transform slick.geometry.transform +function commonShape:transform(transform) + self.center:init(0, 0) + + for i = 1, self.vertexCount do + local preTransformedVertex = self.preTransformedVertices[i] + local postTransformedVertex = self.vertices[i] + if not postTransformedVertex then + postTransformedVertex = point.new() + self.vertices[i] = postTransformedVertex + end + postTransformedVertex:init(transform:transformPoint(preTransformedVertex.x, preTransformedVertex.y)) + + postTransformedVertex:add(self.center, self.center) + end + self.center:divideScalar(self.vertexCount, self.center) + + self.bounds:init(self.vertices[1].x, self.vertices[1].y, self.vertices[1].x, self.vertices[1].y) + for i = 2, self.vertexCount do + self.bounds:expand(self.vertices[i].x, self.vertices[i].y) + end + + for i = 1, self.normalCount do + local preTransformedNormal = self.preTransformedNormals[i] + local postTransformedNormal = self.normals[i] + if not postTransformedNormal then + postTransformedNormal = point.new() + self.normals[i] = postTransformedNormal + end + postTransformedNormal:init(transform:transformNormal(preTransformedNormal.x, preTransformedNormal.y)) + postTransformedNormal:normalize(postTransformedNormal) + end +end + +--- @param query slick.collision.shapeCollisionResolutionQuery +function commonShape:getAxes(query) + query:getAxes() +end + +--- @param query slick.collision.shapeCollisionResolutionQuery +--- @param axis slick.geometry.point +--- @param interval slick.collision.interval +--- @param offset slick.geometry.point? +function commonShape:project(query, axis, interval, offset) + query:project(axis, interval, offset) +end + +local _cachedDistanceSegment = segment.new() + +--- @param p slick.geometry.point +function commonShape:distance(p) + if self:inside(p) then + return 0 + end + + local minDistance = math.huge + + for i = 1, self.vertexCount do + local j = i % self.vertexCount + 1 + + _cachedDistanceSegment:init(self.vertices[i], self.vertices[j]) + local distanceSquared = _cachedDistanceSegment:distanceSquared(p) + if distanceSquared < minDistance then + minDistance = distanceSquared + end + end + + if minDistance < math.huge then + return math.sqrt(minDistance) + end + + return math.huge +end + +local _cachedRaycastHit = point.new() +local _cachedRaycastSegment = segment.new() + +--- @param r slick.geometry.ray +--- @param normal slick.geometry.point? +--- @return boolean, number?, number? +function commonShape:raycast(r, normal) + local bestDistance = math.huge + local hit, x, y + + for i = 1, self.vertexCount do + local j = i % self.vertexCount + 1 + + local a = self.vertices[i] + local b = self.vertices[j] + + _cachedRaycastSegment:init(a, b) + local h, hx, hy = r:hitSegment(_cachedRaycastSegment) + if h and hx and hy then + hit = true + + _cachedRaycastHit:init(hx, hy) + local distance = _cachedRaycastHit:distanceSquared(r.origin) + if distance < bestDistance then + bestDistance = distance + x, y = hx, hy + + if normal then + a:direction(b, normal) + end + end + end + end + + if normal and hit then + normal:normalize(normal) + normal:left(normal) + end + + return hit or false, x, y +end + +return commonShape diff --git a/game/love_src/lib/slick/collision/init.lua b/game/love_src/lib/slick/collision/init.lua new file mode 100644 index 0000000..74f9502 --- /dev/null +++ b/game/love_src/lib/slick/collision/init.lua @@ -0,0 +1,14 @@ +--- @alias slick.collision.shapeInterface slick.collision.commonShape + +--- @alias slick.collision.shape slick.collision.polygon | slick.collision.box | slick.collision.commonShape +--- @alias slick.collision.shapelike slick.collision.shape | slick.collision.shapeGroup | slick.collision.shapeInterface | slick.collision.polygonMesh + +local collision = { + quadTree = require("slick.collision.quadTree"), + quadTreeNode = require("slick.collision.quadTreeNode"), + quadTreeQuery = require("slick.collision.quadTreeQuery"), + polygon = require("slick.collision.polygon"), + shapeCollisionResolutionQuery = require("slick.collision.shapeCollisionResolutionQuery"), +} + +return collision diff --git a/game/love_src/lib/slick/collision/interval.lua b/game/love_src/lib/slick/collision/interval.lua new file mode 100644 index 0000000..e3be6f6 --- /dev/null +++ b/game/love_src/lib/slick/collision/interval.lua @@ -0,0 +1,119 @@ +local slicktable = require "slick.util.slicktable" + +--- @alias slick.collision.intervalIndex { +--- value: number, +--- index: number, +--- } + +--- @class slick.collision.interval +--- @field min number +--- @field max number +--- @field indexCount number +--- @field indices slick.collision.intervalIndex[] +--- @field private indicesCache slick.collision.intervalIndex[] +local interval = {} +local metatable = { __index = interval } + +--- @return slick.collision.interval +function interval.new() + return setmetatable({ indices = {}, indicesCache = {}, minIndex = 0, maxIndex = 0 }, metatable) +end + +function interval:init() + self.min = nil + self.max = nil + self.minIndex = 0 + self.maxIndex = 0 + slicktable.clear(self.indices) +end + +--- @return number +--- @return number +function interval:get() + return self.min or 0, self.max or 0 +end + +--- @param value number +function interval:update(value, index) + self.min = math.min(self.min or value, value) + self.max = math.max(self.max or value, value) + + local i = #self.indices + 1 + local indexInfo = self.indicesCache[i] + if not indexInfo then + indexInfo = {} + self.indicesCache[i] = indexInfo + end + + indexInfo.index = index + indexInfo.value = value + + table.insert(self.indices, indexInfo) +end + +local function _lessIntervalIndex(a, b) + return a.value < b.value +end + +function interval:sort() + table.sort(self.indices, _lessIntervalIndex) + + for i, indexInfo in ipairs(self.indices) do + if indexInfo.value >= self.min and self.minIndex == 0 then + self.minIndex = i + end + + if indexInfo.value <= self.max and indexInfo.value >= self.min then + self.maxIndex = i + end + end +end + +--- @param other slick.collision.interval +function interval:copy(other) + other.min = self.min + other.max = self.max + other.minIndex = self.minIndex + other.maxIndex = self.maxIndex + + slicktable.clear(other.indices) + for i, selfIndexInfo in ipairs(self.indices) do + local otherIndexInfo = other.indicesCache[i] + if not otherIndexInfo then + otherIndexInfo = {} + table.insert(other.indicesCache, otherIndexInfo) + end + + otherIndexInfo.index = selfIndexInfo.index + otherIndexInfo.value = selfIndexInfo.value + + table.insert(other.indices, otherIndexInfo) + end +end + +--- @param min number +--- @param max number +function interval:set(min, max) + assert(min <= max) + + self.min = min + self.max = max +end + +function interval:overlaps(other) + return not (self.min > other.max or other.min > self.max) +end + +function interval:distance(other) + if self:overlaps(other) then + return math.min(self.max, other.max) - math.max(self.min, other.min) + else + return 0 + end +end + +function interval:contains(other) + return other.min > self.min and other.max < self.max +end + +return interval diff --git a/game/love_src/lib/slick/collision/lineSegment.lua b/game/love_src/lib/slick/collision/lineSegment.lua new file mode 100644 index 0000000..579c561 --- /dev/null +++ b/game/love_src/lib/slick/collision/lineSegment.lua @@ -0,0 +1,94 @@ +local commonShape = require("slick.collision.commonShape") +local point = require("slick.geometry.point") +local segment = require("slick.geometry.segment") +local transform = require("slick.geometry.transform") +local slickmath = require("slick.util.slickmath") + +--- @class slick.collision.lineSegment: slick.collision.commonShape +--- @field segment slick.geometry.segment +--- @field private preTransformedSegment slick.geometry.segment +local lineSegment = setmetatable({}, { __index = commonShape }) +local metatable = { __index = lineSegment } + +--- @param entity slick.entity? +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +--- @return slick.collision.lineSegment +function lineSegment.new(entity, x1, y1, x2, y2) + local result = setmetatable(commonShape.new(entity), metatable) + + result.segment = segment.new() + result.preTransformedSegment = segment.new() + + --- @cast result slick.collision.lineSegment + result:init(x1, y1, x2, y2) + return result +end + +local _cachedInitNormal = point.new() + +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +function lineSegment:init(x1, y1, x2, y2) + commonShape.init(self) + + self.preTransformedSegment.a:init(x1, y1) + self.preTransformedSegment.b:init(x2, y2) + + if not self.preTransformedSegment.a:lessThanOrEqual(self.preTransformedSegment.b) then + self.preTransformedSegment.a, self.preTransformedSegment.b = self.preTransformedSegment.b, self.preTransformedSegment.a + end + + self:addPoints( + self.preTransformedSegment.a.x, + self.preTransformedSegment.a.y, + self.preTransformedSegment.b.x, + self.preTransformedSegment.b.y) + + self.preTransformedSegment.a:direction(self.preTransformedSegment.b, _cachedInitNormal) + _cachedInitNormal:normalize(_cachedInitNormal) + + self:addNormal(_cachedInitNormal.x, _cachedInitNormal.y) + + _cachedInitNormal:left(_cachedInitNormal) + self:addNormal(_cachedInitNormal.x, _cachedInitNormal.y) + + self:transform(transform.IDENTITY) + + assert(self.vertexCount == 2, "line segment must have 2 points") + assert(self.normalCount == 2, "line segment must have 2 normals") +end + +--- @param transform slick.geometry.transform +function lineSegment:transform(transform) + commonShape.transform(self, transform) + + self.segment.a:init(transform:transformPoint(self.preTransformedSegment.a.x, self.preTransformedSegment.a.y)) + self.segment.b:init(transform:transformPoint(self.preTransformedSegment.b.x, self.preTransformedSegment.b.y)) +end + +--- @param p slick.geometry.point +--- @return boolean +function lineSegment:inside(p) + local intersection, x, y = slickmath.intersection(self.vertices[1], self.vertices[2], p, p) + return intersection and not (x and y) +end + +--- @param p slick.geometry.point +--- @return number +function lineSegment:distance(p) + return self.segment:distance(p) +end + +--- @param r slick.geometry.ray +--- @return boolean, number?, number? +function lineSegment:raycast(r) + local h, x, y = r:hitSegment(self.segment) + return h, x, y +end + +return lineSegment diff --git a/game/love_src/lib/slick/collision/polygon.lua b/game/love_src/lib/slick/collision/polygon.lua new file mode 100644 index 0000000..98abc75 --- /dev/null +++ b/game/love_src/lib/slick/collision/polygon.lua @@ -0,0 +1,46 @@ +local commonShape = require("slick.collision.commonShape") +local transform = require("slick.geometry.transform") + +--- @class slick.collision.polygon: slick.collision.commonShape +local polygon = setmetatable({}, { __index = commonShape }) +local metatable = { __index = polygon } + +--- @param entity slick.entity | slick.cache +--- @param x1 number? +--- @param y1 number? +--- @param x2 number? +--- @param y2 number? +--- @param x3 number? +--- @param y3 number? +--- @param ... number +--- @return slick.collision.polygon +function polygon.new(entity, x1, y1, x2, y2, x3, y3, ...) + local result = setmetatable(commonShape.new(entity), metatable) + --- @cast result slick.collision.polygon + + if x1 and y1 and x2 and y2 and x3 and y3 then + result:init(x1, y1, x2, y2, x3, y3, ...) + end + + return result +end + +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +--- @param x3 number +--- @param y3 number +--- @param ... number +function polygon:init(x1, y1, x2, y2, x3, y3, ...) + commonShape.init(self) + + self:addPoints(x1, y1, x2, y2, x3, y3, ...) + self:buildNormals() + self:transform(transform.IDENTITY) + + assert(self.vertexCount >= 3, "polygon must have at least 3 points") + assert(self.vertexCount == self.normalCount, "polygon must have as many normals as vertices") +end + +return polygon diff --git a/game/love_src/lib/slick/collision/polygonMesh.lua b/game/love_src/lib/slick/collision/polygonMesh.lua new file mode 100644 index 0000000..949c5ce --- /dev/null +++ b/game/love_src/lib/slick/collision/polygonMesh.lua @@ -0,0 +1,77 @@ +local polygon = require ("slick.collision.polygon") + +--- @class slick.collision.polygonMesh +--- @field tag any +--- @field entity slick.entity +--- @field boundaries number[][] +--- @field polygons slick.collision.polygon[] +--- @field cleanupOptions slick.geometry.triangulation.delaunayCleanupOptions +--- @field triangulationOptions slick.geometry.triangulation.delaunayTriangulationOptions +local polygonMesh = {} +local metatable = { __index = polygonMesh } + +--- @param entity slick.entity +--- @param ... number[] +--- @return slick.collision.polygonMesh +function polygonMesh.new(entity, ...) + local result = setmetatable({ + entity = entity, + boundaries = { ... }, + polygons = {}, + cleanupOptions = {}, + triangulationOptions = { + refine = true, + interior = true, + exterior = false, + polygonization = true + } + }, metatable) + + return result +end + +--- @param triangulator slick.geometry.triangulation.delaunay +function polygonMesh:build(triangulator) + local points = {} + local edges = {} + + local totalPoints = 0 + for _, boundary in ipairs(self.boundaries) do + local numPoints = #boundary / 2 + + for i = 1, numPoints do + local j = (i - 1) * 2 + 1 + local x, y = unpack(boundary, j, j + 1) + + table.insert(points, x) + table.insert(points, y) + table.insert(edges, i + totalPoints) + table.insert(edges, i % numPoints + 1 + totalPoints) + end + + totalPoints = totalPoints + numPoints + end + + points, edges = triangulator:clean(points, edges, self.cleanupOptions) + local triangles, _, polygons = triangulator:triangulate(points, edges, self.triangulationOptions) + + local p = polygons or triangles + for _, vertices in ipairs(p) do + local outputVertices = {} + + for _, vertex in ipairs(vertices) do + local index = (vertex - 1) * 2 + 1 + local x, y = unpack(points, index, index + 1) + + table.insert(outputVertices, x) + table.insert(outputVertices, y) + end + + local instantiatedPolygon = polygon.new(self.entity, unpack(outputVertices)) + instantiatedPolygon.tag = self.tag + + table.insert(self.polygons, instantiatedPolygon) + end +end + +return polygonMesh diff --git a/game/love_src/lib/slick/collision/quadTree.lua b/game/love_src/lib/slick/collision/quadTree.lua new file mode 100644 index 0000000..fa8f5ce --- /dev/null +++ b/game/love_src/lib/slick/collision/quadTree.lua @@ -0,0 +1,283 @@ +local quadTreeNode = require("slick.collision.quadTreeNode") +local point = require("slick.geometry.point") +local rectangle = require("slick.geometry.rectangle") +local util = require("slick.util") +local pool = require("slick.util.pool") +local slicktable = require("slick.util.slicktable") + +--- @class slick.collision.quadTree +--- @field root slick.collision.quadTreeNode +--- @field expand boolean +--- @field private depth number? **internal** +--- @field private nodesPool slick.util.pool **internal** +--- @field private rectanglesPool slick.util.pool **internal** +--- @field private data table **internal** +--- @field bounds slick.geometry.rectangle +--- @field maxLevels number +--- @field maxData number +local quadTree = {} +local metatable = { __index = quadTree } + +--- @class slick.collision.quadTreeOptions +--- @field x number? +--- @field y number? +--- @field width number? +--- @field height number? +--- @field maxLevels number? +--- @field maxData number? +--- @field expand boolean? +local defaultOptions = { + x = -1024, + y = -1024, + width = 2048, + height = 2048, + maxLevels = 8, + maxData = 8, + expand = true +} + +--- @param options slick.collision.quadTreeOptions? +--- @return slick.collision.quadTree +function quadTree.new(options) + options = options or defaultOptions + + assert((options.width or defaultOptions.width) > 0, "width must be greater than 0") + assert((options.height or defaultOptions.height) > 0, "height must be greater than 0") + + local result = setmetatable({ + maxLevels = options.maxLevels or defaultOptions.maxLevels, + maxData = options.maxData or defaultOptions.maxData, + expand = options.expand == nil and defaultOptions.expand or not not options.expand, + depth = 1, + bounds = rectangle.new( + (options.x or defaultOptions.x), + (options.y or defaultOptions.y), + (options.x or defaultOptions.x) + (options.width or defaultOptions.width), + (options.y or defaultOptions.y) + (options.height or defaultOptions.height) + ), + nodesPool = pool.new(quadTreeNode), + rectanglesPool = pool.new(rectangle), + data = {} + }, metatable) + + result.root = result:_newNode(nil, result:left(), result:top(), result:right(), result:bottom()) + + return result +end + +--- Rebuilds the quad tree with the new options, preserving all existing data. +--- All options will be set to the new options, if set. +--- @param options slick.collision.quadTreeOptions +function quadTree:rebuild(options) + --- @diagnostic disable-next-line: invisible + self.root:_snip() -- this method is internal, not private + self.nodesPool:deallocate(self.root) + + assert(options.width == nil or options.width > 0, "width must be greater than 0") + assert(options.height == nil or options.height > 0, "height must be greater than 0") + + local x = options.x or self.bounds:left() + local y = options.y or self.bounds:top() + local width = options.width or self.bounds:width() + local height = options.height or self.bounds:height() + + self.maxLevels = options.maxLevels or self.maxLevels + self.maxData = options.maxData or self.maxData + + if options.expand ~= nil then + self.expand = options.expand + end + + self.bounds:init(x, y, x + width, y + height) + self.root = self:_newNode(nil, self.bounds:left(), self.bounds:top(), self.bounds:right(), self.bounds:bottom()) + + for data, r in pairs(self.data) do + self:_tryExpand(r) + self.root:insert(data, r) + end +end + +function quadTree:clear() + for data, r in pairs(self.data) do + self.root:remove(data, r) + self.rectanglesPool:deallocate(r) + end + + slicktable.clear(self.data) +end + +--- Returns the exact bounds of all data in the quad tree. +--- @return number x1 +--- @return number y1 +--- @return number x2 +--- @return number y2 +function quadTree:computeExactBounds() + local left, right, top, bottom + + for _, r in pairs(self.data) do + left = math.min(left or r:left(), r:left()) + right = math.max(right or r:right(), r:right()) + top = math.min(top or r:top(), r:top()) + bottom = math.max(bottom or r:bottom(), r:bottom()) + end + + return left, top, right, bottom +end + +--- Returns the maximum left of the quad tree. +--- @return number +function quadTree:left() + return self.bounds:left() +end + +--- Returns the maximum right of the quad tree. +--- @return number +function quadTree:right() + return self.bounds:right() +end + +--- Returns the maximum top of the quad tree. +--- @return number +function quadTree:top() + return self.bounds:top() +end + +--- Returns the maximum bottom of the quad tree. +--- @return number +function quadTree:bottom() + return self.bounds:bottom() +end + +--- Returns true if quad tree has `data` +--- @param data any +--- @return boolean +function quadTree:has(data) + return self.data[data] ~= nil +end + +--- @private +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +--- @return slick.geometry.rectangle +function quadTree:_newRectangle(x1, y1, x2, y2) + --- @type slick.geometry.rectangle + return self.rectanglesPool:allocate(x1, y1, x2, y2) +end + +--- @private +--- @param parent slick.collision.quadTreeNode? +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +--- @return slick.collision.quadTreeNode +function quadTree:_newNode(parent, x1, y1, x2, y2) + --- @type slick.collision.quadTreeNode + return self.nodesPool:allocate(self, parent, x1, y1, x2, y2) +end + +local _cachedInsertRectangle = rectangle.new() + +--- @overload fun(rectangle: slick.geometry.rectangle) +--- @overload fun(p: slick.geometry.rectangle, width: number?, height: number?) +--- @overload fun(p1: slick.geometry.point, p2: slick.geometry.point?) +--- @overload fun(x1: number?, y1: number?, x2: number?, y2: number?) +local function _getRectangle(a, b, c, d) + --- @type slick.geometry.rectangle + local r + if util.is(a, rectangle) then + --- @cast a slick.geometry.rectangle + r = a + elseif util.is(a, point) then + r = _cachedInsertRectangle + if b and util.is(b, point) then + r:init(a.x, a.y, b.x, b.y) + else + r:init(a.x, a.y, (a.x + (b or 0)), (a.y + (c or 0))) + end + else + r = _cachedInsertRectangle + + --- @cast a number + --- @cast b number + r:init(a, b, c, d) + end + + return r +end + +--- @private +--- @param r slick.geometry.rectangle +function quadTree:_tryExpand(r) + if r:overlaps(self.bounds) then + return + end + + if not self.expand then + error("shape is completely outside of quad tree and quad tree is not set to auto-expand", 3) + return + end + + while not r:overlaps(self.bounds) do + self.maxLevels = self.maxLevels + 1 + self.root = self.root:expand(r) + + self.bounds:init(self.root:left(), self.root:top(), self.root:right(), self.root:bottom()) + end +end + +--- Inserts `data` into the tree using the provided bounds. +--- +--- `data` must **not** be in the tree already; if it is, this method will raise an error. +--- Instead, use `quadTree.update` to move (or insert) an object. +--- @param data any +--- @overload fun(self: slick.collision.quadTree, data: any, rectangle: slick.geometry.rectangle) +--- @overload fun(self: slick.collision.quadTree, data: any, p: slick.geometry.rectangle, width: number?, height: number?) +--- @overload fun(self: slick.collision.quadTree, data: any, p1: slick.geometry.point, p2: slick.geometry.point?) +--- @overload fun(self: slick.collision.quadTree, data: any, x1: number?, y1: number?, x2: number?, y2: number?) +--- @return slick.collision.quadTree +function quadTree:insert(data, a, b, c, d) + assert(not self.data[data], "data needs to be removed before inserting or use update") + + local r = _getRectangle(a, b, c, d) + self:_tryExpand(r) + + self.data[data] = self:_newRectangle(r:left(), r:top(), r:right(), r:bottom()) + self.root:insert(data, r) +end + +--- Removes `data` from the tree. +--- +--- `data` **must** be in the tree already; if it is not, this method will raise an assert. +--- @param data any +function quadTree:remove(data) + assert(self.data[data] ~= nil) + + local r = self.data[data] + self.data[data] = nil + + self.root:remove(data, r) + self.rectanglesPool:deallocate(r) +end + +--- Updates `data` with new bounds. +--- +--- This essentially safely does a `remove` then `insert`. +--- @param data any +--- @overload fun(self: slick.collision.quadTree, data: any, rectangle: slick.geometry.rectangle) +--- @overload fun(self: slick.collision.quadTree, data: any, p: slick.geometry.rectangle, width: number?, height: number?) +--- @overload fun(self: slick.collision.quadTree, data: any, p1: slick.geometry.point, p2: slick.geometry.point?) +--- @overload fun(self: slick.collision.quadTree, data: any, x1: number?, y1: number?, x2: number?, y2: number?) +--- @see slick.collision.quadTree.insert +--- @see slick.collision.quadTree.remove +function quadTree:update(data, a, b, c, d) + if self.data[data] then + self:remove(data) + end + + self:insert(data, a, b, c, d) +end + +return quadTree diff --git a/game/love_src/lib/slick/collision/quadTreeNode.lua b/game/love_src/lib/slick/collision/quadTreeNode.lua new file mode 100644 index 0000000..2ee884a --- /dev/null +++ b/game/love_src/lib/slick/collision/quadTreeNode.lua @@ -0,0 +1,374 @@ +local rectangle = require("slick.geometry.rectangle") +local slicktable = require("slick.util.slicktable") + +--- @class slick.collision.quadTreeNode +--- @field tree slick.collision.quadTree +--- @field level number +--- @field count number +--- @field parent slick.collision.quadTreeNode? +--- @field children slick.collision.quadTreeNode[] +--- @field data any[] +--- @field private uniqueData table +--- @field bounds slick.geometry.rectangle +local quadTreeNode = {} +local metatable = { __index = quadTreeNode } + +--- @return slick.collision.quadTreeNode +function quadTreeNode.new() + return setmetatable({ + level = 0, + bounds = rectangle.new(), + count = 0, + children = {}, + data = {}, + uniqueData = {} + }, metatable) +end + +--- @param tree slick.collision.quadTree +--- @param parent slick.collision.quadTreeNode? +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +function quadTreeNode:init(tree, parent, x1, y1, x2, y2) + self.tree = tree + self.parent = parent + self.level = (parent and parent.level or 0) + 1 + self.depth = self.level + self.bounds:init(x1, y1, x2, y2) + + slicktable.clear(self.children) + slicktable.clear(self.data) + slicktable.clear(self.uniqueData) +end + +--- @private +--- @param parent slick.collision.quadTreeNode? +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +--- @return slick.collision.quadTreeNode +function quadTreeNode:_newNode(parent, x1, y1, x2, y2) + --- @diagnostic disable-next-line: invisible + return self.tree:_newNode(parent, x1, y1, x2, y2) +end + +--- Returns the maximum left of the quad tree. +--- @return number +function quadTreeNode:left() + return self.bounds:left() +end + +--- Returns the maximum right of the quad tree. +--- @return number +function quadTreeNode:right() + return self.bounds:right() +end + +--- Returns the maximum top of the quad tree. +--- @return number +function quadTreeNode:top() + return self.bounds:top() +end + +--- Returns the maximum bottom of the quad tree. +--- @return number +function quadTreeNode:bottom() + return self.bounds:bottom() +end + +--- @return number +function quadTreeNode:width() + return self.bounds:width() +end + +--- @return number +function quadTreeNode:height() + return self.bounds:height() +end + +--- @private +--- @param node slick.collision.quadTreeNode +function quadTreeNode._incrementLevel(node) + node.level = node.level + 1 +end + +--- Visits all nodes, including this one, calling `func` on the node. +--- @param func fun(node: slick.collision.quadTreeNode) +function quadTreeNode:visit(func) + func(self) + + for _, c in ipairs(self.children) do + c:visit(func) + end +end + +--- @private +--- @param func fun(node: slick.collision.quadTreeNode) +--- @param ignore slick.collision.quadTreeNode +function quadTreeNode:_visit(func, ignore) + if self == ignore then + return + end + + func(self) + + for _, c in ipairs(self.children) do + c:visit(func) + end +end + +--- Visits all parent nodes and their children, calling `func` on the node. +--- This method iterates from the parent node down, skipping the node `ascend` was called on. +--- @param func fun(node: slick.collision.quadTreeNode) +function quadTreeNode:ascend(func) + if self.tree.root ~= self then + self.tree.root:_visit(func, self) + end +end + +local _cachedQuadTreeNodeData = {} + +--- @param node slick.collision.quadTreeNode +local function _gatherData(node) + for _, data in ipairs(node.data) do + _cachedQuadTreeNodeData[data] = true + end +end + +--- Expands this node to fit 'bounds'. +--- @param bounds slick.geometry.rectangle +--- @return slick.collision.quadTreeNode +function quadTreeNode:expand(bounds) + assert(not self.parent, "can only expand root node") + assert(not bounds:overlaps(self.bounds), "bounds is within quad tree") + assert(bounds:left() > -math.huge and bounds:right() < math.huge, "x axis infinite") + assert(bounds:top() > -math.huge and bounds:bottom() < math.huge, "y axis infinite") + + slicktable.clear(_cachedQuadTreeNodeData) + self:visit(_gatherData) + + local halfWidth = self:width() / 2 + local halfHeight = self:height() / 2 + + local left, right = false, false + if bounds:right() < self:left() + halfWidth then + right = true + else + left = true + end + + local top, bottom = false, false + if bounds:bottom() < self:top() + halfHeight then + bottom = true + else + top = true + end + + local parent + local topLeft, topRight, bottomLeft, bottomRight + if top and left then + parent = self:_newNode(nil, self:left(), self:top(), self:right() + self:width(), self:bottom() + self:height()) + topLeft = self + topRight = self:_newNode(parent, self:right(), self:top(), self:right() + self:width(), self:bottom()) + bottomLeft = self:_newNode(parent, self:left(), self:bottom(), self:right(), self:bottom() + self:height()) + bottomRight = self:_newNode(parent, self:right(), self:bottom(), self:right() + self:width(), self:bottom() + self:height()) + elseif top and right then + parent = self:_newNode(nil, self:left() - self:width(), self:top(), self:right(), self:bottom() + self:height()) + topLeft = self:_newNode(parent, self:left() - self:width(), self:top(), self:left(), self:bottom()) + topRight = self + bottomLeft = self:_newNode(parent, self:left() - self:width(), self:bottom(), self:left(), self:bottom() + self:height()) + bottomRight = self:_newNode(parent, self:left(), self:bottom(), self:right(), self:bottom() + self:height()) + elseif bottom and left then + parent = self:_newNode(nil, self:left(), self:top() - self:height(), self:right() + self:width(), self:bottom()) + topLeft = self:_newNode(parent, self:left(), self:top() - self:height(), self:right(), self:top()) + topRight = self:_newNode(parent, self:right(), self:top() - self:height(), self:right() + self:width(), self:top()) + bottomLeft = self + bottomRight = self:_newNode(parent, self:right(), self:top(), self:right() + self:width(), self:bottom()) + elseif bottom and right then + parent = self:_newNode(nil, self:left() - self:width(), self:top() - self:height(), self:right(), self:bottom()) + topLeft = self:_newNode(parent, self:left() - self:width(), self:top() - self:height(), self:left(), self:top()) + topRight = self:_newNode(parent, self:left(), self:top() - self:height(), self:right(), self:top()) + bottomLeft = self:_newNode(parent, self:left() - self:width(), self:top(), self:left(), self:bottom()) + bottomRight = self + else + assert(false, "critical logic error") + end + + table.insert(parent.children, topLeft) + table.insert(parent.children, topRight) + table.insert(parent.children, bottomLeft) + table.insert(parent.children, bottomRight) + + for _, child in ipairs(parent.children) do + if child ~= self then + for data in pairs(_cachedQuadTreeNodeData) do + --- @diagnostic disable-next-line: invisible + local r = self.tree.data[data] + if r:overlaps(child.bounds) then + child:insert(data, r) + end + end + end + end + + self:visit(quadTreeNode._incrementLevel) + + parent.count = self.count + self.parent = parent + + return parent +end + +--- Inserts `data` given the `bounds` into this node. +--- +--- `data` must not already be added to this node. +--- @param data any +--- @param bounds slick.geometry.rectangle +function quadTreeNode:insert(data, bounds) + if (#self.children == 0 and #self.data < self.tree.maxData) or self.level >= self.tree.maxLevels then + assert(self.uniqueData[data] == nil, "data is already in node") + + self.uniqueData[data] = true + table.insert(self.data, data) + + self.count = self.count + 1 + + return + end + + if #self.children == 0 and #self.data >= self.tree.maxData then + self.count = 0 + self:split() + end + + for _, child in ipairs(self.children) do + if bounds:overlaps(child.bounds) then + child:insert(data, bounds) + end + end + + self.count = self.count + 1 +end + +--- @param data any +--- @param bounds slick.geometry.rectangle +function quadTreeNode:remove(data, bounds) + if #self.children > 0 then + for _, child in ipairs(self.children) do + if bounds:overlaps(child.bounds) then + child:remove(data, bounds) + end + end + + self.count = self.count - 1 + + if self.count <= self.tree.maxData then + self:collapse() + end + + return + end + + if not self.uniqueData[data] then + return + end + + for i, d in ipairs(self.data) do + if d == data then + table.remove(self.data, i) + self.count = self.count - 1 + + self.uniqueData[d] = nil + + return + end + end +end + +--- Splits the node into children nodes. +--- Moves any data from this node to the children nodes. +function quadTreeNode:split() + assert(#self.data >= self.tree.maxData, "cannot split; still has room") + + local width = self:right() - self:left() + local height = self:bottom() - self:top() + + local childWidth = width / 2 + local childHeight = height / 2 + + local topLeft = self:_newNode(self, self:left(), self:top(), self:left() + childWidth, self:top() + childHeight) + local topRight = self:_newNode(self, self:left() + childWidth, self:top(), self:right(), self:top() + childHeight) + local bottomLeft = self:_newNode(self, self:left(), self:top() + childHeight, self:left() + childWidth, self:bottom()) + local bottomRight = self:_newNode(self, self:left() + childWidth, self:top() + childHeight, self:right(), self:bottom()) + + table.insert(self.children, topLeft) + table.insert(self.children, topRight) + table.insert(self.children, bottomLeft) + table.insert(self.children, bottomRight) + + for _, data in ipairs(self.data) do + --- @diagnostic disable-next-line: invisible + local r = self.tree.data[data] + self:insert(data, r) + end + + slicktable.clear(self.data) + slicktable.clear(self.uniqueData) +end + +local _collectResult = { n = 0, unique = {}, data = {} } + +--- @private +--- @param node slick.collision.quadTreeNode +function quadTreeNode._collect(node) + for _, data in ipairs(node.data) do + if not _collectResult.unique[data] then + _collectResult.unique[data] = true + table.insert(_collectResult.data, data) + + _collectResult.n = _collectResult.n + 1 + end + end +end + +--- @package +--- Deallocates all children nodes. +function quadTreeNode:_snip() + for _, child in ipairs(self.children) do + child:_snip() + + --- @diagnostic disable-next-line: invisible + self.tree.nodesPool:deallocate(child) + end + + slicktable.clear(self.children) +end + +--- Collapses the children node into this node. +function quadTreeNode:collapse() + _collectResult.n = 0 + slicktable.clear(_collectResult.unique) + slicktable.clear(_collectResult.data) + + self:visit(self._collect) + + if _collectResult.n <= self.tree.maxData then + self:_snip() + + for _, data in ipairs(_collectResult.data) do + self.uniqueData[data] = true + table.insert(self.data, data) + end + + self.count = #self.data + + return true + end + + return false +end + +return quadTreeNode diff --git a/game/love_src/lib/slick/collision/quadTreeQuery.lua b/game/love_src/lib/slick/collision/quadTreeQuery.lua new file mode 100644 index 0000000..3f2f9cb --- /dev/null +++ b/game/love_src/lib/slick/collision/quadTreeQuery.lua @@ -0,0 +1,232 @@ +local point = require("slick.geometry.point") +local ray = require("slick.geometry.ray") +local rectangle = require("slick.geometry.rectangle") +local segment = require("slick.geometry.segment") +local util = require("slick.util") +local slicktable = require("slick.util.slicktable") +local slickmath = require("slick.util.slickmath") + +--- @class slick.collision.quadTreeQuery +--- @field tree slick.collision.quadTree +--- @field results any[] +--- @field bounds slick.geometry.rectangle +--- @field private data table +local quadTreeQuery = {} +local metatable = { __index = quadTreeQuery } + +--- @param tree slick.collision.quadTree +--- @return slick.collision.quadTreeQuery +function quadTreeQuery.new(tree) + return setmetatable({ + tree = tree, + results = {}, + bounds = rectangle.new(), + data = {} + }, metatable) +end + +--- @private +function quadTreeQuery:_beginQuery() + slicktable.clear(self.results) + slicktable.clear(self.data) + + self.bounds.topLeft:init(math.huge, math.huge) + self.bounds.bottomRight:init(-math.huge, -math.huge) +end + +--- @private +--- @param r slick.geometry.rectangle +function quadTreeQuery:_expand(r) + self.bounds.topLeft.x = math.min(self.bounds.topLeft.x, r.topLeft.x) + self.bounds.topLeft.y = math.min(self.bounds.topLeft.y, r.topLeft.y) + self.bounds.bottomRight.x = math.max(self.bounds.bottomRight.x, r.bottomRight.x) + self.bounds.bottomRight.y = math.max(self.bounds.bottomRight.y, r.bottomRight.y) +end + +--- @private +function quadTreeQuery:_endQuery() + if #self.results == 0 then + self.bounds:init(0, 0, 0, 0) + end +end + +--- @private +--- @param node slick.collision.quadTreeNode? +--- @param p slick.geometry.point +--- @param E number +function quadTreeQuery:_performPointQuery(node, p, E) + if not node then + node = self.tree.root + self:_beginQuery() + end + + if slickmath.withinRange(p.x, node:left(), node:right(), E) and slickmath.withinRange(p.y, node:top(), node:bottom(), E) then + if #node.children > 0 then + for _, c in ipairs(node.children) do + self:_performPointQuery(c, p, E) + end + else + for _, d in ipairs(node.data) do + --- @diagnostic disable-next-line: invisible + local r = self.tree.data[d] + if not self.data[d] and slickmath.withinRange(p.x, r:left(), r:right(), E) and slickmath.withinRange(p.y, r:top(), r:bottom(), E) then + table.insert(self.results, d) + self.data[d] = true + self:_expand(r) + end + end + end + end +end + +--- @private +--- @param node slick.collision.quadTreeNode? +--- @param r slick.geometry.rectangle +function quadTreeQuery:_performRectangleQuery(node, r) + if not node then + node = self.tree.root + self:_beginQuery() + end + + if r:overlaps(node.bounds) then + if #node.children > 0 then + for _, c in ipairs(node.children) do + self:_performRectangleQuery(c, r) + end + else + for _, d in ipairs(node.data) do + --- @diagnostic disable-next-line: invisible + local otherRectangle = self.tree.data[d] + + if not self.data[d] and r:overlaps(otherRectangle) then + table.insert(self.results, d) + self.data[d] = true + + self:_expand(r) + end + end + end + end +end + +local _cachedQuerySegment = segment.new() + +--- @private +--- @param node slick.collision.quadTreeNode? +--- @param s slick.geometry.segment +--- @param E number +function quadTreeQuery:_performSegmentQuery(node, s, E) + if not node then + node = self.tree.root + self:_beginQuery() + end + + local overlaps = (s:left() <= node:right() + E and s:right() + E >= node:left()) and + (s:top() <= node:bottom() + E and s:bottom() + E >= node:top()) + if overlaps then + if #node.children > 0 then + for _, c in ipairs(node.children) do + self:_performSegmentQuery(c, s, E) + end + else + for _, d in ipairs(node.data) do + --- @diagnostic disable-next-line: invisible + local r = self.tree.data[d] + + local intersection + + -- Top + _cachedQuerySegment.a:init(r:left(), r:top()) + _cachedQuerySegment.b:init(r:right(), r:top()) + intersection = slickmath.intersection(s.a, s.b, _cachedQuerySegment.a, _cachedQuerySegment.b, E) + + if not intersection then + -- Right + _cachedQuerySegment.a:init(r:right(), r:top()) + _cachedQuerySegment.b:init(r:right(), r:bottom()) + intersection = slickmath.intersection(s.a, s.b, _cachedQuerySegment.a, _cachedQuerySegment.b, E) + end + + if not intersection then + -- Bottom + _cachedQuerySegment.a:init(r:right(), r:bottom()) + _cachedQuerySegment.b:init(r:left(), r:bottom()) + intersection = slickmath.intersection(s.a, s.b, _cachedQuerySegment.a, _cachedQuerySegment.b, E) + end + + if not intersection then + -- Left + _cachedQuerySegment.a:init(r:left(), r:bottom()) + _cachedQuerySegment.b:init(r:left(), r:top()) + intersection = slickmath.intersection(s.a, s.b, _cachedQuerySegment.a, _cachedQuerySegment.b, E) + end + + if intersection or (r:inside(s.a) or r:inside(s.b)) then + table.insert(self.results, d) + self.data[d] = true + + self:_expand(r) + end + end + end + end +end + +--- @private +--- @param node slick.collision.quadTreeNode? +--- @param r slick.geometry.ray +function quadTreeQuery:_performRayQuery(node, r) + if not node then + node = self.tree.root + self:_beginQuery() + end + + if r:hitRectangle(node.bounds) then + if #node.children > 0 then + for _, c in ipairs(node.children) do + self:_performRayQuery(c, r) + end + else + for _, d in ipairs(node.data) do + --- @diagnostic disable-next-line: invisible + local bounds = self.tree.data[d] + + if r:hitRectangle(bounds) then + table.insert(self.results, d) + self.data[d] = true + + self:_expand(bounds) + end + end + end + end +end + +--- Performs a query against the quad tree with the provided shape. +--- @param shape slick.geometry.point | slick.geometry.rectangle | slick.geometry.segment | slick.geometry.ray +--- @param E number? +function quadTreeQuery:perform(shape, E) + E = E or 0 + + if util.is(shape, point) then + --- @cast shape slick.geometry.point + self:_performPointQuery(nil, shape, E) + elseif util.is(shape, rectangle) then + --- @cast shape slick.geometry.rectangle + self:_performRectangleQuery(nil, shape) + elseif util.is(shape, segment) then + --- @cast shape slick.geometry.segment + self:_performSegmentQuery(nil, shape, E) + elseif util.is(shape, ray) then + --- @cast shape slick.geometry.ray + self:_performRayQuery(nil, shape) + else + error("unhandled shape type in query; expected point, rectangle, segment, or ray", 2) + end + + self:_endQuery() + + return #self.results > 0 +end + +return quadTreeQuery diff --git a/game/love_src/lib/slick/collision/shapeCollisionResolutionQuery.lua b/game/love_src/lib/slick/collision/shapeCollisionResolutionQuery.lua new file mode 100644 index 0000000..a11910a --- /dev/null +++ b/game/love_src/lib/slick/collision/shapeCollisionResolutionQuery.lua @@ -0,0 +1,909 @@ +local interval = require "slick.collision.interval" +local point = require "slick.geometry.point" +local segment = require "slick.geometry.segment" +local slickmath = require "slick.util.slickmath" +local slicktable = require "slick.util.slicktable" + +local SIDE_NONE = 0 +local SIDE_LEFT = -1 +local SIDE_RIGHT = 1 + +--- @alias slick.collision.shapeCollisionResolutionQueryAxis { +--- parent: slick.collision.shapeCollisionResolutionQueryShape, +--- normal: slick.geometry.point, +--- segment: slick.geometry.segment, +--- } + +--- @alias slick.collision.shapeCollisionResolutionQueryShape { +--- shape: slick.collision.shapeInterface, +--- offset: slick.geometry.point, +--- axesCount: number, +--- axes: slick.collision.shapeCollisionResolutionQueryAxis[], +--- currentInterval: slick.collision.interval, +--- minInterval: slick.collision.interval, +--- } + +--- @class slick.collision.shapeCollisionResolutionQuery +--- @field epsilon number +--- @field collision boolean +--- @field normal slick.geometry.point +--- @field currentNormal slick.geometry.point +--- @field otherNormal slick.geometry.point +--- @field depth number +--- @field private otherDepth number +--- @field currentDepth number +--- @field time number +--- @field currentOffset slick.geometry.point +--- @field otherOffset slick.geometry.point +--- @field contactPointsCount number +--- @field contactPoints slick.geometry.point[] +--- @field normals slick.geometry.point[] +--- @field alternateNormals slick.geometry.point[] +--- @field private allNormals slick.geometry.point[] +--- @field private allNormalsCount number +--- @field private depths number[] +--- @field private normalsShape slick.collision.shapeCollisionResolutionQueryShape[] +--- @field private firstTime number +--- @field private lastTime number +--- @field private currentShape slick.collision.shapeCollisionResolutionQueryShape +--- @field private otherShape slick.collision.shapeCollisionResolutionQueryShape +--- @field private axis slick.collision.shapeCollisionResolutionQueryAxis? +--- @field private otherAxis slick.collision.shapeCollisionResolutionQueryAxis? +--- @field private currentAxis slick.collision.shapeCollisionResolutionQueryAxis? +--- @field private relativeDirection slick.geometry.point +local shapeCollisionResolutionQuery = {} +local metatable = { __index = shapeCollisionResolutionQuery } + +--- @return slick.collision.shapeCollisionResolutionQueryShape +local function _newQueryShape() + return { + offset = point.new(), + axesCount = 0, + axes = {}, + currentInterval = interval.new(), + minInterval = interval.new(), + } +end + +--- @param E number? +--- @return slick.collision.shapeCollisionResolutionQuery +function shapeCollisionResolutionQuery.new(E) + return setmetatable({ + epsilon = E or slickmath.EPSILON, + collision = false, + depth = 0, + currentDepth = 0, + otherDepth = 0, + normal = point.new(), + currentNormal = point.new(), + otherNormal = point.new(), + time = 0, + firstTime = 0, + lastTime = 0, + currentOffset = point.new(), + otherOffset = point.new(), + contactPointsCount = 0, + contactPoints = { point.new() }, + normals = {}, + alternateNormals = {}, + depths = {}, + normalsShape = {}, + allNormals = {}, + allNormalsCount = 0, + currentShape = _newQueryShape(), + otherShape = _newQueryShape(), + relativeDirection = point.new() + }, metatable) +end + +--- @return slick.collision.shapeInterface +function shapeCollisionResolutionQuery:getSelfShape() + return self.currentShape.shape +end + +--- @return slick.collision.shapeInterface +function shapeCollisionResolutionQuery:getOtherShape() + return self.otherShape.shape +end + +--- @private +function shapeCollisionResolutionQuery:_swapShapes() + self.otherShape, self.currentShape = self.currentShape, self.otherShape +end + +function shapeCollisionResolutionQuery:reset() + self.collision = false + self.depth = 0 + self.otherDepth = 0 + self.currentDepth = 0 + self.time = math.huge + self.currentOffset:init(0, 0) + self.otherOffset:init(0, 0) + self.normal:init(0, 0) + self.otherNormal:init(0, 0) + self.currentNormal:init(0, 0) + self.contactPointsCount = 0 + self.allNormalsCount = 0 + + slicktable.clear(self.normals) + slicktable.clear(self.alternateNormals) +end + +--- @private +function shapeCollisionResolutionQuery:_beginQuery() + self.currentShape.axesCount = 0 + self.otherShape.axesCount = 0 + self.axis = nil + self.otherAxis = nil + self.currentAxis = nil + + self.collision = false + self.depth = 0 + self.otherDepth = 0 + self.currentDepth = 0 + self.firstTime = -math.huge + self.lastTime = math.huge + self.currentOffset:init(0, 0) + self.otherOffset:init(0, 0) + self.normal:init(0, 0) + self.otherNormal:init(0, 0) + self.currentNormal:init(0, 0) + self.contactPointsCount = 0 + self.relativeDirection:init(0, 0) + self.allNormalsCount = 0 +end + +function shapeCollisionResolutionQuery:addAxis() + self.currentShape.axesCount = self.currentShape.axesCount + 1 + local index = self.currentShape.axesCount + local axis = self.currentShape.axes[index] + if not axis then + axis = { parent = self.currentShape, normal = point.new(), segment = segment.new() } + self.currentShape.axes[index] = axis + end + + return axis +end + +local _cachedOtherSegment = segment.new() +local _cachedCurrentPoint = point.new() +local _cachedOtherNormal = point.new() + +--- @private +--- @param a slick.collision.shapeCollisionResolutionQueryShape +--- @param b slick.collision.shapeCollisionResolutionQueryShape +--- @param aOffset slick.geometry.point +--- @param bOffset slick.geometry.point +--- @param scalar number +--- @return boolean +function shapeCollisionResolutionQuery:_isShapeMovingAwayFromShape(a, b, aOffset, bOffset, scalar) + local currentVertexCount = a.shape.vertexCount + local currentVertices = a.shape.vertices + + local otherVertexCount = b.shape.vertexCount + local otherVertices = b.shape.vertices + + for i = 1, otherVertexCount do + local j = slickmath.wrap(i, 1, otherVertexCount) + + otherVertices[i]:add(bOffset, _cachedOtherSegment.a) + otherVertices[j]:add(bOffset, _cachedOtherSegment.b) + + _cachedOtherSegment.a:direction(_cachedOtherSegment.b, _cachedOtherNormal) + _cachedOtherNormal:normalize(_cachedOtherNormal) + _cachedOtherNormal:left(_cachedOtherNormal) + + local sameSide = true + for k = 1, currentVertexCount do + currentVertices[k]:add(aOffset, _cachedCurrentPoint) + + local direction = slickmath.direction(_cachedOtherSegment.a, _cachedOtherSegment.b, _cachedCurrentPoint, self.epsilon) + if direction < 0 then + sameSide = false + break + end + end + + if sameSide then + if (scalar * self.relativeDirection:dot(_cachedOtherNormal)) >= -self.epsilon then + return true + end + end + end + + return false +end + +local _lineSegmentDirection = point.new() +local _lineSegmentRelativePosition = point.new() +local _lineSegmentShapePosition = point.new() +local _lineSegmentShapeVertexPosition = point.new() + +--- @private +--- @param shape slick.collision.shapeInterface +--- @param offset slick.geometry.point +--- @param direction slick.geometry.point +--- @param point slick.geometry.point +--- @param fun fun(number, number): number +--- @return number +function shapeCollisionResolutionQuery:_dotShapeSegment(shape, offset, direction, point, fun) + offset:sub(point, _lineSegmentRelativePosition) + + local dot + for i = 1, shape.vertexCount do + local vertex = shape.vertices[i] + vertex:add(_lineSegmentRelativePosition, _lineSegmentShapeVertexPosition) + local d = direction:dot(_lineSegmentShapeVertexPosition) + dot = fun(d, dot or d) + end + + return dot +end + +--- @private +--- @param lineSegmentShape slick.collision.shapeCollisionResolutionQueryShape +--- @param otherShape slick.collision.shapeCollisionResolutionQueryShape +--- @param otherOffset slick.geometry.point +--- @param worldOffset slick.geometry.point +function shapeCollisionResolutionQuery:_correctLineSegmentNormals(lineSegmentShape, otherShape, otherOffset, worldOffset) + assert(lineSegmentShape.shape.vertexCount == 2, "shape must be line segment") + + worldOffset:add(otherOffset, _lineSegmentShapePosition) + + local a = lineSegmentShape.shape.vertices[1] + local b = lineSegmentShape.shape.vertices[2] + a:direction(b, _lineSegmentDirection) + + -- Check if we're behind a (the beginning of the segment) or in front of b (the end of the segment) + local dotA = self:_dotShapeSegment(otherShape.shape, _lineSegmentShapePosition, _lineSegmentDirection, a, math.max) + local dotB = self:_dotShapeSegment(otherShape.shape, _lineSegmentShapePosition, _lineSegmentDirection, b, math.min) + + local normal, depth + if lineSegmentShape == self.currentShape then + normal = self.currentNormal + depth = self.currentDepth + elseif lineSegmentShape == self.otherShape then + normal = self.otherNormal + depth = self.otherDepth + end + assert(normal and depth, "incorrect shape; couldn't determine normal") + + if not (dotA > 0 and dotB < 0) then + -- If we're not to the side of the segment, we need to swap the normal. + self:_addNormal(depth, lineSegmentShape, normal.x, normal.y) + normal:left(normal) + + if dotA >= 0 and dotB >= 0 then + normal:negate(normal) + end + else + otherShape.shape.center:add(_lineSegmentShapePosition, _lineSegmentShapePosition) + local side = slickmath.direction( + lineSegmentShape.shape.vertices[1], + lineSegmentShape.shape.vertices[2], + _lineSegmentShapePosition) + + if side == 0 then + self:_addNormal(depth, lineSegmentShape, -normal.x, -normal.y) + else + normal:multiplyScalar(side, normal) + end + end + + normal:negate(normal) + if normal == self.otherNormal then + self.normal:init(normal.x, normal.y) + end +end + +local _cachedRelativeVelocity = point.new() +local _cachedSelfFutureCenter = point.new() +local _cachedSelfVelocityMinusOffset = point.new() +local _cachedDirection = point.new() + +local _cachedSegmentA = segment.new() +local _cachedSegmentB = segment.new() + +--- @private +--- @param selfShape slick.collision.commonShape +--- @param otherShape slick.collision.commonShape +--- @param selfOffset slick.geometry.point +--- @param otherOffset slick.geometry.point +--- @param selfVelocity slick.geometry.point +--- @param otherVelocity slick.geometry.point +function shapeCollisionResolutionQuery:_performPolygonPolygonProjection(selfShape, otherShape, selfOffset, otherOffset, selfVelocity, otherVelocity) + self.currentShape.shape = selfShape + self.currentShape.offset:init(selfOffset.x, selfOffset.y) + self.otherShape.shape = otherShape + self.otherShape.offset:init(otherOffset.x, otherOffset.y) + + self.currentShape.shape:getAxes(self) + self:_swapShapes() + self.currentShape.shape:getAxes(self) + self:_swapShapes() + + otherVelocity:sub(selfVelocity, _cachedRelativeVelocity) + selfVelocity:add(selfShape.center, _cachedSelfFutureCenter) + + selfVelocity:sub(selfOffset, _cachedSelfVelocityMinusOffset) + + _cachedRelativeVelocity:normalize(self.relativeDirection) + self.relativeDirection:negate(self.relativeDirection) + + self.depth = math.huge + self.otherDepth = math.huge + self.currentDepth = math.huge + + local hit = true + local side = SIDE_NONE + + local currentInterval = self.currentShape.currentInterval + local otherInterval = self.otherShape.currentInterval + + local isTouching = true + if _cachedRelativeVelocity:lengthSquared() == 0 then + for i = 1, self.currentShape.axesCount + self.otherShape.axesCount do + local axis = self:_getAxis(i) + + currentInterval:init() + otherInterval:init() + + self:_handleAxis(axis) + + if self:_compareIntervals(axis) then + hit = true + else + hit = false + break + end + end + + if not hit then + isTouching = false + end + else + for i = 1, self.currentShape.axesCount + self.otherShape.axesCount do + local axis = self:_getAxis(i) + + currentInterval:init() + otherInterval:init() + + local willHit, futureSide = self:_handleTunnelAxis(axis, _cachedRelativeVelocity) + if not willHit then + hit = false + + if not isTouching then + break + end + end + + if isTouching and not self:_compareIntervals(axis) then + isTouching = false + + if not hit then + break + end + end + + if futureSide then + currentInterval:copy(self.currentShape.minInterval) + otherInterval:copy(self.otherShape.minInterval) + + side = futureSide + end + end + end + + if hit and (self.depth == math.huge or self.depth < self.epsilon) and _cachedRelativeVelocity:lengthSquared() > 0 then + hit = not ( + self:_isShapeMovingAwayFromShape(self.currentShape, self.otherShape, selfOffset, otherOffset, 1) or + self:_isShapeMovingAwayFromShape(self.otherShape, self.currentShape, otherOffset, selfOffset, -1)) + end + + if self.firstTime > 1 then + hit = false + end + + if not hit and self.depth < self.epsilon then + self.depth = 0 + end + + if self.firstTime == -math.huge and self.lastTime >= 0 and self.lastTime <= 1 then + self.firstTime = 0 + end + + if hit and isTouching then + self.currentShape.shape.center:direction(self.otherShape.shape.center, _cachedDirection) + _cachedDirection:normalize(_cachedDirection) + + if _cachedDirection:dot(self.normal) > 0 then + self.normal:negate(self.normal) + end + + if _cachedDirection:dot(self.currentNormal) < 0 then + self.currentNormal:negate(self.currentNormal) + end + end + + if self.firstTime <= 0 and self.depth == 0 and _cachedRelativeVelocity:lengthSquared() == 0 then + hit = false + end + + if not hit then + self:_clear() + return + end + + self.time = math.max(self.firstTime, 0) + + if (self.firstTime == 0 and self.lastTime <= 1) or (self.firstTime == -math.huge and self.lastTime == math.huge) then + self.normal:multiplyScalar(self.depth, self.currentOffset) + self.normal:multiplyScalar(-self.depth, self.otherOffset) + else + selfVelocity:multiplyScalar(self.time, self.currentOffset) + otherVelocity:multiplyScalar(self.time, self.otherOffset) + end + + if self.time > 0 and self.currentOffset:lengthSquared() == 0 then + self.time = 0 + self.depth = 0 + end + + if side == SIDE_RIGHT or side == SIDE_LEFT then + if not isTouching then + self.allNormalsCount = 0 + end + + local currentInterval = self.currentShape.minInterval + local otherInterval = self.otherShape.minInterval + + currentInterval:sort() + otherInterval:sort() + + if side == SIDE_LEFT then + local selfA = currentInterval.indices[currentInterval.minIndex].index + local selfB = currentInterval.indices[currentInterval.minIndex + 1].index + if ((selfA == 1 or selfB == 1) and (selfA == selfShape.vertexCount or selfB == selfShape.vertexCount)) then + selfA, selfB = math.max(selfA, selfB), math.min(selfA, selfB) + else + selfA, selfB = math.min(selfA, selfB), math.max(selfA, selfB) + end + + selfShape.vertices[selfA]:add(self.currentOffset, _cachedSegmentA.a) + selfShape.vertices[selfB]:add(self.currentOffset, _cachedSegmentA.b) + + _cachedSegmentA.a:direction(_cachedSegmentA.b, self.currentNormal) + self.currentNormal:normalize(self.currentNormal) + self.currentNormal:left(self.currentNormal) + + local otherA = otherInterval.indices[otherInterval.maxIndex].index + local otherB = otherInterval.indices[otherInterval.maxIndex - 1].index + if ((otherA == 1 or otherB == 1) and (otherA == otherShape.vertexCount or otherB == otherShape.vertexCount)) then + otherA, otherB = math.max(otherA, otherB), math.min(otherA, otherB) + else + otherA, otherB = math.min(otherA, otherB), math.max(otherA, otherB) + end + + otherShape.vertices[otherA]:add(self.otherOffset, _cachedSegmentB.a) + otherShape.vertices[otherB]:add(self.otherOffset, _cachedSegmentB.b) + + _cachedSegmentB.a:direction(_cachedSegmentB.b, self.otherNormal) + self.otherNormal:normalize(self.otherNormal) + self.otherNormal:left(self.otherNormal) + elseif side == SIDE_RIGHT then + local selfA = currentInterval.indices[currentInterval.maxIndex].index + local selfB = currentInterval.indices[currentInterval.maxIndex - 1].index + if ((selfA == 1 or selfB == 1) and (selfA == selfShape.vertexCount or selfB == selfShape.vertexCount)) then + selfA, selfB = math.max(selfA, selfB), math.min(selfA, selfB) + else + selfA, selfB = math.min(selfA, selfB), math.max(selfA, selfB) + end + + selfShape.vertices[selfA]:add(self.currentOffset, _cachedSegmentA.a) + selfShape.vertices[selfB]:add(self.currentOffset, _cachedSegmentA.b) + + _cachedSegmentA.a:direction(_cachedSegmentA.b, self.currentNormal) + self.currentNormal:normalize(self.currentNormal) + self.currentNormal:left(self.currentNormal) + + local otherA = otherInterval.indices[otherInterval.minIndex].index + local otherB = otherInterval.indices[otherInterval.minIndex + 1].index + if ((otherA == 1 or otherB == 1) and (otherA == otherShape.vertexCount or otherB == otherShape.vertexCount)) then + otherA, otherB = math.max(otherA, otherB), math.min(otherA, otherB) + else + otherA, otherB = math.min(otherA, otherB), math.max(otherA, otherB) + end + + otherShape.vertices[otherA]:add(self.otherOffset, _cachedSegmentB.a) + otherShape.vertices[otherB]:add(self.otherOffset, _cachedSegmentB.b) + + _cachedSegmentB.a:direction(_cachedSegmentB.b, self.otherNormal) + self.otherNormal:normalize(self.otherNormal) + self.otherNormal:left(self.otherNormal) + end + + self.normal:init(self.otherNormal.x, self.otherNormal.y) + + local intersection, x, y + if _cachedSegmentA:overlap(_cachedSegmentB) then + intersection, x, y = slickmath.intersection(_cachedSegmentA.a, _cachedSegmentA.b, _cachedSegmentB.a, _cachedSegmentB.b, self.epsilon) + + if intersection and not (x and y) then + intersection = slickmath.intersection(_cachedSegmentA.a, _cachedSegmentA.a, _cachedSegmentB.a, _cachedSegmentB.b, self.epsilon) + if intersection then + self:_addContactPoint(_cachedSegmentA.a.x, _cachedSegmentB.a.y) + end + + intersection = slickmath.intersection(_cachedSegmentA.b, _cachedSegmentA.b, _cachedSegmentB.a, _cachedSegmentB.b, self.epsilon) + if intersection then + self:_addContactPoint(_cachedSegmentA.b.x, _cachedSegmentB.b.y) + end + + intersection = slickmath.intersection(_cachedSegmentB.a, _cachedSegmentB.a, _cachedSegmentA.a, _cachedSegmentA.b, self.epsilon) + if intersection then + self:_addContactPoint(_cachedSegmentB.a.x, _cachedSegmentB.a.y) + end + + intersection = slickmath.intersection(_cachedSegmentB.b, _cachedSegmentB.b, _cachedSegmentA.a, _cachedSegmentA.b, self.epsilon) + if intersection then + self:_addContactPoint(_cachedSegmentB.b.x, _cachedSegmentB.b.y) + end + elseif intersection and x and y then + self:_addContactPoint(x, y) + end + end + elseif side == SIDE_NONE then + for j = 1, selfShape.vertexCount do + _cachedSegmentA:init(selfShape.vertices[j], selfShape.vertices[j % selfShape.vertexCount + 1]) + + if self.time > 0 then + _cachedSegmentA.a:add(self.currentOffset, _cachedSegmentA.a) + _cachedSegmentA.b:add(self.currentOffset, _cachedSegmentA.b) + end + + for k = 1, otherShape.vertexCount do + _cachedSegmentB:init(otherShape.vertices[k], otherShape.vertices[k % otherShape.vertexCount + 1]) + + if self.time > 0 then + _cachedSegmentB.a:add(self.otherOffset, _cachedSegmentB.a) + _cachedSegmentB.b:add(self.otherOffset, _cachedSegmentB.b) + end + + if _cachedSegmentA:overlap(_cachedSegmentB) then + local intersection, x, y = slickmath.intersection(_cachedSegmentA.a, _cachedSegmentA.b, _cachedSegmentB.a, _cachedSegmentB.b, self.epsilon) + if intersection and x and y then + self:_addContactPoint(x, y) + end + end + end + end + end + + self.time = math.max(self.firstTime, 0) + self.collision = true + + if self.depth == math.huge then + self.depth = 0 + end + + if self.currentDepth == math.huge then + self.currentDepth = 0 + end + + if self.currentShape.shape.vertexCount == 2 then + self:_correctLineSegmentNormals(self.currentShape, self.otherShape, self.otherOffset, otherOffset) + end + + if self.otherShape.shape.vertexCount == 2 then + self:_correctLineSegmentNormals(self.otherShape, self.currentShape, self.currentOffset, selfOffset) + end +end + +--- @private +--- @param index number +--- @return slick.collision.shapeCollisionResolutionQueryAxis +function shapeCollisionResolutionQuery:_getAxis(index) + local axis + if index <= self.currentShape.axesCount then + axis = self.currentShape.axes[index] + else + axis = self.otherShape.axes[index - self.currentShape.axesCount] + end + + return axis +end + +--- @private +--- @param depth number +--- @param shape slick.collision.shapeCollisionResolutionQueryShape +--- @param x number +--- @param y number +function shapeCollisionResolutionQuery:_addNormal(depth, shape, x, y) + local nextCount = self.allNormalsCount + 1 + local normal = self.allNormals[nextCount] + if not normal then + normal = point.new() + self.allNormals[nextCount] = normal + end + + normal:init(x, y) + normal:round(normal, self.epsilon) + normal:normalize(normal) + + self.depths[nextCount] = depth + self.normalsShape[nextCount] = shape + self.allNormalsCount = nextCount +end + +--- @private +--- @param x number +--- @param y number +function shapeCollisionResolutionQuery:_addContactPoint(x, y) + local nextCount = self.contactPointsCount + 1 + local contactPoint = self.contactPoints[nextCount] + if not contactPoint then + contactPoint = point.new() + self.contactPoints[nextCount] = contactPoint + end + + contactPoint:init(x, y) + + for i = 1, self.contactPointsCount do + if contactPoint:distanceSquared(self.contactPoints[i]) < self.epsilon ^ 2 then + return + end + end + self.contactPointsCount = nextCount +end + +local _intervalAxisNormal = point.new() + +--- @private +--- @param axis slick.collision.shapeCollisionResolutionQueryAxis +--- @return boolean +function shapeCollisionResolutionQuery:_compareIntervals(axis) + local currentInterval = self.currentShape.currentInterval + local otherInterval = self.otherShape.currentInterval + + if not currentInterval:overlaps(otherInterval) then + return false + end + + local depth = currentInterval:distance(otherInterval) + local negate = false + if currentInterval:contains(otherInterval) or otherInterval:contains(currentInterval) then + local max = math.abs(currentInterval.max - otherInterval.max) + local min = math.abs(currentInterval.min - otherInterval.min) + + if max > min then + negate = true + depth = depth + min + else + depth = depth + max + end + end + + _intervalAxisNormal:init(axis.normal.x, axis.normal.y) + if negate then + _intervalAxisNormal:negate(_intervalAxisNormal) + end + + if axis.parent == self.otherShape and slickmath.less(depth, self.otherDepth, self.epsilon) then + if depth < self.otherDepth then + self.otherDepth = depth + self.otherNormal:init(_intervalAxisNormal.x, _intervalAxisNormal.y) + self.otherAxis = axis + end + + self:_addNormal(depth, self.otherShape, _intervalAxisNormal.x, _intervalAxisNormal.y) + end + + if axis.parent == self.currentShape and slickmath.less(depth, self.currentDepth, self.epsilon) then + if depth < self.currentDepth then + self.currentDepth = depth + self.currentNormal:init(_intervalAxisNormal.x, _intervalAxisNormal.y) + self.currentAxis = axis + end + + self:_addNormal(depth, self.currentShape, _intervalAxisNormal.x, _intervalAxisNormal.y) + end + + if depth < self.depth then + self.depth = depth + self.normal:init(_intervalAxisNormal.x, _intervalAxisNormal.y) + self.axis = axis + end + + return true +end + +--- @param selfShape slick.collision.shapeInterface +--- @param otherShape slick.collision.shapeInterface +--- @param selfOffset slick.geometry.point +--- @param otherOffset slick.geometry.point +--- @param selfVelocity slick.geometry.point +--- @param otherVelocity slick.geometry.point +function shapeCollisionResolutionQuery:performProjection(selfShape, otherShape, selfOffset, otherOffset, selfVelocity, otherVelocity) + self:_beginQuery() + self:_performPolygonPolygonProjection(selfShape, otherShape, selfOffset, otherOffset, selfVelocity, otherVelocity) + + if self.collision then + self.normal:round(self.normal, self.epsilon) + self.normal:normalize(self.normal) + self.currentNormal:round(self.currentNormal, self.epsilon) + self.currentNormal:normalize(self.currentNormal) + + slicktable.clear(self.normals) + slicktable.clear(self.alternateNormals) + + table.insert(self.normals, self.normal) + table.insert(self.alternateNormals, self.currentNormal) + + for i = 1, self.allNormalsCount do + --- @type slick.geometry.point[]? + local normals, depth + if self.normalsShape[i] == self.otherShape then + normals = self.normals + depth = self.otherDepth + elseif self.normalsShape[i] == self.currentShape then + normals = self.alternateNormals + depth = self.currentDepth + end + + if normals and slickmath.equal(depth, self.depths[i], self.epsilon) then + local normal = self.allNormals[i] + + local hasNormal = false + for _, otherNormal in ipairs(normals) do + if otherNormal.x == normal.x and otherNormal.y == normal.y then + hasNormal = true + break + end + end + + if not hasNormal then + table.insert(normals, normal) + end + end + end + end + + return self.collision +end + +--- @private +function shapeCollisionResolutionQuery:_clear() + self.depth = 0 + self.time = 0 + self.normal:init(0, 0) + self.contactPointsCount = 0 +end + +function shapeCollisionResolutionQuery:_handleAxis(axis) + self.currentShape.shape:project(self, axis.normal, self.currentShape.currentInterval, self.currentShape.offset) + self:_swapShapes() + self.currentShape.shape:project(self, axis.normal, self.currentShape.currentInterval, self.currentShape.offset) + self:_swapShapes() +end + +--- @param axis slick.collision.shapeCollisionResolutionQueryAxis +--- @param velocity slick.geometry.point +--- @return boolean, -1 | 0 | 1 | nil +function shapeCollisionResolutionQuery:_handleTunnelAxis(axis, velocity) + local speed = velocity:dot(axis.normal) + + self.currentShape.shape:project(self, axis.normal, self.currentShape.currentInterval, self.currentShape.offset) + self:_swapShapes() + self.currentShape.shape:project(self, axis.normal, self.currentShape.currentInterval, self.currentShape.offset) + self:_swapShapes() + + local selfInterval = self.currentShape.currentInterval + local otherInterval = self.otherShape.currentInterval + + local side + if otherInterval.max < selfInterval.min then + if speed <= 0 then + return false, nil + end + + local u = (selfInterval.min - otherInterval.max) / speed + if u > self.firstTime then + side = SIDE_LEFT + self.firstTime = u + end + + local v = (selfInterval.max - otherInterval.min) / speed + self.lastTime = math.min(self.lastTime, v) + + if self.firstTime > self.lastTime then + return false, nil + end + elseif selfInterval.max < otherInterval.min then + if speed >= 0 then + return false, nil + end + + local u = (selfInterval.max - otherInterval.min) / speed + if u > self.firstTime then + side = SIDE_RIGHT + self.firstTime = u + end + + local v = (selfInterval.min - otherInterval.max) / speed + self.lastTime = math.min(self.lastTime, v) + else + if speed > 0 then + local t = (selfInterval.max - otherInterval.min) / speed + self.lastTime = math.min(self.lastTime, t) + + if self.firstTime > self.lastTime then + return false, nil + end + elseif speed < 0 then + local t = (selfInterval.min - otherInterval.max) / speed + self.lastTime = math.min(self.lastTime, t) + + if self.firstTime > self.lastTime then + return false, nil + end + end + end + + if self.firstTime > self.lastTime then + return false, nil + end + + return true, side +end + +--- @param shape slick.collision.shapeInterface +--- @param point slick.geometry.point +--- @return slick.geometry.point? +function shapeCollisionResolutionQuery:getClosestVertex(shape, point) + local minDistance + local result + + for i = 1, shape.vertexCount do + local vertex = shape.vertices[i] + local distance = vertex:distanceSquared(point) + + if distance < (minDistance or math.huge) then + minDistance = distance + result = vertex + end + end + + return result +end + +local _cachedGetAxesCircleCenter = point.new() +function shapeCollisionResolutionQuery:getAxes() + --- @type slick.collision.shapeInterface + local shape = self.currentShape.shape + for i = 1, shape.normalCount do + local normal = shape.normals[i] + + local axis = self:addAxis() + axis.normal:init(normal.x, normal.y) + axis.segment:init(shape.vertices[(i - 1) % shape.vertexCount + 1], shape.vertices[i % shape.vertexCount + 1]) + end +end + +local _cachedOffsetVertex = point.new() + +--- @param axis slick.geometry.point +--- @param interval slick.collision.interval +--- @param offset slick.geometry.point? +function shapeCollisionResolutionQuery:project(axis, interval, offset) + for i = 1, self.currentShape.shape.vertexCount do + local vertex = self.currentShape.shape.vertices[i] + _cachedOffsetVertex:init(vertex.x, vertex.y) + if offset then + _cachedOffsetVertex:add(offset, _cachedOffsetVertex) + end + + interval:update(_cachedOffsetVertex:dot(axis), i) + end +end + +return shapeCollisionResolutionQuery diff --git a/game/love_src/lib/slick/collision/shapeGroup.lua b/game/love_src/lib/slick/collision/shapeGroup.lua new file mode 100644 index 0000000..06573bd --- /dev/null +++ b/game/love_src/lib/slick/collision/shapeGroup.lua @@ -0,0 +1,110 @@ +local cache = require("slick.cache") +local polygonMesh = require("slick.collision.polygonMesh") +local enum = require("slick.enum") +local tag = require("slick.tag") +local util = require("slick.util") + +--- @class slick.collision.shapeGroup +--- @field tag any +--- @field entity slick.entity | slick.cache +--- @field shapes slick.collision.shape[] +local shapeGroup = {} +local metatable = { __index = shapeGroup } + +--- @param entity slick.entity | slick.cache +--- @param tag slick.tag | slick.enum | nil +--- @param ... slick.collision.shapeDefinition +--- @return slick.collision.shapeGroup +function shapeGroup.new(entity, tag, ...) + local result = setmetatable({ + entity = entity, + shapes = {} + }, metatable) + + result:_addShapeDefinitions(tag, ...) + + return result +end + +--- @private +--- @param tagInstance slick.tag | slick.enum | nil +--- @param shapeDefinition slick.collision.shapeDefinition? +--- @param ... slick.collision.shapeDefinition +function shapeGroup:_addShapeDefinitions(tagInstance, shapeDefinition, ...) + if not shapeDefinition then + return + end + + local shape + if shapeDefinition.type == shapeGroup then + shape = shapeDefinition.type.new(self.entity, shapeDefinition.tag, unpack(shapeDefinition.arguments, 1, shapeDefinition.n)) + else + shape = shapeDefinition.type.new(self.entity, unpack(shapeDefinition.arguments, 1, shapeDefinition.n)) + end + + local shapeTag = shapeDefinition.tag or tagInstance + local tagValue = nil + if util.is(shapeTag, tag) then + tagValue = shapeTag and shapeTag.value + elseif util.is(shapeTag, enum) then + tagValue = shapeTag + elseif type(shapeTag) ~= "nil" then + error("expected tag to be an instance of slick.enum or slick.tag") + end + + shape.tag = tagValue + + self:_addShapes(shape) + return self:_addShapeDefinitions(tagInstance, ...) +end + +--- @private +--- @param shape slick.collision.shapelike +---@param ... slick.collision.shapelike +function shapeGroup:_addShapes(shape, ...) + if not shape then + return + end + + if util.is(shape, shapeGroup) then + --- @cast shape slick.collision.shapeGroup + return self:_addShapes(unpack(shape.shapes)) + else + table.insert(self.shapes, shape) + return self:_addShapes(...) + end +end + +function shapeGroup:attach() + local shapes = self.shapes + + local index = 1 + while index <= #shapes do + local shape = shapes[index] + if util.is(shape, polygonMesh) then + --- @type slick.cache + local c + + if util.is(self.entity, cache) then + --- @diagnostic disable-next-line: cast-local-type + c = self.entity + else + c = self.entity.world.cache + end + + --- @diagnostic disable-next-line: cast-type-mismatch + --- @cast shape slick.collision.polygonMesh + shape:build(c.triangulator) + + table.remove(shapes, index) + for i = #shape.polygons, 1, -1 do + local polygon = shape.polygons[i] + table.insert(shapes, index, polygon) + end + else + index = index + 1 + end + end +end + +return shapeGroup diff --git a/game/love_src/lib/slick/draw.lua b/game/love_src/lib/slick/draw.lua new file mode 100644 index 0000000..de7d927 --- /dev/null +++ b/game/love_src/lib/slick/draw.lua @@ -0,0 +1,178 @@ +local lineSegment = require "slick.collision.lineSegment" +local point = require "slick.geometry.point" +local ray = require "slick.geometry.ray" +local rectangle = require "slick.geometry.rectangle" +local segment = require "slick.geometry.segment" +local util = require "slick.util" +local worldQuery = require "slick.worldQuery" + +--- @param node slick.collision.quadTreeNode +local function _drawQuadTreeNode(node) + love.graphics.rectangle("line", node.bounds:left(), node.bounds:top(), node.bounds:width(), node.bounds:height()) + + love.graphics.print(node.level, node.bounds:right() - 16, node.bounds:bottom() - 16) +end + +local function _defaultFilter() + return true +end + +--- @param world slick.world +local function _drawShapes(world) + local items = world:getItems() + for _, item in ipairs(items) do + local entity = world:get(item) + for _, shape in ipairs(entity.shapes.shapes) do + if util.is(shape, lineSegment) then + --- @cast shape slick.collision.lineSegment + love.graphics.line(shape.segment.a.x, shape.segment.a.y, shape.segment.b.x, shape.segment.b.y) + elseif shape.vertexCount == 4 then + love.graphics.polygon("line", shape.vertices[1].x, shape.vertices[1].y, shape.vertices[2].x, + shape.vertices[2].y, shape.vertices[3].x, shape.vertices[3].y, shape.vertices[4].x, + shape.vertices[4].y) + else + for i = 1, shape.vertexCount do + local j = i % shape.vertexCount + 1 + + local a = shape.vertices[i] + local b = shape.vertices[j] + + love.graphics.line(a.x, a.y, b.x, b.y) + end + end + end + end +end + +--- @param world slick.world +local function _drawText(world) + local items = world:getItems() + for _, item in ipairs(items) do + local entity = world:get(item) + for _, shape in ipairs(entity.shapes.shapes) do + love.graphics.print(string.format("%.2f, %.2f", shape.bounds.topLeft.x, shape.bounds.topLeft.y), shape.vertices[1].x, shape.vertices[1].y) + love.graphics.print(string.format("%.2f x %.2f", shape.bounds:width(), shape.bounds:height()), shape.vertices[1].x, shape.vertices[1].y + 8) + end + end +end + +--- @param world slick.world +local function _drawNormals(world) + local items = world:getItems() + for _, item in ipairs(items) do + local entity = world:get(item) + for _, shape in ipairs(entity.shapes.shapes) do + local localSize = math.max(shape.bounds:width(), shape.bounds:height()) / 8 + + for i = 1, shape.vertexCount do + local j = i % shape.vertexCount + 1 + + local a = shape.vertices[i] + local b = shape.vertices[j] + + if i <= shape.normalCount then + local n = shape.normals[i] + love.graphics.line((a.x + b.x) / 2, (a.y + b.y) / 2, (a.x + b.x) / 2 + n.x * localSize, (a.y + b.y) / 2 + n.y * localSize) + end + end + end + end +end + +--- @class slick.draw.options +--- @field text boolean? +--- @field quadTree boolean? +--- @field normals boolean? +local defaultOptions = { + text = true, + quadTree = true, + normals = true +} + +--- @param world slick.world +--- @param queries { filter: slick.worldShapeFilterQueryFunc, shape: slick.geometry.shape }[]? +--- @param options slick.draw.options? +local function draw(world, queries, options) + options = options or defaultOptions + local drawText = options.text == nil and defaultOptions.text or options.text + local drawQuadTree = options.quadTree == nil and defaultOptions.quadTree or options.quadTree + local drawNormals = options.normals == nil and defaultOptions.normals or options.normals + + local bounds = rectangle.new(world.quadTree:computeExactBounds()) + local size = math.min(bounds:width(), bounds:height()) / 16 + + love.graphics.push("all") + + local cr, cg, cb, ca = love.graphics.getColor() + + _drawShapes(world) + + if drawNormals then + love.graphics.setColor(0, 1, 0, ca) + _drawNormals(world) + love.graphics.setColor(cr, cg, cb, ca) + end + + if drawText then + _drawText(world) + end + + if queries then + local query = worldQuery.new(world) + for _, q in ipairs(queries) do + local shape = q.shape + local filter = q.filter + + love.graphics.setColor(0, 0.5, 1, 0.5) + if util.is(shape, point) then + --- @cast shape slick.geometry.point + love.graphics.circle("fill", shape.x, shape.y, 4) + elseif util.is(shape, ray) then + --- @cast shape slick.geometry.ray + love.graphics.line(shape.origin.x, shape.origin.y, shape.origin.x + shape.direction.x * size, shape.origin.y + shape.direction.y * size) + + local left = point.new() + shape.direction:left(left) + + local right = point.new() + shape.direction:right(right) + + love.graphics.line( + shape.origin.x + shape.direction.x * (size / 2) - left.x * (size / 2), + shape.origin.y + shape.direction.y * (size / 2) - left.y * (size / 2), + shape.origin.x + shape.direction.x * size, + shape.origin.y + shape.direction.y * size) + love.graphics.line( + shape.origin.x + shape.direction.x * (size / 2) - right.x * (size / 2), + shape.origin.y + shape.direction.y * (size / 2) - right.y * (size / 2), + shape.origin.x + shape.direction.x * size, + shape.origin.y + shape.direction.y * size) + elseif util.is(shape, rectangle) then + --- @cast shape slick.geometry.rectangle + love.graphics.rectangle("line", shape:left(), shape:top(), shape:width(), shape:height()) + elseif util.is(shape, segment) then + --- @cast shape slick.geometry.segment + love.graphics.line(shape.a.x, shape.a.y, shape.b.x, shape.b.y) + end + + query:performPrimitive(shape, filter or _defaultFilter) + + love.graphics.setColor(1, 0, 0, 1) + for _, result in ipairs(query.results) do + love.graphics.rectangle("fill", result.contactPoint.x - 2, result.contactPoint.y - 2, 4, 4) + for _, contact in ipairs(result.contactPoints) do + love.graphics.rectangle("fill", contact.x - 2, contact.y - 2, 4, 4) + end + end + end + end + + love.graphics.setColor(0, 1, 1, 0.5) + if drawQuadTree then + world.quadTree.root:visit(_drawQuadTreeNode) + end + + love.graphics.pop() +end + +return draw diff --git a/game/love_src/lib/slick/entity.lua b/game/love_src/lib/slick/entity.lua new file mode 100644 index 0000000..ea1d194 --- /dev/null +++ b/game/love_src/lib/slick/entity.lua @@ -0,0 +1,105 @@ +local shapeGroup = require("slick.collision.shapeGroup") +local rectangle = require("slick.geometry.rectangle") +local transform = require("slick.geometry.transform") + +--- @class slick.entity +--- @field item any? +--- @field world slick.world? +--- @field bounds slick.geometry.rectangle +--- @field shapes slick.collision.shapeGroup +--- @field transform slick.geometry.transform +local entity = {} +local metatable = { __index = entity } + +--- @return slick.entity +function entity.new() + local result = setmetatable({ transform = transform.new(), bounds = rectangle.new() }, metatable) + result.shapes = shapeGroup.new(result) + + return result +end + +function entity:init(item) + self.item = item + self.shapes = shapeGroup.new(self) + self.bounds:init(0, 0, 0, 0) + + transform.IDENTITY:copy(self.transform) +end + +function entity:_updateBounds() + local shapes = self.shapes.shapes + if #shapes == 0 then + self.bounds:init(0, 0, 0, 0) + return + end + + self.bounds:init(shapes[1].bounds:left(), shapes[1].bounds:top(), shapes[1].bounds:right(), shapes[1].bounds:bottom()) + for i = 2, #self.shapes.shapes do + self.bounds:expand(shapes[i].bounds:left(), shapes[i].bounds:top()) + self.bounds:expand(shapes[i].bounds:right(), shapes[i].bounds:bottom()) + end +end + +--- @private +function entity:_updateQuadTree() + if not self.world then + return + end + + local shapes = self.shapes.shapes + for _, shape in ipairs(shapes) do + shape:transform(self.transform) + end + self:_updateBounds() + + for _, shape in ipairs(self.shapes.shapes) do + --- @cast shape slick.collision.shape + --- @diagnostic disable-next-line: invisible + self.world:_addShape(shape) + end +end + +--- @param ... slick.collision.shapeDefinition +function entity:setShapes(...) + if self.world then + for _, shape in ipairs(self.shapes.shapes) do + --- @cast shape slick.collision.shape + --- @diagnostic disable-next-line: invisible + self.world:_removeShape(shape) + end + end + + self.shapes = shapeGroup.new(self, nil, ...) + if self.world then + self.shapes:attach() + self:_updateQuadTree() + end +end + +--- @param transform slick.geometry.transform +function entity:setTransform(transform) + transform:copy(self.transform) + self:_updateQuadTree() +end + +--- @param world slick.world +function entity:add(world) + self.world = world + self.shapes:attach() + self:_updateQuadTree() +end + +function entity:detach() + if self.world then + for _, shape in ipairs(self.shapes.shapes) do + --- @cast shape slick.collision.shape + --- @diagnostic disable-next-line: invisible + self.world:_removeShape(shape) + end + end + + self.item = nil +end + +return entity diff --git a/game/love_src/lib/slick/enum.lua b/game/love_src/lib/slick/enum.lua new file mode 100644 index 0000000..c822aff --- /dev/null +++ b/game/love_src/lib/slick/enum.lua @@ -0,0 +1,12 @@ +--- @class slick.enum +--- @field value any +local enum = {} +local metatable = { __index = enum } + +function enum.new(value) + return setmetatable({ + value = value + }, metatable) +end + +return enum diff --git a/game/love_src/lib/slick/geometry/clipper.lua b/game/love_src/lib/slick/geometry/clipper.lua new file mode 100644 index 0000000..35f51fc --- /dev/null +++ b/game/love_src/lib/slick/geometry/clipper.lua @@ -0,0 +1,909 @@ +local quadTree = require "slick.collision.quadTree" +local quadTreeQuery = require "slick.collision.quadTreeQuery" +local merge = require "slick.geometry.merge" +local point = require "slick.geometry.point" +local rectangle = require "slick.geometry.rectangle" +local segment = require "slick.geometry.segment" +local delaunay = require "slick.geometry.triangulation.delaunay" +local edge = require "slick.geometry.triangulation.edge" +local slicktable = require "slick.util.slicktable" +local pool = require "slick.util.pool" +local slickmath = require "slick.util.slickmath" +local search = require "slick.util.search" + +local function _compareNumber(a, b) + return a - b +end + +--- @alias slick.geometry.clipper.clipOperation fun(self: slick.geometry.clipper, a: number, b: number) + +--- @alias slick.geometry.clipper.polygonUserdata { +--- userdata: any, +--- polygons: table, +--- parent: slick.geometry.clipper.polygon, +--- hasEdge: boolean, +--- isExteriorEdge: boolean, +--- isInteriorEdge: boolean, +--- } + +--- @alias slick.geometry.clipper.polygon { +--- points: number[], +--- edges: number[], +--- combinedEdges: number[], +--- interior: number[], +--- exterior: number[], +--- userdata: any[], +--- triangles: number[][], +--- triangleCount: number, +--- polygons: number[][], +--- polygonCount: number, +--- pointToCombinedPointIndex: table, +--- combinedPointToPointIndex: table, +--- quadTreeOptions: slick.collision.quadTreeOptions, +--- quadTree: slick.collision.quadTree, +--- quadTreeQuery: slick.collision.quadTreeQuery, +--- bounds: slick.geometry.rectangle, +--- } + +--- @param quadTreeOptions slick.collision.quadTreeOptions? +--- @return slick.geometry.clipper.polygon +local function _newPolygon(quadTreeOptions) + local quadTree = quadTree.new(quadTreeOptions) + local quadTreeQuery = quadTreeQuery.new(quadTree) + + return { + points = {}, + edges = {}, + combinedEdges = {}, + exterior = {}, + interior = {}, + userdata = {}, + triangles = {}, + triangleCount = 0, + polygons = {}, + polygonCount = 0, + pointToCombinedPointIndex = {}, + combinedPointToPointIndex = {}, + quadTreeOptions = { + maxLevels = quadTreeOptions and quadTreeOptions.maxLevels, + maxData = quadTreeOptions and quadTreeOptions.maxData, + expand = false + }, + quadTree = quadTree, + quadTreeQuery = quadTreeQuery, + bounds = rectangle.new(), + prepareCleanupOptions = {} + } +end + +--- @class slick.geometry.clipper +--- @field private innerPolygonsPool slick.util.pool +--- @field private combinedPoints number[] +--- @field private combinedEdges number[] +--- @field private combinedUserdata slick.geometry.clipper.polygonUserdata[] +--- @field private merge slick.geometry.clipper.merge +--- @field private triangulator slick.geometry.triangulation.delaunay +--- @field private pendingPolygonEdges number[] +--- @field private cachedEdge slick.geometry.triangulation.edge +--- @field private edges slick.geometry.triangulation.edge[] +--- @field private edgesPool slick.util.pool +--- @field private subjectPolygon slick.geometry.clipper.polygon +--- @field private otherPolygon slick.geometry.clipper.polygon +--- @field private resultPolygon slick.geometry.clipper.polygon +--- @field private cachedPoint slick.geometry.point +--- @field private cachedSegment slick.geometry.segment +--- @field private clipCleanupOptions slick.geometry.clipper.clipOptions +--- @field private inputCleanupOptions slick.geometry.clipper.clipOptions? +--- @field private indexToResultIndex table +--- @field private resultPoints number[]? +--- @field private resultEdges number[]? +--- @field private resultUserdata any[]? +--- @field private resultExteriorEdges number[]? +--- @field private resultInteriorEdges number[]? +--- @field private resultIndex number +local clipper = {} +local metatable = { __index = clipper } + +--- @param triangulator slick.geometry.triangulation.delaunay? +--- @param quadTreeOptions slick.collision.quadTreeOptions? +--- @return slick.geometry.clipper +function clipper.new(triangulator, quadTreeOptions) + local self = { + triangulator = triangulator or delaunay.new(), + + combinedPoints = {}, + combinedEdges = {}, + combinedUserdata = {}, + merge = merge.new(), + + innerPolygonsPool = pool.new(), + + pendingPolygonEdges = {}, + + cachedEdge = edge.new(), + edges = {}, + edgesPool = pool.new(edge), + + subjectPolygon = _newPolygon(quadTreeOptions), + otherPolygon = _newPolygon(quadTreeOptions), + resultPolygon = _newPolygon(quadTreeOptions), + + cachedPoint = point.new(), + cachedSegment = segment.new(), + + clipCleanupOptions = {}, + + indexToResultIndex = {}, + resultIndex = 1 + } + + --- @cast self slick.geometry.clipper + --- @param intersection slick.geometry.triangulation.intersection + function self.clipCleanupOptions.intersect(intersection) + --- @diagnostic disable-next-line: invisible + self:_intersect(intersection) + end + + function self.clipCleanupOptions.dissolve(dissolve) + --- @diagnostic disable-next-line: invisible + self:_dissolve(dissolve) + end + + return setmetatable(self, metatable) +end + +--- @private +--- @param t table +--- @param other table +--- @param ... table +--- @return table +function clipper:_mergePolygonSet(t, other, ...) + if not other then + return t + end + + for k, v in pairs(other) do + if not t[k] then + t[k] = self.innerPolygonsPool:allocate() + slicktable.clear(t[k]) + end + + for _, p in ipairs(v) do + local i = search.lessThan(t[k], p, _compareNumber) + 1 + if t[k][i] ~= p then + table.insert(t[k], i, p) + end + end + end + + return self:_mergePolygonSet(t, ...) +end + +--- @private +--- @param intersection slick.geometry.triangulation.intersection +function clipper:_intersect(intersection) + local a1, b1 = intersection.a1Userdata, intersection.b1Userdata + local a2, b2 = intersection.a2Userdata, intersection.b2Userdata + + if self.inputCleanupOptions and self.inputCleanupOptions.intersect then + intersection.a1Userdata = a1.userdata + intersection.b1Userdata = b1.userdata + + intersection.a2Userdata = a2.userdata + intersection.b2Userdata = b2.userdata + + self.inputCleanupOptions.intersect(intersection) + + intersection.a1Userdata, intersection.b1Userdata = a1, b1 + intersection.a2Userdata, intersection.b2Userdata = a2, b2 + end + + local userdata = self.combinedUserdata[intersection.resultIndex] + if not userdata then + userdata = { polygons = {} } + self.combinedUserdata[intersection.resultIndex] = userdata + else + slicktable.clear(userdata.polygons) + userdata.parent = nil + end + + userdata.userdata = intersection.resultUserdata + userdata.isExteriorEdge = userdata.isExteriorEdge or + intersection.a1Userdata.isExteriorEdge or + intersection.a2Userdata.isExteriorEdge or + intersection.b1Userdata.isExteriorEdge or + intersection.b2Userdata.isExteriorEdge + userdata.isInteriorEdge = userdata.isInteriorEdge or + intersection.a1Userdata.isInteriorEdge or + intersection.a2Userdata.isInteriorEdge or + intersection.b1Userdata.isInteriorEdge or + intersection.b2Userdata.isInteriorEdge + + self:_mergePolygonSet(userdata.polygons, a1.polygons, b1.polygons, a2.polygons, b2.polygons) + + intersection.resultUserdata = userdata +end + +--- @private +--- @param dissolve slick.geometry.triangulation.dissolve +function clipper:_dissolve(dissolve) + if self.inputCleanupOptions and self.inputCleanupOptions.dissolve then + --- @type slick.geometry.clipper.polygonUserdata + local u = dissolve.userdata + + --- @type slick.geometry.clipper.polygonUserdata + local o = dissolve.otherUserdata + + dissolve.userdata = u.userdata + dissolve.otherUserdata = o.userdata + + self.inputCleanupOptions.dissolve(dissolve) + + dissolve.userdata = u + dissolve.otherUserdata = o + + if dissolve.resultUserdata ~= nil then + o.userdata = dissolve.resultUserdata + dissolve.resultUserdata = nil + end + end +end + +function clipper:reset() + self.edgesPool:reset() + self.innerPolygonsPool:reset() + + slicktable.clear(self.subjectPolygon.points) + slicktable.clear(self.subjectPolygon.edges) + slicktable.clear(self.subjectPolygon.userdata) + + slicktable.clear(self.otherPolygon.points) + slicktable.clear(self.otherPolygon.edges) + slicktable.clear(self.otherPolygon.userdata) + + slicktable.clear(self.combinedPoints) + slicktable.clear(self.combinedEdges) + + slicktable.clear(self.edges) + slicktable.clear(self.pendingPolygonEdges) + + slicktable.clear(self.indexToResultIndex) + self.resultIndex = 1 + + self.inputCleanupOptions = nil + + self.resultPoints = nil + self.resultEdges = nil + self.resultUserdata = nil +end + +--- @type slick.geometry.triangulation.delaunayTriangulationOptions +local _triangulateOptions = { + refine = true, + interior = true, + exterior = false, + polygonization = true +} + +local _cachedPolygonBounds = rectangle.new() + +--- @private +--- @param points number[] +--- @param exterior number[]? +--- @param interior number[]? +--- @param userdata any[]? +--- @param polygon slick.geometry.clipper.polygon +function clipper:_addPolygon(points, exterior, interior, userdata, polygon) + slicktable.clear(polygon.combinedEdges) + slicktable.clear(polygon.exterior) + slicktable.clear(polygon.interior) + + if exterior then + for _, e in ipairs(exterior) do + table.insert(polygon.exterior, e) + table.insert(polygon.combinedEdges, e) + end + end + + if interior then + for _, e in ipairs(interior) do + table.insert(polygon.interior, e) + table.insert(polygon.combinedEdges, e) + end + end + + if userdata then + for _, u in ipairs(userdata) do + table.insert(polygon.userdata, u) + end + end + + self.triangulator:clean(points, polygon.exterior, nil, nil, polygon.points, polygon.edges) + local _, triangleCount, _, polygonCount = self.triangulator:triangulate(polygon.points, polygon.edges, _triangulateOptions, polygon.triangles, polygon.polygons) + + polygon.triangleCount = triangleCount + polygon.polygonCount = polygonCount or 0 + + if #polygon.points > 0 then + polygon.bounds:init(polygon.points[1], polygon.points[2]) + + for i = 3, #polygon.points, 2 do + polygon.bounds:expand(polygon.points[i], polygon.points[i + 1]) + end + else + polygon.bounds:init(0, 0, 0, 0) + end + + polygon.quadTreeOptions.x = polygon.bounds:left() + polygon.quadTreeOptions.y = polygon.bounds:top() + polygon.quadTreeOptions.width = math.max(polygon.bounds:width(), self.triangulator.epsilon) + polygon.quadTreeOptions.height = math.max(polygon.bounds:height(), self.triangulator.epsilon) + + polygon.quadTree:clear() + polygon.quadTree:rebuild(polygon.quadTreeOptions) + + for i = 1, polygon.polygonCount do + local p = polygon.polygons[i] + + _cachedPolygonBounds.topLeft:init(math.huge, math.huge) + _cachedPolygonBounds.bottomRight:init(-math.huge, -math.huge) + + for _, vertex in ipairs(p) do + local xIndex = (vertex - 1) * 2 + 1 + local yIndex = xIndex + 1 + + _cachedPolygonBounds:expand(polygon.points[xIndex], polygon.points[yIndex]) + end + + polygon.quadTree:insert(p, _cachedPolygonBounds) + end +end + +--- @private +--- @param polygon slick.geometry.clipper.polygon +function clipper:_preparePolygon(points, polygon) + local numPoints = #self.combinedPoints / 2 + for i = 1, #points, 2 do + local x = points[i] + local y = points[i + 1] + + table.insert(self.combinedPoints, x) + table.insert(self.combinedPoints, y) + + local vertexIndex = (i + 1) / 2 + local combinedIndex = vertexIndex + numPoints + local userdata = self.combinedUserdata[combinedIndex] + if not userdata then + userdata = { polygons = {} } + self.combinedUserdata[combinedIndex] = userdata + else + slicktable.clear(userdata.polygons) + end + + userdata.parent = polygon + userdata.polygons[polygon] = self.innerPolygonsPool:allocate() + slicktable.clear(userdata.polygons[polygon]) + + userdata.userdata = polygon.userdata[vertexIndex] + userdata.hasEdge = false + + local index = (i - 1) / 2 + 1 + polygon.pointToCombinedPointIndex[index] = combinedIndex + polygon.combinedPointToPointIndex[combinedIndex] = index + end + + for i = 1, polygon.polygonCount do + local p = polygon.polygons[i] + + for _, vertexIndex in ipairs(p) do + local combinedIndex = vertexIndex + numPoints + local userdata = self.combinedUserdata[combinedIndex] + if userdata then + local polygons = userdata.polygons[polygon] + local innerPolygonIndex = search.lessThan(polygons, i, _compareNumber) + 1 + if polygons[innerPolygonIndex] ~= i then + table.insert(polygons, innerPolygonIndex, i) + end + end + end + end + + for i = 1, #polygon.exterior, 2 do + local a = polygon.exterior[i] + numPoints + local b = polygon.exterior[i + 1] + numPoints + + self.combinedUserdata[a].hasEdge = true + self.combinedUserdata[a].isExteriorEdge = true + self.combinedUserdata[b].hasEdge = true + self.combinedUserdata[b].isExteriorEdge = true + + table.insert(self.combinedEdges, a) + table.insert(self.combinedEdges, b) + end + + for i = 1, #polygon.interior, 2 do + local a = polygon.interior[i] + numPoints + local b = polygon.interior[i + 1] + numPoints + + self.combinedUserdata[a].hasEdge = true + self.combinedUserdata[a].isInteriorEdge = true + self.combinedUserdata[b].hasEdge = true + self.combinedUserdata[b].isInteriorEdge = true + + table.insert(self.combinedEdges, a) + table.insert(self.combinedEdges, b) + end +end + +--- @private +--- @param polygon slick.geometry.clipper.polygon +function clipper:_finishPolygon(polygon) + slicktable.clear(polygon.userdata) +end + +--- @private +--- @param operation slick.geometry.clipper.clipOperation +function clipper:_mergePoints(operation) + for i = 1, #self.resultPolygon.points, 2 do + local index = (i - 1) / 2 + 1 + local combinedUserdata = self.resultPolygon.userdata[index] + + local x = self.resultPolygon.points[i] + local y = self.resultPolygon.points[i + 1] + + if not combinedUserdata.hasEdge then + if operation == self.difference and not self:_pointInside(x, y, self.otherPolygon) then + self:_addResultEdge(index) + elseif operation == self.union then + self:_addResultEdge(index) + elseif operation == self.intersection and (self:_pointInside(x, y, self.subjectPolygon) and self:_pointInside(x, y, self.otherPolygon)) then + self:_addResultEdge(index) + end + end + end +end + +--- @private +function clipper:_mergeUserdata() + if not (self.inputCleanupOptions and self.inputCleanupOptions.merge) then + return + end + + local n = #self.combinedPoints / 2 + for i = 1, n do + local combinedUserdata = self.combinedUserdata[i] + + if combinedUserdata.parent then + if combinedUserdata.parent == self.subjectPolygon then + self.merge:init( + "subject", + self.subjectPolygon.combinedPointToPointIndex[i], + self.subjectPolygon.userdata[self.subjectPolygon.combinedPointToPointIndex[i]], + i) + elseif combinedUserdata.parent == self.otherPolygon then + self.merge:init( + "other", + self.otherPolygon.combinedPointToPointIndex[i], + self.otherPolygon.userdata[self.otherPolygon.combinedPointToPointIndex[i]], + i) + end + + self.inputCleanupOptions.merge(self.merge) + + if self.merge.resultUserdata ~= nil then + self.resultUserdata[self.merge.resultIndex] = self.merge.resultUserdata + end + end + end +end + +--- @private +function clipper:_segmentInsidePolygon(s, polygon, vertices) + local isABIntersection, isABCollinear = false, false + for i = 1, #vertices do + local j = slickmath.wrap(i, 1, #vertices) + + local aIndex = (vertices[i] - 1) * 2 + 1 + local bIndex = (vertices[j] - 1) * 2 + 1 + + local ax = polygon.points[aIndex] + local ay = polygon.points[aIndex + 1] + local bx = polygon.points[bIndex] + local by = polygon.points[bIndex + 1] + + self.cachedSegment.a:init(ax, ay) + self.cachedSegment.b:init(bx, by) + + isABCollinear = isABCollinear or slickmath.collinear(self.cachedSegment.a, self.cachedSegment.b, s.a, s.b, self.triangulator.epsilon) + + local intersection, _, _, u, v = slickmath.intersection(self.cachedSegment.a, self.cachedSegment.b, s.a, s.b, self.triangulator.epsilon) + if intersection and u and v and (u > self.triangulator.epsilon and u + self.triangulator.epsilon < 1) and (v > self.triangulator.epsilon and v + self.triangulator.epsilon < 1) then + isABIntersection = true + end + end + + local isAInside, isACollinear = self:_pointInsidePolygon(s.a, polygon, vertices) + local isBInside, isBCollinear = self:_pointInsidePolygon(s.b, polygon, vertices) + + local isABInside = (isAInside or isACollinear) and (isBInside or isBCollinear) + + return isABIntersection or isABInside, isABCollinear, isAInside, isBInside +end + +--- @private +--- @param p slick.geometry.point +--- @param polygon slick.geometry.clipper.polygon +--- @param vertices number[] +--- @return boolean, boolean +function clipper:_pointInsidePolygon(p, polygon, vertices) + local isCollinear = false + + local px = p.x + local py = p.y + + local minDistance = math.huge + + local isInside = false + for i = 1, #vertices do + local j = slickmath.wrap(i, 1, #vertices) + + local aIndex = (vertices[i] - 1) * 2 + 1 + local bIndex = (vertices[j] - 1) * 2 + 1 + + local ax = polygon.points[aIndex] + local ay = polygon.points[aIndex + 1] + local bx = polygon.points[bIndex] + local by = polygon.points[bIndex + 1] + + self.cachedSegment.a:init(ax, ay) + self.cachedSegment.b:init(bx, by) + + isCollinear = isCollinear or slickmath.collinear(self.cachedSegment.a, self.cachedSegment.b, p, p, self.triangulator.epsilon) + minDistance = math.min(self.cachedSegment:distance(p), minDistance) + + local z = (bx - ax) * (py - ay) / (by - ay) + ax + if ((ay > py) ~= (by > py) and px < z) then + isInside = not isInside + end + end + + return isInside and minDistance > self.triangulator.epsilon, isCollinear or minDistance < self.triangulator.epsilon +end + + +local _cachedInsidePoint = point.new() + +--- @private +--- @param x number +--- @param y number +--- @param polygon slick.geometry.clipper.polygon +function clipper:_pointInside(x, y, polygon) + _cachedInsidePoint:init(x, y) + polygon.quadTreeQuery:perform(_cachedInsidePoint, self.triangulator.epsilon) + + local isInside, isCollinear + for _, result in ipairs(polygon.quadTreeQuery.results) do + --- @cast result number[] + local i, c = self:_pointInsidePolygon(_cachedInsidePoint, polygon, result) + + isInside = isInside or i + isCollinear = isCollinear or c + end + + return isInside, isCollinear +end + +local _cachedInsideSegment = segment.new() + +--- @private +--- @param ax number +--- @param ay number +--- @param bx number +--- @param by number +--- @param polygon slick.geometry.clipper.polygon +function clipper:_segmentInside(ax, ay, bx, by, polygon) + _cachedInsideSegment.a:init(ax, ay) + _cachedInsideSegment.b:init(bx, by) + polygon.quadTreeQuery:perform(_cachedInsideSegment, self.triangulator.epsilon) + + local intersection, collinear, aInside, bInside = false, false, false, false + for _, result in ipairs(polygon.quadTreeQuery.results) do + --- @cast result number[] + local i, c, a, b = self:_segmentInsidePolygon(_cachedInsideSegment, polygon, result) + intersection = intersection or i + collinear = collinear or c + aInside = aInside or a + bInside = bInside or b + end + + return intersection or (aInside and bInside), collinear +end + +--- @private +--- @param segment slick.geometry.segment +--- @param side -1 | 0 | 1 +--- @param parentPolygon slick.geometry.clipper.polygon +--- @param childPolygons number[] +--- @param ... number[] +function clipper:_hasAnyOnSideImpl(segment, side, parentPolygon, childPolygons, ...) + if not childPolygons and select("#", ...) == 0 then + return false + end + + if childPolygons then + for _, childPolygonIndex in ipairs(childPolygons) do + local childPolygon = parentPolygon.polygons[childPolygonIndex] + + for i = 1, #childPolygon do + local xIndex = (childPolygon[i] - 1) * 2 + 1 + local yIndex = xIndex + 1 + + local x, y = parentPolygon.points[xIndex], parentPolygon.points[yIndex] + self.cachedPoint:init(x, y) + local otherSide = slickmath.direction(segment.a, segment.b, self.cachedPoint, self.triangulator.epsilon) + if side == otherSide then + return true + end + end + end + end + + return self:_hasAnyOnSideImpl(segment, side, parentPolygon, ...) +end + +--- @private +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +--- @param side -1 | 0 | 1 +--- @param parentPolygon slick.geometry.clipper.polygon +--- @param childPolygons number[] +--- @param ... number[] +function clipper:_hasAnyOnSide(x1, y1, x2, y2, side, parentPolygon, childPolygons, ...) + self.cachedSegment.a:init(x1, y1) + self.cachedSegment.b:init(x2, y2) + + return self:_hasAnyOnSideImpl(self.cachedSegment, side, parentPolygon, childPolygons, ...) +end + +--- @private +function clipper:_addPendingEdge(a, b) + self.cachedEdge:init(a, b) + local found = search.first(self.edges, self.cachedEdge, edge.compare) + + if not found then + table.insert(self.pendingPolygonEdges, a) + table.insert(self.pendingPolygonEdges, b) + + local e = self.edgesPool:allocate(a, b) + table.insert(self.edges, search.lessThan(self.edges, e, edge.compare) + 1, e) + end +end + +--- @private +function clipper:_popPendingEdge() + local b = table.remove(self.pendingPolygonEdges) + local a = table.remove(self.pendingPolygonEdges) + + return a, b +end + +--- @private +--- @param a number? +--- @param b number? +function clipper:_addResultEdge(a, b) + local aResultIndex = self.indexToResultIndex[a] + if not aResultIndex and a then + aResultIndex = self.resultIndex + self.resultIndex = self.resultIndex + 1 + + self.indexToResultIndex[a] = aResultIndex + + local j = (a - 1) * 2 + 1 + local k = j + 1 + + table.insert(self.resultPoints, self.resultPolygon.points[j]) + table.insert(self.resultPoints, self.resultPolygon.points[k]) + + if self.resultUserdata then + self.resultUserdata[aResultIndex] = self.resultPolygon.userdata[a].userdata + end + end + + local bResultIndex = self.indexToResultIndex[b] + if not bResultIndex and b then + bResultIndex = self.resultIndex + self.resultIndex = self.resultIndex + 1 + + self.indexToResultIndex[b] = bResultIndex + + local j = (b - 1) * 2 + 1 + local k = j + 1 + + table.insert(self.resultPoints, self.resultPolygon.points[j]) + table.insert(self.resultPoints, self.resultPolygon.points[k]) + + if self.resultUserdata then + self.resultUserdata[bResultIndex] = self.resultPolygon.userdata[b].userdata + end + end + + if a and b then + table.insert(self.resultEdges, aResultIndex) + table.insert(self.resultEdges, bResultIndex) + + if self.resultExteriorEdges and (self.resultPolygon.userdata[a].isExteriorEdge or self.resultPolygon.userdata[b].isExteriorEdge) then + table.insert(self.resultExteriorEdges, aResultIndex) + table.insert(self.resultExteriorEdges, bResultIndex) + end + + if self.resultInteriorEdges and (self.resultPolygon.userdata[a].isInteriorEdge or self.resultPolygon.userdata[b].isInteriorEdge) then + table.insert(self.resultInteriorEdges, aResultIndex) + table.insert(self.resultInteriorEdges, bResultIndex) + end + end +end + +--- @param a number +--- @param b number +function clipper:intersection(a, b) + local aIndex = (a - 1) * 2 + 1 + local bIndex = (b - 1) * 2 + 1 + + --- @type slick.geometry.clipper.polygonUserdata + local aUserdata = self.resultPolygon.userdata[a] + --- @type slick.geometry.clipper.polygonUserdata + local bUserdata = self.resultPolygon.userdata[b] + + local aOtherPolygons = aUserdata.polygons[self.otherPolygon] + local bOtherPolygons = bUserdata.polygons[self.otherPolygon] + + local ax, ay = self.resultPolygon.points[aIndex], self.resultPolygon.points[aIndex + 1] + local bx, by = self.resultPolygon.points[bIndex], self.resultPolygon.points[bIndex + 1] + + local abInsideSubject = self:_segmentInside(ax, ay, bx, by, self.subjectPolygon) + local abInsideOther, abCollinearOther = self:_segmentInside(ax, ay, bx, by, self.otherPolygon) + + local hasAnyCollinearOtherPoints = self:_hasAnyOnSide(ax, ay, bx, by, 0, self.otherPolygon, aOtherPolygons, bOtherPolygons) + local hasAnyCollinearSubjectPoints = self:_hasAnyOnSide(ax, ay, bx, by, 0, self.otherPolygon, aOtherPolygons, bOtherPolygons) + + if (abInsideOther and abInsideSubject) or (not abCollinearOther and ((abInsideOther and hasAnyCollinearSubjectPoints) or (abInsideSubject and hasAnyCollinearOtherPoints))) then + self:_addResultEdge(a, b) + end +end + +--- @param a number +--- @param b number +function clipper:union(a, b) + local aIndex = (a - 1) * 2 + 1 + local bIndex = (b - 1) * 2 + 1 + + local ax, ay = self.resultPolygon.points[aIndex], self.resultPolygon.points[aIndex + 1] + local bx, by = self.resultPolygon.points[bIndex], self.resultPolygon.points[bIndex + 1] + + local abInsideSubject, abCollinearSubject = self:_segmentInside(ax, ay, bx, by, self.subjectPolygon) + local abInsideOther, abCollinearOther = self:_segmentInside(ax, ay, bx, by, self.otherPolygon) + + abInsideSubject = abInsideSubject or abCollinearSubject + abInsideOther = abInsideOther or abCollinearOther + + if (abInsideOther or abInsideSubject) and not (abInsideOther and abInsideSubject) then + self:_addResultEdge(a, b) + end +end + +--- @param a number +--- @param b number +function clipper:difference(a, b) + local aIndex = (a - 1) * 2 + 1 + local bIndex = (b - 1) * 2 + 1 + + local ax, ay = self.resultPolygon.points[aIndex], self.resultPolygon.points[aIndex + 1] + local bx, by = self.resultPolygon.points[bIndex], self.resultPolygon.points[bIndex + 1] + + --- @type slick.geometry.clipper.polygonUserdata + local aUserdata = self.resultPolygon.userdata[a] + --- @type slick.geometry.clipper.polygonUserdata + local bUserdata = self.resultPolygon.userdata[b] + + local aOtherPolygons = aUserdata.polygons[self.otherPolygon] + local bOtherPolygons = bUserdata.polygons[self.otherPolygon] + + local hasAnyCollinearOtherPoints = self:_hasAnyOnSide(ax, ay, bx, by, 0, self.otherPolygon, aOtherPolygons, bOtherPolygons) + + local abInsideSubject = self:_segmentInside(ax, ay, bx, by, self.subjectPolygon) + local abInsideOther = self:_segmentInside(ax, ay, bx, by, self.otherPolygon) + + if abInsideSubject and (not abInsideOther or hasAnyCollinearOtherPoints) then + self:_addResultEdge(a, b) + end +end + +--- @alias slick.geometry.clipper.mergeFunction fun(combine: slick.geometry.clipper.merge) +--- @class slick.geometry.clipper.clipOptions : slick.geometry.triangulation.delaunayCleanupOptions +--- @field merge slick.geometry.clipper.mergeFunction? +local clipOptions = {} + +--- @param operation slick.geometry.clipper.clipOperation +--- @param subjectPoints number[] +--- @param subjectEdges number[] | number[][] +--- @param otherPoints number[] +--- @param otherEdges number[] | number[][] +--- @param options slick.geometry.clipper.clipOptions? +--- @param subjectUserdata any[]? +--- @param otherUserdata any[]? +--- @param resultPoints number[]? +--- @param resultEdges number[]? +--- @param resultUserdata any[]? +--- @param resultExteriorEdges number[]? +--- @param resultInteriorEdges number[]? +function clipper:clip(operation, subjectPoints, subjectEdges, otherPoints, otherEdges, options, subjectUserdata, otherUserdata, resultPoints, resultEdges, resultUserdata, resultExteriorEdges, resultInteriorEdges) + self:reset() + + if type(subjectEdges) == "table" and #subjectEdges >= 1 and type(subjectEdges[1]) == "table" then + --- @cast subjectEdges number[][] + self:_addPolygon(subjectPoints, subjectEdges[1], subjectEdges[2], subjectUserdata, self.subjectPolygon) + else + self:_addPolygon(subjectPoints, subjectEdges, nil, subjectUserdata, self.subjectPolygon) + end + + if type(otherEdges) == "table" and #otherEdges >= 1 and type(otherEdges[1]) == "table" then + --- @cast otherEdges number[][] + self:_addPolygon(otherPoints, otherEdges[1], otherEdges[2], otherUserdata, self.otherPolygon) + else + self:_addPolygon(otherPoints, otherEdges, nil, otherUserdata, self.otherPolygon) + end + + self:_preparePolygon(subjectPoints, self.subjectPolygon) + self:_preparePolygon(otherPoints, self.otherPolygon) + + self.inputCleanupOptions = options + self.triangulator:clean(self.combinedPoints, self.combinedEdges, self.combinedUserdata, self.clipCleanupOptions, self.resultPolygon.points, self.resultPolygon.edges, self.resultPolygon.userdata) + + resultPoints = resultPoints or {} + resultEdges = resultEdges or {} + resultUserdata = (subjectUserdata and otherUserdata) and resultUserdata or {} + + self.resultPoints = resultPoints + self.resultEdges = resultEdges + self.resultUserdata = resultUserdata + self.resultPoints = resultPoints + self.resultInteriorEdges = resultInteriorEdges + self.resultExteriorEdges = resultExteriorEdges + + slicktable.clear(resultPoints) + slicktable.clear(resultEdges) + if resultUserdata then + slicktable.clear(resultUserdata) + end + if resultInteriorEdges then + slicktable.clear(resultInteriorEdges) + end + if resultExteriorEdges then + slicktable.clear(resultExteriorEdges) + end + + for i = 1, #self.resultPolygon.edges, 2 do + local a = self.resultPolygon.edges[i] + local b = self.resultPolygon.edges[i + 1] + + operation(self, a, b) + end + + self:_mergePoints(operation) + self:_mergeUserdata() + + self.resultPoints = nil + self.resultEdges = nil + self.resultUserdata = nil + + for i = 1, #self.combinedUserdata do + -- Don't leak user-provided resources. + self.combinedUserdata[i].userdata = nil + end + + return resultPoints, resultEdges, resultUserdata, resultExteriorEdges, resultInteriorEdges +end + +return clipper diff --git a/game/love_src/lib/slick/geometry/init.lua b/game/love_src/lib/slick/geometry/init.lua new file mode 100644 index 0000000..ffc85a9 --- /dev/null +++ b/game/love_src/lib/slick/geometry/init.lua @@ -0,0 +1,14 @@ +--- @alias slick.geometry.shape slick.geometry.point | slick.geometry.ray | slick.geometry.rectangle | slick.geometry.segment + +local geometry = { + clipper = require("slick.geometry.clipper"), + triangulation = require("slick.geometry.triangulation"), + point = require("slick.geometry.point"), + ray = require("slick.geometry.ray"), + rectangle = require("slick.geometry.rectangle"), + segment = require("slick.geometry.segment"), + simple = require("slick.geometry.simple"), + transform = require("slick.geometry.transform"), +} + +return geometry diff --git a/game/love_src/lib/slick/geometry/merge.lua b/game/love_src/lib/slick/geometry/merge.lua new file mode 100644 index 0000000..7e8fbc8 --- /dev/null +++ b/game/love_src/lib/slick/geometry/merge.lua @@ -0,0 +1,42 @@ +local point = require("slick.geometry.point") +local slickmath = require("slick.util.slickmath") + +--- @class slick.geometry.clipper.merge +--- @field source "subject" | "other" +--- @field target "subject" | "other" +--- @field sourceIndex number +--- @field sourceUserdata any +--- @field resultIndex number +--- @field resultUserdata any +local merge = {} +local metatable = { __index = merge } + +--- @return slick.geometry.clipper.merge +function merge.new() + return setmetatable({}, metatable) +end + +--- @param source "subject" | "other" +--- @param sourceIndex number +--- @param sourceUserdata any +--- @param resultIndex number +function merge:init(source, sourceIndex, sourceUserdata, resultIndex) + self.source = source + if source == "subject" then + self.target = "other" + else + self.target = "subject" + end + + self.sourceIndex = sourceIndex + self.sourceUserdata = sourceUserdata + self.resultIndex = resultIndex + self.resultUserdata = nil +end + +--- @param m slick.geometry.clipper.merge +function merge.default(m) + -- No-op. +end + +return merge diff --git a/game/love_src/lib/slick/geometry/point.lua b/game/love_src/lib/slick/geometry/point.lua new file mode 100644 index 0000000..e15e7e8 --- /dev/null +++ b/game/love_src/lib/slick/geometry/point.lua @@ -0,0 +1,233 @@ +local slickmath = require("slick.util.slickmath") + +--- @class slick.geometry.point +--- @field x number +--- @field y number +local point = {} +local metatable = { + __index = point, + __tostring = function(self) + return string.format("slick.geometry.point (x = %.2f, y = %.2f)", self.x, self.y) + end +} + + +--- @param x number? +--- @param y number? +--- @return slick.geometry.point +function point.new(x, y) + return setmetatable({ x = x or 0, y = y or 0 }, metatable) +end + +--- @param x number +--- @param y number +function point:init(x, y) + self.x = x + self.y = y +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @return slick.util.search.compareResult +function point.compare(a, b, E) + local result = slickmath.sign(a.x - b.x, E or slickmath.EPSILON) + if result ~= 0 then + return result + end + + return slickmath.sign(a.y - b.y, E or slickmath.EPSILON) +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @return boolean +function point.less(a, b) + return point.compare(a, b) < 0 +end + +--- @param other slick.geometry.point +--- @return slick.geometry.point +function point:higher(other) + if self:greaterThan(other) then + return self + end + + return other +end + +--- @param other slick.geometry.point +--- @return slick.geometry.point +function point:lower(other) + if self:lessThan(other) then + return self + end + + return other +end + +--- @param other slick.geometry.point +--- @return boolean +function point:equal(other) + return self.x == other.x and self.y == other.y +end + +--- @param other slick.geometry.point +--- @return boolean +function point:notEqual(other) + return not point:equal(other) +end + +--- @param other slick.geometry.point +--- @return boolean +function point:greaterThan(other) + return self.x > other.x or (self.x == other.x and self.y > other.y) +end + +--- @param other slick.geometry.point +--- @return boolean +function point:greaterThanEqual(other) + return self:greaterThan(other) or self:equal(other) +end + +--- @param other slick.geometry.point +--- @return boolean +function point:lessThan(other) + return self.x < other.x or (self.x == other.x and self.y < other.y) +end + +--- @param other slick.geometry.point +--- @return boolean +function point:lessThanOrEqual(other) + return self:lessThan(other) or self:equal(other) +end + +--- @param other slick.geometry.point +--- @param result slick.geometry.point +function point:direction(other, result) + result:init(other.x - self.x, other.y - self.y) +end + +--- @param other slick.geometry.point +function point:left(other) + other:init(self.y, -self.x) +end + +--- @param other slick.geometry.point +function point:right(other) + other:init(-self.y, self.x) +end + +--- @param other slick.geometry.point +--- @return number +function point:dot(other) + return self.x * other.x + self.y * other.y +end + +--- @param other slick.geometry.point +--- @param result slick.geometry.point +function point:add(other, result) + result.x = self.x + other.x + result.y = self.y + other.y +end + +--- @param other number +--- @param result slick.geometry.point +function point:addScalar(other, result) + result.x = self.x + other + result.y = self.y + other +end + +--- @param other slick.geometry.point +--- @param result slick.geometry.point +function point:sub(other, result) + result.x = self.x - other.x + result.y = self.y - other.y +end + +--- @param other number +--- @param result slick.geometry.point +function point:subScalar(other, result) + result.x = self.x - other + result.y = self.y - other +end + +--- @param other slick.geometry.point +--- @param result slick.geometry.point +function point:multiply(other, result) + result.x = self.x * other.x + result.y = self.y * other.y +end + +--- @param other number +--- @param result slick.geometry.point +function point:multiplyScalar(other, result) + result.x = self.x * other + result.y = self.y * other +end + +--- @param other slick.geometry.point +--- @param result slick.geometry.point +function point:divide(other, result) + result.x = self.x / other.x + result.y = self.y / other.y +end + +--- @param other number +--- @param result slick.geometry.point +function point:divideScalar(other, result) + result.x = self.x / other + result.y = self.y / other +end + +--- @return number +function point:lengthSquared() + return self.x ^ 2 + self.y ^ 2 +end + +--- @return number +function point:length() + return math.sqrt(self:lengthSquared()) +end + +--- @param other slick.geometry.point +--- @return number +function point:distanceSquared(other) + return (self.x - other.x) ^ 2 + (self.y - other.y) ^ 2 +end + +--- @param other slick.geometry.point +--- @return number +function point:distance(other) + return math.sqrt(self:distanceSquared(other)) +end + +--- @param result slick.geometry.point +function point:normalize(result) + local length = self:length() + if length > 0 then + result.x = self.x / length + result.y = self.y / length + end +end + +--- @param other slick.geometry.point +--- @param E number +function point:round(other, E) + other.x = self.x + if other.x > -E and other.x < E then + other.x = 0 + end + + other.y = self.y + if other.y > -E and other.y < E then + other.y = 0 + end +end + +--- @param result slick.geometry.point +function point:negate(result) + result.x = -self.x + result.y = -self.y +end + +return point diff --git a/game/love_src/lib/slick/geometry/ray.lua b/game/love_src/lib/slick/geometry/ray.lua new file mode 100644 index 0000000..9cbf834 --- /dev/null +++ b/game/love_src/lib/slick/geometry/ray.lua @@ -0,0 +1,122 @@ +local point = require("slick.geometry.point") +local slickmath = require("slick.util.slickmath") + +--- @class slick.geometry.ray +--- @field origin slick.geometry.point +--- @field direction slick.geometry.point +local ray = {} +local metatable = { __index = ray } + +--- @param origin slick.geometry.point? +--- @param direction slick.geometry.point? +--- @return slick.geometry.ray +function ray.new(origin, direction) + local result = setmetatable({ + origin = point.new(origin and origin.x, origin and origin.y), + direction = point.new(direction and direction.x, direction and direction.y), + }, metatable) + + if result.direction:lengthSquared() > 0 then + result.direction:normalize(result.direction) + end + + return result +end + +--- @param origin slick.geometry.point +--- @param direction slick.geometry.point +function ray:init(origin, direction) + self.origin:init(origin.x, origin.y) + self.direction:init(direction.x, direction.y) + self.direction:normalize(self.direction) +end + +--- @param distance number +--- @param result slick.geometry.point +function ray:project(distance, result) + result:init(self.direction.x, self.direction.y) + result:multiplyScalar(distance, result) + self.origin:add(result, result) +end + +local _cachedHitSegmentPointA = point.new() +local _cachedHitSegmentPointB = point.new() +local _cachedHitSegmentPointC = point.new() +local _cachedHitSegmentPointD = point.new() +local _cachedHitSegmentResult = point.new() +local _cachedHitSegmentDirection = point.new() + +--- @param s slick.geometry.segment +--- @param E number? +--- @return boolean, number?, number?, number? +function ray:hitSegment(s, E) + E = E or 0 + + _cachedHitSegmentPointA:init(s.a.x, s.a.y) + _cachedHitSegmentPointB:init(s.b.x, s.b.y) + + _cachedHitSegmentPointC:init(self.origin.x, self.origin.y) + self.origin:add(self.direction, _cachedHitSegmentPointD) + + local bax = _cachedHitSegmentPointB.x - _cachedHitSegmentPointA.x + local bay = _cachedHitSegmentPointB.y - _cachedHitSegmentPointA.y + local dcx = _cachedHitSegmentPointD.x - _cachedHitSegmentPointC.x + local dcy = _cachedHitSegmentPointD.y - _cachedHitSegmentPointC.y + + local baCrossDC = bax * dcy - bay * dcx + local dcCrossBA = dcx * bay - dcy * bax + if baCrossDC == 0 or dcCrossBA == 0 then + return false + end + + local acx = _cachedHitSegmentPointA.x - _cachedHitSegmentPointC.x + local acy = _cachedHitSegmentPointA.y - _cachedHitSegmentPointC.y + + local dcCrossAC = dcx * acy - dcy * acx + + local u = dcCrossAC / baCrossDC + if u < -E or u > (1 + E) then + return false + end + + + local rx = _cachedHitSegmentPointA.x + bax * u + local ry = _cachedHitSegmentPointA.y + bay * u + + _cachedHitSegmentResult:init(rx, ry) + self.origin:direction(_cachedHitSegmentResult, _cachedHitSegmentDirection) + if _cachedHitSegmentDirection:dot(self.direction) < 0 then + return false + end + + return true, rx, ry, u +end + +--- @param r slick.geometry.rectangle +--- @return boolean, number?, number? +function ray:hitRectangle(r) + -- https://tavianator.com/fast-branchless-raybounding-box-intersections/ + local inverseDirectionX = 1 / self.direction.x + local inverseDirectionY = 1 / self.direction.y + local tMin, tMax + + local tx1 = (r:left() - self.origin.x) * inverseDirectionX + local tx2 = (r:right() - self.origin.x) * inverseDirectionX + + tMin = math.min(tx1, tx2) + tMax = math.max(tx1, tx2) + + local ty1 = (r:top() - self.origin.y) * inverseDirectionY + local ty2 = (r:bottom() - self.origin.y) * inverseDirectionY + + tMin = math.max(tMin, math.min(ty1, ty2)) + tMax = math.min(tMax, math.max(ty1, ty2)) + + if tMax >= tMin then + return true, self.origin.x + self.direction.x * tMin, self.origin.y + self.direction.y * tMin + else + return false + end +end + +return ray diff --git a/game/love_src/lib/slick/geometry/rectangle.lua b/game/love_src/lib/slick/geometry/rectangle.lua new file mode 100644 index 0000000..a3bfb04 --- /dev/null +++ b/game/love_src/lib/slick/geometry/rectangle.lua @@ -0,0 +1,102 @@ +local point = require("slick.geometry.point") +local slickmath = require("slick.util.slickmath") + +--- @class slick.geometry.rectangle +--- @field topLeft slick.geometry.point +--- @field bottomRight slick.geometry.point +local rectangle = {} +local metatable = { + __index = rectangle +} + +--- @param x1 number? +--- @param y1 number? +--- @param x2 number? +--- @param y2 number? +--- @return slick.geometry.rectangle +function rectangle.new(x1, y1, x2, y2) + local result = setmetatable({ topLeft = point.new(), bottomRight = point.new() }, metatable) + result:init(x1, y1, x2, y2) + + return result +end + +--- @param x1 number? +--- @param y1 number? +--- @param x2 number? +--- @param y2 number? +function rectangle:init(x1, y1, x2, y2) + x1 = x1 or 0 + x2 = x2 or x1 + y1 = y1 or 0 + y2 = y2 or y1 + + self.topLeft:init(math.min(x1, x2), math.min(y1, y2)) + self.bottomRight:init(math.max(x1, x2), math.max(y1, y2)) +end + +function rectangle:left() + return self.topLeft.x +end + +function rectangle:right() + return self.bottomRight.x +end + +function rectangle:top() + return self.topLeft.y +end + +function rectangle:bottom() + return self.bottomRight.y +end + +function rectangle:width() + return self:right() - self:left() +end + +function rectangle:height() + return self:bottom() - self:top() +end + +--- @param x number +--- @param y number +function rectangle:expand(x, y) + self.topLeft.x = math.min(self.topLeft.x, x) + self.topLeft.y = math.min(self.topLeft.y, y) + self.bottomRight.x = math.max(self.bottomRight.x, x) + self.bottomRight.y = math.max(self.bottomRight.y, y) +end + +---@param x number +---@param y number +function rectangle:move(x, y) + self.topLeft.x = self.topLeft.x + x + self.topLeft.y = self.topLeft.y + y + self.bottomRight.x = self.bottomRight.x + x + self.bottomRight.y = self.bottomRight.y + y +end + +--- @param x number +--- @param y number +function rectangle:sweep(x, y) + self:expand(x - self:width(), y - self:height()) + self:expand(x + self:width(), y + self:height()) +end + +--- @param other slick.geometry.rectangle +--- @return boolean +function rectangle:overlaps(other) + return self:left() <= other:right() and self:right() >= other:left() and + self:top() <= other:bottom() and self:bottom() >= other:top() +end + +--- @param p slick.geometry.point +--- @param E number? +--- @return boolean +function rectangle:inside(p, E) + E = E or 0 + return slickmath.withinRange(p.x, self:left(), self:right(), E) and slickmath.withinRange(p.y, self:top(), self:bottom(), E) +end + +return rectangle diff --git a/game/love_src/lib/slick/geometry/segment.lua b/game/love_src/lib/slick/geometry/segment.lua new file mode 100644 index 0000000..bb3675d --- /dev/null +++ b/game/love_src/lib/slick/geometry/segment.lua @@ -0,0 +1,211 @@ +local point = require("slick.geometry.point") + +--- @class slick.geometry.segment +--- @field a slick.geometry.point +--- @field b slick.geometry.point +local segment = {} +local metatable = { __index = segment } + +--- @param a slick.geometry.point? +--- @param b slick.geometry.point? +--- @return slick.geometry.segment +function segment.new(a, b) + return setmetatable({ + a = point.new(a and a.x, a and a.y), + b = point.new(b and b.x, b and b.y), + }, metatable) +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +function segment:init(a, b) + self.a:init(a.x, a.y) + self.b:init(b.x, b.y) +end + +--- @return number +function segment:left() + return math.min(self.a.x, self.b.x) +end + +--- @return number +function segment:right() + return math.max(self.a.x, self.b.x) +end + +--- @return number +function segment:top() + return math.min(self.a.y, self.b.y) +end + +--- @return number +function segment:bottom() + return math.max(self.a.y, self.b.y) +end + +--- @param delta number +--- @param result slick.geometry.point +function segment:lerp(delta, result) + result.x = self.b.x * delta + self.a.x * (1 - delta) + result.y = self.b.y * delta + self.a.y * (1 - delta) +end + +local _cachedProjectionBMinusA = point.new() +local _cachedProjectionPMinusA = point.new() + +--- Unlike `project`, this treats the line segment as a line. +--- @param p slick.geometry.point +--- @param result slick.geometry.point +--- @return number +function segment:projectLine(p, result) + local distanceSquared = self.a:distanceSquared(self.b) + if distanceSquared == 0 then + result:init(self.a.x, self.a.y) + return 0 + end + + p:sub(self.a, _cachedProjectionPMinusA) + self.b:sub(self.a, _cachedProjectionBMinusA) + + local t = _cachedProjectionPMinusA:dot(_cachedProjectionBMinusA) / distanceSquared + + _cachedProjectionBMinusA:multiplyScalar(t, result) + self.a:add(result, result) + + return t +end + +--- @param p slick.geometry.point +--- @param result slick.geometry.point +--- @return number +function segment:project(p, result) + local distanceSquared = self.a:distanceSquared(self.b) + if distanceSquared == 0 then + result:init(self.a.x, self.a.y) + return 0 + end + + p:sub(self.a, _cachedProjectionPMinusA) + self.b:sub(self.a, _cachedProjectionBMinusA) + + local t = math.max(0, math.min(1, _cachedProjectionPMinusA:dot(_cachedProjectionBMinusA) / distanceSquared)) + + _cachedProjectionBMinusA:multiplyScalar(t, result) + self.a:add(result, result) + + return t +end + +local _cachedDistanceProjectedAB = point.new() +--- @param p slick.geometry.point +function segment:distanceSquared(p) + self:project(p, _cachedDistanceProjectedAB) + return _cachedDistanceProjectedAB:distanceSquared(p) +end + +--- @param p slick.geometry.point +function segment:distance(p) + return math.sqrt(self:distanceSquared(p)) +end + +--- @alias slick.geometry.segmentCompareFunc fun(a: slick.geometry.segment, b: slick.geometry.segment): slick.util.search.compareResult + +--- @param a slick.geometry.segment +--- @param b slick.geometry.segment +--- @param E number? +--- @return slick.util.search.compareResult +function segment.compare(a, b, E) + local aMinPoint, aMaxPoint + if a.b:lessThan(a.a) then + aMinPoint = a.b + aMaxPoint = a.a + else + aMinPoint = a.a + aMaxPoint = a.b + end + + local bMinPoint, bMaxPoint + if b.b:lessThan(b.a) then + bMinPoint = b.b + bMaxPoint = b.a + else + bMinPoint = b.a + bMaxPoint = b.b + end + + local s = point.compare(aMinPoint, bMinPoint, E) + if s ~= 0 then + return s + end + + return point.compare(aMaxPoint, bMaxPoint, E) +end + +--- @param a slick.geometry.segment +--- @param b slick.geometry.segment +--- @return boolean +function segment.less(a, b) + return segment.compare(a, b) < 0 +end + +--- @param other slick.geometry.segment +--- @return boolean +function segment:lessThan(other) + return self.a:lessThan(other.a) or + (self.a:equal(other.a) and self.b:lessThan(other.b)) +end + +--- @param other slick.geometry.segment +--- @return boolean +function segment:overlap(other) + local selfLeft = math.min(self.a.x, self.b.x) + local selfRight = math.max(self.a.x, self.b.x) + local selfTop = math.min(self.a.y, self.b.y) + local selfBottom = math.max(self.a.y, self.b.y) + + local otherLeft = math.min(other.a.x, other.b.x) + local otherRight = math.max(other.a.x, other.b.x) + local otherTop = math.min(other.a.y, other.b.y) + local otherBottom = math.max(other.a.y, other.b.y) + + return (selfLeft <= otherRight and selfRight >= otherLeft) and + (selfTop <= otherBottom and selfBottom >= otherTop) +end + +--- @param other slick.geometry.segment +--- @param E number? +--- @return boolean +--- @return number? +--- @return number? +--- @return number? +function segment:intersection(other, E) + E = E or 0 + + local bax = self.b.x - self.a.x + local bay = self.b.y - self.a.y + local dcx = other.b.x - other.a.x + local dcy = other.b.y - other.a.y + + local baCrossDC = bax * dcy - bay * dcx + local dcCrossBA = dcx * bay - dcy * bax + if baCrossDC == 0 or dcCrossBA == 0 then + return false, nil, nil, nil + end + + local acx = self.a.x - other.a.x + local acy = self.a.y - other.a.y + + local dcCrossAC = dcx * acy - dcy * acx + + local u = dcCrossAC / baCrossDC + if u < -E or u > (1 + E) then + return false + end + + local rx = self.a.x + bax * u + local ry = self.a.y + bay * u + + return true, rx, ry, u +end + +return segment diff --git a/game/love_src/lib/slick/geometry/simple.lua b/game/love_src/lib/slick/geometry/simple.lua new file mode 100644 index 0000000..ed052f3 --- /dev/null +++ b/game/love_src/lib/slick/geometry/simple.lua @@ -0,0 +1,186 @@ +local clipper = require "slick.geometry.clipper" +local delaunay = require "slick.geometry.triangulation.delaunay" +local util = require "slick.util" +local slickmath = require "slick.util.slickmath" + +local simple = {} + +--- @param contours number[][] +--- @return number[], number[] +local function _getPointEdges(contours) + local points = {} + local edges = {} + + for _, contour in ipairs(contours) do + local numPoints = #points + for j = 1, #contour, 2 do + table.insert(points, contour[j]) + table.insert(points, contour[j + 1]) + + table.insert(edges, (numPoints / 2) + (j + 1) / 2) + table.insert(edges, (numPoints / 2) + (slickmath.wrap(j, 2, #contour) + 1) / 2) + end + end + + return points, edges +end + +--- @param points number[] +--- @param polygons number[][]? +--- @return number[][] +local function _getPolygons(points, polygons) + local result = {} + + if not polygons then + return result + end + + for _, polygon in ipairs(polygons) do + local resultPolygon = {} + for _, vertex in ipairs(polygon) do + local i = (vertex - 1) * 2 + 1 + + table.insert(resultPolygon, points[i]) + table.insert(resultPolygon, points[i + 1]) + end + table.insert(result, resultPolygon) + end + + return result +end + +local triangulateOptions = { + refine = true, + interior = true, + exterior = false, + polygonization = false +} + +--- @param contours number[][] +--- @return number[][] +function simple.triangulate(contours) + local points, edges = _getPointEdges(contours) + + local triangulator = delaunay.new() + local cleanPoints, cleanEdges = triangulator:clean(points, edges) + local triangles = triangulator:triangulate(cleanPoints, cleanEdges, triangulateOptions) + + return _getPolygons(cleanPoints, triangles) +end + +local polygonizeOptions = { + refine = true, + interior = true, + exterior = false, + polygonization = true, + maxPolygonVertexCount = math.huge +} + +--- @overload fun(n: number, contours: number[][]): number[][] +--- @overload fun(contours: number[][]): number[][] +--- @return number[][] +function simple.polygonize(n, b) + local points, edges + if type(n) == "number" then + polygonizeOptions.maxPolygonVertexCount = math.max(n, 3) + points, edges = _getPointEdges(b) + else + polygonizeOptions.maxPolygonVertexCount = math.huge + points, edges = _getPointEdges(n) + end + + local triangulator = delaunay.new() + local cleanPoints, cleanEdges = triangulator:clean(points, edges) + local _, _, polygons = triangulator:triangulate(cleanPoints, cleanEdges, polygonizeOptions) + + return _getPolygons(cleanPoints, polygons) +end + +--- @class slick.simple.clipOperation +--- @field operation slick.geometry.clipper.clipOperation +--- @field subject number[][] | slick.simple.clipOperation +--- @field other number[][] | slick.simple.clipOperation +local clipOperation = {} +local clipOperationMetatable = { __index = clipOperation } + +--- @package +--- @param clipper slick.geometry.clipper +function clipOperation:perform(clipper) + local subjectPoints, subjectEdges + if util.is(self.subject, clipOperation) then + subjectPoints, subjectEdges = self.subject:perform(clipper) + else + subjectPoints, subjectEdges = _getPointEdges(self.subject) + end + + local otherPoints, otherEdges + if util.is(self.other, clipOperation) then + otherPoints, otherEdges = self.other:perform(clipper) + else + otherPoints, otherEdges = _getPointEdges(self.other) + end + + return clipper:clip(self.operation, subjectPoints, subjectEdges, otherPoints, otherEdges) +end + +--- @param subject number[][] | slick.simple.clipOperation +--- @param other number[][] | slick.simple.clipOperation +--- @return slick.simple.clipOperation +function simple.newUnionClipOperation(subject, other) + return setmetatable({ + operation = clipper.union, + subject = subject, + other = other + }, clipOperationMetatable) +end + +--- @param subject number[][] | slick.simple.clipOperation +--- @param other number[][] | slick.simple.clipOperation +--- @return slick.simple.clipOperation +function simple.newDifferenceClipOperation(subject, other) + return setmetatable({ + operation = clipper.difference, + subject = subject, + other = other + }, clipOperationMetatable) +end + +--- @param subject number[][] | slick.simple.clipOperation +--- @param other number[][] | slick.simple.clipOperation +--- @return slick.simple.clipOperation +function simple.newIntersectionClipOperation(subject, other) + return setmetatable({ + operation = clipper.intersection, + subject = subject, + other = other + }, clipOperationMetatable) +end + + +--- @param operation slick.simple.clipOperation +--- @param maxVertexCount number? +--- @return number[][] +function simple.clip(operation, maxVertexCount) + maxVertexCount = math.max(maxVertexCount or 3, 3) + + assert(util.is(operation, clipOperation)) + + local triangulator = delaunay.new() + local c = clipper.new(triangulator) + + local clippedPoints, clippedEdges = operation:perform(c) + + local result + if maxVertexCount == 3 then + local triangles = triangulator:triangulate(clippedPoints, clippedEdges, triangulateOptions) + result = triangles + else + polygonizeOptions.maxPolygonVertexCount = maxVertexCount + local _, _, polygons = triangulator:triangulate(clippedPoints, clippedEdges, polygonizeOptions) + result = polygons + end + + return _getPolygons(clippedPoints, result) +end + +return simple diff --git a/game/love_src/lib/slick/geometry/transform.lua b/game/love_src/lib/slick/geometry/transform.lua new file mode 100644 index 0000000..083063a --- /dev/null +++ b/game/love_src/lib/slick/geometry/transform.lua @@ -0,0 +1,152 @@ +local slickmath = require("slick.util.slickmath") + +--- Represents a transform. +--- @class slick.geometry.transform +--- @field private immutable boolean +--- @field x number +--- @field y number +--- @field rotation number +--- @field private rotationCos number +--- @field private rotationSin number +--- @field scaleX number +--- @field scaleY number +--- @field offsetX number +--- @field offsetY number +local transform = {} +local metatable = { __index = transform } + +--- Constructs a new transform. +--- @param x number? translation on the x axis (defaults to 0) +--- @param y number? translation on the y axis (defaults to 0) +--- @param rotation number? rotation in radians (defaults to 0) +--- @param scaleX number? scale along the x axis (defaults to 1) +--- @param scaleY number? scale along the y axis (defaults to 1) +--- @param offsetX number? offset along the x axis (defaults to 0) +--- @param offsetY number? offsete along the y axis (defaults to 0) +function transform.new(x, y, rotation, scaleX, scaleY, offsetX, offsetY) + local result = setmetatable({}, metatable) + result:setTransform(x, y, rotation, scaleX, scaleY, offsetX, offsetY) + result.immutable = false + + return result +end + +--- @package +--- @param x number? translation on the x axis (defaults to 0) +--- @param y number? translation on the y axis (defaults to 0) +--- @param rotation number? rotation in radians (defaults to 0) +--- @param scaleX number? scale along the x axis (defaults to 1) +--- @param scaleY number? scale along the y axis (defaults to 1) +--- @param offsetX number? offset along the x axis (defaults to 0) +--- @param offsetY number? offsete along the y axis (defaults to 0) +function transform._newImmutable(x, y, rotation, scaleX, scaleY, offsetX, offsetY) + local result = setmetatable({}, metatable) + result:setTransform(x, y, rotation, scaleX, scaleY, offsetX, offsetY) + result.immutable = true + + return result +end + +--- Same as setTransform. +--- @param x number? translation on the x axis +--- @param y number? translation on the y axis +--- @param rotation number? rotation in radians +--- @param scaleX number? scale along the x axis +--- @param scaleY number? scale along the y axis +--- @param offsetX number? offset along the x axis +--- @param offsetY number? offsete along the y axis +--- @see slick.geometry.transform.setTransform +function transform:init(x, y, rotation, scaleX, scaleY, offsetX, offsetY) + self:setTransform(x, y, rotation, scaleX, scaleY, offsetX, offsetY) +end + +--- Constructs a transform. +--- @param x number? translation on the x axis +--- @param y number? translation on the y axis +--- @param rotation number? rotation in radians +--- @param scaleX number? scale along the x axis +--- @param scaleY number? scale along the y axis +--- @param offsetX number? offset along the x axis +--- @param offsetY number? offsete along the y axis +function transform:setTransform(x, y, rotation, scaleX, scaleY, offsetX, offsetY) + self.x = x or self.x or 0 + self.y = y or self.y or 0 + self.rotation = rotation or self.rotation or 0 + self.rotationCos = math.cos(self.rotation) + self.rotationSin = math.sin(self.rotation) + self.scaleX = scaleX or self.scaleX or 1 + self.scaleY = scaleY or self.scaleY or 1 + self.offsetX = offsetX or self.offsetX or 0 + self.offsetY = offsetY or self.offsetY or 0 +end + +--- Transforms (x, y) by the transform and returns the transformed coordinates. +--- @param x number +--- @param y number +--- @return number x +--- @return number y +function transform:transformPoint(x, y) + local ox = x - self.offsetX + local oy = y - self.offsetY + local rx = ox * self.rotationCos - oy * self.rotationSin + local ry = ox * self.rotationSin + oy * self.rotationCos + local sx = rx * self.scaleX + local sy = ry * self.scaleY + local resultX = sx + self.x + local resultY = sy + self.y + + return resultX, resultY +end + +--- Transforms the normal (x, y) by this transform. +--- This is essentially the inverse-transpose of just the rotation and scale components. +--- @param x number +--- @param y number +--- @return number x +--- @return number y +function transform:transformNormal(x, y) + local sx = x / self.scaleX + local sy = y / self.scaleY + local resultX = sx * self.rotationCos - sy * self.rotationSin + local resultY = sx * self.rotationSin + sy * self.rotationCos + + return resultX, resultY +end + +--- Transforms (x, y) by the inverse of the transform and returns the inverse transformed coordinates. +--- @param x number +--- @param y number +--- @return number x +--- @return number y +function transform:inverseTransformPoint(x, y) + local tx = x - self.x + local ty = y - self.y + local sx = tx / self.scaleX + local sy = ty / self.scaleY + local rx = sx * self.rotationCos + sy * self.rotationSin + local ry = sy * self.rotationCos - sx * self.rotationSin + local resultX = rx + self.offsetX + local resultY = ry + self.offsetY + + return resultX, resultY +end + +--- Copies this transform to `other`. +--- @param other slick.geometry.transform +function transform:copy(other) + assert(not other.immutable) + + other.x = self.x + other.y = self.y + other.rotation = self.rotation + other.rotationCos = self.rotationCos + other.rotationSin = self.rotationSin + other.scaleX = self.scaleX + other.scaleY = self.scaleY + other.offsetX = self.offsetX + other.offsetY = self.offsetY +end + +transform.IDENTITY = transform._newImmutable() + +return transform diff --git a/game/love_src/lib/slick/geometry/triangulation/delaunay.lua b/game/love_src/lib/slick/geometry/triangulation/delaunay.lua new file mode 100644 index 0000000..0c1edc9 --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/delaunay.lua @@ -0,0 +1,1879 @@ +local point = require("slick.geometry.point") +local segment = require("slick.geometry.segment") +local dissolve = require("slick.geometry.triangulation.dissolve") +local edge = require("slick.geometry.triangulation.edge") +local hull = require("slick.geometry.triangulation.hull") +local intersection = require("slick.geometry.triangulation.intersection") +local delaunaySortedEdge = require("slick.geometry.triangulation.delaunaySortedEdge") +local delaunaySortedPoint = require("slick.geometry.triangulation.delaunaySortedPoint") +local map = require("slick.geometry.triangulation.map") +local sweep = require("slick.geometry.triangulation.sweep") +local pool = require("slick.util.pool") +local search = require("slick.util.search") +local slickmath = require("slick.util.slickmath") +local slicktable = require("slick.util.slicktable") + +--- @class slick.geometry.triangulation.delaunayTriangulationOptions +--- @field public refine boolean? +--- @field public interior boolean? +--- @field public exterior boolean? +--- @field public polygonization boolean? +--- @field public maxPolygonVertexCount number? +local defaultTriangulationOptions = { + refine = true, + interior = true, + exterior = false, + polygonization = true, + maxPolygonVertexCount = math.huge +} + +--- @alias slick.geometry.triangulation.intersectFunction fun(intersection: slick.geometry.triangulation.intersection) +--- @alias slick.geometry.triangulation.dissolveFunction fun(dissolve: slick.geometry.triangulation.dissolve) +--- @alias slick.geometry.triangulation.mapFunction fun(map: slick.geometry.triangulation.map) + +--- @class slick.geometry.triangulation.delaunayCleanupOptions +--- @field public intersect slick.geometry.triangulation.intersectFunction? +--- @field public dissolve slick.geometry.triangulation.dissolveFunction? +--- @field public map slick.geometry.triangulation.mapFunction? +local defaultCleanupOptions = { + intersect = intersection.default, + dissolve = dissolve.default, + map = map.default +} + +--- @alias slick.geometry.triangulation.delaunayWorkingPolygon { vertices: number[], merged: boolean? } + +--- @class slick.geometry.triangulation.delaunay +--- @field epsilon number (read-only) +--- @field debug boolean (read-only) +--- @field private pointsPool slick.util.pool +--- @field private points slick.geometry.point[] +--- @field private pointsToEdges table +--- @field private sortedPointsPool slick.util.pool +--- @field private sortedPoints slick.geometry.triangulation.delaunaySortedPoint[] +--- @field private intersection slick.geometry.triangulation.intersection +--- @field private dissolve slick.geometry.triangulation.dissolve +--- @field private map slick.geometry.triangulation.map +--- @field private segmentsPool slick.util.pool +--- @field private edgesPool slick.util.pool +--- @field private sortedEdgesPool slick.util.pool +--- @field private cachedSegment slick.geometry.segment +--- @field private cachedEdge slick.geometry.triangulation.edge +--- @field private activeEdges slick.geometry.triangulation.delaunaySortedEdge[] +--- @field private temporaryEdges slick.geometry.triangulation.edge[] +--- @field private sortedEdges slick.geometry.triangulation.delaunaySortedEdge[] +--- @field private pendingEdges slick.geometry.triangulation.edge[] | number[] +--- @field private edges slick.geometry.triangulation.edge[] +--- @field private sweepPool slick.util.pool +--- @field private sweeps slick.geometry.triangulation.sweep[] +--- @field private hullsPool slick.util.pool +--- @field private hulls slick.geometry.triangulation.hull[] +--- @field private cachedTriangle number[] +--- @field private triangulation { n: number, triangles: number[][], sorted: number[][], unsorted: number[][] } +--- @field private filter { flags: number[], neighbors: number[], constraints: boolean[], current: number[], next: number[] } +--- @field private index { n: number, vertices: number[][], triangles: number[][], stack: number[] } +--- @field private polygonization { n: number, polygons: slick.geometry.triangulation.delaunayWorkingPolygon[], edges: slick.geometry.triangulation.edge[], pending: slick.geometry.triangulation.edge[], edgesToPolygons: slick.geometry.triangulation.delaunayWorkingPolygon[][] } +local delaunay = {} +local metatable = { __index = delaunay } + +--- @param triangle number[] +--- @return number +--- @return number +--- @return number +local function _unpackTriangle(triangle) + return triangle[1], triangle[2], triangle[3] +end + +--- @param a number +--- @param b number +--- @param c number +local function _sortTriangle(a, b, c) + local x, y, z = a, b, c + if b < c then + if b < a then + x = b + y = c + z = a + end + elseif c < a then + x = c + y = a + z = b + end + + return x, y, z +end + +--- @param value number +local function _greaterThanZero(value) + return value > 0 +end + +--- @param value number +local function _lessThanZero(value) + return value < 0 +end + +--- @param a number[] +--- @param b number[] +local function _compareTriangle(a, b) + local ai, aj, ak = _sortTriangle(_unpackTriangle(a)) + local bi, bj, bk = _sortTriangle(_unpackTriangle(b)) + + if ai == bi then + if aj == bj then + return ak - bk + else + return aj - bj + end + end + + return ai - bi +end + +--- @param a number[] +--- @param b number[] +local function _lessTriangle(a, b) + return _compareTriangle(a, b) < 0 +end + +--- @param p slick.geometry.triangulation.delaunaySortedPoint +--- @param id number +local function _compareSortedPointID(p, id) + return p.id - id +end + +--- @param a slick.geometry.triangulation.delaunaySortedPoint +--- @param b slick.geometry.triangulation.delaunaySortedPoint +local function _lessSortedPointID(a, b) + return _compareSortedPointID(a, b.id) < 0 +end + +--- @param e slick.geometry.triangulation.delaunaySortedEdge +--- @param x number +--- @return slick.util.search.compareResult +local function _compareSortedEdgeX(e, x) + return slickmath.sign(e.segment:right() - x) +end + +--- @param e slick.geometry.triangulation.delaunaySortedEdge +--- @param p slick.geometry.point +--- @return slick.util.search.compareResult +local function _compareSortedEdgePoint(e, p) + local left = e.segment:left() + + return slickmath.sign(left - p.x) +end + +--- @class slick.geometry.triangulation.delaunayOptions +local defaultDelaunayOptions = { + epsilon = slickmath.EPSILON, + debug = false +} + +--- @param options slick.geometry.triangulation.delaunayOptions? +function delaunay.new(options) + options = options or defaultDelaunayOptions + local epsilon = options.epsilon or defaultDelaunayOptions.epsilon + local debug = options.debug == nil and defaultDelaunayOptions.debug or not not options.debug + + return setmetatable({ + epsilon = epsilon, + debug = debug, + + pointsPool = pool.new(point), + points = {}, + pointsToEdges = {}, + + intersection = intersection.new(), + dissolve = dissolve.new(), + map = map.new(), + + sortedPointsPool = pool.new(delaunaySortedPoint), + sortedPoints = {}, + + segmentsPool = pool.new(segment), + edgesPool = pool.new(edge), + sortedEdgesPool = pool.new(delaunaySortedEdge), + activeEdges = {}, + temporaryEdges = {}, + pendingEdges = {}, + cachedSegment = segment.new(point.new(), point.new()), + cachedEdge = edge.new(0, 0), + sortedEdges = {}, + edges = {}, + + sweepPool = pool.new(sweep), + sweeps = {}, + + hullsPool = pool.new(hull), + hulls = {}, + + cachedTriangle = { 0, 0, 0 }, + triangulation = { n = 0, triangles = {}, sorted = {}, unsorted = {} }, + filter = { flags = {}, neighbors = {}, constraints = {}, current = {}, next = {} }, + index = { n = 0, vertices = {}, triangles = {}, stack = {} }, + polygonization = { n = 0, polygons = {}, edges = {}, pending = {}, edgesToPolygons = {} } + }, metatable) +end + +--- @private +function delaunay:_debugVerifyPoints() + local sortedPoints = self.sortedPoints + for i = 1, #sortedPoints do + for j = i + 1, #sortedPoints do + local a = sortedPoints[i] + local b = sortedPoints[j] + + assert(a.point:distance(b.point) >= self.epsilon) + end + end + + local sortedEdges = self.sortedEdges + local edges = self.edges + + assert(#sortedEdges == #edges) + + for i = 1, #edges do + local found = false + for j = 1, #sortedEdges do + if sortedEdges[j].edge.min == edges[i].min and sortedEdges[j].edge.max == edges[i].max then + found = true + break + end + end + + assert(found) + end + + for i = 1, #sortedEdges do + local found = false + for j = 1, #edges do + if sortedEdges[j].edge.min == edges[i].min and sortedEdges[j].edge.max == edges[i].max then + found = true + break + end + end + + assert(found) + end + + for i = 2, #edges do + assert(edge.compare(edges[i - 1], edges[i]) <= 0) + assert(edge.compare(edges[i], edges[i - 1]) >= 0) + end +end + +--- @private +--- @param dissolve slick.geometry.triangulation.dissolveFunction +--- @return boolean +function delaunay:_dedupePoints(dissolve, userdata) + local didDedupe = false + + local edges = self.edges + local points = self.points + local sortedPoints = self.sortedPoints + local pendingEdges = self.pendingEdges + + if self.debug then + self:_debugVerifyEdges() + end + + slicktable.clear(pendingEdges) + + local index = 1 + while index <= #sortedPoints do + local sortedPoint = sortedPoints[index] + + local nextIndex = index + 1 + while nextIndex <= #sortedPoints and sortedPoint.point:distance(sortedPoints[nextIndex].point) < self.epsilon do + didDedupe = true + + local nextPoint = sortedPoints[nextIndex] + self.dissolve:init(sortedPoint.point, sortedPoint.id, userdata and userdata[sortedPoint.id], nextPoint.id, userdata and userdata[nextPoint.id]) + dissolve(self.dissolve) + + if self.dissolve.resultUserdata ~= nil then + userdata[sortedPoint.id] = self.dissolve.resultUserdata + end + + local pointEdges = self.pointsToEdges[nextPoint.id] + for i = #pointEdges, 1, -1 do + local e = pointEdges[i] + + if e.a == nextPoint.id or e.b == nextPoint.id then + local index = search.lessThanEqual(pendingEdges, e, edge.compare) + + --- @cast pendingEdges slick.geometry.triangulation.edge[] + if not (index > 0 and edge.compare(pendingEdges[index], e) == 0) then + table.insert(pendingEdges, index + 1, e) + end + end + end + + self.sortedPointsPool:deallocate(table.remove(sortedPoints, nextIndex)) + end + + index = nextIndex + end + + if self.debug then + self:_debugVerifyEdges() + end + + for _, e in ipairs(pendingEdges) do + self.cachedSegment:init(points[e.a], points[e.b]) + local pointA = sortedPoints[search.first(sortedPoints, points[e.a], delaunaySortedPoint.comparePoint)] + local pointB = sortedPoints[search.first(sortedPoints, points[e.b], delaunaySortedPoint.comparePoint)] + + if pointA and pointB then + self.cachedEdge:init(e.a, e.b) + local hasOldEdge = search.first(edges, self.cachedEdge, edge.compare) ~= nil + + self.cachedEdge:init(pointA.id, pointB.id) + local hasNewEdge = search.first(edges, self.cachedEdge, edge.compare) ~= nil + + self:_dissolveEdge(e.a, e.b) + if pointA.id ~= pointB.id and hasOldEdge and not hasNewEdge then + self:_addEdge(pointA.id, pointB.id) + self:_addSortedEdge(pointA.id, pointB.id) + end + end + end + + if self.debug then + self:_debugVerifyPoints() + end + + return didDedupe +end + +--- @private +function delaunay:_debugVerifyEdges() + local edges = self.edges + local sortedEdges = self.sortedEdges + + for i = 1, #edges do + local found = false + for j = 1, #sortedEdges do + if sortedEdges[j].edge.min == edges[i].min and sortedEdges[j].edge.max == edges[i].max then + found = true + break + end + end + + assert(found) + end + + for i = 1, #sortedEdges do + local found = false + for j = 1, #edges do + if sortedEdges[j].edge.min == edges[i].min and sortedEdges[j].edge.max == edges[i].max then + found = true + break + end + end + + assert(found) + end + + for i = 2, #edges do + assert(edge.compare(edges[i - 1], edges[i]) <= 0) + assert(edge.compare(edges[i], edges[i - 1]) >= 0) + end +end + +--- @private +function delaunay:_debugVerifyDuplicateEdges() + local edges = self.edges + + for i = 1, #edges do + for j = i + 1, #edges do + local a = edges[i] + local b = edges[j] + assert(not (a.min == b.min and a.max == b.max)) + end + end +end + +--- @private +--- @return boolean +function delaunay:_dedupeEdges() + local didDedupe = false + local edges = self.edges + + local index = 1 + while index < #edges - 1 do + local e = edges[index] + local n = edges[index + 1] + + if e.a == e.b then + didDedupe = true + self:_dissolveEdge(e.a, e.b) + elseif e.min == n.min and e.max == n.max then + didDedupe = true + self:_dissolveEdge(e.a, e.b) + else + index = index + 1 + end + end + + if self.debug then + self:_debugVerifyEdges() + self:_debugVerifyDuplicateEdges() + end + + return didDedupe +end + +local function _greater(a, b) + return a > b +end + +--- @private +function delaunay:_splitEdgesAgainstPoints(intersect, userdata) + local points = self.points + local sortedPoints = self.sortedPoints + local sortedEdges = self.sortedEdges + + for i = #sortedEdges, 1, -1 do + local sortedEdge = sortedEdges[i] + local e = sortedEdge.edge + local s = sortedEdge.segment + + local start = math.max(search.lessThanEqual(sortedPoints, s.a, delaunaySortedPoint.comparePoint), 1) + local stop = search.lessThanEqual(sortedPoints, s.b, delaunaySortedPoint.comparePoint, start) + + local dissolve = false + for j = stop, start, -1 do + local sortedPoint = sortedPoints[j] + if e.a ~= sortedPoint.id and e.b ~= sortedPoint.id then + local intersection, x, y = slickmath.intersection(s.a, s.b, sortedPoint.point, sortedPoint.point) + if intersection and not (x and y) then + self:_addEdge(e.a, sortedPoint.id) + self:_addEdge(sortedPoint.id, e.b) + + self:_addSortedEdge(e.a, sortedPoint.id) + self:_addSortedEdge(sortedPoint.id, e.b) + + self.intersection:init(sortedPoint.id) + + self.intersection:setLeftEdge( + points[e.a], points[sortedPoint.id], + e.a, sortedPoint.id, + 1, + userdata and userdata[e.a], + userdata and userdata[sortedPoint.id]) + + self.intersection:setRightEdge( + points[sortedPoint.id], points[e.b], + sortedPoint.id, e.b, + 1, + userdata and userdata[sortedPoint.id], + userdata and userdata[e.b]) + + self.intersection.result:init(sortedPoint.point.x, sortedPoint.point.y) + + intersect(self.intersection) + + if userdata then + if self.intersection.resultUserdata ~= nil then + userdata[self.intersection.resultIndex] = self.intersection.resultUserdata + end + + userdata[self.intersection.a1Index] = self.intersection.a1Userdata + userdata[self.intersection.b1Index] = self.intersection.b1Userdata + userdata[self.intersection.a2Index] = self.intersection.a2Userdata + userdata[self.intersection.b2Index] = self.intersection.b2Userdata + end + + dissolve = true + end + end + end + + if dissolve then + self:_dissolveEdge(e.a, e.b) + end + end + + if self.debug then + self:_debugVerifyEdges() + end +end + +--- @private +--- @param intersect slick.geometry.triangulation.intersectFunction +--- @param userdata any[]? +--- @return boolean +function delaunay:_splitEdgesAgainstEdges(intersect, userdata) + local isDirty = false + + local points = self.points + local sortedPoints = self.sortedPoints + local edges = self.edges + local sortedEdges = self.sortedEdges + local temporaryEdges = self.temporaryEdges + local pendingEdges = self.pendingEdges + local activeEdges = self.activeEdges + + slicktable.clear(temporaryEdges) + slicktable.clear(pendingEdges) + slicktable.clear(activeEdges) + + local rightEdge = sortedEdges[1] and sortedEdges[1].segment:right() or math.huge + table.insert(activeEdges, sortedEdges[1]) + + for i = 2, #sortedEdges do + local selfEdge = sortedEdges[i] + + local leftEdge = selfEdge.segment:left() + + if leftEdge > rightEdge then + rightEdge = leftEdge + + local stop = search.lessThan(activeEdges, leftEdge, _compareSortedEdgeX) + for j = stop, 1, -1 do + assert(activeEdges[j].segment:right() < leftEdge) + table.remove(activeEdges, j) + end + end + + local intersected = false + for j, otherEdge in ipairs(activeEdges) do + local overlaps = selfEdge.segment:overlap(otherEdge.segment) + local connected = (selfEdge.edge.a == otherEdge.edge.a or selfEdge.edge.a == otherEdge.edge.b or selfEdge.edge.b == otherEdge.edge.a or selfEdge.edge.b == otherEdge.edge.b) + + if overlaps and not connected then + local a1 = points[selfEdge.edge.a] + local b1 = points[selfEdge.edge.b] + local a2 = points[otherEdge.edge.a] + local b2 = points[otherEdge.edge.b] + + local intersection, x, y, u, v = slickmath.intersection(a1, b1, a2, b2) + if intersection and x and y and u and v then + intersected = true + isDirty = true + + -- Edges intersect. + self:_addPoint(x, y) + + local point = points[#points] + local sortedPoint = self:_newSortedPoint(#points) + table.insert(sortedPoints, search.lessThan(sortedPoints, sortedPoint, sortedPoint.compare) + 1, sortedPoint) + + table.insert(temporaryEdges, self:_newEdge(selfEdge.edge.a, sortedPoint.id)) + table.insert(temporaryEdges, self:_newEdge(sortedPoint.id, selfEdge.edge.b)) + table.insert(temporaryEdges, self:_newEdge(otherEdge.edge.a, sortedPoint.id)) + table.insert(temporaryEdges, self:_newEdge(sortedPoint.id, otherEdge.edge.b)) + + self.intersection:init(#points) + + self.intersection:setLeftEdge( + a1, b1, + selfEdge.edge.a, selfEdge.edge.b, + u, + userdata and userdata[selfEdge.edge.a], + userdata and userdata[selfEdge.edge.b]) + + self.intersection:setRightEdge( + a2, b2, + otherEdge.edge.a, otherEdge.edge.b, + v, + userdata and userdata[otherEdge.edge.a], + userdata and userdata[otherEdge.edge.b]) + + self.intersection.result:init(point.x, point.y) + + intersect(self.intersection) + point:init(self.intersection.result.x, self.intersection.result.y) + + if userdata then + if self.intersection.resultUserdata ~= nil then + userdata[self.intersection.resultIndex] = self.intersection.resultUserdata + end + + userdata[self.intersection.a1Index] = self.intersection.a1Userdata + userdata[self.intersection.b1Index] = self.intersection.b1Userdata + userdata[self.intersection.a2Index] = self.intersection.a2Userdata + userdata[self.intersection.b2Index] = self.intersection.b2Userdata + end + + table.insert(pendingEdges, search.first(edges, selfEdge.edge, edge.compare)) + table.insert(pendingEdges, search.first(edges, otherEdge.edge, edge.compare)) + + table.remove(activeEdges, j) + + break + end + end + end + + if not intersected then + local index = search.lessThan(activeEdges, selfEdge.segment:right(), _compareSortedEdgeX) + table.insert(activeEdges, index + 1, selfEdge) + end + end + + table.sort(pendingEdges, _greater) + for i = 1, #pendingEdges do + local index = pendingEdges[i] + local previousIndex = i > 1 and pendingEdges[i - 1] + + if index ~= previousIndex and type(index) == "number" then + local e = edges[index] + + self:_dissolveEdge(e.a, e.b) + end + end + + for _, e in ipairs(temporaryEdges) do + self:_addEdge(e.a, e.b) + self:_addSortedEdge(e.a, e.b) + + self.edgesPool:deallocate(e) + end + + if self.debug then + self:_debugVerifyEdges() + end + + return isDirty +end + +--- @param points number[] +--- @param edges number[] +--- @param userdata any[]? +--- @param options slick.geometry.triangulation.delaunayCleanupOptions? +--- @param outPoints number[]? +--- @param outEdges number[]? +--- @param outUserdata any[]? +function delaunay:clean(points, edges, userdata, options, outPoints, outEdges, outUserdata) + options = options or defaultCleanupOptions + + local dissolveFunc = options.dissolve == nil and defaultCleanupOptions.dissolve or options.dissolve + dissolveFunc = dissolveFunc or dissolve.default + + local intersectFunc = options.intersect == nil and defaultCleanupOptions.intersect or options.intersect + intersectFunc = intersectFunc or intersection.default + + local mapFunc = options.map == nil and defaultCleanupOptions.map or options.map + mapFunc = mapFunc or map.default + + self:reset() + + for i = 1, #points, 2 do + local x, y = points[i], points[i + 1] + self:_addPoint(x, y) + + local index = #self.points + local sortedPoint = self:_newSortedPoint(index) + table.insert(self.sortedPoints, search.lessThan(self.sortedPoints, sortedPoint, delaunaySortedPoint.compare) + 1, sortedPoint) + end + + if edges then + for i = 1, #edges, 2 do + local e1 = edges[i] + local e2 = edges[i + 1] + + if e1 ~= e2 then + self:_addEdge(e1, e2) + self:_addSortedEdge(e1, e2) + end + end + end + + local continue + repeat + continue = false + + self:_dedupePoints(dissolveFunc, userdata) + self:_dedupeEdges() + self:_splitEdgesAgainstPoints(intersectFunc, userdata) + + continue = self:_splitEdgesAgainstEdges(intersectFunc, userdata) + until not continue + + table.sort(self.sortedPoints, _lessSortedPointID) + + outPoints = outPoints or {} + outEdges = outEdges or {} + + slicktable.clear(outPoints) + slicktable.clear(outEdges) + + if userdata then + outUserdata = outUserdata or {} + slicktable.clear(outUserdata) + end + + local currentPointIndex = 1 + for i = 1, #self.sortedPoints do + local sortedPoint = self.sortedPoints[i] + sortedPoint.newID = currentPointIndex + currentPointIndex = currentPointIndex + 1 + + table.insert(outPoints, sortedPoint.point.x) + table.insert(outPoints, sortedPoint.point.y) + + if userdata and outUserdata then + outUserdata[sortedPoint.newID] = userdata[sortedPoint.id] + end + + if mapFunc then + self.map:init(sortedPoint.point, sortedPoint.id, sortedPoint.newID) + mapFunc(self.map) + end + end + + for i = 1, #self.edges do + local e = self.edges[i] + + assert(e.min ~= e.max) + + local a = search.first(self.sortedPoints, e.min, _compareSortedPointID) + local b = a and search.first(self.sortedPoints, e.max, _compareSortedPointID, a) + + if a and b then + local pointA = self.sortedPoints[a] + local pointB = self.sortedPoints[b] + + table.insert(outEdges, pointA.newID) + table.insert(outEdges, pointB.newID) + end + end + + return outPoints, outEdges, outUserdata +end + +--- @param points number[] +--- @param edges number[] +--- @param options slick.geometry.triangulation.delaunayTriangulationOptions? +--- @param result number[][]? +--- @param polygons number[][]? +--- @return number[][], number, number[][]?, number? +function delaunay:triangulate(points, edges, options, result, polygons) + options = options or defaultTriangulationOptions + + local refine = options.refine == nil and defaultTriangulationOptions.refine or options.refine + local interior = options.interior == nil and defaultTriangulationOptions.interior or options.interior + local exterior = options.exterior == nil and defaultTriangulationOptions.exterior or options.exterior + local polygonization = options.polygonization == nil and defaultTriangulationOptions.polygonization or options.polygonization + local maxPolygonVertexCount = options.maxPolygonVertexCount or defaultTriangulationOptions.maxPolygonVertexCount + + self:reset() + + if #points == 0 then + return result or {}, 0, polygons or (polygonization and {}) or nil, 0 + end + + if self.debug then + assert(points and #points >= 6 and #points % 2 == 0, + "expected three or more points in the form of x1, y1, x2, y2, ..., xn, yn") + assert(not edges or #edges == 0 or #edges % 2 == 0, + "expected zero or two or more indices in the form of a1, b1, a2, b2, ... an, bn") + end + + for i = 1, #points, 2 do + local x, y = points[i], points[i + 1] + self:_addPoint(x, y) + end + + if edges then + for i = 1, #edges, 2 do + local p1 = edges[i] + local p2 = edges[i + 1] + self:_addEdge(p1, p2) + end + end + + self:_sweep() + self:_triangulate() + + if refine or interior or exterior or polygonization then + self:_buildIndex() + + if refine then + self:_refine() + end + + self:_materialize() + + if interior and exterior then + self:_filter(0) + elseif interior then + self:_filter(-1) + elseif exterior then + self:_filter(1) + end + end + + result = result or {} + + local triangles = self.triangulation.triangles + for i = 1, #triangles do + local inputTriangle = triangles[i] + local outputTriangle = result[i] + + if outputTriangle then + outputTriangle[1], outputTriangle[2], outputTriangle[3] = _unpackTriangle(inputTriangle) + else + outputTriangle = { _unpackTriangle(inputTriangle) } + table.insert(result, outputTriangle) + end + end + + local polygonCount + if polygonization then + polygons = polygons or {} + + --- @cast maxPolygonVertexCount number + polygons, polygonCount = self:_polygonize(maxPolygonVertexCount, polygons) + end + + return result, #triangles, polygons, polygonCount +end + +--- @private +function delaunay:_sweep() + for i, point in ipairs(self.points) do + self:_addSweep(sweep.TYPE_POINT, point, i) + end + + for i, edge in ipairs(self.edges) do + local a, b = self.points[edge.a], self.points[edge.b] + if b.x < a.x then + a, b = b, a + end + + if a.x ~= b.x then + self:_addSweep(sweep.TYPE_EDGE_START, self:_newSegment(a, b), i) + self:_addSweep(sweep.TYPE_EDGE_STOP, self:_newSegment(b, a), i) + end + end + + table.sort(self.sweeps, sweep.less) +end + +--- @private +function delaunay:_triangulate() + local minX = self.sweeps[1].point.x + minX = minX - (1 + math.abs(minX) * 2 ^ -52) + table.insert(self.hulls, self:_newHull(self:_newPoint(minX, 1), self:_newPoint(minX, 0), 0)) + + for _, sweep in ipairs(self.sweeps) do + if sweep.type == sweep.TYPE_POINT then + local point = sweep.data + + --- @cast point slick.geometry.point + self:_addPointToHulls(point, sweep.index) + elseif sweep.type == sweep.TYPE_EDGE_START then + self:_splitHulls(sweep) + elseif sweep.type == sweep.TYPE_EDGE_STOP then + self:_mergeHulls(sweep) + else + if self.debug then + assert(false, "unhandled sweep event type") + end + end + end +end + +--- @private +--- @param i number +--- @param j number +--- @param k number +function delaunay:_addTriangleToIndex(i, j, k) + table.insert(self.index.vertices[i], j) + table.insert(self.index.vertices[i], k) + + table.insert(self.index.vertices[j], k) + table.insert(self.index.vertices[j], i) + + table.insert(self.index.vertices[k], i) + table.insert(self.index.vertices[k], j) +end + +--- @private +--- @param i number +--- @param j number +--- @param k number +function delaunay:_removeTriangleFromIndex(i, j, k) + self:_removeTriangleVertex(i, j, k) + self:_removeTriangleVertex(j, k, i) + self:_removeTriangleVertex(k, i, j) +end + +--- @private +--- @param i number +--- @param j number +--- @param k number +function delaunay:_removeTriangleVertex(i, j, k) + local vertices = self.index.vertices[i] + + for index = 2, #vertices, 2 do + if vertices[index - 1] == j and vertices[index] == k then + vertices[index - 1] = vertices[#vertices - 1] + vertices[index] = vertices[#vertices] + + table.remove(vertices, #vertices) + table.remove(vertices, #vertices) + + break + end + end +end + +--- @private +--- @param i number +--- @param j number +--- @return number? +function delaunay:_getOppositeVertex(j, i) + local vertices = self.index.vertices[i] + for k = 2, #vertices, 2 do + if vertices[k] == j then + return vertices[k - 1] + end + end + + return nil +end + +--- @private +--- @param i number +--- @param j number +function delaunay:_flipTriangle(i, j) + local a = self:_getOppositeVertex(i, j) + local b = self:_getOppositeVertex(j, i) + + if self.debug then + assert(a, "cannot flip triangle (no opposite vertex for IJ)") + assert(b, "cannot flip triangle (no opposite vertex for JI)") + end + + --- @cast a number + --- @cast b number + + self:_removeTriangleFromIndex(i, j, a) + self:_removeTriangleFromIndex(j, i, b) + + self:_addTriangleToIndex(i, b, a) + self:_addTriangleToIndex(j, a, b) +end + +--- @private +--- @param a number +--- @param b number +--- @param x number +function delaunay:_testFlipTriangle(a, b, x) + local y = self:_getOppositeVertex(a, b) + if not y then + return + end + + if b < a then + a, b = b, a + x, y = y, x + end + + if self:_isTriangleEdgeConstrained(a, b) then + return + end + + local result = slickmath.inside( + self.points[a], + self.points[b], + self.points[x], + self.points[y]) + if result < 0 then + table.insert(self.index.stack, a) + table.insert(self.index.stack, b) + end +end + +--- @private +function delaunay:_buildIndex() + local vertices = self.index.vertices + + if #vertices < #self.points then + for _ = #vertices, #self.points do + table.insert(vertices, {}) + end + end + + self.index.n = #self.points + for i = 1, self.index.n do + slicktable.clear(vertices[i]) + end + + local unsorted = self.triangulation.unsorted + for i = 1, self.triangulation.n do + local triangle = unsorted[i] + self:_addTriangleToIndex(_unpackTriangle(triangle)) + end +end + +--- @private +function delaunay:_isTriangleEdgeConstrained(i, j) + self.cachedEdge:init(i, j) + return search.first(self.edges, self.cachedEdge, edge.compare) ~= nil +end + +--- @private +function delaunay:_refine() + for i = 1, #self.points do + local vertices = self.index.vertices[i] + for j = 2, #vertices, 2 do + local first = i + local second = vertices[j] + + if second < first and not self:_isTriangleEdgeConstrained(first, second) then + local x = vertices[j - 1] + local y + + for k = 2, #vertices, 2 do + if vertices[k - 1] == second then + y = vertices[k] + end + end + + + if y then + local result = slickmath.inside( + self.points[first], + self.points[second], + self.points[x], + self.points[y]) + + if result < 0 then + table.insert(self.index.stack, first) + table.insert(self.index.stack, second) + end + end + end + end + end + + local stack = self.index.stack + while #stack > 0 do + local b = table.remove(stack, #stack) + local a = table.remove(stack, #stack) + + local x, y + local vertices = self.index.vertices[a] + for i = 2, #vertices, 2 do + local s = vertices[i - 1] + local t = vertices[i] + + if s == b then + y = t + elseif t == b then + x = s + end + end + + if x and y then + local result = slickmath.inside( + self.points[a], + self.points[b], + self.points[x], + self.points[y]) + + if result < 0 then + self:_flipTriangle(a, b) + self:_testFlipTriangle(x, a, y) + self:_testFlipTriangle(a, y, x) + self:_testFlipTriangle(y, b, x) + self:_testFlipTriangle(b, x, y) + end + end + end +end + +--- @private +function delaunay:_sortTriangulation() + local sorted = self.triangulation.sorted + local unsorted = self.triangulation.unsorted + + slicktable.clear(sorted) + + for i = 1, self.triangulation.n do + table.insert(sorted, unsorted[i]) + end + + table.sort(sorted, _lessTriangle) +end + +--- @private +function delaunay:_prepareFilter() + local flags = self.filter.flags + local neighbors = self.filter.neighbors + local constraints = self.filter.constraints + + for _ = 1, self.triangulation.n do + table.insert(flags, 0) + + table.insert(neighbors, 0) + table.insert(neighbors, 0) + table.insert(neighbors, 0) + + table.insert(constraints, false) + table.insert(constraints, false) + table.insert(constraints, false) + end + + local t = self.cachedTriangle + local sorted = self.triangulation.sorted + + for i = 1, self.triangulation.n do + local triangle = sorted[i] + + for j = 1, 3 do + local x = triangle[j] + local y = triangle[j % 3 + 1] + local z = self:_getOppositeVertex(y, x) or 0 + + t[1], t[2], t[3] = y, x, z + local neighbor = search.first(sorted, t, _compareTriangle) or 0 + local hasConstraint = self:_isTriangleEdgeConstrained(x, y) + + local index = 3 * (i - 1) + j + neighbors[index] = neighbor + constraints[index] = hasConstraint + + if neighbor <= 0 then + if hasConstraint then + table.insert(self.filter.next, i) + else + table.insert(self.filter.current, i) + flags[i] = 1 + end + end + end + end +end + +--- @private +function delaunay:_performFilter() + local flags = self.filter.flags + local neighbors = self.filter.neighbors + local constraints = self.filter.constraints + local current = self.filter.current + local next = self.filter.next + + local side = 1 + while #current > 0 or #next > 0 do + while #current > 0 do + local triangle = table.remove(current, #current) + if flags[triangle] ~= -side then + flags[triangle] = side + + for j = 1, 3 do + local index = 3 * (triangle - 1) + j + local neighbor = neighbors[index] + if neighbor > 0 and flags[neighbor] == 0 then + if constraints[index] then + table.insert(next, neighbor) + else + table.insert(current, neighbor) + flags[neighbor] = side + end + end + end + end + end + + next, current = current, next + slicktable.clear(next) + side = -side + end +end + +--- @private +function delaunay:_skip() + local unsorted = self.triangulation.unsorted + local triangles = self.triangulation.triangles + + for i = 1, self.triangulation.n do + table.insert(triangles, unsorted[i]) + end +end + +--- @private +--- @param direction -1 | 0 | 1 +function delaunay:_filter(direction) + if direction == 0 then + self:_skip() + return + end + + self:_sortTriangulation() + self:_prepareFilter() + self:_performFilter() + + local flags = self.filter.flags + local sorted = self.triangulation.sorted + local result = self.triangulation.triangles + + for i = 1, self.triangulation.n do + if flags[i] == direction then + table.insert(result, sorted[i]) + end + end +end + +--- @private +function delaunay:_materialize() + self.triangulation.n = 0 + + for i = 1, self.index.n do + local vertices = self.index.vertices[i] + local triangles = self.index.triangles[i] + if not triangles then + triangles = {} + table.insert(self.index.triangles, triangles) + end + + for j = 1, #vertices, 2 do + local s = vertices[j] + local t = vertices[j + 1] + + if i < math.min(s, t) then + self:_addTriangle(i, s, t) + table.insert(triangles, self.triangulation.n) + end + end + end +end + +--- @private +function delaunay:_buildPolygons() + local polygons = self.polygonization.polygons + local triangles = self.triangulation.triangles + local edges = self.polygonization.edges + local pending = self.polygonization.pending + local edgesToPolygons = self.polygonization.edgesToPolygons + + for i, triangle in ipairs(triangles) do + local polygon = polygons[i] + if not polygon then + polygon = { + vertices = {}, + merged = false + } + + table.insert(polygons, polygon) + else + slicktable.clear(polygon.vertices) + polygon.merged = false + end + + for j, vertex in ipairs(triangle) do + table.insert(polygon.vertices, vertex) + + local a = vertex + local b = triangle[j % #triangle + 1] + self.cachedEdge:init(a, b) + + local index = search.first(edges, self.cachedEdge, edge.compare) + if not index then + index = search.lessThan(edges, self.cachedEdge, edge.compare) + 1 + table.insert(edges, index, self:_newEdge(a, b)) + table.insert(pending, edges[index]) + end + end + end + + for i = 1, #triangles do + local polygon = polygons[i] + local vertices = polygon.vertices + for j, vertex in ipairs(vertices) do + local a = vertex + local b = vertices[j % #vertices + 1] + self.cachedEdge:init(a, b) + + local index = search.first(edges, self.cachedEdge, edge.compare) + if index then + local edgePolygons = edgesToPolygons[index] + if not edgePolygons then + edgePolygons = {} + edgesToPolygons[index] = edgePolygons + end + + table.insert(edgePolygons, polygon) + else + if self.debug then + assert(false, "critical logic error (edge not found)") + end + end + end + end + + self.polygonization.n = #triangles +end + +--- @private +--- @param polygon slick.geometry.triangulation.delaunayWorkingPolygon +--- @param otherPolygon slick.geometry.triangulation.delaunayWorkingPolygon? +function delaunay:_replacePolygon(polygon, otherPolygon) + local edges = self.polygonization.edges + local edgesToPolygons = self.polygonization.edgesToPolygons + + local vertices = polygon.vertices + for i, vertex in ipairs(vertices) do + local a = vertex + local b = vertices[i % #vertices + 1] + + self.cachedEdge:init(a, b) + + local index = search.first(edges, self.cachedEdge, edge.compare) + local polygonsWithEdge = edgesToPolygons[index] + if polygonsWithEdge then + local hasOtherPolygon = false + for j = #polygonsWithEdge, 1, -1 do + if polygonsWithEdge[j] == otherPolygon then + hasOtherPolygon = true + end + end + + if not hasOtherPolygon and otherPolygon then + table.insert(polygonsWithEdge, otherPolygon) + end + end + end +end + +--- @private +--- @param destinationPolygon slick.geometry.triangulation.delaunayWorkingPolygon +--- @param sourcePolygon slick.geometry.triangulation.delaunayWorkingPolygon +--- @param destinationPolygonVertexIndex number +--- @param sourcePolygonVertexIndex number +function delaunay:_mergePolygons(destinationPolygon, sourcePolygon, destinationPolygonVertexIndex, sourcePolygonVertexIndex) + local destinationVertices = destinationPolygon.vertices + local sourceVertices = sourcePolygon.vertices + + for i = 1, #sourceVertices - 2 do + local sourceIndex = (i + sourcePolygonVertexIndex) % #sourceVertices + 1 + table.insert(destinationVertices, destinationPolygonVertexIndex + i, sourceVertices[sourceIndex]) + end + + if self.debug then + local points = self.points + + -- Make sure the polygon is convex. + local currentSign + for i, index1 in ipairs(destinationVertices) do + local index2 = destinationVertices[(i % #destinationVertices) + 1] + local index3 = destinationVertices[((i + 1) % #destinationVertices) + 1] + + local p1 = points[index1] + local p2 = points[index2] + local p3 = points[index3] + + local sign = slickmath.direction(p1, p2, p3) + if not currentSign then + currentSign = sign + end + + assert(currentSign == sign and sign ~= 0, "critical logic error (created concave polygon during polygonization)") + end + end + + self:_replacePolygon(sourcePolygon, destinationPolygon) + + sourcePolygon.merged = true +end + +--- @private +--- @param destinationPolygon slick.geometry.triangulation.delaunayWorkingPolygon +--- @param sourcePolygon slick.geometry.triangulation.delaunayWorkingPolygon +--- @return boolean +--- @return number +--- @return integer +--- @return integer +function delaunay:_canMergePolygons(destinationPolygon, sourcePolygon) + if destinationPolygon.merged or sourcePolygon.merged then + return false, 0, 1, 1 + end + + local destinationVertices = destinationPolygon.vertices + local sourceVertices = sourcePolygon.vertices + for j = 1, #destinationVertices do + local a = destinationVertices[j] + local b = destinationVertices[j % #destinationVertices + 1] + local c = destinationVertices[(j - 2) % #destinationVertices + 1] + local d = destinationVertices[(j + 1) % #destinationVertices + 1] + + for k = 1, #sourceVertices do + local s = sourceVertices[k] + local t = sourceVertices[k % #sourceVertices + 1] + + if a == t and b == s then + local p = sourceVertices[(k + 1) % #sourceVertices + 1] + local q = sourceVertices[(k + #sourceVertices - 2) % #sourceVertices + 1] + + local p1 = self.points[c] + local p2 = self.points[a] + local p3 = self.points[p] + local p4 = self.points[q] + + local t1 = self.points[b] + local t2 = self.points[d] + + local s1 = self.points[destinationVertices[1]] + local s2 = self.points[destinationVertices[2]] + local s3 = self.points[destinationVertices[3]] + + local signP1 = slickmath.direction(p1, p2, p3) + local signP2 = slickmath.direction(p4, t1, t2) + local signS = slickmath.direction(s1, s2, s3) + + if signP1 == signP2 and signP1 == signS then + local angle = slickmath.angle(p1, p2, p3) + + return true, angle, j, k + end + end + end + end + + return false, 0, 1, 1 +end + +--- @private +--- @param maxVertexCount number +--- @param result number[][] +--- @return number[][], integer +function delaunay:_polygonize(maxVertexCount, result) + self:_buildPolygons() + + local pendingEdges = self.polygonization.pending + local edges = self.polygonization.edges + local edgesToPolygons = self.polygonization.edgesToPolygons + + while #pendingEdges > 0 do + local e = table.remove(pendingEdges, #pendingEdges) + local index = search.first(edges, e, edge.compare) + + local bestMagnitude + local sourcePolygonIndex, destinationPolygonIndex + local sourcePolygonVertexIndex, destinationPolygonVertexIndex + + -- This might look N^2 but there's only ever two polygons that share an edge so it's just... O(1) + local polygons = edgesToPolygons[index] + for i = 1, #polygons do + for j = i + 1, #polygons do + local canMerge, magnitude, s, t = self:_canMergePolygons(polygons[i], polygons[j]) + if canMerge then + if magnitude > (bestMagnitude or -math.huge) then + bestMagnitude = magnitude + destinationPolygonIndex = i + sourcePolygonIndex = j + destinationPolygonVertexIndex = s + sourcePolygonVertexIndex = t + end + end + end + end + + if bestMagnitude then + local destinationPolygon = polygons[destinationPolygonIndex] + local sourcePolygon = polygons[sourcePolygonIndex] + + -- 2 vertices are shared between the polygons. + -- So when merging them, there's actually two less than their combined sum. + -- E.g., a triangle would have one edge shared with another triangle - so: + -- - 1 vertex from the destination triangle + -- - 1 vertex from the source triangle + -- - 2 shared forming the shared edge from destination and source (so 4 vertex indices total), + -- If we just counted the sum of the vertex arrays, we'd get 6, which is incorrect. + -- The new polygon would have 4 vertices! + if #destinationPolygon.vertices + #sourcePolygon.vertices - 2 <= maxVertexCount then + self:_mergePolygons(destinationPolygon, sourcePolygon, destinationPolygonVertexIndex, sourcePolygonVertexIndex) + end + end + end + + local polygons = self.polygonization.polygons + + local index = 0 + for i = 1, self.polygonization.n do + if not polygons[i].merged then + index = index + 1 + + local outputPolygon = result[index] + if not outputPolygon then + outputPolygon = {} + table.insert(result, outputPolygon) + else + slicktable.clear(outputPolygon) + end + + local inputPolygon = polygons[i] + for _, vertex in ipairs(inputPolygon.vertices) do + table.insert(outputPolygon, vertex) + end + + self:_replacePolygon(inputPolygon, nil) + end + end + + return result, index +end + +function delaunay:reset() + self.pointsPool:reset() + self.sortedPointsPool:reset() + self.segmentsPool:reset() + self.edgesPool:reset() + self.sortedEdgesPool:reset() + self.sweepPool:reset() + self.hullsPool:reset() + + slicktable.clear(self.points) + slicktable.clear(self.sortedPoints) + slicktable.clear(self.temporaryEdges) + slicktable.clear(self.edges) + slicktable.clear(self.sortedEdges) + slicktable.clear(self.sweeps) + slicktable.clear(self.hulls) + + self.triangulation.n = 0 + slicktable.clear(self.triangulation.sorted) + slicktable.clear(self.triangulation.triangles) + + slicktable.clear(self.filter.flags) + slicktable.clear(self.filter.neighbors) + slicktable.clear(self.filter.constraints) + slicktable.clear(self.filter.current) + slicktable.clear(self.filter.next) + + self.index.n = 0 + slicktable.clear(self.index.stack) + + self.polygonization.n = 0 + slicktable.clear(self.polygonization.edges) + slicktable.clear(self.polygonization.pending) + + for i = 1, #self.polygonization.edgesToPolygons do + slicktable.clear(self.polygonization.edgesToPolygons[i]) + end + + for i = 1, #self.index.vertices do + slicktable.clear(self.index.vertices[i]) + end + + for i = 1, #self.index.triangles do + slicktable.clear(self.index.triangles[i]) + end +end + +function delaunay:clear() + self:reset() + + self.pointsPool:clear() + self.sortedPointsPool:clear() + self.segmentsPool:clear() + self.edgesPool:clear() + self.sortedEdgesPool:clear() + self.sweepPool:clear() + self.hullsPool:clear() + + slicktable.clear(self.polygonization.polygons) + slicktable.clear(self.polygonization.edgesToPolygons) + + slicktable.clear(self.activeEdges) + slicktable.clear(self.temporaryEdges) + slicktable.clear(self.pendingEdges) + slicktable.clear(self.sortedEdges) + slicktable.clear(self.sortedPoints) + + slicktable.clear(self.index.vertices) + slicktable.clear(self.triangulation.unsorted) +end + +--- @private +--- @param x number +--- @param y number +--- @return slick.geometry.point +function delaunay:_newPoint(x, y) + --- @type slick.geometry.point + return self.pointsPool:allocate(x, y) +end + +--- @private +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @return slick.geometry.segment +function delaunay:_newSegment(a, b) + --- @type slick.geometry.segment + return self.segmentsPool:allocate(a, b) +end + +--- @private +--- @param a number +--- @param b number +--- @return slick.geometry.triangulation.edge +function delaunay:_newEdge(a, b) + --- @type slick.geometry.triangulation.edge + return self.edgesPool:allocate(a, b) +end + +--- @private +--- @param e slick.geometry.triangulation.edge +--- @param segment slick.geometry.segment +--- @return slick.geometry.triangulation.delaunaySortedEdge +function delaunay:_newSortedEdge(e, segment) + assert(self.points[e.a]:equal(segment.a) or self.points[e.a]:equal(segment.b)) + assert(self.points[e.b]:equal(segment.a) or self.points[e.b]:equal(segment.b)) + + --- @type slick.geometry.triangulation.delaunaySortedEdge + return self.sortedEdgesPool:allocate(e, segment) +end + +--- @private +--- @param index number +--- @return slick.geometry.triangulation.delaunaySortedPoint +function delaunay:_newSortedPoint(index) + --- @type slick.geometry.triangulation.delaunaySortedPoint + return self.sortedPointsPool:allocate(self.points[index], index) +end + +--- @private +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param index number +--- @return slick.geometry.triangulation.hull +function delaunay:_newHull(a, b, index) + --- @type slick.geometry.triangulation.hull + return self.hullsPool:allocate(a, b, index) +end + +--- @private +--- @param x number +--- @param y number +function delaunay:_addPoint(x, y) + table.insert(self.points, self:_newPoint(x, y)) + + local edges = self.pointsToEdges[#self.points] + if not edges then + self.pointsToEdges[#self.points] = {} + else + slicktable.clear(edges) + end +end + +--- @private +--- @param edgeA number +--- @param edgeB number +function delaunay:_addEdge(edgeA, edgeB) + assert(edgeA ~= edgeB) + + local e = self:_newEdge(edgeA, edgeB) + table.insert(self.edges, search.lessThan(self.edges, e, edge.compare) + 1, e) + + local a = self.pointsToEdges[e.a] + if not a then + a = {} + self.pointsToEdges[e.a] = a + end + + table.insert(a, search.lessThan(a, e, edge.compare) + 1, e) + + local b = self.pointsToEdges[e.b] + if not b then + b = {} + self.pointsToEdges[e.b] = b + end + + table.insert(b, search.lessThan(b, e, edge.compare) + 1, e) +end + +function delaunay:_addSortedEdge(a, b) + assert(a ~= b) + + self.cachedEdge:init(a, b) + self.cachedSegment:init(self.points[a], self.points[b]) + + local index = search.lessThan(self.sortedEdges, self.cachedSegment, delaunaySortedEdge.compareSegment) + table.insert(self.sortedEdges, index + 1, self:_newSortedEdge(self.cachedEdge, self.cachedSegment)) +end + +--- @private +--- @param edgeA number +--- @param edgeB number +function delaunay:_dissolveSortedEdge(edgeA, edgeB) + local sortedEdges = self.sortedEdges + + self.cachedSegment:init(self.points[edgeA], self.points[edgeB]) + local start = search.first(sortedEdges, self.cachedSegment, delaunaySortedEdge.compareSegment) + local stop = start and search.last(sortedEdges, self.cachedSegment, delaunaySortedEdge.compareSegment, start) + + assert(start and stop) + + local dissolved = false + for j = stop, start, -1 do + local sortedEdge = sortedEdges[j] + if sortedEdge.edge.min == math.min(edgeA, edgeB) and sortedEdge.edge.max == math.max(edgeA, edgeB) then + dissolved = true + self.sortedEdgesPool:deallocate(table.remove(sortedEdges, j)) + break + end + end + + assert(dissolved) +end + +--- @private +--- @param edgeA number +--- @param edgeB number +function delaunay:_dissolveEdge(edgeA, edgeB) + self.cachedEdge:init(edgeA, edgeB) + + local edgeIndex = search.first(self.edges, self.cachedEdge, edge.compare) + assert(edgeIndex) + + local e + while edgeIndex do + e = table.remove(self.edges, edgeIndex) + + assert(e.min == math.min(edgeA, edgeB)) + assert(e.max == math.max(edgeA, edgeB)) + + local a = self.pointsToEdges[e.a] + if a then + local index = search.first(a, e, edge.compare) + assert(index) + + table.remove(a, index) + end + + local b = self.pointsToEdges[e.b] + if b then + local index = search.first(b, e, edge.compare) + assert(index) + + table.remove(b, index) + end + + self:_dissolveSortedEdge(edgeA, edgeB) + + edgeIndex = search.first(self.edges, self.cachedEdge, edge.compare) + end + + self.edgesPool:deallocate(e) +end + +--- @private +--- @param i number +--- @param j number +--- @param k number +function delaunay:_addTriangle(i, j, k) + local index = self.triangulation.n + 1 + local unsorted = self.triangulation.unsorted + + if index > #unsorted then + local triangle = { i, j, k } + table.insert(unsorted, triangle) + else + local triangle = unsorted[index] + triangle[1], triangle[2], triangle[3] = i, j, k + end + + self.triangulation.n = index +end + +--- @private +--- @param sweepType slick.geometry.triangulation.sweepType +--- @param data slick.geometry.point | slick.geometry.segment +--- @param index number +function delaunay:_addSweep(sweepType, data, index) + --- @type slick.geometry.triangulation.sweep + local event = self.sweepPool:allocate(sweepType, data, index) + table.insert(self.sweeps, event) + return event +end + +--- @private +--- @param points number[] +--- @param point slick.geometry.point +--- @param index number +--- @param swap boolean +--- @param compare fun(value: number): boolean +function delaunay:_addPointToHull(points, point, index, swap, compare) + for i = #points, 2, -1 do + local index1 = points[i - 1] + local index2 = points[i] + + local point1 = self.points[index1] + local point2 = self.points[index2] + + if compare(slickmath.direction(point1, point2, point, self.epsilon)) then + if swap then + index1, index2 = index2, index1 + end + + self:_addTriangle(index1, index2, index) + table.remove(points, i) + end + end + + table.insert(points, index) +end + +--- @private +--- @param point slick.geometry.point +--- @param index number +function delaunay:_addPointToHulls(point, index) + local lowIndex = search.lessThan(self.hulls, point, hull.point) + local highIndex = search.greaterThan(self.hulls, point, hull.point) + + if self.debug then + assert(lowIndex, "hull for lower bound not found") + assert(highIndex, "hull for upper bound not found") + end + + for i = lowIndex, highIndex - 1 do + local hull = self.hulls[i] + + self:_addPointToHull(hull.lowerPoints, point, index, true, _greaterThanZero) + self:_addPointToHull(hull.higherPoints, point, index, false, _lessThanZero) + end +end + +--- @private +--- @param sweep slick.geometry.triangulation.sweep +function delaunay:_splitHulls(sweep) + local index = search.lessThanEqual(self.hulls, sweep, hull.sweep) + local hull = self.hulls[index] + + local otherHull = self:_newHull(sweep.data.a, sweep.data.b, sweep.index) + for _, otherPoint in ipairs(hull.higherPoints) do + table.insert(otherHull.higherPoints, otherPoint) + end + + local otherPoint = hull.higherPoints[#hull.higherPoints] + table.insert(otherHull.lowerPoints, otherPoint) + + slicktable.clear(hull.higherPoints) + table.insert(hull.higherPoints, otherPoint) + + table.insert(self.hulls, index + 1, otherHull) +end + +--- @private +--- @param sweep slick.geometry.triangulation.sweep +function delaunay:_mergeHulls(sweep) + sweep.data.a, sweep.data.b = sweep.data.b, sweep.data.a + + local index = search.last(self.hulls, sweep, hull.sweep) + local upper = self.hulls[index] + local lower = self.hulls[index - 1] + + lower.higherPoints, upper.higherPoints = upper.higherPoints, lower.higherPoints + + table.remove(self.hulls, index) + self.hullsPool:deallocate(upper) +end + +return delaunay diff --git a/game/love_src/lib/slick/geometry/triangulation/delaunaySortedEdge.lua b/game/love_src/lib/slick/geometry/triangulation/delaunaySortedEdge.lua new file mode 100644 index 0000000..8992cf9 --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/delaunaySortedEdge.lua @@ -0,0 +1,46 @@ +local segment = require("slick.geometry.segment") +local edge = require("slick.geometry.triangulation.edge") + +--- @class slick.geometry.triangulation.delaunaySortedEdge +--- @field edge slick.geometry.triangulation.edge +--- @field segment slick.geometry.segment +local delaunaySortedEdge = {} +local metatable = { __index = delaunaySortedEdge } + +--- @return slick.geometry.triangulation.delaunaySortedEdge +function delaunaySortedEdge.new() + return setmetatable({ + edge = edge.new(), + segment = segment.new() + }, metatable) +end + +--- @param a slick.geometry.triangulation.delaunaySortedEdge +--- @param b slick.geometry.triangulation.delaunaySortedEdge +--- @return slick.util.search.compareResult +function delaunaySortedEdge.compare(a, b) + return segment.compare(a.segment, b.segment, 0) +end + +--- @param sortedEdge slick.geometry.triangulation.delaunaySortedEdge +--- @param segment slick.geometry.segment +--- @return slick.util.search.compareResult +function delaunaySortedEdge.compareSegment(sortedEdge, segment) + return segment.compare(sortedEdge.segment, segment, 0) +end + +--- @param a slick.geometry.triangulation.delaunaySortedEdge +--- @param b slick.geometry.triangulation.delaunaySortedEdge +--- @return boolean +function delaunaySortedEdge.less(a, b) + return delaunaySortedEdge.compare(a, b) < 0 +end + +--- @param e slick.geometry.triangulation.edge +--- @param segment slick.geometry.segment +function delaunaySortedEdge:init(e, segment) + self.edge:init(e.a, e.b) + self.segment:init(segment.a, segment.b) +end + +return delaunaySortedEdge diff --git a/game/love_src/lib/slick/geometry/triangulation/delaunaySortedPoint.lua b/game/love_src/lib/slick/geometry/triangulation/delaunaySortedPoint.lua new file mode 100644 index 0000000..ccf394d --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/delaunaySortedPoint.lua @@ -0,0 +1,62 @@ +local point = require("slick.geometry.point") + +--- @class slick.geometry.triangulation.delaunaySortedPoint +--- @field point slick.geometry.point +--- @field id number +--- @field newID number +local delaunaySortedPoint = {} +local metatable = { __index = delaunaySortedPoint } + +--- @return slick.geometry.triangulation.delaunaySortedPoint +function delaunaySortedPoint.new() + return setmetatable({ + id = 0, + newID = 0, + point = point.new() + }, metatable) +end + +--- @param s slick.geometry.triangulation.delaunaySortedPoint +--- @param p slick.geometry.point +--- @return slick.util.search.compareResult +function delaunaySortedPoint.comparePoint(s, p) + return point.compare(s.point, p) +end + +--- @param a slick.geometry.triangulation.delaunaySortedPoint +--- @param b slick.geometry.triangulation.delaunaySortedPoint +--- @return slick.util.search.compareResult +function delaunaySortedPoint.compare(a, b) + return point.compare(a.point, b.point) +end + +--- @param a slick.geometry.triangulation.delaunaySortedPoint +--- @param b slick.geometry.triangulation.delaunaySortedPoint +--- @return slick.util.search.compareResult +function delaunaySortedPoint.compareID(a, b) + return a.id - b.id +end + +--- @param a slick.geometry.triangulation.delaunaySortedPoint +--- @param b slick.geometry.triangulation.delaunaySortedPoint +--- @return boolean +function delaunaySortedPoint.less(a, b) + return delaunaySortedPoint.compare(a, b) < 0 +end + +--- @param a slick.geometry.triangulation.delaunaySortedPoint +--- @param b slick.geometry.triangulation.delaunaySortedPoint +--- @return boolean +function delaunaySortedPoint.lessID(a, b) + return delaunaySortedPoint.compareID(a, b) < 0 +end + +--- @param point slick.geometry.point +--- @param id number +function delaunaySortedPoint:init(point, id) + self.id = id + self.newID = 0 + self.point:init(point.x, point.y) +end + +return delaunaySortedPoint diff --git a/game/love_src/lib/slick/geometry/triangulation/dissolve.lua b/game/love_src/lib/slick/geometry/triangulation/dissolve.lua new file mode 100644 index 0000000..b35e6bd --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/dissolve.lua @@ -0,0 +1,37 @@ +local point = require("slick.geometry.point") + +--- @class slick.geometry.triangulation.dissolve +--- @field point slick.geometry.point +--- @field index number +--- @field userdata any? +--- @field otherIndex number +--- @field otherUserdata any? +local dissolve = {} +local metatable = { __index = dissolve } + +function dissolve.new() + return setmetatable({ + point = point.new() + }, metatable) +end + +--- @param p slick.geometry.point +--- @param index number +--- @param userdata any? +--- @param otherIndex number +--- @param otherUserdata any +function dissolve:init(p, index, userdata, otherIndex, otherUserdata) + self.point:init(p.x, p.y) + self.index = index + self.userdata = userdata + self.otherIndex = otherIndex + self.otherUserdata = otherUserdata + self.resultUserdata = nil +end + +--- @param d slick.geometry.triangulation.dissolve +function dissolve.default(d) + -- No-op. +end + +return dissolve diff --git a/game/love_src/lib/slick/geometry/triangulation/edge.lua b/game/love_src/lib/slick/geometry/triangulation/edge.lua new file mode 100644 index 0000000..023faed --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/edge.lua @@ -0,0 +1,52 @@ +--- @class slick.geometry.triangulation.edge +--- @field a number +--- @field b number +--- @field min number +--- @field max number +local edge = {} +local metatable = { __index = edge } + +--- @param a number? +--- @param b number? +--- @return slick.geometry.triangulation.edge +function edge.new(a, b) + return setmetatable({ + a = a, + b = b, + min = a and b and math.min(a, b), + max = a and b and math.max(a, b), + }, metatable) +end + +--- @param a slick.geometry.triangulation.edge +--- @param b slick.geometry.triangulation.edge +function edge.less(a, b) + if a.min == b.min then + return a.max < b.max + end + + return a.min < b.min +end + +--- @param a slick.geometry.triangulation.edge +--- @param b slick.geometry.triangulation.edge +--- @return -1 | 0 | 1 +function edge.compare(a, b) + local min = a.min - b.min + if min ~= 0 then + return min + end + + return a.max - b.max +end + +--- @param a number +--- @param b number +function edge:init(a, b) + self.a = a + self.b = b + self.min = math.min(a, b) + self.max = math.max(a, b) +end + +return edge diff --git a/game/love_src/lib/slick/geometry/triangulation/hull.lua b/game/love_src/lib/slick/geometry/triangulation/hull.lua new file mode 100644 index 0000000..259ddf5 --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/hull.lua @@ -0,0 +1,69 @@ +local slickmath = require("slick.util.slickmath") +local slicktable = require("slick.util.slicktable") + +--- @class slick.geometry.triangulation.hull +--- @field a slick.geometry.point +--- @field b slick.geometry.point +--- @field lowerPoints number[] +--- @field higherPoints number[] +--- @field index number +local hull = {} +local metatable = { __index = hull } + +--- @return slick.geometry.triangulation.hull +function hull.new() + return setmetatable({ + higherPoints = {}, + lowerPoints = {} + }, metatable) +end + +--- @param hull slick.geometry.triangulation.hull +--- @param point slick.geometry.point +--- @return slick.util.search.compareResult +function hull.point(hull, point) + return slickmath.direction(hull.a, hull.b, point) +end + +--- @param hull slick.geometry.triangulation.hull +--- @param sweep slick.geometry.triangulation.sweep +--- @return slick.util.search.compareResult +function hull.sweep(hull, sweep) + local direction + + if hull.a.x < sweep.data.a.x then + direction = slickmath.direction(hull.a, hull.b, sweep.data.a) + else + direction = slickmath.direction(sweep.data.b, sweep.data.a, hull.a) + end + + if direction ~= 0 then + return direction + end + + if sweep.data.b.x < hull.b.x then + direction = slickmath.direction(hull.a, hull.b, sweep.data.b) + else + direction = slickmath.direction(sweep.data.b, sweep.data.a, hull.b) + end + + if direction ~= 0 then + return direction + end + + return hull.index - sweep.index +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param index number +function hull:init(a, b, index) + self.a = a + self.b = b + self.index = index + + slicktable.clear(self.higherPoints) + slicktable.clear(self.lowerPoints) +end + +return hull diff --git a/game/love_src/lib/slick/geometry/triangulation/init.lua b/game/love_src/lib/slick/geometry/triangulation/init.lua new file mode 100644 index 0000000..0b2e86d --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/init.lua @@ -0,0 +1,10 @@ +return { + delaunay = require("slick.geometry.triangulation.delaunay"), + delaunaySortedEdge = require("slick.geometry.triangulation.delaunaySortedEdge"), + delaunaySortedPoint = require("slick.geometry.triangulation.delaunaySortedPoint"), + dissolve = require("slick.geometry.triangulation.dissolve"), + edge = require("slick.geometry.triangulation.edge"), + hull = require("slick.geometry.triangulation.hull"), + intersection = require("slick.geometry.triangulation.intersection"), + sweep = require("slick.geometry.triangulation.sweep"), +} diff --git a/game/love_src/lib/slick/geometry/triangulation/intersection.lua b/game/love_src/lib/slick/geometry/triangulation/intersection.lua new file mode 100644 index 0000000..9120936 --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/intersection.lua @@ -0,0 +1,109 @@ +local point = require("slick.geometry.point") +local slickmath = require("slick.util.slickmath") + +--- @class slick.geometry.triangulation.intersection +--- @field a1 slick.geometry.point +--- @field a1Index number +--- @field a1Userdata any? +--- @field b1 slick.geometry.point +--- @field b1Index number +--- @field b1Userdata any? +--- @field delta1 number +--- @field a2 slick.geometry.point +--- @field a2Index number +--- @field a2Userdata any? +--- @field b2 slick.geometry.point +--- @field b2Index number +--- @field b2Userdata any? +--- @field delta2 number +--- @field result slick.geometry.point +--- @field resultIndex number +--- @field resultUserdata any? +--- @field private s slick.geometry.point +--- @field private t slick.geometry.point +--- @field private p slick.geometry.point +--- @field private q slick.geometry.point +local intersection = {} +local metatable = { __index = intersection } + +function intersection.new() + return setmetatable({ + a1 = point.new(), + b1 = point.new(), + a2 = point.new(), + b2 = point.new(), + a1Index = 0, + b1Index = 0, + a2Index = 0, + b2Index = 0, + result = point.new(), + delta1 = 0, + delta2 = 0, + s = point.new(), + t = point.new(), + p = point.new(), + q = point.new(), + }, metatable) +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param aIndex number +--- @param bIndex number +--- @param delta number +--- @param aUserdata any +--- @param bUserdata any +function intersection:setLeftEdge(a, b, aIndex, bIndex, delta, aUserdata, bUserdata) + self.a1:init(a.x, a.y) + self.b1:init(b.x, b.y) + self.a1Index = aIndex + self.b1Index = bIndex + self.a1Userdata = aUserdata + self.b1Userdata = bUserdata + self.delta1 = delta +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param aIndex number +--- @param bIndex number +--- @param delta number +--- @param aUserdata any +--- @param bUserdata any +function intersection:setRightEdge(a, b, aIndex, bIndex, delta, aUserdata, bUserdata) + self.a2:init(a.x, a.y) + self.b2:init(b.x, b.y) + self.a2Index = aIndex + self.b2Index = bIndex + self.a2Userdata = aUserdata + self.b2Userdata = bUserdata + self.delta2 = delta +end + +--- @param resultIndex number +function intersection:init(resultIndex) + self.a1Userdata = nil + self.b1Userdata = nil + self.a2Userdata = nil + self.b2Userdata = nil + self.resultUserdata = nil + self.resultIndex = resultIndex +end + +--- @param i slick.geometry.triangulation.intersection +function intersection.default(i) + -- No-op. +end + +--- @param userdata any? +--- @param x number? +--- @param y number? +function intersection:setResult(userdata, x, y) + self.resultUserdata = userdata + + if x and y then + self.result:init(x, y) + end +end + +return intersection diff --git a/game/love_src/lib/slick/geometry/triangulation/map.lua b/game/love_src/lib/slick/geometry/triangulation/map.lua new file mode 100644 index 0000000..86b81b5 --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/map.lua @@ -0,0 +1,32 @@ +local point = require("slick.geometry.point") + +--- @class slick.geometry.triangulation.map +--- @field point slick.geometry.point +--- @field index number +--- @field userdata any? +--- @field otherIndex number +--- @field otherUserdata any? +local map = {} +local metatable = { __index = map } + +function map.new() + return setmetatable({ + point = point.new() + }, metatable) +end + +--- @param p slick.geometry.point +--- @param oldIndex number +--- @param newIndex number +function map:init(p, oldIndex, newIndex) + self.point:init(p.x, p.y) + self.oldIndex = oldIndex + self.newIndex = newIndex +end + +--- @param d slick.geometry.triangulation.map +function map.default(d) + -- No-op. +end + +return map diff --git a/game/love_src/lib/slick/geometry/triangulation/sweep.lua b/game/love_src/lib/slick/geometry/triangulation/sweep.lua new file mode 100644 index 0000000..d74750a --- /dev/null +++ b/game/love_src/lib/slick/geometry/triangulation/sweep.lua @@ -0,0 +1,78 @@ +local slickmath = require("slick.util.slickmath") +local point = require("slick.geometry.point") +local segment = require("slick.geometry.segment") +local util = require("slick.util") + +--- @class slick.geometry.triangulation.sweep +--- @field type slick.geometry.triangulation.sweepType +--- @field data slick.geometry.point | slick.geometry.segment +--- @field point slick.geometry.point? +--- @field index number +local sweep = {} +local metatable = { __index = sweep } + +--- @alias slick.geometry.triangulation.sweepType 0 | 1 | 2 | 3 +sweep.TYPE_NONE = 0 +sweep.TYPE_POINT = 1 +sweep.TYPE_EDGE_STOP = 2 +sweep.TYPE_EDGE_START = 3 + +--- @return slick.geometry.triangulation.sweep +function sweep.new() + return setmetatable({ + type = sweep.TYPE_NONE, + index = 0 + }, metatable) +end + +--- @param data slick.geometry.point | slick.geometry.segment +--- @return slick.geometry.point +local function _getPointFromData(data) + if util.is(data, segment) then + return data.a + elseif util.is(data, point) then + --- @cast data slick.geometry.point + return data + end + + --- @diagnostic disable-next-line: missing-return + assert(false, "expected 'slick.geometry.point' or 'slick.geometry.segment'") +end + +--- @param a slick.geometry.triangulation.sweep +--- @param b slick.geometry.triangulation.sweep +--- @return boolean +function sweep.less(a, b) + if a.point:lessThan(b.point) then + return true + elseif a.point:equal(b.point) then + if a.type < b.type then + return true + elseif a.type == b.type then + if a.type == sweep.TYPE_EDGE_START or a.type == sweep.TYPE_EDGE_STOP then + local direction = slickmath.direction(a.point, b.point, b.data.b) + if direction ~= 0 then + return direction < 0 + end + end + + return a.index < b.index + else + return false + end + else + return false + end +end + +--- @param sweepType slick.geometry.triangulation.sweepType +--- @param data slick.geometry.point | slick.geometry.segment +--- @param index number +function sweep:init(sweepType, data, index) + self.type = sweepType + self.data = data + self.index = index + self.point = _getPointFromData(data) +end + +return sweep diff --git a/game/love_src/lib/slick/init.lua b/game/love_src/lib/slick/init.lua new file mode 100644 index 0000000..e66f06c --- /dev/null +++ b/game/love_src/lib/slick/init.lua @@ -0,0 +1,142 @@ +local PATH = (...):gsub("[^%.]+$", "") + +--- @module "slick.cache" +local cache + +--- @module "slick.collision" +local collision + +--- @module "slick.draw" +local draw + +--- @module "slick.entity" +local entity + +--- @module "slick.enum" +local enum + +--- @module "slick.geometry" +local geometry + +--- @module "slick.navigation" +local navigation + +--- @module "slick.options" +local defaultOptions + +--- @module "slick.responses" +local responses + +--- @module "slick.shape" +local shape + +--- @module "slick.tag" +local tag + +--- @module "slick.util" +local util + +--- @module "slick.world" +local world + +--- @module "slick.worldQuery" +local worldQuery + +--- @module "slick.worldQueryResponse" +local worldQueryResponse + +--- @module "slick.meta" +local meta + +local function load() + local requireImpl = require + local require = function(path) + return requireImpl(PATH .. path) + end + + local patchedG = { + __index = _G + } + + local g = { require = require } + g._G = g + + setfenv(0, setmetatable(g, patchedG)) + + cache = require("slick.cache") + collision = require("slick.collision") + draw = require("slick.draw") + entity = require("slick.entity") + enum = require("slick.enum") + geometry = require("slick.geometry") + navigation = require("slick.navigation") + defaultOptions = require("slick.options") + responses = require("slick.responses") + shape = require("slick.shape") + tag = require("slick.tag") + util = require("slick.util") + world = require("slick.world") + worldQuery = require("slick.worldQuery") + worldQueryResponse = require("slick.worldQueryResponse") + + meta = require("slick.meta") +end + +do + local l = coroutine.create(load) + repeat + local s, r = coroutine.resume(l) + if not s then + error(debug.traceback(l, r)) + end + until coroutine.status(l) == "dead" +end + +return { + _VERSION = meta._VERSION, + _DESCRIPTION = meta._DESCRIPTION, + _URL = meta._URL, + _LICENSE = meta._LICENSE, + + cache = cache, + collision = collision, + defaultOptions = defaultOptions, + entity = entity, + geometry = geometry, + shape = shape, + tag = tag, + util = util, + world = world, + worldQuery = worldQuery, + worldQueryResponse = worldQueryResponse, + responses = responses, + + newCache = cache.new, + newWorld = world.new, + newWorldQuery = worldQuery.new, + newTransform = geometry.transform.new, + + newRectangleShape = shape.newRectangle, + newChainShape = shape.newChain, + newCircleShape = shape.newCircle, + newLineSegmentShape = shape.newLineSegment, + newPolygonShape = shape.newPolygon, + newPolygonMeshShape = shape.newPolygonMesh, + newPolylineShape = shape.newPolyline, + newMeshShape = shape.newMesh, + newShapeGroup = shape.newShapeGroup, + newEnum = enum.new, + newTag = tag.new, + + triangulate = geometry.simple.triangulate, + polygonize = geometry.simple.polygonize, + clip = geometry.simple.clip, + + newUnionClipOperation = geometry.simple.newUnionClipOperation, + newIntersectionClipOperation = geometry.simple.newIntersectionClipOperation, + newDifferenceClipOperation = geometry.simple.newDifferenceClipOperation, + + navigation = navigation, + + drawWorld = draw +} diff --git a/game/love_src/lib/slick/meta/init.lua b/game/love_src/lib/slick/meta/init.lua new file mode 100644 index 0000000..9dd6d9c --- /dev/null +++ b/game/love_src/lib/slick/meta/init.lua @@ -0,0 +1,380 @@ +return { + _VERSION = "4.0.8", + _DESCRIPTION = "slick is a simple to use polygon collision library inspired by bump.lua", + _URL = "https://github.com/erinmaus/slick", + _LICENSE = [[ + Mozilla Public License Version 2.0 + ================================== + + 1. Definitions + -------------- + + 1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + + 1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + + 1.3. "Contribution" + means Covered Software of a particular Contributor. + + 1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + + 1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + + 1.6. "Executable Form" + means any form of the work other than Source Code Form. + + 1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + + 1.8. "License" + means this document. + + 1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + + 1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + + 1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + + 1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + + 1.13. "Source Code Form" + means the form of the work preferred for making modifications. + + 1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + + 2. License Grants and Conditions + -------------------------------- + + 2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + (a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + (b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + + 2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + + 2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + (a) for any code that a Contributor has removed from Covered Software; + or + + (b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + (c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + + 2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + + 2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights + to grant the rights to its Contributions conveyed by this License. + + 2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + + 2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted + in Section 2.1. + + 3. Responsibilities + ------------------- + + 3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + + 3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + (a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + + (b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + + 3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + + 3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, + or limitations of liability) contained within the Source Code Form of + the Covered Software, except that You may alter any license notices to + the extent required to remedy known factual inaccuracies. + + 3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + + 4. Inability to Comply Due to Statute or Regulation + --------------------------------------------------- + + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Software due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description must + be placed in a text file included with all distributions of the Covered + Software under this License. Except to the extent prohibited by statute + or regulation, such description must be sufficiently detailed for a + recipient of ordinary skill to be able to understand it. + + 5. Termination + -------------- + + 5.1. The rights granted under this License will terminate automatically + if You fail to comply with any of its terms. However, if You become + compliant, then the rights granted under this License from a particular + Contributor are reinstated (a) provisionally, unless and until such + Contributor explicitly and finally terminates Your grants, and (b) on an + ongoing basis, if such Contributor fails to notify You of the + non-compliance by some reasonable means prior to 60 days after You have + come back into compliance. Moreover, Your grants from a particular + Contributor are reinstated on an ongoing basis if such Contributor + notifies You of the non-compliance by some reasonable means, this is the + first time You have received notice of non-compliance with this License + from such Contributor, and You become compliant prior to 30 days after + Your receipt of the notice. + + 5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + + 5.3. In the event of termination under Sections 5.1 or 5.2 above, all + end user license agreements (excluding distributors and resellers) which + have been validly granted by You or Your distributors under this License + prior to termination shall survive termination. + + ************************************************************************ + * * + * 6. Disclaimer of Warranty * + * ------------------------- * + * * + * Covered Software is provided under this License on an "as is" * + * basis, without warranty of any kind, either expressed, implied, or * + * statutory, including, without limitation, warranties that the * + * Covered Software is free of defects, merchantable, fit for a * + * particular purpose or non-infringing. The entire risk as to the * + * quality and performance of the Covered Software is with You. * + * Should any Covered Software prove defective in any respect, You * + * (not any Contributor) assume the cost of any necessary servicing, * + * repair, or correction. This disclaimer of warranty constitutes an * + * essential part of this License. No use of any Covered Software is * + * authorized under this License except under this disclaimer. * + * * + ************************************************************************ + + ************************************************************************ + * * + * 7. Limitation of Liability * + * -------------------------- * + * * + * Under no circumstances and under no legal theory, whether tort * + * (including negligence), contract, or otherwise, shall any * + * Contributor, or anyone who distributes Covered Software as * + * permitted above, be liable to You for any direct, indirect, * + * special, incidental, or consequential damages of any character * + * including, without limitation, damages for lost profits, loss of * + * goodwill, work stoppage, computer failure or malfunction, or any * + * and all other commercial damages or losses, even if such party * + * shall have been informed of the possibility of such damages. This * + * limitation of liability shall not apply to liability for death or * + * personal injury resulting from such party's negligence to the * + * extent applicable law prohibits such limitation. Some * + * jurisdictions do not allow the exclusion or limitation of * + * incidental or consequential damages, so this exclusion and * + * limitation may not apply to You. * + * * + ************************************************************************ + + 8. Litigation + ------------- + + Any litigation relating to this License may be brought only in the + courts of a jurisdiction where the defendant maintains its principal + place of business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. + Nothing in this Section shall prevent a party's ability to bring + cross-claims or counter-claims. + + 9. Miscellaneous + ---------------- + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides + that the language of a contract shall be construed against the drafter + shall not be used to construe this License against a Contributor. + + 10. Versions of the License + --------------------------- + + 10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + + 10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + + 10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + + 10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses + + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + + Exhibit A - Source Code Form License Notice + ------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to look + for such a notice. + + You may add additional accurate notices of copyright ownership. + + Exhibit B - "Incompatible With Secondary Licenses" Notice + --------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + ]] +} diff --git a/game/love_src/lib/slick/navigation/edge.lua b/game/love_src/lib/slick/navigation/edge.lua new file mode 100644 index 0000000..5228ed5 --- /dev/null +++ b/game/love_src/lib/slick/navigation/edge.lua @@ -0,0 +1,49 @@ +--- @class slick.navigation.edge +--- @field a slick.navigation.vertex +--- @field b slick.navigation.vertex +--- @field min number +--- @field max number +local edge = {} +local metatable = { __index = edge } + +--- @param a slick.navigation.vertex +--- @param b slick.navigation.vertex +--- @param interior boolean? +--- @param exterior boolean? +--- @return slick.navigation.edge +function edge.new(a, b, interior, exterior) + return setmetatable({ + a = a, + b = b, + min = math.min(a.index, b.index), + max = math.max(a.index, b.index), + interior = not not interior, + exterior = not not exterior + }, metatable) +end + +--- @param other slick.navigation.edge +--- @return boolean +function edge:same(other) + return self == other or (self.min == other.min and self.max == other.max) +end + +--- @param a slick.navigation.edge +--- @param b slick.navigation.edge +--- @return -1 | 0 | 1 +function edge.compare(a, b) + if a.min == b.min then + return a.max - b.max + else + return a.min - b.min + end +end + +--- @param a slick.navigation.edge +--- @param b slick.navigation.edge +--- @return boolean +function edge.less(a, b) + return edge.compare(a, b) < 0 +end + +return edge diff --git a/game/love_src/lib/slick/navigation/init.lua b/game/love_src/lib/slick/navigation/init.lua new file mode 100644 index 0000000..5865ae9 --- /dev/null +++ b/game/love_src/lib/slick/navigation/init.lua @@ -0,0 +1,8 @@ +return { + edge = require("slick.navigation.edge"), + mesh = require("slick.navigation.mesh"), + meshBuilder = require("slick.navigation.meshBuilder"), + path = require("slick.navigation.path"), + triangle = require("slick.navigation.triangle"), + vertex = require("slick.navigation.vertex"), +} diff --git a/game/love_src/lib/slick/navigation/mesh.lua b/game/love_src/lib/slick/navigation/mesh.lua new file mode 100644 index 0000000..88d78f6 --- /dev/null +++ b/game/love_src/lib/slick/navigation/mesh.lua @@ -0,0 +1,432 @@ +local quadTree = require "slick.collision.quadTree" +local quadTreeQuery = require "slick.collision.quadTreeQuery" +local point = require "slick.geometry.point" +local rectangle = require "slick.geometry.rectangle" +local segment = require "slick.geometry.segment" +local edge = require "slick.navigation.edge" +local triangle = require "slick.navigation.triangle" +local vertex = require "slick.navigation.vertex" +local search = require "slick.util.search" +local slickmath = require "slick.util.slickmath" + +--- @class slick.navigation.mesh +--- @field vertices slick.navigation.vertex[] +--- @field edges slick.navigation.edge[] +--- @field bounds slick.geometry.rectangle +--- @field vertexNeighbors table +--- @field triangles slick.navigation.triangle[] +--- @field inputPoints number[] +--- @field inputEdges number[] +--- @field inputExteriorEdges number[] +--- @field inputInteriorEdges number[] +--- @field inputUserdata any[] +--- @field vertexToTriangle table +--- @field triangleNeighbors table +--- @field sharedTriangleEdges table> +--- @field edgeTriangles table +--- @field quadTree slick.collision.quadTree? +--- @field quadTreeQuery slick.collision.quadTreeQuery? +local mesh = {} +local metatable = { __index = mesh } + + +--- @param aTriangles slick.navigation.triangle[] +--- @param bTriangles slick.navigation.triangle[] +--- @param e slick.navigation.edge +--- @return slick.navigation.triangle?, slick.navigation.triangle? +local function _findSharedTriangle(aTriangles, bTriangles, e) + for _, t1 in ipairs(aTriangles) do + for _, t2 in ipairs(bTriangles) do + if t1.index ~= t2.index then + if t1.vertices[e.a.index] and t1.vertices[e.b.index] and t2.vertices[e.a.index] and t2.vertices[e.b.index] then + if t1.index ~= t2.index then + return t1, t2 + end + end + end + end + end + + return nil, nil +end + +--- @overload fun(points: number[], userdata: any[], edges: number[]): slick.navigation.mesh +--- @overload fun(points: number[], userdata: any[], exteriorEdges: number[], interiorEdges: number[]): slick.navigation.mesh +--- @overload fun(points: number[], userdata: any[], edges: number[], triangles: number[][]): slick.navigation.mesh +--- @return slick.navigation.mesh +function mesh.new(points, userdata, edges, z) + local self = setmetatable({ + vertices = {}, + edges = {}, + vertexNeighbors = {}, + triangleNeighbors = {}, + triangles = {}, + inputPoints = {}, + inputEdges = {}, + inputUserdata = {}, + inputExteriorEdges = {}, + inputInteriorEdges = {}, + vertexToTriangle = {}, + sharedTriangleEdges = {}, + edgeTriangles = {}, + bounds = rectangle.new(points[1], points[2], points[1], points[2]) + }, metatable) + + for i = 1, #points, 2 do + local n = (i - 1) / 2 + 1 + local vertex = vertex.new(point.new(points[i], points[i + 1]), userdata and userdata[n] or nil, n) + + table.insert(self.vertices, vertex) + + table.insert(self.inputPoints, points[i]) + table.insert(self.inputPoints, points[i + 1]) + + self.inputUserdata[n] = userdata and userdata[n] or nil + + self.bounds:expand(points[i], points[i + 1]) + end + + for i = 1, #edges, 2 do + table.insert(self.inputEdges, edges[i]) + table.insert(self.inputEdges, edges[i + 1]) + table.insert(self.inputExteriorEdges, edges[i]) + table.insert(self.inputExteriorEdges, edges[i + 1]) + end + + if z and type(z) == "table" and #z >= 1 and type(z[1]) == "table" and #z[1] == 3 then + for _, t in ipairs(z) do + local n = triangle.new(self.vertices[t[1]], self.vertices[t[2]], self.vertices[t[3]], #self.triangles + 1) + + for i = 1, #t do + local j = (i % #t) + 1 + + local s = t[i] + local t = t[j] + + local e1 = edge.new(self.vertices[s], self.vertices[t]) + local e2 = edge.new(self.vertices[t], self.vertices[s]) + table.insert(self.edges, e1) + + local neighborsI = self.vertexNeighbors[s] + if not neighborsI then + neighborsI = {} + self.vertexNeighbors[s] = neighborsI + end + + local neighborsJ = self.vertexNeighbors[t] + if not neighborsJ then + neighborsJ = {} + self.vertexNeighbors[t] = neighborsJ + end + + do + local hasE = false + for _, neighbor in ipairs(neighborsI) do + if neighbor.min == e1.min and neighbor.max == e1.max then + hasE = true + break + end + end + + if not hasE then + table.insert(neighborsI, e1) + end + end + + do + local hasE = false + for _, neighbor in ipairs(neighborsJ) do + if neighbor.min == e2.min and neighbor.max == e2.max then + hasE = true + break + end + end + + if not hasE then + table.insert(neighborsJ, e2) + end + end + + local v = self.vertexToTriangle[s] + if not v then + v = {} + self.vertexToTriangle[s] = v + end + + table.insert(v, n) + end + + table.insert(self.triangles, n) + end + + table.sort(self.edges, edge.less) + + for _, e in ipairs(self.edges) do + local aTriangles = self.vertexToTriangle[e.a.index] + local bTriangles = self.vertexToTriangle[e.b.index] + + local a, b = _findSharedTriangle(aTriangles, bTriangles, e) + if a and b then + self.edgeTriangles[e] = { a, b } + + do + local x = self.sharedTriangleEdges[a.index] + if not x then + x = {} + self.sharedTriangleEdges[a.index] = x + end + + x[b.index] = e + end + + do + local x = self.sharedTriangleEdges[b.index] + if not x then + x = {} + self.sharedTriangleEdges[b.index] = x + end + + x[a.index] = e + end + + do + local neighbors = self.triangleNeighbors[a.index] + if neighbors == nil then + neighbors = {} + self.triangleNeighbors[a.index] = neighbors + end + + local hasT = false + for _, neighbor in ipairs(neighbors) do + if neighbor.index == b.index then + hasT = true + break + end + end + + if not hasT then + table.insert(neighbors, b) + assert(#neighbors <= 3) + end + end + + do + local neighbors = self.triangleNeighbors[b.index] + if neighbors == nil then + neighbors = {} + self.triangleNeighbors[b.index] = neighbors + end + + local hasT = false + for _, neighbor in ipairs(neighbors) do + if neighbor.index == a.index then + hasT = true + break + end + end + + if not hasT then + table.insert(neighbors, a) + assert(#neighbors <= 3) + end + end + end + end + elseif z and type(z) == "table" and #z >= 2 and #z % 2 == 0 and type(z[1]) == "number" then + for i = 1, #z, 2 do + table.insert(self.inputEdges, z[i]) + table.insert(self.inputEdges, z[i + 1]) + table.insert(self.inputInteriorEdges, z[i]) + table.insert(self.inputInteriorEdges, z[i + 1]) + end + end + + return self +end + +--- @type slick.collision.quadTreeOptions +local _quadTreeOptions = { + x = 0, + y = 0, + width = 0, + height = 0 +} + +--- @private +function mesh:_buildQuadTree() + _quadTreeOptions.x = self.bounds:left() + _quadTreeOptions.y = self.bounds:top() + _quadTreeOptions.width = self.bounds:width() + _quadTreeOptions.height = self.bounds:height() + + self.quadTree = quadTree.new(_quadTreeOptions) + self.quadTreeQuery = quadTreeQuery.new(self.quadTree) + + for _, triangle in ipairs(self.triangles) do + self.quadTree:insert(triangle, triangle.bounds) + end +end + +local _getTrianglePoint = point.new() + +--- @param x number +--- @param y number +--- @return slick.navigation.triangle | nil +function mesh:getContainingTriangle(x, y) + if not self.quadTree then + self:_buildQuadTree() + end + + _getTrianglePoint:init(x, y) + self.quadTreeQuery:perform(_getTrianglePoint, slickmath.EPSILON) + + for _, hit in ipairs(self.quadTreeQuery.results) do + --- @cast hit slick.navigation.triangle + + local inside = true + local currentSide + for i = 1, #hit.triangle do + local side = slickmath.direction( + hit.triangle[i].point, + hit.triangle[(i % #hit.triangle) + 1].point, + _getTrianglePoint) + + -- Point is collinear with edge. + -- We consider this inside. + if side == 0 then + break + end + + if not currentSide then + currentSide = side + elseif currentSide ~= side then + inside = false + break + end + end + + if inside then + return hit + end + end + + return nil +end + +--- @param index number +--- @return slick.navigation.vertex +function mesh:getVertex(index) + return self.vertices[index] +end + +--- @param index number +--- @return slick.navigation.edge[] +function mesh:getTriangleNeighbors(index) + return self.triangleNeighbors[index] +end + +--- @param index number +--- @return slick.navigation.edge[] +function mesh:getVertexNeighbors(index) + return self.vertexNeighbors[index] +end + +local _insideSegment = segment.new() +local _triangleSegment = segment.new() +local _insideTriangleSegment = segment.new() + +--- @param a slick.navigation.vertex +--- @param b slick.navigation.vertex +--- @return boolean, number?, number? +function mesh:cross(a, b) + if not self.quadTree then + self:_buildQuadTree() + end + + _insideSegment:init(a.point, b.point) + self.quadTreeQuery:perform(_insideSegment) + + local hasIntersectedTriangle = false + local bestDistance = math.huge + local bestX, bestY + for _, hit in ipairs(self.quadTreeQuery.results) do + --- @cast hit slick.navigation.triangle + + local intersectedTriangle = false + for i, vertex in ipairs(hit.triangle) do + local otherVertex = hit.triangle[(i % #hit.triangle) + 1] + + _triangleSegment:init(vertex.point, otherVertex.point) + _insideTriangleSegment:init(vertex.point, otherVertex.point) + _insideTriangleSegment.a:init(vertex.point.x, vertex.point.y) + _insideTriangleSegment.b:init(otherVertex.point.x, otherVertex.point.y) + + local i, x, y, u, v = slickmath.intersection(vertex.point, otherVertex.point, a.point, b.point, slickmath.EPSILON) + if i and u and v then + intersectedTriangle = true + + if not self:isSharedEdge(vertex.index, otherVertex.index) then + local distance = (x - a.point.x) ^ 2 + (x - a.point.y) ^ 2 + if distance < bestDistance then + bestDistance = distance + bestX, bestY = x, y + end + end + end + end + + hasIntersectedTriangle = hasIntersectedTriangle or intersectedTriangle + end + + return hasIntersectedTriangle, bestX, bestY +end + +local _a = vertex.new(point.new(0, 0), nil, 1) +local _b = vertex.new(point.new(0, 0), nil, 2) +local _edge = edge.new(_a, _b) + +--- @param a number +--- @param b number +--- @return slick.navigation.edge +function mesh:getEdge(a, b) + _edge.a = self.vertices[a] + _edge.b = self.vertices[b] + _edge.min = math.min(a, b) + _edge.max = math.max(a, b) + + local index = search.first(self.edges, _edge, edge.compare) + return self.edges[index] +end + +--- @param a slick.navigation.triangle +--- @param b slick.navigation.triangle +--- @return slick.navigation.edge +function mesh:getSharedTriangleEdge(a, b) + local t = self.sharedTriangleEdges[a.index] + local e = t and t[b.index] + + return e +end + +--- @param a number +--- @param b number +function mesh:isSharedEdge(a, b) + local edge = self:getEdge(a, b) + local triangles = self.edgeTriangles[edge] + return triangles ~= nil and #triangles == 2 +end + +--- @param a number +--- @param b number +--- @return slick.navigation.triangle ... +function mesh:getEdgeTriangles(a, b) + local edge = self:getEdge(a, b) + local triangles = self.edgeTriangles[edge] + if not triangles then + return + end + + return unpack(triangles) +end + +return mesh diff --git a/game/love_src/lib/slick/navigation/meshBuilder.lua b/game/love_src/lib/slick/navigation/meshBuilder.lua new file mode 100644 index 0000000..a05d211 --- /dev/null +++ b/game/love_src/lib/slick/navigation/meshBuilder.lua @@ -0,0 +1,272 @@ +local cache = require "slick.cache" +local polygon = require "slick.collision.polygon" +local shapeGroup = require "slick.collision.shapeGroup" +local clipper = require "slick.geometry.clipper" +local enum = require "slick.enum" +local mesh = require "slick.navigation.mesh" +local tag = require "slick.tag" +local util = require "slick.util" +local slicktable = require "slick.util.slicktable" +local slickmath = require "slick.util.slickmath" +local lineSegment = require "slick.collision.lineSegment" + +--- @alias slick.navigation.navMeshBuilder.combineMode "union" | "difference" + +--- @alias slick.navigation.navMeshBuilder.layerSettings { +--- key: any, +--- combineMode: slick.navigation.navMeshBuilder.combineMode, +--- mesh: slick.navigation.mesh?, +--- } + +--- @class slick.navigation.navMeshBuilder +--- @field cache slick.cache +--- @field clipper slick.geometry.clipper +--- @field layers table +--- @field layerMeshes table +--- @field layerCombineMode slick.navigation.navMeshBuilder.combineMode +--- @field private cachedPolygon slick.collision.polygon +--- @field private pointsCache number[] +--- @field private edgesCache number[] +--- @field private userdataCache number[] +--- @field private inputTriangles number[][] +--- @field private trianglesCache number[][] +local navMeshBuilder = {} +local metatable = { __index = navMeshBuilder } + +--- @type slick.geometry.triangulation.delaunayTriangulationOptions +local triangulationOptions = { + refine = true, + interior = true, + exterior = false, + polygonization = false +} + +local function _getKey(t) + if util.is(t, tag) then + return t.value + elseif util.is(t, enum) then + return t + else + error("expected tag to be instance of slick.tag or slick.enum") + end +end + +--- @type slick.options +local defaultOptions = { + epsilon = slickmath.EPSILON, + debug = false +} + +--- @param options slick.options? +--- @return slick.navigation.navMeshBuilder +function navMeshBuilder.new(options) + local c = cache.new(options or defaultOptions) + return setmetatable({ + cache = c, + clipper = clipper.new(c.triangulator), + layers = {}, + layerMeshes = {}, + layerSettings = {}, + cachedPolygon = polygon.new(c), + pointsCache = {}, + edgesCache = {}, + userdataCache = {}, + }, metatable) +end + +--- @param t slick.tag | slick.enum +--- @param combineMode slick.navigation.navMeshBuilder.combineMode? +function navMeshBuilder:addLayer(t, combineMode) + local key = _getKey(t) + + for _, layer in ipairs(self.layers) do + if layer.key == key then + error("layer already exists") + end + end + + if combineMode == nil then + if #self.layers == 0 then + combineMode = "union" + else + combineMode = "difference" + end + end + + table.insert(self.layers, { + key = key, + combineMode = combineMode, + points = {}, + userdata = {}, + edges = {} + }) + + self.layerMeshes[key] = {} +end + +--- @param t slick.tag | slick.enum +--- @param m slick.navigation.mesh +function navMeshBuilder:addMesh(t, m) + local key = _getKey(t) + + local meshes = self.layerMeshes[key] + if not meshes then + error("layer with given slick.tag or slick.enum does not exist") + end + + table.insert(meshes, m) +end + +--- @param shape slick.collision.shape +--- @param points number[] +--- @param edges number[] +--- @param userdata any[] +local function _shapeToPointEdges(shape, points, edges, userdata, userdataValue) + slicktable.clear(points) + slicktable.clear(edges) + slicktable.clear(userdata) + + if util.is(shape, lineSegment) then + --- @cast shape slick.collision.lineSegment + for i = 1, #shape.vertices do + local v = shape.vertices[i] + + table.insert(points, v.x) + table.insert(points, v.y) + + if i < #shape.vertices then + table.insert(edges, i) + table.insert(edges, i + 1) + end + + if userdataValue ~= nil then + table.insert(userdata, userdataValue) + end + end + else + + --- @cast shape slick.collision.commonShape + for i, v in ipairs(shape.vertices) do + table.insert(points, v.x) + table.insert(points, v.y) + + local j = (i % #shape.vertices) + 1 + table.insert(edges, i) + table.insert(edges, j) + + if userdataValue ~= nil then + table.insert(userdata, userdataValue) + end + end + end +end + +local _previousShapePoints, _previousShapeEdges, _previousShapeUserdata = {}, {}, {} +local _currentShapePoints, _currentShapeEdges, _currentShapeUserdata = {}, {}, {} +local _nextShapePoints, _nextShapeEdges, _nextShapeUserdata = {}, {}, {} + +--- @param t slick.tag | slick.enum +--- @param shape slick.collision.shapeDefinition +--- @param userdata any +function navMeshBuilder:addShape(t, shape, userdata) + local shapes = shapeGroup.new(self.cache, nil, shape) + shapes:attach() + + _shapeToPointEdges(shapes.shapes[1], _previousShapePoints, _previousShapeEdges, _previousShapeUserdata, userdata) + local finalPoints, finalUserdata, finalEdges = _previousShapePoints, _previousShapeUserdata, _previousShapeEdges + + for i = 2, #shapes.shapes do + local shape = shapes.shapes[i] + + _shapeToPointEdges(shape, _currentShapePoints, _currentShapeEdges, _currentShapeUserdata, userdata) + + self.clipper:clip( + clipper.union, + _previousShapePoints, _previousShapeEdges, + _currentShapePoints, _currentShapeEdges, + nil, + _previousShapeUserdata, + _currentShapeUserdata, + _nextShapePoints, _nextShapeEdges, _nextShapeUserdata) + + finalPoints, finalUserdata, finalEdges = _nextShapePoints, _nextShapeUserdata, _nextShapeEdges + + _previousShapePoints, _nextShapePoints = _nextShapePoints, _previousShapePoints + _previousShapeEdges, _nextShapeEdges = _nextShapeEdges, _previousShapeEdges + _previousShapeUserdata, _nextShapeUserdata = _nextShapeUserdata, _previousShapeUserdata + end + + local m = mesh.new(finalPoints, finalUserdata, finalEdges) + self:addMesh(t, m) +end + +--- @private +--- @param options slick.geometry.clipper.clipOptions? +function navMeshBuilder:_prepareLayers(options) + for _, layer in ipairs(self.layers) do + local meshes = self.layerMeshes[layer.key] + + if #meshes >= 1 then + local currentPoints, currentEdges, currentUserdata, currentExteriorEdges, currentInteriorEdges = meshes[1].inputPoints, meshes[1].edges, meshes[1].inputUserdata, meshes[1].inputExteriorEdges, meshes[1].inputInteriorEdges + + for i = 2, #meshes do + currentPoints, currentEdges, currentUserdata, currentExteriorEdges, currentInteriorEdges = self.clipper:clip( + clipper.union, + currentPoints, { currentExteriorEdges, currentInteriorEdges }, + meshes[i].inputPoints, { meshes[i].inputExteriorEdges, meshes[i].inputInteriorEdges }, + options, + currentUserdata, + meshes[i].inputUserdata, + {}, {}, {}, {}, {}) + end + + layer.mesh = mesh.new(currentPoints, currentUserdata, currentExteriorEdges, currentInteriorEdges) + end + end +end + +--- @private +--- @param options slick.geometry.clipper.clipOptions? +function navMeshBuilder:_combineLayers(options) + local currentPoints, currentEdges, currentUserdata, currentExteriorEdges, currentInteriorEdges = self.layers[1].mesh.inputPoints, self.layers[1].mesh.edges, self.layers[1].mesh.inputUserdata, self.layers[1].mesh.inputExteriorEdges, self.layers[1].mesh.inputInteriorEdges + + for i = 2, #self.layers do + local layer = self.layers[i] + + local func + if layer.combineMode == "union" then + func = clipper.union + elseif layer.combineMode == "difference" then + func = clipper.difference + end + + if func and layer.mesh then + currentPoints, currentEdges, currentUserdata, currentExteriorEdges, currentInteriorEdges = self.clipper:clip( + func, + currentPoints, { currentExteriorEdges, currentInteriorEdges }, + layer.mesh.inputPoints, { layer.mesh.inputExteriorEdges, layer.mesh.inputInteriorEdges }, + options, + currentUserdata, + layer.mesh.inputUserdata, + {}, {}, {}, {}, {}) + end + end + + if #self.layers == 1 then + currentPoints, currentEdges, currentUserdata = self.cache.triangulator:clean(currentPoints, currentEdges, currentUserdata, options) + end + + return currentPoints, currentExteriorEdges, currentUserdata +end + +--- @param options slick.geometry.clipper.clipOptions? +function navMeshBuilder:build(options) + self:_prepareLayers(options) + + local points, edges, userdata = self:_combineLayers(options) + local triangles = self.cache.triangulator:triangulate(points, edges, triangulationOptions) + + return mesh.new(points, userdata, edges, triangles) +end + +return navMeshBuilder diff --git a/game/love_src/lib/slick/navigation/path.lua b/game/love_src/lib/slick/navigation/path.lua new file mode 100644 index 0000000..16ceb24 --- /dev/null +++ b/game/love_src/lib/slick/navigation/path.lua @@ -0,0 +1,453 @@ +local point = require "slick.geometry.point" +local segment = require "slick.geometry.segment" +local edge = require "slick.navigation.edge" +local vertex = require "slick.navigation.vertex" +local slicktable = require "slick.util.slicktable" +local slickmath = require "slick.util.slickmath" + +--- @class slick.navigation.pathOptions +--- @field optimize boolean? +--- @field neighbor nil | fun(from: slick.navigation.triangle, to: slick.navigation.triangle, e: slick.navigation.edge): boolean +--- @field neighbors nil | fun(mesh: slick.navigation.mesh, triangle: slick.navigation.triangle): slick.navigation.triangle[] | nil +--- @field distance nil | fun(from: slick.navigation.triangle, to: slick.navigation.triangle, e: slick.navigation.edge): number +--- @field heuristic nil | fun(triangle: slick.navigation.triangle, goalX: number, goalY: number): number +--- @field visit nil | fun(from: slick.navigation.triangle, to: slick.navigation.triangle, e: slick.navigation.edge): boolean? +--- @field yield nil | fun(): boolean? +local defaultPathOptions = { + optimize = true +} + +--- @param from slick.navigation.triangle +--- @param to slick.navigation.triangle +--- @param e slick.navigation.edge +--- @return boolean +function defaultPathOptions.neighbor(from, to, e) + return true +end + +--- @param mesh slick.navigation.mesh +--- @param triangle slick.navigation.triangle +--- @return slick.navigation.edge[] | nil +function defaultPathOptions.neighbors(mesh, triangle) + return mesh:getTriangleNeighbors(triangle.index) +end + +local _distanceEdgeSegment = segment.new() +local _distanceEdgeCenter = point.new() +--- @param from slick.navigation.triangle +--- @param to slick.navigation.triangle +--- @param e slick.navigation.edge +function defaultPathOptions.distance(from, to, e) + _distanceEdgeSegment:init(e.a.point, e.b.point) + _distanceEdgeSegment:lerp(0.5, _distanceEdgeCenter) + return from.centroid:distance(_distanceEdgeCenter) + to.centroid:distance(_distanceEdgeCenter) +end + + +--- @param triangle slick.navigation.triangle +--- @param goalX number +--- @param goalY number +--- @return number +function defaultPathOptions.heuristic(triangle, goalX, goalY) + return math.sqrt((triangle.centroid.x - goalX) ^ 2 + (triangle.centroid.y - goalY) ^ 2) +end + +function defaultPathOptions.yield() + -- Nothing. +end + +function defaultPathOptions.visit(triangle, e) + -- Nothing. +end + +--- @class slick.navigation.impl.pathBehavior +--- @field start slick.navigation.triangle | nil +--- @field goal slick.navigation.triangle | nil +local internalPathBehavior = {} + +--- @class slick.navigation.path +--- @field private options slick.navigation.pathOptions +--- @field private behavior slick.navigation.impl.pathBehavior +--- @field private fScores table +--- @field private gScores table +--- @field private hScores table +--- @field private visitedEdges table +--- @field private visitedTriangles table +--- @field private pending slick.navigation.triangle[] +--- @field private closed slick.navigation.triangle[] +--- @field private neighbors slick.navigation.triangle[] +--- @field private graph table +--- @field private path slick.navigation.edge[] +--- @field private portals slick.navigation.vertex[] +--- @field private funnel slick.navigation.vertex[] +--- @field private result slick.navigation.vertex[] +--- @field private startVertex slick.navigation.vertex +--- @field private goalVertex slick.navigation.vertex +--- @field private startEdge slick.navigation.edge +--- @field private goalEdge slick.navigation.edge +--- @field private sharedStartGoalEdge slick.navigation.edge +--- @field private _sortFScoreFunc fun(a: slick.navigation.triangle, b: slick.navigation.triangle): boolean +--- @field private _sortHScoreFunc fun(a: slick.navigation.triangle, b: slick.navigation.triangle): boolean +local path = {} +local metatable = { __index = path } + +--- @param options slick.navigation.pathOptions? +function path.new(options) + options = options or defaultPathOptions + + local self = setmetatable({ + options = { + optimize = options.optimize == nil and defaultPathOptions.optimize or not not options.optimize, + neighbor = options.neighbor or defaultPathOptions.neighbor, + neighbors = options.neighbors or defaultPathOptions.neighbors, + distance = options.distance or defaultPathOptions.distance, + heuristic = options.heuristic or defaultPathOptions.heuristic, + visit = options.visit or defaultPathOptions.visit, + yield = options.yield or defaultPathOptions.yield, + }, + + behavior = {}, + + fScores = {}, + gScores = {}, + hScores = {}, + visitedEdges = {}, + visitedTriangles = {}, + pending = {}, + closed = {}, + neighbors = {}, + graph = {}, + path = {}, + portals = {}, + funnel = {}, + result = {}, + + startVertex = vertex.new(point.new(0, 0), nil, -1), + goalVertex = vertex.new(point.new(0, 0), nil, -2), + }, metatable) + + self.startEdge = edge.new(self.startVertex, self.startVertex) + self.goalEdge = edge.new(self.goalVertex, self.goalVertex) + self.sharedStartGoalEdge = edge.new(self.startVertex, self.goalVertex) + + function self._sortFScoreFunc(a, b) + --- @diagnostic disable-next-line: invisible + return (self.fScores[a] or math.huge) > (self.fScores[b] or math.huge) + end + + function self._sortHScoreFunc(a, b) + --- @diagnostic disable-next-line: invisible + return (self.hScores[a] or math.huge) > (self.hScores[b] or math.huge) + end + + return self +end + +--- @private +--- @param mesh slick.navigation.mesh +--- @param triangle slick.navigation.triangle +--- @return slick.navigation.triangle[] +function path:_neighbors(mesh, triangle) + slicktable.clear(self.neighbors) + + local neighbors = self.options.neighbors(mesh, triangle) + if neighbors then + for _, neighbor in ipairs(neighbors) do + if self.options.neighbor(triangle, neighbor, mesh:getSharedTriangleEdge(triangle, neighbor)) then + table.insert(self.neighbors, neighbor) + end + end + end + + return self.neighbors +end + +--- @private +function path:_reset() + slicktable.clear(self.fScores) + slicktable.clear(self.gScores) + slicktable.clear(self.hScores) + slicktable.clear(self.visitedEdges) + slicktable.clear(self.visitedTriangles) + slicktable.clear(self.pending) + slicktable.clear(self.graph) +end + +--- @private +function path:_funnel() + slicktable.clear(self.funnel) + slicktable.clear(self.portals) + + table.insert(self.portals, self.path[1].a) + table.insert(self.portals, self.path[1].a) + for i = 2, #self.path - 1 do + local p = self.path[i] + + local C, D = p.a, p.b + local L, R = self.portals[#self.portals - 1], self.portals[#self.portals] + + local sign = slickmath.direction(D.point, C.point, L.point, slickmath.EPSILON) + sign = sign == 0 and slickmath.direction(D.point, C.point, R.point, slickmath.EPSILON) or sign + + if sign > 0 then + table.insert(self.portals, D) + table.insert(self.portals, C) + else + table.insert(self.portals, C) + table.insert(self.portals, D) + end + end + table.insert(self.portals, self.path[#self.path].b) + table.insert(self.portals, self.path[#self.path].b) + + local apex, left, right = self.portals[1], self.portals[1], self.portals[2] + local leftIndex, rightIndex = 1, 1 + + table.insert(self.funnel, apex) + + local n = #self.portals / 2 + local index = 2 + while index <= n do + local i = (index - 1) * 2 + 1 + local j = i + 1 + + local otherLeft = self.portals[i] + local otherRight = self.portals[j] + + local skip = false + if slickmath.direction(right.point, otherRight.point, apex.point, slickmath.EPSILON) <= 0 then + if apex.index == right.index or slickmath.direction(left.point, otherRight.point, apex.point, slickmath.EPSILON) > 0 then + right = otherRight + rightIndex = index + else + table.insert(self.funnel, left) + apex = left + right = left + + rightIndex = leftIndex + + index = leftIndex + skip = true + end + end + + if not skip and slickmath.direction(left.point, otherLeft.point, apex.point, slickmath.EPSILON) >= 0 then + if apex.index == left.index or slickmath.direction(right.point, otherLeft.point, apex.point, slickmath.EPSILON) < 0 then + left = otherLeft + leftIndex = index + else + table.insert(self.funnel, right) + apex = right + left = right + + leftIndex = rightIndex + + index = rightIndex + end + end + + index = index + 1 + end + + table.insert(self.funnel, self.portals[#self.portals]) +end + +--- @private +--- @param mesh slick.navigation.mesh +--- @param startX number +--- @param startY number +--- @param goalX number +--- @param goalY number +--- @param nearest boolean +--- @param result number[]? +--- @return number[]?, slick.navigation.vertex[]? +function path:_find(mesh, startX, startY, goalX, goalY, nearest, result) + self:_reset() + + self.behavior.start = mesh:getContainingTriangle(startX, startY) + self.behavior.goal = mesh:getContainingTriangle(goalX, goalY) + + if not self.behavior.start then + return nil + end + + self.fScores[self.behavior.start] = 0 + self.gScores[self.behavior.start] = 0 + + self.startVertex.point:init(startX, startY) + self.goalVertex.point:init(goalX, goalY) + + local pending = true + local current = self.behavior.start + while pending and current and current ~= self.behavior.goal do + if current == self.behavior.start then + self.visitedEdges[self.startEdge] = true + else + assert(current ~= self.behavior.goal, "cannot visit goal") + assert(self.graph[current], "current has no previous") + + local edge = mesh:getSharedTriangleEdge(self.graph[current], current) + assert(edge, "missing edge between previous and current") + + self.visitedEdges[edge] = true + end + + if not self.visitedTriangles[current] then + table.insert(self.closed, current) + self.visitedTriangles[current] = true + end + + for _, neighbor in ipairs(self:_neighbors(mesh, current)) do + local edge = mesh:getSharedTriangleEdge(current, neighbor) + if not self.visitedEdges[edge] then + local continuePathfinding = self.options.visit(current, neighbor, edge) + if continuePathfinding == false then + pending = false + break + end + + local distance = self.options.distance(current, neighbor, edge) + local pendingGScore = (self.gScores[current] or math.huge) + distance + if pendingGScore < (self.gScores[neighbor] or math.huge) then + local heuristic = self.options.heuristic(neighbor, goalX, goalY) + + self.graph[neighbor] = current + self.gScores[neighbor] = pendingGScore + self.hScores[neighbor] = heuristic + self.fScores[neighbor] = pendingGScore + heuristic + + table.insert(self.pending, neighbor) + table.sort(self.pending, self._sortFScoreFunc) + end + end + end + + local continuePathfinding = self.options.yield() + if continuePathfinding == false then + pending = false + end + + current = table.remove(self.pending) + end + + local reachedGoal = current == self.behavior.goal and self.behavior.goal + if not reachedGoal then + if not nearest then + return nil + end + + local bestTriangle = nil + local bestHScore = math.huge + for _, triangle in ipairs(self.closed) do + local hScore = self.hScores[triangle] + if hScore and hScore < bestHScore then + bestHScore = hScore + bestTriangle = triangle + end + end + + if not bestTriangle then + return nil + end + + current = bestTriangle + end + + slicktable.clear(self.path) + while current do + local next = self.graph[current] + if next then + table.insert(self.path, 1, mesh:getSharedTriangleEdge(current, next)) + end + + current = self.graph[current] + end + + if #self.path == 0 then + self.startVertex.point:init(startX, startY) + self.goalVertex.point:init(goalX, goalY) + + table.insert(self.path, self.sharedStartGoalEdge) + else + self.startEdge.a.point:init(startX, startY) + self.startEdge.b = self.path[1].a + self.startEdge.min = math.min(self.startEdge.a.index, self.startEdge.b.index) + self.startEdge.max = math.max(self.startEdge.a.index, self.startEdge.b.index) + + if self.startEdge.a.point:distance(self.startEdge.b.point) > slickmath.EPSILON then + table.insert(self.path, 1, self.startEdge) + end + + if reachedGoal then + self.goalEdge.a = self.path[#self.path].b + self.goalEdge.b.point:init(goalX, goalY) + self.goalEdge.min = math.min(self.goalEdge.a.index, self.goalEdge.b.index) + self.goalEdge.max = math.max(self.goalEdge.a.index, self.goalEdge.b.index) + + if self.goalEdge.a.point:distance(self.goalEdge.b.point) > slickmath.EPSILON then + table.insert(self.path, self.goalEdge) + end + end + end + + --- @type slick.navigation.vertex[] + local path + if self.options.optimize and #self.path > 1 then + self:_funnel() + path = self.funnel + else + slicktable.clear(self.result) + if #self.path == 1 then + local p = self.path[1] + table.insert(self.result, p.a) + table.insert(self.result, p.b) + else + table.insert(self.result, self.path[1].a) + + for i = 1, #self.path - 1 do + local p1 = self.path[i] + local p2 = self.path[i + 1] + + if p1.b.index == p2.a.index then + table.insert(self.result, p1.b) + elseif p1.a.index == p2.b.index then + table.insert(self.result, p1.a) + end + end + + table.insert(self.result, self.path[#self.path].b) + end + path = self.result + end + + result = result or {} + slicktable.clear(result) + for _, vertex in ipairs(path) do + table.insert(result, vertex.point.x) + table.insert(result, vertex.point.y) + end + + return result, path +end + +--- @param mesh slick.navigation.mesh +--- @param startX number +--- @param startY number +--- @param goalX number +--- @param goalY number +--- @return number[]?, slick.navigation.vertex[]? +function path:find(mesh, startX, startY, goalX, goalY) + return self:_find(mesh, startX, startY, goalX, goalY, false) +end + +--- @param mesh slick.navigation.mesh +--- @param startX number +--- @param startY number +--- @param goalX number +--- @param goalY number +--- @return number[]?, slick.navigation.vertex[]? +function path:nearest(mesh, startX, startY, goalX, goalY) + return self:_find(mesh, startX, startY, goalX, goalY, true) +end + +return path diff --git a/game/love_src/lib/slick/navigation/triangle.lua b/game/love_src/lib/slick/navigation/triangle.lua new file mode 100644 index 0000000..a75dd34 --- /dev/null +++ b/game/love_src/lib/slick/navigation/triangle.lua @@ -0,0 +1,38 @@ +local point = require "slick.geometry.point" +local rectangle = require "slick.geometry.rectangle" + +--- @class slick.navigation.triangle +--- @field triangle slick.navigation.vertex[] +--- @field vertices table +--- @field bounds slick.geometry.rectangle +--- @field centroid slick.geometry.point +--- @field index number +local triangle = {} +local metatable = { __index = triangle } + +--- @param a slick.navigation.vertex +--- @param b slick.navigation.vertex +--- @param c slick.navigation.vertex +--- @param index number +function triangle.new(a, b, c, index) + local self = setmetatable({ + triangle = { a, b, c }, + vertices = { + [a.index] = a, + [b.index] = b, + [c.index] = c + }, + centroid = point.new((a.point.x + b.point.x + c.point.x) / 3, (a.point.y + b.point.y + c.point.y) / 3), + bounds = rectangle.new(a.point.x, a.point.y), + index = index + }, metatable) + + for i = 2, #self.triangle do + local p = self.triangle[i].point + self.bounds:expand(p.x, p.y) + end + + return self +end + +return triangle diff --git a/game/love_src/lib/slick/navigation/vertex.lua b/game/love_src/lib/slick/navigation/vertex.lua new file mode 100644 index 0000000..2ea0652 --- /dev/null +++ b/game/love_src/lib/slick/navigation/vertex.lua @@ -0,0 +1,19 @@ +--- @class slick.navigation.vertex +--- @field point slick.geometry.point +--- @field userdata any +--- @field index number +local vertex = {} +local metatable = { __index = vertex } + +--- @param point slick.geometry.point +--- @param userdata any +--- @param index number +function vertex.new(point, userdata, index) + return setmetatable({ + point = point, + userdata = userdata, + index = index + }, metatable) +end + +return vertex diff --git a/game/love_src/lib/slick/options.lua b/game/love_src/lib/slick/options.lua new file mode 100644 index 0000000..e5ce8da --- /dev/null +++ b/game/love_src/lib/slick/options.lua @@ -0,0 +1,36 @@ +--- @class slick.options +--- @field epsilon number? +--- @field maxBounces number? +--- @field maxJitter number? +--- @field debug boolean? +--- @field quadTreeX number? +--- @field quadTreeY number? +--- @field quadTreeMaxLevels number? +--- @field quadTreeMaxData number? +--- @field quadTreeExpand boolean? +--- @field quadTreeOptimizationMargin number? +--- @field sharedCache slick.cache? +local defaultOptions = { + debug = false, + + maxBounces = 4, + maxJitter = 1, + + quadTreeMaxLevels = 8, + quadTreeMaxData = 8, + quadTreeExpand = true, + quadTreeOptimizationMargin = 0.25 +} + +--- @type slick.options +local defaultOptionsWrapper = setmetatable( + {}, + { + __metatable = true, + __index = defaultOptions, + __newindex = function() + error("default options is immutable", 2) + end + }) + +return defaultOptionsWrapper diff --git a/game/love_src/lib/slick/responses.lua b/game/love_src/lib/slick/responses.lua new file mode 100644 index 0000000..34e63f5 --- /dev/null +++ b/game/love_src/lib/slick/responses.lua @@ -0,0 +1,267 @@ +local point = require "slick.geometry.point" +local worldQuery = require "slick.worldQuery" +local slickmath = require "slick.util.slickmath" + +local _workingQueries = setmetatable({}, { __mode = "k" }) + +local function getWorkingQuery(world) + local workingQuery = _workingQueries[world] + if not _workingQueries[world] then + workingQuery = worldQuery.new(world) + _workingQueries[world] = workingQuery + end + + return workingQuery +end + +local _cachedSlideNormal = point.new() +local _cachedSlideCurrentPosition = point.new() +local _cachedSlideTouchPosition = point.new() +local _cachedSlideGoalPosition = point.new() +local _cachedSlideGoalDirection = point.new() +local _cachedSlideNewGoalPosition = point.new() +local _cachedSlideDirection = point.new() + +local function trySlide(normalX, normalY, touchX, touchY, x, y, goalX, goalY) + _cachedSlideCurrentPosition:init(x, y) + _cachedSlideTouchPosition:init(touchX, touchY) + _cachedSlideGoalPosition:init(goalX, goalY) + + _cachedSlideNormal:init(normalX, normalY) + _cachedSlideNormal:left(_cachedSlideGoalDirection) + + _cachedSlideCurrentPosition:direction(_cachedSlideGoalPosition, _cachedSlideNewGoalPosition) + _cachedSlideNewGoalPosition:normalize(_cachedSlideDirection) + + local goalDotDirection = _cachedSlideNewGoalPosition:dot(_cachedSlideGoalDirection) + _cachedSlideGoalDirection:multiplyScalar(goalDotDirection, _cachedSlideGoalDirection) + _cachedSlideTouchPosition:add(_cachedSlideGoalDirection, _cachedSlideNewGoalPosition) + + return _cachedSlideNewGoalPosition.x, _cachedSlideNewGoalPosition.y +end + +--- @param world slick.world +--- @param response slick.worldQueryResponse +--- @param query slick.worldQuery +--- @param x number +--- @param y number +--- @param goalX number +--- @param goalY number +--- @return boolean +local function findDidSlide(world, response, query, x, y, goalX, goalY) + if #query.results == 0 or query.results[1].time > 0 then + return true + end + + local didSlide = true + for _, otherResponse in ipairs(query.results) do + if otherResponse.time > 0 then + didSlide = false + break + end + + local otherResponseName = world:respond(otherResponse, query, x, y, goalX, goalY, true) + if (otherResponse.shape == response.shape and otherResponse.otherShape == response.otherShape) or otherResponseName == "slide" then + didSlide = false + break + end + end + + return didSlide +end + +local function didMove(x, y, goalX, goalY) + return not (x == goalX and y == goalY) +end + +local _slideNormals = {} + +--- @param world slick.world +--- @param query slick.worldQuery +--- @param response slick.worldQueryResponse +--- @param x number +--- @param y number +--- @param goalX number +--- @param goalY number +--- @param filter slick.worldFilterQueryFunc +--- @param result slick.worldQuery +--- @return number, number, number, number, string?, slick.worldQueryResponse? +local function slide(world, query, response, x, y, goalX, goalY, filter, result) + result:push(response) + + local touchX, touchY = response.touch.x, response.touch.y + local newGoalX, newGoalY = goalX, goalY + + local index = query:getResponseIndex(response) + local didSlide = false + local q = getWorkingQuery(world) + + _slideNormals[1], _slideNormals[2] = response.normals, response.alternateNormals + + for _, normals in ipairs(_slideNormals) do + for _, normal in ipairs(normals) do + local workingGoalX, workingGoalY = trySlide(normal.x, normal.y, response.touch.x, response.touch.y, x, y, goalX, goalY) + if didMove(response.touch.x, response.touch.y, workingGoalX, workingGoalY) then + world:project(response.item, response.touch.x, response.touch.y, workingGoalX, workingGoalY, filter, q) + didSlide = findDidSlide(world, response, q, response.touch.x, response.touch.y, workingGoalX, workingGoalY) + + if didSlide then + newGoalX = workingGoalX + newGoalY = workingGoalY + break + end + end + end + + if didSlide then + break + end + end + + if didSlide then + for i = index + 1, #query.results do + local otherResponse = query.results[i] + if not slickmath.lessThanEqual(otherResponse.time, response.time) then + break + end + + world:respond(otherResponse, query, touchX, touchY, newGoalX, newGoalY, false) + result:push(otherResponse) + end + + world:project(response.item, touchX, touchY, newGoalX, newGoalY, filter, query) + return touchX, touchY, newGoalX, newGoalY, nil, query.results[1] + else + local shape, otherShape = response.shape, response.otherShape + world:project(response.item, touchX, touchY, goalX, goalY, filter, query) + + --- @cast otherShape slick.collision.shape + local index = query:getShapeResponseIndex(shape, otherShape) + local nextIndex = index and (index + 1) or 1 + + --- @type slick.worldQueryResponse? + local nextResponse = query.results[nextIndex] + + if index and nextResponse then + local currentResponse = query.results[index] + if nextResponse.time > currentResponse.time then + nextResponse = nil + end + end + + return touchX, touchY, goalX, goalY, nil, nextResponse + end +end + +--- @param world slick.world +--- @param query slick.worldQuery +--- @param response slick.worldQueryResponse +--- @param x number +--- @param y number +--- @param goalX number +--- @param goalY number +--- @param filter slick.worldFilterQueryFunc +--- @param result slick.worldQuery +--- @return number, number, number, number, string?, slick.worldQueryResponse? +local function touch(world, query, response, x, y, goalX, goalY, filter, result) + result:push(response) + + local touchX, touchY = response.touch.x, response.touch.y + + local index = query:getResponseIndex(response) + local nextResponse = query.results[index + 1] + return touchX, touchY, touchX, touchY, nil, nextResponse +end + +--- @param world slick.world +--- @param query slick.worldQuery +--- @param response slick.worldQueryResponse +--- @param x number +--- @param y number +--- @param goalX number +--- @param goalY number +--- @param filter slick.worldFilterQueryFunc +--- @param result slick.worldQuery +--- @return number, number, number, number, string?, slick.worldQueryResponse? +local function cross(world, query, response, x, y, goalX, goalY, filter, result) + result:push(response) + + local index = query:getResponseIndex(response) + local nextResponse = query.results[index + 1] + + if not nextResponse then + return goalX, goalY, goalX, goalY, nil, nil + end + + return nextResponse.touch.x, nextResponse.touch.y, goalX, goalY, nil, nextResponse +end + +local _cachedBounceCurrentPosition = point.new() +local _cachedBounceTouchPosition = point.new() +local _cachedBounceGoalPosition = point.new() +local _cachedBounceNormal = point.new() +local _cachedBounceNewGoalPosition = point.new() +local _cachedBounceDirection = point.new() + +--- @param response slick.worldQueryResponse +--- @param x number +--- @param y number +--- @param goalX number +--- @param goalY number +local function getBounceNormal(response, x, y, goalX, goalY) + _cachedBounceCurrentPosition:init(x, y) + _cachedBounceTouchPosition:init(response.touch.x, response.touch.y) + _cachedBounceGoalPosition:init(goalX, goalY) + + _cachedBounceCurrentPosition:direction(_cachedBounceGoalPosition, _cachedBounceDirection) + _cachedBounceDirection:normalize(_cachedBounceDirection) + + local bounceNormalDot = 2 * response.normal:dot(_cachedBounceDirection) + response.normal:multiplyScalar(bounceNormalDot, _cachedBounceNormal) + _cachedBounceDirection:sub(_cachedBounceNormal, _cachedBounceNormal) + _cachedBounceNormal:normalize(_cachedBounceNormal) + + if _cachedBounceNormal:lengthSquared() == 0 then + response.normal:negate(_cachedBounceNormal) + end + + return _cachedBounceNormal +end + +--- @param world slick.world +--- @param query slick.worldQuery +--- @param response slick.worldQueryResponse +--- @param x number +--- @param y number +--- @param goalX number +--- @param goalY number +--- @param filter slick.worldFilterQueryFunc +--- @param result slick.worldQuery +--- @return number, number, number, number, string?, slick.worldQueryResponse? +local function bounce(world, query, response, x, y, goalX, goalY, filter, result) + local bounceNormal = getBounceNormal(response, x, y, goalX, goalY) + + local maxDistance = _cachedBounceCurrentPosition:distance(_cachedBounceGoalPosition) + local currentDistance = _cachedBounceCurrentPosition:distance(_cachedBounceTouchPosition) + local remainingDistance = maxDistance - currentDistance + + bounceNormal:multiplyScalar(remainingDistance, _cachedBounceNewGoalPosition) + _cachedBounceNewGoalPosition:add(_cachedBounceTouchPosition, _cachedBounceNewGoalPosition) + + local newGoalX = _cachedBounceNewGoalPosition.x + local newGoalY = _cachedBounceNewGoalPosition.y + local touchX, touchY = response.touch.x, response.touch.y + + response.extra.bounceNormal = query:allocate(point, bounceNormal.x, bounceNormal.y) + result:push(response, false) + + world:project(response.item, touchX, touchY, newGoalX, newGoalY, filter, query) + return touchX, touchY, newGoalX, newGoalY, nil, query.results[1] +end + +return { + slide = slide, + touch = touch, + cross = cross, + bounce = bounce +} diff --git a/game/love_src/lib/slick/shape.lua b/game/love_src/lib/slick/shape.lua new file mode 100644 index 0000000..a73d428 --- /dev/null +++ b/game/love_src/lib/slick/shape.lua @@ -0,0 +1,209 @@ +local box = require("slick.collision.box") +local lineSegment = require("slick.collision.lineSegment") +local polygon = require("slick.collision.polygon") +local polygonMesh = require("slick.collision.polygonMesh") +local shapeGroup = require("slick.collision.shapeGroup") +local tag = require("slick.tag") +local enum = require("slick.enum") +local util = require("slick.util") + +--- @param x number +--- @param y number +--- @param w number +--- @param h number +--- @param tag slick.tag | slick.enum | nil +--- @return slick.collision.shapeDefinition +local function newRectangle(x, y, w, h, tag) + return { + type = box, + n = 4, + tag = tag, + arguments = { x, y, w, h } + } +end + +--- @param x number +--- @param y number +--- @param radius number +--- @param segments number? +--- @param tag slick.tag | slick.enum | nil +--- @return slick.collision.shapeDefinition +local function newCircle(x, y, radius, segments, tag) + local points = segments or math.max(math.floor(math.sqrt(radius * 20)), 8) + local vertices = {} + local angleStep = (2 * math.pi) / points + + for angle = 0, 2 * math.pi - angleStep, angleStep do + table.insert(vertices, x + radius * math.cos(angle)) + table.insert(vertices, y + radius * math.sin(angle)) + end + + return { + type = polygon, + n = #vertices, + tag = tag, + arguments = vertices + } +end + +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +--- @param tag slick.tag | slick.enum | nil +--- @return slick.collision.shapeDefinition +local function newLineSegment(x1, y1, x2, y2, tag) + return { + type = lineSegment, + n = 4, + tag = tag, + arguments = { x1, y1, x2, y2 } + } +end + +local function _newChainHelper(points, i, j) + local length = #points / 2 + + i = i or 1 + j = j or length + + local k = (i % length) + 1 + local x1, y1 = points[(i - 1) * 2 + 1], points[(i - 1) * 2 + 2] + local x2, y2 = points[(k - 1) * 2 + 1], points[(k - 1) * 2 + 2] + if i == j then + return newLineSegment(x1, y1, x2, y2) + else + return newLineSegment(x1, y1, x2, y2), _newChainHelper(points, i + 1, j) + end +end + +--- @param points number[] an array of points in the form { x1, y2, x2, y2, ... } +--- @param tag slick.tag | slick.enum | nil +--- @return slick.collision.shapeDefinition +local function newChain(points, tag) + assert(#points % 2 == 0, "expected a list of (x, y) tuples") + assert(#points >= 6, "expected a minimum of 3 points") + + return { + type = shapeGroup, + n = #points / 2, + tag = tag, + arguments = { _newChainHelper(points) } + } +end + +local function _newPolylineHelper(segments, i, j) + i = i or 1 + j = j or #segments + + if i == j then + return newLineSegment(unpack(segments[i])) + else + return newLineSegment(unpack(segments[i])), _newPolylineHelper(segments, i + 1, j) + end +end + +--- @param segments number[][] an array of segments in the form { { x1, y1, x2, y2 }, { x1, y1, x2, y2 }, ... } +--- @param tag slick.tag | slick.enum | nil +--- @return slick.collision.shapeDefinition +local function newPolyline(segments, tag) + return { + type = shapeGroup, + n = #segments, + tag = tag, + arguments = { _newPolylineHelper(segments) } + } +end + +--- @param vertices number[] a list of x, y coordinates in the form `{ x1, y1, x2, y2, ..., xn, yn }` +--- @param tag slick.tag | slick.enum | nil +--- @return slick.collision.shapeDefinition +local function newPolygon(vertices, tag) + return { + type = polygon, + n = #vertices, + tag = tag, + arguments = { unpack(vertices) } + } +end + +--- @param ... any +--- @return number, slick.tag? +local function _getTagAndCount(...) + local n = select("#", ...) + + local maybeTag = select(select("#", ...), ...) + if util.is(maybeTag, tag) or util.is(maybeTag, enum) then + return n - 1, maybeTag + end + + return n, nil +end + +--- @param ... number[] | slick.tag | slick.enum a list of x, y coordinates in the form `{ x1, y1, x2, y2, ..., xn, yn }` +--- @return slick.collision.shapeDefinition +local function newPolygonMesh(...) + local n, tag = _getTagAndCount(...) + + return { + type = polygonMesh, + n = n, + tag = tag, + arguments = { ... } + } +end + +local function _newMeshHelper(polygons, i, j) + i = i or 1 + j = j or #polygons + + if i == j then + return newPolygon(polygons[i]) + else + return newPolygon(polygons[i]), _newMeshHelper(polygons, i + 1, j) + end +end + +--- @param polygons number[][] an array of segments in the form { { x1, y1, x2, y2, x3, y3, ..., xn, yn }, ... } +--- @param tag slick.tag | slick.enum | nil +--- @return slick.collision.shapeDefinition +local function newMesh(polygons, tag) + return { + type = shapeGroup, + n = #polygons, + tag = tag, + arguments = { _newMeshHelper(polygons) } + } +end + +--- @alias slick.collision.shapeDefinition { +--- type: { new: fun(entity: slick.entity | slick.cache, ...: any): slick.collision.shapelike }, +--- n: number, +--- tag: slick.tag?, +--- arguments: table, +--- } + +--- @param ... slick.collision.shapeDefinition | slick.tag +--- @return slick.collision.shapeDefinition +local function newShapeGroup(...) + local n, tag = _getTagAndCount(...) + + return { + type = shapeGroup, + n = n, + tag = tag, + arguments = { ... } + } +end + +return { + newRectangle = newRectangle, + newCircle = newCircle, + newLineSegment = newLineSegment, + newChain = newChain, + newPolyline = newPolyline, + newPolygon = newPolygon, + newPolygonMesh = newPolygonMesh, + newMesh = newMesh, + newShapeGroup = newShapeGroup, +} diff --git a/game/love_src/lib/slick/tag.lua b/game/love_src/lib/slick/tag.lua new file mode 100644 index 0000000..17c333e --- /dev/null +++ b/game/love_src/lib/slick/tag.lua @@ -0,0 +1,12 @@ +--- @class slick.tag +--- @field value any +local tag = {} +local metatable = { __index = tag } + +function tag.new(value) + return setmetatable({ + value = value + }, metatable) +end + +return tag diff --git a/game/love_src/lib/slick/util/common.lua b/game/love_src/lib/slick/util/common.lua new file mode 100644 index 0000000..4b2e850 --- /dev/null +++ b/game/love_src/lib/slick/util/common.lua @@ -0,0 +1,9 @@ +return { + is = function(obj, t) + return type(t) == "table" and type(obj) == "table" and getmetatable(obj) and getmetatable(obj).__index == t + end, + + type = function(obj) + return type(obj) == "table" and getmetatable(obj) and getmetatable(obj).__index + end +} diff --git a/game/love_src/lib/slick/util/init.lua b/game/love_src/lib/slick/util/init.lua new file mode 100644 index 0000000..6be37ed --- /dev/null +++ b/game/love_src/lib/slick/util/init.lua @@ -0,0 +1,11 @@ +local common = require("slick.util.common") + +return { + math = require("slick.util.slickmath"), + pool = require("slick.util.pool"), + search = require("slick.util.search"), + table = require("slick.util.slicktable"), + + is = common.is, + type = common.type +} diff --git a/game/love_src/lib/slick/util/pool.lua b/game/love_src/lib/slick/util/pool.lua new file mode 100644 index 0000000..149b466 --- /dev/null +++ b/game/love_src/lib/slick/util/pool.lua @@ -0,0 +1,106 @@ +local slicktable = require("slick.util.slicktable") +local util = require("slick.util.common") + +--- @class slick.util.pool +--- @field type { new: function } +--- @field used table +--- @field free table +local pool = {} +local metatable = { __index = pool } + +--- Constructs a new pool for the provided type. +--- @param poolType any +--- @return slick.util.pool +function pool.new(poolType) + return setmetatable({ + type = poolType, + used = {}, + free = {} + }, metatable) +end + +--- Removes `value` from this pool's scope. `value` will not be re-used. +--- Only removing allocated (**not free!**) values is permitted. If `value` is in the free list, +--- this will fail and return false. +--- @param value any +--- @return boolean result true if `value` was removed from this pool, false otherwise +function pool:remove(value) + if self.used[value] then + self.used[value] = nil + return true + end + + return false +end + +--- Adds `value` to this pool's scope. Behavior is undefined if `value` belongs to another pool. +--- If `value` is not exactly of the type this pool manages then it will not be added to this pool. +--- @param value any +--- @return boolean result true if `value` was added to this pool, false otherwise +function pool:add(value) + if util.is(value, self.type) then + self.used[value] = true + return true + end + + return false +end + +--- Moves `value` from the source pool to the target pool. +--- This effective removes `value` from `source` and adds `value` to `target` +--- @param source slick.util.pool +--- @param target slick.util.pool +--- @param value any +--- @return boolean result true if `value` was moved successfully, `false` otherwise +--- @see slick.util.pool.add +--- @see slick.util.pool.remove +function pool.swap(source, target, value) + return source:remove(value) and target:add(value) +end + +--- Allocates a new type, initializing the new instance with the provided arguments. +--- @param ... any arguments to pass to the new instance +--- @return any +function pool:allocate(...) + local result + if #self.free == 0 then + result = self.type and self.type.new() or {} + if self.type then + result:init(...) + end + + self.used[result] = true + else + result = table.remove(self.free, #self.free) + if self.type then + result:init(...) + end + + self.used[result] = true + end + return result +end + +--- Returns an instance to the pool. +--- @param t any the type to return to the pool +function pool:deallocate(t) + self.used[t] = nil + table.insert(self.free, t) +end + +--- Moves all used instances to the free instance list. +--- Anything returned by allocate is no longer considered valid - the instance may be reused. +--- @see slick.util.pool.allocate +function pool:reset() + for v in pairs(self.used) do + self:deallocate(v) + end +end + +--- Clears all tracking for free and used instances. +function pool:clear() + slicktable.clear(self.used) + slicktable.clear(self.free) +end + +return pool diff --git a/game/love_src/lib/slick/util/search.lua b/game/love_src/lib/slick/util/search.lua new file mode 100644 index 0000000..005e39b --- /dev/null +++ b/game/love_src/lib/slick/util/search.lua @@ -0,0 +1,147 @@ +local search = {} + +--- A result from a compare function. +--- A value compare than one means compare, zero means equal, and greater than one means greater +--- when comparing 'a' to 'b' (in that order). +---@alias slick.util.search.compareResult -1 | 0 | 1 + +--- A compare function to be used in a binary search. +--- @generic T +--- @generic O +--- @alias slick.util.search.compareFunc fun(a: T, b: O): slick.util.search.compareResult + +--- Finds the first value equal to `value` and returns the index of that value +--- @generic T +--- @generic O +--- @param array T[] +--- @param value T +--- @param compare slick.util.search.compareFunc +--- @param start number? +--- @param stop number? +--- @return number? +function search.first(array, value, compare, start, stop) + local result = search.lessThanEqual(array, value, compare, start, stop) + if result >= (start or 1) and result <= (stop or #array) and compare(array[result], value) == 0 then + return result + end + + return nil +end + +--- Finds the last value equal to `value` and returns the index of that value +--- @generic T +--- @generic O +--- @param array T[] +--- @param value T +--- @param compare slick.util.search.compareFunc +--- @param start number? +--- @param stop number? +--- @return number? +function search.last(array, value, compare, start, stop) + local result = search.greaterThanEqual(array, value, compare, start, stop) + if result >= (start or 1) and result <= (stop or #array) and compare(array[result], value) == 0 then + return result + end + + return nil +end + +--- Finds the first value less than `value` and returns the index of that value +--- @generic T +--- @generic O +--- @param array T[] +--- @param value T +--- @param compare slick.util.search.compareFunc +--- @param start number? +--- @param stop number? +--- @return number +function search.lessThan(array, value, compare, start, stop) + start = start or 1 + stop = stop or #array + + local result = start - 1 + while start <= stop do + local midPoint = math.floor((start + stop + 1) / 2) + if compare(array[midPoint], value) < 0 then + result = midPoint + start = midPoint + 1 + else + stop = midPoint - 1 + end + end + + return result +end + +--- Finds the first value less than or equal to `value` and returns the index of that value +--- @generic T +--- @generic O +--- @param array T[] +--- @param value T +--- @param compare slick.util.search.compareFunc +--- @param start number? +--- @param stop number? +--- @return number +function search.lessThanEqual(array, value, compare, start, stop) + local result = search.lessThan(array, value, compare, start, stop) + if result < (stop or #array) then + if compare(array[result + 1], value) == 0 then + result = result + 1 + end + end + + return result +end + +--- Finds the first value less greater than `value` and returns the index of that value +--- @generic T +--- @generic O +--- @param array T[] +--- @param value T +--- @param compare slick.util.search.compareFunc +--- @param start number? +--- @param stop number? +--- @return number +function search.greaterThan(array, value, compare, start, stop) + local start = start or 1 + local stop = stop or #array + + local result = stop + 1 + while start <= stop do + local midPoint = math.floor((start + stop + 1) / 2) + if compare(array[midPoint], value) > 0 then + result = midPoint + stop = midPoint - 1 + else + start = midPoint + 1 + end + end + + return result +end + +--- Finds the first value greater than or equal to `value` and returns the index of that value +--- @generic T +--- @generic O +--- @param array T[] +--- @param value T +--- @param compare slick.util.search.compareFunc +--- @param start number? +--- @param stop number? +--- @return number +function search.greaterThanEqual(array, value, compare, start, stop) + local result = search.greaterThan(array, value, compare, start, stop) + if result > (start or 1) then + if compare(array[result - 1], value) == 0 then + result = result - 1 + end + end + + return result +end + +--- @generic T +--- @generic O +--- @alias slick.util.search.searchFunc fun(array: T[], value: O, compare: slick.util.search.compareFunc, start: number?, stop: number?) + +return search diff --git a/game/love_src/lib/slick/util/slickmath.lua b/game/love_src/lib/slick/util/slickmath.lua new file mode 100644 index 0000000..220d36d --- /dev/null +++ b/game/love_src/lib/slick/util/slickmath.lua @@ -0,0 +1,324 @@ +local slickmath = {} + +slickmath.EPSILON = 1e-5 + +--- @param value number +--- @param increment number +--- @param max number +--- @return number +function slickmath.wrap(value, increment, max) + return (value + increment - 1) % max + 1 +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param c slick.geometry.point +--- @return number +function slickmath.angle(a, b, c) + local abx = a.x - b.x + local aby = a.y - b.y + local cbx = c.x - b.x + local cby = c.y - b.y + + local abLength = math.sqrt(abx ^ 2 + aby ^ 2) + local cbLength = math.sqrt(cbx ^ 2 + cby ^ 2) + + if abLength == 0 or cbLength == 0 then + return 0 + end + + local abNormalX = abx / abLength + local abNormalY = aby / abLength + local cbNormalX = cbx / cbLength + local cbNormalY = cby / cbLength + + local dot = abNormalX * cbNormalX + abNormalY * cbNormalY + if not (dot >= -1 and dot <= 1) then + return 0 + end + + return math.acos(dot) +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @return number +function slickmath.cross(a, b, c) + local left = (a.y - c.y) * (b.x - c.x) + local right = (a.x - c.x) * (b.y - c.y) + + return left - right +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param c slick.geometry.point +--- @param E number? +--- @return -1 | 0 | 1 +function slickmath.direction(a, b, c, E) + local result = slickmath.cross(a, b, c) + return slickmath.sign(result, E) +end + +--- Checks if `d` is inside the circumscribed circle created by `a`, `b`, and `c` +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param c slick.geometry.point +--- @param d slick.geometry.point +--- @return -1 | 0 | 1 +function slickmath.inside(a, b, c, d) + local ax = a.x - d.x + local ay = a.y - d.y + local bx = b.x - d.x + local by = b.y - d.y + local cx = c.x - d.x + local cy = c.y - d.y + + local i = (ax * ax + ay * ay) * (bx * cy - cx * by) + local j = (bx * bx + by * by) * (ax * cy - cx * ay) + local k = (cx * cx + cy * cy) * (ax * by - bx * ay) + local result = i - j + k + + return slickmath.sign(result) +end + +local function _collinear(a, b, c, d, E) + local abl = math.min(a, b) + local abh = math.max(a, b) + + local cdl = math.min(c, d) + local cdh = math.max(c, d) + + if cdh + E < abl or abh + E < cdl then + return false + end + + return true +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param c slick.geometry.point +--- @param d slick.geometry.point +--- @return boolean +function slickmath.collinear(a, b, c, d, E) + E = E or 0 + + local acdSign = slickmath.direction(a, c, d, E) + local bcdSign = slickmath.direction(b, c, d, E) + local cabSign = slickmath.direction(c, a, b, E) + local dabSign = slickmath.direction(d, a, b, E) + + if acdSign == 0 and bcdSign == 0 and cabSign == 0 and dabSign == 0 then + return _collinear(a.x, b.x, c.x, d.x, E) and _collinear(a.y, b.y, c.y, d.y, E) + end + + return false +end + +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param c slick.geometry.point +--- @param d slick.geometry.point +--- @param E number? +--- @return boolean, number?, number?, number?, number? +function slickmath.intersection(a, b, c, d, E) + E = E or 0 + + local acdSign = slickmath.direction(a, c, d, E) + local bcdSign = slickmath.direction(b, c, d, E) + if (acdSign < 0 and bcdSign < 0) or (acdSign > 0 and bcdSign > 0) then + return false + end + + local cabSign = slickmath.direction(c, a, b, E) + local dabSign = slickmath.direction(d, a, b, E) + if (cabSign < 0 and dabSign < 0) or (cabSign > 0 and dabSign > 0) then + return false + end + + if acdSign == 0 and bcdSign == 0 and cabSign == 0 and dabSign == 0 then + return slickmath.collinear(a, b, c, d, E) + end + + local bax = b.x - a.x + local bay = b.y - a.y + local dcx = d.x - c.x + local dcy = d.y - c.y + + local baCrossDC = bax * dcy - bay * dcx + local dcCrossBA = dcx * bay - dcy * bax + if baCrossDC == 0 or dcCrossBA == 0 then + return false + end + + local acx = a.x - c.x + local acy = a.y - c.y + local cax = c.x - a.x + local cay = c.y - a.y + + local dcCrossAC = dcx * acy - dcy * acx + local baCrossCA = bax * cay - bay * cax + + local u = dcCrossAC / baCrossDC + local v = baCrossCA / dcCrossBA + + if u < -E or u > (1 + E) or v < -E or v > (1 + E) then + return false + end + + local rx = a.x + bax * u + local ry = a.y + bay * u + + return true, rx, ry, u, v +end + +--- @param s slick.geometry.segment +--- @param p slick.geometry.point +--- @param r number +--- @param E number? +--- @return boolean, number?, number? +function slickmath.lineCircleIntersection(s, p, r, E) + E = E or 0 + + local p1 = s.a + local p2 = s.b + + local rSquared = r ^ 2 + + local dx = p2.x - p1.x + local dy = p2.y - p1.y + + local fx = p1.x - p.x + local fy = p1.y - p.y + + local a = dx ^ 2 + dy ^ 2 + local b = 2 * (dx * fx + dy * fy) + local c = fx ^ 2 + fy ^ 2 - rSquared + + local d = b ^ 2 - 4 * a * c + if a <= 0 or d < -E then + return false, nil, nil + end + + d = math.sqrt(math.max(d, 0)) + + local u = (-b - d) / (2 * a) + local v = (-b + d) / (2 * a) + + return true, u, v +end + +--- @param p1 slick.geometry.point +--- @param r1 number +--- @param p2 slick.geometry.point +--- @param r2 number +--- @return boolean, number?, number?, number?, number? +function slickmath.circleCircleIntersection(p1, r1, p2, r2) + local nx = p2.x - p1.x + local ny = p2.y - p1.y + + local radius = r1 + r2 + local magnitude = nx ^ 2 + ny ^ 2 + if magnitude <= radius ^ 2 then + if magnitude == 0 then + return true, nil, nil, nil, nil + elseif magnitude < math.abs(r1 - r2) ^ 2 then + return true, nil, nil, nil, nil + end + + local d = math.sqrt(magnitude) + + if d > 0 then + nx = nx / d + ny = ny / d + end + + local a = (r1 ^ 2 - r2 ^ 2 + magnitude) / (2 * d) + local h = math.sqrt(r1 ^ 2 - a ^ 2) + + local directionX = p2.x - p1.x + local directionY = p2.y - p1.y + local p3x = p1.x + a * directionX / d + local p3y = p1.y + a * directionY / d + + local result1X = p3x + h * directionY / d + local result1Y = p3y - h * directionX / d + + local result2X = p3x - h * directionY / d + local result2Y = p3y + h * directionX / d + + return true, result1X, result1Y, result2X, result2Y + end + + return false, nil, nil, nil, nil +end + +--- @param value number +--- @param E number? +--- @return -1 | 0 | 1 +function slickmath.sign(value, E) + E = E or 0 + + if math.abs(value) <= E then + return 0 + end + + if value > 0 then + return 1 + elseif value < 0 then + return -1 + end + + return 0 +end + +--- @param min number +--- @param max number +--- @param rng love.RandomGenerator? +--- @return number +function slickmath.random(min, max, rng) + if rng then + return rng:random(min, max) + end + + if love and love.math then + return love.math.random(min, max) + end + + return math.random(min, max) +end + +function slickmath.withinRange(value, min, max, E) + E = E or slickmath.EPSILON + + return value > min - E and value < max + E +end + +function slickmath.equal(a, b, E) + E = E or slickmath.EPSILON + + return math.abs(a - b) < E +end + +function slickmath.less(a, b, E) + E = E or slickmath.EPSILON + + return a < b + E +end + +function slickmath.greater(a, b, E) + E = E or slickmath.EPSILON + + return a > b - E +end + +function slickmath.lessThanEqual(a, b, E) + return slickmath.less(a, b, E) or slickmath.equal(a, b, E) +end + +function slickmath.greaterThanEqual(a, b, E) + return slickmath.greater(a, b, E) or slickmath.equal(a, b, E) +end + +return slickmath diff --git a/game/love_src/lib/slick/util/slicktable.lua b/game/love_src/lib/slick/util/slicktable.lua new file mode 100644 index 0000000..a847302 --- /dev/null +++ b/game/love_src/lib/slick/util/slicktable.lua @@ -0,0 +1,39 @@ +local slicktable = {} + +--- @type fun(t: table) +local clear +do + local s, r = pcall(require, "table.clear") + if s then + clear = r + else + function clear(t) + while #t > 0 do + table.remove(t, #t) + end + + for k in pairs(t) do + t[k] = nil + end + end + end +end + +slicktable.clear = clear + +--- @param t table +--- @param i number? +--- @param j number? +local function reverse(t, i, j) + i = i or 1 + j = j or #t + + if i > j then + t[i], t[j] = t[j], t[i] + return reverse(t, i + 1, j - 1) + end +end + +slicktable.reverse = reverse + +return slicktable diff --git a/game/love_src/lib/slick/world.lua b/game/love_src/lib/slick/world.lua new file mode 100644 index 0000000..c7d02b4 --- /dev/null +++ b/game/love_src/lib/slick/world.lua @@ -0,0 +1,641 @@ +local cache = require("slick.cache") +local quadTree = require("slick.collision.quadTree") +local entity = require("slick.entity") +local point = require("slick.geometry.point") +local ray = require("slick.geometry.ray") +local rectangle = require("slick.geometry.rectangle") +local segment = require("slick.geometry.segment") +local transform = require("slick.geometry.transform") +local defaultOptions = require("slick.options") +local responses = require("slick.responses") +local worldQuery = require("slick.worldQuery") +local util = require("slick.util") +local slickmath = require("slick.util.slickmath") +local slicktable = require("slick.util.slicktable") + +--- @alias slick.worldFilterQueryFunc fun(item: any, other: any, shape: slick.collision.shape, otherShape: slick.collision.shape): string | slick.worldVisitFunc | false +local function defaultWorldFilterQueryFunc() + return "slide" +end + +--- @alias slick.worldShapeFilterQueryFunc fun(item: any, shape: slick.collision.shape): boolean +local function defaultWorldShapeFilterQueryFunc() + return true +end + +--- @alias slick.worldResponseFunc fun(world: slick.world, query: slick.worldQuery, response: slick.worldQueryResponse, x: number, y: number, goalX: number, goalY: number, filter: slick.worldFilterQueryFunc, result: slick.worldQuery): number, number, number, number, string?, slick.worldQueryResponse +--- @alias slick.worldVisitFunc fun(item: any, world: slick.world, query: slick.worldQuery, response: slick.worldQueryResponse, x: number, y: number, goalX: number, goalY: number, projection: boolean): string + +--- @class slick.world +--- @field cache slick.cache +--- @field quadTree slick.collision.quadTree +--- @field options slick.options +--- @field quadTreeOptions slick.collision.quadTreeOptions +--- @field private responses table +--- @field private entities slick.entity[] +--- @field private itemToEntity table +--- @field private freeWorldQueries slick.worldQuery[] +--- @field private freeList number[] +--- @field private cachedQuery slick.worldQuery +--- @field private cachedPushQuery slick.worldQuery +local world = {} +local metatable = { __index = world } + +--- @param t slick.collision.quadTreeOptions? +--- @param width number? +--- @param height number? +--- @param options slick.options? +--- @return slick.collision.quadTreeOptions +local function _getQuadTreeOptions(t, width, height, options) + t = t or {} + options = options or defaultOptions + + t.width = width or t.width + t.height = height or t.height + t.x = options.quadTreeX or t.x or defaultOptions.quadTreeX + t.y = options.quadTreeY or t.y or defaultOptions.quadTreeY + t.maxLevels = options.quadTreeMaxLevels or t.maxLevels or defaultOptions.quadTreeMaxLevels + t.maxData = options.quadTreeMaxData or t.maxData or defaultOptions.quadTreeMaxData + t.expand = options.quadTreeExpand == nil and (t.expand == nil and defaultOptions.quadTreeExpand or t.expand) or options.quadTreeExpand + + return t +end + +--- @param width number +--- @param height number +--- @param options slick.options? +function world.new(width, height, options) + assert(type(width) == "number" and width > 0, "expected width to be number > 0") + assert(type(height) == "number" and height > 0, "expected height to be number > 0") + + options = options or defaultOptions + + local quadTreeOptions = _getQuadTreeOptions({}, width, height, options) + + local selfOptions = { + debug = options.debug == nil and defaultOptions.debug or options.debug, + epsilon = options.epsilon or defaultOptions.epsilon or slickmath.EPSILON, + maxBounces = options.maxBounces or defaultOptions.maxBounces, + maxJitter = options.maxJitter or defaultOptions.maxJitter, + quadTreeOptimizationMargin = options.quadTreeOptimizationMargin or defaultOptions.quadTreeOptimizationMargin + } + + local self = setmetatable({ + cache = cache.new(options), + options = selfOptions, + quadTreeOptions = quadTreeOptions, + quadTree = quadTree.new(quadTreeOptions), + entities = {}, + itemToEntity = {}, + freeList = {}, + visited = {}, + responses = {}, + freeWorldQueries = {} + }, metatable) + + self.cachedQuery = worldQuery.new(self) + self.cachedPushQuery = worldQuery.new(self) + + self:addResponse("slide", responses.slide) + self:addResponse("touch", responses.touch) + self:addResponse("cross", responses.cross) + self:addResponse("bounce", responses.bounce) + + return self +end + +local _cachedTransform = transform.new() + +--- @overload fun(e: slick.entity, x: number, y: number, shape: slick.collision.shapelike): slick.entity +--- @overload fun(e: slick.entity, transform: slick.geometry.transform, shape: slick.collision.shapelike): slick.entity +--- @return slick.geometry.transform, slick.collision.shapeDefinition +local function _getTransformShapes(e, a, b, c) + if type(a) == "number" and type(b) == "number" then + e.transform:copy(_cachedTransform) + _cachedTransform:setTransform(a, b) + + --- @cast c slick.collision.shapeDefinition + return _cachedTransform, c + end + + assert(util.is(a, transform)) + + --- @cast a slick.geometry.transform + --- @cast b slick.collision.shapeDefinition + return a, b +end + +--- @param item any +--- @return slick.entity +--- @overload fun(self: slick.world, item: any, x: number, y: number, shape: slick.collision.shapeDefinition): slick.entity +--- @overload fun(self: slick.world, item: any, transform: slick.geometry.transform, shape: slick.collision.shapeDefinition): slick.entity +function world:add(item, a, b, c) + assert(not self:has(item), "item exists in world") + + --- @type slick.entity + local e + + --- @type number + local i + if #self.freeList > 0 then + i = table.remove(self.freeList) + e = self.entities[i] + else + e = entity.new() + table.insert(self.entities, e) + i = #self.entities + end + + e:init(item) + + local transform, shapes = _getTransformShapes(e, a, b, c) + e:setTransform(transform) + e:setShapes(shapes) + e:add(self) + + self.itemToEntity[item] = i + + --- @type slick.worldQuery + local query = table.remove(self.freeWorldQueries) or worldQuery.new(self) + query:reset() + + return e +end + +--- @param item any +--- @return slick.entity +function world:get(item) + return self.entities[self.itemToEntity[item]] +end + +--- @param items any[]? +--- @return any[] +function world:getItems(items) + items = items or {} + slicktable.clear(items) + + for item in pairs(self.itemToEntity) do + table.insert(items, item) + end + + return items +end + +function world:has(item) + return self:get(item) ~= nil +end + +--- @overload fun(self: slick.world, item: any, x: number, y: number, shape: slick.collision.shapeDefinition): number, number +--- @overload fun(self: slick.world, item: any, transform: slick.geometry.transform, shape: slick.collision.shapeDefinition): number, number +function world:update(item, a, b, c) + local e = self:get(item) + + local transform, shapes = _getTransformShapes(e, a, b, c) + if shapes then + e:setShapes(shapes) + end + e:setTransform(transform) + + return transform.x, transform.y +end + +--- @overload fun(self: slick.world, item: any, filter: slick.worldFilterQueryFunc, x: number, y: number, shape: slick.collision.shapeDefinition?): number, number +--- @overload fun(self: slick.world, item: any, filter: slick.worldFilterQueryFunc, transform: slick.geometry.transform, shape: slick.collision.shapeDefinition?): number, number +function world:push(item, filter, a, b, c) + local e = self:get(item) + local transform, shapes = _getTransformShapes(e, a, b, c) + self:update(item, transform, shapes) + + local cachedQuery = self.cachedQuery + local x, y = transform.x, transform.y + local originalX, originalY = x, y + + local visited = self.cachedPushQuery + visited:reset() + + self:project(item, x, y, x, y, filter, cachedQuery) + while #cachedQuery.results > 0 do + --- @type slick.worldQueryResponse + local result + for _, r in ipairs(cachedQuery.results) do + if r.offset:lengthSquared() > 0 then + result = r + break + end + end + + if not result then + break + end + + local count = 0 + for _, visitedResult in ipairs(visited.results) do + if visitedResult.shape == result.shape and visitedResult.otherShape == result.otherShape then + count = count + 1 + end + end + + local pushFactor = 1.1 ^ count + local offsetX, offsetY = result.offset.x, result.offset.y + offsetX = offsetX * pushFactor + offsetY = offsetY * pushFactor + + x = x + offsetX + y = y + offsetY + + visited:push(result) + self:project(item, x, y, x, y, filter, cachedQuery) + end + + self:project(item, x, y, originalX, originalY, filter, cachedQuery) + if #cachedQuery.results >= 1 then + local result = cachedQuery.results[1] + x, y = result.touch.x, result.touch.y + end + + transform:setTransform(x, y) + e:setTransform(transform) + + return x, y +end + +local _cachedRotateBounds = rectangle.new() +local _cachedRotateItems = {} + +--- @param item any +--- @param angle number +--- @param rotateFilter slick.worldFilterQueryFunc +--- @param pushFilter slick.worldFilterQueryFunc +function world:rotate(item, angle, rotateFilter, pushFilter, query) + query = query or worldQuery.new(self) + + local e = self:get(item) + + e.transform:copy(_cachedTransform) + _cachedTransform:setTransform(nil, nil, angle) + + _cachedRotateBounds:init(e.bounds:left(), e.bounds:top(), e.bounds:right(), e.bounds:bottom()) + e:setTransform(_cachedTransform) + _cachedRotateBounds:expand(e.bounds.topLeft.x, e.bounds.topLeft.y) + _cachedRotateBounds:expand(e.bounds.bottomRight.x, e.bounds.bottomRight.y) + + slicktable.clear(_cachedRotateItems) + _cachedRotateItems[item] = true + + local responses, numResponses = self:queryRectangle(_cachedRotateBounds:left(), _cachedRotateBounds:top(), _cachedRotateBounds:width(), _cachedRotateBounds:height(), rotateFilter, query) + for _, response in ipairs(responses) do + if not _cachedRotateItems[response.item] then + _cachedRotateItems[response.item] = true + self:push(response.item, pushFilter, response.entity.transform.x, response.entity.transform.y) + end + end + + return responses, numResponses, query +end + +world.wiggle = world.push + +--- @param deltaTime number +function world:frame(deltaTime) + -- Nothing for now. +end + +--- @param item any +function world:remove(item) + local entityIndex = self.itemToEntity[item] + local e = self.entities[entityIndex] + + e:detach() + table.insert(self.freeList, entityIndex) + + self.itemToEntity[item] = nil +end + +--- @param item any +--- @param x number +--- @param y number +--- @param goalX number +--- @param goalY number +--- @param filter slick.worldFilterQueryFunc? +--- @param query slick.worldQuery? +--- @return slick.worldQueryResponse[], number, slick.worldQuery +function world:project(item, x, y, goalX, goalY, filter, query) + query = query or worldQuery.new(self) + local e = self:get(item) + + query:performProjection(e, x, y, goalX, goalY, filter or defaultWorldFilterQueryFunc) + + return query.results, #query.results, query +end + +--- @param item any +--- @param x number +--- @param y number +--- @param filter slick.worldFilterQueryFunc? +--- @param query slick.worldQuery? +--- @return slick.worldQueryResponse[], number, slick.worldQuery +function world:test(item, x, y, filter, query) + return self:project(item, x, y, x, y, filter, query) +end + +local _cachedQueryRectangle = rectangle.new() + +--- @param x number +--- @param y number +--- @param w number +--- @param h number +--- @param filter slick.worldShapeFilterQueryFunc? +--- @param query slick.worldQuery? +--- @return slick.worldQueryResponse[], number, slick.worldQuery +function world:queryRectangle(x, y, w, h, filter, query) + query = query or worldQuery.new(self) + + _cachedQueryRectangle:init(x, y, x + w, y + h) + query:performPrimitive(_cachedQueryRectangle, filter or defaultWorldShapeFilterQueryFunc) + + return query.results, #query.results, query +end + +local _cachedQuerySegment = segment.new() + +--- @param x1 number +--- @param y1 number +--- @param x2 number +--- @param y2 number +--- @param filter slick.worldShapeFilterQueryFunc? +--- @param query slick.worldQuery? +--- @return slick.worldQueryResponse[], number, slick.worldQuery +function world:querySegment(x1, y1, x2, y2, filter, query) + query = query or worldQuery.new(self) + + _cachedQuerySegment.a:init(x1, y1) + _cachedQuerySegment.b:init(x2, y2) + query:performPrimitive(_cachedQuerySegment, filter or defaultWorldShapeFilterQueryFunc) + + return query.results, #query.results, query +end + +local _cachedQueryRay = ray.new() + +--- @param originX number +--- @param originY number +--- @param directionX number +--- @param directionY number +--- @param filter slick.worldShapeFilterQueryFunc? +--- @param query slick.worldQuery? +--- @return slick.worldQueryResponse[], number, slick.worldQuery +function world:queryRay(originX, originY, directionX, directionY, filter, query) + query = query or worldQuery.new(self) + + _cachedQueryRay.origin:init(originX, originY) + _cachedQueryRay.direction:init(directionX, directionY) + if _cachedQueryRay.direction:lengthSquared() > 0 then + _cachedQueryRay.direction:normalize(_cachedQueryRay.direction) + end + + query:performPrimitive(_cachedQueryRay, filter or defaultWorldShapeFilterQueryFunc) + + return query.results, #query.results, query +end + +local _cachedQueryPoint = point.new() + +--- @param x number +--- @param y number +--- @param filter slick.worldShapeFilterQueryFunc? +--- @param query slick.worldQuery? +--- @return slick.worldQueryResponse[], number, slick.worldQuery +function world:queryPoint(x, y, filter, query) + query = query or worldQuery.new(self) + + _cachedQueryPoint:init(x, y) + query:performPrimitive(_cachedQueryPoint, filter or defaultWorldShapeFilterQueryFunc) + + return query.results, #query.results, query +end + +--- @param result slick.worldQueryResponse +--- @param query slick.worldQuery +--- @param x number +--- @param y number +--- @param goalX number +--- @param goalY number +--- @param projection? boolean +--- @return string +function world:respond(result, query, x, y, goalX, goalY, projection) + --- @type string + local responseName + if type(result.response) == "function" or type(result.response) == "table" then + responseName = result.response(result.item, self, query, result, x, y, goalX, goalY, not not projection) + elseif type(result.response) == "string" then + --- @diagnostic disable-next-line: cast-local-type + responseName = result.response + else + responseName = "slide" + end + result.response = responseName + + --- @cast responseName string + return responseName +end + +local _cachedRemappedHandlers = {} + +--- @param item any +--- @param goalX number +--- @param goalY number +--- @param filter slick.worldFilterQueryFunc? +--- @param query slick.worldQuery? +--- @return number, number, slick.worldQueryResponse[], number, slick.worldQuery +function world:check(item, goalX, goalY, filter, query) + if query then + query:reset() + else + query = worldQuery.new(self) + end + + slicktable.clear(_cachedRemappedHandlers) + + + local cachedQuery = self.cachedQuery + filter = filter or defaultWorldFilterQueryFunc + + local e = self:get(item) + local x, y = e.transform.x, e.transform.y + + self:project(item, x, y, goalX, goalY, filter, cachedQuery) + if #cachedQuery.results == 0 then + return goalX, goalY, query.results, #query.results, query + end + + local previousX, previousY + + local actualX, actualY + local bounces = 0 + while bounces < self.options.maxBounces and #cachedQuery.results > 0 do + bounces = bounces + 1 + + local result = cachedQuery.results[1] + + --- @type slick.collision.shape + local shape, otherShape + repeat + shape = result.shape + otherShape = result.otherShape + + --- @type string + local responseName = self:respond(result, query, x, y, goalX, goalY, false) + responseName = _cachedRemappedHandlers[otherShape] or responseName + + assert(type(responseName) == "string", "expect name of response handler as string") + + local response = self:getResponse(responseName) + + local remappedResponseName, nextResult + x, y, goalX, goalY, remappedResponseName, nextResult = response(self, cachedQuery, result, x, y, goalX, goalY, filter, query) + + --- @cast otherShape slick.collision.shapelike + _cachedRemappedHandlers[otherShape] = remappedResponseName + + result = nextResult + until not result or (shape == result.shape and otherShape == result.otherShape) + + local isStationary = x == goalX and y == goalY + local didMove = not (x == previousX and y == previousY) + + local isSameCollision = #cachedQuery.results >= 1 and cachedQuery.results[1].shape == shape and cachedQuery.results[1].otherShape == otherShape + for i = 2, #cachedQuery.results do + if isSameCollision then + break + end + + if cachedQuery.results[i].time > cachedQuery.results[1].time then + break + end + + if cachedQuery.results[i].shape == shape and cachedQuery.results[i].otherShape == otherShape then + isSameCollision = true + break + end + end + + local hasNoCollisions = #cachedQuery.results == 0 + + if hasNoCollisions or isStationary then + actualX = goalX + actualY = goalY + break + else + actualX = x + actualY = y + end + + if didMove and not result then + break + end + + if not didMove and isSameCollision then + break + end + + previousX, previousY = x, y + end + + return actualX, actualY, query.results, #query.results, query +end + +--- @param item any +--- @param goalX number +--- @param goalY number +--- @param filter slick.worldFilterQueryFunc? +--- @param query slick.worldQuery? +--- @return number +--- @return number +--- @return slick.worldQueryResponse[] +--- @return number +--- @return slick.worldQuery +function world:move(item, goalX, goalY, filter, query) + local actualX, actualY, _, _, query = self:check(item, goalX, goalY, filter, query) + self:update(item, actualX, actualY) + + return actualX, actualY, query.results, #query.results, query +end + +--- @param width number? +--- @param height number? +--- @param options slick.options? +function world:optimize(width, height, options) + local x1, y1, x2, y2 = self.quadTree:computeExactBounds() + + local realWidth = x2 - x1 + local realHeight = y2 - y1 + + width = width or realWidth + height = height or realHeight + + local x = options and options.quadTreeX or x1 + local y = options and options.quadTreeY or y1 + + local margin = options and options.quadTreeOptimizationMargin or self.options.quadTreeOptimizationMargin + self.options.quadTreeOptimizationMargin = margin + + x = x - realWidth * (margin / 2) + y = y - realHeight * (margin / 2) + width = width * (1 + margin / 2) + height = height * (1 + margin / 2) + + self.quadTreeOptions.x = x + self.quadTreeOptions.y = y + self.quadTreeOptions.width = width + self.quadTreeOptions.height = height + + _getQuadTreeOptions(self.quadTreeOptions, width, height, options) + self.quadTree:rebuild(self.quadTreeOptions) +end + +--- @package +--- @param shape slick.collision.shape +function world:_addShape(shape) + self.quadTree:update(shape, shape.bounds) +end + +--- @package +--- @param shape slick.collision.shape +function world:_removeShape(shape) + if self.quadTree:has(shape) then + self.quadTree:remove(shape) + end +end + +--- @param name string +--- @param response slick.worldResponseFunc +function world:addResponse(name, response) + assert(not self.responses[name]) + + self.responses[name] = response +end + +--- @param name string +function world:removeResponse(name) + assert(self.responses[name]) + + self.responses[name] = nil +end + +--- @param name string +--- @return slick.worldResponseFunc +function world:getResponse(name) + if not self.responses[name] then + error(string.format("Unknown collision type: %s", name)) + end + + return self.responses[name] +end + +--- @param name string +--- @return boolean +function world:hasResponse(name) + return self.responses[name] ~= nil +end + +return world diff --git a/game/love_src/lib/slick/worldQuery.lua b/game/love_src/lib/slick/worldQuery.lua new file mode 100644 index 0000000..95cf339 --- /dev/null +++ b/game/love_src/lib/slick/worldQuery.lua @@ -0,0 +1,482 @@ +local worldQueryResponse = require("slick.worldQueryResponse") +local box = require("slick.collision.box") +local commonShape = require("slick.collision.commonShape") +local quadTreeQuery = require("slick.collision.quadTreeQuery") +local ray = require("slick.geometry.ray") +local shapeCollisionResolutionQuery = require("slick.collision.shapeCollisionResolutionQuery") +local point = require("slick.geometry.point") +local rectangle = require("slick.geometry.rectangle") +local segment = require("slick.geometry.segment") +local transform = require("slick.geometry.transform") +local util = require("slick.util") +local pool = require ("slick.util.pool") +local slickmath = require("slick.util.slickmath") +local slicktable = require("slick.util.slicktable") + +--- @class slick.worldQuery +--- @field world slick.world +--- @field quadTreeQuery slick.collision.quadTreeQuery +--- @field results slick.worldQueryResponse[] +--- @field private cachedResults slick.worldQueryResponse[] +--- @field private collisionQuery slick.collision.shapeCollisionResolutionQuery +--- @field pools table **internal** +local worldQuery = {} +local metatable = { __index = worldQuery } + +--- @param world slick.world +--- @return slick.worldQuery +function worldQuery.new(world) + return setmetatable({ + world = world, + quadTreeQuery = quadTreeQuery.new(world.quadTree), + results = {}, + cachedResults = {}, + collisionQuery = shapeCollisionResolutionQuery.new(world.options.epsilon), + pools = {} + }, metatable) +end + +--- @param type any +--- @param ... unknown +--- @return any +function worldQuery:allocate(type, ...) + local p = self:getPool(type) + return p:allocate(...) +end + +--- @param type any +--- @return slick.util.pool +function worldQuery:getPool(type) + local p = self.pools[type] + if not p then + p = pool.new(type) + self.pools[type] = p + end + + return p +end + +local _cachedQueryTransform = transform.new() +local _cachedQueryBoxShape = box.new(nil, 0, 0, 1, 1) +local _cachedQueryVelocity = point.new() +local _cachedQueryOffset = point.new() + +--- @private +--- @param shape slick.collision.shapeInterface +--- @param filter slick.worldShapeFilterQueryFunc +function worldQuery:_performShapeQuery(shape, filter) + for _, otherShape in ipairs(self.quadTreeQuery.results) do + --- @cast otherShape slick.collision.shapeInterface + local response = filter(otherShape.entity.item, otherShape) + + if response then + self.collisionQuery:performProjection(shape, otherShape, _cachedQueryOffset, _cachedQueryOffset, _cachedQueryVelocity, _cachedQueryVelocity) + if self.collisionQuery.collision then + self:_addCollision(otherShape, nil, response, shape.center, true) + end + end + end +end + +--- @private +--- @param p slick.geometry.point +--- @param filter slick.worldShapeFilterQueryFunc +function worldQuery:_performPrimitivePointQuery(p, filter) + self.collisionQuery:reset() + + for _, otherShape in ipairs(self.quadTreeQuery.results) do + --- @cast otherShape slick.collision.shapeInterface + local response = filter(otherShape.entity.item, otherShape) + if response then + local inside = otherShape:inside(p) + if inside then + self:_addCollision(otherShape, nil, response, _cachedQueryOffset, true) + end + end + end +end + +local _cachedRayQueryTouch = point.new() +local _cachedRayNormal = point.new() + +--- @private +--- @param r slick.geometry.ray +--- @param filter slick.worldShapeFilterQueryFunc +function worldQuery:_performPrimitiveRayQuery(r, filter) + self.collisionQuery:reset() + + for _, otherShape in ipairs(self.quadTreeQuery.results) do + --- @cast otherShape slick.collision.shapeInterface + local response = filter(otherShape.entity.item, otherShape) + if response then + local inside, x, y = otherShape:raycast(r, _cachedRayNormal) + if inside and x and y then + _cachedRayQueryTouch:init(x, y) + + local result = self:_addCollision(otherShape, nil, response, _cachedRayQueryTouch, true) + result.contactPoint:init(x, y) + result.distance = _cachedRayQueryTouch:distance(r.origin) + + if otherShape.vertexCount == 2 then + self:_correctLineSegmentNormals(r.origin, otherShape.vertices[1], otherShape.vertices[2], result.normal) + else + result.normal:init(_cachedRayNormal.x, _cachedRayNormal.y) + end + end + end + end +end + +--- @private +--- @param r slick.geometry.rectangle +--- @param filter slick.worldShapeFilterQueryFunc +function worldQuery:_performPrimitiveRectangleQuery(r, filter) + _cachedQueryTransform:setTransform(r:left(), r:top(), 0, r:width(), r:height()) + _cachedQueryBoxShape:transform(_cachedQueryTransform) + + self:_performShapeQuery(_cachedQueryBoxShape, filter) +end + +local _lineSegmentRelativePosition = point.new() + +--- @private +--- @param point slick.geometry.point +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param normal slick.geometry.point +function worldQuery:_correctLineSegmentNormals(point, a, b, normal) + a:direction(b, normal) + normal:normalize(normal) + + local side = slickmath.direction(a, b, point, self.world.options.epsilon) + if side == 0 then + point:sub(a, _lineSegmentRelativePosition) + local dotA = normal:dot(_lineSegmentRelativePosition) + + point:sub(b, _lineSegmentRelativePosition) + local dotB = normal:dot(_lineSegmentRelativePosition) + + if dotA < 0 and dotB < 0 then + normal:negate(normal) + end + else + normal:left(normal) + normal:multiplyScalar(side, normal) + end +end + +--- @private +--- @param segment slick.geometry.segment +--- @param result slick.worldQueryResponse +--- @param x number +--- @param y number +function worldQuery:_addLineSegmentContactPoint(segment, result, x, y) + for _, c in ipairs(result.contactPoints) do + if c.x == x and c.y == y then + return + end + end + + local contactPoint = self:allocate(point, x, y) + table.insert(result.contactPoints, contactPoint) + + local distance = contactPoint:distance(segment.a) + if distance < result.distance then + result.distance = distance + result.contactPoint:init(x, y) + result.touch:init(x, y) + end +end + +local _cachedLineSegmentIntersectionPoint = point.new() + +--- @private +--- @param otherShape slick.collision.commonShape +--- @param segment slick.geometry.segment +--- @param a slick.geometry.point +--- @param b slick.geometry.point +--- @param response boolean +--- @param result slick.worldQueryResponse +function worldQuery:_lineSegmentLineSegmentIntersection(otherShape, segment, a, b, response, result) + local intersection, x, y, u, v = slickmath.intersection(segment.a, segment.b, a, b, self.world.options.epsilon) + if not intersection or not (u >= 0 and u <= 1 and v >= 0 and v <= 1) then + return false, result + end + + if not result then + _cachedLineSegmentIntersectionPoint:init(x or 0, y or 0) + result = self:_addCollision(otherShape, nil, response, _cachedLineSegmentIntersectionPoint, true) + result.distance = math.huge + end + + if x and y then + self:_addLineSegmentContactPoint(segment, result, x, y) + else + intersection = slickmath.intersection(segment.a, segment.a, a, b, self.world.options.epsilon) + if intersection then + self:_addLineSegmentContactPoint(segment, result, segment.a.x, segment.a.y) + end + + intersection = slickmath.intersection(segment.b, segment.b, a, b, self.world.options.epsilon) + if intersection then + self:_addLineSegmentContactPoint(segment, result, segment.b.x, segment.b.y) + end + + intersection = slickmath.intersection(a, a, segment.a, segment.b, self.world.options.epsilon) + if intersection then + self:_addLineSegmentContactPoint(segment, result, a.x, a.y) + end + + intersection = slickmath.intersection(b, b, segment.a, segment.b, self.world.options.epsilon) + if intersection then + self:_addLineSegmentContactPoint(segment, result, b.x, b.y) + end + end + + return true, result +end + +--- @private +--- @param segment slick.geometry.segment +--- @param filter slick.worldShapeFilterQueryFunc +function worldQuery:_performPrimitiveSegmentQuery(segment, filter) + self.collisionQuery:reset() + + for _, otherShape in ipairs(self.quadTreeQuery.results) do + --- @cast otherShape slick.collision.shapeInterface + local response = filter(otherShape.entity.item, otherShape) + if response then + --- @type slick.worldQueryResponse + local result + local intersection = false + if otherShape.vertexCount == 2 then + local a = otherShape.vertices[1] + local b = otherShape.vertices[2] + + intersection, result = self:_lineSegmentLineSegmentIntersection(otherShape, segment, a, b, response, result) + if intersection then + self:_correctLineSegmentNormals(segment.a, a, b, result.normal) + end + else + for i = 1, otherShape.vertexCount do + local a = otherShape.vertices[i] + local b = otherShape.vertices[slickmath.wrap(i, 1, otherShape.vertexCount)] + + local distance = result and result.distance or math.huge + intersection, result = self:_lineSegmentLineSegmentIntersection(otherShape, segment, a, b, response, result) + if intersection then + if result.distance < distance then + a:direction(b, result.normal) + result.normal:normalize(result.normal) + result.normal:left(result.normal) + end + end + end + end + end + end +end + +--- @param shape slick.geometry.point | slick.geometry.rectangle | slick.geometry.segment | slick.geometry.ray | slick.collision.commonShape +--- @param filter slick.worldShapeFilterQueryFunc +function worldQuery:performPrimitive(shape, filter) + if util.is(shape, commonShape) then + --- @cast shape slick.collision.commonShape + self:_beginPrimitiveQuery(shape.bounds) + else + --- @cast shape slick.geometry.point | slick.geometry.rectangle | slick.geometry.segment | slick.geometry.ray + self:_beginPrimitiveQuery(shape) + end + + if util.is(shape, rectangle) then + --- @cast shape slick.geometry.rectangle + self:_performPrimitiveRectangleQuery(shape, filter) + elseif util.is(shape, point) then + --- @cast shape slick.geometry.point + self:_performPrimitivePointQuery(shape, filter) + elseif util.is(shape, segment) then + --- @cast shape slick.geometry.segment + self:_performPrimitiveSegmentQuery(shape, filter) + elseif util.is(shape, ray) then + --- @cast shape slick.geometry.ray + self:_performPrimitiveRayQuery(shape, filter) + elseif util.is(shape, commonShape) then + --- @cast shape slick.collision.commonShape + self:_performShapeQuery(shape, filter) + end + + self:_endQuery() +end + +local _cachedSelfVelocity = point.new() +local _cachedSelfOffset = point.new() +local _cachedOtherVelocity = point.new() +local _cachedEntityBounds = rectangle.new() +local _cachedShapeBounds = rectangle.new() +local _cachedSelfPosition = point.new() +local _cachedSelfProjectedPosition = point.new() +local _cachedSelfOffsetPosition = point.new() +local _cachedOtherOffset = point.new() + +--- @param entity slick.entity +--- @param goalX number +--- @param goalY number +--- @param filter slick.worldFilterQueryFunc +function worldQuery:performProjection(entity, x, y, goalX, goalY, filter) + self:_beginQuery(entity, x, y, goalX, goalY) + + _cachedSelfPosition:init(entity.transform.x, entity.transform.y) + _cachedSelfProjectedPosition:init(x, y) + _cachedSelfPosition:direction(_cachedSelfProjectedPosition, _cachedSelfOffset) + + local offsetX = -entity.transform.x + x + local offsetY = -entity.transform.y + y + + _cachedSelfOffsetPosition:init(x, y) + + _cachedSelfVelocity:init(goalX, goalY) + _cachedSelfOffsetPosition:direction(_cachedSelfVelocity, _cachedSelfVelocity) + + _cachedEntityBounds:init(entity.bounds:left(), entity.bounds:top(), entity.bounds:right(), entity.bounds:bottom()) + _cachedEntityBounds:move(offsetX, offsetY) + _cachedEntityBounds:sweep(goalX, goalY) + + for _, otherShape in ipairs(self.quadTreeQuery.results) do + --- @cast otherShape slick.collision.shapeInterface + if otherShape.entity ~= entity and _cachedEntityBounds:overlaps(otherShape.bounds) then + for _, shape in ipairs(entity.shapes.shapes) do + _cachedShapeBounds:init(shape.bounds:left(), shape.bounds:top(), shape.bounds:right(), shape.bounds:bottom()) + _cachedShapeBounds:move(offsetX, offsetY) + _cachedShapeBounds:sweep(goalX + shape.bounds:left() - entity.transform.x, goalY + shape.bounds:top() - entity.transform.y) + + if _cachedShapeBounds:overlaps(otherShape.bounds) then + local response = filter(entity.item, otherShape.entity.item, shape, otherShape) + if response then + self.collisionQuery:performProjection(shape, otherShape, _cachedSelfOffset, _cachedOtherOffset, _cachedSelfVelocity, _cachedOtherVelocity) + + if self.collisionQuery.collision then + self:_addCollision(shape, otherShape, response, _cachedSelfProjectedPosition, false) + end + end + end + end + end + end + + self:_endQuery() +end + +function worldQuery:sort() + table.sort(self.results, worldQueryResponse.less) +end + +function worldQuery:reset() + slicktable.clear(self.results) + + for _, pool in pairs(self.pools) do + pool:reset() + end +end + +local _cachedBounds = rectangle.new() + +--- @private +--- @param entity slick.entity +--- @param x number +--- @param y number +--- @param goalX number +--- @param goalY number +function worldQuery:_beginQuery(entity, x, y, goalX, goalY) + self:reset() + + _cachedBounds:init(entity.bounds:left(), entity.bounds:top(), entity.bounds:right(), entity.bounds:bottom()) + _cachedBounds:move(-entity.transform.x + x, -entity.transform.y + y) + _cachedBounds:sweep(goalX, goalY) + + self.quadTreeQuery:perform(_cachedBounds) +end + +--- @private +--- @param shape slick.geometry.point | slick.geometry.rectangle | slick.geometry.segment | slick.geometry.ray +function worldQuery:_beginPrimitiveQuery(shape) + self:reset() + self.quadTreeQuery:perform(shape) +end + +--- @private +function worldQuery:_endQuery() + self:sort() +end + +--- @private +--- @param shape slick.collision.shapeInterface +--- @param otherShape slick.collision.shapeInterface? +--- @param response string | slick.worldVisitFunc | boolean +--- @param primitive boolean +--- @return slick.worldQueryResponse +function worldQuery:_addCollision(shape, otherShape, response, offset, primitive) + local index = #self.results + 1 + local result = self.cachedResults[index] + if not result then + result = worldQueryResponse.new(self) + table.insert(self.cachedResults, result) + end + + result:init(shape, otherShape, response, offset, self.collisionQuery) + table.insert(self.results, result) + + return self.results[#self.results] +end + +--- @param response slick.worldQueryResponse +--- @return number +function worldQuery:getResponseIndex(response) + for i, otherResponse in ipairs(self.results) do + if otherResponse == response then + return i + end + end + + return #self.results + 1 +end + +--- @param shape slick.collision.shape +--- @param otherShape slick.collision.shape +--- @return integer? +function worldQuery:getShapeResponseIndex(shape, otherShape) + for i, response in ipairs(self.results) do + if response.shape == shape and response.otherShape == otherShape then + return i + end + end + + return nil +end + +--- @param response slick.worldQueryResponse +--- @param copy boolean? +function worldQuery:push(response, copy) + if copy == nil then + copy = true + end + + local index = #self.results + 1 + local result = self.cachedResults[index] + if not result then + result = worldQueryResponse.new(self) + table.insert(self.cachedResults, result) + end + + response:move(result, copy) + table.insert(self.results, result) +end + +--- @param other slick.worldQuery +--- @param copy boolean? +function worldQuery:move(other, copy) + for _, response in ipairs(self.results) do + other:push(response, copy) + end +end + +return worldQuery diff --git a/game/love_src/lib/slick/worldQueryResponse.lua b/game/love_src/lib/slick/worldQueryResponse.lua new file mode 100644 index 0000000..ff6e0bb --- /dev/null +++ b/game/love_src/lib/slick/worldQueryResponse.lua @@ -0,0 +1,214 @@ +local point = require("slick.geometry.point") +local util = require("slick.util") +local pool = require("slick.util.pool") +local slicktable = require("slick.util.slicktable") + +--- @class slick.worldQueryResponse +--- @field query slick.worldQuery +--- @field response string | slick.worldVisitFunc | true +--- @field item any +--- @field entity slick.entity | slick.cache +--- @field shape slick.collision.shape +--- @field other any? +--- @field otherEntity slick.entity | slick.cache | nil +--- @field otherShape slick.collision.shape? +--- @field normal slick.geometry.point +--- @field alternateNormal slick.geometry.point +--- @field normals slick.geometry.point[] +--- @field alternateNormals slick.geometry.point[] +--- @field depth number +--- @field alternateDepth number +--- @field time number +--- @field offset slick.geometry.point +--- @field touch slick.geometry.point +--- @field isProjection boolean +--- @field contactPoint slick.geometry.point +--- @field contactPoints slick.geometry.point[] +--- @field distance number +--- @field extra table +local worldQueryResponse = {} +local metatable = { __index = worldQueryResponse } + +--- @return slick.worldQueryResponse +function worldQueryResponse.new(query) + return setmetatable({ + query = query, + response = "slide", + normal = point.new(), + alternateNormal = point.new(), + normals = {}, + alternateNormals = {}, + depth = 0, + alternateDepth = 0, + time = 0, + offset = point.new(), + touch = point.new(), + isProjection = false, + contactPoint = point.new(), + contactPoints = {}, + extra = {} + }, metatable) +end + +--- @param a slick.worldQueryResponse +--- @param b slick.worldQueryResponse +function worldQueryResponse.less(a, b) + if a.time == b.time then + if a.depth == b.depth then + return a.distance < b.distance + else + return a.depth > b.depth + end + end + + return a.time < b.time +end + +local _cachedInitItemPosition = point.new() + +--- @param shape slick.collision.shapeInterface +--- @param otherShape slick.collision.shapeInterface? +--- @param response string | slick.worldVisitFunc | true +--- @param position slick.geometry.point +--- @param query slick.collision.shapeCollisionResolutionQuery +function worldQueryResponse:init(shape, otherShape, response, position, query) + self.response = response + + self.shape = shape + self.entity = shape.entity + self.item = shape.entity.item + + self.otherShape = otherShape + self.otherEntity = self.otherShape and self.otherShape.entity + self.other = self.otherEntity and self.otherEntity.item + + self.normal:init(query.normal.x, query.normal.y) + self.alternateNormal:init(query.currentNormal.x, query.currentNormal.y) + self.alternateDepth = query.currentDepth + self.depth = query.depth + self.time = query.time + + self.offset:init(query.currentOffset.x, query.currentOffset.y) + position:add(self.offset, self.touch) + + local closestContactPointDistance = math.huge + + --- @type slick.geometry.point + local closestContactPoint + + _cachedInitItemPosition:init(self.entity.transform.x, self.entity.transform.y) + + slicktable.clear(self.contactPoints) + for i = 1, query.contactPointsCount do + local inputContactPoint = query.contactPoints[i] + local outputContactPoint = self.query:allocate(point, inputContactPoint.x, inputContactPoint.y) + table.insert(self.contactPoints, outputContactPoint) + + local distanceSquared = outputContactPoint:distance(_cachedInitItemPosition) + if distanceSquared < closestContactPointDistance then + closestContactPointDistance = distanceSquared + closestContactPoint = outputContactPoint + end + end + + slicktable.clear(self.normals) + for _, inputNormal in ipairs(query.normals) do + local outputNormal = self.query:allocate(point, inputNormal.x, inputNormal.y) + table.insert(self.normals, outputNormal) + end + + slicktable.clear(self.alternateNormals) + for _, inputNormal in ipairs(query.alternateNormals) do + local outputNormal = self.query:allocate(point, inputNormal.x, inputNormal.y) + table.insert(self.alternateNormals, outputNormal) + end + + if closestContactPoint then + self.contactPoint:init(closestContactPoint.x, closestContactPoint.y) + else + self.contactPoint:init(0, 0) + end + + self.distance = self.shape:distance(self.touch) + + slicktable.clear(self.extra) +end + +function worldQueryResponse:isTouchingWillNotPenetrate() + return self.time == 0 and self.depth == 0 +end + +function worldQueryResponse:isTouchingWillPenetrate() + return self.time == 0 and (self.isProjection and self.depth >= 0 or self.depth > 0) +end + +function worldQueryResponse:notTouchingWillTouch() + return self.time > 0 +end + +--- @param other slick.worldQueryResponse +--- @param copy boolean? +function worldQueryResponse:move(other, copy) + other.response = self.response + + other.shape = self.shape + other.entity = self.entity + other.item = self.item + + other.otherShape = self.otherShape + other.otherEntity = self.otherEntity + other.other = self.other + + other.normal:init(self.normal.x, self.normal.y) + other.alternateNormal:init(self.alternateNormal.x, self.alternateNormal.y) + other.depth = self.depth + other.alternateDepth = self.alternateDepth + other.time = self.time + other.offset:init(self.offset.x, self.offset.y) + other.touch:init(self.touch.x, self.touch.y) + other.isProjection = self.isProjection + + other.contactPoint:init(self.contactPoint.x, self.contactPoint.y) + other.distance = self.distance + + slicktable.clear(other.contactPoints) + for i, inputContactPoint in ipairs(self.contactPoints) do + local outputContactPoint = other.query:allocate(point, inputContactPoint.x, inputContactPoint.y) + table.insert(other.contactPoints, outputContactPoint) + end + + slicktable.clear(other.normals) + for i, inputNormal in ipairs(self.normals) do + local outputNormal = other.query:allocate(point, inputNormal.x, inputNormal.y) + table.insert(other.normals, outputNormal) + end + + slicktable.clear(other.alternateNormals) + for i, inputNormal in ipairs(self.alternateNormals) do + local outputNormal = other.query:allocate(point, inputNormal.x, inputNormal.y) + table.insert(other.alternateNormals, outputNormal) + end + + if not copy then + slicktable.clear(self.contactPoints) + slicktable.clear(self.normals) + + other.extra, self.extra = self.extra, other.extra + slicktable.clear(self.extra) + + for key, value in pairs(other.extra) do + local keyType = util.type(key) + local valueType = util.type(value) + + if keyType then + pool.swap(self.query:getPool(keyType), other.query:getPool(keyType), key) + end + + if valueType then + pool.swap(self.query:getPool(valueType), other.query:getPool(valueType), value) + end + end + end +end + +return worldQueryResponse diff --git a/game/love_src/src/entities/racing/racer.lua b/game/love_src/src/entities/racing/racer.lua index ef33feb..ec25927 100644 --- a/game/love_src/src/entities/racing/racer.lua +++ b/game/love_src/src/entities/racing/racer.lua @@ -1,3 +1,5 @@ +local racing = require("love_src.src.system.racing_phy") + local entity = {} entity.__index = entity @@ -7,10 +9,13 @@ function entity.load(actor, finish) self.data = { pos = {0, 100, 0}, color = {255/255, 255/255, 255/255}, + actor = actor, - current_speed = 0.0, - current_accel = 0.0, + race = { + speed = 0.0, + accel = 10.0, + }, finish = finish } @@ -20,28 +25,29 @@ end function entity:update(dt) if (self.data.pos[1] > self.data.finish[1]) then self.data.pos[1] = 0 - self.data.current_speed = 0 - self.data.current_accel = 0 + self.data.race.speed = 0 + self.data.race.accel = 10.0 end - self:accel(dt) - self.data.pos[1] = self.data.pos[1] + self.data.current_speed * dt + self.data.race.speed = racing.accelerate(dt, 1, self.data.race.speed, self.data.actor.data.max_speed, self.data.race.accel) + -- self:accel(dt) + self.data.pos[1] = self.data.pos[1] + self.data.race.speed * dt end -function entity:accel(dt) - if (self.data.current_accel <= self.data.actor.data.accel) then - self.data.current_accel = self.data.current_accel + dt - end - if (self.data.current_speed <= self.data.actor.data.max_speed) then - self.data.current_speed = self.data.current_speed + self.data.current_accel * dt - end -end +-- function entity:accel(dt) +-- if (self.data.current_accel <= self.data.actor.data.accel) then +-- self.data.current_accel = self.data.current_accel + dt +-- end +-- if (self.data.current_speed <= self.data.actor.data.max_speed) then +-- self.data.current_speed = self.data.current_speed + self.data.current_accel * dt +-- end +-- end function entity:draw() love.graphics.push() love.graphics.setColor(self.data.color[1], self.data.color[2], self.data.color[3]) love.graphics.points(self.data.pos[1], self.data.pos[2]) - love.graphics.print(self.data.current_speed, self.data.pos[1], self.data.pos[2]) - love.graphics.print(string.format("Current Accel : %s", self.data.current_accel), 0, 40) + love.graphics.print(self.data.race.speed, self.data.pos[1], self.data.pos[2]) + -- love.graphics.print(string.format("Current Accel : %s", self.data.current_accel), 0, 40) love.graphics.pop() self.data.actor:draw() end diff --git a/game/love_src/src/entities/shared/actor.lua b/game/love_src/src/entities/shared/actor.lua index fc4d4de..075c1d3 100644 --- a/game/love_src/src/entities/shared/actor.lua +++ b/game/love_src/src/entities/shared/actor.lua @@ -2,12 +2,20 @@ local entity = {} entity.__index = entity -function entity.load(name) +_data = { + name = "p1", + + max_speed = 100.0, + accel = 2.0, + grip = 2.0, + brake = 2.0, + + ui = {0, 0} +} + +function entity.load(data) local self = setmetatable({}, entity) - self.data = { - max_speed = 100.0, - accel = 2.0, - } + self.data = data or _data return self end @@ -16,8 +24,11 @@ end function entity:draw() love.graphics.push() - love.graphics.print(string.format("Max Speed : %s", self.data.max_speed), 0, 0) - love.graphics.print(string.format("Accel : %s", self.data.accel), 0, 20) + love.graphics.print(string.format("Name : %s", self.data.name, self.data.ui[1], self.data.ui[2])) + love.graphics.print(string.format("Max Speed : %s", self.data.max_speed), self.data.ui[1], self.data.ui[2] + 20) + love.graphics.print(string.format("Accel : %s", self.data.accel),self.data.ui[1], self.data.ui[2] + 40) + love.graphics.print(string.format("Grip : %s", self.data.grip), self.data.ui[1], self.data.ui[2] + 60) + love.graphics.print(string.format("Brake : %s", self.data.brake), self.data.ui[1], self.data.ui[2] + 80) love.graphics.pop() end diff --git a/game/love_src/src/modes/racing.lua b/game/love_src/src/modes/racing.lua index f0f4141..e02b05b 100644 --- a/game/love_src/src/modes/racing.lua +++ b/game/love_src/src/modes/racing.lua @@ -1,30 +1,80 @@ +local slick = require("love_src.lib.slick") + local racer = require("love_src.src.entities.racing.racer") +local player = require("love_src.src.entities.shared.actor").load() local mode = {} local finish = {100, 100, 0} -local entities = {} +local time = 0 -function mode.load(player) - entities.racer = racer.load(player, finish) + +local w, h = 800, 600 + +function mode:load() + self.entities = {} + self.level = {} + self.world = slick.newWorld(w, h, { + quadTreeX = 0, + quadTreeY = 0 + }) + + local new_racer = racer.load(player, finish) + + self.world:add(new_racer, 200, 500, slick.newCircleShape(0, 0, 16)) + + table.insert(self.entities, new_racer) + + self.world:add(self.level, 0, 0, slick.newShapeGroup( + -- Boxes surrounding the map + slick.newRectangleShape(0, 0, w, 8), -- top + slick.newRectangleShape(0, 0, 8, h), -- left + slick.newRectangleShape(w - 8, 0, 8, h), -- right + slick.newRectangleShape(0, h - 8, w, 8), -- bottom + -- Triangles in corners + slick.newPolygonShape({ 8, h - h / 8, w / 4, h - 8, 8, h - 8 }), + slick.newPolygonShape({ w - w / 4, h, w - 8, h / 2, w - 8, h }), + -- Convex shape + slick.newPolygonMeshShape({ w / 2 + w / 4, h / 4, w / 2 + w / 4 + w / 8, h / 4 + h / 8, w / 2 + w / 4, h / 4 + h / 4, w / 2 + w / 4 + w / 16, h / 4 + h / 8 }) + )) end -function mode.update(dt) - entities.racer:update(dt) +local function movePlayer(p, dt) + local goalX, goalY = p.x + dt * p.velocityX, p.y + dt * p.velocityY + p.x, p.y = world:move(p, goalX, goalY) end -function mode.draw() - entities.racer:draw() +function mode:update(dt) + for _,e in pairs(self.entities) do + e:update(dt) + + -- local goalX, goalY = w / 2, -1000 + + -- local actualX, actualY, collisions, count = self.world:move(e, goalX, goalY, function(item, other, shape, otherShape) + -- return "slide" + -- end) + -- print(actualX, actualY) + -- movePlayer(e, dt) + end + time = time + dt end -function mode.keyreleased(key, scancode) +function mode:draw() + for _,e in pairs(self.entities) do + e:draw() + end + slick.drawWorld(self.world) + love.graphics.print(time, love.graphics.getWidth() / 2, love.graphics.getHeight()/ 2) end -function mode.keypressed(key, scancode, isrepeat) +function mode:keyreleased(key, scancode) end -function mode.mousereleased(x, y, button, istouch, presses) +function mode:keypressed(key, scancode, isrepeat) +end + +function mode:mousereleased(x, y, button, istouch, presses) end return mode diff --git a/game/love_src/src/system/racing_phy.lua b/game/love_src/src/system/racing_phy.lua new file mode 100644 index 0000000..970f94f --- /dev/null +++ b/game/love_src/src/system/racing_phy.lua @@ -0,0 +1,25 @@ +local function accelerate(dt, gas_input, speed, max_speed, accel, min_speed) + if (min_speed == nil) then + min_speed = 0 + end + local new_speed = speed + accel * dt * gas_input + if (new_speed >= max_speed) then + new_speed = max_speed + elseif (new_speed <= min_speed) then + new_speed = min_speed + elseif(new_speed <= -max_speed) then + new_speed = -max_speed + end + return new_speed +end + +local function drift(dt, pos_x, speed, max_speed, curve, centrifugal) + local speed_percent = (speed/max_speed) + local dx = dt * 2 * speed_percent -- at top speed, should be able to cross from left to right (-1 to 1) in 1 second + return pos_x - (dx * speed_percent * curve * centrifugal); +end + +return { + accelerate = accelerate, + drift = drift +} diff --git a/game/love_src/src/world/top_down_race/component/race.lua b/game/love_src/src/world/top_down_race/component/race.lua new file mode 100644 index 0000000..afbd402 --- /dev/null +++ b/game/love_src/src/world/top_down_race/component/race.lua @@ -0,0 +1,51 @@ +local components = {} + +components.dict = { + velocity = "race.velocity", + max_speed = "race.max_speed", + accel = "race.accel", + grip = "race.grip", + steer = "race.steer", + brake = "race.brake", + decel = "race.decel", + drag = "race.drag", + direction = "race.direction" +} + +function components.velocity (c, x) + c.data = x +end + +function components.max_speed (c, x) + c.data = x +end + +function components.accel (c, x) + c.data = x +end + +function components.grip (c, x) + c.data = x +end + +function components.steer (c, x) + c.data = x +end + +function components.brake (c, x) + c.data = x +end + +function components.decel (c, x) + c.data = x +end + +function components.drag (c, x) + c.data = x +end + +function components.direction (c, x) + c.data = x +end + +return components diff --git a/game/love_src/src/world/top_down_race/init.lua b/game/love_src/src/world/top_down_race/init.lua new file mode 100644 index 0000000..aaac13b --- /dev/null +++ b/game/love_src/src/world/top_down_race/init.lua @@ -0,0 +1,47 @@ +local reap = require("lib.reap") + +local BASE = reap.base_path(...) + +local world = require("love_src.wrapper.Concord.world") + +local debug_entity = require("love_src.src.world.common.template.debug_entity") +local racer = require("love_src.src.world.top_down_race.template.racer") +local wm = require("world_map") + +local wrapper = world:extend() + +local name = "top_down_race" + +function wrapper:new() + wrapper.super.new(self, BASE, name) +end + +function wrapper:load(_args) + wrapper.super.load(self, { + "love_src/src/world/common/system/", + "love_src/src/world/top_down_race/system/" + }, { + { + assemblage = debug_entity.assembleDebug, + data = { + position = {0, 0}, + label = name + } + }, + { + assemblage = racer.assemble, + data = { + accel = 10.0, + brake = 10.0, + grip = 10.0, + max_speed = 100.0, + steer = 10.0, + velocity = 10.0, + decel = 2.0, + drag = 2.0 + } + } + }) +end + +return wrapper diff --git a/game/love_src/src/world/top_down_race/system/velocity.lua b/game/love_src/src/world/top_down_race/system/velocity.lua new file mode 100644 index 0000000..13fd1a0 --- /dev/null +++ b/game/love_src/src/world/top_down_race/system/velocity.lua @@ -0,0 +1,125 @@ +local system_constructor = require("love_src.wrapper.Concord.system") +local race = require("love_src.src.world.top_down_race.component.race") +local racing_phy = require("love_src.src.system.racing_phy") + +local component = require("love_src.wrapper.Concord.component") + +local vm = require("lib.vornmath") + +local system = {} + +system.__index = system + +system.pool = { + pool = { + race.dict.accel, + race.dict.brake, + race.dict.grip, + race.dict.max_speed, + race.dict.steer, + race.dict.velocity, + race.dict.decel, + race.dict.drag + } +} + +system.components = { + [race.dict.accel] = race.accel, + [race.dict.brake] = race.brake, + [race.dict.grip] = race.grip, + [race.dict.max_speed] = race.max_speed, + [race.dict.steer] = race.steer, + [race.dict.velocity] = race.velocity, + [race.dict.decel] = race.decel, + [race.dict.drag] = race.drag +} + +function system.new() + local new_system = system_constructor.new("velocity", system.pool) + if (new_system) then + for k, v in pairs(system) do + new_system[k] = v + end + return new_system + else + return nil + end +end + +local function get(e) + return + e[race.dict.accel].data, + e[race.dict.brake].data, + e[race.dict.grip].data, + e[race.dict.max_speed].data, + e[race.dict.steer].data, + e[race.dict.velocity].data, + e[race.dict.drag].data, + e[race.dict.decel].data +end + +local function draw(text, x, y, sx, sy, angle) + love.graphics.push() + love.graphics.scale(sx, sy) + love.graphics.rotate(angle) + love.graphics.print(text, x, y) + love.graphics.pop() +end + +function system:load() + component.component("race.pos", function (c, x, y) + c.data = vm.vec2(x, y) + end) + component.component("race.scale", function (c, x, y) + c.data = vm.vec2(x, y) + end) + component.component("race.angle", function (c, x) + c.data = x + end) + + for _, e in ipairs(self.pool) do + e:give("race.pos", 10, 10) + e:give("race.scale", 1, 1) + e:give("race.angle", 0) + end +end + +function system:update(dt) + for _, e in ipairs(self.pool) do + local accel, brake, grip, max_speed, steer, velocity, drag, decel = get(e) + local up = love.keyboard.isDown("up") + local down = love.keyboard.isDown("down") + local left = love.keyboard.isDown("left") + local right = love.keyboard.isDown("right") + + if (up) then + e[race.dict.velocity].data = racing_phy.accelerate(dt, 1, velocity, max_speed, accel - drag) + elseif(down) then + e[race.dict.velocity].data = racing_phy.accelerate(dt, -1, velocity, max_speed, accel - drag, -max_speed) + else + e[race.dict.velocity].data = racing_phy.accelerate(dt, 1, velocity, max_speed, -drag -decel) + end + + if (left) then + e["race.angle"].data = e["race.angle"].data - 1 * dt + end + if (right) then + e["race.angle"].data = e["race.angle"].data + 1 * dt + end + + velocity = e[race.dict.velocity].data + + e["race.pos"].data = vm.vec2(e["race.pos"].data[1] + velocity, e["race.pos"].data[2]) + -- print(racing_phy.drift(dt, 1, velocity, max_speed, 2, 10)) + end +end + +function system:draw() + for _, e in ipairs(self.pool) do + local accel, brake, grip, max_speed, steer, velocity = get(e) + local x, y, sx, sy, angle = e["race.pos"].data[1], e["race.pos"].data[2], e["race.scale"].data[1], e["race.scale"].data[2], e["race.angle"].data + draw(velocity, x, y, sx, sy, angle) + end +end + +return system diff --git a/game/love_src/src/world/top_down_race/template/racer.lua b/game/love_src/src/world/top_down_race/template/racer.lua new file mode 100644 index 0000000..c7fc621 --- /dev/null +++ b/game/love_src/src/world/top_down_race/template/racer.lua @@ -0,0 +1,26 @@ +local race = require("love_src.src.world.top_down_race.component.race") + +local template = {} + +template.default_data = { + accel = 10.0, + brake = 10.0, + grip = 10.0, + max_speed = 10.0, + steer = 10.0, + velocity = 10.0, + decel = 5.0, + drag = 2.0 +} +function template.assemble(e, data) + e:give(race.dict.accel, data.accel) + e:give(race.dict.brake, data.brake) + e:give(race.dict.grip, data.grip) + e:give(race.dict.max_speed, data.max_speed) + e:give(race.dict.steer, data.steer) + e:give(race.dict.velocity, data.velocity) + e:give(race.dict.decel, data.decel) + e:give(race.dict.drag, data.drag) +end + +return template diff --git a/game/main.lua b/game/main.lua index dca0e3e..fbfd4e5 100644 --- a/game/main.lua +++ b/game/main.lua @@ -1,226 +1,226 @@ -local ffi = require 'ffi' -local joysticks +-- local ffi = require 'ffi' +-- local joysticks -function init() - joysticks = love.joystick.getJoysticks() - for i, joystick in ipairs(joysticks) do - print(i, joystick:getName()) - end +-- function init() +-- joysticks = love.joystick.getJoysticks() +-- for i, joystick in ipairs(joysticks) do +-- print(i, joystick:getName()) +-- end - ffi.cdef[[ -void load(const char * source_path); -void update_window(int width, int height); -void draw(); -void update_keyboard(int up, int down, int left, int right, - int w, int s, int a, int d, - int t, int g, int f, int h, - int i, int k, int j, int l); -void update_mouse(int x, int y); -void update_joystick(int joystick_index, - float lx, float ly, float rx, float ry, float tl, float tr, - int up, int down, int left, int right, - int a, int b, int x, int y, - int leftshoulder, int rightshoulder, - int start); -void update(float time); +-- ffi.cdef[[ +-- void load(const char * source_path); +-- void update_window(int width, int height); +-- void draw(); +-- void update_keyboard(int up, int down, int left, int right, +-- int w, int s, int a, int d, +-- int t, int g, int f, int h, +-- int i, int k, int j, int l); +-- void update_mouse(int x, int y); +-- void update_joystick(int joystick_index, +-- float lx, float ly, float rx, float ry, float tl, float tr, +-- int up, int down, int left, int right, +-- int a, int b, int x, int y, +-- int leftshoulder, int rightshoulder, +-- int start); +-- void update(float time); - int draw_font_start(); - int draw_font(int font_ix, char const * text, int x, int y); +-- int draw_font_start(); +-- int draw_font(int font_ix, char const * text, int x, int y); - void draw_line_quad_start(); - void draw_line(int x1, int y1, int x2, int y2); - void draw_set_color(float r, float g, float b); - void draw_quad(int x1, int y1, int x2, int y2, - int x3, int y3, int x4, int y4); -]] - local source_path = love.filesystem.getSource() - test = ffi.load(source_path .. "/test.so") - test.load(source_path) -end +-- void draw_line_quad_start(); +-- void draw_line(int x1, int y1, int x2, int y2); +-- void draw_set_color(float r, float g, float b); +-- void draw_quad(int x1, int y1, int x2, int y2, +-- int x3, int y3, int x4, int y4); +-- ]] +-- local source_path = love.filesystem.getSource() +-- test = ffi.load(source_path .. "/test.so") +-- test.load(source_path) +-- end -local update = function(time) - for joystick_index, joystick in ipairs(joysticks) do - if joystick_index > 8 then - break - end - local lx = joystick:getGamepadAxis("leftx") - local ly = joystick:getGamepadAxis("lefty") - local rx = joystick:getGamepadAxis("rightx") - local ry = joystick:getGamepadAxis("righty") - local tl = joystick:getGamepadAxis("triggerleft") - local tr = joystick:getGamepadAxis("triggerright") - local up = joystick:isGamepadDown("dpup") - local down = joystick:isGamepadDown("dpdown") - local left = joystick:isGamepadDown("dpleft") - local right = joystick:isGamepadDown("dpright") - local a = joystick:isGamepadDown("a") - local b = joystick:isGamepadDown("b") - local x = joystick:isGamepadDown("x") - local y = joystick:isGamepadDown("y") - local leftshoulder = joystick:isGamepadDown("leftshoulder") - local rightshoulder = joystick:isGamepadDown("rightshoulder") - local start = joystick:isGamepadDown("start") - --print("start", i, start) - test.update_joystick(joystick_index - 1, - lx, ly, rx, ry, tl, tr, - up, down, left, right, - a, b, x, y, - leftshoulder, rightshoulder, - start) - end +-- local update = function(time) +-- for joystick_index, joystick in ipairs(joysticks) do +-- if joystick_index > 8 then +-- break +-- end +-- local lx = joystick:getGamepadAxis("leftx") +-- local ly = joystick:getGamepadAxis("lefty") +-- local rx = joystick:getGamepadAxis("rightx") +-- local ry = joystick:getGamepadAxis("righty") +-- local tl = joystick:getGamepadAxis("triggerleft") +-- local tr = joystick:getGamepadAxis("triggerright") +-- local up = joystick:isGamepadDown("dpup") +-- local down = joystick:isGamepadDown("dpdown") +-- local left = joystick:isGamepadDown("dpleft") +-- local right = joystick:isGamepadDown("dpright") +-- local a = joystick:isGamepadDown("a") +-- local b = joystick:isGamepadDown("b") +-- local x = joystick:isGamepadDown("x") +-- local y = joystick:isGamepadDown("y") +-- local leftshoulder = joystick:isGamepadDown("leftshoulder") +-- local rightshoulder = joystick:isGamepadDown("rightshoulder") +-- local start = joystick:isGamepadDown("start") +-- --print("start", i, start) +-- test.update_joystick(joystick_index - 1, +-- lx, ly, rx, ry, tl, tr, +-- up, down, left, right, +-- a, b, x, y, +-- leftshoulder, rightshoulder, +-- start) +-- end - local up = love.keyboard.isDown("up") - local down = love.keyboard.isDown("down") - local left = love.keyboard.isDown("left") - local right = love.keyboard.isDown("right") - local w = love.keyboard.isDown("w") - local s = love.keyboard.isDown("s") - local a = love.keyboard.isDown("a") - local d = love.keyboard.isDown("d") - local t = love.keyboard.isDown("t") - local g = love.keyboard.isDown("g") - local f = love.keyboard.isDown("f") - local h = love.keyboard.isDown("h") - local i = love.keyboard.isDown("i") - local k = love.keyboard.isDown("k") - local j = love.keyboard.isDown("j") - local l = love.keyboard.isDown("l") - test.update_keyboard(up, down, left, right, - w, s, a, d, - t, g, f, h, - i, k, j, l); +-- local up = love.keyboard.isDown("up") +-- local down = love.keyboard.isDown("down") +-- local left = love.keyboard.isDown("left") +-- local right = love.keyboard.isDown("right") +-- local w = love.keyboard.isDown("w") +-- local s = love.keyboard.isDown("s") +-- local a = love.keyboard.isDown("a") +-- local d = love.keyboard.isDown("d") +-- local t = love.keyboard.isDown("t") +-- local g = love.keyboard.isDown("g") +-- local f = love.keyboard.isDown("f") +-- local h = love.keyboard.isDown("h") +-- local i = love.keyboard.isDown("i") +-- local k = love.keyboard.isDown("k") +-- local j = love.keyboard.isDown("j") +-- local l = love.keyboard.isDown("l") +-- test.update_keyboard(up, down, left, right, +-- w, s, a, d, +-- t, g, f, h, +-- i, k, j, l); - test.update(time) -end +-- test.update(time) +-- end -local draw = function() - test.draw() -end +-- local draw = function() +-- test.draw() +-- end -local nico_draw = function() - ---------------------------------------------------------------------- - -- font drawing - ---------------------------------------------------------------------- +-- local nico_draw = function() +-- ---------------------------------------------------------------------- +-- -- font drawing +-- ---------------------------------------------------------------------- - -- call "draw_font_start()" prior each "group" of "draw_font()" calls - -- - -- a "group" of draw_font() calls are back-to-back/consecutive, - -- with no non-font drawing between them. - -- - -- For example: +-- -- call "draw_font_start()" prior each "group" of "draw_font()" calls +-- -- +-- -- a "group" of draw_font() calls are back-to-back/consecutive, +-- -- with no non-font drawing between them. +-- -- +-- -- For example: - local font_ix = test.draw_font_start() - local x = 512 - local y = 50 - y = y + test.draw_font(font_ix, "lua test", x, y) - y = y + test.draw_font(font_ix, "cool", x, y) +-- local font_ix = test.draw_font_start() +-- local x = 512 +-- local y = 50 +-- y = y + test.draw_font(font_ix, "lua test", x, y) +-- y = y + test.draw_font(font_ix, "cool", x, y) - -- note that "font_ix" is the current "best font" as calculated - -- from the current window size, and might change next frame if the - -- window is resized. - -- - -- Any of this of course could be changed to match your precise - -- requirements. +-- -- note that "font_ix" is the current "best font" as calculated +-- -- from the current window size, and might change next frame if the +-- -- window is resized. +-- -- +-- -- Any of this of course could be changed to match your precise +-- -- requirements. - ---------------------------------------------------------------------- - -- line drawing - ---------------------------------------------------------------------- +-- ---------------------------------------------------------------------- +-- -- line drawing +-- ---------------------------------------------------------------------- - -- call "draw_line_quad_start()" prior to each "group" of - -- "draw_line()" or "draw_quad()" calls - -- - -- a "group" of draw_line()/draw_quad() calls are - -- back-to-back/consecutive, with no non-line/quad drawing between - -- them. - -- - -- For example: +-- -- call "draw_line_quad_start()" prior to each "group" of +-- -- "draw_line()" or "draw_quad()" calls +-- -- +-- -- a "group" of draw_line()/draw_quad() calls are +-- -- back-to-back/consecutive, with no non-line/quad drawing between +-- -- them. +-- -- +-- -- For example: - test.draw_line_quad_start() - test.draw_set_color(1.0, 0.0, 0.0) -- r, g, b (0.0 to 1.0) - test.draw_line(0, 0, 1024, 1024) -- x1, y1, x2, y2 - test.draw_line(700, 300, 400, 500) - test.draw_set_color(0.0, 1.0, 0.0) - test.draw_line(700, 300, 400, 700) +-- test.draw_line_quad_start() +-- test.draw_set_color(1.0, 0.0, 0.0) -- r, g, b (0.0 to 1.0) +-- test.draw_line(0, 0, 1024, 1024) -- x1, y1, x2, y2 +-- test.draw_line(700, 300, 400, 500) +-- test.draw_set_color(0.0, 1.0, 0.0) +-- test.draw_line(700, 300, 400, 700) - -- x1, y1, x2, y2, - -- x3, y3, x4, y4, - -- - -- vertices must be specified In "counter clockwise" order, as in: - -- - -- 2──1 - -- │ │ valid (counter clockwise) - -- 3──4 - -- - -- these can also be rotated, as in: - -- - -- 3 - -- ╱ ╲ - -- 4 2 valid (counter clockwise) - -- ╲ ╱ - -- 1 - -- - -- however "mirroring" is not valid, as in: - -- - -- 1──2 - -- │ │ not valid (clockwise) - -- 4──3 - -- - test.draw_set_color(0.0, 0.0, 1.0) - test.draw_quad( - 600, 600, -- top right - 500, 600, -- top left - 500, 700, -- bottom left - 600, 700 -- bottom right - ) - test.draw_set_color(0.0, 0.5, 1.0) - test.draw_quad( - 900, 900, -- bottom - 950, 850, -- right - 900, 800, -- top - 850, 850 -- left - ) +-- -- x1, y1, x2, y2, +-- -- x3, y3, x4, y4, +-- -- +-- -- vertices must be specified In "counter clockwise" order, as in: +-- -- +-- -- 2──1 +-- -- │ │ valid (counter clockwise) +-- -- 3──4 +-- -- +-- -- these can also be rotated, as in: +-- -- +-- -- 3 +-- -- ╱ ╲ +-- -- 4 2 valid (counter clockwise) +-- -- ╲ ╱ +-- -- 1 +-- -- +-- -- however "mirroring" is not valid, as in: +-- -- +-- -- 1──2 +-- -- │ │ not valid (clockwise) +-- -- 4──3 +-- -- +-- test.draw_set_color(0.0, 0.0, 1.0) +-- test.draw_quad( +-- 600, 600, -- top right +-- 500, 600, -- top left +-- 500, 700, -- bottom left +-- 600, 700 -- bottom right +-- ) +-- test.draw_set_color(0.0, 0.5, 1.0) +-- test.draw_quad( +-- 900, 900, -- bottom +-- 950, 850, -- right +-- 900, 800, -- top +-- 850, 850 -- left +-- ) - -- If you want to draw a large number of lines or quads in bulk - -- (e.g: 10,000+ lines/quads per frame), this interface might not be good - -- enough, and we should discuss this in more detail. -end +-- -- If you want to draw a large number of lines or quads in bulk +-- -- (e.g: 10,000+ lines/quads per frame), this interface might not be good +-- -- enough, and we should discuss this in more detail. +-- end -function love.run() - init() +-- function love.run() +-- init() - return function() - love.event.pump() - for name, a,b,c,d,e,f,g,h in love.event.poll() do - if name == "quit" then - if c or not love.quit or not love.quit() then - return a or 0, b - end - end - end +-- return function() +-- love.event.pump() +-- for name, a,b,c,d,e,f,g,h in love.event.poll() do +-- if name == "quit" then +-- if c or not love.quit or not love.quit() then +-- return a or 0, b +-- end +-- end +-- end - local width - local height - local flags - width, height, flags = love.window.getMode() - test.update_window(width, height) +-- local width +-- local height +-- local flags +-- width, height, flags = love.window.getMode() +-- test.update_window(width, height) - local time = love.timer.getTime() - update(time) +-- local time = love.timer.getTime() +-- update(time) - draw() +-- draw() - local mouse_down = love.mouse.isDown(1) - if mouse_down then - local x, y = love.mouse.getPosition() - test.update_mouse(x, y) - end +-- local mouse_down = love.mouse.isDown(1) +-- if mouse_down then +-- local x, y = love.mouse.getPosition() +-- test.update_mouse(x, y) +-- end - -- nico_draw() +-- -- nico_draw() - love.graphics.present() - love.timer.sleep(0.001) - end -end +-- love.graphics.present() +-- love.timer.sleep(0.001) +-- end +-- end -- function love.load(args) -- init() @@ -248,43 +248,44 @@ end -- end --- local wm = require("world_map") +local wm = require("world_map") --- world = { --- ["main_menu"] = require("love_src.src.world.main_menu")(), --- ["1_intro"] = require("love_src.src.world.1_intro")(), --- ["2_town_square"] = require("love_src.src.world.2_town_square")(), --- ["race"] = require("love_src.src.world.race")(), --- ["train"] = require("love_src.src.world.train")(), --- }; +world = { + ["top_down_race"] = require("love_src.src.world.top_down_race")(), + -- ["main_menu"] = require("love_src.src.world.main_menu")(), + -- ["1_intro"] = require("love_src.src.world.1_intro")(), + -- ["2_town_square"] = require("love_src.src.world.2_town_square")(), + -- ["race"] = require("love_src.src.world.race")(), + -- ["train"] = require("love_src.src.world.train")(), +}; --- current = wm["2_town_square"] +current = wm["top_down_race"] --- function load_world(world_to_load) --- current = world_to_load --- world[current]:reload() --- end +function load_world(world_to_load) + current = world_to_load + world[current]:reload() +end --- function love.load() --- world[current]:load() --- end +function love.load() + world[current]:load() +end --- function love.update(dt) --- world[current]:update(dt) --- end +function love.update(dt) + world[current]:update(dt) +end --- function love.draw() --- world[current]:draw() --- end +function love.draw() + world[current]:draw() +end --- function love.keyreleased(key, scancode) --- world[current]:keyreleased(key, scancode) --- end +function love.keyreleased(key, scancode) + world[current]:keyreleased(key, scancode) +end --- function love.keypressed(key, scancode, isrepeat) --- world[current]:keypressed(key, scancode, isrepeat) --- end +function love.keypressed(key, scancode, isrepeat) + world[current]:keypressed(key, scancode, isrepeat) +end --- function love.mousereleased(x, y, button, istouch, presses) --- world[current]:mousereleased(x, y, button, istouch, presses) --- end +function love.mousereleased(x, y, button, istouch, presses) + world[current]:mousereleased(x, y, button, istouch, presses) +end diff --git a/game/world_map.lua b/game/world_map.lua index 65f5ccf..d603b18 100644 --- a/game/world_map.lua +++ b/game/world_map.lua @@ -1,4 +1,5 @@ return { + ["top_down_race"] = "top_down_race", ["main_menu"] = "main_menu", ["1_intro"] = "1_intro", ["2_town_square"] = "2_town_square",