From c4ad946ef21e1cbb6bf4de033ac9a53e4e1b1365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa?= Date: Sun, 8 Sep 2024 15:19:18 +0100 Subject: [PATCH] Use floating point for vector coordinates --- .../geometry/AxisAlignedBoundingBox.scala | 12 ++++ .../eu/joaocosta/minart/geometry/Circle.scala | 55 ++++--------------- .../minart/geometry/ConvexPolygon.scala | 8 +-- .../eu/joaocosta/minart/geometry/Matrix.scala | 11 ++-- .../eu/joaocosta/minart/geometry/Shape.scala | 22 +++----- .../eu/joaocosta/minart/graphics/Plane.scala | 2 +- .../minart/geometry/ConvexPolygonSpec.scala | 10 +--- .../minart/geometry/MatrixSpec.scala | 2 +- 8 files changed, 47 insertions(+), 75 deletions(-) diff --git a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/AxisAlignedBoundingBox.scala b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/AxisAlignedBoundingBox.scala index 5f1d36c6..2ea771cb 100644 --- a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/AxisAlignedBoundingBox.scala +++ b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/AxisAlignedBoundingBox.scala @@ -117,6 +117,18 @@ object AxisAlignedBoundingBox { this } + def add(x: Double, y: Double): this.type = { + val floorX = math.floor(x).toInt + val floorY = math.floor(y).toInt + val ceilX = math.ceil(x).toInt + val ceilY = math.ceil(y).toInt + if (floorX < x1) x1 = floorX + if (floorY < y1) y1 = floorY + if (ceilX > x2) x2 = ceilX + if (ceilY > y2) y2 = ceilY + this + } + def add(point: Shape.Point): this.type = add(point.x, point.y) def result(): AxisAlignedBoundingBox = diff --git a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Circle.scala b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Circle.scala index 7c54b5af..a094a11f 100644 --- a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Circle.scala +++ b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Circle.scala @@ -9,7 +9,7 @@ package eu.joaocosta.minart.geometry * @param center center of the circle. * @param radius circle radius */ -final case class Circle(center: Shape.Point, radius: Int) extends Shape { +final case class Circle(center: Shape.Point, radius: Double) extends Shape { /** The absolute radius */ val absRadius = math.abs(radius) @@ -21,7 +21,7 @@ final case class Circle(center: Shape.Point, radius: Int) extends Shape { val x = center.x - absRadius val y = center.y - absRadius val d = absRadius * 2 - AxisAlignedBoundingBox(x, y, d, d) + AxisAlignedBoundingBox(math.floor(x).toInt, math.floor(y).toInt, math.ceil(d).toInt, math.ceil(d).toInt) } val knownFace: Option[Shape.Face] = @@ -59,7 +59,7 @@ final case class Circle(center: Shape.Point, radius: Int) extends Shape { override def translate(dx: Double, dy: Double): Shape = if (dx == 0 && dy == 0) this - else Circle.PreciseCircle(center.x + dx, center.y + dy, radius) + else Circle(Shape.Point(center.x + dx, center.y + dy), radius) override def flipH: Circle = Circle(Shape.Point(-center.x, center.y), -radius) @@ -68,15 +68,18 @@ final case class Circle(center: Shape.Point, radius: Int) extends Shape { Circle(Shape.Point(center.x, -center.y), -radius) override def scale(s: Double): Shape = - Circle.PreciseCircle(center.x * s, center.y * s, radius * s) + if (s == 1.0) this + else Circle(Shape.Point(center.x * s, center.y * s), radius * s) override def rotate(theta: Double): Shape = { val matrix = Matrix.rotation(theta) if (matrix == Matrix.identity) this else { - Circle.PreciseCircle( - matrix.applyX(center.x.toDouble, center.y.toDouble), - matrix.applyY(center.x.toDouble, center.y.toDouble), + Circle( + Shape.Point( + matrix.applyX(center.x.toDouble, center.y.toDouble), + matrix.applyY(center.x.toDouble, center.y.toDouble) + ), radius ) } @@ -85,41 +88,3 @@ final case class Circle(center: Shape.Point, radius: Int) extends Shape { override def transpose: Circle = Circle(center = Shape.Point(center.y, center.x), -radius) } - -object Circle { - private[Circle] final case class PreciseCircle(centerX: Double, centerY: Double, radius: Double) extends Shape { - lazy val toCircle = Circle( - center = Shape.Point(centerX.toInt, centerY.toInt), - radius = radius.toInt - ) - - def knownFace: Option[Shape.Face] = toCircle.knownFace - def aabb: AxisAlignedBoundingBox = toCircle.aabb - def faceAt(x: Int, y: Int): Option[Shape.Face] = - toCircle.faceAt(x, y) - override def contains(x: Int, y: Int): Boolean = - toCircle.contains(x, y) - override def translate(dx: Double, dy: Double) = - if (dx == 0 && dy == 0) this - else copy(centerX = centerX + dx, centerY = centerY + dy) - override def flipH: Shape = - copy(centerX = -centerX, radius = -radius) - override def flipV: Shape = - copy(centerY = -centerY, radius = -radius) - override def scale(s: Double): Shape = - Circle.PreciseCircle(centerX * s, centerY * s, radius * s) - override def rotate(theta: Double): Shape = { - val matrix = Matrix.rotation(theta) - if (matrix == Matrix.identity) this - else { - Circle.PreciseCircle( - matrix.applyX(centerX, centerY), - matrix.applyY(centerX, centerY), - radius - ) - } - } - override def transpose: Shape = - Circle.PreciseCircle(centerY, centerX, -radius) - } -} diff --git a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/ConvexPolygon.scala b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/ConvexPolygon.scala index a90849d8..d9433109 100644 --- a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/ConvexPolygon.scala +++ b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/ConvexPolygon.scala @@ -24,13 +24,13 @@ final case class ConvexPolygon(vertices: Vector[Shape.Point]) extends Shape { lazy val knownFace: Option[Shape.Face] = faceAt(vertices.head) - private def edgeFunction(x1: Int, y1: Int, x2: Int, y2: Int, x3: Int, y3: Int): Int = + private def edgeFunction(x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double): Double = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1) - private def edgeFunction(p1: Shape.Point, p2: Shape.Point, p3: Shape.Point): Int = + private def edgeFunction(p1: Shape.Point, p2: Shape.Point, p3: Shape.Point): Double = edgeFunction(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y) - private def rawWeights(x: Int, y: Int): Iterator[Int] = { + private def rawWeights(x: Double, y: Double): Iterator[Double] = { (0 until size).iterator.map(idx => val current = vertices(idx) val next = if (idx + 1 >= size) vertices(0) else vertices(idx + 1) @@ -38,7 +38,7 @@ final case class ConvexPolygon(vertices: Vector[Shape.Point]) extends Shape { ) } - private lazy val maxWeight: Int = + private lazy val maxWeight: Double = (vertices.tail) .sliding(2) .collect { case Vector(b, c) => diff --git a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Matrix.scala b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Matrix.scala index bf288b4c..74cf095f 100644 --- a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Matrix.scala +++ b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Matrix.scala @@ -59,16 +59,16 @@ final case class Matrix(a: Double, b: Double, c: Double, d: Double, e: Double, f ) inline def applyX(x: Double, y: Double): Double = a * x + b * y + c * 1 - inline def applyX(x: Int, y: Int): Int = (a * x + b * y + c * 1).toInt + inline def applyX(x: Int, y: Int): Double = (a * x + b * y + c * 1) inline def applyY(x: Double, y: Double): Double = d * x + e * y + f * 1 - inline def applyY(x: Int, y: Int): Int = (d * x + e * y + f * 1).toInt + inline def applyY(x: Int, y: Int): Double = (d * x + e * y + f * 1) /** Applies the transformation to (x, y). */ def apply(x: Double, y: Double): (Double, Double) = (applyX(x, y), applyY(x, y)) /** Applies the transformation to (x, y). */ - def apply(x: Int, y: Int): (Int, Int) = { + def apply(x: Int, y: Int): (Double, Double) = { (applyX(x, y), applyY(x, y)) } } @@ -169,7 +169,10 @@ object Matrix { if (ct == 1.0) Matrix.identity else { val st = Math.sin(theta) - Matrix(ct, -st, 0, st, ct, 0) + // cos and sin have precision issues near 0, so we round the result here to help with multiplications + if (math.abs(ct) < 1e-10) Matrix(0, -st, 0, st, 0, 0) + if (math.abs(st) < 1e-10) Matrix(ct, 0, 0, 0, ct, 0) + else Matrix(ct, -st, 0, st, ct, 0) } /** Shear matrix. diff --git a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Shape.scala b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Shape.scala index 06597734..75bf70e8 100644 --- a/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Shape.scala +++ b/core/shared/src/main/scala/eu/joaocosta/minart/geometry/Shape.scala @@ -40,7 +40,7 @@ trait Shape { * @param y y coordinates of the point * @return None if the point is not contained, Some(face) if the point is contained. */ - final def faceAt(point: Shape.Point): Option[Shape.Face] = faceAt(point.x, point.y) + final def faceAt(point: Shape.Point): Option[Shape.Face] = faceAt(point.x.toInt, point.y.toInt) /** Checks if this shape contains a point. * @@ -60,7 +60,7 @@ trait Shape { * @param y y coordinates of the point * @return None if the point is not contained, Some(face) if the point is contained. */ - final def contains(point: Shape.Point): Boolean = contains(point.x, point.y) + final def contains(point: Shape.Point): Boolean = contains(point.x.toInt, point.y.toInt) /** Contramaps the points in this shape using a matrix. * @@ -133,12 +133,8 @@ trait Shape { object Shape { /** Coordinates of a point in the shape. - * - * For performance reasons, only integer coordinates are supported, - * although shapes are free to use floating point in intermediate states - * and transformations. */ - final case class Point(x: Int, y: Int) + final case class Point(x: Double, y: Double) /** The shape of a circle. * @@ -222,16 +218,16 @@ object Shape { matrix.applyY(shape.aabb.x1, shape.aabb.y2), matrix.applyY(shape.aabb.x2, shape.aabb.y2) ) - val minX = xs.min - val minY = ys.min - val maxX = xs.max - val maxY = ys.max + val minX = math.floor(xs.min).toInt + val minY = math.floor(ys.min).toInt + val maxX = math.ceil(xs.max).toInt + val maxY = math.ceil(ys.max).toInt AxisAlignedBoundingBox(minX, minY, maxX - minX, maxY - minY) } def faceAt(x: Int, y: Int): Option[Shape.Face] = - shape.faceAt(matrix.inverse.applyX(x, y), matrix.inverse.applyY(x, y)) + shape.faceAt(math.round(matrix.inverse.applyX(x, y)).toInt, math.round(matrix.inverse.applyY(x, y)).toInt) override def contains(x: Int, y: Int): Boolean = - shape.contains(matrix.inverse.applyX(x, y), matrix.inverse.applyY(x, y)) + shape.contains(math.round(matrix.inverse.applyX(x, y)).toInt, math.round(matrix.inverse.applyY(x, y)).toInt) override def mapMatrix(matrix: Matrix) = MatrixShape(matrix.multiply(this.matrix), shape) } diff --git a/core/shared/src/main/scala/eu/joaocosta/minart/graphics/Plane.scala b/core/shared/src/main/scala/eu/joaocosta/minart/graphics/Plane.scala index 69985ff7..cfd6e965 100644 --- a/core/shared/src/main/scala/eu/joaocosta/minart/graphics/Plane.scala +++ b/core/shared/src/main/scala/eu/joaocosta/minart/graphics/Plane.scala @@ -222,7 +222,7 @@ trait Plane extends Function2[Int, Int, Color] { outer => object Plane { private[Plane] final case class MatrixPlane(invMatrix: Matrix, plane: Plane) extends Plane { def getPixel(x: Int, y: Int): Color = { - plane.getPixel(invMatrix.applyX(x, y), invMatrix.applyY(x, y)) + plane.getPixel(invMatrix.applyX(x, y).toInt, invMatrix.applyY(x, y).toInt) } override def contramapMatrix(matrix: Matrix) = diff --git a/core/shared/src/test/scala/eu/joaocosta/minart/geometry/ConvexPolygonSpec.scala b/core/shared/src/test/scala/eu/joaocosta/minart/geometry/ConvexPolygonSpec.scala index df7c5a06..86ceef21 100644 --- a/core/shared/src/test/scala/eu/joaocosta/minart/geometry/ConvexPolygonSpec.scala +++ b/core/shared/src/test/scala/eu/joaocosta/minart/geometry/ConvexPolygonSpec.scala @@ -132,14 +132,10 @@ class ConvexPolygonSpec extends munit.FunSuite { .scale(2, 2) .scale(0.5) val expectedPolygon = ConvexPolygon( - /* Should be rounded to - * Vector(Point(3, 0), Point(5, 8), Point(0, 8)) - * But the results are floored. - )*/ Vector( - Point(2, 0), - Point(5, 7), - Point(0, 7) + Point(2.5, 0), + Point(5, 7.5), + Point(0, 7.5) ) ) diff --git a/core/shared/src/test/scala/eu/joaocosta/minart/geometry/MatrixSpec.scala b/core/shared/src/test/scala/eu/joaocosta/minart/geometry/MatrixSpec.scala index a0edc69b..59df6c4e 100644 --- a/core/shared/src/test/scala/eu/joaocosta/minart/geometry/MatrixSpec.scala +++ b/core/shared/src/test/scala/eu/joaocosta/minart/geometry/MatrixSpec.scala @@ -7,7 +7,7 @@ class MatrixSpec extends munit.FunSuite { // [4 5 6] [2] [28] (4*3 + 5*2 + 6*1) // [0 0 1] [1] [ 1] val testMatrix = Matrix(1, 2, 3, 4, 5, 6) - assertEquals(testMatrix.apply(3, 2), (10, 28)) + assertEquals(testMatrix.apply(3, 2), (10.0, 28.0)) } test("Can be multiplied") {