Skip to content

Commit 75636c3

Browse files
committed
always determinisic toDualGraph()
1 parent 6654ab0 commit 75636c3

File tree

3 files changed

+66
-22
lines changed

3 files changed

+66
-22
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Changes
1515
* Optimised `PGS_CirclePacking.tangencyPack()`. It's now around 1.5-2x faster and has higher precision.
1616
* `PGS_Conversion.roundVertexCoords()` now returns a rounded copy of the input (rather than mutating the input).
17+
* Outputs from `PGS_Conversion.toDualGraph()` will now always iterate deterministically on inputs with the same geometry but having a different structure.
1718

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

src/main/java/micycle/pgs/PGS_Conversion.java

+43-19
Original file line numberDiff line numberDiff line change
@@ -1060,32 +1060,43 @@ public static SimpleGraph<PVector, PEdge> toCentroidDualGraph(PShape mesh) {
10601060
*/
10611061
static SimpleGraph<PShape, DefaultEdge> toDualGraph(Collection<PShape> meshFaces) {
10621062
final SimpleGraph<PShape, DefaultEdge> graph = new SimpleGraph<>(DefaultEdge.class);
1063-
// map of which edge belong to each face; used to detect half-edges
1064-
final HashMap<PEdge, PShape> edgesMap = new HashMap<>(meshFaces.size() * 4);
1063+
final Map<PEdge, List<PShape>> edgeFacesMap = new HashMap<>();
10651064

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

1077-
if (neighbour != null) {
1078-
// edge seen before, so faces must be adjacent; create edge between faces
1079-
if (neighbour.equals(face)) { // probably bad input (3 edges the same)
1080-
System.err.println("toDualGraph(): Bad input — saw the same edge 3 times.");
1081-
continue; // continue to prevent self-loop in graph
1082-
}
1083-
graph.addEdge(neighbour, face);
1084-
} else {
1085-
edgesMap.put(e, face); // edge is new
1086-
}
1075+
PEdge edge = new PEdge(a, b);
1076+
edgeFacesMap.computeIfAbsent(edge, k -> new ArrayList<>()).add(face);
10871077
}
10881078
}
1079+
1080+
// Phase 2: Process edges in sorted order for graph iteration consistency
1081+
edgeFacesMap.entrySet().stream().sorted(Comparator.comparing(e -> e.getKey())) // Sort edges to ensure deterministic processing
1082+
.forEach(entry -> {
1083+
List<PShape> faces = entry.getValue();
1084+
if (faces.size() == 2) {
1085+
// If exactly two faces share this edge, connect them in the dual graph
1086+
PShape f1 = faces.get(0);
1087+
PShape f2 = faces.get(1);
1088+
if (!f1.equals(f2)) {
1089+
graph.addEdge(f1, f2); // Avoid self-loops
1090+
} else {
1091+
// Handle case where the same face is associated with the edge twice
1092+
System.err.println("toDualGraph(): Bad input — saw the same edge 3+ times for face: " + f1);
1093+
}
1094+
} else if (faces.size() > 2) {
1095+
// Handle edges shared by more than two faces
1096+
System.err.println("toDualGraph(): Bad input — edge shared by more than two faces: " + entry.getKey().toString());
1097+
}
1098+
});
1099+
10891100
return graph;
10901101
}
10911102

@@ -1738,14 +1749,27 @@ public static PShape disableAllStroke(PShape shape) {
17381749
* narrow gaps can appear between otherwise flush shapes. If the shape is a
17391750
* GROUP, the rounding is applied to all child shapes.
17401751
*
1741-
* @param shape the PShape to round vertex coordinates for
1742-
* @return a rounded copy of the input shape
1752+
* @param shape the PShape to round vertex coordinates for.
1753+
* @return a rounded copy of the input shape.
1754+
* @see #roundVertexCoords(PShape, int)
17431755
* @since 1.1.3
17441756
*/
17451757
public static PShape roundVertexCoords(PShape shape) {
17461758
return roundVertexCoords(shape, 0);
17471759
}
17481760

1761+
/**
1762+
* Rounds the x and y coordinates (to <code>n</code> decimal places) of all
1763+
* vertices belonging to the shape. This can sometimes fix a visual problem in
1764+
* Processing where narrow gaps can appear between otherwise flush shapes. If
1765+
* the shape is a GROUP, the rounding is applied to all child shapes.
1766+
*
1767+
* @param shape the PShape to round vertex coordinates for.
1768+
* @param n The number of decimal places to which the coordinates should be
1769+
* rounded.
1770+
* @return a rounded copy of the input shape.
1771+
* @since 2.1
1772+
*/
17491773
public static PShape roundVertexCoords(PShape shape, int n) {
17501774
return PGS_Processing.transform(shape, s -> {
17511775
var c = copy(s);

src/main/java/micycle/pgs/commons/PEdge.java

+22-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* @author Michael Carleton
1212
*
1313
*/
14-
public class PEdge {
14+
public class PEdge implements Comparable<PEdge> {
1515

1616
public final PVector a, b;
1717

@@ -70,8 +70,7 @@ public PEdge slice(double from, double to) {
7070
to = 1;
7171
}
7272
if (from < 0 || to > 1 || from > to) {
73-
throw new IllegalArgumentException(
74-
"Parameters 'from' and 'to' must be between 0 and 1, and 'from' must be less than or equal to 'to'.");
73+
throw new IllegalArgumentException("Parameters 'from' and 'to' must be between 0 and 1, and 'from' must be less than or equal to 'to'.");
7574
}
7675

7776
final PVector pointFrom;
@@ -123,4 +122,24 @@ public String toString() {
123122
private static boolean equals(PVector a, PVector b) {
124123
return a.x == b.x && a.y == b.y;
125124
}
125+
126+
@Override
127+
public int compareTo(PEdge other) {
128+
PVector thisMidpoint = midpoint();
129+
PVector otherMidpoint = other.midpoint();
130+
return comparePVectors(thisMidpoint, otherMidpoint);
131+
}
132+
133+
/**
134+
* Helper method to compare two PVectors lexicographically.
135+
*/
136+
private int comparePVectors(PVector v1, PVector v2) {
137+
if (v1.x != v2.x) {
138+
return Float.compare(v1.x, v2.x);
139+
}
140+
if (v1.y != v2.y) {
141+
return Float.compare(v1.y, v2.y);
142+
}
143+
return Float.compare(v1.z, v2.z);
144+
}
126145
}

0 commit comments

Comments
 (0)