Skip to content

Commit

Permalink
adjust skeleton selection (remove kendzi)
Browse files Browse the repository at this point in the history
  • Loading branch information
micycle1 committed Feb 11, 2025
1 parent 2de71c9 commit ccafb84
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 106 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Optimised `PGS_CirclePacking.tangencyPack()`. It's now around 1.5-2x faster and has higher precision.
* `PGS_Conversion.roundVertexCoords()` now returns a rounded copy of the input (rather than mutating the input).
* Outputs from `PGS_Conversion.toDualGraph()` will now always iterate deterministically on inputs with the same geometry but having a different structure.
* `PGS_Contour.straightSkeleton()` now always uses a more robust approach (which has been sped up considerably too).

### Fixed
* `PGS_Morphology.rounding()` no longer gives invalid results.
Expand Down
131 changes: 25 additions & 106 deletions src/main/java/micycle/pgs/PGS_Contour.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.vecmath.Point3d;

Expand Down Expand Up @@ -234,15 +235,26 @@ public static PShape chordalAxis(PShape shape) {
* consisting of straight-line segments only. Roughly, it is the geometric graph
* whose edges are the traces of vertices of shrinking mitered offset curves of
* the polygon.
*
* <p>
* For a single polygon, this method returns a GROUP PShape containing three
* children:
* <ul>
* <li>Child 0: GROUP PShape consisting of skeleton faces.</li>
* <li>Child 1: LINES PShape representing branches, which are lines connecting
* the skeleton to the polygon's edge.</li>
* <li>Child 2: LINES PShape composed of bones, depicting the pure straight
* skeleton of the polygon.</li>
* </ul>
* <p>
* For multi-polygons, the method returns a master GROUP PShape. This master
* shape includes multiple skeleton GROUP shapes, each corresponding to a single
* polygon and structured as described above.
*
* @param shape a single polygon (that can contain holes), or a multi polygon
* (whose polygons can contain holes)
* @return when the input is a single polygon, returns a GROUP PShape containing
* 3 children: child 1 = GROUP PShape of skeleton faces; child 2 = LINES
* PShape of branches (lines that connect skeleton to edge); child 3 =
* LINES PShape of bones (the pure straight skeleton). For
* multi-polygons, a master GROUP shape of skeleton GROUP shapes
* (described above) is returned.
*
* @return PShape based on the input polygon structure, either as a single or
* multi-polygon skeleton representation.
*/
public static PShape straightSkeleton(PShape shape) {
final Geometry g = fromPShape(shape);
Expand All @@ -257,25 +269,7 @@ public static PShape straightSkeleton(PShape shape) {
return shape;
}

/**
*
* @param polygon a single polygon that can contain holes
* @return
*/
private static PShape straightSkeleton(Polygon polygon) {
/*
* Kenzi implementation (since PGS 1.3.0) is much faster (~50x!) but can fail on
* more complicated inputs. Therefore try Kenzi implementation first, but fall
* back to Twak implementation if it fails.
*/
try {
return straightSkeletonKendzi(polygon);
} catch (Exception e) {
return straightSkeletonTwak(polygon);
}
}

private static PShape straightSkeletonTwak(Polygon polygon) {
if (polygon.getCoordinates().length > 1000) {
polygon = (Polygon) DouglasPeuckerSimplifier.simplify(polygon, 2);
}
Expand Down Expand Up @@ -303,7 +297,7 @@ private static PShape straightSkeletonTwak(Polygon polygon) {
skeleton.skeleton(); // compute skeleton

skeleton.output.faces.values().forEach(f -> {
final List<Point3d> vertices = f.getLoopL().iterator().next().asList();
List<Point3d> vertices = f.getLoopL().iterator().next().stream().toList();
List<PVector> faceVertices = new ArrayList<>();

for (int i = 0; i < vertices.size(); i++) {
Expand Down Expand Up @@ -352,95 +346,20 @@ private static PShape straightSkeletonTwak(Polygon polygon) {
return lines;
}

private static PShape straightSkeletonKendzi(Polygon polygon) {
final LinearRing[] rings = new LinearRingIterator(polygon).getLinearRings();
Set<Vector2dc> edgeCoordsSet = new HashSet<>();
final List<Vector2dc> points = ringToVec(rings[0], edgeCoordsSet);
final List<List<Vector2dc>> holes = new ArrayList<>();
for (int i = 1; i < rings.length; i++) {
holes.add(ringToVec(rings[i], edgeCoordsSet));
}

final SkeletonOutput so = kendzi.math.geometry.skeleton.Skeleton.skeleton(points, holes, new SkeletonConfiguration());
final PShape skeleton = new PShape(PConstants.GROUP);
final PShape faces = new PShape(PConstants.GROUP);
/*
* Create PEdges first to prevent lines being duplicated in output shapes since
* faces share branches and bones.
*/
final Set<PEdge> branchEdges = new HashSet<>();
final Set<PEdge> boneEdges = new HashSet<>();
so.getFaces().forEach(f -> {
/*
* q stores the index of second vertex of the face that is a shape vertex. This
* is used to rotate f.getPoints() so that the vertices of every face PShape
* begin at the shape edge.
*/
int q = 0;
for (int i = 0; i < f.getPoints().size(); i++) {
final Vector2dc p1 = f.getPoints().get(i);
final Vector2dc p2 = f.getPoints().get((i + 1) % f.getPoints().size());
final boolean a = edgeCoordsSet.contains(p1);
final boolean b = edgeCoordsSet.contains(p2);
if (a ^ b) { // branch (xor)
branchEdges.add(new PEdge(p1.x(), p1.y(), p2.x(), p2.y()));
q = i;
} else {
if (!a) { // bone
boneEdges.add(new PEdge(p1.x(), p1.y(), p2.x(), p2.y()));
} else {
q = i;
}
}
}

List<PVector> faceVertices = new ArrayList<>(f.getPoints().size());
Collections.rotate(f.getPoints(), -q + 1);
f.getPoints().forEach(p -> faceVertices.add(new PVector((float) p.x(), (float) p.y())));

PShape face = PGS_Conversion.fromPVector(faceVertices);
face.setStroke(true);
face.setStrokeWeight(2);
face.setStroke(ColorUtils.composeColor(147, 112, 219));
faces.addChild(face);
});

final PShape bones = prepareLinesPShape(null, null, 4);
boneEdges.forEach(e -> {
bones.vertex(e.a.x, e.a.y);
bones.vertex(e.b.x, e.b.y);
});
bones.endShape();

final PShape branches = prepareLinesPShape(ColorUtils.composeColor(40, 235, 180), null, null);
branchEdges.forEach(e -> {
branches.vertex(e.a.x, e.a.y);
branches.vertex(e.b.x, e.b.y);
});
branches.endShape();

skeleton.addChild(faces);
skeleton.addChild(branches);
skeleton.addChild(bones);

return skeleton;
}

/**
* Generates a topographic-like isoline contour map from the shape's vertices.
* The "elevation" (or z value) of points is the euclidean distance between a
* point in the shape and the given "high" point.
* Generates a topographic-like isoline contour map from the shape's vertices
* and a given "high point". Isolines represent the "elevation", or euclidean
* distance, between a location in the shape and the "high point".
* <p>
* Assigns each point feature a number equal to the distance between geometry's
* centroid and the point.
*
* @param shape
* @param shape the bounds in which to draw isolines
* @param highPoint position of "high" point within the shape
* @param intervalSpacing distance between successive isolines
* @return PShape containing isolines linework
* @return PShape containing isolines linework
*/
public static PShape isolines(PShape shape, PVector highPoint, double intervalSpacing) {

/*
* Also See:
* https://github.com/hageldave/JPlotter/blob/master/jplotter/src/main/java/
Expand Down

0 comments on commit ccafb84

Please sign in to comment.