Skip to content

Commit

Permalink
Use floating point for vector coordinates
Browse files Browse the repository at this point in the history
  • Loading branch information
JD557 committed Sep 8, 2024
1 parent 5992517 commit c4ad946
Show file tree
Hide file tree
Showing 8 changed files with 47 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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] =
Expand Down Expand Up @@ -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)
Expand All @@ -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
)
}
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,21 @@ 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)
edgeFunction(current.x, current.y, next.x, next.y, x, y)
)
}

private lazy val maxWeight: Int =
private lazy val maxWeight: Double =
(vertices.tail)
.sliding(2)
.collect { case Vector(b, c) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down

0 comments on commit c4ad946

Please sign in to comment.