Okay, I have the bulk of the code complete but untested. To make it work, findAdjacent
needs to be called with a table whose keys are arrays of the Vector2 points of each region. Let me know if you need help doing that. This code is pretty close to the solution and should only have minor bugs, if any. Also lmk if you have any trouble debugging it.
It returns a table whose keys (key 1) is each region, and the value is a table with the keys (key 2) equal to all the regions adjacent to key 1. Value 2 is just true. It should make sense when you look at the code.
local function union(from, to)
for key, value in next, from do
to[key] = value
end
end
-- We can cache centers since regions are immutable
local centerCache = setmetatable({}, {__key = 'k'})
local function getCenter(region)
if centerCache[region] then
return centerCache[region]
end
local center = Vector2.new()
for i, point in next, region do
center = center + point
end
center = center / #region
centerCache[region] = center
return center
end
-- Line point a, line point b, center c, point p
local function isInside(a, b, c, p)
local ab = b - a
return ab:Cross(c - a) * ab:Cross(p - a) > 0
end
local function findConvexParts(originalRegion)
local region = union(originalRegion, {})
local convexParts = {[region] = true}
local center = getCenter(region)
local i = 1
local a = region[1]
local b = region[2]
local c = region[3]
while c do
if isInside(a, c, center, b) then
local prev
if i == 1 then
prev = region[#region]
else
prev = region[i - 1]
end
-- Triangle are garenteed to be convex, being the 2D simplex
convexParts[{b, a, prev}] = true
-- Bye-bye point a
table.remove(region, i)
center = center - a / #region
else
i = i + 1
end
a = b
b = c
c = region[(i+1 % #region) + 1]
end
return convexParts
end
local function getOutwardNormal(a, b, center)
local line = b - a
return line:Cross(line:Cross(center - a)).Unit
end
local maxGap = 0.5
local function expand(originalRegion)
local region = {}
local center = getCenter(originalRegion)
local prevI = #region
for i, point in next, originalRegion do
local prev = originalRegion[prevI]
local next = originalRegion[(i % #region) + 1]
local normA = getOutwardNormal(point, prev, center)
local normB = getOutwardNormal(point, next, center)
region[i] = point + maxGap * (normA + normB)
end
return region
end
local function contains(region, other, center)
for i, a in next, region do
local b = region[(i % #region) + 1]
for j, p in next, other do
if not isInside(a, b, center, p) then
other[j] = nil
end
end
end
if next(other) then
return true
end
end
-- 2D intersection algorithm
-- For 2D shapes to intersect, one must have a point inside another
-- Since they are convex the seperating line must be the line between them
local function isIntersecting(region1Original, region2Original)
local region1 = union(region1Original, {})
local region2 = union(region2Original, {})
return contains(region1Original, region2, getCenter(region1))
or contains(region2Original, region1, getCenter(region2))
end
local function getBoundingCircle(region)
local center = getCenter(region)
local maxRadius = 0
for i, point in next, region do
local radius = (point - center).Magnitude
if radius > maxRadius then
maxRadius = radius
end
end
return {
center = center;
radius = maxRadius;
}
end
local function circlesIntersect(circle, otherCircle)
return (circle.center - otherCircle.center).Magnitude < circle.radius + otherCircle.radius
end
local function findAdjacent(originalRegions)
local toOriginal = {}
local adjacent = {}
local circles = {}
for region in next, originalRegions do
adjacent[region] = {}
for part in next, findConvexParts(expand(region)) do
toOriginal[part] = region
circles[part] = getBoundingCircle(region)
end
end
-- Use very quick bounding circle tests to cut down number of checks
local checkList = {}
for part, circle in next, circles do
for other, otherCircle in next, circles, part do
if toOriginal[part] ~= toOriginal[other] and circlesIntersect(circle, otherCircle) then
checkList[{part, other}] = true
end
end
end
-- Perform exact 2D intersection tests on near parts
for set in next, checkList do
local part1 = set[1]
local part2 = set[2]
local original1 = toOriginal[part1]
local original2 = toOriginal[part2]
-- We'll need to see if the regions the parts are from
-- have already been found to be adjacent
if not adjacent[original1][original2]
and isIntersecting(part1, part2)
then
adjacent[original1][original2] = true
adjacent[original2][original1] = true
end
end
return adjacent
end