Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use floating point for vector coordinates #523

Merged
merged 1 commit into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading