DelayNoMore/resolv_tailored/shape.go
2023-02-26 20:39:30 +08:00

443 lines
12 KiB
Go

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 {
dy := line.End[1] - line.Start[1]
dx := line.End[0] - line.Start[0]
return Vector{dy, -dx}.Unit()
}
// 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 {
Points *RingBuffer
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 {
cp := &ConvexPolygon{
Points: NewRingBuffer(6), // I don't expected more points to be coped with in this particular game
Closed: true,
}
cp.AddPoints(points...)
return cp
}
func (cp *ConvexPolygon) GetPointByOffset(offset int32) Vector {
if cp.Points.Cnt <= offset {
return nil
}
return cp.Points.GetByFrameId(cp.Points.StFrameId + offset).(Vector)
}
func (cp *ConvexPolygon) Clone() Shape {
newPoly := NewConvexPolygon()
newPoly.X = cp.X
newPoly.Y = cp.Y
for i := int32(0); i < cp.Points.Cnt; i++ {
newPoly.Points.Put(cp.GetPointByOffset(i))
}
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 {
// "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!")
}
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
}
// Lines returns a slice of transformed Lines composing the ConvexPolygon.
func (cp *ConvexPolygon) Lines() []*Line {
vertices := cp.Transformed()
linesCnt := len(vertices)
if !cp.Closed {
linesCnt -= 1
}
lines := make([]*Line, linesCnt)
for i := 0; i < linesCnt; i++ {
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])
lines[i] = line
}
return lines
}
// Transformed returns the ConvexPolygon's points / vertices, transformed according to the ConvexPolygon's position.
func (cp *ConvexPolygon) Transformed() []Vector {
transformed := make([]Vector, cp.Points.Cnt)
for i := int32(0); i < cp.Points.Cnt; i++ {
point := cp.GetPointByOffset(i)
transformed[i] = Vector{point[0] + cp.X, point[1] + cp.Y}
}
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 {
lines := cp.Lines()
axes := make([]Vector, len(lines))
for i, line := range lines {
axes[i] = line.Normal()
}
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
if poly, isPoly := other.(*ConvexPolygon); isPoly {
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 {
// Do nothing
} 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
}