modified shape structures and operations on said shapes

This commit is contained in:
Jason_DiDonato@yahoo.com 2021-01-24 23:11:02 -05:00
parent 4304ea7f4d
commit 9d86844396
5 changed files with 759 additions and 242 deletions

View file

@ -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<u,v,w> = 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
))
}
}
}
}

View file

@ -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<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

@ -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

View file

@ -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
}
}
}
}

View file

@ -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 { }