diff options
Diffstat (limited to 'src/test/scala')
26 files changed, 1622 insertions, 0 deletions
diff --git a/src/test/scala/sims/test/ClippingTest.scala b/src/test/scala/sims/test/ClippingTest.scala new file mode 100644 index 0000000..317abfe --- /dev/null +++ b/src/test/scala/sims/test/ClippingTest.scala @@ -0,0 +1,58 @@ +package sims.test + +import org.scalatest.FunSuite +import org.scalatest.matchers.ShouldMatchers + +import sims.math._ +import sims.collision._ + +class ClippingTest extends FunSuite with ShouldMatchers { + + /* + test("clipping segment to segment, outside clipping zone") { + val A = new Segment((1, 1), (3, 0)) + val B = new Segment((0,0), (0, 1)) + val C = new Segment((4,-1), (10, 5)) + + (B clipped A) should equal (None) + (C clipped A) should equal (None) + } + + test("clipping segment to segment, inside clipping zone") { + val segments = List( + new Segment((2, 3), (4.5, 8)), + new Segment((3, -4), (2, -5)), + new Segment((0, 0), (10, 0)), + new Segment((1.1, -1234), (9.999, 0.001)), + new Segment((5, 5), (5, -5)) + ) + + val clippers = List( + new Segment((0, 0), (10, 0)), + new Segment((10, 0), (0, 0)), + new Segment((120, -0.01), (-130, -0.01)), + new Segment((-2.3, 10000), (14, 10000)) + ) + + def translate(s: Segment, v: Vector2D) = { + new Segment(s.point1 + v, s.point2 + v) + } + + def rotate(s: Segment, r: Double) = { + new Segment(s.point1 rotate r, s.point2 rotate r) + } + + for (s <- segments; c <- clippers) {(s clipped c).get should equal (s)} + + for (i <- 0 until 1000) for(s <- segments; c <- clippers) { + val r = new scala.util.Random + def next = r.nextDouble * 1000 * (if (r.nextBoolean) -1 else 1) + val x = (next, next) + val y = next + (translate(rotate(s, y), x) clipped translate(rotate(c, y), x)).get should equal (translate(rotate(s, y), x)) + } + + } + */ + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/LinearOverlapTest.scala b/src/test/scala/sims/test/LinearOverlapTest.scala new file mode 100644 index 0000000..aa99f6f --- /dev/null +++ b/src/test/scala/sims/test/LinearOverlapTest.scala @@ -0,0 +1,92 @@ +package sims.test + +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers + +import sims.math._ +import sims.collision._ + +class LinearOverlapTest extends FlatSpec with ShouldMatchers { + + "A segment" should "throw IllegalArgumentException if both of its " + + "vertices degenerate into a single one" in { + evaluating { val s1 = Segment(Vector2D(0,0), Vector2D(0,0)) } should produce [IllegalArgumentException] + } + + it should "not intersect with itself" in { + val s1 = Segment(Vector2D(2, 2), Vector2D(3, 5)) + s1 intersection s1 should equal (None) + } + + "Two segments" should "have an intersection point if they intersect" in { + val s1 = Segment(Vector2D(0, 0), Vector2D(3, 1)) + val s2 = Segment(Vector2D(0, 1), Vector2D(3, -2)) + s1 intersection s2 should not equal (None) + } + + it should "have an intersection point if they share a vertice" in { + val s1 = Segment(Vector2D(1, 2), Vector2D(3, 1)) + val s2 = Segment(Vector2D(3, 1), Vector2D(3, -2)) + s1 intersection s2 should not equal (None) + } + + it should "have an intersection point if one contains one of the other's vertices" in { + val s1 = Segment(Vector2D(2, 4), Vector2D(3, 100)) + val s2 = Segment(Vector2D(1, 3), Vector2D(3, 5)) + s1 intersection s2 should not equal (None) + } + + it should "not have an intersection point if they are parallel" in { + val s1 = Segment(Vector2D(0, 0), Vector2D(3, 1)) + val s2 = Segment(Vector2D(0, 1), Vector2D(3, 2)) + s1 intersection s2 should equal (None) + } + + it should "not have an intersection point if they are parallel and lie on each other" in { + val s1 = Segment(Vector2D(2, 2), Vector2D(6, 6)) + val s2 = Segment(Vector2D(3, 3), Vector2D(4, 4)) + s1 intersection s2 should equal (None) + } + + + "A ray and a segment" should "have an intersection point if they intersect" in { + val r1 = Ray(Vector2D(3, 5), Vector2D(3, -1)) + val s1 = Segment(Vector2D(6.32, math.sqrt(4.0)), Vector2D(10, 15.5)) + r1 intersection s1 should not equal (None) + } + + it should "have an intersection point if they share a vertice" in { + val r1 = Ray(Vector2D(3, 4), Vector2D(2, 1)) + val s1 = Segment(Vector2D(0, 10), Vector2D(3, 4)) + r1 intersection s1 should not equal (None) + } + + it should "have an intersection point if the ray contains one of the segment's vertices" in { + val r1 = Ray(Vector2D(0, 0), Vector2D(1, 2)) + val s1 = Segment(Vector2D(2, 4), Vector2D(5, 4)) + r1 intersection s1 should not equal (None) + } + + it should "have an intersection point if the segment contains the ray's vertice" in { + val r1 = Ray(Vector2D(0, math.Pi), Vector2D(1, 2)) + val s1 = Segment(Vector2D(0, 0), Vector2D(0, 4)) + r1 intersection s1 should not equal (None) + + val r2 = Ray(Vector2D(2, 3), Vector2D(-2, -1)) + val s2 = Segment(Vector2D(0, 4), Vector2D(4, 2)) + r2 intersection s2 should not equal (None) + } + + it should "not have an intersection point if they are parallel" in { + val r1 = Ray(Vector2D(2, 3), Vector2D(3, 4)) + val s1 = Segment(Vector2D(1, 4), Vector2D(4, 8)) + r1 intersection s1 should equal (None) + } + + it should "not have an intersection point if they lie on each other" in { + val r1 = Ray(Vector2D(0, 1), Vector2D(2, 3)) + val s1 = Segment(Vector2D(-1, 0), Vector2D(4, 4)) + r1 intersection s1 should equal (None) + } + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gjk/EPA.scala b/src/test/scala/sims/test/gjk/EPA.scala new file mode 100644 index 0000000..e64a73e --- /dev/null +++ b/src/test/scala/sims/test/gjk/EPA.scala @@ -0,0 +1,98 @@ +package sims.test.gjk + +import scala.collection.mutable.ListBuffer +import sims.math._ + +/** The implementation was adapted from dyn4j by William Bittle (see http://www.dyn4j.org). */ +object EPA { + + val MaxIterations = 100 + + val DistanceEpsilon = 0.0001 + + protected class Edge(val normal: Vector2D, val distance: Double, val index: Int) + + private def winding(simplex: ListBuffer[Vector2D]) = { + for (i <- 0 until simplex.size) { + val j = if (i + 1 == simplex.size) 0 else i + 1 + //val winding = math.signum(simplex(j) - simplex(i)) + } + + } + + + def getPenetration(simplex: ListBuffer[Vector2D], minkowskiSum: MinkowskiSum): Penetration = { + // this method is called from the GJK detect method and therefore we can assume + // that the simplex has 3 points + + // get the winding of the simplex points + // the winding may be different depending on the points added by GJK + // however EPA will preserve the winding so we only need to compute this once + val winding = math.signum((simplex(1) - simplex(0)) cross (simplex(2) - simplex(1))).toInt + // store the last point added to the simplex + var point = Vector2D.Null + // the current closest edge + var edge: Edge = null; + // start the loop + for (i <- 0 until MaxIterations) { + // get the closest edge to the origin + edge = this.findClosestEdge(simplex, winding); + // get a new support point in the direction of the edge normal + point = minkowskiSum.support(edge.normal); + + // see if the new point is significantly past the edge + val projection: Double = point dot edge.normal + if ((projection - edge.distance) < DistanceEpsilon) { + // then the new point we just made is not far enough + // in the direction of n so we can stop now and + // return n as the direction and the projection + // as the depth since this is the closest found + // edge and it cannot increase any more + return new Penetration(edge.normal, projection) + } + + // lastly add the point to the simplex + // this breaks the edge we just found to be closest into two edges + // from a -> b to a -> newPoint -> b + simplex.insert(edge.index, point); + } + // if we made it here then we know that we hit the maximum number of iterations + // this is really a catch all termination case + // set the normal and depth equal to the last edge we created + return new Penetration(edge.normal, point dot edge.normal) + } + + private def findClosestEdge(simplex: ListBuffer[Vector2D], winding: Int): Edge = { + // get the current size of the simplex + val size = simplex.size; + // create an edge + var edge = new Edge(Vector2D.Null, Double.PositiveInfinity, 0) + // find the edge on the simplex closest to the origin + for (i <- 0 until size) { + // compute j + val j = if (i + 1 == size) 0 else i + 1 + // get the points that make up the current edge + val a = simplex(i); + val b = simplex(j); + // create the edge + val direction = simplex(j) - simplex(i); + // depending on the winding get the edge normal + // it would findClosestEdge(List<Vector2> simplex, int winding) { + // get the current size of the simplex + val normal = if (winding > 0) direction.rightNormal.unit + else direction.leftNormal.unit + + // project the first point onto the normal (it doesnt matter which + // you project since the normal is perpendicular to the edge) + val d: Double = math.abs(simplex(i) dot normal); + // record the closest edge + if (d < edge.distance) + edge = new Edge(normal, d, j) + } + // return the closest edge + return edge; + } + +} + +class Penetration(val normal: Vector2D, val overlap: Double)
\ No newline at end of file diff --git a/src/test/scala/sims/test/gjk/FeatureManifold.scala b/src/test/scala/sims/test/gjk/FeatureManifold.scala new file mode 100644 index 0000000..527d8ba --- /dev/null +++ b/src/test/scala/sims/test/gjk/FeatureManifold.scala @@ -0,0 +1,89 @@ +package sims.test.gjk + +import sims.collision._ +import sims.math._ + +object FeatureManifold { + + def farthestFeature(collidable: Collidable, direction: Vector2D): Either[Vector2D, Segment] = collidable match { + + case c: Circle => Left(c.position + direction.unit * c.radius) + + case p: ConvexPolygon => { + var max = p.vertices(0) dot direction.unit + + //maximum vertice index + var i = 0 + + for (j <- 0 until p.vertices.length) { + val d = p.vertices(j) dot direction.unit + if (d > max) { + max = d + i = j + } + } + + /* 1) vertex is considered to be the first point of a segment + * 2) polygons vertices are ordered counter-clockwise + * + * implies: + * previous segment is the (i-1)th segment + * next segment is the ith segment */ + val prev = if (i == 0) p.sides.last else p.sides(i - 1) + val next = p.sides(i) + + // check which segment is less parallel to direction + val side = + if ((prev.direction0 dot direction).abs <= (next.direction0 dot direction).abs) prev + else next + + Right(side) + } + + case _ => throw new IllegalArgumentException("Collidable is of unknown type.") + + } + + def getCollisionPoints(pair: (Collidable, Collidable), normal: Vector2D): Option[Manifold] = { + var points = new scala.collection.mutable.ArrayBuffer[Vector2D] + + val feature1 = farthestFeature(pair._1, normal) + + //is feature 1 a vertex? + if (feature1.isLeft) { + return Some(Manifold(Array(feature1.left.get), normal)) + } + + val feature2 = farthestFeature(pair._2, -normal) + + //is feature 2 a vertex? + if (feature2.isLeft) { + return Some(Manifold(Array(feature2.left.get), -normal)) + } + + //neither feature is a vertex + val side1 = feature1.right.get + val side2 = feature2.right.get + + + val flipped = (side1.direction0 dot normal).abs > (side2.direction0 dot normal).abs + val reference = if (!flipped) side1 else side2 + val incident = if (!flipped) side2 else side1 + + + //both features are sides, clip feature2 to feature1 + val clipped = incident clipped reference + + /*val n = if (!flipped) normal else -normal + clipped match { + case None => None + case Some(Segment(point1, point2)) => Some( + Manifold(Array(point1, point2) filter ((v: Vector2D) => ((v - reference.point1) dot n) <= 0), n) + ) + }*/ + error("") + } + +} + +case class Manifold(pts: Array[Vector2D], normal: Vector2D)
\ No newline at end of file diff --git a/src/test/scala/sims/test/gjk/GJK.scala b/src/test/scala/sims/test/gjk/GJK.scala new file mode 100644 index 0000000..a8b1732 --- /dev/null +++ b/src/test/scala/sims/test/gjk/GJK.scala @@ -0,0 +1,115 @@ +package sims.test.gjk + +import scala.collection.mutable.ListBuffer +import sims.collision._ +import sims.collision.narrowphase.NarrowPhaseDetector +import sims.math._ + +/** A narrowphase detector using the Gilbert-Johnson-Keerthi algorithm. */ +class GJK[A <: Collidable: ClassManifest] extends NarrowPhaseDetector[A] { + + /** Checks whether the given simplex contains the origin. If it does, `None` is returned. + * Otherwise a new search direction is returned and the simplex is updated. */ + protected def checkSimplex(simplex: ListBuffer[Vector2D], direction: Vector2D): Option[Vector2D] = { + if (simplex.length == 2) { //simplex == 2 + val a = simplex(0) + val b = simplex(1) + val ab = b - a + val ao = -a + + if (ao directionOf ab) { + Some(ab cross ao cross ab) + } else { + simplex.remove(1) + Some(ao) + } + } // end simplex == 2 + + else if (simplex.length == 3) { //simplex == 3 + val a = simplex(0) + val b = simplex(1) + val c = simplex(2) + val ab = b - a + val ac = c - a + val ao = -a + val winding = ab cross ac + + if (ao directionOf (ab cross winding)) { + if (ao directionOf ab) { + simplex.remove(2) + Some(ab cross ao cross ab) + } else if (ao directionOf ac) { + simplex.remove(1) + Some(ac cross ao cross ac) + } else { + simplex.remove(2) + simplex.remove(1) + Some(ao) + } + } else { + if (ao directionOf (winding cross ac)) { + if (ao directionOf ac) { + simplex.remove(1) + Some(ac cross ao cross ac) + } else { + simplex.remove(2) + simplex.remove(1) + Some(ao) + } + } else { + None + } + } + } //end simplex == 3 + + else throw new IllegalArgumentException("Invalid simplex size.") + + } + + + def getPenetration(pair: (A, A)): Option[Penetration] = { + implicit val pr = pair + val ms = new MinkowskiSum(pair) + import ms._ + val s = support(Vector2D.i) + val simplex = new ListBuffer[Vector2D] + simplex prepend s + var direction = -s + + var counter = 0 + while (counter < 100) { + + val a = support(direction) + + if ((a dot direction) < 0) return None + + simplex prepend a + + val newDirection = checkSimplex(simplex, direction) + + if (newDirection.isEmpty) return Some(EPA.getPenetration(simplex, ms)) + else direction = newDirection.get + + counter += 1 + } + throw new IllegalArgumentException("Something went wrong, should not reach here.") + } + + override def collision(pair: (A, A)): Option[Collision[A]] = { + val p = getPenetration(pair) + if (p == None) return None + val man = FeatureManifold.getCollisionPoints(pair, p.get.normal) + if (man == None) return None + + Some(new Collision[A] { + val item1 = pair._1 + val item2 = pair._2 + val normal = man.get.normal + val overlap = p.get.overlap + val points = man.get.pts.toList + }) + } + +} + + diff --git a/src/test/scala/sims/test/gjk/GJK2.scala b/src/test/scala/sims/test/gjk/GJK2.scala new file mode 100644 index 0000000..7fdaec3 --- /dev/null +++ b/src/test/scala/sims/test/gjk/GJK2.scala @@ -0,0 +1,168 @@ +package sims.test.gjk + +/** + * Created by IntelliJ IDEA. + * User: jakob + * Date: 3/27/11 + * Time: 7:47 PM + * To change this template use File | Settings | File Templates. + */ + +import scala.collection.mutable.ListBuffer +import sims.math._ +import sims.collision._ +import sims.collision.narrowphase.NarrowPhaseDetector + +case class Separation(distance: Double, normal: Vector2D, point1: Vector2D, point2: Vector2D) +class GJK2[A <: Collidable: ClassManifest] extends NarrowPhaseDetector[A] { + + val margin = 0.1 + + case class MinkowskiPoint(convex1: Collidable, convex2: Collidable, direction: Vector2D) { + val point1 = convex1.support(direction) - direction.unit * margin + val point2 = convex2.support(-direction) + direction.unit * margin + val point = point1 - point2 + } + + def support(c1: A, c2: A, direction: Vector2D) = MinkowskiPoint(c1, c2, direction) + implicit def minkowsi2Vector(mp: MinkowskiPoint) = mp.point + + def separation(c1: A, c2: A): Option[(Separation)] = { + + //initial search direction + val direction0 = Vector2D.i + + //simplex points + var a = support(c1, c2, direction0) + var b = support(c1, c2, -direction0) + + var counter = 0 + while (counter < 100) { + + //closest point on the current simplex closest to origin + val point = segmentClosestPoint(a, b,Vector2D.Null) + + if (point.isNull) return None + + //new search direction + val direction = -point.unit + + //new Minkowski Sum point + val c = support(c1, c2, direction) + + if (containsOrigin(a, b, c)) return None + + val dc = (direction dot c) + val da = (direction dot a) + + if (dc - da < 0.0001) { + val (point1, point2) = findClosestPoints(a, b) + return Some(Separation(dc, direction, point1, point2)) + } + + if (a.lengthSquare < b.lengthSquare) b = c + else a = c + //counter += 1 + } + return None + } + + + def findClosestPoints(a: MinkowskiPoint, b: MinkowskiPoint): (Vector2D, Vector2D) = { + var p1 = Vector2D.Null + var p2 = Vector2D.Null + + // find lambda1 and lambda2 + val l: Vector2D = b - a + + // check if a and b are the same point + if (l.isNull) { + // then the closest points are a or b support points + p1 = a.point1 + p2 = a.point2 + } else { + // otherwise compute lambda1 and lambda2 + val ll = l dot l; + val l2 = -l.dot(a) / ll; + val l1 = 1 - l2; + + // check if either lambda1 or lambda2 is less than zero + if (l1 < 0) { + // if lambda1 is less than zero then that means that + // the support points of the Minkowski point B are + // the closest points + p1 = b.point1 + p2 = b.point2 + } else if (l2 < 0) { + // if lambda2 is less than zero then that means that + // the support points of the Minkowski point A are + // the closest points + p1 = a.point1 + p2 = a.point2 + } else { + // compute the closest points using lambda1 and lambda2 + // this is the expanded version of + // p1 = a.p1.multiply(l1).add(b.p1.multiply(l2)); + // p2 = a.p2.multiply(l1).add(b.p2.multiply(l2)); + p1 = a.point1 * l1 + b.point1 * l2 + p2 = a.point2 * l1 + b.point2 * l2 + } + } + + (p1, p2) + } + + def segmentClosestPoint(a: Vector2D, b: Vector2D, point: Vector2D): Vector2D = { + if (a == b) return a + val direction = b - a + var t = ((point - a) dot (direction)) / (direction dot direction) + if (t < 0) t = 0 + if (t > 1) t = 1 + a + direction * t + } + + def containsOrigin(a: Vector2D, b: Vector2D, c: Vector2D): Boolean = { + val sa = a.cross(b); + val sb = b.cross(c); + val sc = c.cross(a); + // this is sufficient (we do not need to test sb * sc) + sa * sb > 0 && sa * sc > 0 + } + + def collision(pair: (A, A)): Option[Collision[A]] = { + pair match { + case (c1: Circle, c2: Circle) => collisionCircleCircle(c1, c2)(pair) + case _ => gjkCollision(pair) + } + } + + private def gjkCollision(pair: (A, A)): Option[Collision[A]] = { + val so = separation(pair._1, pair._2) + if (so.isEmpty) return None //deep contact is not implemented yet + val s = so.get + Some(new Collision[A] { + val item1 = pair._1 + val item2 = pair._2 + val overlap = -(s.distance - 2 * margin) + val points = List(s.point1) + val normal = s.normal.unit + }) + + } + + private def collisionCircleCircle(c1: Circle, c2: Circle)(pair: (A, A)) = { + val d = (c2.position - c1.position) + val l = d.length + if (l <= c1.radius + c2.radius) { + val p = c1.position + d.unit * (l - c2.radius) + Some(new Collision[A] { + val item1 = pair._1 + val item2 = pair._2 + val normal = d.unit + val points = List(p) + val overlap = (c1.radius + c2.radius - l) + }) + } else None + } + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gjk/GJKTest.scala b/src/test/scala/sims/test/gjk/GJKTest.scala new file mode 100644 index 0000000..d2ba22f --- /dev/null +++ b/src/test/scala/sims/test/gjk/GJKTest.scala @@ -0,0 +1,115 @@ +package sims.test.gjk + +import processing.core.PApplet +import processing.core.PConstants._ + +class GJKTest extends PApplet { + implicit val top = this + + import sims.dynamics._ + import sims.math._ + import sims.test.gui._ + import sims.test.gui.RichShape._ + + var s1: GraphicalShape = _ + var s2: GraphicalShape = _ + + override def setup() = { + size(600, 600, P2D) + background(255,255,255) + frameRate(60) + + + s1 = (new Rectangle(1, 3) {position = Vector2D(5,5)}).toGraphical + s2 = (new Rectangle(1, 2) {position = Vector2D(8,8); rotation = 0.2}).toGraphical + + } + + val PPM = 39.37f * 96 + var viewScale: Float = 1.0f / 80 + + val GJK = new GJK2[Shape] + + var invert = false + def pair = if (!invert) (s1, s2) else (s2, s1) + + override def draw() = { + smooth() + background(255,255,255) + translate(0, height) + scale(viewScale * PPM, -viewScale * PPM) + + if (keyCode == 32) invert = true + else invert = false + + val collision = GJK.collision(pair._1.shape, pair._2.shape) + /*if (collision != None) { + pushMatrix() + rectMode(CORNER) + stroke(255, 0, 50) + strokeWeight(10) + fill(0, 0, 0, 0) + rect(0, 0, 600, 600) + strokeWeight(1) + popMatrix() + }*/ + //val separation = GJK.collision(pair._1.shape, pair._2.shape) + //if (!separation.isEmpty) + //List(separation.get.point1, separation.get.point2) foreach (p => ellipse(p.x.toFloat, p.y.toFloat, 0.1f, 0.1f)) + + + + label() + s2.shape.position = Vector2D(mouseX / viewScale / PPM, -(mouseY - height) / viewScale / PPM) + + s1.render() + s2.render() + + + + collision match { + case Some(c) => { + stroke(0, 255, 0) + for (p <- c.points) { + ellipse(p.x.toFloat, p.y.toFloat, 0.1f, 0.1f) + val s = p + val e = p + c.normal + line(s.x.toFloat, s.y.toFloat, e.x.toFloat, e.y.toFloat) + println(c.overlap) + } + } + case _ => () + } + + /*stroke(255, 0, 255) + val f = FeatureManifold.farthestFeature(pair._1.shape, Vector2D.j + Vector2D.i) + f match { + case Left(p) => ellipse(p.x.toFloat, p.y.toFloat, 0.1f, 0.1f) + case Right(s) => line(s.point1.x.toFloat, s.point1.y.toFloat, s.point2.x.toFloat, s.point2.y.toFloat) + }*/ + + } + + private val fontSize = 16 + private val f = createFont("Monospaced.plain", fontSize) + private def label() = { + val size = 16 + fill(0, 0, 0) + textMode(SCREEN) + textFont(f) + + val p1 = pair._1.shape + val p2 = pair._2.shape + text("1", (p1.position.x * PPM * viewScale).toFloat, (height - p1.position.y * PPM * viewScale).toFloat) + text("2", (p2.position.x * PPM * viewScale).toFloat, (height - p2.position.y * PPM * viewScale).toFloat) + } + +} + +object GJKTest { + + def main(args: Array[String]): Unit = { + PApplet.main(args ++ Array("sims.test.gjk.GJKTest")) + } + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gjk/MinkowskiSum.scala b/src/test/scala/sims/test/gjk/MinkowskiSum.scala new file mode 100644 index 0000000..7c574ce --- /dev/null +++ b/src/test/scala/sims/test/gjk/MinkowskiSum.scala @@ -0,0 +1,11 @@ +package sims.test.gjk + +import sims.collision._ +import sims.math._ + +class MinkowskiSum(pair: (Collidable, Collidable)) { + + def support(direction: Vector2D) = + pair._1.support(direction) - pair._2.support(-direction) + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/DebugWorld.scala b/src/test/scala/sims/test/gui/DebugWorld.scala new file mode 100644 index 0000000..937bd77 --- /dev/null +++ b/src/test/scala/sims/test/gui/DebugWorld.scala @@ -0,0 +1,29 @@ +package sims.test.gui + +class DebugWorld extends sims.dynamics.World with Publisher { + + override def +=(b: sims.dynamics.Body) = { + super.+=(b) + publish(BodyAdded(this, b)) + } + + override def -=(b: sims.dynamics.Body) = { + super.-=(b) + publish(BodyRemoved(this, b)) + } + + override def +=(j: sims.dynamics.Joint) = { + super.+=(j) + publish(JointAdded(this, j)) + } + + override def -=(j: sims.dynamics.Joint) = { + super.-=(j) + publish(JointRemoved(this, j)) + } + + override def step() = { + super.step() + publish(Stepped(this)) + } +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/KeyManager.scala b/src/test/scala/sims/test/gui/KeyManager.scala new file mode 100644 index 0000000..f7cc199 --- /dev/null +++ b/src/test/scala/sims/test/gui/KeyManager.scala @@ -0,0 +1,53 @@ +package sims.test.gui + +import processing.core.PApplet + +class KeyManager(implicit top: Main) { + + def keyPressed(keyCode: Int) = keyCode match { + // ENTER + case 10 => top.SceneManager.currentScene.world.step() + + // SPACE + case 32 => top.paused = !top.paused + + // PAGE UP + case 33 => top.viewScale += top.viewScale * 0.02f + + // PAGE DOWN + case 34 => top.viewScale -= top.viewScale * 0.02f + + // 0 + case 36 => {top.offsetX = 0; top.offsetY = 0} + + // LEFT + case 37 => top.offsetX += 50 + + // UP + case 38 => top.offsetY -= 50 + + // RIGHT + case 39 => top.offsetX -= 50 + + // DOWN + case 40 => top.offsetY += 50 + + // , (<) + case 44 => top.SceneManager.previousScene() + + // . (>) + case 46 => top.SceneManager.nextScene() + + // b + case 66 => top.SceneManager.currentScene.world.errorReduction += 0.1 + + //v + case 86 => top.SceneManager.currentScene.world.errorReduction -= 0.1 + + case 45 => top.SceneManager.currentScene.world.iterations -= 1 + case 61 => top.SceneManager.currentScene.world.iterations += 1 + + case x: Any => println("unknown key: " + x) + } + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/Main.scala b/src/test/scala/sims/test/gui/Main.scala new file mode 100644 index 0000000..745d793 --- /dev/null +++ b/src/test/scala/sims/test/gui/Main.scala @@ -0,0 +1,143 @@ +package sims.test.gui + +import processing.core.PApplet +import processing.core.PConstants._ +import scala.collection.mutable.ArrayBuffer + +import sims.math._ +import sims.test.gui.RichShape._ +import sims.test.gui.scenes._ +import sims.dynamics.Shape + +class Main extends PApplet { + implicit val top = this + + val SceneManager = new SceneManager + import SceneManager._ + + val KeyManager = new KeyManager + import KeyManager._ + + var (offsetX, offsetY) = (200.0f, 100.0f) + val PPM = 39.37f * 96 + var viewScale: Float = 1.0f / 80 + + private val fontSize = 16 + private val f = createFont("Monospaced.plain", fontSize) + private def displayText(lines: String*) = { + val size = 16 + val indent = 10 + + fill(0, 0, 0) + textMode(SCREEN) + textFont(f) + + for (i <- 0 until lines.length) text(lines(i), indent, (i + 1) * size) + } + + + override def setup() = { + size(screenWidth * 2 / 3, screenHeight * 2 / 3, P2D) + background(255,255,255) + frameRate(60) + //frame.setResizable(true) + currentScene = scenes(0) + } + + var paused = true + override def draw() = { + smooth() + background(255,255,255) + + translate(offsetX, height - offsetY) + scale(viewScale * PPM, -viewScale * PPM) + + val t0 = System.nanoTime() + if (!paused) currentScene.world.step() + val collisions = if (currentScene.world.collisionDetection) SceneManager.currentScene.world.detector.collisions() else Seq() + val dStep = System.nanoTime() - t0 + + for (g <- graphicals) g.render() + fill(255, 0, 0) + stroke(20, 0, 0) + for (c <- collisions; p <- c.points) { + ellipse(p.x.toFloat, p.y.toFloat, 0.1f, 0.1f) + stroke(0, 255, 0) + val s = p + val e = p + c.normal + line(s.x.toFloat, s.y.toFloat, e.x.toFloat, e.y.toFloat) + } + + //_.points.foreach((v) => ellipse(v.x.toFloat, v.y.toFloat, 0.1f, 0.1f))) + + val dRender = System.nanoTime() - t0 - dStep + + displayText( + "status : " + (if (paused) "paused" else "running"), + "------------", + "fps [Hz]: " + frameRate, + "------------", + "step [ms]: " + (dStep / 1E6f), + " [%] : " + (dStep.toFloat / (dStep + dRender) * 100), + "render [ms]: " + (dRender / 1E6f), + " [%] : " + (dRender.toFloat / (dStep + dRender) * 100), + "------------", + "memory [MB]: " + java.lang.Runtime.getRuntime.totalMemory / 1E6, + "load : " + java.lang.management.ManagementFactory.getOperatingSystemMXBean.getSystemLoadAverage(), + "------------", + "bodies : " + currentScene.world.bodies.length, + "shapes : " + currentScene.world.shapes.length, + "joints : " + currentScene.world.joints.length, + "constraints: " + currentScene.world.joints.map(_.constraints.length).sum, + "collisions : " + collisions.length, + "it [1] : " + currentScene.world.iterations, + "dt [ms]: " + currentScene.world.h.toFloat, + "erp [ms]: " + currentScene.world.errorReduction.toFloat, + "------------", + "(" + scaledMouseX + ", " + scaledMouseY + ")" + ) + } + + def drawGrid() = { + + } + + override def keyPressed() = KeyManager.keyPressed(keyCode) + + def scaledMouseX = (mouseX - offsetX) / viewScale / PPM + def scaledMouseY = (height - mouseY - offsetY) / viewScale / PPM + + var mouseJoint: Option[MouseJoint] = None + override def mousePressed(): Unit = { + import Vector2D._ + val body = currentScene.world.bodies.find(_.contains((scaledMouseX, scaledMouseY))) + if (body.isEmpty) return () + val mj = new MouseJoint(body.get, (scaledMouseX, scaledMouseY)) + currentScene.world += mj + mouseJoint = Some(mj) + } + + override def mouseReleased(): Unit = { + if (mouseJoint.isEmpty) return () + currentScene.world -= mouseJoint.get + mouseJoint = None + } + + override def mouseDragged(): Unit = { + import Vector2D._ + + if (mouseJoint.isEmpty) return () + + mouseJoint.get.anchor = (scaledMouseX, scaledMouseY) + + } + + + +} + +object Main { + def main(args : Array[String]) : Unit = { + PApplet.main(args ++ Array("sims.test.gui.Main")) + } +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/MouseJoint.scala b/src/test/scala/sims/test/gui/MouseJoint.scala new file mode 100644 index 0000000..0d74dd5 --- /dev/null +++ b/src/test/scala/sims/test/gui/MouseJoint.scala @@ -0,0 +1,38 @@ +package sims.test.gui + +import sims.dynamics._ +import sims.dynamics.constraints._ +import sims.math._ + +class MouseJoint(val body1: Body, var anchor: Vector2D) extends Joint { + val body2 = new Body() {fixed = true} + + private val self = this + + private val local1 = anchor - body1.position + + private val rotation01 = body1.rotation + + def r1 = (local1 rotate (body1.rotation - rotation01)) + + def x1 = body1.position + r1 + def x2 = anchor + + def x = x2 - x1 + + val constraints = List( + new Constraint { + val body1 = self.body1 + val body2 = self.body2 + def value = x.x + def jacobian = new Jacobian(-Vector2D(1, 0), r1.y, Vector2D.i, 0) + }, + new Constraint { + val body1 = self.body1 + val body2 = self.body2 + def value = x.y + def jacobian = new Jacobian(-Vector2D(0, 1), -r1.x, Vector2D.j, 0) + } + ) + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/RichJoint.scala b/src/test/scala/sims/test/gui/RichJoint.scala new file mode 100644 index 0000000..2c3d5fd --- /dev/null +++ b/src/test/scala/sims/test/gui/RichJoint.scala @@ -0,0 +1,51 @@ +package sims.test.gui + +import processing.core.PApplet +import processing.core.PConstants._ +import sims.dynamics._ + +class RichJoint(joint: Joint) { +private implicit def double2Float(x: Double): Float = x.toFloat + + def toGraphical(implicit parent: PApplet) = new GraphicalJoint(joint) { + + val top = parent + + val render = joint match { + + case j: DistanceJoint => () => { + top.pushMatrix() + top.stroke(0, 0, 0) + top.fill(0, 0, 0) + top.line(j.x1.x, j.x1.y, j.x2.x, j.x2.y) + top.stroke(100, 100, 100) + top.line(j.body1.position.x, j.body1.position.y, j.x1.x, j.x1.y) + top.line(j.body2.position.x, j.body2.position.y, j.x2.x, j.x2.y) + top.popMatrix() + } + + case j: RevoluteJoint => () => { + top.pushMatrix() + top.stroke(0, 0, 0) + top.fill(0, 0, 0) + top.line(j.x1.x, j.x1.y, j.x2.x, j.x2.y) + top.stroke(100, 100, 100) + top.line(j.body1.position.x, j.body1.position.y, j.x1.x, j.x1.y) + top.line(j.body2.position.x, j.body2.position.y, j.x2.x, j.x2.y) + top.popMatrix() + } + + case j: MouseJoint => () => () + + case _ => throw new IllegalArgumentException("Cannot create graphical joint: unknown joint.") + } + + } + +} + +object RichJoint { + + implicit def jointToRichShape(j: Joint) = new RichJoint(j) + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/RichShape.scala b/src/test/scala/sims/test/gui/RichShape.scala new file mode 100644 index 0000000..6817ebe --- /dev/null +++ b/src/test/scala/sims/test/gui/RichShape.scala @@ -0,0 +1,49 @@ +package sims.test.gui + +import processing.core.PApplet +import processing.core.PConstants._ +import sims.dynamics._ + +class RichShape(shape: Shape) { + private implicit def double2Float(x: Double): Float = x.toFloat + + def toGraphical(implicit parent: PApplet) = new GraphicalShape(shape) { + + val top = parent + + val render = shape match { + + case c: Circle => () => { + top.pushMatrix() + top.stroke(0, 0, 0) + top.fill(0, 0, 255, 200) + top.translate(c.position.x, c.position.y) + top.rotate(-c.rotation) + top.ellipseMode(CENTER) + top.ellipse(0, 0, c.radius * 2, c.radius * 2) + top.line(0,0, c.radius, 0) + top.popMatrix() + } + + case r: Rectangle => () => { + top.pushMatrix() + top.translate(r.position.x, r.position.y) + top.rotate(-r.rotation) + top.fill(255, 0, 0, 200) + top.rectMode(CENTER) + top.rect(0, 0, r.halfWidth * 2, r.halfHeight * 2) + top.popMatrix() + } + + case _ => throw new IllegalArgumentException("Cannot create graphical shape: unknown shape.") + } + + } + +} + +object RichShape { + + implicit def shapeToRichShape(s: Shape) = new RichShape(s) + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/Scene.scala b/src/test/scala/sims/test/gui/Scene.scala new file mode 100644 index 0000000..6e1664e --- /dev/null +++ b/src/test/scala/sims/test/gui/Scene.scala @@ -0,0 +1,13 @@ +package sims.test.gui + +trait Scene extends Reactor { + def name: String = this.getClass().getName() + def description: String = "" + + val world = new DebugWorld + + def init(): Unit + + def exit(): Unit = {} + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/SceneManager.scala b/src/test/scala/sims/test/gui/SceneManager.scala new file mode 100644 index 0000000..39da308 --- /dev/null +++ b/src/test/scala/sims/test/gui/SceneManager.scala @@ -0,0 +1,95 @@ +package sims.test.gui + +import processing.core.PApplet +import scala.collection.mutable.ArrayBuffer +import sims.math._ +import sims.test.gui.scenes._ +import sims.test.gui.RichShape._ +import sims.test.gui.RichJoint._ + +class SceneManager(implicit top: PApplet) { + + /* Contains objects that will be rendered on `draw()`. */ + private var _graphicals = new ArrayBuffer[Graphical[_]] + def graphicals: Seq[Graphical[_]] = _graphicals + + /* Current scene. */ + private var _currentScene: Scene = EmptyScene + + /* Get current scene. */ + def currentScene = _currentScene + + /* Set current scene. */ + def currentScene_=(newScene: Scene) = { + + // remove reactions + currentScene.deafTo(currentScene.world) + currentScene.reactions.clear() + + // empty world + currentScene.world.clear() + + // clear graphical objects + _graphicals.clear() + + // custom exit behavior + currentScene.exit() + + // add new reactions to create / remove graphical objects + newScene.listenTo(newScene.world) + newScene.reactions += { + case BodyAdded(newScene.world, body) => for (s <- body.shapes) _graphicals += s.toGraphical + case BodyRemoved(newScene.world, body) => for (s <- body.shapes) { + val index = _graphicals.findIndexOf((g: Graphical[_]) => g match { + case gs: GraphicalShape => gs.physical == s + case _ => false + }) + _graphicals.remove(index) + } + + case JointAdded(newScene.world, joint) => _graphicals += joint.toGraphical + case JointRemoved(newScene.world, joint) => { + val index = _graphicals.findIndexOf((g: Graphical[_]) => g match { + case gj: GraphicalJoint => gj.physical == joint + case _ => false + }) + _graphicals.remove(index) + } + + } + + // custom initialization + newScene.init() + + // set current scene + _currentScene = newScene + + println("set scene to '" + currentScene.name + "'") + } + + private var currentSceneIndex = 0 + val scenes = List( + BasicScene, + CollisionScene, + LongCollisionScene, + CloudScene, + PyramidScene, + ShiftedStackScene, + JointScene + ) + + def nextScene() = { + currentSceneIndex += 1 + currentScene = scenes(mod(currentSceneIndex, scenes.length)) + } + + def previousScene() = { + currentSceneIndex -= 1 + currentScene = scenes(mod(currentSceneIndex, scenes.length)) + } + + def restartScene() = { + currentScene = currentScene + } + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/events.scala b/src/test/scala/sims/test/gui/events.scala new file mode 100644 index 0000000..22015ce --- /dev/null +++ b/src/test/scala/sims/test/gui/events.scala @@ -0,0 +1,49 @@ +package sims.test.gui + +import scala.collection.mutable.ListBuffer +import sims.dynamics._ + +trait Event +case class BodyAdded(world: World, body: Body) extends Event +case class BodyRemoved(world: World, body: Body) extends Event +case class Stepped(wordl: World) extends Event +case class JointAdded(world: World, joint: Joint) extends Event +case class JointRemoved(world: World, joint: Joint) extends Event + +object Reactions { + class Impl extends Reactions { + private val parts = new ListBuffer[Reaction] + def isDefinedAt(e: Event) = parts exists (_ isDefinedAt e) + def +=(r: Reaction) = parts += r + def -=(r: Reaction) = parts -= r + def clear() = parts.clear() + def apply(e: Event) { + for (p <- parts; if p isDefinedAt e) p(e) + } + } + type Reaction = PartialFunction[Event, Unit] +} + +abstract class Reactions extends Reactions.Reaction { + def +=(r: Reactions.Reaction): Unit + def -=(r: Reactions.Reaction): Unit + def clear(): Unit +} + +trait Reactor { + val reactions: Reactions = new Reactions.Impl + + def listenTo(ps: Publisher*) = for (p <- ps) p.subscribe(reactions) + def deafTo(ps: Publisher*) = for (p <- ps) p.unsubscribe(reactions) +} + +trait Publisher { + import Reactions._ + + private val listeners = new ListBuffer[Reaction] + + def subscribe(listener: Reaction) = listeners += listener + def unsubscribe(listener: Reaction) = listeners -= listener + + def publish(e: Event) { for (l <- listeners) l(e) } +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/graphicals.scala b/src/test/scala/sims/test/gui/graphicals.scala new file mode 100644 index 0000000..39b2ea0 --- /dev/null +++ b/src/test/scala/sims/test/gui/graphicals.scala @@ -0,0 +1,13 @@ +package sims.test.gui + +import processing.core.PApplet +import sims.dynamics.Shape +import sims.dynamics.Joint + +abstract class Graphical[+A](val physical: A) { + val top: PApplet + val render: () => Unit +} + +abstract class GraphicalShape(val shape: Shape) extends Graphical[Shape](shape) +abstract class GraphicalJoint(val joint: Joint) extends Graphical[Joint](joint)
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/scenes/BasicScene.scala b/src/test/scala/sims/test/gui/scenes/BasicScene.scala new file mode 100644 index 0000000..9598ab1 --- /dev/null +++ b/src/test/scala/sims/test/gui/scenes/BasicScene.scala @@ -0,0 +1,18 @@ +package sims.test.gui +package scenes + +import sims.math._ +import sims.dynamics._ +import sims.dynamics._ + +object BasicScene extends Scene { + + def init() = { + world.gravity = Vector2D.Null + val s = new Circle(1) + world += new Body(s) {linearVelocity = Vector2D(0.1, 0.01); angularVelocity = 1} + world += new Body(new Rectangle(2,1)) {linearVelocity = Vector2D(0.1, 0.01); angularVelocity = 1} + } + + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/scenes/CloudScene.scala b/src/test/scala/sims/test/gui/scenes/CloudScene.scala new file mode 100644 index 0000000..660e309 --- /dev/null +++ b/src/test/scala/sims/test/gui/scenes/CloudScene.scala @@ -0,0 +1,47 @@ +package sims.test.gui +package scenes + +import sims.math._ +import sims.dynamics._ + +object CloudScene extends Scene { + override def description = "A cloud of circles." + + val MaxItems = 1000 + val MaxItemSize = 0.2 + val Width = 10 + val Height = 10 + + val random = new scala.util.Random(1234567890) + def randomCircles(): Seq[Body] = for (i <- 0 until MaxItems) yield { + val rX = random.nextDouble * Width + val rY = random.nextDouble * Height + val c = new Circle(random.nextDouble * MaxItemSize) { + position = Vector2D(rX, rY) + } + new Body(c) { + linearVelocity = Vector2D(random.nextDouble * (if (random.nextBoolean) 1 else -1), random.nextDouble * (if (random.nextBoolean) 1 else -1)) + angularVelocity = random.nextDouble * (if (random.nextBoolean) 1 else -1) * 10 + } + } + + def frame() = { + val points = List(Vector2D(-1, -1), Vector2D(11, -1), Vector2D(11, 11), Vector2D(-1, 11)) + for (i <- 0 until points.length) yield { + val sp = points(i) + val ep = points((i + 1) % points.length) + val center = (sp + ep) / 2 + val r = new Rectangle((center - ep).length, 0.2) { + position = center + rotation = math.Pi / 2 * i + } + new Body(r) {fixed = true} + } + } + + override def init() = { + world.gravity = Vector2D.Null + for (r <- randomCircles()) world += r + //for (r <- frame()) world += r + } +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/scenes/CollisionScene.scala b/src/test/scala/sims/test/gui/scenes/CollisionScene.scala new file mode 100644 index 0000000..cf2ea61 --- /dev/null +++ b/src/test/scala/sims/test/gui/scenes/CollisionScene.scala @@ -0,0 +1,41 @@ +package sims.test.gui +package scenes + +import sims.dynamics._ +import sims.math._ +import sims.dynamics._ + +object CollisionScene extends Scene { + override def description = "A basic collision detection test." + + var c1 = (new Circle(1) {restitution = 1.0}).asBody + var c2 = (new Circle(1) {position = Vector2D(3, 0)}).asBody + var r1 = (new Rectangle(0.5, 0.5) {position = Vector2D(6,0)}).asBody + + def init() = { + c1 = (new Circle(1) {restitution = 1.0}).asBody + c2 = (new Circle(1) {position = Vector2D(3, 0); restitution = 1.0}).asBody + r1 = (new Rectangle(0.5, 0.5) {position = Vector2D(6,0)}).asBody + + c1.linearVelocity = Vector2D(1, 0) + + world.gravity = Vector2D(0, 0) + world += c1 + world += c2 + world += r1 + + reactions += { + case Stepped(`world`) => + println( + "p: " + (c1.linearMomentum.length + c2.linearMomentum.length + r1.linearMomentum.length).toFloat + + "\tE: " + (E(c1) + E(c2) + E(r1)).toFloat + ) + } + } + + + def E(b: Body) = { + (b.linearVelocity dot b.linearVelocity) * b.mass / 2 + } + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/scenes/EmptyScene.scala b/src/test/scala/sims/test/gui/scenes/EmptyScene.scala new file mode 100644 index 0000000..a88679a --- /dev/null +++ b/src/test/scala/sims/test/gui/scenes/EmptyScene.scala @@ -0,0 +1,6 @@ +package sims.test.gui +package scenes + +object EmptyScene extends Scene { + def init() = () +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/scenes/JointScene.scala b/src/test/scala/sims/test/gui/scenes/JointScene.scala new file mode 100644 index 0000000..4a0726f --- /dev/null +++ b/src/test/scala/sims/test/gui/scenes/JointScene.scala @@ -0,0 +1,107 @@ +package sims.test.gui +package scenes + +import sims.dynamics._ +import sims.math._ +import sims.dynamics._ + +object JointScene extends Scene { + + override def init() = { + val b1 = new Body(new Circle(0.1)) {fixed = true} + val b2 = new Body(new Rectangle(0.1, 0.5)) {position = Vector2D(2,2)} + val j = new DistanceJoint(b1, b1.position, b2, b2.position + Vector2D(0, -0.5)) + world += b1 + world += b2 + world += j + + val chainBodies = for (i <- 0 until 10) yield new Body(new Rectangle(0.5, 0.1)){fixed = i == 0 || i == 9; position = Vector2D(i, 5)} + val chainHinges = for (i <- 0 until chainBodies.length - 1) yield + new RevoluteJoint(chainBodies(i), chainBodies(i + 1), Vector2D(i + 0.5, 5)) + for (b <- chainBodies) world += b + for (j <- chainHinges) world += j + + import sims.dsl._ + val c = new Body(new Circle(0.1)) {position = Vector2D(4, 0)} + world += c + world += c distance chainBodies(4) + + val r = new Body(new Rectangle(2, 0.1)) {position = Vector2D(4, 0)} + world += r + world += c revolute r + + val c2 = new Body(new Circle(0.2)) {position = Vector2D(2, 2)} + world += c2 + world += r :@@ (-2, 0) distance c2 + + val r2 = new Body(new Rectangle(0.1, 0.2)) {position = Vector2D(6, 2)} + world += r2 + world += r :@@ (2, 0) distance (0, -0.2) @@: r2 + + val r3 = new Body(new Rectangle(0.3, 0.1)) {position = Vector2D(6.3, 2.2)} + world += r3 + world += r2 :@ (6, 2.2) revolute r3 + + // chaos pendulum + { + val c1 = new Body(new Circle(0.1)) {fixed = true; position = Vector2D(12, 2)} + world += c1 + val r1 = new Body(new Rectangle(1, 0.1)) {position = Vector2D(13, 2)} + world += r1 + val r2 = new Body(new Rectangle(1, 0.1)) {position = Vector2D(15, 2)} + world += r2 + + world += c1 revolute r1 + world += r1 :@@ (1, 0) revolute r2 + + } + + // net + { + val w = 10 + val h = 10 + val d = 0.2 + + val nodes = + for (i <- (0 until w).toArray) yield + for (j <- (0 until h).toArray) yield + new Body(new Circle(0.05)) {fixed = i == 0 && j == h - 1 ; position = Vector2D(i * d, j * d) + Vector2D(-3, 2)} + + for (n <- nodes.flatten) world += n + + val joints = { + var r: List[DistanceJoint] = Nil + for(i <- 0 to nodes.length - 1; j <- 0 to nodes(i).length - 1) { + if (i > 0) + r = (nodes(i-1)(j) distance nodes(i)(j)) :: r + if (j > 0) + r = (nodes(i)(j-1) distance nodes(i)(j)) :: r + } + r + } + + for (j <- joints) world += j + + } + + + world.collisionDetection = false + world.iterations = 10 + world.errorReduction = 1 + + + /* + val r1 = new Body(new Rectangle(0.5, 0.1)) {fixed = true; position = Vector2D(5, 5)} + val r2 = new Body(new Rectangle(0.5, 0.1)) {position = Vector2D(6, 5)} + val r3 = new Body(new Rectangle(0.5, 0.1)) {position = Vector2D(7, 5)} + val j12 = new RevoluteJoint(r1, r2, Vector2D(5.5, 5)) + val j23 = new RevoluteJoint(r2, r3, Vector2D(6.5, 5)) + world += r1 + world += r2 + world += r3 + world += j12 + world += j23 + */ + } + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/scenes/LongCollisionScene.scala b/src/test/scala/sims/test/gui/scenes/LongCollisionScene.scala new file mode 100644 index 0000000..d153f1b --- /dev/null +++ b/src/test/scala/sims/test/gui/scenes/LongCollisionScene.scala @@ -0,0 +1,54 @@ +package sims.test.gui +package scenes + +import sims.dynamics._ +import sims.math._ +import sims.dynamics._ + +object LongCollisionScene extends Scene { + override def description = "A test to verify conservation in a collision." + + def makeBodies() = (for (i <- 0 until 10) yield { + new Circle(0.2) { + position = Vector2D(0 + 2.01 * radius * i, 0) + restitution = 0.8 + } + }.asBody) ++ (for (i <- 0 until 10) yield { + new Circle(0.2) { + position = Vector2D(6 + 2.01 * radius * i, 0) + restitution = 0.8 + } + }.asBody) + + override def init() = { + val bodies = makeBodies() + bodies(0).fixed = true + bodies(19).fixed = true + for (b <- bodies) world += b + world.gravity = Vector2D.Null + + val bullet = new Body(new Circle(0.2) { + position = Vector2D(5, 0) + restitution = 0.8}) { + linearVelocity = Vector2D(3, 0) + } + world += bullet + + world.iterations = 10 + + registerListeners() + } + + def registerListeners() = { + reactions += { + case Stepped(`world`) => println( + "P: " + world.bodies.map(P(_)).sum.toFloat + + "\tE: " + world.bodies.map(E(_)).sum.toFloat + ) + } + } + + def P(b: Body) = if (b.fixed) 0 else b.linearMomentum.length + def E(b: Body) = if (b.fixed) 0 else (b.linearVelocity dot b.linearVelocity) * b.mass * 0.5 + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/scenes/PyramidScene.scala b/src/test/scala/sims/test/gui/scenes/PyramidScene.scala new file mode 100644 index 0000000..19438de --- /dev/null +++ b/src/test/scala/sims/test/gui/scenes/PyramidScene.scala @@ -0,0 +1,40 @@ +package sims.test.gui +package scenes + +import sims.math._ +import sims.dynamics._ +import sims.dynamics._ + +object PyramidScene extends Scene { + override def description = "A pyramid made out of circles." + + val Base = 40 + var Radius = 0.2 + + val s = math.sqrt(3) + + def pyramid: Seq[Body] = (for (i <- 0 until Base) yield { + for (j <- 0 until Base - i) yield new Body( + new Circle(Radius) { + position = Vector2D(2 * j * Radius + i * Radius, s * i * Radius) + restitution = 0.5 + } + ) {fixed = (i == 0)} + }).flatten + + override def init() = { + //world.gravity = Vector2D.Null + for (b <- pyramid) world += b + + val b = new Circle(0.3) { + override val density = 10.0 + position = Vector2D(4,15) + } + val bd = new Body(b){ + linearVelocity = Vector2D(0, -2.5) + } + + world += bd + } + +}
\ No newline at end of file diff --git a/src/test/scala/sims/test/gui/scenes/ShiftedStackScene.scala b/src/test/scala/sims/test/gui/scenes/ShiftedStackScene.scala new file mode 100644 index 0000000..c8a7af6 --- /dev/null +++ b/src/test/scala/sims/test/gui/scenes/ShiftedStackScene.scala @@ -0,0 +1,30 @@ +package sims.test.gui +package scenes + +import sims.math._ +import sims.dynamics._ + +object ShiftedStackScene extends Scene { + override def description = "A stack of shifted rectangles." + /*override val world = new DebugWorld{ + import sims.collision._ + import sims.collision.narrowphase._ + import sims.collision.broadphase._ + override val detector = SAP[Shape] narrowedBy new sims.test.gjk.GJK[Shape] + }*/ + + val width = 1.0 + val height = 0.2 + + def stack() = for (i <- 0 until 2) yield + new Body(new Rectangle(width / 2, height / 2) { + position = Vector2D(0.25 * (i % 2) , i * height) + restitution = 0.0 + }) {fixed = i == 0} + + override def init() = { + for (s <- stack()) world += s + world.iterations = 100 + } + +}
\ No newline at end of file |