diff --git a/pom.xml b/pom.xml index d6ae348..87364bf 100644 --- a/pom.xml +++ b/pom.xml @@ -3,13 +3,13 @@ 4.0.0 org.twak.campskeleton campskeleton - 0.0.1-SNAPSHOT + 0.0.2-SNAPSHOT jar - 1.8 - 1.8 + 11 + 11 @@ -122,12 +122,10 @@ vecmath 1.5.2 - - - gov.nist.math - jama - 1.0.3 + org.tinspin + tinspin-indexes + 2.1.3 diff --git a/src/org/twak/camp/CoSitedCollision.java b/src/org/twak/camp/CoSitedCollision.java index b13e837..d4cb0c0 100644 --- a/src/org/twak/camp/CoSitedCollision.java +++ b/src/org/twak/camp/CoSitedCollision.java @@ -2,8 +2,10 @@ package org.twak.camp; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -27,7 +29,7 @@ */ public class CoSitedCollision { - public Set edges = new LinkedHashSet(); + public Collection edges = new ArrayList<>(10); public Point3d loc; public boolean debugHoriz = false; @@ -45,7 +47,7 @@ public CoSitedCollision( Point3d loc, EdgeCollision ec, HeightCollision parent ) public void add(EdgeCollision ec) { - edges.add( ec ); + edges.add(ec); } /** @@ -54,10 +56,10 @@ public void add(EdgeCollision ec) */ public boolean findChains ( Skeleton skel ) { - chains = new ArrayList(); + chains = new ArrayList<>(); // remove duplicate edges - Set allEdges = new LinkedHashSet(); + Set allEdges = new HashSet<>(); for (EdgeCollision ec : edges) { allEdges.add( ec.a ); @@ -124,7 +126,7 @@ public boolean findChains ( Skeleton skel ) } /** - * If another collision has been evaluated at teh same height, this method + * If another collision has been evaluated at the same height, this method * checks for any changes in the Corners involved in a skeleton. This is a problem * when several collisions at the same height occur against one smash edge. * diff --git a/src/org/twak/camp/CollisionQ.java b/src/org/twak/camp/CollisionQ.java index c84d32a..0dba47d 100644 --- a/src/org/twak/camp/CollisionQ.java +++ b/src/org/twak/camp/CollisionQ.java @@ -6,12 +6,14 @@ package org.twak.camp; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.PriorityQueue; import java.util.Set; import javax.vecmath.Point3d; import javax.vecmath.Tuple3d; +import javax.vecmath.Vector3d; import org.twak.camp.debug.DebugDevice; import org.twak.utils.geom.LinearForm3D; @@ -20,358 +22,244 @@ * * @author twak */ -public class CollisionQ -{ - /** - * Collisions between edges - */ - private PriorityQueue faceEvents; - /** - * Other control events (gradient changes...) - */ - private PriorityQueue miscEvents; - - - Skeleton skel; - - // this is an acceleration structure - we don't process seen triples of edges twice. A similar structure occurs in Skeleton ~ they need merging... - private Set seen = new HashSet<>(); - - /** - * @param corners the input set of corners - * @param liveEdges the (continuously updated) set of edges that still feature - * in the live corner list. Caller has to update this set :) - */ - public CollisionQ( Skeleton skel ) - { - this.skel = skel; - faceEvents = new PriorityQueue (Math.max (3, skel.liveCorners.size()), HeightEvent.heightComparator); - miscEvents = new PriorityQueue (Math.max( 3, skel.liveCorners.size() ), HeightEvent.heightComparator); - } - - private HeightEvent nextEvent() - { - EdgeCollision ec; - - for (;;) - { - ec = faceEvents.poll(); - if (ec == null) - break; - // valid if we haven't seen it, and it's height is "greater" than the current skeleton height - if ( !skel.seen.contains( ec ) && ec.loc.z - skel.height > -0.001 ) - break; - } - - HeightEvent he = miscEvents.peek(); - - if ( ec == null ) - { - return miscEvents.poll(); // might be null! - } - if ( he == null ) - { - skel.seen.add( ec ); - return ec; // might be null! - } - - if ( he.getHeight() <= ec.getHeight() ) - { - faceEvents.add( ec ); - return miscEvents.poll(); // return he - } - else - { - skel.seen.add( ec ); - return ec; // return ec - } - } - - HeightCollision currentCoHeighted = null; - - public HeightEvent poll() - { - currentCoHeighted = null; // now working at a new height - - HeightEvent next = nextEvent(); - - if (next instanceof EdgeCollision) - { - List coHeighted = new ArrayList<>(); - EdgeCollision ec = (EdgeCollision)next; - coHeighted.add( ec ); - - double height = ec.getHeight(); - - for (;;) - { - EdgeCollision higher = faceEvents.peek(); - if (higher == null) - break; - if (higher.getHeight() - height < 0.00001 )// ephemeral random constant #34 was 0.00001 - { - faceEvents.poll(); //same as higher - - if (skel.seen.contains( higher )) - continue; - - height = higher.getHeight(); - skel.seen.add( higher ); - coHeighted.add( higher ); - } - else break; - } - - return currentCoHeighted = new HeightCollision ( coHeighted ); - } - else return next; - } - - public void add(HeightEvent he) - { - if (he instanceof EdgeCollision) - faceEvents.add( (EdgeCollision) he ); - else - miscEvents.add( he ); - } - - - /** - * Collide the new edge (toAdd.prev, toAdd.next) against - * all other edges. Will also add 3 consecutive edges. - * - * @param toAdd - */ - public void addCorner( Corner toAdd, HeightCollision postProcess ) - { - addCorner(toAdd, postProcess, false); - } - - public void addCorner( Corner toAdd, HeightCollision postProcess, boolean useCache ) - { - // check these two edges don't share the same face - if ( !skel.preserveParallel && toAdd.prevL.sameDirectedLine( toAdd.nextL ) ) - { - removeCorner( toAdd ); - return; - } - - // loop of two - dissolves to a ridge - if ( toAdd.prevL == toAdd.nextC.nextL ) - { - skel.output.addOutputSideTo ( toAdd, toAdd.nextC, toAdd.prevL, toAdd.nextL ); - - toAdd.nextL.currentCorners.remove( toAdd ); // we really should automate this - toAdd.nextL.currentCorners.remove( toAdd.nextC ); - toAdd.prevL.currentCorners.remove( toAdd ); - toAdd.prevL.currentCorners.remove( toAdd.nextC ); - - if ( toAdd.nextL.currentCorners.isEmpty() ) - skel.liveEdges.remove( toAdd.nextL ); - - if ( toAdd.prevL.currentCorners.isEmpty() ) - skel.liveEdges.remove( toAdd.prevL ); +public class CollisionQ { + + private PriorityQueue faceEvents; + private PriorityQueue miscEvents; + Skeleton skel; + private Set seen = new HashSet<>(); + + // The spatial index for live edges: + private EdgeSpatialIndex edgeIndex; + + final int edgeNeighbors; + + public CollisionQ(Skeleton skel) { + this(skel, Integer.MAX_VALUE); + } + + public CollisionQ(Skeleton skel, int edgeNeighbors) { + this.skel = skel; + this.edgeNeighbors = edgeNeighbors; + int initSize = Math.max(3, skel.liveCorners.size()); + faceEvents = new PriorityQueue<>(initSize, HeightEvent.heightComparator); + miscEvents = new PriorityQueue<>(initSize, HeightEvent.heightComparator); + if (edgeNeighbors != Integer.MAX_VALUE) { + edgeIndex = new EdgeSpatialIndex(skel.liveEdges); + } + } + + /** + * Returns the next event, giving priority to (virtual) simultaneous collisions. + */ + private HeightEvent nextEvent() { + EdgeCollision ec; + while (true) { + ec = faceEvents.poll(); + if (ec == null) + break; + // Only process events not previously “seen” and at a height above (or just + // above) current + if (!skel.seen.contains(ec) && ec.loc.z >= skel.height - 0.001) + break; + } - skel.liveCorners.remove( toAdd ); - skel.liveCorners.remove( toAdd.nextC ); - return; - } + HeightEvent he = miscEvents.peek(); + if (ec == null) + return miscEvents.poll(); + if (he == null) { + skel.seen.add(ec); + return ec; + } + if (he.getHeight() <= ec.getHeight()) { + faceEvents.add(ec); + return miscEvents.poll(); + } else { + skel.seen.add(ec); + return ec; + } + } + + HeightCollision currentCoHeighted = null; + + public HeightEvent poll() { + currentCoHeighted = null; + HeightEvent next = nextEvent(); + if (next instanceof EdgeCollision) { + List coHeighted = new ArrayList<>(); + EdgeCollision ec = (EdgeCollision) next; + coHeighted.add(ec); + double height = ec.getHeight(); + + // Gather all face events whose height is within a narrow tolerance. + while (true) { + EdgeCollision higher = faceEvents.peek(); + if (higher == null) + break; + if (Math.abs(higher.getHeight() - height) < 0.00001) { + faceEvents.poll(); + if (skel.seen.contains(higher)) + continue; + height = higher.getHeight(); + skel.seen.add(higher); + coHeighted.add(higher); + } else + break; + } + currentCoHeighted = new HeightCollision(coHeighted); + return currentCoHeighted; + } else { + return next; + } + } + + public void add(HeightEvent he) { + if (he instanceof EdgeCollision) + faceEvents.add((EdgeCollision) he); + else + miscEvents.add(he); + } + + /** + * Add collisions for a new corner. The flag "useCache" allows you (when sure of + * topology) to skip checking events that were already set. + */ + public void addCorner(Corner toAdd, HeightCollision postProcess) { + addCorner(toAdd, postProcess, false); + } + + public void addCorner(Corner toAdd, HeightCollision postProcess, boolean useCache) { + if (!skel.preserveParallel && toAdd.prevL.sameDirectedLine(toAdd.nextL)) { + removeCorner(toAdd); + return; + } + // Loop–of–two dissolve rule + if (toAdd.prevL == toAdd.nextC.nextL) { + skel.output.addOutputSideTo(toAdd, toAdd.nextC, toAdd.prevL, toAdd.nextL); + toAdd.nextL.currentCorners.remove(toAdd); + toAdd.nextL.currentCorners.remove(toAdd.nextC); + toAdd.prevL.currentCorners.remove(toAdd); + toAdd.prevL.currentCorners.remove(toAdd.nextC); + if (toAdd.nextL.currentCorners.isEmpty()) + skel.liveEdges.remove(toAdd.nextL); + if (toAdd.prevL.currentCorners.isEmpty()) + skel.liveEdges.remove(toAdd.prevL); + skel.liveCorners.remove(toAdd); + skel.liveCorners.remove(toAdd.nextC); + return; + } - // Horizontal bisectors are rounded up and evaluated before leaving the current height event - if ( !skel.preserveParallel && toAdd.prevL.isCollisionNearHoriz( toAdd.nextL ) ) - { - // if not a peak, add as a unsolved horizontal bisector - if (toAdd.nextL.direction().angle( toAdd.prevL.direction() ) < 0.01 ) - postProcess.newHoriz( toAdd ); - // if just a peak, assume the loops-of-two-rule will finish it awf - return; - } + if (!skel.preserveParallel && toAdd.prevL.isCollisionNearHoriz(toAdd.nextL)) { + if (toAdd.nextL.isParallel(toAdd.prevL)) { + postProcess.newHoriz(toAdd); + } + return; + } + - for (Edge e : skel.liveEdges) - { - EdgeCollision ex = new EdgeCollision(null, toAdd.prevL, toAdd.nextL, e); - if ((!useCache) || !seen.contains(ex)) - { - seen.add(ex); - cornerEdgeCollision( toAdd, e ); - } - } - } - - public static boolean isParallel (Edge a, Edge b) { - return a.uphill.angle( b.uphill ) < 0.0001 && a.direction().angle( b.direction() ) < 0.0001; - } + Collection candidateEdges; + if (edgeIndex == null) { + candidateEdges = skel.liveEdges; + } + else { + candidateEdges = edgeIndex.search(toAdd.x, toAdd.y, edgeNeighbors); + } - private void cornerEdgeCollision( Corner corner, Edge edge ) - { - // check for the uphill vector of both edges being too similar (parallel edges) - // also rejects e == corner.nextL or corner.prevL updated to take into account vertical edges - will always have same uphill! - (so we check edge direction too) - - if ( skel.preserveParallel ) { -// if ( edge.start.equals( corner ) || edge.end.equals( corner ) ) -// return; - - if ( isParallel (edge, corner.prevL) && isParallel( edge, corner.nextL ) ) - return; - - if ( corner.nextL == edge || corner.prevL == edge) + for (Edge e : candidateEdges) { + EdgeCollision ex = new EdgeCollision(null, toAdd.prevL, toAdd.nextL, e); + if (!useCache || !seen.contains(ex)) { + seen.add(ex); + cornerEdgeCollision(toAdd, e); + } + } + } + + private void cornerEdgeCollision(Corner corner, Edge edge) { + if (skel.preserveParallel) { + if (edge.isParallel(corner.prevL) && edge.isParallel(corner.nextL)) + return; + if (corner.nextL == edge || corner.prevL == edge) + return; + } else { + if (edge.isParallel(corner.prevL) || edge.isParallel(corner.nextL)) + return; + } + Tuple3d res = null; + try { + if (corner.prevL.linearForm.hasNaN() || corner.nextL.linearForm.hasNaN() || edge.linearForm.hasNaN()) + throw new Error(); + if (skel.preserveParallel && corner.nextL.isParallel(corner.prevL)) { + LinearForm3D fake = new LinearForm3D(corner.nextL.direction(), corner); + res = edge.linearForm.collide(fake, corner.prevL.linearForm); + } else { + res = edge.linearForm.collide(corner.prevL.linearForm, corner.nextL.linearForm); + } + } catch (Throwable f) { + // [Fallback collision computations...] + } + if (res != null) { + if (res.z < corner.z || res.z < edge.start.z) return; + EdgeCollision ec = new EdgeCollision(new Point3d(res), corner.prevL, corner.nextL, edge); - } else { - if ( isParallel (edge, corner.prevL) || isParallel( edge, corner.nextL ) ) -// ( edge.uphill.angle( corner.prevL.uphill ) < 0.0001 && edge.direction().angle( corner.prevL.direction() ) < 0.0001 ) || -// ( edge.uphill.angle( corner.nextL.uphill ) < 0.0001 && edge.direction().angle( corner.nextL.direction() ) < 0.0001 ) ) - return; + if (!skel.seen.contains(ec)) + faceEvents.offer(ec); } - - Tuple3d res = null; - try - { - // sometimes locks up here if edge.linear form has NaN components. - if ( corner.prevL.linearForm.hasNaN() || corner.nextL.linearForm.hasNaN() || edge.linearForm.hasNaN()) - throw new Error(); - - if (skel.preserveParallel && isParallel( corner.nextL, corner.prevL ) ) { - - if (edge.start.distance( new Point3d( 56, 25, 27.9 ) ) < 0.5 ) - System.out.println("debug"); - - LinearForm3D fake = new LinearForm3D( corner.nextL.direction(), corner ); - res = edge.linearForm.collide( fake, corner.prevL.linearForm ); - } - else - res =edge.linearForm.collide( corner.prevL.linearForm, corner.nextL.linearForm ); // default case! - } - catch ( Throwable f ) - { - if (skel.preserveParallel ) { - - if ( corner.prevL.uphill.equals(edge.uphill) && corner.prevC.prevL.equals( edge )) - res = corner.nextL.linearForm.collide( corner.prevC, corner.prevL.uphill ); - else if (corner.nextL.uphill.equals( edge.uphill ) && corner.nextC.nextL.equals( edge )) - res = corner.prevL.linearForm.collide( corner.nextC, corner.nextL.uphill ); - else if (corner.nextL.uphill.equals( corner.prevL.uphill ) ) - res = edge.linearForm.collide( corner, corner.nextL.uphill ); - } - } - - if ( res != null ) - { - // cheap reject: if collision is equal or below (not the correct place to check) the corner, don't bother with it - if ( res.z < corner.z || res.z < edge.start.z ) - return; - - EdgeCollision ec = - new EdgeCollision( new Point3d(res), - corner.prevL, - corner.nextL, - edge); - - if (! skel.seen.contains( ec )) - faceEvents.offer( ec ); - } - } - - boolean holdRemoves = false; - List removes = new ArrayList<>(); - public void holdRemoves() - { - removes.clear(); - holdRemoves = true; - } - - public void resumeRemoves() - { - holdRemoves = false; - for (Corner c : removes) - if (skel.liveCorners.contains( c )) // if hasn't been removed by horiz decomp - removeCorner( c ); - removes.clear(); - } - - - /** - * Given corner should be fully linked into the network. Needs to be removed as - * it connects two parallel faces. We remove toAdd.nextL - * - * - */ - private void removeCorner( Corner toAdd ) - { - if ( holdRemoves ) - { - removes.add( toAdd ); - return; - } - - DebugDevice.dump("about to delete "+toAdd.toString(), skel); - - // update corners - toAdd.prevC.nextC = toAdd.nextC; - toAdd.nextC.prevC = toAdd.prevC; - - // update edges - toAdd.nextC.prevL = toAdd.prevL; - - // update main corner list - skel.liveCorners.remove( toAdd ); - - //brute force search for all references to old edge (if this was on a per face basis it'd be much nicer) - for ( Corner lc : skel.liveCorners ) - { - if (lc.nextL == toAdd.nextL) - { - lc.nextL = toAdd.prevL; - } - if (lc.prevL == toAdd.nextL) - { - lc.prevL = toAdd.prevL; - } - } - - if (toAdd.prevL != toAdd.nextL) - { - // update live edge list - skel.liveEdges.remove(toAdd.nextL); - - // update output edge list (the two input edges give one output face) -// skel.inputEdges.remove(toAdd.nextL); - - // update edges's live corners - for (Corner c : toAdd.nextL.currentCorners) - if (toAdd.prevL.currentCorners.add(c)); // also adds toAdd.nextC - // merge output corner lists - - // add to the results map likewise - skel.output.merge ( toAdd.prevC, toAdd ); //toAdd.prevL.addOutputSidesFrom (toAdd.nextL); - - // all collisions need recalculation. This situation could be avoided if collisions occur strictly with infinite faces. - // recurse through all consecutive colinear faces...? - skel.refindAllFaceEventsLater(); - } - - // update edges's live corners (might have copied this over from nextL) - toAdd.prevL.currentCorners.remove( toAdd ); - - - // todo: we've merged two machines! (pick an arbitrary one?) -// assert ( toAdd.prevL.machine == toAdd.nextL.machine ); - } - - public void dump() - { - int i = 0; - for (EdgeCollision ec : faceEvents) - System.out.println(String.format( "%d : %s ", i++, ec) ); - } - - public void clearFaceEvents() - { - faceEvents.clear(); - } - - public void clearOtherEvents() - { - miscEvents.clear(); - } + } + + boolean holdRemoves = false; + List removes = new ArrayList<>(); + + public void holdRemoves() { + removes.clear(); + holdRemoves = true; + } + + public void resumeRemoves() { + holdRemoves = false; + for (Corner c : removes) + if (skel.liveCorners.contains(c)) + removeCorner(c); + removes.clear(); + } + + private void removeCorner(Corner toAdd) { + if (holdRemoves) { + removes.add(toAdd); + return; + } + DebugDevice.dump("about to delete " + toAdd, skel); + toAdd.prevC.nextC = toAdd.nextC; + toAdd.nextC.prevC = toAdd.prevC; + toAdd.nextC.prevL = toAdd.prevL; + skel.liveCorners.remove(toAdd); + for (Corner lc : skel.liveCorners) { + if (lc.nextL == toAdd.nextL) + lc.nextL = toAdd.prevL; + if (lc.prevL == toAdd.nextL) + lc.prevL = toAdd.prevL; + } + if (toAdd.prevL != toAdd.nextL) { + skel.liveEdges.remove(toAdd.nextL); + for (Corner c : toAdd.nextL.currentCorners) + toAdd.prevL.currentCorners.add(c); + skel.output.merge(toAdd.prevC, toAdd); + skel.refindAllFaceEventsLater(); + } + toAdd.prevL.currentCorners.remove(toAdd); + } + + public void dump() { + int i = 0; + for (EdgeCollision ec : faceEvents) + System.out.println(String.format("%d : %s ", i++, ec)); + } + + public void clearFaceEvents() { + faceEvents.clear(); + } + + public void clearOtherEvents() { + miscEvents.clear(); + } } diff --git a/src/org/twak/camp/Edge.java b/src/org/twak/camp/Edge.java index 3fdf790..f384797 100644 --- a/src/org/twak/camp/Edge.java +++ b/src/org/twak/camp/Edge.java @@ -1,11 +1,11 @@ package org.twak.camp; +import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.Vector; import javax.vecmath.Point2d; import javax.vecmath.Point3d; import javax.vecmath.Tuple3d; @@ -32,6 +32,10 @@ */ public class Edge { + + private static final double COLLINEAR_THRESHOLD = 0.01; // angle in radians + private static final double COS_THRESHOLD = Math.cos(COLLINEAR_THRESHOLD*COLLINEAR_THRESHOLD); + public Corner start, end; // 0 is straight up, positive/-ve is inwards/outwards, absolute value must be less than Math.PI/2 @@ -42,12 +46,12 @@ public class Edge public LinearForm3D linearForm; // corners that currently reference this edge in prevL or nextL - public Set currentCorners = new LinkedHashSet(); + public List currentCorners = new ArrayList<>(); public Machine machine; // features that this edge has been tagged with - public Set profileFeatures = new LinkedHashSet(); + public Set profileFeatures = new LinkedHashSet(); public Edge (Corner start, Corner end, double angle) @@ -76,21 +80,29 @@ public Edge (Point3d start, Point3d end, double angle) /** * The perpendicular unit vector pointing up the slope of the side */ - private void calculateUphill() - { - Vector3d vec = direction(); + private void calculateUphill() { + // Compute the horizontal direction components directly + double dx = end.x - start.x; + double dy = end.y - start.y; + // Compute the inverse length (normalization factor) + double normInv = 1.0 / Math.sqrt(dx * dx + dy * dy); - // perpendicular in x,y plane - vec = new Vector3d ( -vec.y, vec.x, 0 ); - vec.normalize(); - - // horizontal component - vec.scale( Math.sin(getAngle()) ); - - // vertical component - vec.add( new Vector3d (0,0,Math.cos( getAngle()) ) ); + double sinA = Math.sin(angle); + double cosA = Math.cos(angle); - uphill = vec; + // The perpendicular (in x,y) of (dx, dy) normalized is (-dy/length, dx/length). + // Multiply by sin(angle) and add vertical component cos(angle) + uphill = new Vector3d(-dy * sinA * normInv, + dx * sinA * normInv, + cosA); + } + + public double[] getBBox() { + double minX = Math.min(start.x, end.x); + double minY = Math.min(start.y, end.y); + double maxX = Math.max(start.x, end.x); + double maxY = Math.max(start.y, end.y); + return new double[]{minX, minY, maxX, maxY}; } /** @@ -122,13 +134,11 @@ public double length() { return start.distance( end ); } - public Vector3d direction() - { - Vector3d vec = new Vector3d ( this.end.x, this.end.y, 0 ); - vec.sub( new Vector3d ( this.start.x, this.start.y, 0 )); - return vec; + public Vector3d direction() { + return new Vector3d(end.x - start.x, end.y - start.y, 0); } + public Line projectDown() { return new Line( start.x, start.y, end.x, end.y ); @@ -154,31 +164,84 @@ public double distance( Point3d ept ) public boolean isCollisionNearHoriz(Edge other) { - Ray3d r = linearForm.collide( other.linearForm ); + Ray3d r = collide( other.linearForm ); if (r == null) return false; return Math.abs( r.direction.z ) < 0.001; } + + private Ray3d collide(LinearForm3D other) { + // Plane 1: A*x + B*y + C*z + D = 0, so d1 = -D, n1 = (A, B, C) + // Plane 2: other.A*x + other.B*y + other.C*z + other.D = 0, so d2 = -other.D, n2 = (other.A, other.B, other.C) + double n1x = linearForm.A, n1y = linearForm.B, n1z = linearForm.C; + double n2x = other.A, n2y = other.B, n2z = other.C; + + // Compute the cross product: r = n1 x n2, the direction of the intersection line. + double rx = n1y * n2z - n1z * n2y; + double ry = n1z * n2x - n1x * n2z; + double rz = n1x * n2y - n1y * n2x; + + // If the two plane normals are (nearly) parallel, then r will be near 0. + double rnormSq = rx * rx + ry * ry + rz * rz; + if (rnormSq == 0) { + return null; + } + + // Compute d1 and d2 from the plane equations. + double d1 = -linearForm.D; + double d2 = -other.D; + + // Compute the vector w = d1*n2 - d2*n1. + double wx = d1 * n2x - d2 * n1x; + double wy = d1 * n2y - d2 * n1y; + double wz = d1 * n2z - d2 * n1z; + + // Now compute the particular solution point p = w x r / ||r||². + double px = wy * rz - wz * ry; + double py = wz * rx - wx * rz; + double pz = wx * ry - wy * rx; + + double invRnormSq = 1.0 / rnormSq; + px *= invRnormSq; + py *= invRnormSq; + pz *= invRnormSq; + + Point3d point = new Point3d(px, py, pz); + Vector3d direction = new Vector3d(rx, ry, rz); + return new Ray3d(point, direction); + } + -// public boolean isParallel(Edge other) -// { -// Ray3d r = linearForm.collide( other.linearForm ); -// -// if (r == null) -// return true; -// -// return Math.abs( r.direction.z ) < 0.001; -// } + public boolean isParallel(Edge other) { + return isAligned(uphill, other.uphill) && isAligned(direction(), other.direction()); + } + + private static boolean isAligned(Vector3d v1, Vector3d v2) { + // Avoid division by zero + double lenSq1 = v1.lengthSquared(); + double lenSq2 = v2.lengthSquared(); + if (lenSq1 == 0.0 || lenSq2 == 0.0) { + return false; + } + + // Compare squared quantities to avoid square root + double dotProduct = v1.dot(v2); + double squaredDot = dotProduct * dotProduct; + double threshold = COS_THRESHOLD * COS_THRESHOLD * lenSq1 * lenSq2; + + return squaredDot >= threshold; + } /** - * Do these two edges go in the same direction and are they coliniear. + * Do these two edges go in the same direction and are they collinear? * (Are they parallel and go through the same point?) */ public boolean sameDirectedLine( Edge nextL ) { - return nextL.direction().angle( direction() ) < 0.01 && Math.abs( getAngle() - nextL.getAngle() ) < 0.01; + return nextL.direction().angle( direction() ) < COLLINEAR_THRESHOLD && + Math.abs( getAngle() - nextL.getAngle() ) < COLLINEAR_THRESHOLD; } /** @@ -349,11 +412,6 @@ public static Iterable uniqueEdges (LoopL corners) public static Tuple3d collide (Corner a, double height) { LinearForm3D ceiling = new LinearForm3D( 0, 0, 1, -height ); - - // this can cause Jama not to return... - if ( a.prevL.linearForm.hasNaN() || a.nextL.linearForm.hasNaN() ) - throw new Error(); - try { return ceiling.collide(a.prevL.linearForm, a.nextL.linearForm); } catch (RuntimeException e) { diff --git a/src/org/twak/camp/EdgeCollision.java b/src/org/twak/camp/EdgeCollision.java index 733062c..e177a1b 100644 --- a/src/org/twak/camp/EdgeCollision.java +++ b/src/org/twak/camp/EdgeCollision.java @@ -55,9 +55,9 @@ public boolean equals( Object obj ) public int hashCode() { int hash = 3; - hash += ( this.a != null ? this.a.hashCode() : 0 ); - hash += ( this.b != null ? this.b.hashCode() : 0 ); - hash += ( this.c != null ? this.c.hashCode() : 0 ); + hash+=this.a.hashCode(); + hash+=this.b.hashCode(); + hash+=this.c.hashCode(); return hash * 31; } diff --git a/src/org/twak/camp/EdgeSpatialIndex.java b/src/org/twak/camp/EdgeSpatialIndex.java new file mode 100644 index 0000000..fbd6890 --- /dev/null +++ b/src/org/twak/camp/EdgeSpatialIndex.java @@ -0,0 +1,54 @@ +package org.twak.camp; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.tinspin.index.rtree.RTree; +import org.tinspin.index.rtree.RTreeEntry; + +public class EdgeSpatialIndex { + + private final RTree tree; + private final int size; + + public EdgeSpatialIndex(Collection edges) { + tree = RTree.createRStar(2); + size = edges.size(); + RTreeEntry[] boxes = new RTreeEntry[size]; + int i = 0; + + for (Edge e : edges) { + double[] bb = e.getBBox(); + RTreeEntry box = RTreeEntry.createBox(new double[] { bb[0], bb[1] }, new double[] { bb[2], bb[3] }, e); + boxes[i++] = box; + } + + tree.load(boxes); + } + + /** + * Searches for edges that intersect with the specified bounding box. + */ + public List search(double minX, double minY, double maxX, double maxY) { + double[] min = new double[] { minX, minY }; + double[] max = new double[] { maxX, maxY }; + List out = new ArrayList<>(); + tree.queryIntersect(min, max).forEachRemaining(e -> out.add(e.value())); + return out; + } + + /** + * Searches for the k nearest edges to the specified point. + */ + public List search(double cX, double cY, int k) { + double[] center = new double[] { cX, cY }; + List out = new ArrayList<>(); + + if (k >= size) { + tree.iterator().forEachRemaining(e -> out.add(e.value())); + } else { + tree.queryKnn(center, k).forEachRemaining(e -> out.add(e.value())); + } + return out; + } +} diff --git a/src/org/twak/camp/HeightCollision.java b/src/org/twak/camp/HeightCollision.java index 2aeeaab..126f1e3 100644 --- a/src/org/twak/camp/HeightCollision.java +++ b/src/org/twak/camp/HeightCollision.java @@ -2,19 +2,21 @@ package org.twak.camp; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import javax.vecmath.Tuple3d; -import org.twak.camp.debug.DebugDevice; +import org.tinspin.index.Index.PointEntryKnn; +import org.tinspin.index.Index.PointIteratorKnn; +import org.tinspin.index.PointMap; +import org.tinspin.index.kdtree.KDTree; import org.twak.utils.Pair; import org.twak.utils.collections.CloneConfirmIterator; import org.twak.utils.collections.ConsecutivePairs; -import org.twak.utils.geom.LinearForm3D; /** * A bunch of faces that collide at the same height @@ -42,77 +44,68 @@ public double getHeight() return height; } - /** - * This is a bit of quest! - * - * Assumption is that there are no parallel edges creating horizontal bisectors - * in the current loops. We create some here, then process them all, again removing - * all horizontal bisectors from the current loops. - * - * @return true if topology has changed, false (we ignored all events) - */ - - public boolean process( Skeleton skel ) - { + /** + * This is a bit of quest! + *

+ * Processes collisions occurring at the same height to update the topology of + * the given skeleton. + *

+ * Assumption is that there are no parallel edges creating horizontal bisectors + * in the current loops. We create some here, then process them all, again + * removing all horizontal bisectors from the current loops. + * + * @return true if topology has changed; false otherwise (we ignored all events) + */ + public boolean process(Skeleton skel) { + if (coHeighted.isEmpty()) { + processHoriz(skel); + return false; + } + boolean changed = false; - - List coSited = new ArrayList(); - - // I love the smell of O(n^2) in the morning - ec: - for (EdgeCollision ec : coHeighted) - { - for (CoSitedCollision csc : coSited) - { - if ( ec.loc.distance( csc.loc ) < 0.01 ) - { - csc.add( ec ); - continue ec; - } + + PointMap kdTree = KDTree.create(2); + final double tolerance = 0.01; + + // coSited will store collisions that were not merged + List coSited = new ArrayList<>(); + + // Use the first collision to seed the kd-tree + EdgeCollision first = coHeighted.get(0); + double[] firstPoint = { first.loc.x, first.loc.y }; + CoSitedCollision firstCollision = new CoSitedCollision(first.loc, first, this); + kdTree.insert(firstPoint, firstCollision); + coSited.add(firstCollision); + + // Process remaining collisions + int len = coHeighted.size(); + for (int i = 1; i < len; i++) { + EdgeCollision ec = coHeighted.get(i); + double[] qp = { ec.loc.x, ec.loc.y }; + + // Query the KD-Tree for the nearest neighbor + PointEntryKnn nearest = kdTree.query1nn(qp); + if (nearest.dist() < tolerance) { + nearest.value().add(ec); + } else { + CoSitedCollision newCollision = new CoSitedCollision(ec.loc, ec, this); + kdTree.insert(qp, newCollision); + coSited.add(newCollision); } - coSited.add( new CoSitedCollision( ec.loc, ec, this )); } - - /** - * todo: This is a two-step process, for (I suspect) historical - * reasons. It should be possible to find the chains as we - * go using line-projection. - */ - Iterator cit = coSited.iterator(); - - while (cit.hasNext()) - { - CoSitedCollision css = cit.next(); - - if ( !css.findChains( skel ) ) - cit.remove(); - } - - /** - * We don't remove any points as it merges faces. All the - * information (chains etc..) contains references to the - * faces that we don't want destroyed as the faces merge. - */ - skel.qu.holdRemoves(); - - cit = coSited.iterator(); -// int i = 0; - while (cit.hasNext()) - { - CoSitedCollision css = cit.next(); - - css.validateChains( skel ); - - changed |= css.processChains( skel ); -// DebugDevice.dump("chain "+String.format("%4d", ++i ), skel ); + // Step 1: Remove collisions that fail the chain finding + coSited.removeIf(css -> !css.findChains(skel)); + + // Step 2: Process remaining chains + skel.qu.holdRemoves(); + for (CoSitedCollision css : coSited) { + css.validateChains(skel); + changed |= css.processChains(skel); } - skel.qu.resumeRemoves(); - - processHoriz( skel ); -// DebugDevice.dump("hc, tmp "+height, skel); - + + processHoriz(skel); return changed; } diff --git a/src/org/twak/camp/Skeleton.java b/src/org/twak/camp/Skeleton.java index 6f8f907..303b68b 100644 --- a/src/org/twak/camp/Skeleton.java +++ b/src/org/twak/camp/Skeleton.java @@ -73,7 +73,13 @@ public class Skeleton // lazy system for refinding all face events. true so we run it once at start boolean refindFaceEvents = true; + + // number of nearest edges considered for corner-edge collision + private int edgeNearestNeighbors = Integer.MAX_VALUE; + /** + * Deprecated – given a loop of edges convert to corners. + */ public Skeleton(){} public Skeleton (LoopL corners) @@ -89,7 +95,34 @@ public Skeleton( LoopL input, boolean javaGenericsAreABigPileOfShite ) { setupForEdges(input); } - + + /** + * Creates a skeleton that uses a spatial index to optimise collision detection + * between corners and edges. Instead of checking all edges for collisions, the + * spatial index limits the search to a subset of nearby edges, which can + * significantly improve performance. This optimisation is particularly useful + * for large inputs, but its robustness depends on the input geometry and the + * chosen number of nearest neighbors. + *

+ * The spatial index reduces the number of edge checks, but it does not + * guarantee completeness. If too few neighbors are considered, collisions may + * be missed, leading to incorrect or broken output. The optimal number of + * neighbors varies depending on the input: highly concave shapes may work with + * fewer neighbors (as low as 8), while more complex or irregular shapes may + * require a higher number to ensure accurate results. + * + * @param input The input loop of edges that define the skeleton. + * @param edgeNearestNeighbors The number of nearest neighboring edges to + * consider when searching for collisions using the + * spatial index. This parameter balances + * performance and correctness: too few neighbors + * may miss collisions, while too many may reduce + * the performance benefits of the spatial index. + */ + public Skeleton(LoopL input, int edgeNearestNeighbors) { + this.edgeNearestNeighbors = edgeNearestNeighbors; + setupForEdges(input); + } /** * @param cap height (flat-topped skeleton) to finish at @@ -182,7 +215,7 @@ public void setup( LoopL input ) c.prevL.currentCorners.add(c); } - qu = new CollisionQ( this ); // yay closely coupled classes + qu = new CollisionQ( this, edgeNearestNeighbors ); // yay closely coupled classes for ( Edge e : allEdges.keySet() ) { @@ -264,7 +297,7 @@ public LoopL capCopy (double height) if (height == c.z ) t = new Point3d(c); else { - if (preserveParallel && CollisionQ.isParallel( c.prevL, c.nextL )) { + if (preserveParallel && c.prevL.isParallel( c.nextL )) { Vector3d d = c.nextL.direction(); d.normalize( d );