package uk.org.squirm3.engine;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import uk.org.squirm3.model.Atom;
import uk.org.squirm3.model.Configuration;
import uk.org.squirm3.model.DraggingPoint;
import uk.org.squirm3.model.Reaction;
import uk.org.squirm3.model.type.def.BasicType;
final class Collider {
// ------- data ---------
private final List<? extends Atom> atoms;
// structures for space-division speed-up:
private final List<List<List<Integer>>> buckets; // each bucket has a list
// of
// the indices of
// the sq3Atoms contained within it
private final int n_buckets_x, n_buckets_y; // the horizontal and vertical
// dimensions are divided into
// this many buckets
private final int width, height;
// we store the size to ensure we access the right bucket (dimensions might
// change)
protected float bucket_width, bucket_height;
private float MAX_SPEED = 5.0f; // recomputed when R changes (thanks Ralph)
private final Set<Atom> reactedAtoms = new HashSet<Atom>();
// ------- methods ---------
public Collider(final Configuration configuration) {
atoms = new ArrayList<Atom>(configuration.getAtoms());
final int w = (int) configuration.getWidth();
final int h = (int) configuration.getHeight();
// size the buckets structure so each bucket is approximately R in size
// (approx 1 atom per bucket)
final float R = Atom.getAtomSize();
// recompute MAX_SPEED to allow for the new R
MAX_SPEED = 5.0f * R / 22.0f; // (thanks Ralph)
n_buckets_x = Math.round(w / (1.0f * R));
n_buckets_y = Math.round(h / (1.0f * R));
// (else div0 error)
buckets = new ArrayList<List<List<Integer>>>(); // (garbage
// collection takes
// care of old
// array)
// allocate each
for (int x = 0; x < n_buckets_x; x++) {
final List<List<Integer>> list = new ArrayList<List<Integer>>();
buckets.add(list);
for (int y = 0; y < n_buckets_y; y++) {
list.add(new LinkedList<Integer>());
}
}
width = w;
height = h;
bucket_width = w / (float) n_buckets_x;
bucket_height = h / (float) n_buckets_y;
// insert any atoms currently present
for (int i = 0; i < atoms.size(); i++) {
final Atom a = atoms.get(i);
int bucket_x, bucket_y;
bucket_x = whichBucketX(a.getPhysicalPoint().getPositionX());
bucket_y = whichBucketY(a.getPhysicalPoint().getPositionY());
buckets.get(bucket_x).get(bucket_y).add(new Integer(i));
}
}
// TODO protect the collection : copy or immutable view
public List<? extends Atom> getAtoms() {
return atoms;
}
private int whichBucketX(final float x) {
int w = (int) Math.floor(x / bucket_width);
if (w < 0) {
w = 0;
} else if (w >= n_buckets_x) {
w = n_buckets_x - 1;
}
return w;
}
private int whichBucketY(final float y) {
int w = (int) Math.floor(y / bucket_height);
if (w < 0) {
w = 0;
} else if (w >= n_buckets_y) {
w = n_buckets_y - 1;
}
return w;
}
// straight Euler method computation of spring forces per timestep
// uses a maximum speed limiter to prevent numerical problems
// doesn't lose overall speed however, since force computation typically
// overestimates anyway
// but reliable and quick
// disadvantage: bonded atom groups lose group momentum as speed limiter
// kicks in
// some possible alternative physics:
// - a hard-sphere type physics, where instead of a constant timestep we
// search for future
// collisions between the spheres and run the sim forward to that point, and
// recompute their
// velocities as a result of the collision. might be promising to explore
// for OB - but how to include
// bonds (and dragging)?
// - a lattice-based physics can run very fast indeed but doesn't look as
// satisfying
public void doTimeStep(final DraggingPoint draggingPoint,
final List<Reaction> reactions) {
// boolean is_dragging,int which_being_dragged, int mouse_x,int mouse_y
// COMPUTE AND REACT
// we shuffle the reactions list in order to prevent any reaction
// artefacts, since
// reactions are applied as they are found, and conflicting reactions
// would only ever have
// the first one applied, eg. with a1c1->a2c2 and x1c1->x4c4, only the
// first version would apply
// to any a1c1 pairs. Now each reaction has an equal chance of being
// chosen.
Collections.shuffle(reactions);
// starting over for this iteration
reactedAtoms.clear();
// TODO the collider should not be responsible for the mangement of
// reactions
final float R = Atom.getAtomSize();
final float diam = 2.0f * R;
final float diam2 = diam * diam;
for (int i = 0; i < atoms.size(); i++) {
Atom a = atoms.get(i);
// bounce off the walls
if (a.getPhysicalPoint().getPositionX() < R) {
a.getPhysicalPoint().setSpeedX(
a.getPhysicalPoint().getSpeedX()
+ getForce(R
- a.getPhysicalPoint().getPositionX()));
}
if (a.getPhysicalPoint().getPositionY() < R) {
a.getPhysicalPoint().setSpeedY(
a.getPhysicalPoint().getSpeedY()
+ getForce(R
- a.getPhysicalPoint().getPositionY()));
}
if (a.getPhysicalPoint().getPositionX() > width - R) {
a.getPhysicalPoint().setSpeedX(
a.getPhysicalPoint().getSpeedX()
- getForce(a.getPhysicalPoint().getPositionX()
- (width - R)));
}
if (a.getPhysicalPoint().getPositionY() > height - R) {
a.getPhysicalPoint().setSpeedY(
a.getPhysicalPoint().getSpeedY()
- getForce(a.getPhysicalPoint().getPositionY()
- (height - R)));
}
// bounce off other atoms that are within 2R distance of this one
// what square radius must we search for neighbours?
final int rx = (int) Math.ceil(diam / bucket_width);
final int ry = (int) Math.ceil(diam / bucket_height);
// what bucket is the atom in?
final int wx = whichBucketX(a.getPhysicalPoint().getPositionX());
final int wy = whichBucketY(a.getPhysicalPoint().getPositionY());
// accumulate the list of any atoms in this square radius (clamped
// to the valid area)
for (int x = Math.max(0, wx - rx); x <= Math.min(n_buckets_x - 1,
wx + rx); x++) {
for (int y = Math.max(0, wy - ry); y <= Math.min(
n_buckets_y - 1, wy + ry); y++) {
// add each atom that is in this bucket
final Iterator<Integer> it = buckets.get(x).get(y)
.listIterator();
while (it.hasNext()) {
final int iOther = it.next().intValue();
if (iOther <= i) {
continue; // using Newton's "action&reaction" as a
}
// shortcut
Atom b = atoms.get(iOther);
if (new Point2D.Float(a.getPhysicalPoint()
.getPositionX(), a.getPhysicalPoint()
.getPositionY()).distanceSq(new Point2D.Float(b
.getPhysicalPoint().getPositionX(), b
.getPhysicalPoint().getPositionY())) < diam2) {
// this is a collision - can any reactions apply to
// these two atoms?
if (!a.isKiller() && !b.isKiller()) {
for (int twice = 0; twice < 2
&& !reactedAtoms.contains(a)
&& !reactedAtoms.contains(b); twice++) {
// try each reaction in turn
final Iterator<Reaction> iterator = reactions
.listIterator();
while (iterator.hasNext()
&& !reactedAtoms.contains(a)
&& !reactedAtoms.contains(b)) {
if (iterator.next().tryOn(a, b)) {
reactedAtoms.add(a);
reactedAtoms.add(b);
}
}
// now swap a and b and try again
final Atom temp = a;
a = b;
b = temp;
}
} else {
// the killer atom breaks the other atoms bonds
// (unless other is an 'a' atom)
if (a.isKiller()) {
if (b.getType() != BasicType.A) {
b.breakAllBonds();
}
} else {
if (a.getType() != BasicType.A) {
a.breakAllBonds();
}
}
}
// atoms bounce off other atoms
final float sep = (float) new Point2D.Float(a
.getPhysicalPoint().getPositionX(), a
.getPhysicalPoint().getPositionY())
.distance(new Point2D.Float(b
.getPhysicalPoint().getPositionX(),
b.getPhysicalPoint().getPositionY()));
final float force = getForce(diam - sep);
// push from the other atom
final float dx = force
* (a.getPhysicalPoint().getPositionX() - b
.getPhysicalPoint().getPositionX())
/ sep;
final float dy = force
* (a.getPhysicalPoint().getPositionY() - b
.getPhysicalPoint().getPositionY())
/ sep;
a.getPhysicalPoint().setSpeedX(
a.getPhysicalPoint().getSpeedX() + dx);
a.getPhysicalPoint().setSpeedY(
a.getPhysicalPoint().getSpeedY() + dy);
b.getPhysicalPoint().setSpeedX(
b.getPhysicalPoint().getSpeedX() - dx); // using
// Newton's
// "action&reaction"
// as
// a
// shortcut
b.getPhysicalPoint().setSpeedY(
b.getPhysicalPoint().getSpeedY() - dy);
}
}
}
}
// bonds act like springs
final Iterator<Atom> it = a.getBonds().iterator();
while (it.hasNext()) {
final Atom other = it.next();
final float sep = (float) new Point2D.Float(a
.getPhysicalPoint().getPositionX(), a
.getPhysicalPoint().getPositionY())
.distance(new Point2D.Float(other.getPhysicalPoint()
.getPositionX(), other.getPhysicalPoint()
.getPositionY()));
final float force = getForce(sep - diam) / 4.0f; // this
// determines
// the bond spring
// stiffness
// pull towards the other atom
final float dx = force
* (other.getPhysicalPoint().getPositionX() - a
.getPhysicalPoint().getPositionX()) / sep;
final float dy = force
* (other.getPhysicalPoint().getPositionY() - a
.getPhysicalPoint().getPositionY()) / sep;
a.getPhysicalPoint().setSpeedX(
a.getPhysicalPoint().getSpeedX() + dx);
a.getPhysicalPoint().setSpeedY(
a.getPhysicalPoint().getSpeedY() + dy);
}
// the user can pull atoms about using the mouse
if (draggingPoint != null
&& draggingPoint.getWhichBeingDragging() == i) {
// normalise the pull vector
float pullX = draggingPoint.getX()
- a.getPhysicalPoint().getPositionX();
float pullY = draggingPoint.getY()
- a.getPhysicalPoint().getPositionY();
final float dist = (float) Math.sqrt(pullX * pullX + pullY
* pullY);
pullX /= dist;
pullY /= dist;
a.getPhysicalPoint().setSpeedX(
a.getPhysicalPoint().getSpeedX() + 2.0f * pullX);
a.getPhysicalPoint().setSpeedY(
a.getPhysicalPoint().getSpeedY() + 2.0f * pullY);
}
// limit the velocity of each atom to prevent numerical problems
final float speed = (float) Math.sqrt(a.getPhysicalPoint()
.getSpeedX()
* a.getPhysicalPoint().getSpeedX()
+ a.getPhysicalPoint().getSpeedY()
* a.getPhysicalPoint().getSpeedY());
if (speed > MAX_SPEED) {
a.getPhysicalPoint().setSpeedX(
a.getPhysicalPoint().getSpeedX() * MAX_SPEED / speed);
a.getPhysicalPoint().setSpeedY(
a.getPhysicalPoint().getSpeedY() * MAX_SPEED / speed);
}
}
// MOVE ATOMS
for (int i = 0; i < atoms.size(); i++) {
final Atom a = atoms.get(i);
if (a.isStuck()) {
continue; // special atoms that don't move
}
int current_bucket_x, current_bucket_y;
current_bucket_x = whichBucketX(a.getPhysicalPoint().getPositionX());
current_bucket_y = whichBucketY(a.getPhysicalPoint().getPositionY());
a.getPhysicalPoint().setPositionX(
a.getPhysicalPoint().getPositionX()
+ atoms.get(i).getPhysicalPoint().getSpeedX());
a.getPhysicalPoint().setPositionY(
a.getPhysicalPoint().getPositionY()
+ atoms.get(i).getPhysicalPoint().getSpeedY());
int new_bucket_x, new_bucket_y;
new_bucket_x = whichBucketX(a.getPhysicalPoint().getPositionX());
new_bucket_y = whichBucketY(a.getPhysicalPoint().getPositionY());
// do we need to move the atom to a new bucket?
if (new_bucket_x != current_bucket_x
|| new_bucket_y != current_bucket_y) {
// remove the atom index from the list
final List<Integer> list = buckets.get(current_bucket_x).get(
current_bucket_y);
final Iterator<Integer> it = list.listIterator(0);
while (it.hasNext()) {
if (it.next().intValue() == i) {
it.remove();
}
}
buckets.get(new_bucket_x).get(new_bucket_y).add(new Integer(i));
}
}
}
private float getForce(final float d) {
final float R = Atom.getAtomSize();
return 1.0f * d * 22.0f / R; // what is the overlap/overstretch force
// for distance d?
// (now inversely proportional to R, thanks Ralph)
}
}