mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2025-08-15 10:47:21 +00:00
458 lines
17 KiB
Lua
458 lines
17 KiB
Lua
---
|
|
--
|
|
-- ### Author: **nielsvaes/coconutcockpit**
|
|
--
|
|
-- ===
|
|
-- @module Shapes.POLYGON
|
|
|
|
--- POLYGON class.
|
|
-- @type POLYGON
|
|
-- @field #string ClassName Name of the class.
|
|
-- @field #table Points List of 3D points defining the shape, this will be assigned automatically if you're passing in a drawing from the Mission Editor
|
|
-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically if you're passing in a drawing from the Mission Editor
|
|
-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically if you're passing in a drawing from the Mission Editor
|
|
-- @field #table Triangles List of TRIANGLEs that make up the shape of the POLYGON after being triangulated
|
|
-- @extends Core.Base#BASE
|
|
|
|
--- *Polygons are fashionable at the moment* -- Trip Hawkins
|
|
--
|
|
-- ===
|
|
--
|
|
-- # POLYGON
|
|
-- POLYGONs can be fetched from the drawings in the Mission Editor if the drawing is:
|
|
-- * A closed shape made with line segments
|
|
-- * A closed shape made with a freehand line
|
|
-- * A freehand drawn polygon
|
|
-- * A rect
|
|
-- Use the POLYGON:FindOnMap() of POLYGON:Find() functions for this. You can also create a non existing polygon in memory using the POLYGON:New() function. Pass in a
|
|
-- any number of Vec2s into this function to define the shape of the polygon you want.
|
|
--
|
|
-- You can draw very intricate and complex polygons in the Mission Editor to avoid (or include) map objects. You can then generate random points within this complex
|
|
-- shape for spawning groups or checking positions.
|
|
--
|
|
-- When a POLYGON is made, it's automatically triangulated. The resulting triangles are stored in POLYGON.Triangles. This also immeadiately saves the surface area
|
|
-- of the POLYGON. Because the POLYGON is triangulated, it's possible to generate random points within this POLYGON without having to use a trial and error method to see if
|
|
-- the point is contained within the shape.
|
|
-- Using POLYGON:GetRandomVec2() will result in a truly, non-biased, random Vec2 within the shape. You'll want to use this function most. There's also POLYGON:GetRandomNonWeightedVec2
|
|
-- which ignores the size of the triangles in the polygon to pick a random points. This will result in more points clumping together in parts of the polygon where the triangles are
|
|
-- the smallest.
|
|
|
|
---
|
|
-- @field #POLYGON
|
|
POLYGON = {
|
|
ClassName = "POLYGON",
|
|
Points = {},
|
|
Coords = {},
|
|
Triangles = {},
|
|
SurfaceArea = 0,
|
|
TriangleMarkIDs = {},
|
|
OutlineMarkIDs = {},
|
|
Angle = nil, -- for arrows
|
|
Heading = nil -- for arrows
|
|
}
|
|
|
|
--- Finds a polygon on the map by its name. The polygon must be added in the mission editor.
|
|
-- @param #string shape_name Name of the polygon to find
|
|
-- @return #POLYGON The found polygon, or nil if not found
|
|
function POLYGON:FindOnMap(shape_name)
|
|
local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name))
|
|
|
|
for _, layer in pairs(env.mission.drawings.layers) do
|
|
for _, object in pairs(layer["objects"]) do
|
|
if object["name"] == shape_name then
|
|
if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then
|
|
for _, point in UTILS.spairs(object["points"]) do
|
|
local p = {x = object["mapX"] + point["x"],
|
|
y = object["mapY"] + point["y"] }
|
|
local coord = COORDINATE:NewFromVec2(p)
|
|
self.Points[#self.Points + 1] = p
|
|
self.Coords[#self.Coords + 1] = coord
|
|
end
|
|
elseif object["polygonMode"] == "rect" then
|
|
local angle = object["angle"]
|
|
local half_width = object["width"] / 2
|
|
local half_height = object["height"] / 2
|
|
|
|
local p1 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle)
|
|
local p2 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle)
|
|
local p3 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle)
|
|
local p4 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle)
|
|
|
|
self.Points = {p1, p2, p3, p4}
|
|
for _, point in pairs(self.Points) do
|
|
self.Coords[#self.Coords + 1] = COORDINATE:NewFromVec2(point)
|
|
end
|
|
elseif object["polygonMode"] == "arrow" then
|
|
for _, point in UTILS.spairs(object["points"]) do
|
|
local p = {x = object["mapX"] + point["x"],
|
|
y = object["mapY"] + point["y"] }
|
|
local coord = COORDINATE:NewFromVec2(p)
|
|
self.Points[#self.Points + 1] = p
|
|
self.Coords[#self.Coords + 1] = coord
|
|
end
|
|
self.Angle = object["angle"]
|
|
self.Heading = UTILS.ClampAngle(self.Angle + 90)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if #self.Points == 0 then
|
|
return nil
|
|
end
|
|
|
|
self.CenterVec2 = self:GetCentroid()
|
|
self.Triangles = self:Triangulate()
|
|
self.SurfaceArea = self:__CalculateSurfaceArea()
|
|
|
|
self.TriangleMarkIDs = {}
|
|
self.OutlineMarkIDs = {}
|
|
return self
|
|
end
|
|
|
|
--- Creates a polygon from a zone. The zone must be defined in the mission.
|
|
-- @param #string zone_name Name of the zone
|
|
-- @return #POLYGON The polygon created from the zone, or nil if the zone is not found
|
|
function POLYGON:FromZone(zone_name)
|
|
for _, zone in pairs(env.mission.triggers.zones) do
|
|
if zone["name"] == zone_name then
|
|
return POLYGON:New(unpack(zone["verticies"] or {}))
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Finds a polygon by its name in the database.
|
|
-- @param #string shape_name Name of the polygon to find
|
|
-- @return #POLYGON The found polygon, or nil if not found
|
|
function POLYGON:Find(shape_name)
|
|
return _DATABASE:FindShape(shape_name)
|
|
end
|
|
|
|
--- Creates a new polygon from a list of points. Each point is a table with 'x' and 'y' fields.
|
|
-- @param #table ... Points of the polygon
|
|
-- @return #POLYGON The new polygon
|
|
function POLYGON:New(...)
|
|
local self = BASE:Inherit(self, SHAPE_BASE:New())
|
|
|
|
self.Points = {...}
|
|
self.Coords = {}
|
|
for _, point in UTILS.spairs(self.Points) do
|
|
table.insert(self.Coords, COORDINATE:NewFromVec2(point))
|
|
end
|
|
self.Triangles = self:Triangulate()
|
|
self.SurfaceArea = self:__CalculateSurfaceArea()
|
|
|
|
return self
|
|
end
|
|
|
|
--- Calculates the centroid of the polygon. The centroid is the average of the 'x' and 'y' coordinates of the points.
|
|
-- @return #table The centroid of the polygon
|
|
function POLYGON:GetCentroid()
|
|
local function sum(t)
|
|
local total = 0
|
|
for _, value in pairs(t) do
|
|
total = total + value
|
|
end
|
|
return total
|
|
end
|
|
|
|
local x_values = {}
|
|
local y_values = {}
|
|
local length = table.length(self.Points)
|
|
|
|
for _, point in pairs(self.Points) do
|
|
table.insert(x_values, point.x)
|
|
table.insert(y_values, point.y)
|
|
end
|
|
|
|
local x = sum(x_values) / length
|
|
local y = sum(y_values) / length
|
|
|
|
return {
|
|
["x"] = x,
|
|
["y"] = y
|
|
}
|
|
end
|
|
|
|
--- Returns the coordinates of the polygon. Each coordinate is a COORDINATE object.
|
|
-- @return #table The coordinates of the polygon
|
|
function POLYGON:GetCoordinates()
|
|
return self.Coords
|
|
end
|
|
|
|
--- Returns the start coordinate of the polygon. The start coordinate is the first point of the polygon.
|
|
-- @return #COORDINATE The start coordinate of the polygon
|
|
function POLYGON:GetStartCoordinate()
|
|
return self.Coords[1]
|
|
end
|
|
|
|
--- Returns the end coordinate of the polygon. The end coordinate is the last point of the polygon.
|
|
-- @return #COORDINATE The end coordinate of the polygon
|
|
function POLYGON:GetEndCoordinate()
|
|
return self.Coords[#self.Coords]
|
|
end
|
|
|
|
--- Returns the start point of the polygon. The start point is the first point of the polygon.
|
|
-- @return #table The start point of the polygon
|
|
function POLYGON:GetStartPoint()
|
|
return self.Points[1]
|
|
end
|
|
|
|
--- Returns the end point of the polygon. The end point is the last point of the polygon.
|
|
-- @return #table The end point of the polygon
|
|
function POLYGON:GetEndPoint()
|
|
return self.Points[#self.Points]
|
|
end
|
|
|
|
--- Returns the points of the polygon. Each point is a table with 'x' and 'y' fields.
|
|
-- @return #table The points of the polygon
|
|
function POLYGON:GetPoints()
|
|
return self.Points
|
|
end
|
|
|
|
--- Calculates the surface area of the polygon. The surface area is the sum of the areas of the triangles that make up the polygon.
|
|
-- @return #number The surface area of the polygon
|
|
function POLYGON:GetSurfaceArea()
|
|
return self.SurfaceArea
|
|
end
|
|
|
|
--- Calculates the bounding box of the polygon. The bounding box is the smallest rectangle that contains the polygon.
|
|
-- @return #table The bounding box of the polygon
|
|
function POLYGON:GetBoundingBox()
|
|
local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[1].x, self.Points[1].y
|
|
|
|
for i = 2, #self.Points do
|
|
local x, y = self.Points[i].x, self.Points[i].y
|
|
|
|
if x < min_x then
|
|
min_x = x
|
|
end
|
|
if y < min_y then
|
|
min_y = y
|
|
end
|
|
if x > max_x then
|
|
max_x = x
|
|
end
|
|
if y > max_y then
|
|
max_y = y
|
|
end
|
|
end
|
|
return {
|
|
{x=min_x, y=min_x}, {x=max_x, y=min_y}, {x=max_x, y=max_y}, {x=min_x, y=max_y}
|
|
}
|
|
end
|
|
|
|
--- Triangulates the polygon. The polygon is divided into triangles.
|
|
-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it
|
|
-- @return #table The triangles of the polygon
|
|
function POLYGON:Triangulate(points)
|
|
points = points or self.Points
|
|
local triangles = {}
|
|
|
|
local function get_orientation(shape_points)
|
|
local sum = 0
|
|
for i = 1, #shape_points do
|
|
local j = i % #shape_points + 1
|
|
sum = sum + (shape_points[j].x - shape_points[i].x) * (shape_points[j].y + shape_points[i].y)
|
|
end
|
|
return sum >= 0 and "clockwise" or "counter-clockwise" -- sum >= 0, return "clockwise", else return "counter-clockwise"
|
|
end
|
|
|
|
local function ensure_clockwise(shape_points)
|
|
local orientation = get_orientation(shape_points)
|
|
if orientation == "counter-clockwise" then
|
|
-- Reverse the order of shape_points so they're clockwise
|
|
local reversed = {}
|
|
for i = #shape_points, 1, -1 do
|
|
table.insert(reversed, shape_points[i])
|
|
end
|
|
return reversed
|
|
end
|
|
return shape_points
|
|
end
|
|
|
|
local function is_clockwise(p1, p2, p3)
|
|
local cross_product = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)
|
|
return cross_product < 0
|
|
end
|
|
|
|
local function divide_recursively(shape_points)
|
|
if #shape_points == 3 then
|
|
table.insert(triangles, TRIANGLE:New(shape_points[1], shape_points[2], shape_points[3]))
|
|
elseif #shape_points > 3 then -- find an ear -> a triangle with no other points inside it
|
|
for i, p1 in ipairs(shape_points) do
|
|
local p2 = shape_points[(i % #shape_points) + 1]
|
|
local p3 = shape_points[(i + 1) % #shape_points + 1]
|
|
local triangle = TRIANGLE:New(p1, p2, p3)
|
|
local is_ear = true
|
|
|
|
if not is_clockwise(p1, p2, p3) then
|
|
is_ear = false
|
|
else
|
|
for _, point in ipairs(shape_points) do
|
|
if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then
|
|
is_ear = false
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if is_ear then
|
|
-- Check if any point in the original polygon is inside the ear triangle
|
|
local is_valid_triangle = true
|
|
for _, point in ipairs(points) do
|
|
if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then
|
|
is_valid_triangle = false
|
|
break
|
|
end
|
|
end
|
|
if is_valid_triangle then
|
|
table.insert(triangles, triangle)
|
|
local remaining_points = {}
|
|
for j, point in ipairs(shape_points) do
|
|
if point ~= p2 then
|
|
table.insert(remaining_points, point)
|
|
end
|
|
end
|
|
divide_recursively(remaining_points)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
points = ensure_clockwise(points)
|
|
divide_recursively(points)
|
|
return triangles
|
|
end
|
|
|
|
function POLYGON:CovarianceMatrix()
|
|
local cx, cy = self:GetCentroid()
|
|
local covXX, covYY, covXY = 0, 0, 0
|
|
for _, p in ipairs(self.points) do
|
|
covXX = covXX + (p.x - cx)^2
|
|
covYY = covYY + (p.y - cy)^2
|
|
covXY = covXY + (p.x - cx) * (p.y - cy)
|
|
end
|
|
covXX = covXX / (#self.points - 1)
|
|
covYY = covYY / (#self.points - 1)
|
|
covXY = covXY / (#self.points - 1)
|
|
return covXX, covYY, covXY
|
|
end
|
|
|
|
function POLYGON:Direction()
|
|
local covXX, covYY, covXY = self:CovarianceMatrix()
|
|
-- Simplified calculation for the largest eigenvector's direction
|
|
local theta = 0.5 * math.atan2(2 * covXY, covXX - covYY)
|
|
return math.cos(theta), math.sin(theta)
|
|
end
|
|
|
|
--- Returns a random Vec2 within the polygon. The Vec2 is weighted by the areas of the triangles that make up the polygon.
|
|
-- @return #table The random Vec2
|
|
function POLYGON:GetRandomVec2()
|
|
local weights = {}
|
|
for _, triangle in pairs(self.Triangles) do
|
|
weights[triangle] = triangle.SurfaceArea / self.SurfaceArea
|
|
end
|
|
|
|
local random_weight = math.random()
|
|
local accumulated_weight = 0
|
|
for triangle, weight in pairs(weights) do
|
|
accumulated_weight = accumulated_weight + weight
|
|
if accumulated_weight >= random_weight then
|
|
return triangle:GetRandomVec2()
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Returns a random non-weighted Vec2 within the polygon. The Vec2 is chosen from one of the triangles that make up the polygon.
|
|
-- @return #table The random non-weighted Vec2
|
|
function POLYGON:GetRandomNonWeightedVec2()
|
|
return self.Triangles[math.random(1, #self.Triangles)]:GetRandomVec2()
|
|
end
|
|
|
|
--- Checks if a point is contained within the polygon. The point is a table with 'x' and 'y' fields.
|
|
-- @param #table point The point to check
|
|
-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it
|
|
-- @return #bool True if the point is contained, false otherwise
|
|
function POLYGON:ContainsPoint(point, polygon_points)
|
|
local x = point.x
|
|
local y = point.y
|
|
|
|
polygon_points = polygon_points or self.Points
|
|
|
|
local counter = 0
|
|
local num_points = #polygon_points
|
|
for current_index = 1, num_points do
|
|
local next_index = (current_index % num_points) + 1
|
|
local current_x, current_y = polygon_points[current_index].x, polygon_points[current_index].y
|
|
local next_x, next_y = polygon_points[next_index].x, polygon_points[next_index].y
|
|
if ((current_y > y) ~= (next_y > y)) and (x < (next_x - current_x) * (y - current_y) / (next_y - current_y) + current_x) then
|
|
counter = counter + 1
|
|
end
|
|
end
|
|
return counter % 2 == 1
|
|
end
|
|
|
|
--- Draws the polygon on the map. The polygon can be drawn with or without inner triangles. This is just for debugging
|
|
-- @param #bool include_inner_triangles Whether to include inner triangles in the drawing
|
|
function POLYGON:Draw(include_inner_triangles)
|
|
include_inner_triangles = include_inner_triangles or false
|
|
for i=1, #self.Coords do
|
|
local c1 = self.Coords[i]
|
|
local c2 = self.Coords[i % #self.Coords + 1]
|
|
table.add(self.OutlineMarkIDs, c1:LineToAll(c2))
|
|
end
|
|
|
|
|
|
if include_inner_triangles then
|
|
for _, triangle in ipairs(self.Triangles) do
|
|
triangle:Draw()
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Removes the drawing of the polygon from the map.
|
|
function POLYGON:RemoveDraw()
|
|
for _, triangle in pairs(self.Triangles) do
|
|
triangle:RemoveDraw()
|
|
end
|
|
for _, mark_id in pairs(self.OutlineMarkIDs) do
|
|
UTILS.RemoveMark(mark_id)
|
|
end
|
|
end
|
|
|
|
--- Calculates the surface area of the polygon. The surface area is the sum of the areas of the triangles that make up the polygon.
|
|
-- @return #number The surface area of the polygon
|
|
function POLYGON:__CalculateSurfaceArea()
|
|
local area = 0
|
|
for _, triangle in pairs(self.Triangles) do
|
|
area = area + triangle.SurfaceArea
|
|
end
|
|
return area
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|