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 );