diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index c483b8d21..c95366e45 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -122,6 +122,15 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Route.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Account.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Actions/Act_Assist.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/ShapeBase.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Circle.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Cube.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Line.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Oval.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Polygon.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Triangle.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Shapes/Arrow.lua' ) + __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/UserSound.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/SoundOutput.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Sound/Radio.lua' ) diff --git a/Moose Development/Moose/Shapes/Circle.lua b/Moose Development/Moose/Shapes/Circle.lua new file mode 100644 index 000000000..04c153d86 --- /dev/null +++ b/Moose Development/Moose/Shapes/Circle.lua @@ -0,0 +1,259 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.CIRCLE + +--- CIRCLE class. +-- @type CIRCLE +-- @field #string ClassName Name of the class. +-- @field #number Radius Radius of the circle + +--- *It's NOT hip to be square* -- Someone, somewhere, probably +-- +-- === +-- +-- # CIRCLE +-- CIRCLEs can be fetched from the drawings in the Mission Editor + +-- This class has some of the standard CIRCLE functions you'd expect. One function of interest is CIRCLE:PointInSector() that you can use if a point is +-- within a certain sector (pizza slice) of a circle. This can be useful for many things, including rudimentary, "radar-like" searches from a unit. + +-- @field #CIRCLE + +--- CIRCLE class with properties and methods for handling circles. +CIRCLE = { + ClassName = "CIRCLE", + Radius = nil, +} +--- Finds a circle on the map by its name. The circle must have been added in the Mission Editor +-- @param #string shape_name Name of the circle to find +-- @return #CIRCLE The found circle, or nil if not found +function CIRCLE: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 string.find(object["name"], shape_name, 1, true) then + if object["polygonMode"] == "circle" then + self.Radius = object["radius"] + end + end + end + end + + return self +end + +--- Finds a circle by its name in the database. +-- @param #string shape_name Name of the circle to find +-- @return #CIRCLE The found circle, or nil if not found +function CIRCLE:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new circle from a center point and a radius. +-- @param #table vec2 The center point of the circle +-- @param #number radius The radius of the circle +-- @return #CIRCLE The new circle +function CIRCLE:New(vec2, radius) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = vec2 + self.Radius = radius + return self +end + +--- Gets the radius of the circle. +-- @return #number The radius of the circle +function CIRCLE:GetRadius() + return self.Radius +end + +--- Checks if a point is contained within the circle. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function CIRCLE:ContainsPoint(point) + if ((point.x - self.CenterVec2.x) ^ 2 + (point.y - self.CenterVec2.y) ^ 2) ^ 0.5 <= self.Radius then + return true + end + return false +end + +--- Checks if a point is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #table point The point to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if the point is contained, false otherwise +function CIRCLE:PointInSector(point, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + local function are_clockwise(v1, v2) + return -v1.x * v2.y + v1.y * v2.x > 0 + end + + local function is_in_radius(rp) + return rp.x * rp.x + rp.y * rp.y <= radius ^ 2 + end + + local rel_pt = { + x = point.x - center.x, + y = point.y - center.y + } + + local rel_sector_start = { + x = sector_start.x - center.x, + y = sector_start.y - center.y, + } + + local rel_sector_end = { + x = sector_end.x - center.x, + y = sector_end.y - center.y, + } + + return not are_clockwise(rel_sector_start, rel_pt) and + are_clockwise(rel_sector_end, rel_pt) and + is_in_radius(rel_pt, radius) +end + +--- Checks if a unit is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string unit_name The name of the unit to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if the unit is contained, false otherwise +function CIRCLE:UnitInSector(unit_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + if self:PointInSector(UNIT:FindByName(unit_name):GetVec2(), sector_start, sector_end, center, radius) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string group_name The name of the group to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if any unit of the group is contained, false otherwise +function CIRCLE:AnyOfGroupInSector(group_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within a sector of the circle. The start and end sector need to be clockwise +-- @param #string group_name The name of the group to check +-- @param #table sector_start The start point of the sector +-- @param #table sector_end The end point of the sector +-- @param #table center The center point of the sector +-- @param #number radius The radius of the sector +-- @return #bool True if all units of the group are contained, false otherwise +function CIRCLE:AllOfGroupInSector(group_name, sector_start, sector_end, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if not self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then + return false + end + end + return true +end + +--- Checks if a unit is contained within a radius of the circle. +-- @param #string unit_name The name of the unit to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if the unit is contained, false otherwise +function CIRCLE:UnitInRadius(unit_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + if UTILS.IsInRadius(center, UNIT:FindByName(unit_name):GetVec2(), radius) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within a radius of the circle. +-- @param #string group_name The name of the group to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if any unit of the group is contained, false otherwise +function CIRCLE:AnyOfGroupInRadius(group_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if UTILS.IsInRadius(center, unit:GetVec2(), radius) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within a radius of the circle. +-- @param #string group_name The name of the group to check +-- @param #table center The center point of the radius +-- @param #number radius The radius to check +-- @return #bool True if all units of the group are contained, false otherwise +function CIRCLE:AllOfGroupInRadius(group_name, center, radius) + center = center or self.CenterVec2 + radius = radius or self.Radius + + for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do + if not UTILS.IsInRadius(center, unit:GetVec2(), radius) then + return false + end + end + return true +end + +--- Returns a random Vec2 within the circle. +-- @return #table The random Vec2 +function CIRCLE:GetRandomVec2() + local angle = math.random() * 2 * math.pi + + local rx = math.random(0, self.Radius) * math.cos(angle) + self.CenterVec2.x + local ry = math.random(0, self.Radius) * math.sin(angle) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Returns a random Vec2 on the border of the circle. +-- @return #table The random Vec2 +function CIRCLE:GetRandomVec2OnBorder() + local angle = math.random() * 2 * math.pi + + local rx = self.Radius * math.cos(angle) + self.CenterVec2.x + local ry = self.Radius * math.sin(angle) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Calculates the bounding box of the circle. The bounding box is the smallest rectangle that contains the circle. +-- @return #table The bounding box of the circle +function CIRCLE:GetBoundingBox() + local min_x = self.CenterVec2.x - self.Radius + local min_y = self.CenterVec2.y - self.Radius + local max_x = self.CenterVec2.x + self.Radius + local max_y = self.CenterVec2.y + self.Radius + + 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 + diff --git a/Moose Development/Moose/Shapes/Cube.lua b/Moose Development/Moose/Shapes/Cube.lua new file mode 100644 index 000000000..ae3f73090 --- /dev/null +++ b/Moose Development/Moose/Shapes/Cube.lua @@ -0,0 +1,66 @@ +CUBE = { + ClassName = "CUBE", + Points = {}, + Coords = {} +} + +--- Points need to be added in the following order: +--- p1 -> p4 make up the front face of the cube +--- p5 -> p8 make up the back face of the cube +--- p1 connects to p5 +--- p2 connects to p6 +--- p3 connects to p7 +--- p4 connects to p8 +--- +--- 8-----------7 +--- /| /| +--- / | / | +--- 4--+--------3 | +--- | | | | +--- | | | | +--- | | | | +--- | 5--------+--6 +--- | / | / +--- |/ |/ +--- 1-----------2 +--- +function CUBE:New(p1, p2, p3, p4, p5, p6, p7, p8) + local self = BASE:Inherit(self, SHAPE_BASE) + self.Points = {p1, p2, p3, p4, p5, p6, p7, p8} + for _, point in spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec3(point)) + end + return self +end + +function CUBE:GetCenter() + local center = { x=0, y=0, z=0 } + for _, point in pairs(self.Points) do + center.x = center.x + point.x + center.y = center.y + point.y + center.z = center.z + point.z + end + + center.x = center.x / 8 + center.y = center.y / 8 + center.z = center.z / 8 + return center +end + +function CUBE:ContainsPoint(point, cube_points) + cube_points = cube_points or self.Points + local min_x, min_y, min_z = math.huge, math.huge, math.huge + local max_x, max_y, max_z = -math.huge, -math.huge, -math.huge + + -- Find the minimum and maximum x, y, and z values of the cube points + for _, p in ipairs(cube_points) do + if p.x < min_x then min_x = p.x end + if p.y < min_y then min_y = p.y end + if p.z < min_z then min_z = p.z end + if p.x > max_x then max_x = p.x end + if p.y > max_y then max_y = p.y end + if p.z > max_z then max_z = p.z end + end + + return point.x >= min_x and point.x <= max_x and point.y >= min_y and point.y <= max_y and point.z >= min_z and point.z <= max_z +end diff --git a/Moose Development/Moose/Shapes/Line.lua b/Moose Development/Moose/Shapes/Line.lua new file mode 100644 index 000000000..08f7c84a0 --- /dev/null +++ b/Moose Development/Moose/Shapes/Line.lua @@ -0,0 +1,331 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.LINE + +--- OVAL class. +-- @type OVAL +-- @field #string ClassName Name of the class. +-- @field #number Points points of the line +-- @field #number Coords coordinates of the line + +-- +-- === + +-- @field #LINE +LINE = { + ClassName = "LINE", + Points = {}, + Coords = {}, +} + +--- Finds a line on the map by its name. The line must be drawn in the Mission Editor +-- @param #string line_name Name of the line to find +-- @return #LINE The found line, or nil if not found +function LINE:FindOnMap(line_name) + local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(line_name)) + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == line_name then + if object["primitiveType"] == "Line" 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) + table.insert(self.Points, p) + table.insert(self.Coords, coord) + end + end + end + end + end + + self:I(#self.Points) + if #self.Points == 0 then + return nil + end + + self.MarkIDs = {} + + return self +end + +--- Finds a line by its name in the database. +-- @param #string shape_name Name of the line to find +-- @return #LINE The found line, or nil if not found +function LINE:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new line from two points. +-- @param #table vec2 The first point of the line +-- @param #number radius The second point of the line +-- @return #LINE The new line +function LINE:New(...) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.Points = {...} + self:I(self.Points) + for _, point in UTILS.spairs(self.Points) do + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + return self +end + +--- Creates a new line from a circle. +-- @param #table center_point center point of the circle +-- @param #number radius radius of the circle, half length of the line +-- @param #number angle_degrees degrees the line will form from center point +-- @return #LINE The new line +function LINE:NewFromCircle(center_point, radius, angle_degrees) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = center_point + local angleRadians = math.rad(angle_degrees) + + local point1 = { + x = center_point.x + radius * math.cos(angleRadians), + y = center_point.y + radius * math.sin(angleRadians) + } + + local point2 = { + x = center_point.x + radius * math.cos(angleRadians + math.pi), + y = center_point.y + radius * math.sin(angleRadians + math.pi) + } + + for _, point in pairs{point1, point2} do + table.insert(self.Points, point) + table.insert(self.Coords, COORDINATE:NewFromVec2(point)) + end + + return self +end + +--- Gets the coordinates of the line. +-- @return #table The coordinates of the line +function LINE:Coordinates() + return self.Coords +end + +--- Gets the start coordinate of the line. The start coordinate is the first point of the line. +-- @return #COORDINATE The start coordinate of the line +function LINE:GetStartCoordinate() + return self.Coords[1] +end + +--- Gets the end coordinate of the line. The end coordinate is the last point of the line. +-- @return #COORDINATE The end coordinate of the line +function LINE:GetEndCoordinate() + return self.Coords[#self.Coords] +end + +--- Gets the start point of the line. The start point is the first point of the line. +-- @return #table The start point of the line +function LINE:GetStartPoint() + return self.Points[1] +end + +--- Gets the end point of the line. The end point is the last point of the line. +-- @return #table The end point of the line +function LINE:GetEndPoint() + return self.Points[#self.Points] +end + +--- Gets the length of the line. +-- @return #number The length of the line +function LINE:GetLength() + local total_length = 0 + for i=1, #self.Points - 1 do + local x1, y1 = self.Points[i]["x"], self.Points[i]["y"] + local x2, y2 = self.Points[i+1]["x"], self.Points[i+1]["y"] + local segment_length = math.sqrt((x2 - x1)^2 + (y2 - y1)^2) + total_length = total_length + segment_length + end + return total_length +end + +--- Returns a random point on the line. +-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it +-- @return #table The random point +function LINE:GetRandomPoint(points) + points = points or self.Points + local rand = math.random() -- 0->1 + + local random_x = points[1].x + rand * (points[2].x - points[1].x) + local random_y = points[1].y + rand * (points[2].y - points[1].y) + + return { x= random_x, y= random_y } +end + +--- Gets the heading of the line. +-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it +-- @return #number The heading of the line +function LINE:GetHeading(points) + points = points or self.Points + + local angle = math.atan2(points[2].y - points[1].y, points[2].x - points[1].x) + + angle = math.deg(angle) + if angle < 0 then + angle = angle + 360 + end + + return angle +end + + +--- Return each part of the line as a new line +-- @return #table The points +function LINE:GetIndividualParts() + local parts = {} + if #self.Points == 2 then + parts = {self} + end + + for i=1, #self.Points -1 do + local p1 = self.Points[i] + local p2 = self.Points[i % #self.Points + 1] + table.add(parts, LINE:New(p1, p2)) + end + + return parts +end + +--- Gets a number of points in between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @return #table The points +function LINE:GetPointsInbetween(amount, start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + if amount == 0 then return {start_point, end_point} end + + amount = amount + 1 + local points = {} + + local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } + local divided = { x = difference.x / amount, y = difference.y / amount } + + for j=0, amount do + local part_pos = {x = divided.x * j, y = divided.y * j} + -- add part_pos vector to the start point so the new point is placed along in the line + local point = {x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} + table.insert(points, point) + end + return points +end + +--- Gets a number of points in between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @return #table The points +function LINE:GetCoordinatesInBetween(amount, start_point, end_point) + local coords = {} + for _, pt in pairs(self:GetPointsInbetween(amount, start_point, end_point)) do + table.add(coords, COORDINATE:NewFromVec2(pt)) + end + return coords +end + + +function LINE:GetRandomPoint(start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + + local fraction = math.random() + + local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y } + local part_pos = {x = difference.x * fraction, y = difference.y * fraction} + local random_point = { x = start_point.x + part_pos.x, y = start_point.y + part_pos.y} + + return random_point +end + + +function LINE:GetRandomCoordinate(start_point, end_point) + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + + return COORDINATE:NewFromVec2(self:GetRandomPoint(start_point, end_point)) +end + + +--- Gets a number of points on a sine wave between the start and end points of the line. +-- @param #number amount The number of points to get +-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point +-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point +-- @param #number frequency (Optional) The frequency of the sine wave, default 1 +-- @param #number phase (Optional) The phase of the sine wave, default 0 +-- @param #number amplitude (Optional) The amplitude of the sine wave, default 100 +-- @return #table The points +function LINE:GetPointsBetweenAsSineWave(amount, start_point, end_point, frequency, phase, amplitude) + amount = amount or 20 + start_point = start_point or self:GetStartPoint() + end_point = end_point or self:GetEndPoint() + frequency = frequency or 1 -- number of cycles per unit of x + phase = phase or 0 -- offset in radians + amplitude = amplitude or 100 -- maximum height of the wave + + local points = {} + + -- Returns the y-coordinate of the sine wave at x + local function sine_wave(x) + return amplitude * math.sin(2 * math.pi * frequency * (x - start_point.x) + phase) + end + + -- Plot x-amount of points on the sine wave between point_01 and point_02 + local x = start_point.x + local step = (end_point.x - start_point.x) / 20 + for _=1, amount do + local y = sine_wave(x) + x = x + step + table.add(points, {x=x, y=y}) + end + return points +end + +--- Calculates the bounding box of the line. The bounding box is the smallest rectangle that contains the line. +-- @return #table The bounding box of the line +function LINE:GetBoundingBox() + local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[2].x, self.Points[2].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 + +--- Draws the line on the map. +-- @param #table points The points of the line +function LINE:Draw() + for i=1, #self.Coords -1 do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.MarkIDs, c1:LineToAll(c2)) + end +end + +--- Removes the drawing of the line from the map. +function LINE:RemoveDraw() + for _, mark_id in pairs(self.MarkIDs) do + UTILS.RemoveMark(mark_id) + end +end diff --git a/Moose Development/Moose/Shapes/Oval.lua b/Moose Development/Moose/Shapes/Oval.lua new file mode 100644 index 000000000..d2f85a822 --- /dev/null +++ b/Moose Development/Moose/Shapes/Oval.lua @@ -0,0 +1,213 @@ +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.OVAL + +--- OVAL class. +-- @type OVAL +-- @field #string ClassName Name of the class. +-- @field #number MajorAxis The major axis (radius) of the oval +-- @field #number MinorAxis The minor axis (radius) of the oval +-- @field #number Angle The angle the oval is rotated on + +--- *The little man removed his hat, what an egg shaped head he had* -- Agatha Christie +-- +-- === +-- +-- # OVAL +-- OVALs can be fetched from the drawings in the Mission Editor + +-- The major and minor axes define how elongated the shape of an oval is. This class has some basic functions that the other SHAPE classes have as well. +-- Since it's not possible to draw the shape of an oval while the mission is running, right now the draw function draws 2 cicles. One with the major axis and one with +-- the minor axis. It then draws a diamond shape on an angle where the corners touch the major and minor axes to give an indication of what the oval actually +-- looks like. + +-- Using ovals can be handy to find an area on the ground that is actually an intersection of a cone and a plane. So imagine you're faking the view cone of +-- a targeting pod and + +-- @field #OVAL + +--- OVAL class with properties and methods for handling ovals. +OVAL = { + ClassName = "OVAL", + MajorAxis = nil, + MinorAxis = nil, + Angle = 0, + DrawPoly=nil +} + +--- Finds an oval on the map by its name. The oval must be drawn on the map. +-- @param #string shape_name Name of the oval to find +-- @return #OVAL The found oval, or nil if not found +function OVAL: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 string.find(object["name"], shape_name, 1, true) then + if object["polygonMode"] == "oval" then + self.CenterVec2 = { x = object["mapX"], y = object["mapY"] } + self.MajorAxis = object["r1"] + self.MinorAxis = object["r2"] + self.Angle = object["angle"] + end + end + end + end + + return self +end + +--- Finds an oval by its name in the database. +-- @param #string shape_name Name of the oval to find +-- @return #OVAL The found oval, or nil if not found +function OVAL:Find(shape_name) + return _DATABASE:FindShape(shape_name) +end + +--- Creates a new oval from a center point, major axis, minor axis, and angle. +-- @param #table vec2 The center point of the oval +-- @param #number major_axis The major axis of the oval +-- @param #number minor_axis The minor axis of the oval +-- @param #number angle The angle of the oval +-- @return #OVAL The new oval +function OVAL:New(vec2, major_axis, minor_axis, angle) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.CenterVec2 = vec2 + self.MajorAxis = major_axis + self.MinorAxis = minor_axis + self.Angle = angle or 0 + + return self +end + +--- Gets the major axis of the oval. +-- @return #number The major axis of the oval +function OVAL:GetMajorAxis() + return self.MajorAxis +end + +--- Gets the minor axis of the oval. +-- @return #number The minor axis of the oval +function OVAL:GetMinorAxis() + return self.MinorAxis +end + +--- Gets the angle of the oval. +-- @return #number The angle of the oval +function OVAL:GetAngle() + return self.Angle +end + +--- Sets the major axis of the oval. +-- @param #number value The new major axis +function OVAL:SetMajorAxis(value) + self.MajorAxis = value +end + +--- Sets the minor axis of the oval. +-- @param #number value The new minor axis +function OVAL:SetMinorAxis(value) + self.MinorAxis = value +end + +--- Sets the angle of the oval. +-- @param #number value The new angle +function OVAL:SetAngle(value) + self.Angle = value +end + +--- Checks if a point is contained within the oval. +-- @param #table point The point to check +-- @return #bool True if the point is contained, false otherwise +function OVAL:ContainsPoint(point) + local cos, sin = math.cos, math.sin + local dx = point.x - self.CenterVec2.x + local dy = point.y - self.CenterVec2.y + local rx = dx * cos(self.Angle) + dy * sin(self.Angle) + local ry = -dx * sin(self.Angle) + dy * cos(self.Angle) + return rx * rx / (self.MajorAxis * self.MajorAxis) + ry * ry / (self.MinorAxis * self.MinorAxis) <= 1 +end + +--- Returns a random Vec2 within the oval. +-- @return #table The random Vec2 +function OVAL:GetRandomVec2() + local theta = math.rad(self.Angle) + + local random_point = math.sqrt(math.random()) --> uniformly + --local random_point = math.random() --> more clumped around center + local phi = math.random() * 2 * math.pi + local x_c = random_point * math.cos(phi) + local y_c = random_point * math.sin(phi) + local x_e = x_c * self.MajorAxis + local y_e = y_c * self.MinorAxis + local rx = (x_e * math.cos(theta) - y_e * math.sin(theta)) + self.CenterVec2.x + local ry = (x_e * math.sin(theta) + y_e * math.cos(theta)) + self.CenterVec2.y + + return {x=rx, y=ry} +end + +--- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval. +-- @return #table The bounding box of the oval +function OVAL:GetBoundingBox() + local min_x = self.CenterVec2.x - self.MajorAxis + local min_y = self.CenterVec2.y - self.MinorAxis + local max_x = self.CenterVec2.x + self.MajorAxis + local max_y = self.CenterVec2.y + self.MinorAxis + + 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 + +--- Draws the oval on the map, for debugging +-- @param #number angle (Optional) The angle of the oval. If nil will use self.Angle +function OVAL:Draw() + --for pt in pairs(self:PointsOnEdge(20)) do + -- COORDINATE:NewFromVec2(pt) + --end + + self.DrawPoly = POLYGON:NewFromPoints(self:PointsOnEdge(20)) + self.DrawPoly:Draw(true) + + + + + ---- TODO: draw a better shape using line segments + --angle = angle or self.Angle + --local coor = self:GetCenterCoordinate() + -- + --table.add(self.MarkIDs, coor:CircleToAll(self.MajorAxis)) + --table.add(self.MarkIDs, coor:CircleToAll(self.MinorAxis)) + --table.add(self.MarkIDs, coor:LineToAll(coor:Translate(self.MajorAxis, self.Angle))) + -- + --local pt_1 = coor:Translate(self.MajorAxis, self.Angle) + --local pt_2 = coor:Translate(self.MinorAxis, self.Angle - 90) + --local pt_3 = coor:Translate(self.MajorAxis, self.Angle - 180) + --local pt_4 = coor:Translate(self.MinorAxis, self.Angle - 270) + --table.add(self.MarkIDs, pt_1:QuadToAll(pt_2, pt_3, pt_4), -1, {0, 1, 0}, 1, {0, 1, 0}) +end + +--- Removes the drawing of the oval from the map +function OVAL:RemoveDraw() + self.DrawPoly:RemoveDraw() +end + + +function OVAL:PointsOnEdge(num_points) + num_points = num_points or 20 + local points = {} + local dtheta = 2 * math.pi / num_points + + for i = 0, num_points - 1 do + local theta = i * dtheta + local x = self.CenterVec2.x + self.MajorAxis * math.cos(theta) * math.cos(self.Angle) - self.MinorAxis * math.sin(theta) * math.sin(self.Angle) + local y = self.CenterVec2.y + self.MajorAxis * math.cos(theta) * math.sin(self.Angle) + self.MinorAxis * math.sin(theta) * math.cos(self.Angle) + table.insert(points, {x = x, y = y}) + end + + return points +end + + diff --git a/Moose Development/Moose/Shapes/Polygon.lua b/Moose Development/Moose/Shapes/Polygon.lua new file mode 100644 index 000000000..a40256ecf --- /dev/null +++ b/Moose Development/Moose/Shapes/Polygon.lua @@ -0,0 +1,458 @@ +-- +-- +-- ### 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 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Moose Development/Moose/Shapes/ShapeBase.lua b/Moose Development/Moose/Shapes/ShapeBase.lua new file mode 100644 index 000000000..7042a44ad --- /dev/null +++ b/Moose Development/Moose/Shapes/ShapeBase.lua @@ -0,0 +1,216 @@ +--- **Shapes** - Class that serves as the base shapes drawn in the Mission Editor +-- +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- === +-- @module Shapes.SHAPE_BASE +-- @image CORE_Pathline.png + + +--- SHAPE_BASE class. +-- @type SHAPE_BASE +-- @field #string ClassName Name of the class. +-- @field #string Name Name of the shape +-- @field #table CenterVec2 Vec2 of the center of the shape, this will be assigned automatically +-- @field #table Points List of 3D points defining the shape, this will be assigned automatically +-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically +-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically +-- @extends Core.Base#BASE + +--- *I'm in love with the shape of you -- Ed Sheeran +-- +-- === +-- +-- # SHAPE_BASE +-- The class serves as the base class to deal with these shapes using MOOSE. You should never use this class on its own, +-- rather use: +-- CIRCLE +-- LINE +-- OVAL +-- POLYGON +-- TRIANGLE (although this one's a bit special as well) + +-- === +-- The idea is that anything you draw on the map in the Mission Editor can be turned in a shape to work with in MOOSE. +-- This is the base class that all other shape classes are built on. There are some shared functions, most of which are overridden in the derived classes + +-- @field #SHAPE_BASE + + +SHAPE_BASE = { + ClassName = "SHAPE_BASE", + Name = "", + CenterVec2 = nil, + Points = {}, + Coords = {}, + MarkIDs = {}, + ColorString = "", + ColorRGBA = {} +} + +--- Creates a new instance of SHAPE_BASE. +-- @return #SHAPE_BASE The new instance +function SHAPE_BASE:New() + local self = BASE:Inherit(self, BASE:New()) + return self +end + +--- Finds a shape on the map by its name. +-- @param #string shape_name Name of the shape to find +-- @return #SHAPE_BASE The found shape +function SHAPE_BASE:FindOnMap(shape_name) + local self = BASE:Inherit(self, BASE:New()) + + local found = false + + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if object["name"] == shape_name then + self.Name = object["name"] + self.CenterVec2 = { x = object["mapX"], y = object["mapY"] } + self.ColorString = object["colorString"] + self.ColorRGBA = UTILS.HexToRGBA(self.ColorString) + found = true + end + end + end + if not found then + self:E("Can't find a shape with name " .. shape_name) + end + return self +end + +function SHAPE_BASE:GetAllShapes(filter) + filter = filter or "" + local return_shapes = {} + for _, layer in pairs(env.mission.drawings.layers) do + for _, object in pairs(layer["objects"]) do + if string.contains(object["name"], filter) then + table.add(return_shapes, object) + end + end + end + + return return_shapes +end + +--- Offsets the shape to a new position. +-- @param #table new_vec2 The new position +function SHAPE_BASE:Offset(new_vec2) + local offset_vec2 = UTILS.Vec2Subtract(new_vec2, self.CenterVec2) + self.CenterVec2 = new_vec2 + if self.ClassName == "POLYGON" then + for _, point in pairs(self.Points) do + point.x = point.x + offset_vec2.x + point.y = point.y + offset_vec2.y + end + end +end + +--- Gets the name of the shape. +-- @return #string The name of the shape +function SHAPE_BASE:GetName() + return self.Name +end + +function SHAPE_BASE:GetColorString() + return self.ColorString +end + +function SHAPE_BASE:GetColorRGBA() + return self.ColorRGBA +end + +function SHAPE_BASE:GetColorRed() + return self.ColorRGBA.R +end + +function SHAPE_BASE:GetColorGreen() + return self.ColorRGBA.G +end + +function SHAPE_BASE:GetColorBlue() + return self.ColorRGBA.B +end + +function SHAPE_BASE:GetColorAlpha() + return self.ColorRGBA.A +end + +--- Gets the center position of the shape. +-- @return #table The center position +function SHAPE_BASE:GetCenterVec2() + return self.CenterVec2 +end + +--- Gets the center coordinate of the shape. +-- @return #COORDINATE The center coordinate +function SHAPE_BASE:GetCenterCoordinate() + return COORDINATE:NewFromVec2(self.CenterVec2) +end + +--- Gets the coordinate of the shape. +-- @return #COORDINATE The coordinate +function SHAPE_BASE:GetCoordinate() + return self:GetCenterCoordinate() +end + +--- Checks if a point is contained within the shape. +-- @param #table _ The point to check +-- @return #bool True if the point is contained, false otherwise +function SHAPE_BASE:ContainsPoint(_) + self:E("This needs to be set in the derived class") +end + +--- Checks if a unit is contained within the shape. +-- @param #string unit_name The name of the unit to check +-- @return #bool True if the unit is contained, false otherwise +function SHAPE_BASE:ContainsUnit(unit_name) + local unit = UNIT:FindByName(unit_name) + + if unit == nil or not unit:IsAlive() then + return false + end + + if self:ContainsPoint(unit:GetVec2()) then + return true + end + return false +end + +--- Checks if any unit of a group is contained within the shape. +-- @param #string group_name The name of the group to check +-- @return #bool True if any unit of the group is contained, false otherwise +function SHAPE_BASE:ContainsAnyOfGroup(group_name) + local group = GROUP:FindByName(group_name) + + if group == nil or not group:IsAlive() then + return false + end + + for _, unit in pairs(group:GetUnits()) do + if self:ContainsPoint(unit:GetVec2()) then + return true + end + end + return false +end + +--- Checks if all units of a group are contained within the shape. +-- @param #string group_name The name of the group to check +-- @return #bool True if all units of the group are contained, false otherwise +function SHAPE_BASE:ContainsAllOfGroup(group_name) + local group = GROUP:FindByName(group_name) + + if group == nil or not group:IsAlive() then + return false + end + + for _, unit in pairs(group:GetUnits()) do + if not self:ContainsPoint(unit:GetVec2()) then + return false + end + end + return true +end diff --git a/Moose Development/Moose/Shapes/Triangle.lua b/Moose Development/Moose/Shapes/Triangle.lua new file mode 100644 index 000000000..c60b2aeef --- /dev/null +++ b/Moose Development/Moose/Shapes/Triangle.lua @@ -0,0 +1,86 @@ +-- TRIANGLE class with properties and methods for handling triangles. This class is mostly used by the POLYGON class, but you can use it on its own as well +-- +-- ### Author: **nielsvaes/coconutcockpit** +-- +-- +TRIANGLE = { + ClassName = "TRIANGLE", + Points = {}, + Coords = {}, + SurfaceArea = 0 +} + +--- Creates a new triangle from three points. The points need to be given as Vec2s +-- @param #table p1 The first point of the triangle +-- @param #table p2 The second point of the triangle +-- @param #table p3 The third point of the triangle +-- @return #TRIANGLE The new triangle +function TRIANGLE:New(p1, p2, p3) + local self = BASE:Inherit(self, SHAPE_BASE:New()) + self.Points = {p1, p2, p3} + + local center_x = (p1.x + p2.x + p3.x) / 3 + local center_y = (p1.y + p2.y + p3.y) / 3 + self.CenterVec2 = {x=center_x, y=center_y} + + for _, pt in pairs({p1, p2, p3}) do + table.add(self.Coords, COORDINATE:NewFromVec2(pt)) + end + + self.SurfaceArea = math.abs((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)) * 0.5 + + self.MarkIDs = {} + return self +end + +--- Checks if a point is contained within the triangle. +-- @param #table pt The point to check +-- @param #table points (optional) The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it +-- @return #bool True if the point is contained, false otherwise +function TRIANGLE:ContainsPoint(pt, points) + points = points or self.Points + + local function sign(p1, p2, p3) + return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y) + end + + local d1 = sign(pt, self.Points[1], self.Points[2]) + local d2 = sign(pt, self.Points[2], self.Points[3]) + local d3 = sign(pt, self.Points[3], self.Points[1]) + + local has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0) + local has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0) + + return not (has_neg and has_pos) +end + +--- Returns a random Vec2 within the triangle. +-- @param #table points The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it +-- @return #table The random Vec2 +function TRIANGLE:GetRandomVec2(points) + points = points or self.Points + local pt = {math.random(), math.random()} + table.sort(pt) + local s = pt[1] + local t = pt[2] - pt[1] + local u = 1 - pt[2] + + return {x = s * points[1].x + t * points[2].x + u * points[3].x, + y = s * points[1].y + t * points[2].y + u * points[3].y} +end + +--- Draws the triangle on the map, just for debugging +function TRIANGLE:Draw() + for i=1, #self.Coords do + local c1 = self.Coords[i] + local c2 = self.Coords[i % #self.Coords + 1] + table.add(self.MarkIDs, c1:LineToAll(c2)) + end +end + +--- Removes the drawing of the triangle from the map. +function TRIANGLE:RemoveDraw() + for _, mark_id in pairs(self.MarkIDs) do + UTILS.RemoveMark(mark_id) + end +end diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 6bf7e1fc9..96cf8c1b4 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -3513,6 +3513,25 @@ function string.contains(str, value) return string.match(str, value) end + +--- Moves an object from one table to another +-- @param #obj object to move +-- @param #from_table table to move from +-- @param #to_table table to move to +function table.move_object(obj, from_table, to_table) + local index + for i, v in pairs(from_table) do + if v == obj then + index = i + end + end + + if index then + local moved = table.remove(from_table, index) + table.insert_unique(to_table, moved) + end +end + --- Given tbl is a indexed table ({"hello", "dcs", "world"}), checks if element exists in the table. --- The table can be made up out of complex tables or values as well -- @param #table tbl @@ -3731,6 +3750,25 @@ function UTILS.OctalToDecimal(Number) return tonumber(Number,8) end + +--- HexToRGBA +-- @param hex_string table +-- @return #table R, G, B, A +function UTILS.HexToRGBA(hex_string) + local hexNumber = tonumber(string.sub(hex_string, 3), 16) -- convert the string to a number + -- extract RGBA components + local alpha = hexNumber % 256 + hexNumber = (hexNumber - alpha) / 256 + local blue = hexNumber % 256 + hexNumber = (hexNumber - blue) / 256 + local green = hexNumber % 256 + hexNumber = (hexNumber - green) / 256 + local red = hexNumber % 256 + + return {R = red, G = green, B = blue, A = alpha} +end + + --- Function to save the position of a set of #OPSGROUP (ARMYGROUP) objects. -- @param Core.Set#SET_OPSGROUP Set of ops objects to save -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. @@ -3768,7 +3806,7 @@ function UTILS.SaveSetOfOpsGroups(Set,Path,Filename,Structured) data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d,%s\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata) else data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z) - end + end end end -- save the data @@ -3780,12 +3818,12 @@ end -- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems. -- @param #string Filename The name of the file. -- @return #table Returns a table of data entries: `{ groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure, legion=legion, alttemplate=alttemplate }` --- Returns nil when the file cannot be read. +-- Returns nil when the file cannot be read. function UTILS.LoadSetOfOpsGroups(Path,Filename) local filename = Filename or "SetOfGroups" local datatable = {} - + if UTILS.CheckFileExists(Path,filename) then local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename) -- remove header @@ -3820,20 +3858,20 @@ end -- @param #number tgtHdg The absolute heading from the reference object to the target object/point in 0-360 -- @return #string text Text in clock heading such as "4 O'CLOCK" -- @usage Display the range and clock distance of a BTR in relation to REAPER 1-1's heading: --- +-- -- myUnit = UNIT:FindByName( "REAPER 1-1" ) -- myTarget = GROUP:FindByName( "BTR-1" ) --- +-- -- coordUnit = myUnit:GetCoordinate() -- coordTarget = myTarget:GetCoordinate() --- +-- -- hdgUnit = myUnit:GetHeading() -- hdgTarget = coordUnit:HeadingTo( coordTarget ) -- distTarget = coordUnit:Get3DDistance( coordTarget ) --- +-- -- clockString = UTILS.ClockHeadingString( hdgUnit, hdgTarget ) --- --- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! +-- +-- -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m! -- MESSAGE:New("Contact BTR at " .. clockString .. " for " .. distTarget .. "m!):ToUnit( myUnit ) function UTILS.ClockHeadingString(refHdg,tgtHdg) local relativeAngle = tgtHdg - refHdg