diff --git a/src/main/scala/net/psforever/objects/geometry/Closest.scala b/src/main/scala/net/psforever/objects/geometry/Closest.scala new file mode 100644 index 00000000..397a7d24 --- /dev/null +++ b/src/main/scala/net/psforever/objects/geometry/Closest.scala @@ -0,0 +1,280 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.geometry + +import net.psforever.types.Vector3 + +object Closest { + object Distance { + def apply(point : Vector3, seg : Segment2D) : Float = { + val segdx = seg.bx - seg.ax + val segdy = seg.by - seg.ay + ((point.x - seg.ax) * segdx + (point.y - seg.ay) * segdy) / + Vector3.MagnitudeSquared(Vector3(segdx, segdy, 0)) + } + + def apply(line1 : Line2D, line2 : Line2D) : Float = { + if (Intersection.Test(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(seg1: Segment2D, seg2: Segment2D): Float = { + if (Intersection.Test(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(v1a, seg2), + math.min( + apply(v1b, seg2), + math.min( + apply(v2a, seg1), + apply(v2b, seg1) + ) + ) + ) + } + } + + def apply(c1: Circle, c2 : Circle): Float = { + math.max(0, Vector3.Magnitude(Vector3(c1.x - c2.x, c1.y - c2.y, 0)) - c1.radius - c2.radius) + } + + /** + * na + * @param line1 na + * @param line2 na + * @return the shortest distance between the lines; + * if parallel, the common perpendicular distance between the lines; + * if coincidental, this distance will be 0 + */ + def apply(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 or coincidental + // construct a right triangle with one leg on line1 and the hypotenuse between the line's known points + val hypotenuse = Vector3(line2.x - line1.x, line2.y - line1.y, line2.z - line1.z) + val legOnLine1 = line1.d * Vector3.DotProduct(hypotenuse, line1.d) + Vector3.Magnitude(hypotenuse - legOnLine1) + } + } + + def apply(seg1: Segment3D, seg2: Segment3D): Float = { + //TODO make not as expensive as finding the plotted closest distance segment + Segment(seg1, seg2) match { + case Some(seg) => seg.length + case None => Float.MaxValue + } + } + + def apply(s1: Sphere, s2 : Sphere): Float = { + math.max(0, Vector3.Magnitude(Vector3(s1.x - s2.x, s1.y - s2.y, s1.z - s2.z)) - s1.radius - s2.radius) + } + } + + object Segment { + /** + * na + * @param c1 na + * @param c2 na + * @return a line segment that represents the closest distance between the circle's circumferences; + * `None`, if the circles have no distance between them (overlapping) + */ + def apply(c1 : Circle, c2 : Circle): Option[Segment2D] = { + val distance = Distance(c1, c2) + if (distance > 0) { + val c1x = c1.x + val c1y = c1.y + val v = Vector3.Unit(Vector3(c2.x - c1x, c2.y - c1y, 0f)) + val c1d = v * c1.radius + val c2d = v * c2.radius + Some( + Segment2D( + c1x + c1d.x, c1y + c1d.y, + c1x + c2d.x, c1y + c2d.y, + ) + ) + } else { + None + } + } + + /** + * na + * @param line1 na + * @param line2 na + * @return a line segment representing the closest distance between the two not intersecting lines; + * in the case of parallel lines, one of infinite closest distances is plotted; + * `None`, if the lines intersect with each other + */ + def apply(line1 : Line3D, line2 : Line3D): Option[Segment3D] = { + val p1 = Vector3(line1.x, line1.y, line1.z) + val p3 = Vector3(line2.x, line2.y, line2.z) + 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) { + // without a denominator, we have no cross product solution + val p13u = Vector3.Unit(p13) + if (p21 == p13u || p21 == Vector3.neg(p13u)) { //coincidental lines overlap / intersect + None + } else { //parallel lines + val connecting = Vector3(line2.x - line1.x, line2.y - line1.y, line2.z - line1.z) + val legOnLine1 = line1.d * Vector3.DotProduct(connecting, line1.d) + val v = connecting - legOnLine1 + Some(Segment3D( + line1.x, line1.y, line1.z, + line1.x + v.x, line1.y + v.y, line1.z + v.z + )) + } + } 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(line1 : Segment3D, line2 : Segment3D): Option[Segment3D] = { + val uline1 = Vector3.Unit(line1.d) + val uline2 = Vector3.Unit(line2.d) + apply(Line3D(line1.ax, line1.ay, line1.az, uline1), Line3D(line2.ax, line2.ay, line2.az, uline2)) match { + case Some(seg: Segment3D) => // common skew lines and parallel 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 => + val connectingU = Vector3.Unit(Vector3(line2.ax - line1.ax, line2.ay - line1.ay, line2.az - line1.az)) + if (uline1 == connectingU || uline1 == Vector3.neg(connectingU)) { // coincidental line segments + val line1a = Vector3(line1.ax, line1.ay, line1.az) + val line1b = Vector3(line1.bx, line1.by, line1.bz) + val line2a = Vector3(line2.ax, line2.ay, line2.az) + val line2b = Vector3(line2.bx, line2.by, line2.bz) + if (Vector3.Unit(line2a - line1a) != Vector3.Unit(line2b - line1a) || + Vector3.Unit(line2a - line1b) != Vector3.Unit(line2b - line1b) || + Vector3.Unit(line1a - line2a) != Vector3.Unit(line1b - line2a) || + Vector3.Unit(line1a - line2b) != Vector3.Unit(line1b - line2b)) { + Some(Segment3D( + line1.ax, line1.ay, line1a.z, + line1.ax, line1.ay, line1a.z + )) // overlap regions + } + else { + val segs = List((line1a, line2a), (line1a, line2b), (line2a, line1b)) + val (a, b) = segs({ + //val dist = segs.map { case (_a, _b) => Vector3.DistanceSquared(_a, _b) } + //dist.indexOf(dist.min) + var index = 0 + var minDist = Vector3.DistanceSquared(segs.head._1, segs.head._2) + (1 to 2).foreach { i => + val dist = Vector3.DistanceSquared(segs(i)._1, segs(i)._2) + if (minDist < dist) { + index = i + minDist = dist + } + } + index + }) + Some(Segment3D(a.x, a.y, a.z, b.x, b.y, b.z)) // connecting across the smallest gap + } + } else { + None + } + } + } + + /** + * na + * @param s1 na + * @param s2 na + * @return a line segment that represents the closest distance between the sphere's surface areas; + * `None`, if the spheres have no distance between them (overlapping) + */ + def apply(s1 : Sphere, s2 : Sphere): Option[Segment3D] = { + val distance = Distance(s1, s2) + if (distance > 0) { + val s1x = s1.x + val s1y = s1.y + val s1z = s1.z + val v = Vector3.Unit(Vector3(s2.x - s1x, s2.y - s1y, s2.z - s1z)) + val s1d = v * s1.radius + val s2d = v * (s1.radius + distance) + Some(Segment3D(s1x + s1d.x, s1y + s1d.y, s1y + s1d.y, s1x + s2d.x, s1y + s2d.y, s1y + s2d.y)) + } else { + None + } + } + + def apply(line : Line3D, sphere : Sphere): Option[Segment3D] = { + val sphereAsPoint = Vector3(sphere.x, sphere.y, sphere.z) + val lineAsPoint = Vector3(line.x, line.y, line.z) + val direct = sphereAsPoint - lineAsPoint + val projectionOfDirect = line.d * Vector3.DotProduct(direct, line.d) + val heightFromProjection = projectionOfDirect - direct + val heightFromProjectionDist = Vector3.Magnitude(heightFromProjection) + if (heightFromProjectionDist <= sphere.radius) { //intersection + None + } else { + val pointOnLine = lineAsPoint + projectionOfDirect + val pointOnSphere = pointOnLine + + Vector3.Unit(heightFromProjection) * (heightFromProjectionDist - sphere.radius) + Some(Segment3D( + pointOnLine.x, pointOnLine.y, pointOnLine.z, + pointOnSphere.x, pointOnSphere.y, pointOnSphere.z + )) + } + } + } +} diff --git a/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala b/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala deleted file mode 100644 index ddd5ab15..00000000 --- a/src/main/scala/net/psforever/objects/geometry/ClosestDistance.scala +++ /dev/null @@ -1,173 +0,0 @@ -// 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 index 4233b689..1a679d02 100644 --- a/src/main/scala/net/psforever/objects/geometry/Geometry.scala +++ b/src/main/scala/net/psforever/objects/geometry/Geometry.scala @@ -40,6 +40,12 @@ object Segment2D { } } +final case class Circle(x: Float, y: Float, radius: Float) + +object Circle { + def apply(radius: Float): Circle = Circle(0f, 0f, radius) +} + 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 { @@ -52,6 +58,20 @@ object Segment3D { } } +final case class Sphere(x: Float, y: Float, z: Float, radius: Float) + +final case class Cylinder(circle: Circle, z: Float, height: Float) + +object Cylinder { + def apply(x: Float, y: Float, z: Float, radius: Float, height: Float): Cylinder = { + Cylinder(Circle(x, y, radius), z, height) + } +} + +object Sphere { + def apply(p: Vector3, radius: Float): Sphere = Sphere(p.x, p.y, p.z, radius) +} + object Geometry { def equalFloats(value1: Float, value2: Float, off: Float = 0.001f): Boolean = { val diff = value1 - value2 diff --git a/src/main/scala/net/psforever/objects/geometry/Intersection.scala b/src/main/scala/net/psforever/objects/geometry/Intersection.scala index 74dc781c..a2e2834c 100644 --- a/src/main/scala/net/psforever/objects/geometry/Intersection.scala +++ b/src/main/scala/net/psforever/objects/geometry/Intersection.scala @@ -8,17 +8,17 @@ object Intersection { /** * 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. + * In that case, however, they can still "intersect" if provided that the lines are coincidental. */ - def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line2D, line2 : Line2D): Boolean = { + def apply(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) + u == Vector3.Zero || line1.d == u || line1.d == Vector3.neg(u) } } - private def pointOnSegment(ax : Float, ay : Float, px : Float, py : Float, bx : Float, by : Float): Boolean = { + 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) } @@ -41,9 +41,9 @@ object Intersection { * @return the orientation value */ private def orientationOfPoints( - ax : Float, ay : Float, - px : Float, py : Float, - bx : Float, by : Float + 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 @@ -57,16 +57,16 @@ object Intersection { * 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 = { + def apply(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 ln1ax = line1.ax + val ln1ay = line1.ay + val ln1bx = line1.bx + val ln1by = line1.by + val ln2ax = line2.ax + val ln2ay = line2.ay + val ln2bx = line2.bx + val ln2by = line2.by 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) @@ -85,11 +85,15 @@ object Intersection { * 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(line1: Line3D, line2: Line3D): Boolean = { + apply(line1, line2, 0.15f) } - def apply(origin1 : Vector3, origin2 : Vector3, line1 : Line3D, line2 : Line3D, threshold: Float): Boolean = { - ClosestDistance.Between(origin1, origin2, line1, line2) < threshold + def apply(line1: Line3D, line2: Line3D, threshold: Float): Boolean = { + Closest.Distance(line1, line2) < threshold + } + + def apply(c1: Circle, c2 : Circle): Boolean = { + Vector3.Magnitude(Vector3(c1.x - c2.x, c1.y - c2.y, 0)) <= c1.radius + c2.radius } /** @@ -97,11 +101,58 @@ object Intersection { * 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(seg1: Segment3D, seg2: Segment3D): Boolean = { + apply(seg1, seg2, 0.15f) } - def apply(origin1 : Vector3, origin2 : Vector3, seg1 : Segment3D, seg2 : Segment3D, threshold: Float): Boolean = { - ClosestDistance.Between(origin1, origin2, seg1, seg2) < threshold + def apply(seg1: Segment3D, seg2: Segment3D, threshold: Float): Boolean = { + Closest.Distance(seg1, seg2) < threshold + } + + def apply(s1: Sphere, s2 : Sphere): Boolean = { + Vector3.Magnitude( + Vector3( + s1.x - s2.x, + s1.y - s2.y, + s1.z - s2.z + ) + ) <= s1.radius + s2.radius + } + + def apply(c1: Cylinder, c2: Cylinder): Boolean = { + apply(c1.circle, c2.circle) && + ((c1.height >= c2.z && c1.z <= c2.height) || (c2.height >= c1.z && c2.z <= c1.height)) + } + + def apply(cylinder: Cylinder, sphere: Sphere): Boolean = { + val cylinderCircle = cylinder.circle + val cylinderCircleRadius = cylinderCircle.radius + val cylinderTop = cylinder.z + cylinder.height + val sphereRadius = sphere.radius + val sphereBase = sphere.z - sphereRadius + val sphereTop = sphere.z + sphereRadius + if (apply(cylinderCircle, Circle(sphere.x, sphere.y, sphereRadius)) && + ((sphereTop >= cylinder.z && sphereBase <= cylinderTop) || + (cylinderTop >= sphereBase && cylinder.z <= sphereTop))) { + // potential intersection ... + val sphereAsPoint = Vector3(sphere.x, sphere.y, sphere.z) + val cylinderAsPoint = Vector3(cylinderCircle.x, cylinderCircle.y, cylinder.z) + val segmentFromCylinderToSphere = sphereAsPoint - cylinderAsPoint + val segmentFromCylinderToSphereXY = segmentFromCylinderToSphere.xy + if ((cylinder.z <= sphere.z && sphere.z <= cylinderTop) || + Vector3.MagnitudeSquared(segmentFromCylinderToSphereXY) <= cylinderCircleRadius * cylinderCircleRadius) { + true // top or bottom of sphere, or widest part of the sphere, must interact with the cylinder + } else { + // only option left is the curves of the sphere interacting with the cylinder's rim, top or base + val directionFromCylinderToSphere = Vector3.Unit(segmentFromCylinderToSphereXY) + val pointOnCylinderRimBase = cylinderAsPoint + directionFromCylinderToSphere * cylinderCircleRadius + val pointOnCylinderRimTop = pointOnCylinderRimBase + Vector3.z(cylinder.height) + val sqSphereRadius = sphereRadius * sphereRadius + Vector3.DistanceSquared(sphereAsPoint, pointOnCylinderRimTop) <= sqSphereRadius || + Vector3.DistanceSquared(sphereAsPoint, pointOnCylinderRimBase) <= sqSphereRadius + } + } else { + false + } } } } diff --git a/src/test/scala/objects/GeometryTest.scala b/src/test/scala/objects/GeometryTest.scala index 71b4734e..ea782da9 100644 --- a/src/test/scala/objects/GeometryTest.scala +++ b/src/test/scala/objects/GeometryTest.scala @@ -1,15 +1,175 @@ // Copyright (c) 2020 PSForever package objects -import net.psforever.objects.geometry.{Intersection, Line3D, Segment3D} +import net.psforever.objects.geometry._ import net.psforever.types.Vector3 import org.specs2.mutable.Specification class IntersectionTest extends Specification { + "Line2D" should { + "detect intersection on target points(s)" in { + //these lines intersect at (0, 0) + val result = Intersection.Test( + Line2D(0,0, 1,0), + Line2D(0,0, 0,1) + ) + result mustEqual true + } + + "detect intersection on a target point" in { + //these lines intersect at (0, 0); start of segment 1, middle of segment 2 + val result = Intersection.Test( + Line2D( 0,0, 0,1), + Line2D(-1,0, 1,0) + ) + result mustEqual true + } + + "detect intersection anywhere else" in { + //these lines intersect at (0.5f, 0.5f) + val result = Intersection.Test( + Line2D(0,0, 1,1), + Line2D(1,0, 0,1) + ) + result mustEqual true + } + + "detect intersection anywhere else (2)" in { + //these lines intersect at (0, 0.5) + val result = Intersection.Test( + Line2D(0, 0, 1, 0), + Line2D(0.5f,1, 0.5f,-1) + ) + result mustEqual true + } + + "not detect intersection if the lines are parallel" in { + val result = Intersection.Test( + Line2D(0,0, 1,1), + Line2D(1,0, 2,1) + ) + result mustEqual false + } + + "detect intersection if the lines overlap" in { + //the lines are coincidental + val result = Intersection.Test( + Line2D(0,0, 1,1), + Line2D(1,1, 2,2) + ) + result mustEqual true + } + } + + "Segment2D" should { + "detect intersection on target points(s)" in { + //these line segments intersect at (0, 0) + val result = Intersection.Test( + Segment2D(0,0, 1,0), + Segment2D(0,0, 0,1) + ) + result mustEqual true + } + + "detect intersection on a target point" in { + //these line segments intersect at (0, 0); start of segment 1, middle of segment 2 + val result = Intersection.Test( + Segment2D( 0,0, 0,1), + Segment2D(-1,0, 1,0) + ) + result mustEqual true + } + + "detect intersection anywhere else" in { + //these line segments intersect at (0.5f, 0.5f) + val result = Intersection.Test( + Segment2D(0,0, 1,1), + Segment2D(1,0, 0,1) + ) + result mustEqual true + } + + "detect intersection anywhere else (2)" in { + //these line segments intersect at (0, 0.5) + val result = Intersection.Test( + Segment2D(0, 0, 1, 0), + Segment2D(0.5f,1, 0.5f,-1) + ) + result mustEqual true + } + + "not detect intersection if the lines are parallel" in { + val result = Intersection.Test( + Segment2D(0,0, 1,1), + Segment2D(1,0, 2,1) + ) + result mustEqual false + } + + "detect intersection if the lines overlap" in { + //the lines are coincidental + val result = Intersection.Test( + Line2D(0,0, 1,1), + Line2D(1,1, 2,2) + ) + result mustEqual true + } + } + + "Circle" should { + "intersect when overlapping (coincidental)" in { + val result = Intersection.Test( + Circle(0,0, 1), + Circle(0,0, 1) + ) + result mustEqual true + } + + "intersect when overlapping (engulfed)" in { + val result = Intersection.Test( + Circle(0,0, 2), + Circle(1,0, 1) + ) + result mustEqual true + } + + "intersect when overlapping (partial 1)" in { + val result = Intersection.Test( + Circle(0,0, 2), + Circle(2,0, 1) + ) + result mustEqual true + } + + "intersect when overlapping (partial 2)" in { + val result = Intersection.Test( + Circle(0, 0, 2), + Circle(2.5f,0, 1) + ) + result mustEqual true + } + + "intersect when the circumferences are touching" in { + val result = Intersection.Test( + Circle(0,0, 2), + Circle(3,0, 1) + ) + result mustEqual true + } + + "not intersect when not touching" in { + val result = Intersection.Test( + Circle(0,0, 2), + Circle(4,0, 1) + ) + result mustEqual false + } + } + "Line3D" should { "detect intersection on target point(s)" in { //these lines intersect at (0, 0, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Line3D(0,0,0, Vector3(1,0,0)), Line3D(0,0,0, Vector3(0,1,0)) ) @@ -18,60 +178,42 @@ class IntersectionTest extends Specification { "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, + val result = Intersection.Test( 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 { + "detect intersection anywhere else" in { //these lines intersect at (0.5f, 0.5f, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( 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 { + "detect intersection anywhere else (2)" in { //these lines intersect at (0, 0.5, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( 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, + "not detect intersection if the lines are parallel" in { + val result = Intersection.Test( 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 { + "detect intersection if the lines 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, + val result = Intersection.Test( Line3D(0,0,0, Vector3.Unit(Vector3(2,0,0))), Line3D(1,0,0, Vector3.Unit(Vector3(3,0,0))) ) @@ -80,7 +222,7 @@ class IntersectionTest extends Specification { "not detect intersection (generic skew)" in { //these segments will not intersect - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(-3,-8,7, Vector3.Unit(Vector3(-3,-9,8))), Segment3D(6,3,0, Vector3.Unit(Vector3(2,0,0))) ) @@ -91,7 +233,7 @@ class IntersectionTest extends Specification { "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, + val result = Intersection.Test( Segment3D(0,0,0, 1,0,0), Segment3D(0,0,0, 0,1,0) ) @@ -100,7 +242,7 @@ class IntersectionTest extends Specification { "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, + val result = Intersection.Test( Segment3D(0,0,0, 0,2,0), Segment3D(-1,0,0, 1,0,0) ) @@ -109,7 +251,7 @@ class IntersectionTest extends Specification { "detect intersection on the farther point(s)" in { //these segments intersect at (0, 1, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,1, 0,1,0), Segment3D(1,0,0, 0,1,0) ) @@ -118,7 +260,7 @@ class IntersectionTest extends Specification { "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, + val result = Intersection.Test( Segment3D(1,0,0, 1,1,0), Segment3D(2,0,0, 0,2,0) ) @@ -127,7 +269,7 @@ class IntersectionTest extends Specification { "detect intersection in the middle(s)" in { //these segments intersect at (0.5f, 0.5f, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 1,1,0), Segment3D(1,0,0, 0,1,0) ) @@ -136,7 +278,7 @@ class IntersectionTest extends Specification { "detect intersection in the middle " in { //these segments intersect at (0, 0.5, 0) - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 1,0,0), Segment3D(0.5f,1,0, 0.5f,-1,0) ) @@ -145,7 +287,7 @@ class IntersectionTest extends Specification { "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, + val result = Intersection.Test( Segment3D(1,1,0, 2,2,0), Segment3D(1,0,0, 2,0,0) ) @@ -154,7 +296,7 @@ class IntersectionTest extends Specification { "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, + val result = Intersection.Test( Segment3D(0,0,0, 1,1,0), Segment3D(2,0,0, 2,1,0) ) @@ -162,33 +304,230 @@ class IntersectionTest extends Specification { } "not detect intersection if the line segments are parallel" in { - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(0,0,0, 1,1,1), Segment3D(1,1,2, 2,2,3) ) result mustEqual false } - "detect overlap" in { + "detect intersection with overlapping" 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, + val result = Intersection.Test( Segment3D(0,0,0, 2,0,0), Segment3D(1,0,0, 3,0,0) ) result mustEqual true } + "not detect intersection with coincidental, non-overlapping" in { + //the sub-segment (1,0,0) to (2,0,0) is an overlap region shared between the two segments + val result = Intersection.Test( + Segment3D(0,0,0, 1,0,0), + Segment3D(2,0,0, 3,0,0) + ) + result mustEqual false + } + "not detect intersection (generic skew)" in { //these segments will not intersect - val result = Intersection.Test(Vector3.Zero, Vector3.Zero, + val result = Intersection.Test( Segment3D(-3,-8,7, -3,-9,8), Segment3D(6,3,0, 2,0,0) ) result mustEqual false } } + + "Sphere" should { + "intersect when overlapping (coincidental)" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 1), + Sphere(Vector3.Zero, 1) + ) + result mustEqual true + } + + "intersect when overlapping (engulfed)" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 5), + Sphere(Vector3(1,0,0), 1) + ) + result mustEqual true + } + + "intersect when overlapping (partial 1)" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 2), + Sphere(Vector3(2,0,0), 1) + ) + result mustEqual true + } + + "intersect when overlapping (partial 2)" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 2), + Sphere(Vector3(2.5f,0,0), 1) + ) + result mustEqual true + } + + "intersect when the circumferences are touching" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 2), + Sphere(Vector3(3,0,0), 1) + ) + result mustEqual true + } + + "not intersect when not touching" in { + val result = Intersection.Test( + Sphere(Vector3.Zero, 2), + Sphere(Vector3(4,0,0), 1) + ) + result mustEqual false + } + } + + "Cylinder" should { + "detect intersection if overlapping" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 2), + Cylinder(0, 0, 0, 1, 2) + ) + result mustEqual true + } + + "detect intersection if sides clip" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 2), + Cylinder(0.5f, 0.5f, 0, 1, 2) + ) + result mustEqual true + } + + "detect intersection if touching" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 2), + Cylinder(1, 0, 0, 1, 2) + ) + result mustEqual true + } + + "detect intersection if stacked" in { + val result = Intersection.Test( + Cylinder(1, 0, 0, 1, 2), + Cylinder(1, 0, 2, 1, 2) + ) + result mustEqual true + } + + "detect intersection if one is sunken into the other" in { + val result = Intersection.Test( + Cylinder(1, 0, 0, 1, 2), + Cylinder(1, 0, 1, 1, 2) + ) + result mustEqual true + } + + "not detect intersection if not near each other" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 2), + Cylinder(2, 2, 0, 1, 2) + ) + result mustEqual false + } + + "not detect intersection if one is too high / low" in { + val result = Intersection.Test( + Cylinder(1, 0, 0, 1, 2), + Cylinder(1, 0, 5, 1, 2) + ) + result mustEqual false + } + } + + "Cylinder and Sphere" should { + "detect intersection if overlapping" in { + val result = Intersection.Test( + Cylinder(1, 0, 0, 1, 1), + Sphere(1, 0, 2, 1) + ) + result mustEqual true + } + + "detect intersection if cylinder top touches sphere base" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(1, 0, 2, 1) + ) + result mustEqual true + } + + "detect intersection if cylinder base touches sphere top" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(-1, 0, -1, 1) + ) + result mustEqual true + } + + "detect intersection if cylinder edge touches sphere edge" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(2, 0, 0.5f, 1) + ) + result mustEqual true + } + + "detect intersection if on cylinder top rim" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(1.75f, 0, 1.25f, 1) + ) + result mustEqual true + } + + "detect intersection if on cylinder base rim" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(1.75f, 0, -0.5f, 1) + ) + result mustEqual true + } + + "not detect intersection if too far above" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(0, 0, 3, 1) + ) + result mustEqual false + } + + "not detect intersection if too far below" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(0, 0, -3, 1) + ) + result mustEqual false + } + + "not detect intersection if too far out (sideways)" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(2, 2, 0, 1) + ) + result mustEqual false + } + + "not detect intersection if too far out (skew)" in { + val result = Intersection.Test( + Cylinder(0, 0, 0, 1, 1), + Sphere(1.5f, 1.5f, 1.5f, 1) + ) + result mustEqual false + } + } } -object GeometryTest { - -} +object GeometryTest { }