2022-12-25 10:44:29 +00:00
package resolv
import (
"math"
)
type Shape interface {
// Intersection tests to see if a Shape intersects with the other given Shape. dx and dy are delta movement variables indicating
// movement to be applied before the intersection check (thereby allowing you to see if a Shape would collide with another if it
// were in a different relative location). If an Intersection is found, a ContactSet will be returned, giving information regarding
// the intersection.
Intersection ( dx , dy float64 , other Shape ) * ContactSet
// Bounds returns the top-left and bottom-right points of the Shape.
Bounds ( ) ( Vector , Vector )
// Position returns the X and Y position of the Shape.
Position ( ) ( float64 , float64 )
// SetPosition allows you to place a Shape at another location.
SetPosition ( x , y float64 )
// Clone duplicates the Shape.
Clone ( ) Shape
}
// A Line is a helper shape used to determine if two ConvexPolygon lines intersect; you can't create a Line to use as a Shape.
// Instead, you can create a ConvexPolygon, specify two points, and set its Closed value to false.
type Line struct {
Start , End Vector
}
func NewLine ( x , y , x2 , y2 float64 ) * Line {
return & Line {
Start : Vector { x , y } ,
End : Vector { x2 , y2 } ,
}
}
func ( line * Line ) Normal ( ) Vector {
2023-02-15 04:02:07 +00:00
dy := line . End [ 1 ] - line . Start [ 1 ]
dx := line . End [ 0 ] - line . Start [ 0 ]
return Vector { dy , - dx } . Unit ( )
2022-12-25 10:44:29 +00:00
}
// IntersectionPointsLine returns the intersection point of a Line with another Line as a Vector. If no intersection is found, it will return nil.
func ( line * Line ) IntersectionPointsLine ( other * Line ) Vector {
det := ( line . End [ 0 ] - line . Start [ 0 ] ) * ( other . End [ 1 ] - other . Start [ 1 ] ) - ( other . End [ 0 ] - other . Start [ 0 ] ) * ( line . End [ 1 ] - line . Start [ 1 ] )
if det != 0 {
// MAGIC MATH; the extra + 1 here makes it so that corner cases (literally, lines going through corners) works.
// lambda := (float32(((line.Y-b.Y)*(b.X2-b.X))-((line.X-b.X)*(b.Y2-b.Y))) + 1) / float32(det)
lambda := ( ( ( line . Start [ 1 ] - other . Start [ 1 ] ) * ( other . End [ 0 ] - other . Start [ 0 ] ) ) - ( ( line . Start [ 0 ] - other . Start [ 0 ] ) * ( other . End [ 1 ] - other . Start [ 1 ] ) ) + 1 ) / det
// gamma := (float32(((line.Y-b.Y)*(line.X2-line.X))-((line.X-b.X)*(line.Y2-line.Y))) + 1) / float32(det)
gamma := ( ( ( line . Start [ 1 ] - other . Start [ 1 ] ) * ( line . End [ 0 ] - line . Start [ 0 ] ) ) - ( ( line . Start [ 0 ] - other . Start [ 0 ] ) * ( line . End [ 1 ] - line . Start [ 1 ] ) ) + 1 ) / det
if ( 0 < lambda && lambda < 1 ) && ( 0 < gamma && gamma < 1 ) {
// Delta
dx := line . End [ 0 ] - line . Start [ 0 ]
dy := line . End [ 1 ] - line . Start [ 1 ]
// dx, dy := line.GetDelta()
return Vector { line . Start [ 0 ] + ( lambda * dx ) , line . Start [ 1 ] + ( lambda * dy ) }
}
}
return nil
}
type ConvexPolygon struct {
2023-02-17 04:26:07 +00:00
Points * RingBuffer
2022-12-25 10:44:29 +00:00
X , Y float64
Closed bool
}
// NewConvexPolygon creates a new convex polygon from the provided set of X and Y positions of 2D points (or vertices). Should generally be ordered clockwise,
// from X and Y of the first, to X and Y of the last. For example: NewConvexPolygon(0, 0, 10, 0, 10, 10, 0, 10) would create a 10x10 convex
// polygon square, with the vertices at {0,0}, {10,0}, {10, 10}, and {0, 10}.
func NewConvexPolygon ( points ... float64 ) * ConvexPolygon {
2023-02-17 04:26:07 +00:00
cp := & ConvexPolygon {
Points : NewRingBuffer ( 6 ) , // I don't expected more points to be coped with in this particular game
Closed : true ,
}
2022-12-25 10:44:29 +00:00
cp . AddPoints ( points ... )
return cp
}
2023-02-17 04:26:07 +00:00
func ( cp * ConvexPolygon ) GetPointByOffset ( offset int32 ) Vector {
if cp . Points . Cnt <= offset {
return nil
2022-12-25 10:44:29 +00:00
}
2023-02-17 04:26:07 +00:00
return cp . Points . GetByFrameId ( cp . Points . StFrameId + offset ) . ( Vector )
}
func ( cp * ConvexPolygon ) Clone ( ) Shape {
2022-12-25 10:44:29 +00:00
newPoly := NewConvexPolygon ( )
newPoly . X = cp . X
newPoly . Y = cp . Y
2023-02-17 04:26:07 +00:00
for i := int32 ( 0 ) ; i < cp . Points . Cnt ; i ++ {
newPoly . Points . Put ( cp . GetPointByOffset ( i ) )
}
2022-12-25 10:44:29 +00:00
newPoly . Closed = cp . Closed
return newPoly
}
// AddPoints allows you to add points to the ConvexPolygon with a slice or selection of float64s, with each pair indicating an X or Y value for
// a point / vertex (i.e. AddPoints(0, 1, 2, 3) would add two points - one at {0, 1}, and another at {2, 3}).
func ( cp * ConvexPolygon ) AddPoints ( vertexPositions ... float64 ) {
for v := 0 ; v < len ( vertexPositions ) ; v += 2 {
2023-02-17 04:26:07 +00:00
// "resolv.Vector" is an alias of "[]float64", thus already a pointer type
cp . Points . Put ( Vector { vertexPositions [ v ] , vertexPositions [ v + 1 ] } )
}
}
func ( cp * ConvexPolygon ) UpdateAsRectangle ( x , y , w , h float64 ) bool {
// This function might look ugly but it's a fast in-place update!
if 4 != cp . Points . Cnt {
panic ( "ConvexPolygon not having exactly 4 vertices to form a rectangle#1!" )
2022-12-25 10:44:29 +00:00
}
2023-02-17 04:26:07 +00:00
for i := int32 ( 0 ) ; i < cp . Points . Cnt ; i ++ {
thatVec := cp . GetPointByOffset ( i )
if nil == thatVec {
panic ( "ConvexPolygon not having exactly 4 vertices to form a rectangle#2!" )
}
switch i {
case 0 :
thatVec [ 0 ] = x
thatVec [ 1 ] = y
case 1 :
thatVec [ 0 ] = x + w
thatVec [ 1 ] = y
case 2 :
thatVec [ 0 ] = x + w
thatVec [ 1 ] = y + h
case 3 :
thatVec [ 0 ] = x
thatVec [ 1 ] = y + h
}
}
return true
2022-12-25 10:44:29 +00:00
}
// Lines returns a slice of transformed Lines composing the ConvexPolygon.
func ( cp * ConvexPolygon ) Lines ( ) [ ] * Line {
vertices := cp . Transformed ( )
2023-02-15 04:02:07 +00:00
linesCnt := len ( vertices )
if ! cp . Closed {
linesCnt -= 1
}
lines := make ( [ ] * Line , linesCnt )
2022-12-25 10:44:29 +00:00
2023-02-15 04:02:07 +00:00
for i := 0 ; i < linesCnt ; i ++ {
2022-12-25 10:44:29 +00:00
start , end := vertices [ i ] , vertices [ 0 ]
if i < len ( vertices ) - 1 {
end = vertices [ i + 1 ]
}
line := NewLine ( start [ 0 ] , start [ 1 ] , end [ 0 ] , end [ 1 ] )
2023-02-15 04:02:07 +00:00
lines [ i ] = line
2022-12-25 10:44:29 +00:00
}
return lines
}
// Transformed returns the ConvexPolygon's points / vertices, transformed according to the ConvexPolygon's position.
func ( cp * ConvexPolygon ) Transformed ( ) [ ] Vector {
2023-02-17 04:26:07 +00:00
transformed := make ( [ ] Vector , cp . Points . Cnt )
for i := int32 ( 0 ) ; i < cp . Points . Cnt ; i ++ {
point := cp . GetPointByOffset ( i )
2023-02-15 04:02:07 +00:00
transformed [ i ] = Vector { point [ 0 ] + cp . X , point [ 1 ] + cp . Y }
2022-12-25 10:44:29 +00:00
}
return transformed
}
// Bounds returns two Vectors, comprising the top-left and bottom-right positions of the bounds of the
// ConvexPolygon, post-transformation.
func ( cp * ConvexPolygon ) Bounds ( ) ( Vector , Vector ) {
transformed := cp . Transformed ( )
topLeft := Vector { transformed [ 0 ] [ 0 ] , transformed [ 0 ] [ 1 ] }
bottomRight := topLeft . Clone ( )
for i := 0 ; i < len ( transformed ) ; i ++ {
point := transformed [ i ]
if point [ 0 ] < topLeft [ 0 ] {
topLeft [ 0 ] = point [ 0 ]
} else if point [ 0 ] > bottomRight [ 0 ] {
bottomRight [ 0 ] = point [ 0 ]
}
if point [ 1 ] < topLeft [ 1 ] {
topLeft [ 1 ] = point [ 1 ]
} else if point [ 1 ] > bottomRight [ 1 ] {
bottomRight [ 1 ] = point [ 1 ]
}
}
return topLeft , bottomRight
}
// Position returns the position of the ConvexPolygon.
func ( cp * ConvexPolygon ) Position ( ) ( float64 , float64 ) {
return cp . X , cp . Y
}
// SetPosition sets the position of the ConvexPolygon. The offset of the vertices compared to the X and Y position is relative to however
// you initially defined the polygon and added the vertices.
func ( cp * ConvexPolygon ) SetPosition ( x , y float64 ) {
cp . X = x
cp . Y = y
}
// SetPositionVec allows you to set the position of the ConvexPolygon using a Vector. The offset of the vertices compared to the X and Y
// position is relative to however you initially defined the polygon and added the vertices.
func ( cp * ConvexPolygon ) SetPositionVec ( vec Vector ) {
cp . X = vec . X ( )
cp . Y = vec . Y ( )
}
// Move translates the ConvexPolygon by the designated X and Y values.
func ( cp * ConvexPolygon ) Move ( x , y float64 ) {
cp . X += x
cp . Y += y
}
// MoveVec translates the ConvexPolygon by the designated Vector.
func ( cp * ConvexPolygon ) MoveVec ( vec Vector ) {
cp . X += vec . X ( )
cp . Y += vec . Y ( )
}
// Project projects (i.e. flattens) the ConvexPolygon onto the provided axis.
func ( cp * ConvexPolygon ) Project ( axis Vector ) Projection {
axis = axis . Unit ( )
vertices := cp . Transformed ( )
min := axis . Dot ( Vector { vertices [ 0 ] [ 0 ] , vertices [ 0 ] [ 1 ] } )
max := min
for i := 1 ; i < len ( vertices ) ; i ++ {
p := axis . Dot ( Vector { vertices [ i ] [ 0 ] , vertices [ i ] [ 1 ] } )
if p < min {
min = p
} else if p > max {
max = p
}
}
return Projection { min , max }
}
// SATAxes returns the axes of the ConvexPolygon for SAT intersection testing.
func ( cp * ConvexPolygon ) SATAxes ( ) [ ] Vector {
2023-02-15 04:02:07 +00:00
lines := cp . Lines ( )
axes := make ( [ ] Vector , len ( lines ) )
for i , line := range lines {
axes [ i ] = line . Normal ( )
2022-12-25 10:44:29 +00:00
}
return axes
}
// PointInside returns if a Point (a Vector) is inside the ConvexPolygon.
func ( polygon * ConvexPolygon ) PointInside ( point Vector ) bool {
pointLine := NewLine ( point [ 0 ] , point [ 1 ] , point [ 0 ] + 999999999999 , point [ 1 ] )
contactCount := 0
for _ , line := range polygon . Lines ( ) {
if line . IntersectionPointsLine ( pointLine ) != nil {
contactCount ++
}
}
return contactCount == 1
}
type ContactSet struct {
Points [ ] Vector // Slice of Points indicating contact between the two Shapes.
MTV Vector // Minimum Translation Vector; this is the vector to move a Shape on to move it outside of its contacting Shape.
Center Vector // Center of the Contact set; this is the average of all Points contained within the Contact Set.
}
func NewContactSet ( ) * ContactSet {
return & ContactSet {
Points : [ ] Vector { } ,
MTV : Vector { 0 , 0 } ,
Center : Vector { 0 , 0 } ,
}
}
// LeftmostPoint returns the left-most point out of the ContactSet's Points slice. If the Points slice is empty somehow, this returns nil.
func ( cs * ContactSet ) LeftmostPoint ( ) Vector {
var left Vector
for _ , point := range cs . Points {
if left == nil || point [ 0 ] < left [ 0 ] {
left = point
}
}
return left
}
// RightmostPoint returns the right-most point out of the ContactSet's Points slice. If the Points slice is empty somehow, this returns nil.
func ( cs * ContactSet ) RightmostPoint ( ) Vector {
var right Vector
for _ , point := range cs . Points {
if right == nil || point [ 0 ] > right [ 0 ] {
right = point
}
}
return right
}
// TopmostPoint returns the top-most point out of the ContactSet's Points slice. If the Points slice is empty somehow, this returns nil.
func ( cs * ContactSet ) TopmostPoint ( ) Vector {
var top Vector
for _ , point := range cs . Points {
if top == nil || point [ 1 ] < top [ 1 ] {
top = point
}
}
return top
}
// BottommostPoint returns the bottom-most point out of the ContactSet's Points slice. If the Points slice is empty somehow, this returns nil.
func ( cs * ContactSet ) BottommostPoint ( ) Vector {
var bottom Vector
for _ , point := range cs . Points {
if bottom == nil || point [ 1 ] > bottom [ 1 ] {
bottom = point
}
}
return bottom
}
// Intersection tests to see if a ConvexPolygon intersects with the other given Shape. dx and dy are delta movement variables indicating
// movement to be applied before the intersection check (thereby allowing you to see if a Shape would collide with another if it
// were in a different relative location). If an Intersection is found, a ContactSet will be returned, giving information regarding
// the intersection.
func ( cp * ConvexPolygon ) Intersection ( dx , dy float64 , other Shape ) * ContactSet {
contactSet := NewContactSet ( )
ogX := cp . X
ogY := cp . Y
cp . X += dx
cp . Y += dy
2023-02-26 12:39:30 +00:00
if poly , isPoly := other . ( * ConvexPolygon ) ; isPoly {
2022-12-25 10:44:29 +00:00
for _ , line := range cp . Lines ( ) {
for _ , otherLine := range poly . Lines ( ) {
if point := line . IntersectionPointsLine ( otherLine ) ; point != nil {
contactSet . Points = append ( contactSet . Points , point )
}
}
}
}
if len ( contactSet . Points ) > 0 {
2023-02-26 12:39:30 +00:00
// Do nothing
2022-12-25 10:44:29 +00:00
} else {
contactSet = nil
}
cp . X = ogX
cp . Y = ogY
return contactSet
}
// NewRectangle returns a rectangular ConvexPolygon with the vertices in clockwise order. In actuality, an AABBRectangle should be its own
// "thing" with its own optimized Intersection code check.
func NewRectangle ( x , y , w , h float64 ) * ConvexPolygon {
return NewConvexPolygon (
x , y ,
x + w , y ,
x + w , y + h ,
x , y + h ,
)
}
type Projection struct {
Min , Max float64
}
// Overlapping returns whether a Projection is overlapping with the other, provided Projection. Credit to https://www.sevenson.com.au/programming/sat/
func ( projection Projection ) Overlapping ( other Projection ) bool {
return projection . Overlap ( other ) > 0
}
// Overlap returns the amount that a Projection is overlapping with the other, provided Projection. Credit to https://dyn4j.org/2010/01/sat/#sat-nointer
func ( projection Projection ) Overlap ( other Projection ) float64 {
return math . Min ( projection . Max , other . Max ) - math . Max ( projection . Min , other . Min )
}
// IsInside returns whether the Projection is wholly inside of the other, provided Projection.
func ( projection Projection ) IsInside ( other Projection ) bool {
return projection . Min >= other . Min && projection . Max <= other . Max
}