diff --git a/src/main/java/com/github/creme332/model/calculator/PolygonCalculator.java b/src/main/java/com/github/creme332/model/calculator/PolygonCalculator.java index c703888..08f30d6 100644 --- a/src/main/java/com/github/creme332/model/calculator/PolygonCalculator.java +++ b/src/main/java/com/github/creme332/model/calculator/PolygonCalculator.java @@ -6,8 +6,11 @@ import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.awt.Point; public class PolygonCalculator { @@ -187,4 +190,116 @@ public static Polygon transformPolygon(Polygon polygon, AffineTransform transfor return new Polygon(xPoints, yPoints, xPoints.length); } + + static class Edge { + int yMax; // Maximum y value for the edge + double xCurrent; // Current x value along the edge + double inverseSlope; // Slope of the edge (1/m) for calculating x + + public Edge(int yMax, double xCurrent, double inverseSlope) { + this.yMax = yMax; + this.xCurrent = xCurrent; + this.inverseSlope = inverseSlope; + } + + @Override + public String toString() { + return String.format("(%d, %.3f, %.3f)", yMax, xCurrent, inverseSlope); + } + } + + public static List scanFill(Polygon polygon) { + int[] xPoints = polygon.xpoints; + int[] yPoints = polygon.ypoints; + int verticesCount = polygon.npoints; + + // Initialize the edge table as a HashMap + HashMap> edgeTable = new HashMap<>(); + int maxY = Integer.MIN_VALUE; + int minY = Integer.MAX_VALUE; + + // Create an edge table for each scanline + for (int i = 0; i < verticesCount; i++) { + Point p1 = new Point(xPoints[i], yPoints[i]); + Point p2 = new Point(xPoints[(i + 1) % verticesCount], yPoints[(i + 1) % verticesCount]); + + // Ensure p1.y < p2.y for correct edge handling + if (p1.y > p2.y) { + Point temp = p1; + p1 = p2; + p2 = temp; + } + + if (p1.y != p2.y) { // Ignore horizontal edges + int yMin = p1.y; + int yMax = p2.y; + float xCurrent = p1.x; + float inverseSlope = (float) (p2.x - p1.x) / (p2.y - p1.y); + + maxY = Math.max(maxY, yMax); + minY = Math.min(minY, yMin); + + // Add the edge to the corresponding edge list in the edge table + edgeTable.putIfAbsent(yMin, new ArrayList<>()); + edgeTable.get(yMin).add(new Edge(yMax, xCurrent, inverseSlope)); + } + } + + // After the edge table is built, sort the edges for each scanline + for (List edges : edgeTable.values()) { + edges.sort(Comparator + .comparingInt((Edge edge) -> edge.yMax) // Primary: yMax + .thenComparingDouble(edge -> edge.xCurrent) // Secondary: xCurrent + .thenComparingDouble(edge -> edge.inverseSlope) // Tertiary: inverseSlope + ); + } + + // System.out.println(edgeTable); + + // List to store filled points (can be thought of as the output) + List filledPoints = new ArrayList<>(); + + // Active Edge Table (AET) + List activeEdgeTable = new ArrayList<>(); + + // Process each scanline + for (int scanline = minY; scanline <= maxY; scanline += 1) { + // System.out.println("\ny = " + scanline); + + // 1. Move edges from edgeTable to AET where the current scanline starts + if (edgeTable.containsKey(scanline)) { + activeEdgeTable.addAll(edgeTable.get(scanline)); + } + + // 2. Remove edges from AET where scanline >= yMax + final int scanlineNumberCopy = scanline; + activeEdgeTable.removeIf(edge -> scanlineNumberCopy >= edge.yMax); + + // 3. Sort AET by xCurrent + activeEdgeTable.sort(Comparator.comparingDouble(edge -> edge.xCurrent)); + + // System.out.println(activeEdgeTable); + + // 4. Fill the pixels between pairs of x-coordinates + for (int i = 0; i < activeEdgeTable.size() - 1; i += 2) { + Edge e1 = activeEdgeTable.get(i); + Edge e2 = activeEdgeTable.get(i + 1); + + // System.out.println( + // String.format("Plot [%d, %d]", (int) Math.ceil(e1.xCurrent), (int) Math.floor(e2.xCurrent))); + + // Add points between the two x coordinates + for (int x = (int) Math.ceil(e1.xCurrent); x <= (int) Math.floor(e2.xCurrent); x++) { + filledPoints.add(new Point(x, scanline)); + } + } + + // 5. Update xCurrent for all edges in the AET + for (Edge edge : activeEdgeTable) { + edge.xCurrent += edge.inverseSlope; + } + } + + return filledPoints; + } } diff --git a/src/test/java/com/github/creme332/tests/model/calculator/PolygonCalculatorTest.java b/src/test/java/com/github/creme332/tests/model/calculator/PolygonCalculatorTest.java new file mode 100644 index 0000000..19f6535 --- /dev/null +++ b/src/test/java/com/github/creme332/tests/model/calculator/PolygonCalculatorTest.java @@ -0,0 +1,169 @@ +package com.github.creme332.tests.model.calculator; + +import com.github.creme332.model.calculator.PolygonCalculator; + +import org.junit.Ignore; +import org.junit.Test; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +public class PolygonCalculatorTest { + + private final PolygonCalculator calculator = new PolygonCalculator(); + + @Test + public void testGetOrderedPoints() { + int sidesCount = 4; + int length = 10; + int centerX = 5; + int centerY = 5; + int[][] expected = { + { 12, -2, -2, 12 }, + { 12, 12, -2, -2 } + }; + int[][] orderedPoints = calculator.getOrderedPoints(sidesCount, length, centerX, centerY); + + assertArrayEquals(expected[0], orderedPoints[0]); + assertArrayEquals(expected[1], orderedPoints[1]); + } + + @Test + public void testRotateVector() { + Point2D vector = new Point2D.Double(1, 0); + double radAngle = Math.toRadians(90); + Point2D expected = new Point2D.Double(0, 1); + Point2D result = PolygonCalculator.rotateVector(vector, radAngle); + + assertEquals(expected.getX(), result.getX(), 0.001); + assertEquals(expected.getY(), result.getY(), 0.001); + } + + @Test + public void testRotatePointAboutPivot() { + Point2D point = new Point2D.Double(1, 0); + Point2D pivot = new Point2D.Double(0, 0); + double radAngle = Math.toRadians(90); + Point2D expected = new Point2D.Double(0, 1); + Point2D result = PolygonCalculator.rotatePointAboutPivot(point, pivot, radAngle); + + assertEquals(expected.getX(), result.getX(), 0.001); + assertEquals(expected.getY(), result.getY(), 0.001); + } + + @Test + public void testTransformPolygon() { + Polygon polygon = new Polygon(new int[] { 0, 1, 1, 0 }, new int[] { 0, 0, 1, 1 }, 4); + AffineTransform transform = AffineTransform.getTranslateInstance(1, 1); + Polygon expected = new Polygon(new int[] { 1, 2, 2, 1 }, new int[] { 1, 1, 2, 2 }, 4); + Polygon result = PolygonCalculator.transformPolygon(polygon, transform); + + assertArrayEquals(expected.xpoints, result.xpoints); + assertArrayEquals(expected.ypoints, result.ypoints); + } + + @Test + public void testScanFillForSquare() { + Polygon polygon = new Polygon(new int[] { 0, 4, 4, 0 }, new int[] { 0, 0, 4, 4 }, 4); + List expected = Arrays.asList( + new Point(0, 0), new Point(1, 0), new Point(2, 0), new Point(3, 0), new Point(4, 0), + new Point(0, 1), new Point(1, 1), new Point(2, 1), new Point(3, 1), new Point(4, 1), + new Point(0, 2), new Point(1, 2), new Point(2, 2), new Point(3, 2), new Point(4, 2), + new Point(0, 3), new Point(1, 3), new Point(2, 3), new Point(3, 3), new Point(4, 3)); + // Note: Edge y = 4 is purposefully excluded. Scanfill algorithm ignores + // horizontal edges. + List result = PolygonCalculator.scanFill(polygon); + + assertEquals(expected.size(), result.size()); + assertEquals(expected, result); + } + + @Test + public void testGetOrderedPointsForTriangle() { + int sidesCount = 3; + int length = 10; + int centerX = 0; + int centerY = 0; + int[][] orderedPoints = calculator.getOrderedPoints(sidesCount, length, centerX, centerY); + + int[][] expected = { + { 9, -9, 0 }, + { 5, 5, -10 } + }; + + assertArrayEquals(expected[0], orderedPoints[0]); + assertArrayEquals(expected[1], orderedPoints[1]); + } + + @Test + public void testGetOrderedPointsForPentagon() { + int sidesCount = 5; + int length = 10; + int centerX = 0; + int centerY = 0; + int[][] orderedPoints = calculator.getOrderedPoints(sidesCount, length, centerX, centerY); + + int[][] expected = { + { 6, -6, -10, 0, 10 }, + { 8, 8, -3, -10, -3 } + }; + + assertArrayEquals(expected[0], orderedPoints[0]); + assertArrayEquals(expected[1], orderedPoints[1]); + } + + @Test + public void testGetOrderedPointsForIrregularPolygon() { + Polygon polygon = new Polygon(new int[] { 0, 4, 2, -2, -4 }, new int[] { 0, 3, 5, 5, 3 }, 5); + List expectedPoints = Arrays.asList( + new Point(0, 0), new Point(4, 3), new Point(2, 5), + new Point(-2, 5), new Point(-4, 3)); + List actualPoints = new ArrayList<>(); + for (int i = 0; i < polygon.npoints; i++) { + actualPoints.add(new Point(polygon.xpoints[i], polygon.ypoints[i])); + } + + assertEquals(expectedPoints, actualPoints); + } + + @Test + public void testScanFillIrregularPolygon() { + Polygon polygon = new Polygon(new int[] { 0, 3, -3 }, new int[] { 0, 3, 3 }, 3); + List result = PolygonCalculator.scanFill(polygon); + + // Updated expected points to match the correct filling behavior + List expected = Arrays.asList( + new Point(0, 0), + new Point(-1, 1), new Point(0, 1), new Point(1, 1), + new Point(-2, 2), new Point(-1, 2), new Point(0, 2), new Point(1, 2), new Point(2, 2)); + + assertEquals("Filled pixel list size should match", expected.size(), result.size()); + assertTrue("Filled pixels should match expected", result.containsAll(expected) && expected.containsAll(result)); + } + + @Test + public void testScanFillForComplexShape() { + // Exercise 1 From Lecture 8 + Polygon polygon = new Polygon(new int[] { 2, 3, 6, 3, 0 }, new int[] { 1, 5, 6, 8, 4 }, 5); + List result = PolygonCalculator.scanFill(polygon); + + List expected = Arrays.asList( + new Point(2, 1), + new Point(2, 2), + new Point(1, 3), new Point(2, 3), + new Point(0, 4), new Point(1, 4), new Point(2, 4), + new Point(1, 5), new Point(2, 5), new Point(3, 5), + new Point(2, 6), new Point(3, 6), new Point(4, 6), new Point(5, 6), new Point(6, 6), + new Point(3, 7), new Point(4, 7) + ); + + assertEquals("Filled pixel list size should match", expected.size(), result.size()); + assertTrue("Filled pixels should match expected", result.containsAll(expected) && expected.containsAll(result)); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/creme332/tests/utils/TestHelper.java b/src/test/java/com/github/creme332/tests/utils/TestHelper.java index 9a04d2f..86d1208 100644 --- a/src/test/java/com/github/creme332/tests/utils/TestHelper.java +++ b/src/test/java/com/github/creme332/tests/utils/TestHelper.java @@ -17,9 +17,11 @@ private TestHelper() { * * This method does NOT perform JUnit assertions. * - * @param expectedPixels The expected array of pixel coordinates. + * @param expectedPixels The expected array of pixel coordinates. 2D array of + * pixels where each element is in the form {x, y} * @param actualPixels The actual array of pixel coordinates to be compared - * with the expected array. + * with the expected array.2D array of pixels where each + * element is in the form {x, y} */ public static void compare2DArraysDebug(int[][] expectedPixels, int[][] actualPixels) { // Set to store the expected pixel coordinates @@ -82,8 +84,10 @@ public static void compare2DArraysDebug(int[][] expectedPixels, int[][] actualPi /** * Asserts that 2 arrays of pixels are identical. Order of pixels is ignored. * - * @param expectedPixels - * @param actualPixels + * @param expectedPixels 2D array of pixels where each element is in the form + * {x, y} + * @param actualPixels 2D array of pixels where each element is in the form + * {x, y} */ public static void assert2DArrayEquals(int[][] expectedPixels, int[][] actualPixels) { // Set to store the expected pixel coordinates