diff --git a/src/main/java/com/github/creme332/algorithms/LineCalculator.java b/src/main/java/com/github/creme332/algorithms/LineCalculator.java index 863f92d6..51345cb9 100644 --- a/src/main/java/com/github/creme332/algorithms/LineCalculator.java +++ b/src/main/java/com/github/creme332/algorithms/LineCalculator.java @@ -1,33 +1,63 @@ package com.github.creme332.algorithms; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; + public class LineCalculator { private LineCalculator() { - } + /** + * Calculates pixels between any 2 points (x0, y0) and (x1, y1) using the DDA + * line algorithm. High precision calculations are used to reduce floating point + * errors. + * + * @param x0 x-coordinate of start point + * @param y0 y-coordinate of start point + * @param x1 x-coordinate of end point + * @param y1 y-coordinate of end point + * @return A 2D array with 2 elements. The first element is the array of + * x-coordinates and the second element is the array of y-coordinates. + */ public static int[][] dda(int x0, int y0, int x1, int y1) { - int dx = x1 - x0; - int dy = y1 - y0; - int steps = Math.max(Math.abs(dx), Math.abs(dy)); + final int dx = x1 - x0; + final int dy = y1 - y0; + final int steps = Math.max(Math.abs(dx), Math.abs(dy)); - float xInc = (float) dx / steps; - float yInc = (float) dy / steps; + final BigDecimal xInc = BigDecimal.valueOf(dx).divide(BigDecimal.valueOf(steps), MathContext.DECIMAL128); + final BigDecimal yInc = BigDecimal.valueOf(dy).divide(BigDecimal.valueOf(steps), MathContext.DECIMAL128); - float x = x0; - float y = y0; + BigDecimal x = BigDecimal.valueOf(x0); + BigDecimal y = BigDecimal.valueOf(y0); - int[][] pixelCoords = new int[steps + 1][2]; + int[] xpoints = new int[steps + 1]; + int[] ypoints = new int[steps + 1]; for (int i = 0; i <= steps; i++) { - pixelCoords[i][0] = Math.round(x); - pixelCoords[i][1] = Math.round(y); - x += xInc; - y += yInc; + xpoints[i] = x.setScale(0, RoundingMode.HALF_UP).intValue(); + ypoints[i] = y.setScale(0, RoundingMode.HALF_UP).intValue(); + + x = x.add(xInc); + y = y.add(yInc); } - return pixelCoords; + return new int[][] { xpoints, ypoints }; } + /** + * Calculates pixels between any 2 points (x0, y0) and (x1, y1) using the + * Bresenham line algorithm. + * + * @param x0 x-coordinate of start point + * @param y0 y-coordinate of tart point + * @param x1 x-coordinate of end point + * @param y1 y-coordinate of end point + * @return A 2D array with 2 elements. The first element is the array of + * x-coordinates and the second element is the array of y-coordinates. + */ public static int[][] bresenham(int x0, int y0, int x1, int y1) { int dx = Math.abs(x1 - x0); int dy = Math.abs(y1 - y0); @@ -37,13 +67,12 @@ public static int[][] bresenham(int x0, int y0, int x1, int y1) { int err = dx - dy; - int[][] pixelCoords = new int[dx + dy + 1][2]; - int index = 0; + List xpoints = new ArrayList<>(); + List ypoints = new ArrayList<>(); while (true) { - pixelCoords[index][0] = x0; - pixelCoords[index][1] = y0; - index++; + xpoints.add(x0); + ypoints.add(y0); if (x0 == x1 && y0 == y1) break; @@ -59,6 +88,52 @@ public static int[][] bresenham(int x0, int y0, int x1, int y1) { } } - return pixelCoords; + return new int[][] { xpoints.stream().mapToInt(i -> i).toArray(), ypoints.stream().mapToInt(i -> i).toArray() }; + } + + /** + * Another version of the bresenham line algorithm. + * Adapted from: https://github.com/madbence/node-bresenham + * + * @param x0 + * @param y0 + * @param x1 + * @param y1 + * @return + */ + public static int[][] bresenham2(int x0, int y0, int x1, int y1) { + List xpoints = new ArrayList<>(); + List ypoints = new ArrayList<>(); + + int dx = x1 - x0; + int dy = y1 - y0; + int adx = Math.abs(dx); + int ady = Math.abs(dy); + int sx = dx > 0 ? 1 : -1; + int sy = dy > 0 ? 1 : -1; + int eps = 0; + if (adx > ady) { + for (int x = x0, y = y0; sx < 0 ? x >= x1 : x <= x1; x += sx) { + xpoints.add(x); + ypoints.add(y); + + eps += ady; + if (eps << 1 >= adx) { + y += sy; + eps -= adx; + } + } + } else { + for (int x = x0, y = y0; sy < 0 ? y >= y1 : y <= y1; y += sy) { + xpoints.add(x); + ypoints.add(y); + eps += adx; + if (eps << 1 >= ady) { + x += sx; + eps -= ady; + } + } + } + return new int[][] { xpoints.stream().mapToInt(i -> i).toArray(), ypoints.stream().mapToInt(i -> i).toArray() }; } } diff --git a/src/test/java/com/github/creme332/tests/algorithms/LineCalculatorTest.java b/src/test/java/com/github/creme332/tests/algorithms/LineCalculatorTest.java index 257cf063..c0a87c09 100644 --- a/src/test/java/com/github/creme332/tests/algorithms/LineCalculatorTest.java +++ b/src/test/java/com/github/creme332/tests/algorithms/LineCalculatorTest.java @@ -1,76 +1,151 @@ package com.github.creme332.tests.algorithms; -import org.junit.Ignore; +import static org.junit.Assert.assertArrayEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Random; + import org.junit.Test; import com.github.creme332.algorithms.LineCalculator; -import com.github.creme332.tests.utils.TestHelper; public class LineCalculatorTest { + // Helper class to hold test cases + private static class TestCase { + private String description; + private int x0; + private int y0; + private int x1; + private int y1; + private int[][] expected; - @Ignore("Failing test to be fixed later") - public void testDrawDDA() { - int x0 = 2, y0 = 3, x1 = 10, y1 = 8; - int[][] expected = { - { 2, 3 }, { 3, 3 }, { 4, 4 }, { 5, 4 }, { 6, 5 }, { 7, 5 }, { 8, 6 }, { 9, 7 }, { 10, 8 } - }; - - int[][] result = LineCalculator.dda(x0, y0, x1, y1); - TestHelper.assert2DArrayEquals(expected, result); + TestCase(String description, int x0, int y0, int x1, int y1, int[][] expected) { + this.description = description; + this.x0 = x0; + this.y0 = y0; + this.x1 = x1; + this.y1 = y1; + this.expected = expected; + } } - @Ignore("Failing test to be fixed later") - public void testDrawBresenham() { - int x0 = 2, y0 = 3, x1 = 10, y1 = 8; - int[][] expected = { - { 2, 3 }, { 3, 3 }, { 4, 4 }, { 5, 4 }, { 6, 5 }, { 7, 5 }, { 8, 6 }, { 9, 7 }, { 10, 8 } - }; + public static Collection fixedTestCases() { + List testCases = new ArrayList<>(); + + testCases.add(new TestCase("m = 1", 1, 1, 5, 5, new int[][] { + { 1, 2, 3, 4, 5 }, + { 1, 2, 3, 4, 5 } + })); + + testCases.add(new TestCase("m = -1", -5, 5, -1, 1, new int[][] { + { -5, -4, -3, -2, -1 }, + { 5, 4, 3, 2, 1 } + })); + + testCases.add(new TestCase("0 < m < 1", 2, 1, 8, 5, new int[][] { + { 2, 3, 4, 5, 6, 7, 8 }, + { 1, 2, 2, 3, 4, 4, 5 } + })); + + testCases.add(new TestCase("-1 < m < 0", -6, 5, -1, 1, new int[][] { + { -6, -5, -4, -3, -2, -1 }, + { 5, 4, 3, 3, 2, 1 } + })); + + testCases.add(new TestCase("m > 1", 3, 2, 7, 8, new int[][] { + { 3, 4, 4, 5, 6, 6, 7 }, + { 2, 3, 4, 5, 6, 7, 8 } + })); + + testCases.add(new TestCase("m < -1", 2, 8, 5, 3, new int[][] { + { 2, 3, 3, 4, 4, 5 }, + { 8, 7, 6, 5, 4, 3 } + })); + + testCases.add(new TestCase("m < 0 in 1st quadrant", 8, 4, 6, 8, new int[][] { + { 8, 8, 7, 7, 6 }, + { 4, 5, 6, 7, 8 } + })); + + testCases.add(new TestCase("m = 0", 1, 1, 5, 1, new int[][] { + { 1, 2, 3, 4, 5 }, + { 1, 1, 1, 1, 1 } + })); - int[][] result = LineCalculator.bresenham(x0, y0, x1, y1); - TestHelper.assert2DArrayEquals(expected, result); + testCases.add(new TestCase("m = INF", 0, 0, 0, 4, new int[][] { + { 0, 0, 0, 0, 0 }, + { 0, 1, 2, 3, 4 } + })); + + testCases.add(new TestCase("m has recurring decimal places", -10, -9, 7, 9, new int[][] { + { -10, -9, -8, -7, -6, -5, -4, -3, -2, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7 }, + { -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } + })); + return testCases; } @Test - public void testDrawDDAHorizontal() { - int x0 = 1, y0 = 1, x1 = 5, y1 = 1; - int[][] expected = { - { 1, 1 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 1 } - }; - - int[][] result = LineCalculator.dda(x0, y0, x1, y1); - TestHelper.assert2DArrayEquals(expected, result); + public void testDDA() { + for (TestCase test : fixedTestCases()) { + int[][] result = LineCalculator.dda(test.x0, test.y0, test.x1, test.y1); + + try { + assertArrayEquals(test.expected, result); + } catch (AssertionError e) { + System.out.println(test.description); + throw e; + } + + } } @Test - public void testDrawBresenhamHorizontal() { - int x0 = 1, y0 = 1, x1 = 5, y1 = 1; - int[][] expected = { - { 1, 1 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 1 } - }; - - int[][] result = LineCalculator.bresenham(x0, y0, x1, y1); - TestHelper.assert2DArrayEquals(expected, result); + public void testBresenham() { + for (TestCase test : fixedTestCases()) { + int[][] result = LineCalculator.bresenham(test.x0, test.y0, test.x1, test.y1); + + try { + assertArrayEquals(test.expected, result); + } catch (AssertionError e) { + System.out.println(test.description); + throw e; + } + } } - @Test - public void testDrawDDAVertical() { - int x0 = 1, y0 = 1, x1 = 1, y1 = 5; - int[][] expected = { - { 1, 1 }, { 1, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 } - }; - - int[][] result = LineCalculator.dda(x0, y0, x1, y1); - TestHelper.assert2DArrayEquals(expected, result); + public static int[] generateRandomCoordinate() { + final int BOUND = 10; + Random random = new Random(); + int x = random.nextInt(2 * BOUND + 1) - BOUND; // Random integer between -BOUND and +BOUND + int y = random.nextInt(2 * BOUND + 1) - BOUND; // Random integer between -BOUND and +BOUND + return new int[] { x, y }; } - @Test - public void testDrawBresenhamVertical() { - int x0 = 1, y0 = 1, x1 = 1, y1 = 5; - int[][] expected = { - { 1, 1 }, { 1, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 } - }; - - int[][] result = LineCalculator.bresenham(x0, y0, x1, y1); - TestHelper.assert2DArrayEquals(expected, result); + public void testRandom() { + final int NUM_TESTS = 100; + + for (int i = 0; i < NUM_TESTS; i++) { + int[] start = generateRandomCoordinate(); + int[] end = generateRandomCoordinate(); + + int[][] bresenhamResult = LineCalculator.bresenham(start[0], start[1], end[0], end[1]); + int[][] ddaResult = LineCalculator.dda(start[0], start[1], end[0], end[1]); + + try { + assertArrayEquals(bresenhamResult, ddaResult); + } catch (AssertionError e) { + System.out.println( + "Random test failed for coordinates: " + Arrays.toString(start) + " " + + Arrays.toString(end)); + System.out.println("DDA: " + Arrays.deepToString(ddaResult)); + System.out.println("Bresenham: " + Arrays.deepToString(bresenhamResult)); + System.out.println(); + + throw e; // Re-throw the assertion error to ensure the test fails + } + } } }