Skip to content

Commit

Permalink
Fix long-standing errors in layout calculation
Browse files Browse the repository at this point in the history
Images are no longer incorrectly cropped / not centered in the canvas
when drawn.
  • Loading branch information
Noel Welsh authored and Noel Welsh committed Apr 15, 2016
1 parent 9a39af5 commit d863574
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 37 deletions.
59 changes: 38 additions & 21 deletions shared/src/main/scala/doodle/backend/BoundingBox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,32 @@ import doodle.core.{ContextTransform,BezierCurveTo,LineTo,MoveTo,Point,Vec}
* Together this information is used to layout an Image.
*
* Note the origin of the local coordinate system can be any point that is not
* outside the box. It is typically at the center of the box, but it need not
* be there.
* outside the box. There is a convention for placement of the origin that is
* relied upon for layout:
*
* - for a Path, the origin may be anywhere; otherwise
*
* - layout operations align origins of the bounding boxex they enclose along
* one or more dimensions. The origin of the bounding box of such an
* operation is similarly aligned along the relevant dimension and centered
* on any dimension that is not aligned.
*
* To example the above more clearly, and Beside aligns the y coordinates of
* the elements it encloses, and its own origin is aligned along the y-axis
* with the enclosing elements and centered on the x-axis.
*
* Coordinates follow the usual Cartesian system (+ve Y is up, and +ve X is
* right) not the common computer graphic coordinate system (+ve Y is down).
*/
final case class BoundingBox(left: Double, top: Double, right: Double, bottom: Double) {
val center = Point.cartesian((left + right) / 2, (top + bottom) / 2)

val height: Double =
top - bottom

val width: Double =
right - left

val center = Point.cartesian(left + (width / 2), bottom + (height / 2))

def pad(padding: Double): BoundingBox =
BoundingBox(
left - padding,
Expand All @@ -45,32 +56,42 @@ final case class BoundingBox(left: Double, top: Double, right: Double, bottom: D
bottom min toInclude.y
)

def beside(that: BoundingBox): BoundingBox =
def beside(that: BoundingBox): BoundingBox = {
val width = this.width + that.width
val top = this.top max that.top
val bottom = this.bottom min that.bottom

BoundingBox(
-(this.width + that.width) / 2,
this.top max that.top,
(this.width + that.width) / 2,
this.bottom min that.bottom
-(width / 2),
top,
width / 2,
bottom
)
}

def on(that: BoundingBox): BoundingBox =
def on(that: BoundingBox): BoundingBox =
BoundingBox(
this.left min that.left,
this.top max that.top,
this.right max that.right,
this.bottom min that.bottom
)

def above(that: BoundingBox): BoundingBox =
def above(that: BoundingBox): BoundingBox = {
val height = this.height + that.height
val right = this.right max that.right
val left = this.left min that.left

BoundingBox(
this.left min that.left,
(this.height + that.height) / 2,
this.right max that.right,
-(this.height + that.height) / 2
left,
height / 2,
right,
-(height / 2)
)
}

def at(offset: Vec): BoundingBox = {
val topLeft = Point.cartesian(left, top)
val topLeft = Point.cartesian(left, top)
val topRight = Point.cartesian(right, top)
val bottomLeft = Point.cartesian(left, bottom)
val bottomRight = Point.cartesian(right, bottom)
Expand All @@ -88,9 +109,5 @@ object BoundingBox {
BoundingBox(point.x, point.y, point.x, point.y)

def apply(points: Seq[Point]): BoundingBox =
points match {
case Seq() => BoundingBox.empty
case Seq(hd) => BoundingBox(hd)
case hd +: tl => tl.foldLeft(BoundingBox(hd))(_ expand _)
}
points.foldLeft(BoundingBox.empty){ (bb, elt) => bb.expand(elt) }
}
2 changes: 1 addition & 1 deletion shared/src/main/scala/doodle/backend/Image.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ sealed abstract class Image extends Product with Serializable {
lazy val boundingBox: BoundingBox = {
def pathElementsToBoundingBox(elts: Seq[PathElement]): BoundingBox =
BoundingBox(
elts.flatMap {
Point.zero +: elts.flatMap {
case MoveTo(pos) => Seq(pos)
case LineTo(pos) => Seq(pos)
case BezierCurveTo(cp1, cp2, pos) =>
Expand Down
52 changes: 42 additions & 10 deletions shared/src/main/scala/doodle/backend/StandardInterpreter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,55 @@ trait StandardInterpreter extends Interpreter {
val lBox = l.boundingBox
val rBox = r.boundingBox

val lOriginX = origin.x + box.left + (lBox.width / 2)
val rOriginX = origin.x + box.right - (rBox.width / 2)
// Beside always vertically centers l and r, so we don't need
// to calculate center ys for l and r.
// Beside aligns the y coordinate of the origin of the bounding boxes of
// l and r. We need to calculate the x coordinate of the origin of each
// bounding box, remembering that the origin may not be the center of
// the box. We first calculate the the x coordinate of the center of the
// l and r bounding boxes and then displace the centers to their
// respective origins

// The center of the l and r bounding boxes in the current coordinate system
val lCenterX = origin.x + box.left + (lBox.width / 2)
val rCenterX = origin.x + box.right - (rBox.width / 2)

// lBox and rBox may not have their origin at the center of their bounding
// box, so we transform accordingly if need be.
val lOrigin =
Point.cartesian(
lCenterX - lBox.center.x,
origin.y
)
val rOrigin =
Point.cartesian(
rCenterX - rBox.center.x,
origin.y
)

draw(l, canvas, lOrigin)
draw(r, canvas, rOrigin)

draw(l, canvas, Point.cartesian(lOriginX, origin.y))
draw(r, canvas, Point.cartesian(rOriginX, origin.y))
case a @ Above(t, b) =>
val box = a.boundingBox
val tBox = t.boundingBox
val bBox = b.boundingBox

val tOriginY = origin.y + box.top - (tBox.height / 2)
val bOriginY = origin.y + box.bottom + (bBox.height / 2)
val tCenterY = origin.y + box.top - (tBox.height / 2)
val bCenterY = origin.y + box.bottom + (bBox.height / 2)

val tOrigin =
Point.cartesian(
origin.x,
tCenterY - tBox.center.y
)
val bOrigin =
Point.cartesian(
origin.x,
bCenterY - bBox.center.y
)

draw(t, canvas, tOrigin)
draw(b, canvas, bOrigin)

draw(t, canvas, Point.cartesian(origin.x, tOriginY))
draw(b, canvas, Point.cartesian(origin.x, bOriginY))
case At(vec, i) =>
draw(i, canvas, origin + vec)

Expand Down
6 changes: 1 addition & 5 deletions shared/src/main/scala/doodle/core/Image.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,12 @@ sealed abstract class Path extends Image {
case ClosedPath(elts) => OpenPath(elts)
}

def closed: Path =
def close: Path =
this match {
case OpenPath(elts) => ClosedPath(elts)
case ClosedPath(_) => this
}
}
object Path {
def empty: Path =
OpenPath(List.empty[PathElement])
}
final case class OpenPath(elements: Seq[PathElement]) extends Path
final case class ClosedPath(elements: Seq[PathElement]) extends Path
final case class Circle(r: Double) extends Image
Expand Down

0 comments on commit d863574

Please sign in to comment.