add slick, add top down race

This commit is contained in:
fnicon 2026-03-20 16:21:44 +09:00
parent 1dc045cbed
commit 76e61ed233
67 changed files with 12153 additions and 262 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<any, slick.geometry.rectangle> **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

View File

@ -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<any, boolean>
--- @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

View File

@ -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<any, boolean>
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<slick.geometry.clipper.polygon, number[]>,
--- 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<number, number>,
--- combinedPointToPointIndex: table<number, number>,
--- 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<number, number>
--- @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<slick.geometry.clipper.polygon, number[]>
--- @param other table<slick.geometry.clipper.polygon, number[]>
--- @param ... table<slick.geometry.clipper.polygon, number[]>
--- @return table<slick.geometry.clipper.polygon, number[]>
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"),
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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.
]]
}

View File

@ -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

View File

@ -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"),
}

View File

@ -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<number, slick.navigation.edge[]>
--- @field triangles slick.navigation.triangle[]
--- @field inputPoints number[]
--- @field inputEdges number[]
--- @field inputExteriorEdges number[]
--- @field inputInteriorEdges number[]
--- @field inputUserdata any[]
--- @field vertexToTriangle table<number, slick.navigation.triangle[]>
--- @field triangleNeighbors table<number, slick.navigation.triangle[]>
--- @field sharedTriangleEdges table<number, table<number, slick.navigation.edge>>
--- @field edgeTriangles table<slick.navigation.edge, slick.navigation.triangle[]>
--- @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

View File

@ -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<any, slick.navigation.navMeshBuilder.layerSettings>
--- @field layerMeshes table<any, slick.navigation.mesh[]>
--- @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

View File

@ -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<slick.navigation.triangle, number>
--- @field private gScores table<slick.navigation.triangle, number>
--- @field private hScores table<slick.navigation.triangle, number>
--- @field private visitedEdges table<slick.navigation.edge, true>
--- @field private visitedTriangles table<slick.navigation.triangle, true>
--- @field private pending slick.navigation.triangle[]
--- @field private closed slick.navigation.triangle[]
--- @field private neighbors slick.navigation.triangle[]
--- @field private graph table<slick.navigation.triangle, slick.navigation.triangle>
--- @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

View File

@ -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<number, slick.navigation.vertex>
--- @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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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,
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<string, slick.worldResponseFunc>
--- @field private entities slick.entity[]
--- @field private itemToEntity table<any, number>
--- @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

View File

@ -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<any, slick.util.pool> **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

View File

@ -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

View File

@ -1,3 +1,5 @@
local racing = require("love_src.src.system.racing_phy")
local entity = {} local entity = {}
entity.__index = entity entity.__index = entity
@ -7,10 +9,13 @@ function entity.load(actor, finish)
self.data = { self.data = {
pos = {0, 100, 0}, pos = {0, 100, 0},
color = {255/255, 255/255, 255/255}, color = {255/255, 255/255, 255/255},
actor = actor, actor = actor,
current_speed = 0.0, race = {
current_accel = 0.0, speed = 0.0,
accel = 10.0,
},
finish = finish finish = finish
} }
@ -20,28 +25,29 @@ end
function entity:update(dt) function entity:update(dt)
if (self.data.pos[1] > self.data.finish[1]) then if (self.data.pos[1] > self.data.finish[1]) then
self.data.pos[1] = 0 self.data.pos[1] = 0
self.data.current_speed = 0 self.data.race.speed = 0
self.data.current_accel = 0 self.data.race.accel = 10.0
end end
self:accel(dt) self.data.race.speed = racing.accelerate(dt, 1, self.data.race.speed, self.data.actor.data.max_speed, self.data.race.accel)
self.data.pos[1] = self.data.pos[1] + self.data.current_speed * dt -- self:accel(dt)
self.data.pos[1] = self.data.pos[1] + self.data.race.speed * dt
end end
function entity:accel(dt) -- function entity:accel(dt)
if (self.data.current_accel <= self.data.actor.data.accel) then -- if (self.data.current_accel <= self.data.actor.data.accel) then
self.data.current_accel = self.data.current_accel + dt -- self.data.current_accel = self.data.current_accel + dt
end -- end
if (self.data.current_speed <= self.data.actor.data.max_speed) then -- 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 -- self.data.current_speed = self.data.current_speed + self.data.current_accel * dt
end -- end
end -- end
function entity:draw() function entity:draw()
love.graphics.push() love.graphics.push()
love.graphics.setColor(self.data.color[1], self.data.color[2], self.data.color[3]) 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.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(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.print(string.format("Current Accel : %s", self.data.current_accel), 0, 40)
love.graphics.pop() love.graphics.pop()
self.data.actor:draw() self.data.actor:draw()
end end

View File

@ -2,12 +2,20 @@ local entity = {}
entity.__index = 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) local self = setmetatable({}, entity)
self.data = { self.data = data or _data
max_speed = 100.0,
accel = 2.0,
}
return self return self
end end
@ -16,8 +24,11 @@ end
function entity:draw() function entity:draw()
love.graphics.push() love.graphics.push()
love.graphics.print(string.format("Max Speed : %s", self.data.max_speed), 0, 0) love.graphics.print(string.format("Name : %s", self.data.name, self.data.ui[1], self.data.ui[2]))
love.graphics.print(string.format("Accel : %s", self.data.accel), 0, 20) 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() love.graphics.pop()
end end

View File

@ -1,30 +1,80 @@
local slick = require("love_src.lib.slick")
local racer = require("love_src.src.entities.racing.racer") local racer = require("love_src.src.entities.racing.racer")
local player = require("love_src.src.entities.shared.actor").load()
local mode = {} local mode = {}
local finish = {100, 100, 0} 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 end
function mode.update(dt) local function movePlayer(p, dt)
entities.racer:update(dt) local goalX, goalY = p.x + dt * p.velocityX, p.y + dt * p.velocityY
p.x, p.y = world:move(p, goalX, goalY)
end end
function mode.draw() function mode:update(dt)
entities.racer:draw() 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 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 end
function mode.keypressed(key, scancode, isrepeat) function mode:keyreleased(key, scancode)
end 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 end
return mode return mode

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,226 +1,226 @@
local ffi = require 'ffi' -- local ffi = require 'ffi'
local joysticks -- local joysticks
function init() -- function init()
joysticks = love.joystick.getJoysticks() -- joysticks = love.joystick.getJoysticks()
for i, joystick in ipairs(joysticks) do -- for i, joystick in ipairs(joysticks) do
print(i, joystick:getName()) -- print(i, joystick:getName())
end -- end
ffi.cdef[[ -- ffi.cdef[[
void load(const char * source_path); -- void load(const char * source_path);
void update_window(int width, int height); -- void update_window(int width, int height);
void draw(); -- void draw();
void update_keyboard(int up, int down, int left, int right, -- void update_keyboard(int up, int down, int left, int right,
int w, int s, int a, int d, -- int w, int s, int a, int d,
int t, int g, int f, int h, -- int t, int g, int f, int h,
int i, int k, int j, int l); -- int i, int k, int j, int l);
void update_mouse(int x, int y); -- void update_mouse(int x, int y);
void update_joystick(int joystick_index, -- void update_joystick(int joystick_index,
float lx, float ly, float rx, float ry, float tl, float tr, -- float lx, float ly, float rx, float ry, float tl, float tr,
int up, int down, int left, int right, -- int up, int down, int left, int right,
int a, int b, int x, int y, -- int a, int b, int x, int y,
int leftshoulder, int rightshoulder, -- int leftshoulder, int rightshoulder,
int start); -- int start);
void update(float time); -- void update(float time);
int draw_font_start(); -- int draw_font_start();
int draw_font(int font_ix, char const * text, int x, int y); -- int draw_font(int font_ix, char const * text, int x, int y);
void draw_line_quad_start(); -- void draw_line_quad_start();
void draw_line(int x1, int y1, int x2, int y2); -- void draw_line(int x1, int y1, int x2, int y2);
void draw_set_color(float r, float g, float b); -- void draw_set_color(float r, float g, float b);
void draw_quad(int x1, int y1, int x2, int y2, -- void draw_quad(int x1, int y1, int x2, int y2,
int x3, int y3, int x4, int y4); -- int x3, int y3, int x4, int y4);
]] -- ]]
local source_path = love.filesystem.getSource() -- local source_path = love.filesystem.getSource()
test = ffi.load(source_path .. "/test.so") -- test = ffi.load(source_path .. "/test.so")
test.load(source_path) -- test.load(source_path)
end -- end
local update = function(time) -- local update = function(time)
for joystick_index, joystick in ipairs(joysticks) do -- for joystick_index, joystick in ipairs(joysticks) do
if joystick_index > 8 then -- if joystick_index > 8 then
break -- break
end -- end
local lx = joystick:getGamepadAxis("leftx") -- local lx = joystick:getGamepadAxis("leftx")
local ly = joystick:getGamepadAxis("lefty") -- local ly = joystick:getGamepadAxis("lefty")
local rx = joystick:getGamepadAxis("rightx") -- local rx = joystick:getGamepadAxis("rightx")
local ry = joystick:getGamepadAxis("righty") -- local ry = joystick:getGamepadAxis("righty")
local tl = joystick:getGamepadAxis("triggerleft") -- local tl = joystick:getGamepadAxis("triggerleft")
local tr = joystick:getGamepadAxis("triggerright") -- local tr = joystick:getGamepadAxis("triggerright")
local up = joystick:isGamepadDown("dpup") -- local up = joystick:isGamepadDown("dpup")
local down = joystick:isGamepadDown("dpdown") -- local down = joystick:isGamepadDown("dpdown")
local left = joystick:isGamepadDown("dpleft") -- local left = joystick:isGamepadDown("dpleft")
local right = joystick:isGamepadDown("dpright") -- local right = joystick:isGamepadDown("dpright")
local a = joystick:isGamepadDown("a") -- local a = joystick:isGamepadDown("a")
local b = joystick:isGamepadDown("b") -- local b = joystick:isGamepadDown("b")
local x = joystick:isGamepadDown("x") -- local x = joystick:isGamepadDown("x")
local y = joystick:isGamepadDown("y") -- local y = joystick:isGamepadDown("y")
local leftshoulder = joystick:isGamepadDown("leftshoulder") -- local leftshoulder = joystick:isGamepadDown("leftshoulder")
local rightshoulder = joystick:isGamepadDown("rightshoulder") -- local rightshoulder = joystick:isGamepadDown("rightshoulder")
local start = joystick:isGamepadDown("start") -- local start = joystick:isGamepadDown("start")
--print("start", i, start) -- --print("start", i, start)
test.update_joystick(joystick_index - 1, -- test.update_joystick(joystick_index - 1,
lx, ly, rx, ry, tl, tr, -- lx, ly, rx, ry, tl, tr,
up, down, left, right, -- up, down, left, right,
a, b, x, y, -- a, b, x, y,
leftshoulder, rightshoulder, -- leftshoulder, rightshoulder,
start) -- start)
end -- end
local up = love.keyboard.isDown("up") -- local up = love.keyboard.isDown("up")
local down = love.keyboard.isDown("down") -- local down = love.keyboard.isDown("down")
local left = love.keyboard.isDown("left") -- local left = love.keyboard.isDown("left")
local right = love.keyboard.isDown("right") -- local right = love.keyboard.isDown("right")
local w = love.keyboard.isDown("w") -- local w = love.keyboard.isDown("w")
local s = love.keyboard.isDown("s") -- local s = love.keyboard.isDown("s")
local a = love.keyboard.isDown("a") -- local a = love.keyboard.isDown("a")
local d = love.keyboard.isDown("d") -- local d = love.keyboard.isDown("d")
local t = love.keyboard.isDown("t") -- local t = love.keyboard.isDown("t")
local g = love.keyboard.isDown("g") -- local g = love.keyboard.isDown("g")
local f = love.keyboard.isDown("f") -- local f = love.keyboard.isDown("f")
local h = love.keyboard.isDown("h") -- local h = love.keyboard.isDown("h")
local i = love.keyboard.isDown("i") -- local i = love.keyboard.isDown("i")
local k = love.keyboard.isDown("k") -- local k = love.keyboard.isDown("k")
local j = love.keyboard.isDown("j") -- local j = love.keyboard.isDown("j")
local l = love.keyboard.isDown("l") -- local l = love.keyboard.isDown("l")
test.update_keyboard(up, down, left, right, -- test.update_keyboard(up, down, left, right,
w, s, a, d, -- w, s, a, d,
t, g, f, h, -- t, g, f, h,
i, k, j, l); -- i, k, j, l);
test.update(time) -- test.update(time)
end -- end
local draw = function() -- local draw = function()
test.draw() -- test.draw()
end -- end
local nico_draw = function() -- local nico_draw = function()
---------------------------------------------------------------------- -- ----------------------------------------------------------------------
-- font drawing -- -- font drawing
---------------------------------------------------------------------- -- ----------------------------------------------------------------------
-- call "draw_font_start()" prior each "group" of "draw_font()" calls -- -- call "draw_font_start()" prior each "group" of "draw_font()" calls
-- -- --
-- a "group" of draw_font() calls are back-to-back/consecutive, -- -- a "group" of draw_font() calls are back-to-back/consecutive,
-- with no non-font drawing between them. -- -- with no non-font drawing between them.
-- -- --
-- For example: -- -- For example:
local font_ix = test.draw_font_start() -- local font_ix = test.draw_font_start()
local x = 512 -- local x = 512
local y = 50 -- local y = 50
y = y + test.draw_font(font_ix, "lua test", x, y) -- y = y + test.draw_font(font_ix, "lua test", x, y)
y = y + test.draw_font(font_ix, "cool", x, y) -- y = y + test.draw_font(font_ix, "cool", x, y)
-- note that "font_ix" is the current "best font" as calculated -- -- note that "font_ix" is the current "best font" as calculated
-- from the current window size, and might change next frame if the -- -- from the current window size, and might change next frame if the
-- window is resized. -- -- window is resized.
-- -- --
-- Any of this of course could be changed to match your precise -- -- Any of this of course could be changed to match your precise
-- requirements. -- -- requirements.
---------------------------------------------------------------------- -- ----------------------------------------------------------------------
-- line drawing -- -- line drawing
---------------------------------------------------------------------- -- ----------------------------------------------------------------------
-- call "draw_line_quad_start()" prior to each "group" of -- -- call "draw_line_quad_start()" prior to each "group" of
-- "draw_line()" or "draw_quad()" calls -- -- "draw_line()" or "draw_quad()" calls
-- -- --
-- a "group" of draw_line()/draw_quad() calls are -- -- a "group" of draw_line()/draw_quad() calls are
-- back-to-back/consecutive, with no non-line/quad drawing between -- -- back-to-back/consecutive, with no non-line/quad drawing between
-- them. -- -- them.
-- -- --
-- For example: -- -- For example:
test.draw_line_quad_start() -- 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_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(0, 0, 1024, 1024) -- x1, y1, x2, y2
test.draw_line(700, 300, 400, 500) -- test.draw_line(700, 300, 400, 500)
test.draw_set_color(0.0, 1.0, 0.0) -- test.draw_set_color(0.0, 1.0, 0.0)
test.draw_line(700, 300, 400, 700) -- test.draw_line(700, 300, 400, 700)
-- x1, y1, x2, y2, -- -- x1, y1, x2, y2,
-- x3, y3, x4, y4, -- -- x3, y3, x4, y4,
-- -- --
-- vertices must be specified In "counter clockwise" order, as in: -- -- vertices must be specified In "counter clockwise" order, as in:
-- -- --
-- 2──1 -- -- 2──1
-- │ │ valid (counter clockwise) -- -- │ │ valid (counter clockwise)
-- 3──4 -- -- 3──4
-- -- --
-- these can also be rotated, as in: -- -- these can also be rotated, as in:
-- -- --
-- 3 -- -- 3
-- ╱ ╲ -- -- ╱ ╲
-- 4 2 valid (counter clockwise) -- -- 4 2 valid (counter clockwise)
-- ╲ ╱ -- -- ╲ ╱
-- 1 -- -- 1
-- -- --
-- however "mirroring" is not valid, as in: -- -- however "mirroring" is not valid, as in:
-- -- --
-- 1──2 -- -- 1──2
-- │ │ not valid (clockwise) -- -- │ │ not valid (clockwise)
-- 4──3 -- -- 4──3
-- -- --
test.draw_set_color(0.0, 0.0, 1.0) -- test.draw_set_color(0.0, 0.0, 1.0)
test.draw_quad( -- test.draw_quad(
600, 600, -- top right -- 600, 600, -- top right
500, 600, -- top left -- 500, 600, -- top left
500, 700, -- bottom left -- 500, 700, -- bottom left
600, 700 -- bottom right -- 600, 700 -- bottom right
) -- )
test.draw_set_color(0.0, 0.5, 1.0) -- test.draw_set_color(0.0, 0.5, 1.0)
test.draw_quad( -- test.draw_quad(
900, 900, -- bottom -- 900, 900, -- bottom
950, 850, -- right -- 950, 850, -- right
900, 800, -- top -- 900, 800, -- top
850, 850 -- left -- 850, 850 -- left
) -- )
-- If you want to draw a large number of lines or quads in bulk -- -- 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 -- -- (e.g: 10,000+ lines/quads per frame), this interface might not be good
-- enough, and we should discuss this in more detail. -- -- enough, and we should discuss this in more detail.
end -- end
function love.run() -- function love.run()
init() -- init()
return function() -- return function()
love.event.pump() -- love.event.pump()
for name, a,b,c,d,e,f,g,h in love.event.poll() do -- for name, a,b,c,d,e,f,g,h in love.event.poll() do
if name == "quit" then -- if name == "quit" then
if c or not love.quit or not love.quit() then -- if c or not love.quit or not love.quit() then
return a or 0, b -- return a or 0, b
end -- end
end -- end
end -- end
local width -- local width
local height -- local height
local flags -- local flags
width, height, flags = love.window.getMode() -- width, height, flags = love.window.getMode()
test.update_window(width, height) -- test.update_window(width, height)
local time = love.timer.getTime() -- local time = love.timer.getTime()
update(time) -- update(time)
draw() -- draw()
local mouse_down = love.mouse.isDown(1) -- local mouse_down = love.mouse.isDown(1)
if mouse_down then -- if mouse_down then
local x, y = love.mouse.getPosition() -- local x, y = love.mouse.getPosition()
test.update_mouse(x, y) -- test.update_mouse(x, y)
end -- end
-- nico_draw() -- -- nico_draw()
love.graphics.present() -- love.graphics.present()
love.timer.sleep(0.001) -- love.timer.sleep(0.001)
end -- end
end -- end
-- function love.load(args) -- function love.load(args)
-- init() -- init()
@ -248,43 +248,44 @@ end
-- end -- end
-- local wm = require("world_map") local wm = require("world_map")
-- world = { world = {
-- ["main_menu"] = require("love_src.src.world.main_menu")(), ["top_down_race"] = require("love_src.src.world.top_down_race")(),
-- ["1_intro"] = require("love_src.src.world.1_intro")(), -- ["main_menu"] = require("love_src.src.world.main_menu")(),
-- ["2_town_square"] = require("love_src.src.world.2_town_square")(), -- ["1_intro"] = require("love_src.src.world.1_intro")(),
-- ["race"] = require("love_src.src.world.race")(), -- ["2_town_square"] = require("love_src.src.world.2_town_square")(),
-- ["train"] = require("love_src.src.world.train")(), -- ["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) function load_world(world_to_load)
-- current = world_to_load current = world_to_load
-- world[current]:reload() world[current]:reload()
-- end end
-- function love.load() function love.load()
-- world[current]:load() world[current]:load()
-- end end
-- function love.update(dt) function love.update(dt)
-- world[current]:update(dt) world[current]:update(dt)
-- end end
-- function love.draw() function love.draw()
-- world[current]:draw() world[current]:draw()
-- end end
-- function love.keyreleased(key, scancode) function love.keyreleased(key, scancode)
-- world[current]:keyreleased(key, scancode) world[current]:keyreleased(key, scancode)
-- end end
-- function love.keypressed(key, scancode, isrepeat) function love.keypressed(key, scancode, isrepeat)
-- world[current]:keypressed(key, scancode, isrepeat) world[current]:keypressed(key, scancode, isrepeat)
-- end end
-- function love.mousereleased(x, y, button, istouch, presses) function love.mousereleased(x, y, button, istouch, presses)
-- world[current]:mousereleased(x, y, button, istouch, presses) world[current]:mousereleased(x, y, button, istouch, presses)
-- end end

View File

@ -1,4 +1,5 @@
return { return {
["top_down_race"] = "top_down_race",
["main_menu"] = "main_menu", ["main_menu"] = "main_menu",
["1_intro"] = "1_intro", ["1_intro"] = "1_intro",
["2_town_square"] = "2_town_square", ["2_town_square"] = "2_town_square",