From ccafb84362db3013979e17da0db076ba5e10c95c Mon Sep 17 00:00:00 2001 From: Michael Carleton Date: Tue, 11 Feb 2025 22:45:32 +0000 Subject: [PATCH] adjust skeleton selection (remove kendzi) --- CHANGELOG.md | 1 + src/main/java/micycle/pgs/PGS_Contour.java | 131 ++++----------------- 2 files changed, 26 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68dc1dce..8a5dde73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/main/java/micycle/pgs/PGS_Contour.java b/src/main/java/micycle/pgs/PGS_Contour.java index fc8d7861..2dfa23ac 100644 --- a/src/main/java/micycle/pgs/PGS_Contour.java +++ b/src/main/java/micycle/pgs/PGS_Contour.java @@ -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; @@ -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. - * + *

+ * For a single polygon, this method returns a GROUP PShape containing three + * children: + *

+ *

+ * 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); @@ -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); } @@ -303,7 +297,7 @@ private static PShape straightSkeletonTwak(Polygon polygon) { skeleton.skeleton(); // compute skeleton skeleton.output.faces.values().forEach(f -> { - final List vertices = f.getLoopL().iterator().next().asList(); + List vertices = f.getLoopL().iterator().next().stream().toList(); List faceVertices = new ArrayList<>(); for (int i = 0; i < vertices.size(); i++) { @@ -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 edgeCoordsSet = new HashSet<>(); - final List points = ringToVec(rings[0], edgeCoordsSet); - final List> 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 branchEdges = new HashSet<>(); - final Set 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 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". *

* 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/