eq2go/internal/zone/position.go

490 lines
13 KiB
Go

package zone
import (
"math"
)
// Position represents a 3D position with heading
type Position struct {
X float32
Y float32
Z float32
Heading float32
}
// Position2D represents a 2D position
type Position2D struct {
X float32
Y float32
}
// BoundingBox represents an axis-aligned bounding box
type BoundingBox struct {
MinX float32
MinY float32
MinZ float32
MaxX float32
MaxY float32
MaxZ float32
}
// Cylinder represents a cylindrical collision shape
type Cylinder struct {
X float32
Y float32
Z float32
Radius float32
Height float32
}
const (
// EQ2HeadingMax represents the maximum heading value in EQ2 (512 = full circle)
EQ2HeadingMax = 512.0
// DefaultEpsilon for floating point comparisons
DefaultEpsilon = 0.0001
// DegreesToRadians conversion factor
DegreesToRadians = math.Pi / 180.0
// RadiansToDegrees conversion factor
RadiansToDegrees = 180.0 / math.Pi
)
// NewPosition creates a new position with the given coordinates and heading
func NewPosition(x, y, z, heading float32) *Position {
return &Position{
X: x,
Y: y,
Z: z,
Heading: heading,
}
}
// NewPosition2D creates a new 2D position
func NewPosition2D(x, y float32) *Position2D {
return &Position2D{
X: x,
Y: y,
}
}
// NewBoundingBox creates a new bounding box with the given bounds
func NewBoundingBox(minX, minY, minZ, maxX, maxY, maxZ float32) *BoundingBox {
return &BoundingBox{
MinX: minX,
MinY: minY,
MinZ: minZ,
MaxX: maxX,
MaxY: maxY,
MaxZ: maxZ,
}
}
// NewCylinder creates a new cylinder with the given parameters
func NewCylinder(x, y, z, radius, height float32) *Cylinder {
return &Cylinder{
X: x,
Y: y,
Z: z,
Radius: radius,
Height: height,
}
}
// Copy creates a copy of the position
func (p *Position) Copy() *Position {
return &Position{
X: p.X,
Y: p.Y,
Z: p.Z,
Heading: p.Heading,
}
}
// Set updates the position with new values
func (p *Position) Set(x, y, z, heading float32) {
p.X = x
p.Y = y
p.Z = z
p.Heading = heading
}
// SetXYZ updates only the coordinates, leaving heading unchanged
func (p *Position) SetXYZ(x, y, z float32) {
p.X = x
p.Y = y
p.Z = z
}
// SetHeading updates only the heading
func (p *Position) SetHeading(heading float32) {
p.Heading = heading
}
// Distance2D calculates the 2D distance to another position (ignoring Z)
func Distance2D(x1, y1, x2, y2 float32) float32 {
dx := x2 - x1
dy := y2 - y1
return float32(math.Sqrt(float64(dx*dx + dy*dy)))
}
// Distance2DSquared calculates the squared 2D distance (more efficient, avoids sqrt)
func Distance2DSquared(x1, y1, x2, y2 float32) float32 {
dx := x2 - x1
dy := y2 - y1
return dx*dx + dy*dy
}
// Distance3D calculates the 3D distance between two points
func Distance3D(x1, y1, z1, x2, y2, z2 float32) float32 {
dx := x2 - x1
dy := y2 - y1
dz := z2 - z1
return float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz)))
}
// Distance3DSquared calculates the squared 3D distance (more efficient, avoids sqrt)
func Distance3DSquared(x1, y1, z1, x2, y2, z2 float32) float32 {
dx := x2 - x1
dy := y2 - y1
dz := z2 - z1
return dx*dx + dy*dy + dz*dz
}
// Distance4D calculates the 4D distance including heading difference
func Distance4D(x1, y1, z1, h1, x2, y2, z2, h2 float32) float32 {
dx := x2 - x1
dy := y2 - y1
dz := z2 - z1
dh := HeadingDifference(h1, h2)
return float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz + dh*dh)))
}
// DistanceTo2D calculates the 2D distance to another position
func (p *Position) DistanceTo2D(other *Position) float32 {
return Distance2D(p.X, p.Y, other.X, other.Y)
}
// DistanceTo3D calculates the 3D distance to another position
func (p *Position) DistanceTo3D(other *Position) float32 {
return Distance3D(p.X, p.Y, p.Z, other.X, other.Y, other.Z)
}
// DistanceTo4D calculates the 4D distance to another position including heading
func (p *Position) DistanceTo4D(other *Position) float32 {
return Distance4D(p.X, p.Y, p.Z, p.Heading, other.X, other.Y, other.Z, other.Heading)
}
// CalculateHeading calculates the heading from current position to target position
func (p *Position) CalculateHeading(targetX, targetY float32) float32 {
return CalculateHeading(p.X, p.Y, targetX, targetY)
}
// CalculateHeading calculates the EQ2 heading from one point to another
func CalculateHeading(fromX, fromY, toX, toY float32) float32 {
dx := toX - fromX
dy := toY - fromY
if dx == 0 && dy == 0 {
return 0.0
}
// Calculate angle in radians
angle := math.Atan2(float64(dx), float64(dy))
// Convert to EQ2 heading (0-512 scale, 0 = north)
heading := angle * (EQ2HeadingMax / (2 * math.Pi))
// Ensure positive value
if heading < 0 {
heading += EQ2HeadingMax
}
return float32(heading)
}
// HeadingToRadians converts an EQ2 heading to radians
func HeadingToRadians(heading float32) float32 {
return heading * (2 * math.Pi / EQ2HeadingMax)
}
// RadiansToHeading converts radians to an EQ2 heading
func RadiansToHeading(radians float32) float32 {
heading := radians * (EQ2HeadingMax / (2 * math.Pi))
if heading < 0 {
heading += EQ2HeadingMax
}
return heading
}
// HeadingToRadiansDegrees converts an EQ2 heading to degrees
func HeadingToDegrees(heading float32) float32 {
return heading * (360.0 / EQ2HeadingMax)
}
// DegreesToHeading converts degrees to an EQ2 heading
func DegreesToHeading(degrees float32) float32 {
heading := degrees * (EQ2HeadingMax / 360.0)
if heading < 0 {
heading += EQ2HeadingMax
}
return heading
}
// NormalizeHeading ensures a heading is in the valid range [0, 512)
func NormalizeHeading(heading float32) float32 {
for heading < 0 {
heading += EQ2HeadingMax
}
for heading >= EQ2HeadingMax {
heading -= EQ2HeadingMax
}
return heading
}
// HeadingDifference calculates the shortest angular difference between two headings
func HeadingDifference(heading1, heading2 float32) float32 {
diff := heading2 - heading1
// Normalize to [-256, 256] range (half circle)
for diff > EQ2HeadingMax/2 {
diff -= EQ2HeadingMax
}
for diff < -EQ2HeadingMax/2 {
diff += EQ2HeadingMax
}
return diff
}
// GetReciprocalHeading calculates the opposite heading (180 degree turn)
func GetReciprocalHeading(heading float32) float32 {
reciprocal := heading + EQ2HeadingMax/2
if reciprocal >= EQ2HeadingMax {
reciprocal -= EQ2HeadingMax
}
return reciprocal
}
// Equals compares two positions with epsilon tolerance
func (p *Position) Equals(other *Position) bool {
return EqualsWithEpsilon(p.X, other.X, DefaultEpsilon) &&
EqualsWithEpsilon(p.Y, other.Y, DefaultEpsilon) &&
EqualsWithEpsilon(p.Z, other.Z, DefaultEpsilon) &&
EqualsWithEpsilon(p.Heading, other.Heading, DefaultEpsilon)
}
// EqualsXYZ compares only the coordinates (ignoring heading)
func (p *Position) EqualsXYZ(other *Position) bool {
return EqualsWithEpsilon(p.X, other.X, DefaultEpsilon) &&
EqualsWithEpsilon(p.Y, other.Y, DefaultEpsilon) &&
EqualsWithEpsilon(p.Z, other.Z, DefaultEpsilon)
}
// EqualsWithEpsilon compares two float values with the given epsilon tolerance
func EqualsWithEpsilon(a, b, epsilon float32) bool {
diff := a - b
if diff < 0 {
diff = -diff
}
return diff < epsilon
}
// Contains checks if a point is inside the bounding box
func (bb *BoundingBox) Contains(x, y, z float32) bool {
return x >= bb.MinX && x <= bb.MaxX &&
y >= bb.MinY && y <= bb.MaxY &&
z >= bb.MinZ && z <= bb.MaxZ
}
// ContainsPosition checks if a position is inside the bounding box
func (bb *BoundingBox) ContainsPosition(pos *Position) bool {
return bb.Contains(pos.X, pos.Y, pos.Z)
}
// Intersects checks if this bounding box intersects with another
func (bb *BoundingBox) Intersects(other *BoundingBox) bool {
return bb.MinX <= other.MaxX && bb.MaxX >= other.MinX &&
bb.MinY <= other.MaxY && bb.MaxY >= other.MinY &&
bb.MinZ <= other.MaxZ && bb.MaxZ >= other.MinZ
}
// Expand expands the bounding box by the given amount in all directions
func (bb *BoundingBox) Expand(amount float32) {
bb.MinX -= amount
bb.MinY -= amount
bb.MinZ -= amount
bb.MaxX += amount
bb.MaxY += amount
bb.MaxZ += amount
}
// GetCenter returns the center point of the bounding box
func (bb *BoundingBox) GetCenter() *Position {
return &Position{
X: (bb.MinX + bb.MaxX) / 2,
Y: (bb.MinY + bb.MaxY) / 2,
Z: (bb.MinZ + bb.MaxZ) / 2,
}
}
// GetSize returns the size of the bounding box in each dimension
func (bb *BoundingBox) GetSize() (width, height, depth float32) {
return bb.MaxX - bb.MinX, bb.MaxY - bb.MinY, bb.MaxZ - bb.MinZ
}
// Contains2D checks if a 2D point is inside the cylinder (ignoring height)
func (c *Cylinder) Contains2D(x, y float32) bool {
return Distance2DSquared(c.X, c.Y, x, y) <= c.Radius*c.Radius
}
// Contains3D checks if a 3D point is inside the cylinder
func (c *Cylinder) Contains3D(x, y, z float32) bool {
if z < c.Z || z > c.Z+c.Height {
return false
}
return c.Contains2D(x, y)
}
// ContainsPosition checks if a position is inside the cylinder
func (c *Cylinder) ContainsPosition(pos *Position) bool {
return c.Contains3D(pos.X, pos.Y, pos.Z)
}
// DistanceToEdge2D calculates the 2D distance from a point to the cylinder edge
func (c *Cylinder) DistanceToEdge2D(x, y float32) float32 {
distance := Distance2D(c.X, c.Y, x, y)
return distance - c.Radius
}
// GetBoundingBox returns a bounding box that encompasses the cylinder
func (c *Cylinder) GetBoundingBox() *BoundingBox {
return &BoundingBox{
MinX: c.X - c.Radius,
MinY: c.Y - c.Radius,
MinZ: c.Z,
MaxX: c.X + c.Radius,
MaxY: c.Y + c.Radius,
MaxZ: c.Z + c.Height,
}
}
// Copy creates a copy of the 2D position
func (p *Position2D) Copy() *Position2D {
return &Position2D{
X: p.X,
Y: p.Y,
}
}
// DistanceTo calculates the distance to another 2D position
func (p *Position2D) DistanceTo(other *Position2D) float32 {
return Distance2D(p.X, p.Y, other.X, other.Y)
}
// DistanceToSquared calculates the squared distance to another 2D position
func (p *Position2D) DistanceToSquared(other *Position2D) float32 {
return Distance2DSquared(p.X, p.Y, other.X, other.Y)
}
// Equals compares two 2D positions with epsilon tolerance
func (p *Position2D) Equals(other *Position2D) bool {
return EqualsWithEpsilon(p.X, other.X, DefaultEpsilon) &&
EqualsWithEpsilon(p.Y, other.Y, DefaultEpsilon)
}
// InterpolateLinear performs linear interpolation between two positions
func InterpolateLinear(from, to *Position, t float32) *Position {
if t <= 0 {
return from.Copy()
}
if t >= 1 {
return to.Copy()
}
return &Position{
X: from.X + (to.X-from.X)*t,
Y: from.Y + (to.Y-from.Y)*t,
Z: from.Z + (to.Z-from.Z)*t,
Heading: InterpolateHeading(from.Heading, to.Heading, t),
}
}
// InterpolateHeading performs interpolation between two headings, taking the shortest path
func InterpolateHeading(from, to, t float32) float32 {
diff := HeadingDifference(from, to)
result := from + diff*t
return NormalizeHeading(result)
}
// GetRandomPositionInRadius generates a random position within the given radius
func GetRandomPositionInRadius(centerX, centerY, centerZ float32, radius float32) *Position {
// Generate random angle
angle := float32(math.Random() * 2 * math.Pi)
// Generate random distance (uniform distribution in circle)
distance := float32(math.Sqrt(math.Random())) * radius
// Calculate new position
x := centerX + distance*float32(math.Cos(float64(angle)))
y := centerY + distance*float32(math.Sin(float64(angle)))
return &Position{
X: x,
Y: y,
Z: centerZ,
Heading: 0,
}
}
// IsWithinRange checks if two positions are within the specified range
func IsWithinRange(pos1, pos2 *Position, maxRange float32) bool {
return Distance3DSquared(pos1.X, pos1.Y, pos1.Z, pos2.X, pos2.Y, pos2.Z) <= maxRange*maxRange
}
// IsWithinRange2D checks if two positions are within the specified 2D range
func IsWithinRange2D(pos1, pos2 *Position, maxRange float32) bool {
return Distance2DSquared(pos1.X, pos1.Y, pos2.X, pos2.Y) <= maxRange*maxRange
}
// ClampToRange clamps a distance to be within the specified range
func ClampToRange(distance, minRange, maxRange float32) float32 {
if distance < minRange {
return minRange
}
if distance > maxRange {
return maxRange
}
return distance
}
// GetDirectionVector calculates a normalized direction vector from one position to another
func GetDirectionVector(from, to *Position) (dx, dy, dz float32) {
dx = to.X - from.X
dy = to.Y - from.Y
dz = to.Z - from.Z
// Normalize
length := float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz)))
if length > 0 {
dx /= length
dy /= length
dz /= length
}
return dx, dy, dz
}
// MoveTowards moves a position towards a target by the specified distance
func MoveTowards(from, to *Position, distance float32) *Position {
dx, dy, dz := GetDirectionVector(from, to)
return &Position{
X: from.X + dx*distance,
Y: from.Y + dy*distance,
Z: from.Z + dz*distance,
Heading: from.Heading,
}
}