Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PGS 2.1 #135

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to PGS will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Dates are *YYYY-MM-DD*.

## **2.1** *(2025-xx-xx)*

### Added
* `smoothLaneRiesenfeld` to `PGS_Morphology`. Smooths a shape using Lane-Riesenfeld curve subdivision with 4-point refinement to reduce contraction.
* Additional method signature for `PGS_Conversion.roundVertexCoords()` that accepts a number of decimal places.

### Changes
* 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.

### Fixed
* Fixed invalid results given by `PGS_Morphology.rounding()`.

### Removed

## **2.0** *(2025-01-11)*

**NOTE: Beginning at v2.0, PGS is built with Java 17.**
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Library functionality is split over the following classes:
* `PGS_Contour`
* Methods that produce various contours from shapes: medial axes, straight skeletons, offset curves, etc.
* `PGS_Conversion`
* Conversion between *Processing* PShapes and *JTS* Geometries (amongst other formats)
* Conversion between *Processing* PShapes and *JTS* Geometries (amongst other formats).
* `PGS_Hull`
* Convex and concave hulls of polygons and point sets.
* `PGS_Meshing`
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>micycle</groupId>
<artifactId>PGS</artifactId>
<version>2.0</version>
<version>2.1-SNAPSHOT</version>
<name>Processing Geometry Suite</name>
<description>Geometric algorithms for Processing</description>

Expand Down Expand Up @@ -88,7 +88,7 @@
<profile>
<id>uber-jar</id>
<build>
<finalName>${fatJarName}</finalName>
<finalName>${fatJarName}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
Binary file modified resources/contour/distanceField.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified resources/contour/isolines.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified resources/morphology/round.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions src/main/java/micycle/pgs/PGS.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package micycle.pgs;

import static micycle.pgs.PGS_Conversion.fromPShape;
import static micycle.pgs.PGS_Conversion.toPShape;
import static processing.core.PConstants.GROUP;
import static processing.core.PConstants.LINES;
import static processing.core.PConstants.ROUND;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -12,6 +16,7 @@
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.UnaryOperator;

import org.jgrapht.graph.SimpleWeightedGraph;
import org.locationtech.jts.geom.Coordinate;
Expand Down Expand Up @@ -557,4 +562,63 @@ public void remove() {
}
}

/**
* Applies a given function to all lineal elements (LineString and LinearRing)
* in the input PShape. The function processes each LineString or LinearRing and
* returns a modified LineString. The method preserves the structure of the
* input geometry, including exterior-hole relationships in polygons and
* multi-geometries.
*
* @param shape The input PShape containing lineal elements to process.
* @param function A UnaryOperator that takes a LineString or LinearRing and
* returns a modified LineString.
* @return A new PShape with the processed lineal elements. Returns an empty
* PShape for unsupported geometry types.
* @since 2.1
*/
static PShape applyToLinealGeometries(PShape shape, UnaryOperator<LineString> function) {
Geometry g = fromPShape(shape);
switch (g.getGeometryType()) {
case Geometry.TYPENAME_GEOMETRYCOLLECTION :
case Geometry.TYPENAME_MULTIPOLYGON :
case Geometry.TYPENAME_MULTILINESTRING :
PShape group = new PShape(GROUP);
for (int i = 0; i < g.getNumGeometries(); i++) {
group.addChild(applyToLinealGeometries(toPShape(g.getGeometryN(i)), function));
}
return group;
case Geometry.TYPENAME_LINEARRING :
case Geometry.TYPENAME_POLYGON :
// Preserve exterior-hole relations
LinearRing[] rings = new LinearRingIterator(g).getLinearRings();
LinearRing[] processedRings = new LinearRing[rings.length];
for (int i = 0; i < rings.length; i++) {
LinearRing ring = rings[i];
LineString out = function.apply(ring);
if (out.isClosed()) {
processedRings[i] = GEOM_FACTORY.createLinearRing(out.getCoordinates());
} else {
System.err.println("Output LineString is not closed. Closing it automatically.");
Coordinate[] closedCoords = Arrays.copyOf(out.getCoordinates(), out.getNumPoints() + 1);
closedCoords[closedCoords.length - 1] = closedCoords[0]; // Close the ring
processedRings[i] = GEOM_FACTORY.createLinearRing(closedCoords);
}
}
if (processedRings.length == 0) {
return new PShape();
}
LinearRing[] holes = null;
if (processedRings.length > 1) {
holes = Arrays.copyOfRange(processedRings, 1, processedRings.length);
}
return toPShape(GEOM_FACTORY.createPolygon(processedRings[0], holes));
case Geometry.TYPENAME_LINESTRING :
LineString l = (LineString) g;
LineString out = function.apply(l);
return toPShape(out);
default :
return new PShape(); // Return empty (so element is invisible if not processed)
}
}

}
6 changes: 4 additions & 2 deletions src/main/java/micycle/pgs/PGS_Coloring.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
* @since 1.2.0
*/
public final class PGS_Coloring {

public static long SEED = 1337;

private PGS_Coloring() {
}
Expand Down Expand Up @@ -256,10 +258,10 @@ private static Coloring<PShape> findColoring(Collection<PShape> shapes, Coloring
break;
case RLF_BRUTE_FORCE_4COLOR :
int iterations = 0;
long seed = 1337;
long seed = SEED; // init as default
do {
coloring = new RLFColoring<>(graph, seed).getColoring();
seed = ThreadLocalRandom.current().nextLong();
seed = ThreadLocalRandom.current().nextLong(); // randomise seed
iterations++;
} while (coloring.getNumberColors() > 4 && iterations < 250);
break;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/micycle/pgs/PGS_Construction.java
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,7 @@ public static PShape createRandomSFCurve(int nColumns, int nRows, double cellWid
* @param cellWidth visual/pixel width of each cell
* @param cellHeight visual/pixel width of each cell
* @param seed random seed
* @return a stroked PATH PShape
* @return a mitered stroked PATH PShape
* @see #createRandomSFCurve(int, int, double, double)
* @since 1.4.0
*/
Expand Down
120 changes: 93 additions & 27 deletions src/main/java/micycle/pgs/PGS_Conversion.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import micycle.pgs.color.Colors;
import micycle.pgs.commons.Nullable;
import micycle.pgs.commons.PEdge;
import net.jafama.FastMath;
import processing.core.PConstants;
import processing.core.PMatrix;
import processing.core.PShape;
Expand Down Expand Up @@ -1059,32 +1060,43 @@ public static SimpleGraph<PVector, PEdge> toCentroidDualGraph(PShape mesh) {
*/
static SimpleGraph<PShape, DefaultEdge> toDualGraph(Collection<PShape> meshFaces) {
final SimpleGraph<PShape, DefaultEdge> graph = new SimpleGraph<>(DefaultEdge.class);
// map of which edge belong to each face; used to detect half-edges
final HashMap<PEdge, PShape> edgesMap = new HashMap<>(meshFaces.size() * 4);
final Map<PEdge, List<PShape>> edgeFacesMap = new HashMap<>();

// Phase 1: Collect edges and their associated faces
for (PShape face : meshFaces) {
graph.addVertex(face); // always add child so disconnected shapes are colored
graph.addVertex(face);
for (int i = 0; i < face.getVertexCount(); i++) {
final PVector a = face.getVertex(i);
final PVector b = face.getVertex((i + 1) % face.getVertexCount());
PVector a = face.getVertex(i);
PVector b = face.getVertex((i + 1) % face.getVertexCount());
if (a.equals(b)) {
continue;
}
final PEdge e = new PEdge(a, b);
final PShape neighbour = edgesMap.get(e);

if (neighbour != null) {
// edge seen before, so faces must be adjacent; create edge between faces
if (neighbour.equals(face)) { // probably bad input (3 edges the same)
System.err.println("toDualGraph(): Bad input — saw the same edge 3 times.");
continue; // continue to prevent self-loop in graph
}
graph.addEdge(neighbour, face);
} else {
edgesMap.put(e, face); // edge is new
}
PEdge edge = new PEdge(a, b);
edgeFacesMap.computeIfAbsent(edge, k -> new ArrayList<>()).add(face);
}
}

// Phase 2: Process edges in sorted order for graph iteration consistency
edgeFacesMap.entrySet().stream().sorted(Comparator.comparing(e -> e.getKey())) // Sort edges to ensure deterministic processing
.forEach(entry -> {
List<PShape> faces = entry.getValue();
if (faces.size() == 2) {
// If exactly two faces share this edge, connect them in the dual graph
PShape f1 = faces.get(0);
PShape f2 = faces.get(1);
if (!f1.equals(f2)) {
graph.addEdge(f1, f2); // Avoid self-loops
} else {
// Handle case where the same face is associated with the edge twice
System.err.println("toDualGraph(): Bad input — saw the same edge 3+ times for face: " + f1);
}
} else if (faces.size() > 2) {
// Handle edges shared by more than two faces
System.err.println("toDualGraph(): Bad input — edge shared by more than two faces: " + entry.getKey().toString());
}
});

return graph;
}

Expand Down Expand Up @@ -1449,7 +1461,7 @@ public static PShape fromContours(List<PVector> shell, @Nullable List<List<PVect
* @see #fromArray(double[][], boolean)
*/
public static double[][] toArray(PShape shape, boolean keepClosed) {
List<PVector> points = toPVector(shape); // CLOSE
List<PVector> points = toPVector(shape); // unclosed
if (shape.isClosed() && keepClosed) {
points.add(points.get(0).copy()); // since toPVector returns unclosed view
}
Expand Down Expand Up @@ -1486,14 +1498,18 @@ public static PShape fromArray(double[][] shape, boolean close) {

/**
* Flattens a collection of PShapes into a single GROUP PShape which has the
* input shapes as its children.
* input shapes as its children. If the collection contains only one shape, it
* is directly returned.
*
* @since 1.2.0
* @see #flatten(PShape...)
*/
public static PShape flatten(Collection<PShape> shapes) {
PShape group = new PShape(GROUP);
shapes.forEach(group::addChild);
if (group.getChildCount() == 1) {
return group.getChild(0);
}
return group;
}

Expand Down Expand Up @@ -1599,6 +1615,7 @@ public static PShape setAllFillColor(PShape shape, int color) {
*
* @param shape
* @return the input object (having now been mutated)
* @see #setAllStrokeColor(PShape, int, double, int)
* @see #setAllFillColor(PShape, int)
*/
public static PShape setAllStrokeColor(PShape shape, int color, double strokeWeight) {
Expand All @@ -1610,6 +1627,27 @@ public static PShape setAllStrokeColor(PShape shape, int color, double strokeWei
return shape;
}

/**
* Sets the stroke color and cap style for the PShape and all of its children
* recursively.
*
* @param strokeCap either <code>SQUARE</code>, <code>PROJECT</code>, or
* <code>ROUND</code>
* @return the input object (having now been mutated)
* @see #setAllStrokeColor(PShape, int, double)
* @see #setAllFillColor(PShape, int)
* @since 2.1
*/
public static PShape setAllStrokeColor(PShape shape, int color, double strokeWeight, int strokeCap) {
getChildren(shape).forEach(child -> {
child.setStroke(true);
child.setStroke(color);
child.setStrokeWeight((float) strokeWeight);
child.setStrokeCap(strokeCap);
});
return shape;
}

/**
* Sets the stroke color equal to the fill color for the PShape and all of its
* descendent shapes individually (that is, each child shape belonging to the
Expand Down Expand Up @@ -1707,21 +1745,40 @@ public static PShape disableAllStroke(PShape shape) {

/**
* Rounds the x and y coordinates (to the closest int) of all vertices belonging
* to the shape, <b>mutating</b> the shape. This can sometimes fix a visual
* problem in Processing where narrow gaps can appear between otherwise flush
* shapes.
* to the shape. This can sometimes fix a visual problem in Processing where
* narrow gaps can appear between otherwise flush shapes. If the shape is a
* GROUP, the rounding is applied to all child shapes.
*
* @return the input object (having now been mutated)
* @param shape the PShape to round vertex coordinates for.
* @return a rounded copy of the input shape.
* @see #roundVertexCoords(PShape, int)
* @since 1.1.3
*/
public static PShape roundVertexCoords(PShape shape) {
getChildren(shape).forEach(c -> {
return roundVertexCoords(shape, 0);
}

/**
* Rounds the x and y coordinates (to <code>n</code> decimal places) of all
* vertices belonging to the shape. This can sometimes fix a visual problem in
* Processing where narrow gaps can appear between otherwise flush shapes. If
* the shape is a GROUP, the rounding is applied to all child shapes.
*
* @param shape the PShape to round vertex coordinates for.
* @param n The number of decimal places to which the coordinates should be
* rounded.
* @return a rounded copy of the input shape.
* @since 2.1
*/
public static PShape roundVertexCoords(PShape shape, int n) {
return PGS_Processing.transform(shape, s -> {
var c = copy(s);
for (int i = 0; i < c.getVertexCount(); i++) {
final PVector v = c.getVertex(i);
c.setVertex(i, Math.round(v.x), Math.round(v.y));
c.setVertex(i, round(v.x, n), round(v.y, n));
}
return c;
});
return shape;
}

/**
Expand Down Expand Up @@ -1943,6 +2000,12 @@ private static <T> T[] reversedCopy(T[] original) {
return reversed;
}

private static float round(float x, float n) {
float m = (float) FastMath.pow(10, n);

return FastMath.floor(m * x + 0.5f) / m;
}

/**
* A utility class for storing and manipulating the visual properties of PShapes
* from the Processing library. It encapsulates the stroke, fill, stroke color,
Expand Down Expand Up @@ -1990,8 +2053,9 @@ public static class PShapeData {
* Apply this shapedata to a given PShape.
*
* @param other
* @return other (fluent interface)
*/
public void applyTo(PShape other) {
public PShape applyTo(PShape other) {
if (other.getFamily() == GROUP) {
getChildren(other).forEach(c -> applyTo(c));
}
Expand All @@ -2000,6 +2064,8 @@ public void applyTo(PShape other) {
other.setStroke(stroke);
other.setStroke(strokeColor);
other.setStrokeWeight(strokeWeight);

return other;
}

@Override
Expand Down
Loading