From 4304ea7f4dbfa5efa1a0b8aa680e7b97bbc7195f Mon Sep 17 00:00:00 2001 From: "Jason_DiDonato@yahoo.com" Date: Wed, 20 Jan 2021 22:56:29 -0500 Subject: [PATCH] line and line segment intersection code and tests --- .../actors/session/SessionActor.scala | 2 +- .../objects/ExplosiveDeployable.scala | 2 +- .../objects/geometry/ClosestDistance.scala | 173 ++++++++++++++++ .../psforever/objects/geometry/Geometry.scala | 79 +++++++ .../objects/geometry/Intersection.scala | 107 ++++++++++ .../scala/net/psforever/types/Vector3.scala | 8 + src/test/scala/objects/GeometryTest.scala | 194 ++++++++++++++++++ 7 files changed, 563 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/Geometry.scala create mode 100644 src/main/scala/net/psforever/objects/geometry/Intersection.scala create mode 100644 src/test/scala/objects/GeometryTest.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index d0b120fb..f10a9ca6 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -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 diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 591d004f..812fd407 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -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( diff --git a/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala b/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala new file mode 100644 index 00000000..ddd5ab15 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala @@ -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 = 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 + } + } + } +} diff --git a/src/main/scala/net/psforever/objects/geometry/Geometry.scala b/src/main/scala/net/psforever/objects/geometry/Geometry.scala new file mode 100644 index 00000000..4233b689 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala @@ -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 + } + } +} diff --git a/src/main/scala/net/psforever/objects/geometry/Intersection.scala b/src/main/scala/net/psforever/objects/geometry/Intersection.scala new file mode 100644 index 00000000..74dc781c --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/Intersection.scala @@ -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 + } + } +} diff --git a/src/main/scala/net/psforever/types/Vector3.scala b/src/main/scala/net/psforever/types/Vector3.scala index 43c4f43f..e008c7a2 100644 --- a/src/main/scala/net/psforever/types/Vector3.scala +++ b/src/main/scala/net/psforever/types/Vector3.scala @@ -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 diff --git a/src/test/scala/objects/GeometryTest.scala b/src/test/scala/objects/GeometryTest.scala new file mode 100644 index 00000000..71b4734e --- /dev/null +++ b/src/test/scala/objects/GeometryTest.scala @@ -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 { + +}