SNKRX/engine/math/polygon.lua

228 lines
8.5 KiB
Lua

-- A polygon class.
Polygon = Object:extend()
function Polygon:init(vertices)
self.vertices = vertices
self:get_size()
self:get_bounds()
self:get_centroid()
end
-- Draws the polygon.
-- If color is passed in then the polygon will be filled with that color (color is a Color instance)
-- If line_width is passed in then the polygon will not be filled and will instead be drawn as a set of lines of the given width.
function Polygon:draw(color, line_width)
graphics.polygon(self.vertices, color, line_width)
end
-- Noisifies each line that makes up the polygon.
-- offset corresponds to the maximum amount of perpendicular offseting each line will have
-- generations corresponds to the number of times the line will be subdivided
-- The higher the number of generations, the higher the number of final lines generates and the more granular the noisification will be
-- polygon:noisify(15, 4) -> noisifies the polygon with 15 maximum units of offseting with 4 generations
function Polygon:noisify(offset, generations)
local noisified_vertices = {}
for i = 1, #self.vertices, 2 do
local x1, y1 = vs[i], vs[i+1]
local x2, y2 = vs[i+2], vs[i+3]
if not x2 and not y2 then x2, y2 = vs[1], vs[2] end
local noisified_line = math.noisify_line(x1, y1, x2, y2, offset, generations)
table.insert(noisified_vertices, noisified_line)
end
local flattened = table.flatten(noisified_vertices)
local final_vertices = {}
for i = 1, #flattened, 2 do
local x1, y1 = flattened[i], flattened[i+1]
local x2, y2 = flattened[i+2], flattened[i+3]
if not x2 and not y2 then x2, y2 = flattened[1], flattened[2] end
if math.distance(x1, y1, x2, y2) > 0.025 then
table.insert(final_vertices, x1)
table.insert(final_vertices, y1)
end
end
self.vertices = final_vertices
end
local path_to_polygon = function(path)
local vs = {}
for i = 1, path:size() do
local p = path:get(i)
table.insert(vs, tonumber(p.x))
table.insert(vs, tonumber(p.y))
end
return vs
end
local polygon_to_path = function(vs)
local path = clipper.Path()
for i = 1, #vs, 2 do path:add(vs[i], vs[i+1]) end
return path
end
-- Inflates the polygon around its center.
-- polygon:inflate(10) -> inflates the polygon by 10 units
function Polygon:inflate(s)
self.vertices = path_to_polygon(clipper.ClipperOffset():offsetPath(polygon_to_path(self.vertices), s, 'miter', 'closedPolygon'):get(1))
end
-- Moves the polygon directly to the given position.
-- polygon:move_to(20, 20) -> moves the polygon to position 20, 20
function Polygon:move_to(x, y)
if self.x and self.y then self:translate(x - self.x, y - self.y) end
self.x, self.y = x, y
end
-- Translates the polygon by the given amount.
-- polygon:translate(20, 20) -> moves the polygon by 20, 20 units
function Polygon:translate(x, y)
for i = 1, #self.vertices, 2 do
self.vertices[i] = self.vertices[i] + x
self.vertices[i+1] = self.vertices[i+1] + y
end
end
-- Scales the polygon by the given amount around the given pivot.
-- polygon:scale(2) -> scales the polygon by 2 around its center (if set) or 0
function Polygon:scale(sx, sy, ox, oy)
for i = 1, #self.vertices, 2 do
self.vertices[i], self.vertices[i+1] = math.scale_point(self.vertices[i], self.vertices[i+1], sx or 1, sy or sx or 1, ox or self.cx or 0, oy or self.cy or 0)
end
end
-- Rotates the polygon by the given amount around the given pivot.
-- polygon:rotate(math.pi/4) -> rotates the polygon by 45 degrees around its center (if set) or 0
function Polygon:rotate(r, ox, oy)
for i = 1, #self.vertices, 2 do
self.vertices[i], self.vertices[i+1] = math.rotate_point(self.vertices[i], self.vertices[i+1], r, ox or self.cx or 0, oy or self.cy or 0)
end
end
-- Returns the polygons size.
-- This calculates the bounding box of the polygon and sets that size to the .w, .h attributes.
-- w, h = polygon:get_size()
function Polygon:get_size()
local min_x, min_y, max_x, max_y = 1000000, 1000000, -1000000, -1000000
for i = 1, #self.vertices, 2 do
if self.vertices[i] < min_x then min_x = self.vertices[i] end
if self.vertices[i] > max_x then max_x = self.vertices[i] end
if self.vertices[i+1] < min_y then min_y = self.vertices[i+1] end
if self.vertices[i+1] > max_y then max_y = self.vertices[i+1] end
end
self.w, self.h = math.abs(max_x - min_x), math.abs(max_y - min_y)
return self.w, self.h
end
-- Returns the polygons bounding box top-left and bottom-right positions.
-- This calculates the bounding box of the polygon and sets those positions to the .x1, .y1, .x2, .y2 attributes.
-- x1, y1, x2, y2 = polygon:get_bounds()
function Polygon:get_bounds()
local min_x, min_y, max_x, max_y = 1000000, 1000000, -1000000, -1000000
for i = 1, #self.vertices, 2 do
if self.vertices[i] < min_x then min_x = self.vertices[i] end
if self.vertices[i] > max_x then max_x = self.vertices[i] end
if self.vertices[i+1] < min_y then min_y = self.vertices[i+1] end
if self.vertices[i+1] > max_y then max_y = self.vertices[i+1] end
end
self.x1, self.y1, self.x2, self.y2 = min_x, min_y, max_x, max_y
return self.x1, self.y1, self.x2, self.y2
end
-- Returns the centroid of the polygon.
-- This calculates the centroid (average of all points) and sets it to the .x, .y attributes.
-- x, y = polygon:get_centroid()
-- TODO: implement get_visual_center https://github.com/mapbox/polylabel/blob/master/polylabel.js
function Polygon:get_centroid()
local sum_x, sum_y = 0, 0
for i = 1, #self.vertices, 2 do
sum_x = sum_x + self.vertices[i]
sum_y = sum_y + self.vertices[i+1]
end
self.cx, self.cy = sum_x/(#self.vertices/2), sum_y/(#self.vertices/2)
return self.cx, self.cy
end
-- Returns true if this polygon is colliding with the given shape.
-- colliding = polygon:is_colliding_with_shape(shape)
function Polygon:is_colliding_with_shape(shape)
if shape:is(Line) then
return self:is_colliding_with_line(shape)
elseif shape:is(Chain) then
return self:is_colliding_with_chain(shape)
elseif shape:is(Circle) then
return self:is_colliding_with_circle(shape)
elseif shape:is(Polygon) then
return self:is_colliding_with_polygon(shape)
elseif shape:is(Rectangle) then
return self:is_colliding_with_polygon(shape)
elseif shape:is(EmeraldRectangle) then
return self:is_colliding_with_polygon(shape)
elseif shape:is(Triangle) then
return self:is_colliding_with_polygon(shape)
elseif shape:is(EquilateralTriangle) then
return self:is_colliding_with_polygon(shape)
end
end
-- Returns true if the point is inside the polygon.
-- colliding = polygon:is_colliding_with_point(x, y)
function Polygon:is_colliding_with_point(x, y)
return mlib.polygon.checkPoint(x, y, self.vertices)
end
-- Returns true if the line is colliding with this polygon.
-- colliding = polygon:is_colliding_with_line(line)
function Polygon:is_colliding_with_line(line)
return mlib.polygon.isSegmentInside(line.x1, line.y1, line.x2, line.y2, self.vertices)
end
-- Returns true if the chain is colliding with this polygon.
-- colliding = polygon:is_colliding_with_chain(chain)
function Polygon:is_colliding_with_chain(chain)
return chain:is_colliding_with_polygon(self)
end
-- Returns true if the circle is colliding with this circle.
-- colliding = polygon:is_colliding_with_circle(circle)
function Polygon:is_colliding_with_circle(circle)
return mlib.polygon.isCircleCompletelyInside(circle.x, circle.y, circle.rs, self.vertices) or
mlib.circle.isPolygonCompletelyInside(circle.x, circle.y, circle.rs, self.vertices) or
mlib.polygon.getCircleIntersection(circle.x, circle.y, circle.rs, self.vertices)
end
-- Returns true if the polygon is colliding with this polygon.
-- colliding = polygon:is_colliding_with_polygon(other_polygon)
function Polygon:is_colliding_with_polygon(polygon)
return mlib.polygon.isPolygonInside(self.vertices, polygon.vertices) or mlib.polygon.isPolygonInside(polygon.vertices, self.vertices)
end
-- Returned results can be either the merged polygons or the holes inside them (or a polygon inside a hole, or a hole inside that polygon, and so on...)
-- Unfortunately for now it doesn't really report which polygons are which, so this function has some limited utility.
function Polygon.merge_polygons(polygons)
local cl = clipper.Clipper()
local paths = clipper.Paths()
for _, polygon in ipairs(polygons) do paths:add(polygon_to_path(polygon.vertices)) end
cl:addPaths(paths, 'subject')
local out = cl:execute('union')
local out_polygons = {}
for i = 1, out:size() do table.insert(out_polygons, path_to_polygon(out:get(i))) end
return out_polygons
end