line and line segment intersection code and tests

This commit is contained in:
Jason_DiDonato@yahoo.com 2021-01-20 22:56:29 -05:00
parent f557ecc13d
commit 4304ea7f4d
7 changed files with 563 additions and 2 deletions

View file

@ -1433,7 +1433,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
deadState = DeadState.RespawnTime
session = session.copy(player = new Player(avatar))
//xy-coordinates indicate sanctuary spawn bias:
//ay-coordinates indicate sanctuary spawn bias:
player.Position = math.abs(scala.util.Random.nextInt() % avatar.name.hashCode % 4) match {
case 0 => Vector3(8192, 8192, 0) //NE
case 1 => Vector3(8192, 0, 0) //SE

View file

@ -73,7 +73,7 @@ class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with D
case _ => false
}
} =>
// the mine damages itself, which sets it off, which causes an explosion
// the trigger damages the mine, which sets it off, which causes an explosion
// think of this as an initiator to the proper explosion
mine.Destroyed = true
ExplosiveDeployableControl.DamageResolution(

View file

@ -0,0 +1,173 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry
import net.psforever.types.Vector3
object ClosestDistance {
object Between {
def apply(origin1 : Vector3, origin2 : Vector3, point : Vector3, seg : Segment2D) : Float = {
val segdx = seg.bx - seg.ax
val segdy = seg.by - seg.ay
((point.x + origin1.x - seg.ax + origin2.x) * segdx + (point.y + origin1.y - seg.ay + origin2.y) * segdy) /
Vector3.MagnitudeSquared(Vector3(segdx, segdy, 0))
}
def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line2D, line2 : Line2D) : Float = {
if (Intersection.Test(origin1, origin2, line1, line2)) { //intersecting lines
0f
} else {
math.abs(
Vector3.DotProduct(
Vector3(line2.x - line1.x, line2.y - line1.y, 0),
Vector3(-1/line1.d.y, 1/line1.d.x, 0)
)
)
}
}
def apply(origin1: Vector3, origin2: Vector3, seg1: Segment2D, seg2: Segment2D): Float = {
if (Intersection.Test(origin1, origin2, seg1, seg2)) { //intersecting line segments
0f
} else {
val v1a = Vector3(seg1.ax, seg1.ay, 0)
val v2a = Vector3(seg2.ax, seg2.ay, 0)
val v1b = Vector3(seg1.bx, seg1.by, 0)
val v2b = Vector3(seg2.bx, seg2.by, 0)
math.min(
apply(origin1, origin2, v1a, seg2),
math.min(
apply(origin1, origin2, v1b, seg2),
math.min(
apply(origin1, origin2, v2a, seg1),
apply(origin1, origin2, v2b, seg1)
)
)
)
}
}
def apply(origin1: Vector3, origin2: Vector3, line1: Line3D, line2: Line3D): Float = {
val cross = Vector3.CrossProduct(line1.d, line2.d)
if(cross != Vector3.Zero) {
math.abs(
Vector3.DotProduct(cross, Vector3(line1.x - line2.x, line1.y - line2.y, line1.z - line2.z))
) / Vector3.Magnitude(cross)
} else {
//lines are parallel
Vector3.Magnitude(
Vector3.CrossProduct(
line1.d,
Vector3(line2.x - line1.x, line2.y - line1.y, line2.z - line1.z)
)
)
}
}
def apply(origin1: Vector3, origin2: Vector3, seg1: Segment3D, seg2: Segment3D): Float = {
//TODO make not as expensive as finding the plotted closest distance segment
Plotted(origin1, origin2, seg1, seg2) match {
case Some(seg) => seg.length
case None => Float.MaxValue
}
}
}
object Plotted {
/**
* na
* This function can only operate normally if a perpendicular line segment between the two lines can be established,
* this is, if the cross product of the two lines exists.
* As such, for coincidental lines, a segment of zero length from the first line's point is produced.
* @param origin1 na
* @param origin2 na
* @param line1 na
* @param line2 na
* @return na
*/
def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D): Option[Segment3D] = {
val p1 = Vector3(line1.x, line1.y, line1.z)
val p2 = p1 + line1.d
val p3 = Vector3(line2.x, line2.y, line2.z)
val p4 = p3 + line2.d
val p13 = p1 - p3 // vector between point on first line and point on second line
val p43 = line2.d
val p21 = line1.d
if (Vector3.MagnitudeSquared(p43) < Float.MinPositiveValue ||
Vector3.MagnitudeSquared(p21) < Float.MinPositiveValue) {
None
} else {
val d2121 = Vector3.MagnitudeSquared(p21)
val d4343 = Vector3.MagnitudeSquared(p43)
val d4321 = Vector3.DotProduct(p43, p21)
val denom = d2121 * d4343 - d4321 * d4321 // n where d = (m/n) and a(x,y,z) + d * V<u,v,w> = b(x,y,z) for line1
if (math.abs(denom) < Float.MinPositiveValue) {
val p13u = Vector3.Unit(p13)
if (p21 == p13u || p21 == Vector3.neg(p13u)) { //coincidental lines
// can not produce a valid cross product, but a coincidental line does produce an overlap
Some(Segment3D(
line1.x, line1.y, line1.z,
line1.x, line1.y, line1.z
))
} else {
None
}
} else {
val d1343 = Vector3.DotProduct(p13, p43)
val numer = d1343 * d4321 -d4343 * Vector3.DotProduct(p13, p21) // m where d = (m/n) and ..., etc.
val mua = numer / denom
val mub = (d1343 + d4321 * mua) / d4343
Some(Segment3D(
p1.x + mua * p21.x,
p1.y + mua * p21.y,
p1.z + mua * p21.z,
p3.x + mub * p43.x,
p3.y + mub * p43.y,
p3.z + mub * p43.z
))
}
}
}
def apply(origin1 : Vector3, origin2 : Vector3, line1 : Segment3D, line2 : Segment3D): Option[Segment3D] = {
val uline1 = Vector3.Unit(line1.d)
val uline2 = Vector3.Unit(line2.d)
apply(
origin1,
origin2,
Line3D(line1.ax, line1.ay, line1.az, uline1),
Line3D(line2.ax, line2.ay, line2.az, uline2)
) match {
case out @ Some(seg: Segment3D)
if seg.length == 0 && (uline1 == uline2 || uline1 == Vector3.neg(uline2)) => //coincidental lines
out
case Some(seg: Segment3D) => //segment of shortest distance when two segments treated as lines
val sega = Vector3(seg.ax, seg.ay, seg.az)
val p1 = Vector3(line1.ax, line1.ay, line1.az)
val d1 = sega - p1
val out1 = if (!Geometry.equalVectors(Vector3.Unit(d1), uline1)) { //clamp seg.a(xyz) to segment line1's bounds
p1
} else if (Vector3.MagnitudeSquared(d1) > Vector3.MagnitudeSquared(line1.d)) {
Vector3(line1.bx, line1.by, line1.bz)
} else {
sega
}
val segb = Vector3(seg.bx, seg.by, seg.bz)
val p2 = Vector3(line2.ax, line2.ay, line2.az)
val d2 = segb - p2
val out2 = if (!Geometry.equalVectors(Vector3.Unit(d2), uline2)) { //clamp seg.b(xyz) to segment line2's bounds
p2
} else if (Vector3.MagnitudeSquared(d2) > Vector3.MagnitudeSquared(line2.d)) {
Vector3(line2.bx, line2.by, line2.bz)
} else {
segb
}
Some(Segment3D(
out1.x, out1.y, out1.z,
out2.x, out2.y, out2.z
))
case None =>
None
}
}
}
}

View file

@ -0,0 +1,79 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry
import net.psforever.types.Vector3
trait Slope {
def d: Vector3
def length: Float
}
trait Line extends Slope {
assert({
val mag = Vector3.Magnitude(d)
mag - 0.05f < 1f && mag + 0.05f > 1f
}, "not a unit vector")
def length: Float = Float.PositiveInfinity
}
trait Segment extends Slope {
def length: Float = Vector3.Magnitude(d)
}
final case class Line2D(x: Float, y: Float, d: Vector3) extends Line
object Line2D {
def apply(ax: Float, ay: Float, bx: Float, by: Float): Line2D = {
Line2D(ax, ay, Vector3.Unit(Vector3(bx-ax, by-ay, 0)))
}
}
final case class Segment2D(ax: Float, ay: Float, bx: Float, by: Float) extends Segment {
def d: Vector3 = Vector3(bx - ax, by - ay, 0)
}
object Segment2D {
def apply(x: Float, y: Float, z: Float, d: Vector3): Segment2D = {
Segment2D(x, y, x + d.x, y + d.y)
}
}
final case class Line3D(x: Float, y: Float, z: Float, d: Vector3) extends Line
final case class Segment3D(ax: Float, ay: Float, az: Float, bx: Float, by: Float, bz: Float) extends Segment {
def d: Vector3 = Vector3(bx - ax, by - ay, bz - az)
}
object Segment3D {
def apply(x: Float, y: Float, z: Float, d: Vector3): Segment3D = {
Segment3D(x, y, z, z+d.x, y+d.y, z+d.z)
}
}
object Geometry {
def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = {
val diff = value1 - value2
(diff >= 0 && diff <= off) || diff > -off
}
def equalVectors(value1: Vector3, value2: Vector3, off: Float = 0.001f): Boolean = {
equalFloats(value1.x, value2.x, off) &&
equalFloats(value1.y, value2.y, off) &&
equalFloats(value1.z, value2.z, off)
}
def closeToInsignificance(d: Float, epsilon: Float = 10f): Float = {
val ulp = math.ulp(epsilon)
math.signum(d) match {
case -1f =>
val n = math.abs(d)
val p = math.abs(n - n.toInt)
if (p < ulp || d > ulp) d + p else d
case _ =>
val p = math.abs(d - d.toInt)
if (p < ulp || d < ulp) d - p else d
}
}
}

View file

@ -0,0 +1,107 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.geometry
import net.psforever.types.Vector3
object Intersection {
object Test {
/**
* Do these two lines intersect?
* Lines in 2D space will always intersect unless they are parallel or antiparallel.
* In that case, they can still "intersect" if the lines are coincidental.
*/
def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line2D, line2 : Line2D): Boolean = {
line1.d != line2.d || {
//parallel or antiparallel?
val u = Vector3.Unit(Vector3(line2.x - line1.x, line2.y - line1.y, 0))
line1.d == u || line1.d == Vector3.neg(u)
}
}
private def pointOnSegment(ax : Float, ay : Float, px : Float, py : Float, bx : Float, by : Float): Boolean = {
px <= math.max(ax, bx) && px >= math.min(ax, bx) && py <= math.max(ay, by) && py >= math.min(ay, by)
}
object PointTripleOrientation extends Enumeration {
val Colinear, Clockwise, Counterclockwise = Value
}
/**
* Determine the orientation of the given three two-dimensional points.
* Any triple has one of three orientations:
* clockwise - the third point is to the right side of a line plotted by the first two points;
* counterclockwise - the third point is to the left side of a line plotted by the first two points;
* and, colinear - the third point is reachable along the line plotted by the first two points.
* @param ax x-coordinate of the first point
* @param ay y-coordinate of the first point
* @param px x-coordinate of the second point
* @param py y-coordinate of the second point
* @param bx x-coordinate of the third point
* @param by y-coordinate of the third point
* @return the orientation value
*/
private def orientationOfPoints(
ax : Float, ay : Float,
px : Float, py : Float,
bx : Float, by : Float
): PointTripleOrientation.Value = {
val out = (py - ay) * (bx - px) - (px - ax) * (by - py)
if (out == 0) PointTripleOrientation.Colinear
else if (out > 0) PointTripleOrientation.Clockwise
else PointTripleOrientation.Counterclockwise
}
/**
* Do these two line segments intersect?
* Intersection of two two-dimensional line segments can be determined by the orientation of their endpoints.
* If a test of multiple ordered triple points reveals that certain triples have different orientations,
* then we can safely assume the intersection state of the segments.
*/
def apply(origin1 : Vector3, origin2 : Vector3, line1 : Segment2D, line2 : Segment2D): Boolean = {
//setup
val ln1ax = line1.ax + origin1.x
val ln1ay = line1.ay + origin1.y
val ln1bx = ln1ax + origin1.x
val ln1by = ln1ay + origin1.y
val ln2ax = line2.ax + origin2.x
val ln2ay = line2.ay + origin2.y
val ln2bx = ln2ax + origin2.x
val ln2by = ln2ay + origin2.y
val ln1_ln2a = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2ax, ln2ay)
val ln1_ln2b = orientationOfPoints(ln1ax, ln1ay, ln1bx, ln1by, ln2bx, ln2by)
val ln2_ln1a = orientationOfPoints(ln2ax, ln2ay, ln2bx, ln2by, ln1ax, ln1ay)
val ln2_ln1b = orientationOfPoints(ln2ax, ln2ay, ln2bx, ln2by, ln1bx, ln1by)
//results
import PointTripleOrientation._
(ln1_ln2a != ln1_ln2b && ln2_ln1a != ln2_ln1b) ||
(ln1_ln2a == Colinear && pointOnSegment(ln1ax, ln1ay, ln2ax, ln2ay, ln1bx, ln1by)) || // line2 A is on line1
(ln1_ln2b == Colinear && pointOnSegment(ln1ax, ln1ay, ln2bx, ln2by, ln1bx, ln1by)) || // line2 B is on line1
(ln2_ln1a == Colinear && pointOnSegment(ln2ax, ln2ay, ln1ax, ln1ay, ln2bx, ln2by)) || // line1 A is on line2
(ln2_ln1b == Colinear && pointOnSegment(ln2ax, ln2ay, ln1bx, ln1by, ln2bx, ln2by)) // line1 B is on line2
}
/**
* Do these two lines intersect?
* Actual mathematically-sound intersection between lines and line segments in 3D-space is terribly uncommon.
* Instead, check that the closest distance between two line segments is below a threshold value.
*/
def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D): Boolean = {
apply(origin1, origin2, line1, line2, 0.15f)
}
def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D, threshold: Float): Boolean = {
ClosestDistance.Between(origin1, origin2, line1, line2) < threshold
}
/**
* Do these two line segments intersect?
* Actual mathematically-sound intersection between lines and line segments in 3D-space is terribly uncommon.
* Instead, check that the closest distance between two line segments is below a threshold value.
*/
def apply(origin1 : Vector3, origin2 : Vector3, seg1 : Segment3D, seg2 : Segment3D): Boolean = {
apply(origin1, origin2, seg1, seg2, 0.15f)
}
def apply(origin1 : Vector3, origin2 : Vector3, seg1 : Segment3D, seg2 : Segment3D, threshold: Float): Boolean = {
ClosestDistance.Between(origin1, origin2, seg1, seg2) < threshold
}
}
}

View file

@ -117,6 +117,14 @@ object Vector3 {
*/
def z(value: Float): Vector3 = Vector3(0, 0, value)
/**
* Calculate the negation of this vector,
* the same vector in the antiparallel direction.
* @param v the original vector
* @return the negation of the original vector
*/
def neg(v: Vector3): Vector3 = Vector3(-v.x, -v.y, -v.z)
/**
* Calculate the actual distance between two points.
* @param pos1 the first point

View file

@ -0,0 +1,194 @@
// Copyright (c) 2020 PSForever
package objects
import net.psforever.objects.geometry.{Intersection, Line3D, Segment3D}
import net.psforever.types.Vector3
import org.specs2.mutable.Specification
class IntersectionTest extends Specification {
"Line3D" should {
"detect intersection on target point(s)" in {
//these lines intersect at (0, 0, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Line3D(0,0,0, Vector3(1,0,0)),
Line3D(0,0,0, Vector3(0,1,0))
)
result mustEqual true
}
"detect intersection on a target point" in {
//these lines intersect at (0, 0, 0); start of segment 1, middle of segment 2
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Line3D(0,0,0, Vector3(0,1,0)),
Line3D(-1,0,0, Vector3(1,0,0))
)
result mustEqual true
}
"detect intersection in the middle(s)" in {
//these lines intersect at (0.5f, 0.5f, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Line3D(0,0,0, Vector3.Unit(Vector3(1, 1, 0))),
Line3D(1,0,0, Vector3(0,1,0))
)
result mustEqual true
}
"detect intersection in the middle " in {
//these lines intersect at (0, 0.5, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Line3D(0,0,0, Vector3(1,0,0)),
Line3D(0.5f,1,0, Vector3.Unit(Vector3(0.5f,-1,0)))
)
result mustEqual true
}
"detect intersection if the point of intersection would be before the start of the segments" in {
//these lines would intersect at (0, 0, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Line3D(1,1,0, Vector3.Unit(Vector3(2, 2, 0))),
Line3D(1,0,0, Vector3.Unit(Vector3(2,0,0)))
)
result mustEqual true
}
"detect intersection if the point of intersection would be after the end of the segments" in {
//these lines would intersect at (2, 2, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Line3D(0,0,0, Vector3.Unit(Vector3(1,1,0))),
Line3D(2,0,0, Vector3.Unit(Vector3(2,1,0)))
)
result mustEqual true
}
"not detect intersection if the line segments are parallel" in {
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Line3D(0,0,0, Vector3.Unit(Vector3(1,1,1))),
Line3D(1,1,2, Vector3.Unit(Vector3(1,1,1)))
)
result mustEqual false
}
"detect overlap" in {
//the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Line3D(0,0,0, Vector3.Unit(Vector3(2,0,0))),
Line3D(1,0,0, Vector3.Unit(Vector3(3,0,0)))
)
result mustEqual true
}
"not detect intersection (generic skew)" in {
//these segments will not intersect
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(-3,-8,7, Vector3.Unit(Vector3(-3,-9,8))),
Segment3D(6,3,0, Vector3.Unit(Vector3(2,0,0)))
)
result mustEqual false
}
}
"Segment3D" should {
"detect intersection of the first point(s)" in {
//these segments intersect at (0, 0, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(0,0,0, 1,0,0),
Segment3D(0,0,0, 0,1,0)
)
result mustEqual true
}
"detect intersection of the first point" in {
//these segments intersect at (0, 0, 0); start of segment 1, middle of segment 2
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(0,0,0, 0,2,0),
Segment3D(-1,0,0, 1,0,0)
)
result mustEqual true
}
"detect intersection on the farther point(s)" in {
//these segments intersect at (0, 1, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(0,0,1, 0,1,0),
Segment3D(1,0,0, 0,1,0)
)
result mustEqual true
}
"detect intersection on the farther point" in {
//these segments intersect at (1, 1, 0); end of segment 1, middle of segment 2
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(1,0,0, 1,1,0),
Segment3D(2,0,0, 0,2,0)
)
result mustEqual true
}
"detect intersection in the middle(s)" in {
//these segments intersect at (0.5f, 0.5f, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(0,0,0, 1,1,0),
Segment3D(1,0,0, 0,1,0)
)
result mustEqual true
}
"detect intersection in the middle " in {
//these segments intersect at (0, 0.5, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(0,0,0, 1,0,0),
Segment3D(0.5f,1,0, 0.5f,-1,0)
)
result mustEqual true
}
"not detect intersection if the point of intersection would be before the start of the segments" in {
//these segments will not intersect as segments; but, as lines, they would intersect at (0, 0, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(1,1,0, 2,2,0),
Segment3D(1,0,0, 2,0,0)
)
result mustEqual false
}
"not detect intersection if the point of intersection would be after the end of the segments" in {
//these segments will not intersect as segments; but, as lines, they would intersect at (2, 2, 0)
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(0,0,0, 1,1,0),
Segment3D(2,0,0, 2,1,0)
)
result mustEqual false
}
"not detect intersection if the line segments are parallel" in {
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(0,0,0, 1,1,1),
Segment3D(1,1,2, 2,2,3)
)
result mustEqual false
}
"detect overlap" in {
//the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(0,0,0, 2,0,0),
Segment3D(1,0,0, 3,0,0)
)
result mustEqual true
}
"not detect intersection (generic skew)" in {
//these segments will not intersect
val result = Intersection.Test(Vector3.Zero, Vector3.Zero,
Segment3D(-3,-8,7, -3,-9,8),
Segment3D(6,3,0, 2,0,0)
)
result mustEqual false
}
}
}
object GeometryTest {
}