--[[ License A math library made in Lua copyright (C) 2014 Davis Claiborne This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Contact me at davisclaib@gmail.com ]] -- Local Utility Functions ---------------------- {{{ local unpack = table.unpack or unpack -- Used to handle variable-argument functions and whether they are passed as func{ table } or func( unpack( table ) ) local function checkInput( ... ) local input = {} if type( ... ) ~= 'table' then input = { ... } else input = ... end return input end -- Deals with floats / verify false false values. This can happen because of significant figures. local function checkFuzzy( number1, number2 ) return ( number1 - .00001 <= number2 and number2 <= number1 + .00001 ) end -- Remove multiple occurrences from a table. local function removeDuplicatePairs( tab ) for index1 = #tab, 1, -1 do local first = tab[index1] for index2 = #tab, 1, -1 do local second = tab[index2] if index1 ~= index2 then if type( first[1] ) == 'number' and type( second[1] ) == 'number' and type( first[2] ) == 'number' and type( second[2] ) == 'number' then if checkFuzzy( first[1], second[1] ) and checkFuzzy( first[2], second[2] ) then table.remove( tab, index1 ) end elseif first[1] == second[1] and first[2] == second[2] then table.remove( tab, index1 ) end end end end return tab end local function removeDuplicates4Points( tab ) for index1 = #tab, 1, -1 do local first = tab[index1] for index2 = #tab, 1, -1 do local second = tab[index2] if index1 ~= index2 then if type( first[1] ) ~= type( second[1] ) then return false end if type( first[2] ) == 'number' and type( second[2] ) == 'number' and type( first[3] ) == 'number' and type( second[3] ) == 'number' then if checkFuzzy( first[2], second[2] ) and checkFuzzy( first[3], second[3] ) then table.remove( tab, index1 ) end elseif checkFuzzy( first[1], second[1] ) and checkFuzzy( first[2], second[2] ) and checkFuzzy( first[3], second[3] ) then table.remove( tab, index1 ) end end end end return tab end -- Add points to the table. local function addPoints( tab, x, y ) tab[#tab + 1] = x tab[#tab + 1] = y end -- Like removeDuplicatePairs but specifically for numbers in a flat table local function removeDuplicatePointsFlat( tab ) for i = #tab, 1 -2 do for ii = #tab - 2, 3, -2 do if i ~= ii then local x1, y1 = tab[i], tab[i + 1] local x2, y2 = tab[ii], tab[ii + 1] if checkFuzzy( x1, x2 ) and checkFuzzy( y1, y2 ) then table.remove( tab, ii ); table.remove( tab, ii + 1 ) end end end end return tab end -- Check if input is actually a number local function validateNumber( n ) if type( n ) ~= 'number' then return false elseif n ~= n then return false -- nan elseif math.abs( n ) == math.huge then return false else return true end end local function cycle( tab, index ) return tab[( index - 1 ) % #tab + 1] end local function getGreatestPoint( points, offset ) offset = offset or 1 local start = 2 - offset local greatest = points[start] local least = points[start] for i = 2, #points / 2 do i = i * 2 - offset if points[i] > greatest then greatest = points[i] end if points[i] < least then least = points[i] end end return greatest, least end local function isWithinBounds( min, num, max ) return num >= min and num <= max end local function distance2( x1, y1, x2, y2 ) -- Faster since it does not use math.sqrt local dx, dy = x1 - x2, y1 - y2 return dx * dx + dy * dy end -- }}} -- Points -------------------------------------- {{{ local function rotatePoint( x, y, rotation, ox, oy ) ox, oy = ox or 0, oy or 0 return ( x - ox ) * math.cos( rotation ) + ox - ( y - oy ) * math.sin( rotation ), ( x - ox ) * math.sin( rotation ) + ( y - oy ) * math.cos( rotation ) + oy end local function scalePoint( x, y, scale, ox, oy ) ox, oy = ox or 0, oy or 0 return ( x - ox ) * scale + ox, ( y - oy ) * scale + oy end -- }}} -- Lines --------------------------------------- {{{ -- Returns the length of a line. local function getLength( x1, y1, x2, y2 ) local dx, dy = x1 - x2, y1 - y2 return math.sqrt( dx * dx + dy * dy ) end -- Gives the midpoint of a line. local function getMidpoint( x1, y1, x2, y2 ) return ( x1 + x2 ) / 2, ( y1 + y2 ) / 2 end -- Gives the slope of a line. local function getSlope( x1, y1, x2, y2 ) if checkFuzzy( x1, x2 ) then return false end -- Technically it's undefined, but this is easier to program. return ( y1 - y2 ) / ( x1 - x2 ) end -- Gives the perpendicular slope of a line. -- x1, y1, x2, y2 -- slope local function getPerpendicularSlope( ... ) local input = checkInput( ... ) local slope if #input ~= 1 then slope = getSlope( unpack( input ) ) else slope = unpack( input ) end if not slope then return 0 -- Vertical lines become horizontal. elseif checkFuzzy( slope, 0 ) then return false -- Horizontal lines become vertical. else return -1 / slope end end -- Gives the y-intercept of a line. -- x1, y1, x2, y2 -- x1, y1, slope local function getYIntercept( x, y, ... ) local input = checkInput( ... ) local slope if #input == 1 then slope = input[1] else slope = getSlope( x, y, unpack( input ) ) end if not slope then return x, true end -- This way we have some information on the line. return y - slope * x, false end -- Gives the intersection of two lines. -- slope1, slope2, x1, y1, x2, y2 -- slope1, intercept1, slope2, intercept2 -- x1, y1, x2, y2, x3, y3, x4, y4 local function getLineLineIntersection( ... ) local input = checkInput( ... ) local x1, y1, x2, y2, x3, y3, x4, y4 local slope1, intercept1 local slope2, intercept2 local x, y if #input == 4 then -- Given slope1, intercept1, slope2, intercept2. slope1, intercept1, slope2, intercept2 = unpack( input ) -- Since these are lines, not segments, we can use arbitrary points, such as ( 1, y ), ( 2, y ) y1 = slope1 and slope1 * 1 + intercept1 or 1 y2 = slope1 and slope1 * 2 + intercept1 or 2 y3 = slope2 and slope2 * 1 + intercept2 or 1 y4 = slope2 and slope2 * 2 + intercept2 or 2 x1 = slope1 and ( y1 - intercept1 ) / slope1 or intercept1 x2 = slope1 and ( y2 - intercept1 ) / slope1 or intercept1 x3 = slope2 and ( y3 - intercept2 ) / slope2 or intercept2 x4 = slope2 and ( y4 - intercept2 ) / slope2 or intercept2 elseif #input == 6 then -- Given slope1, intercept1, and 2 points on the other line. slope1, intercept1 = input[1], input[2] slope2 = getSlope( input[3], input[4], input[5], input[6] ) intercept2 = getYIntercept( input[3], input[4], input[5], input[6] ) y1 = slope1 and slope1 * 1 + intercept1 or 1 y2 = slope1 and slope1 * 2 + intercept1 or 2 y3 = input[4] y4 = input[6] x1 = slope1 and ( y1 - intercept1 ) / slope1 or intercept1 x2 = slope1 and ( y2 - intercept1 ) / slope1 or intercept1 x3 = input[3] x4 = input[5] elseif #input == 8 then -- Given 2 points on line 1 and 2 points on line 2. slope1 = getSlope( input[1], input[2], input[3], input[4] ) intercept1 = getYIntercept( input[1], input[2], input[3], input[4] ) slope2 = getSlope( input[5], input[6], input[7], input[8] ) intercept2 = getYIntercept( input[5], input[6], input[7], input[8] ) x1, y1, x2, y2, x3, y3, x4, y4 = unpack( input ) end if not slope1 and not slope2 then -- Both are vertical lines if x1 == x3 then -- Have to have the same x positions to intersect return true else return false end elseif not slope1 then -- First is vertical x = x1 -- They have to meet at this x, since it is this line's only x y = slope2 and slope2 * x + intercept2 or 1 elseif not slope2 then -- Second is vertical x = x3 -- Vice-Versa y = slope1 * x + intercept1 elseif checkFuzzy( slope1, slope2 ) then -- Parallel (not vertical) if checkFuzzy( intercept1, intercept2 ) then -- Same intercept return true else return false end else -- Regular lines x = ( -intercept1 + intercept2 ) / ( slope1 - slope2 ) y = slope1 * x + intercept1 end return x, y end -- Gives the closest point on a line to a point. -- perpendicularX, perpendicularY, x1, y1, x2, y2 -- perpendicularX, perpendicularY, slope, intercept local function getClosestPoint( perpendicularX, perpendicularY, ... ) local input = checkInput( ... ) local x, y, x1, y1, x2, y2, slope, intercept if #input == 4 then -- Given perpendicularX, perpendicularY, x1, y1, x2, y2 x1, y1, x2, y2 = unpack( input ) slope = getSlope( x1, y1, x2, y2 ) intercept = getYIntercept( x1, y1, x2, y2 ) elseif #input == 2 then -- Given perpendicularX, perpendicularY, slope, intercept slope, intercept = unpack( input ) x1, y1 = 1, slope and slope * 1 + intercept or 1 -- Need x1 and y1 in case of vertical/horizontal lines. end if not slope then -- Vertical line x, y = x1, perpendicularY -- Closest point is always perpendicular. elseif checkFuzzy( slope, 0 ) then -- Horizontal line x, y = perpendicularX, y1 else local perpendicularSlope = getPerpendicularSlope( slope ) local perpendicularIntercept = getYIntercept( perpendicularX, perpendicularY, perpendicularSlope ) x, y = getLineLineIntersection( slope, intercept, perpendicularSlope, perpendicularIntercept ) end return x, y end -- Gives the intersection of a line and a line segment. -- x1, y1, x2, y2, x3, y3, x4, y4 -- x1, y1, x2, y2, slope, intercept local function getLineSegmentIntersection( x1, y1, x2, y2, ... ) local input = checkInput( ... ) local slope1, intercept1, x, y, lineX1, lineY1, lineX2, lineY2 local slope2, intercept2 = getSlope( x1, y1, x2, y2 ), getYIntercept( x1, y1, x2, y2 ) if #input == 2 then -- Given slope, intercept slope1, intercept1 = input[1], input[2] lineX1, lineY1 = 1, slope1 and slope1 + intercept1 lineX2, lineY2 = 2, slope1 and slope1 * 2 + intercept1 else -- Given x3, y3, x4, y4 lineX1, lineY1, lineX2, lineY2 = unpack( input ) slope1 = getSlope( unpack( input ) ) intercept1 = getYIntercept( unpack( input ) ) end if not slope1 and not slope2 then -- Vertical lines if checkFuzzy( x1, lineX1 ) then return x1, y1, x2, y2 else return false end elseif not slope1 then -- slope1 is vertical x, y = input[1], slope2 * input[1] + intercept2 elseif not slope2 then -- slope2 is vertical x, y = x1, slope1 * x1 + intercept1 else x, y = getLineLineIntersection( slope1, intercept1, slope2, intercept2 ) end local length1, length2, distance if x == true then -- Lines are collinear. return x1, y1, x2, y2 elseif x then -- There is an intersection length1, length2 = getLength( x1, y1, x, y ), getLength( x2, y2, x, y ) distance = getLength( x1, y1, x2, y2 ) else -- Lines are parallel but not collinear. if checkFuzzy( intercept1, intercept2 ) then return x1, y1, x2, y2 else return false end end if length1 <= distance and length2 <= distance then return x, y else return false end end -- Checks if a point is on a line. -- Does not support the format using slope because vertical lines would be impossible to check. local function checkLinePoint( x, y, x1, y1, x2, y2 ) local m = getSlope( x1, y1, x2, y2 ) local b = getYIntercept( x1, y1, m ) if not m then -- Vertical return checkFuzzy( x, x1 ) end return checkFuzzy( y, m * x + b ) end -- }}} -- Segment -------------------------------------- {{{ -- Gives the perpendicular bisector of a line. local function getPerpendicularBisector( x1, y1, x2, y2 ) local slope = getSlope( x1, y1, x2, y2 ) local midpointX, midpointY = getMidpoint( x1, y1, x2, y2 ) return midpointX, midpointY, getPerpendicularSlope( slope ) end -- Gives whether or not a point lies on a line segment. local function checkSegmentPoint( px, py, x1, y1, x2, y2 ) -- Explanation around 5:20: https://www.youtube.com/watch?v=A86COO8KC58 local x = checkLinePoint( px, py, x1, y1, x2, y2 ) if not x then return false end local lengthX = x2 - x1 local lengthY = y2 - y1 if checkFuzzy( lengthX, 0 ) then -- Vertical line if checkFuzzy( px, x1 ) then local low, high if y1 > y2 then low = y2; high = y1 else low = y1; high = y2 end if py >= low and py <= high then return true else return false end else return false end elseif checkFuzzy( lengthY, 0 ) then -- Horizontal line if checkFuzzy( py, y1 ) then local low, high if x1 > x2 then low = x2; high = x1 else low = x1; high = x2 end if px >= low and px <= high then return true else return false end else return false end end local distanceToPointX = ( px - x1 ) local distanceToPointY = ( py - y1 ) local scaleX = distanceToPointX / lengthX local scaleY = distanceToPointY / lengthY if ( scaleX >= 0 and scaleX <= 1 ) and ( scaleY >= 0 and scaleY <= 1 ) then -- Intersection return true end return false end -- Gives the point of intersection between two line segments. local function getSegmentSegmentIntersection( x1, y1, x2, y2, x3, y3, x4, y4 ) local slope1, intercept1 = getSlope( x1, y1, x2, y2 ), getYIntercept( x1, y1, x2, y2 ) local slope2, intercept2 = getSlope( x3, y3, x4, y4 ), getYIntercept( x3, y3, x4, y4 ) if ( ( slope1 and slope2 ) and checkFuzzy( slope1, slope2 ) ) or ( not slope1 and not slope2 ) then -- Parallel lines if checkFuzzy( intercept1, intercept2 ) then -- The same lines, possibly in different points. local points = {} if checkSegmentPoint( x1, y1, x3, y3, x4, y4 ) then addPoints( points, x1, y1 ) end if checkSegmentPoint( x2, y2, x3, y3, x4, y4 ) then addPoints( points, x2, y2 ) end if checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) then addPoints( points, x3, y3 ) end if checkSegmentPoint( x4, y4, x1, y1, x2, y2 ) then addPoints( points, x4, y4 ) end points = removeDuplicatePointsFlat( points ) if #points == 0 then return false end return unpack( points ) else return false end end local x, y = getLineLineIntersection( x1, y1, x2, y2, x3, y3, x4, y4 ) if x and checkSegmentPoint( x, y, x1, y1, x2, y2 ) and checkSegmentPoint( x, y, x3, y3, x4, y4 ) then return x, y end return false end -- }}} -- Math ----------------------------------------- {{{ -- Get the root of a number (i.e. the 2nd (square) root of 4 is 2) local function getRoot( number, root ) return number ^ ( 1 / root ) end -- Checks if a number is prime. local function isPrime( number ) if number < 2 then return false end for i = 2, math.sqrt( number ) do if number % i == 0 then return false end end return true end -- Rounds a number to the xth decimal place (round( 3.14159265359, 4 ) --> 3.1416) local function round( number, place ) local pow = 10 ^ ( place or 0 ) return math.floor( number * pow + .5 ) / pow end -- Gives the summation given a local function local function getSummation( start, stop, func ) local returnValues = {} local sum = 0 for i = start, stop do local value = func( i, returnValues ) returnValues[i] = value sum = sum + value end return sum end -- Gives the percent of change. local function getPercentOfChange( old, new ) if old == 0 and new == 0 then return 0 else return ( new - old ) / math.abs( old ) end end -- Gives the percentage of a number. local function getPercentage( percent, number ) return percent * number end -- Returns the quadratic roots of an equation. local function getQuadraticRoots( a, b, c ) local discriminant = b ^ 2 - ( 4 * a * c ) if discriminant < 0 then return false end discriminant = math.sqrt( discriminant ) local denominator = ( 2 * a ) return ( -b - discriminant ) / denominator, ( -b + discriminant ) / denominator end -- Gives the angle between three points. local function getAngle( x1, y1, x2, y2, x3, y3 ) local a = getLength( x3, y3, x2, y2 ) local b = getLength( x1, y1, x2, y2 ) local c = getLength( x1, y1, x3, y3 ) return math.acos( ( a * a + b * b - c * c ) / ( 2 * a * b ) ) end -- }}} -- Circle --------------------------------------- {{{ -- Gives the area of the circle. local function getCircleArea( radius ) return math.pi * ( radius * radius ) end -- Checks if a point is within the radius of a circle. local function checkCirclePoint( x, y, circleX, circleY, radius ) return getLength( circleX, circleY, x, y ) <= radius end -- Checks if a point is on a circle. local function isPointOnCircle( x, y, circleX, circleY, radius ) return checkFuzzy( getLength( circleX, circleY, x, y ), radius ) end -- Gives the circumference of a circle. local function getCircumference( radius ) return 2 * math.pi * radius end -- Gives the intersection of a line and a circle. local function getCircleLineIntersection( circleX, circleY, radius, x1, y1, x2, y2 ) slope = getSlope( x1, y1, x2, y2 ) intercept = getYIntercept( x1, y1, slope ) if slope then local a = ( 1 + slope ^ 2 ) local b = ( -2 * ( circleX ) + ( 2 * slope * intercept ) - ( 2 * circleY * slope ) ) local c = ( circleX ^ 2 + intercept ^ 2 - 2 * ( circleY ) * ( intercept ) + circleY ^ 2 - radius ^ 2 ) x1, x2 = getQuadraticRoots( a, b, c ) if not x1 then return false end y1 = slope * x1 + intercept y2 = slope * x2 + intercept if checkFuzzy( x1, x2 ) and checkFuzzy( y1, y2 ) then return 'tangent', x1, y1 else return 'secant', x1, y1, x2, y2 end else -- Vertical Lines local lengthToPoint1 = circleX - x1 local remainingDistance = lengthToPoint1 - radius local intercept = math.sqrt( -( lengthToPoint1 ^ 2 - radius ^ 2 ) ) if -( lengthToPoint1 ^ 2 - radius ^ 2 ) < 0 then return false end local bottomX, bottomY = x1, circleY - intercept local topX, topY = x1, circleY + intercept if topY ~= bottomY then return 'secant', topX, topY, bottomX, bottomY else return 'tangent', topX, topY end end end -- Gives the type of intersection of a line segment. local function getCircleSegmentIntersection( circleX, circleY, radius, x1, y1, x2, y2 ) local Type, x3, y3, x4, y4 = getCircleLineIntersection( circleX, circleY, radius, x1, y1, x2, y2 ) if not Type then return false end local slope, intercept = getSlope( x1, y1, x2, y2 ), getYIntercept( x1, y1, x2, y2 ) if isPointOnCircle( x1, y1, circleX, circleY, radius ) and isPointOnCircle( x2, y2, circleX, circleY, radius ) then -- Both points are on line-segment. return 'chord', x1, y1, x2, y2 end if slope then if checkCirclePoint( x1, y1, circleX, circleY, radius ) and checkCirclePoint( x2, y2, circleX, circleY, radius ) then -- Line-segment is fully in circle. return 'enclosed', x1, y1, x2, y2 elseif x3 and x4 then if checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) and not checkSegmentPoint( x4, y4, x1, y1, x2, y2 ) then -- Only the first of the points is on the line-segment. return 'tangent', x3, y3 elseif checkSegmentPoint( x4, y4, x1, y1, x2, y2 ) and not checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) then -- Only the second of the points is on the line-segment. return 'tangent', x4, y4 else -- Neither of the points are on the circle (means that the segment is not on the circle, but "encasing" the circle) if checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) and checkSegmentPoint( x4, y4, x1, y1, x2, y2 ) then return 'secant', x3, y3, x4, y4 else return false end end elseif not x4 then -- Is a tangent. if checkSegmentPoint( x3, y3, x1, y1, x2, y2 ) then return 'tangent', x3, y3 else -- Neither of the points are on the line-segment (means that the segment is not on the circle or "encasing" the circle). local length = getLength( x1, y1, x2, y2 ) local distance1 = getLength( x1, y1, x3, y3 ) local distance2 = getLength( x2, y2, x3, y3 ) if length > distance1 or length > distance2 then return false elseif length < distance1 and length < distance2 then return false else return 'tangent', x3, y3 end end end else local lengthToPoint1 = circleX - x1 local remainingDistance = lengthToPoint1 - radius local intercept = math.sqrt( -( lengthToPoint1 ^ 2 - radius ^ 2 ) ) if -( lengthToPoint1 ^ 2 - radius ^ 2 ) < 0 then return false end local topX, topY = x1, circleY - intercept local bottomX, bottomY = x1, circleY + intercept local length = getLength( x1, y1, x2, y2 ) local distance1 = getLength( x1, y1, topX, topY ) local distance2 = getLength( x2, y2, topX, topY ) if bottomY ~= topY then -- Not a tangent if checkSegmentPoint( topX, topY, x1, y1, x2, y2 ) and checkSegmentPoint( bottomX, bottomY, x1, y1, x2, y2 ) then return 'chord', topX, topY, bottomX, bottomY elseif checkSegmentPoint( topX, topY, x1, y1, x2, y2 ) then return 'tangent', topX, topY elseif checkSegmentPoint( bottomX, bottomY, x1, y1, x2, y2 ) then return 'tangent', bottomX, bottomY else return false end else -- Tangent if checkSegmentPoint( topX, topY, x1, y1, x2, y2 ) then return 'tangent', topX, topY else return false end end end end -- Checks if one circle intersects another circle. local function getCircleCircleIntersection( circle1x, circle1y, radius1, circle2x, circle2y, radius2 ) local length = getLength( circle1x, circle1y, circle2x, circle2y ) if length > radius1 + radius2 then return false end -- If the distance is greater than the two radii, they can't intersect. if checkFuzzy( length, 0 ) and checkFuzzy( radius1, radius2 ) then return 'equal' end if checkFuzzy( circle1x, circle2x ) and checkFuzzy( circle1y, circle2y ) then return 'collinear' end local a = ( radius1 * radius1 - radius2 * radius2 + length * length ) / ( 2 * length ) local h = math.sqrt( radius1 * radius1 - a * a ) local p2x = circle1x + a * ( circle2x - circle1x ) / length local p2y = circle1y + a * ( circle2y - circle1y ) / length local p3x = p2x + h * ( circle2y - circle1y ) / length local p3y = p2y - h * ( circle2x - circle1x ) / length local p4x = p2x - h * ( circle2y - circle1y ) / length local p4y = p2y + h * ( circle2x - circle1x ) / length if not validateNumber( p3x ) or not validateNumber( p3y ) or not validateNumber( p4x ) or not validateNumber( p4y ) then return 'inside' end if checkFuzzy( length, radius1 + radius2 ) or checkFuzzy( length, math.abs( radius1 - radius2 ) ) then return 'tangent', p3x, p3y end return 'intersection', p3x, p3y, p4x, p4y end -- Checks if circle1 is entirely inside of circle2. local function isCircleCompletelyInsideCircle( circle1x, circle1y, circle1radius, circle2x, circle2y, circle2radius ) if not checkCirclePoint( circle1x, circle1y, circle2x, circle2y, circle2radius ) then return false end local Type = getCircleCircleIntersection( circle2x, circle2y, circle2radius, circle1x, circle1y, circle1radius ) if ( Type ~= 'tangent' and Type ~= 'collinear' and Type ~= 'inside' ) then return false end return true end -- Checks if a line-segment is entirely within a circle. local function isSegmentCompletelyInsideCircle( circleX, circleY, circleRadius, x1, y1, x2, y2 ) local Type = getCircleSegmentIntersection( circleX, circleY, circleRadius, x1, y1, x2, y2 ) return Type == 'enclosed' end -- }}} -- Polygon -------------------------------------- {{{ -- Gives the signed area. -- If the points are clockwise the number is negative, otherwise, it's positive. local function getSignedPolygonArea( ... ) local points = checkInput( ... ) -- Shoelace formula (https://en.wikipedia.org/wiki/Shoelace_formula). points[#points + 1] = points[1] points[#points + 1] = points[2] return ( .5 * getSummation( 1, #points / 2, function( index ) index = index * 2 - 1 -- Convert it to work properly. return ( ( points[index] * cycle( points, index + 3 ) ) - ( cycle( points, index + 2 ) * points[index + 1] ) ) end ) ) end -- Simply returns the area of the polygon. local function getPolygonArea( ... ) return math.abs( getSignedPolygonArea( ... ) ) end -- Gives the height of a triangle, given the base. -- base, x1, y1, x2, y2, x3, y3, x4, y4 -- base, area local function getTriangleHeight( base, ... ) local input = checkInput( ... ) local area if #input == 1 then area = input[1] -- Given area. else area = getPolygonArea( input ) end -- Given coordinates. return ( 2 * area ) / base, area end -- Gives the centroid of the polygon. local function getCentroid( ... ) local points = checkInput( ... ) points[#points + 1] = points[1] points[#points + 1] = points[2] local area = getSignedPolygonArea( points ) -- Needs to be signed here in case points are counter-clockwise. -- This formula: https://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon local centroidX = ( 1 / ( 6 * area ) ) * ( getSummation( 1, #points / 2, function( index ) index = index * 2 - 1 -- Convert it to work properly. return ( ( points[index] + cycle( points, index + 2 ) ) * ( ( points[index] * cycle( points, index + 3 ) ) - ( cycle( points, index + 2 ) * points[index + 1] ) ) ) end ) ) local centroidY = ( 1 / ( 6 * area ) ) * ( getSummation( 1, #points / 2, function( index ) index = index * 2 - 1 -- Convert it to work properly. return ( ( points[index + 1] + cycle( points, index + 3 ) ) * ( ( points[index] * cycle( points, index + 3 ) ) - ( cycle( points, index + 2 ) * points[index + 1] ) ) ) end ) ) return centroidX, centroidY end -- Returns whether or not a line intersects a polygon. -- x1, y1, x2, y2, polygonPoints local function getPolygonLineIntersection( x1, y1, x2, y2, ... ) local input = checkInput( ... ) local choices = {} local slope = getSlope( x1, y1, x2, y2 ) local intercept = getYIntercept( x1, y1, slope ) local x3, y3, x4, y4 if slope then x3, x4 = 1, 2 y3, y4 = slope * x3 + intercept, slope * x4 + intercept else x3, x4 = x1, x1 y3, y4 = y1, y2 end for i = 1, #input, 2 do local x1, y1, x2, y2 = getLineSegmentIntersection( input[i], input[i + 1], cycle( input, i + 2 ), cycle( input, i + 3 ), x3, y3, x4, y4 ) if x1 and not x2 then choices[#choices + 1] = { x1, y1 } elseif x1 and x2 then choices[#choices + 1] = { x1, y1, x2, y2 } end -- No need to check 2-point sets since they only intersect each poly line once. end local final = removeDuplicatePairs( choices ) return #final > 0 and final or false end -- Returns if the line segment intersects the polygon. -- x1, y1, x2, y2, polygonPoints local function getPolygonSegmentIntersection( x1, y1, x2, y2, ... ) local input = checkInput( ... ) local choices = {} for i = 1, #input, 2 do local x1, y1, x2, y2 = getSegmentSegmentIntersection( input[i], input[i + 1], cycle( input, i + 2 ), cycle( input, i + 3 ), x1, y1, x2, y2 ) if x1 and not x2 then choices[#choices + 1] = { x1, y1 } elseif x2 then choices[#choices + 1] = { x1, y1, x2, y2 } end end local final = removeDuplicatePairs( choices ) return #final > 0 and final or false end -- Checks if the point lies INSIDE the polygon not on the polygon. local function checkPolygonPoint( px, py, ... ) local points = { unpack( checkInput( ... ) ) } -- Make a new table, as to not edit values of previous. local greatest, least = getGreatestPoint( points, 0 ) if not isWithinBounds( least, py, greatest ) then return false end greatest, least = getGreatestPoint( points ) if not isWithinBounds( least, px, greatest ) then return false end local count = 0 for i = 1, #points, 2 do if checkFuzzy( points[i + 1], py ) then points[i + 1] = py + .001 -- Handles vertices that lie on the point. -- Not exactly mathematically correct, but a lot easier. end if points[i + 3] and checkFuzzy( points[i + 3], py ) then points[i + 3] = py + .001 -- Do not need to worry about alternate case, since points[2] has already been done. end local x1, y1 = points[i], points[i + 1] local x2, y2 = points[i + 2] or points[1], points[i + 3] or points[2] if getSegmentSegmentIntersection( px, py, greatest, py, x1, y1, x2, y2 ) then count = count + 1 end end return count and count % 2 ~= 0 end -- Returns if the line segment is fully or partially inside. -- x1, y1, x2, y2, polygonPoints local function isSegmentInsidePolygon( x1, y1, x2, y2, ... ) local input = checkInput( ... ) local choices = getPolygonSegmentIntersection( x1, y1, x2, y2, input ) -- If it's partially enclosed that's all we need. if choices then return true end if checkPolygonPoint( x1, y1, input ) or checkPolygonPoint( x2, y2, input ) then return true end return false end -- Returns whether two polygons intersect. local function getPolygonPolygonIntersection( polygon1, polygon2 ) local choices = {} for index1 = 1, #polygon1, 2 do local intersections = getPolygonSegmentIntersection( polygon1[index1], polygon1[index1 + 1], cycle( polygon1, index1 + 2 ), cycle( polygon1, index1 + 3 ), polygon2 ) if intersections then for index2 = 1, #intersections do choices[#choices + 1] = intersections[index2] end end end for index1 = 1, #polygon2, 2 do local intersections = getPolygonSegmentIntersection( polygon2[index1], polygon2[index1 + 1], cycle( polygon2, index1 + 2 ), cycle( polygon2, index1 + 3 ), polygon1 ) if intersections then for index2 = 1, #intersections do choices[#choices + 1] = intersections[index2] end end end choices = removeDuplicatePairs( choices ) for i = #choices, 1, -1 do if type( choices[i][1] ) == 'table' then -- Remove co-linear pairs. table.remove( choices, i ) end end return #choices > 0 and choices end -- Returns whether the circle intersects the polygon. -- x, y, radius, polygonPoints local function getPolygonCircleIntersection( x, y, radius, ... ) local input = checkInput( ... ) local choices = {} for i = 1, #input, 2 do local Type, x1, y1, x2, y2 = getCircleSegmentIntersection( x, y, radius, input[i], input[i + 1], cycle( input, i + 2 ), cycle( input, i + 3 ) ) if x2 then choices[#choices + 1] = { Type, x1, y1, x2, y2 } elseif x1 then choices[#choices + 1] = { Type, x1, y1 } end end local final = removeDuplicates4Points( choices ) return #final > 0 and final end -- Returns whether the circle is inside the polygon. -- x, y, radius, polygonPoints local function isCircleInsidePolygon( x, y, radius, ... ) local input = checkInput( ... ) return checkPolygonPoint( x, y, input ) end -- Returns whether the polygon is inside the polygon. local function isPolygonInsidePolygon( polygon1, polygon2 ) local bool = false for i = 1, #polygon2, 2 do local result = false result = isSegmentInsidePolygon( polygon2[i], polygon2[i + 1], cycle( polygon2, i + 2 ), cycle( polygon2, i + 3 ), polygon1 ) if result then bool = true; break end end return bool end -- Checks if a segment is completely inside a polygon local function isSegmentCompletelyInsidePolygon( x1, y1, x2, y2, ... ) local polygon = checkInput( ... ) if not checkPolygonPoint( x1, y1, polygon ) or not checkPolygonPoint( x2, y2, polygon ) or getPolygonSegmentIntersection( x1, y1, x2, y2, polygon ) then return false end return true end -- Checks if a polygon is completely inside another polygon local function isPolygonCompletelyInsidePolygon( polygon1, polygon2 ) for i = 1, #polygon1, 2 do local x1, y1 = polygon1[i], polygon1[i + 1] local x2, y2 = polygon1[i + 2] or polygon1[1], polygon1[i + 3] or polygon1[2] if not isSegmentCompletelyInsidePolygon( x1, y1, x2, y2, polygon2 ) then return false end end return true end -------------- Circle w/ Polygons -------------- -- Gets if a polygon is completely within a circle -- circleX, circleY, circleRadius, polygonPoints local function isPolygonCompletelyInsideCircle( circleX, circleY, circleRadius, ... ) local input = checkInput( ... ) local function isDistanceLess( px, py, x, y, circleRadius ) -- Faster, does not use math.sqrt local distanceX, distanceY = px - x, py - y return distanceX * distanceX + distanceY * distanceY < circleRadius * circleRadius -- Faster. For comparing distances only. end for i = 1, #input, 2 do if not checkCirclePoint( input[i], input[i + 1], circleX, circleY, circleRadius ) then return false end end return true end -- Checks if a circle is completely within a polygon -- circleX, circleY, circleRadius, polygonPoints local function isCircleCompletelyInsidePolygon( circleX, circleY, circleRadius, ... ) local input = checkInput( ... ) if not checkPolygonPoint( circleX, circleY, ... ) then return false end local rad2 = circleRadius * circleRadius for i = 1, #input, 2 do local x1, y1 = input[i], input[i + 1] local x2, y2 = input[i + 2] or input[1], input[i + 3] or input[2] if distance2( x1, y1, circleX, circleY ) <= rad2 then return false end if getCircleSegmentIntersection( circleX, circleY, circleRadius, x1, y1, x2, y2 ) then return false end end return true end -- }}} -- Statistics ----------------------------------- {{{ -- Gets the average of a list of points -- points local function getMean( ... ) local input = checkInput( ... ) mean = getSummation( 1, #input, function( i, t ) return input[i] end ) / #input return mean end local function getMedian( ... ) local input = checkInput( ... ) table.sort( input ) local median if #input % 2 == 0 then -- If you have an even number of terms, you need to get the average of the middle 2. median = getMean( input[#input / 2], input[#input / 2 + 1] ) else median = input[#input / 2 + .5] end return median end -- Gets the mode of a number. local function getMode( ... ) local input = checkInput( ... ) table.sort( input ) local sorted = {} for i = 1, #input do local value = input[i] sorted[value] = sorted[value] and sorted[value] + 1 or 1 end local occurrences, least = 0, {} for i, value in pairs( sorted ) do if value > occurrences then least = { i } occurrences = value elseif value == occurrences then least[#least + 1] = i end end if #least >= 1 then return least, occurrences else return false end end -- Gets the range of the numbers. local function getRange( ... ) local input = checkInput( ... ) local high, low = math.max( unpack( input ) ), math.min( unpack( input ) ) return high - low end -- Gets the variance of a set of numbers. local function getVariance( ... ) local input = checkInput( ... ) local mean = getMean( ... ) local sum = 0 for i = 1, #input do sum = sum + ( mean - input[i] ) * ( mean - input[i] ) end return sum / #input end -- Gets the standard deviation of a set of numbers. local function getStandardDeviation( ... ) return math.sqrt( getVariance( ... ) ) end -- Gets the central tendency of a set of numbers. local function getCentralTendency( ... ) local mode, occurrences = getMode( ... ) return mode, occurrences, getMedian( ... ), getMean( ... ) end -- Gets the variation ratio of a data set. local function getVariationRatio( ... ) local input = checkInput( ... ) local numbers, times = getMode( ... ) times = times * #numbers -- Account for bimodal data return 1 - ( times / #input ) end -- Gets the measures of dispersion of a data set. local function getDispersion( ... ) return getVariationRatio( ... ), getRange( ... ), getStandardDeviation( ... ) end -- }}} return { _VERSION = 'MLib 0.10.0', _DESCRIPTION = 'A math and shape-intersection detection library for Lua', _URL = 'https://github.com/davisdude/mlib', point = { rotate = rotatePoint, scale = scalePoint, }, line = { getLength = getLength, getMidpoint = getMidpoint, getSlope = getSlope, getPerpendicularSlope = getPerpendicularSlope, getYIntercept = getYIntercept, getIntersection = getLineLineIntersection, getClosestPoint = getClosestPoint, getSegmentIntersection = getLineSegmentIntersection, checkPoint = checkLinePoint, -- Aliases getDistance = getLength, getCircleIntersection = getCircleLineIntersection, getPolygonIntersection = getPolygonLineIntersection, getLineIntersection = getLineLineIntersection, }, segment = { checkPoint = checkSegmentPoint, getPerpendicularBisector = getPerpendicularBisector, getIntersection = getSegmentSegmentIntersection, -- Aliases getCircleIntersection = getCircleSegmentIntersection, getPolygonIntersection = getPolygonSegmentIntersection, getLineIntersection = getLineSegmentIntersection, getSegmentIntersection = getSegmentSegmentIntersection, isSegmentCompletelyInsideCircle = isSegmentCompletelyInsideCircle, isSegmentCompletelyInsidePolygon = isSegmentCompletelyInsidePolygon, }, math = { getRoot = getRoot, isPrime = isPrime, round = round, getSummation = getSummation, getPercentOfChange = getPercentOfChange, getPercentage = getPercentage, getQuadraticRoots = getQuadraticRoots, getAngle = getAngle, }, circle = { getArea = getCircleArea, checkPoint = checkCirclePoint, isPointOnCircle = isPointOnCircle, getCircumference = getCircumference, getLineIntersection = getCircleLineIntersection, getSegmentIntersection = getCircleSegmentIntersection, getCircleIntersection = getCircleCircleIntersection, isCircleCompletelyInside = isCircleCompletelyInsideCircle, isPolygonCompletelyInside = isPolygonCompletelyInsideCircle, isSegmentCompletelyInside = isSegmentCompletelyInsideCircle, -- Aliases getPolygonIntersection = getPolygonCircleIntersection, isCircleInsidePolygon = isCircleInsidePolygon, isCircleCompletelyInsidePolygon = isCircleCompletelyInsidePolygon, }, polygon = { getSignedArea = getSignedPolygonArea, getArea = getPolygonArea, getTriangleHeight = getTriangleHeight, getCentroid = getCentroid, getLineIntersection = getPolygonLineIntersection, getSegmentIntersection = getPolygonSegmentIntersection, checkPoint = checkPolygonPoint, isSegmentInside = isSegmentInsidePolygon, getPolygonIntersection = getPolygonPolygonIntersection, getCircleIntersection = getPolygonCircleIntersection, isCircleInside = isCircleInsidePolygon, isPolygonInside = isPolygonInsidePolygon, isCircleCompletelyInside = isCircleCompletelyInsidePolygon, isSegmentCompletelyInside = isSegmentCompletelyInsidePolygon, isPolygonCompletelyInside = isPolygonCompletelyInsidePolygon, -- Aliases isCircleCompletelyOver = isPolygonCompletelyInsideCircle, }, statistics = { getMean = getMean, getMedian = getMedian, getMode = getMode, getRange = getRange, getVariance = getVariance, getStandardDeviation = getStandardDeviation, getCentralTendency = getCentralTendency, getVariationRatio = getVariationRatio, getDispersion = getDispersion, }, }