Skip to content

Commit

Permalink
Ortho Enhancements (#656)
Browse files Browse the repository at this point in the history
* projection: Implement depth ordering and face culling

* grouping: Fix missing scope export

* tests: Update refs

* polygon: Move polygon rel. fn's to new file

* shapes: Make shapes CCW by default

* tests: Update refs

* Changelog

* PR Fixes
  • Loading branch information
johannes-wolf authored Aug 12, 2024
1 parent 035a667 commit 7141c4e
Show file tree
Hide file tree
Showing 22 changed files with 232 additions and 48 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ package called `cetz-plot`.
negative cordinates.
- Element names are now checked to not contain a "." character.
- Fixed intersection bug for content with `anchor:` set.
- **BREAKING** The winding order of _all_ elements has been changed to CCW.

## Draw
- Added `floating` function for drawing elements without affecting bounding boxes.
- The `ortho` function gained a `sorted` and `cull-face` argument to enable
depth ordering and face culling of drawables. Ordering is enabled by default.

## Marks
- Added support for mark `anchor` style key, to adjust mark placement and
Expand Down
2 changes: 1 addition & 1 deletion src/draw.typ
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#import "draw/grouping.typ": intersections, group, anchor, copy-anchors, place-anchors, set-ctx, get-ctx, for-each-anchor, on-layer, place-marks, hide, floating
#import "draw/grouping.typ": intersections, group, scope, anchor, copy-anchors, place-anchors, set-ctx, get-ctx, for-each-anchor, on-layer, place-marks, hide, floating
#import "draw/transformations.typ": set-transform, rotate, translate, scale, set-origin, move-to, set-viewport
#import "draw/styling.typ": set-style, fill, stroke
#import "draw/shapes.typ": circle, circle-through, arc, arc-through, mark, line, grid, content, rect, bezier, bezier-through, catmull, hobby, merge-path
Expand Down
84 changes: 66 additions & 18 deletions src/draw/projection.typ
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
#import "/src/matrix.typ"
#import "/src/drawable.typ"
#import "/src/util.typ"

#let ortho-projection-matrix = ((1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 0, 0),
(0, 0, 0, 1))
#import "/src/polygon.typ"

// Get an orthographic view matrix for 3 angles
#let ortho-matrix(x, y, z) = matrix.mul-mat(
Expand All @@ -18,23 +14,71 @@
matrix.transform-rotate-z(z),
)

// Pushes a view- and projection-matrix to transform all `body` elements. The current context transform is not modified.
#let ortho-projection-matrix = (
(1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 0, 0),
(0, 0, 0, 1),
)

#let _sort-by-distance(drawables) = {
return drawables.sorted(key: d => {
let z = none
for ((kind, ..pts)) in d.segments {
pts = pts.map(p => p.at(2))
z = if z == none {
calc.max(..pts)
} else {
calc.max(z, ..pts)
}
}
return z
})
}

// Filter out all clock-wise polygons, or if `invert` is true,
// all counter clock-wise ones.
#let _filter-cw-faces(drawables, mode: "cw") = {
return drawables.filter(d => {
let poly = polygon.from-segments(d.segments)
poly.first() != poly.last() or polygon.winding-order(poly) == mode
})
}

// Sets up a view matrix to transform all `body` elements. The current context
// transform is not modified.
//
// - body (element): Elements
// - view-matrix (matrix): View matrix
// - projection-matrix (matrix): Projection matrix
// - reset-transform (bool): If true, override (and thus ignore)
// the current transformation with the new matrices instead
// of multiplying them.
#let _projection(body, view-matrix, projection-matrix, reset-transform: false) = {
// - reset-transform (bool): Ignore the current transformation matrix
// - sorted (bool): Sort drawables by maximum distance (front to back)
// - cull-face (none,str): Enable back-face culling if set to `"cw"` for clockwise
// or `"ccw"` for counter-clockwise. Polygons of the specified order will not get drawn.
#let _projection(body, view-matrix, projection-matrix, reset-transform: true, sorted: true, cull-face: "cw") = {
(ctx => {
let transform = ctx.transform
ctx.transform = matrix.mul-mat(projection-matrix, view-matrix)
if not reset-transform {
ctx.transform = matrix.mul-mat(transform, ctx.transform)
}
ctx.transform = view-matrix

let (ctx, drawables, bounds) = process.many(ctx, util.resolve-body(ctx, body))

if cull-face != none {
assert(cull-face in ("cw", "ccw"),
message: "cull-face must be none, cw or ccw.")
drawables = _filter-cw-faces(drawables, mode: cull-face)
}
if sorted {
drawables = _sort-by-distance(drawables)
}

if projection-matrix != none {
drawables = drawable.apply-transform(projection-matrix, drawables)
}

ctx.transform = transform
if not reset-transform {
drawables = drawable.apply-transform(ctx.transform, drawables)
}

return (
ctx: ctx,
Expand Down Expand Up @@ -76,12 +120,16 @@
/// - x (angle): X-axis rotation angle
/// - y (angle): Y-axis rotation angle
/// - z (angle): Z-axis rotation angle
/// - sorted (bool): Sort drawables by maximum distance (front to back)
/// - cull-face (none,str): Enable back-face culling if set to `"cw"` for clockwise
/// or `"ccw"` for counter-clockwise. Polygons of the specified order will not get drawn.
/// - reset-transform (bool): Ignore the current transformation matrix
/// - body (element): Elements to draw
/// - name (none,str):
#let ortho(x: 35.264deg, y: 45deg, z: 0deg, reset-transform: false, body, name: none) = group(name: name, ctx => {
_projection(body, ortho-matrix(x, y, z),
ortho-projection-matrix, reset-transform: reset-transform)
#let ortho(x: 35.264deg, y: 45deg, z: 0deg, sorted: true, cull-face: none, reset-transform: false, body, name: none) = group(name: name, ctx => {
_projection(body, ortho-matrix(x, y, z), ortho-projection-matrix,
sorted: sorted,
cull-face: cull-face,
reset-transform: reset-transform)
})

/// Draw elements on the xy-plane with optional z offset.
Expand Down
24 changes: 12 additions & 12 deletions src/draw/shapes.typ
Original file line number Diff line number Diff line change
Expand Up @@ -1038,10 +1038,10 @@
let (rx, ry) = radius
if rx > 0 or ry > 0 {
let m = 0.551784
let p0 = (p0.at(0) * m * radius.at(0),
p0.at(1) * m * radius.at(1))
let p1 = (p1.at(0) * m * radius.at(0),
p1.at(1) * m * radius.at(1))
let p0 = (p0.at(0) * m * rx,
p0.at(1) * m * ry)
let p1 = (p1.at(0) * m * rx,
p1.at(1) * m * ry)
(path-util.cubic-segment(s, e,
vector.add(s, p0),
vector.add(e, p1)),)
Expand All @@ -1067,14 +1067,14 @@
let (p6, p7) = get-corner-pts(sw, (x1, y1, z), ( 1, 0), ( 0, 1))

let segments = ()
segments += corner-arc(nw, p0, p1, (0, 1), (-1, 0))
if p1 != p2 { segments += (path-util.line-segment((p1, p2)),) }
segments += corner-arc(ne, p2, p3, (1, 0), (0, 1))
if p3 != p4 { segments += (path-util.line-segment((p3, p4)),) }
segments += corner-arc(se, p4, p5, (0, -1), (1, 0))
if p5 != p6 { segments += (path-util.line-segment((p5, p6)),) }
segments += corner-arc(sw, p6, p7, (-1, 0), (0,-1))
if p7 != p0 { segments += (path-util.line-segment((p7, p0)),) }
segments += corner-arc(nw, p1, p0, (-1,0), (0, 1))
if p0 != p7 { segments += (path-util.line-segment((p0, p7)),) }
segments += corner-arc(sw, p7, p6, (0,-1), (-1,0))
if p6 != p5 { segments += (path-util.line-segment((p6, p5)),) }
segments += corner-arc(se, p5, p4, (1, 0), (0,-1))
if p4 != p3 { segments += (path-util.line-segment((p4, p3)),) }
segments += corner-arc(ne, p3, p2, (0, 1), (1, 0))
if p2 != p1 { segments += (path-util.line-segment((p2, p1)),) }

drawable.path(segments, fill: style.fill, stroke: style.stroke, close: true)
}
Expand Down
25 changes: 15 additions & 10 deletions src/drawable.typ
Original file line number Diff line number Diff line change
Expand Up @@ -107,23 +107,28 @@
(
path-util.cubic-segment(
(x, top, z),
(right, y, z),
(x + m * rx, top, z),
(right, y + m * ry, z),
(left, y, z),
(x - m * rx, top, z),
(left, y + m * ry, z),
),
path-util.cubic-segment(
(right, y, z),
(left, y, z),
(x, bottom, z),
(right, y - m * ry, z),
(x + m * rx, bottom, z),
(left, y - m * ry, z),
(x - m * rx, bottom, z),
),
path-util.cubic-segment(
(x, bottom, z),
(left, y, z),
(x - m * rx, bottom, z),
(left, y - m * ry, z),
(right, y, z),
(x + m * rx, bottom, z),
(right, y - m * ry, z),
),
path-util.cubic-segment(
(right, y, z),
(x, top, z),
(right, y + m * ry, z),
(x + m * rx, top, z)
),
path-util.cubic-segment((left, y, z), (x, top, z), (left, y + m * ry, z), (x - m * rx, top, z)),
),
stroke: stroke,
fill: fill,
Expand Down
61 changes: 61 additions & 0 deletions src/polygon.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/// Returns a list of polygon points from
/// a list of segments.
///
/// Cubic segments get linearized by sampling.
///
/// - segment (array): List of segments
/// - samples (int): Number of samples
/// -> array
#let from-segments(segments, samples: 10) = {
import "/src/bezier.typ": cubic-point
let poly = ()
for ((kind, ..pts)) in segments {
if kind == "cubic" {
poly += range(0, samples).map(t => {
cubic-point(..pts, t / (samples - 1))
})
} else {
poly += pts
}
}
return poly
}

/// Computes the signed area of a 2D polygon.
///
/// The formula used is the following:
/// $ 1/2 \sum_{i}=0^{n-1} x_i*y_i+1 - x_i+1*y_i $
///
/// - points (array): List of Vectors of dimension >= 2
/// -> float
#let signed-area(points) = {
let a = 0
let n = points.len()
let (cx, cy) = (0, 0)
for i in range(0, n) {
let (x0, y0, ..) = points.at(i)
let (x1, y1, ..) = points.at(calc.rem(i + 1, n))
cx += (x0 + x1) * (x0 * y1 - x1 * y0)
cy += (y0 + y1) * (x0 * y1 - x1 * y0)
a += x0 * y1 - x1 * y0
}
return .5 * a
}

/// Returns the winding order of a 2D polygon
/// by using it's signed area.
///
/// Returns either "ccw" (counter clock-wise) or "cw" (clock-wise) or none.
///
/// - point (array): List of polygon points
/// -> str,none
#let winding-order(points) = {
let area = signed-area(points)
if area > 0 {
"cw"
} else if area < 0 {
"ccw"
} else {
none
}
}
Binary file modified tests/anchor/element-anchors/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/anchor/path-anchors/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/arc/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/bounds/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/content/transform/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/decorations/path/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/floating/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/group/transform/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/line/element-element/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/mark/auto-offset/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/palette/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/projection/ortho/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 74 additions & 7 deletions tests/projection/ortho/test.typ
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@

set-style(mark: (end: ">"))

line((-l,0), (l,0), stroke: red, name: "x")
content((rel: ((name: "x", anchor: 50%), .5, "x.end"), to: "x.end"), text(red, $x$))
on-layer(-1, {
line((-l,0), (l,0), stroke: red, name: "x")
content((rel: ((name: "x", anchor: 50%), .5, "x.end"), to: "x.end"), text(red, $x$))

line((0,-l), (0,l), stroke: blue, name: "y")
content((rel: ((name: "y", anchor: 50%), .5, "y.end"), to: "y.end"), text(blue, $y$))
line((0,-l), (0,l), stroke: blue, name: "y")
content((rel: ((name: "y", anchor: 50%), .5, "y.end"), to: "y.end"), text(blue, $y$))

line((0,0,-l), (0,0,l), stroke: green, name: "z", mark: (z-up: (1,0,0)))
content((rel: ((name: "z", anchor: 50%), .5, "z.end"), to: "z.end"), text(green, $z$))
line((0,0,-l), (0,0,l), stroke: green, name: "z", mark: (z-up: (1,0,0)))
content((rel: ((name: "z", anchor: 50%), .5, "z.end"), to: "z.end"), text(green, $z$))
})
}

#let checkerboard() = {
Expand All @@ -27,6 +29,13 @@
}
}

#test-case({
import draw: *
ortho(reset-transform: false, {
line((-1, 0), (1, 0), mark: (end: ">"))
})
})

#test-case({
import draw: *
ortho({
Expand Down Expand Up @@ -67,7 +76,7 @@

#test-case({
import draw: *
ortho({
ortho(sorted: true, {
axes(4)
on-yz(x: -1, {
checkerboard()
Expand All @@ -80,3 +89,61 @@
})
})
})

// Ordering
#test-case({
import draw: *
ortho(sorted: true, {
scope({ translate((0, 0, +1)); rect((-1, -1), (1, 1), fill: blue) })
scope({ translate((0, 0, 0)); rect((-1, -1), (1, 1), fill: red) })
scope({ translate((0, 0, -1)); rect((-1, -1), (1, 1), fill: green) })
})
})

// Fully visible
#test-case({
import draw: *
ortho(x: 0deg, y: 0deg, cull-face: "cw", {
line((-1, -1), (1, -1), (1, 1), (-1, 1), close: true)
line((-1,-1), (1,-1), (0,1), close: true)
})
})

// Nothing visible
#test-case({
import draw: *
ortho(x: 0deg, y: 0deg, cull-face: "cw", {
line((-1, -1), (1, -1), (1, 1), (-1, 1), close: true)
rotate(y: 120deg)
line((-1,-1), (1,-1), (0,1), close: true)
})
})

// Face order of library shapes
#test-case({
import draw: *
ortho(cull-face: "cw", {
rect((-1, -1), (1, 1), radius: .5)
})
})

#test-case({
import draw: *
ortho(cull-face: "cw", {
circle((0,0))
})
})

#test-case({
import draw: *
ortho(cull-face: "cw", {
arc((0,0), start: 0deg, stop: 270deg, mode: "PIE")
})
})

#test-case({
import draw: *
ortho(cull-face: "cw", {
content((0,0), [Text])
})
})
Binary file modified tests/rect/rounded/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/rotation/around/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/rotation/ref/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 7141c4e

Please sign in to comment.