diff --git a/CHANGES.md b/CHANGES.md index 03fc11719..7c9171b9a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/src/draw.typ b/src/draw.typ index 53d84ba4f..9baf614b1 100644 --- a/src/draw.typ +++ b/src/draw.typ @@ -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 diff --git a/src/draw/projection.typ b/src/draw/projection.typ index 2e7bb5540..68b227748 100644 --- a/src/draw/projection.typ +++ b/src/draw/projection.typ @@ -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( @@ -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, @@ -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. diff --git a/src/draw/shapes.typ b/src/draw/shapes.typ index 60a997cc7..8bf07966d 100644 --- a/src/draw/shapes.typ +++ b/src/draw/shapes.typ @@ -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)),) @@ -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) } diff --git a/src/drawable.typ b/src/drawable.typ index b8de975d9..f7e6d0194 100644 --- a/src/drawable.typ +++ b/src/drawable.typ @@ -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, diff --git a/src/polygon.typ b/src/polygon.typ new file mode 100644 index 000000000..563d6c227 --- /dev/null +++ b/src/polygon.typ @@ -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 + } +} diff --git a/tests/anchor/element-anchors/ref/1.png b/tests/anchor/element-anchors/ref/1.png index eaba7b51c..b99393bf3 100644 Binary files a/tests/anchor/element-anchors/ref/1.png and b/tests/anchor/element-anchors/ref/1.png differ diff --git a/tests/anchor/path-anchors/ref/1.png b/tests/anchor/path-anchors/ref/1.png index 12e849db6..4c01edc1b 100644 Binary files a/tests/anchor/path-anchors/ref/1.png and b/tests/anchor/path-anchors/ref/1.png differ diff --git a/tests/arc/ref/1.png b/tests/arc/ref/1.png index 10dcda4c5..f28cb4276 100644 Binary files a/tests/arc/ref/1.png and b/tests/arc/ref/1.png differ diff --git a/tests/bounds/ref/1.png b/tests/bounds/ref/1.png index d50ed2242..c70727f74 100644 Binary files a/tests/bounds/ref/1.png and b/tests/bounds/ref/1.png differ diff --git a/tests/content/transform/ref/1.png b/tests/content/transform/ref/1.png index b769e4a05..3107f6350 100644 Binary files a/tests/content/transform/ref/1.png and b/tests/content/transform/ref/1.png differ diff --git a/tests/decorations/path/ref/1.png b/tests/decorations/path/ref/1.png index 4789705fc..619d1068e 100644 Binary files a/tests/decorations/path/ref/1.png and b/tests/decorations/path/ref/1.png differ diff --git a/tests/floating/ref/1.png b/tests/floating/ref/1.png index 155ed7150..925095244 100644 Binary files a/tests/floating/ref/1.png and b/tests/floating/ref/1.png differ diff --git a/tests/group/transform/ref/1.png b/tests/group/transform/ref/1.png index 572692f95..78eafa4e0 100644 Binary files a/tests/group/transform/ref/1.png and b/tests/group/transform/ref/1.png differ diff --git a/tests/line/element-element/ref/1.png b/tests/line/element-element/ref/1.png index 4ddb0945d..cc502d48b 100644 Binary files a/tests/line/element-element/ref/1.png and b/tests/line/element-element/ref/1.png differ diff --git a/tests/mark/auto-offset/ref/1.png b/tests/mark/auto-offset/ref/1.png index 0077a92bd..d5d310943 100644 Binary files a/tests/mark/auto-offset/ref/1.png and b/tests/mark/auto-offset/ref/1.png differ diff --git a/tests/palette/ref/1.png b/tests/palette/ref/1.png index 858b4dc71..e4368156d 100644 Binary files a/tests/palette/ref/1.png and b/tests/palette/ref/1.png differ diff --git a/tests/projection/ortho/ref/1.png b/tests/projection/ortho/ref/1.png index 44bf7cc57..cf7de379f 100644 Binary files a/tests/projection/ortho/ref/1.png and b/tests/projection/ortho/ref/1.png differ diff --git a/tests/projection/ortho/test.typ b/tests/projection/ortho/test.typ index 7c2258a33..a5949f8ea 100644 --- a/tests/projection/ortho/test.typ +++ b/tests/projection/ortho/test.typ @@ -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() = { @@ -27,6 +29,13 @@ } } +#test-case({ + import draw: * + ortho(reset-transform: false, { + line((-1, 0), (1, 0), mark: (end: ">")) + }) +}) + #test-case({ import draw: * ortho({ @@ -67,7 +76,7 @@ #test-case({ import draw: * - ortho({ + ortho(sorted: true, { axes(4) on-yz(x: -1, { checkerboard() @@ -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]) + }) +}) diff --git a/tests/rect/rounded/ref/1.png b/tests/rect/rounded/ref/1.png index 4e4fa9050..8a2c962fd 100644 Binary files a/tests/rect/rounded/ref/1.png and b/tests/rect/rounded/ref/1.png differ diff --git a/tests/rotation/around/ref/1.png b/tests/rotation/around/ref/1.png index bae3f98c9..4d4238bd0 100644 Binary files a/tests/rotation/around/ref/1.png and b/tests/rotation/around/ref/1.png differ diff --git a/tests/rotation/ref/1.png b/tests/rotation/ref/1.png index 7f3426a88..78c466e11 100644 Binary files a/tests/rotation/ref/1.png and b/tests/rotation/ref/1.png differ