Skip to content

Commit

Permalink
Merge pull request #188 from Ramoogur/scanf
Browse files Browse the repository at this point in the history
implement scanfill
  • Loading branch information
creme332 authored Oct 3, 2024
2 parents 530ac69 + 3645157 commit 166f09d
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<Point> scanFill(Polygon polygon) {
int[] xPoints = polygon.xpoints;
int[] yPoints = polygon.ypoints;
int verticesCount = polygon.npoints;

// Initialize the edge table as a HashMap
HashMap<Integer, List<Edge>> 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<Edge> 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<Point> filledPoints = new ArrayList<>();

// Active Edge Table (AET)
List<Edge> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Point> 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<Point> 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<Point> expectedPoints = Arrays.asList(
new Point(0, 0), new Point(4, 3), new Point(2, 5),
new Point(-2, 5), new Point(-4, 3));
List<Point> 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<Point> result = PolygonCalculator.scanFill(polygon);

// Updated expected points to match the correct filling behavior
List<Point> 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<Point> result = PolygonCalculator.scanFill(polygon);

List<Point> 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));
}
}
12 changes: 8 additions & 4 deletions src/test/java/com/github/creme332/tests/utils/TestHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 166f09d

Please sign in to comment.