diff --git a/CHANGELOG.md b/CHANGELOG.md index 82749975..8e8a0277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed * `urquhartFaces()`, `relativeNeighborFaces()`, `gabrielFaces()` and `spannerFaces()` from `PGS_Meshing` now preserve holes from the input. -* The `from` and `to` arguments for `align()` were the wrong way round. * The output of `PGS_Morphology.smoothGaussian()` is no longer (slightly) affected by the vertex ordering of the input. +* The `transform` and `reference` arguments for `PGS_Transformation.align()` were the wrong way round. ### Removed * `simplifyDCE(shape, targetNumVertices)` and `simplifyDCE(shape, vertexRemovalFraction)` in favour a single method that accepts a user-defined termination callback that is supplied with the current vertex candidate's coordinate, relevance score, and the number of vertices remaining. diff --git a/src/main/java/micycle/pgs/PGS_Transformation.java b/src/main/java/micycle/pgs/PGS_Transformation.java index 9d19bc63..fc350a94 100644 --- a/src/main/java/micycle/pgs/PGS_Transformation.java +++ b/src/main/java/micycle/pgs/PGS_Transformation.java @@ -445,15 +445,15 @@ public static PShape homotheticTransformation(PShape shape, PVector center, doub * optimal transformation. The transformation includes translation, rotation and * scaling to maximize overlap between the two shapes. * - * @param alignShape the polygon shape to be transformed and aligned to the - * other shape. - * @param baseShape the shape that the other shape will be aligned to. + * @param shapeToAlign the polygon shape to be transformed and aligned to the + * other shape. + * @param referenceShape the shape that the other shape will be aligned to. * @return a new PShape that is the transformed and aligned version of * sourceShape. * @since 1.4.0 */ - public static PShape align(PShape alignShape, PShape baseShape) { - return align(alignShape, baseShape, 1); + public static PShape align(PShape shapeToAlign, PShape referenceShape) { + return align(shapeToAlign, referenceShape, 1); } /** @@ -464,20 +464,64 @@ public static PShape align(PShape alignShape, PShape baseShape) { * This method signature aligns the shape according to a provided ratio, * indicating how much alignment transformation to apply. * - * @param alignShape the polygon shape to be transformed and aligned to the - * other shape. - * @param baseShape the shape that the other shape will be aligned to. - * @param alignmentRatio a value in [0,1] indicating how much to transform the - * shape from its original position to its most aligned - * position. 0 means no transformation, 1 means maximum - * alignment. + * @param shapeToAlign the polygon shape to be transformed and aligned to the + * reference shape. + * @param referenceShape the shape that the other shape will be aligned to. + * @param alignmentRatio a value between 0 and 1 indicating the degree of + * alignment transformation to apply. * @return a new PShape that is the transformed and aligned version of * alignShape. * @since 1.4.0 */ - public static PShape align(PShape alignShape, PShape baseShape, double alignmentRatio) { - final Geometry g1 = fromPShape(alignShape); - final Geometry g2 = fromPShape(baseShape); + public static PShape align(PShape shapeToAlign, PShape referenceShape, double alignmentRatio) { + return align(shapeToAlign, referenceShape, alignmentRatio, true, true, true); + } + + /** + * Aligns one polygon shape to another, allowing for control over the + * application of scaling, translation, and rotation transformations + * individually. + * + * @param shapeToAlign the polygon shape to be aligned to the reference + * shape. + * @param referenceShape the reference shape to which the + * shapeToAlign will be aligned. + * @param alignmentRatio a value between 0 and 1 indicating the degree of + * alignment transformation to apply. + * @param applyScale if true, applies scaling alignment + * @param applyTranslation if true, applies transformation alignment + * @param applyRotation if true, applies rotation alignment + * @return a new PShape that is the transformed and aligned version of + * shapeToAlign. + * @since 2.0 + */ + public static PShape align(PShape shapeToAlign, PShape referenceShape, double alignmentRatio, boolean applyScale, + boolean applyTranslation, boolean applyRotation) { + final double[] params = getProcrustesParams(shapeToAlign, referenceShape); + + final Geometry g1 = fromPShape(shapeToAlign); + Coordinate c = g1.getCentroid().getCoordinate(); + + double scale = applyScale ? 1 + (params[2] - 1) * alignmentRatio : 1; + double rotation = applyRotation ? params[3] * alignmentRatio : 0; + double translateX = applyTranslation ? params[0] * alignmentRatio : 0; + double translateY = applyTranslation ? params[1] * alignmentRatio : 0; + + AffineTransformation transform = AffineTransformation.scaleInstance(scale, scale, c.x, c.y).rotate(rotation, c.x, c.y) + .translate(translateX, translateY); + + Geometry aligned = transform.transform(g1); + + return toPShape(aligned); + } + + /** + * @return an array having 4 values: the optimal translation (x, y), scale, and + * rotation angle (radians, clockwise). + */ + private static double[] getProcrustesParams(PShape shapeToAlign, PShape referenceShape) { + final Geometry g1 = fromPShape(shapeToAlign); + final Geometry g2 = fromPShape(referenceShape); if (!g1.getGeometryType().equals(Geometry.TYPENAME_POLYGON) || !g2.getGeometryType().equals(Geometry.TYPENAME_POLYGON)) { throw new IllegalArgumentException("Inputs to align() must be polygons."); } @@ -485,26 +529,18 @@ public static PShape align(PShape alignShape, PShape baseShape, double alignment throw new IllegalArgumentException("Polygon inputs to align() must be holeless."); } - // both shapes need same vertex quantity - final int vertices = Math.min(alignShape.getVertexCount(), baseShape.getVertexCount()); - PShape sourceShapeT = alignShape; - PShape transformShapeT = baseShape; - if (alignShape.getVertexCount() > vertices) { - sourceShapeT = PGS_Morphology.simplifyDCE(alignShape, (v, r, verticesRemaining) -> verticesRemaining <= vertices); + // both shapes need same vertex quantity, simplify rather than densify + final int vertices = Math.min(shapeToAlign.getVertexCount(), referenceShape.getVertexCount()); + PShape referenceShapeT = referenceShape; + PShape shapeToAlignT = shapeToAlign; + if (shapeToAlign.getVertexCount() > vertices) { + shapeToAlignT = PGS_Morphology.simplifyDCE(shapeToAlign, (v, r, verticesRemaining) -> verticesRemaining <= vertices); } - if (baseShape.getVertexCount() > vertices) { - transformShapeT = PGS_Morphology.simplifyDCE(baseShape, (v, r, verticesRemaining) -> verticesRemaining <= vertices); + if (referenceShape.getVertexCount() > vertices) { + referenceShapeT = PGS_Morphology.simplifyDCE(referenceShape, (v, r, verticesRemaining) -> verticesRemaining <= vertices); } - double[] m = ProcrustesAlignment.transform((Polygon) fromPShape(sourceShapeT), (Polygon) fromPShape(transformShapeT)); - - Coordinate c = g2.getCentroid().getCoordinate(); - double scale = 1 + (m[2] - 1) * alignmentRatio; - AffineTransformation transform = AffineTransformation.scaleInstance(scale, scale, c.x, c.y).rotate(m[3] * alignmentRatio, c.x, c.y) - .translate(m[0] * alignmentRatio, m[1] * alignmentRatio); - Geometry aligned = transform.transform(g2); - - return toPShape(aligned); + return ProcrustesAlignment.transform((Polygon) fromPShape(referenceShapeT), (Polygon) fromPShape(shapeToAlignT)); } /** diff --git a/src/main/java/micycle/pgs/commons/ProcrustesAlignment.java b/src/main/java/micycle/pgs/commons/ProcrustesAlignment.java index 8c66d502..09beea98 100644 --- a/src/main/java/micycle/pgs/commons/ProcrustesAlignment.java +++ b/src/main/java/micycle/pgs/commons/ProcrustesAlignment.java @@ -30,29 +30,30 @@ private ProcrustesAlignment() { * Performs ProcrustesAlignment Analysis to align two polygons. *

* Finds the optimal scaling, translation and rotation to best align - * transformPolygon with respect to sourcePolygon. + * transformPolygon with respect to referencePolygon. *

* Note: the polygons should have the same number of vertices. * - * @param sourcePolygon the first polygon + * @param referencePolygon the first polygon * @param transformPolygon the polygon to transform/align - * @return an array containing the optimal translation (x, y), scale, and - * rotation angle (radians, clockwise) to align transform polygon to - * source polygon. + * @return an array having 4 values: the optimal translation (x, y), scale, and + * rotation angle (radians, clockwise) to best align the transform + * polygon to the reference polygon. */ - public static double[] transform(final Polygon sourcePolygon, final Polygon transformPolygon) { - final Coordinate[] coordsA = sourcePolygon.getExteriorRing().getCoordinates(); + public static double[] transform(final Polygon referencePolygon, final Polygon transformPolygon) { + final Coordinate[] coordsA = referencePolygon.getExteriorRing().getCoordinates(); final Coordinate[] coordsB = transformPolygon.getExteriorRing().getCoordinates(); if (coordsA.length != coordsB.length) { - throw new IllegalArgumentException("Polygon exterior rings are different lengths!"); + throw new IllegalArgumentException( + "Polygon exterior rings are different lengths (" + coordsA.length + ", " + coordsB.length + ")!"); } // Find optimal translation - Coordinate t = findTranslation(sourcePolygon, transformPolygon); + Coordinate t = findTranslation(referencePolygon, transformPolygon); // Shift to origin (required for scaling & rotation) - Coordinate ca = sourcePolygon.getCentroid().getCoordinate(); + Coordinate ca = referencePolygon.getCentroid().getCoordinate(); Coordinate cb = transformPolygon.getCentroid().getCoordinate(); translate(coordsA, -ca.x, -ca.y); translate(coordsB, -cb.x, -cb.y);