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

line: Add centroid anchor #602

Merged
merged 3 commits into from
Aug 13, 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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ package called `cetz-plot`.
- 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.
- Closed `line` and `merge-path` elements now have a `"centroid"` anchor that
is the calculated centroid of the (non self-intersecting!) shape.

## Marks
- Added support for mark `anchor` style key, to adjust mark placement and
Expand Down
119 changes: 66 additions & 53 deletions src/draw/shapes.typ
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#import "/src/anchor.typ" as anchor_
#import "/src/mark.typ" as mark_
#import "/src/mark-shapes.typ" as mark-shapes_
#import "/src/polygon.typ"
#import "/src/aabb.typ"

#import "transformations.typ": *
Expand All @@ -37,19 +38,19 @@
/// *Root*: `circle`
///
/// - radius (number, array) = 1: A number that defines the size of the circle's radius. Can also be set to a tuple of two numbers to define the radii of an ellipse, the first number is the `x` radius and the second is the `y` radius.
///
///
/// ### Anchors
/// Supports border and path anchors. The "center" anchor is the default.
///
#let circle(position, name: none, anchor: none, ..style) = {
#let circle(position, name: none, anchor: none, ..style) = {
// No extra positional arguments from the style sink
assert.eq(
style.pos(),
(),
message: "Unexpected positional arguments: " + repr(style.pos()),
)
let style = style.named()

(ctx => {
let (ctx, pos) = coordinate.resolve(ctx, position)
let style = styles.resolve(ctx.style, merge: style, root: "circle")
Expand Down Expand Up @@ -163,7 +164,7 @@
},)
}

/// Draws a circular segment.
/// Draws a circular segment.
///
/// ```typc example
/// arc((0,0), start: 45deg, stop: 135deg)
Expand All @@ -190,12 +191,12 @@
///
/// ## Anchors
/// Supports border and path anchors.
/// - **arc-start** The position at which the arc's curve starts, this is the default.
/// - **arc-end** The position of the arc's curve end.
/// - **arc-center** The midpoint of the arc's curve.
/// - **center** The center of the arc, this position changes depending on if the arc is closed or not.
/// - **chord-center** Center of chord of the arc drawn between the start and end point.
/// - **origin** The origin of the arc's circle.
/// - **arc-start**: The position at which the arc's curve starts, this is the default.
/// - **arc-end**: The position of the arc's curve end.
/// - **arc-center**: The midpoint of the arc's curve.
/// - **center**: The center of the arc, this position changes depending on if the arc is closed or not.
/// - **chord-center**: Center of chord of the arc drawn between the start and end point.
/// - **origin**: The origin of the arc's circle.
#let arc(
position,
start: auto,
Expand All @@ -210,18 +211,18 @@
(start, stop, delta).filter(it => { it == auto }).len() == 1,
message: "Exactly two of three options start, stop and delta should be defined.",
)

// No extra positional arguments from the style sink
assert.eq(
style.pos(),
(),
message: "Unexpected positional arguments: " + repr(style.pos()),
)
let style = style.named()

// Coordinate check
let t = coordinate.resolve-system(position)

let start-angle = if start == auto { stop - delta } else { start }
let stop-angle = if stop == auto { start + delta } else { stop }
// Border angles can break if the angle is 0.
Expand Down Expand Up @@ -272,7 +273,7 @@
let center = if style.mode != "CLOSE" {
// A circular sector's center anchor is placed half way between the sector-center and arc-center when the angle is 180deg. At 60deg it is placed 1/3 of the way between, this is mirrored at 300deg.
vector.lerp(
arc-center,
arc-center,
sector-center,
if (stop-angle + start-angle) > 180deg { (stop-angle + start-angle) } else { (stop-angle + start-angle) + 180deg } / 720deg
)
Expand Down Expand Up @@ -335,9 +336,9 @@
/// - name (none, str):
/// - ..style (style):
///
/// ### Styling
/// ### Styling
/// *Root*: `arc`
///
///
/// Uses the same styling as @@arc()
///
/// ### Anchors
Expand Down Expand Up @@ -435,7 +436,7 @@
(),
message: "Unexpected positional arguments: " + repr(style.pos()),
)

let style = style.named()

if type(to) == angle {
Expand All @@ -445,7 +446,7 @@
}

(from, to).map(coordinate.resolve-system)

return (ctx => {
let (ctx, ..pts) = coordinate.resolve(ctx, from, to)
let style = styles.resolve(ctx.style, merge: style, root: "mark")
Expand All @@ -469,7 +470,7 @@
}

/// Draws a line, more than two points can be given to create a line-strip.
///
///
/// ```typc example
/// line((-1.5, 0), (1.5, 0))
/// line((0, -1.5), (0, 1.5))
Expand All @@ -490,20 +491,21 @@
/// - close (bool): If true, the line-strip gets closed to form a polygon
/// - name (none,str):
///
/// ## Styling
/// ## Styling
/// *Root:* `line`
///
/// Supports mark styling.
///
///
/// ## Anchors
/// Supports path anchors.
/// Supports path anchors.
/// - **centroid**: The centroid anchor is calculated for _closed non self-intersecting_ polygons if all vertices share the same z value.
#let line(..pts-style, close: false, name: none) = {
// Extra positional arguments from the pts-style sink are interpreted as coordinates.
let pts = pts-style.pos()
let style = pts-style.named()

assert(pts.len() >= 2, message: "Line must have a minimum of two points")

// Coordinate check
let pts-system = pts.map(coordinate.resolve-system)

Expand All @@ -528,7 +530,7 @@
return util.revert-transform(ctx.transform, pt)
}
}

return (ctx => {
let first-elem = pts.first()
let last-elem = pts.last()
Expand Down Expand Up @@ -557,8 +559,12 @@

// Get bounds
let (transform, anchors) = anchor_.setup(
auto,
(),
name => {
if name == "centroid" {
return polygon.simple-centroid(pts)
}
},
if close != none { ("centroid",) } else { () },
name: name,
transform: ctx.transform,
path-anchors: true,
Expand Down Expand Up @@ -586,7 +592,7 @@
/// ```typc example
/// // Draw a grid
/// grid((0,0), (2,2))
///
///
/// // Draw a smaller blue grid
/// grid((1,1), (2,2), stroke: blue, step: .25)
/// ```
Expand Down Expand Up @@ -700,15 +706,15 @@
/// ```typc example
/// content((0,0), [Hello World!])
/// ```
/// To put text on a line you can let the function calculate the angle between its position and a second coordinate by passing it to `angle`:
/// To put text on a line you can let the function calculate the angle between its position and a second coordinate by passing it to `angle`:
///
/// ```typc example
/// line((0, 0), (3, 1), name: "line")
/// content(
/// ("line.start", 50%, "line.end"),
/// angle: "line.end",
/// padding: .1,
/// anchor: "south",
/// anchor: "south",
/// [Text on a line]
/// )
/// ```
Expand All @@ -727,19 +733,19 @@
/// *Root*: `content`
/// - padding (number, dictionary) = 0: Sets the spacing around content. Can be a single number to set padding on all sides or a dictionary to specify each side specifically. The dictionary follows Typst's `pad` function: https://typst.app/docs/reference/layout/pad/
/// - frame (str, none) = none: Sets the frame style. Can be `none`, "rect" or "circle" and inherits the `stroke` and `fill` style.
///
///
/// ## Anchors
/// Supports border anchors.
#let content(
..args-style,
angle: 0deg,
anchor: none,
name: none,
anchor: none,
name: none,
) = {
let (args, style) = (args-style.pos(), args-style.named())

let (a, b, body) = if args.len() == 2 {
args.insert(1, auto)
args.insert(1, auto)
args
} else if args.len() == 3 {
args
Expand Down Expand Up @@ -961,7 +967,7 @@
message: "Unexpected positional arguments: " + repr(style.pos()),
)
let style = style.named()

return (
ctx => {
let ctx = ctx
Expand Down Expand Up @@ -1111,7 +1117,7 @@
/// let (a, b, c) = ((0, 0), (2, 0), (1, 1))
/// line(a, c, b, stroke: gray)
/// bezier(a, b, c)
///
///
/// let (a, b, c, d) = ((0, -1), (2, -1), (.5, -2), (1.5, 0))
/// line(a, c, d, b, stroke: gray)
/// bezier(a, b, c, d)
Expand All @@ -1121,28 +1127,28 @@
/// - end (coordinate): End position (last coordinate)
/// - name (none,str):
/// - ..ctrl-style (coordinate,style): The first two positional arguments are taken as cubic bezier control points, where the first is the start control point and the second is the end control point. One control point can be given for a quadratic bezier curve instead. Named arguments are for styling.
///
/// ## Styling
///
/// ## Styling
/// *Root* `bezier`
///
///
/// Supports marks.
///
///
/// ## Anchors
/// Supports path anchors.
/// - **ctrl-n**: nth control point where n is an integer starting at 0
///
#let bezier(start, end, ..ctrl-style, name: none) = {
// Extra positional arguments are treated like control points.
let (ctrl, style) = (ctrl-style.pos(), ctrl-style.named())

// Control point check
let len = ctrl.len()
assert(
len in (1, 2),
message: "Bezier curve expects 1 or 2 control points. Got " + str(len),
)
let coordinates = (start, ..ctrl, end)

// Coordinates check
let t = coordinates.map(coordinate.resolve-system)

Expand Down Expand Up @@ -1181,7 +1187,7 @@
}

return (
ctx: ctx,
ctx: ctx,
name: name,
anchors: anchors,
drawables: drawables,
Expand Down Expand Up @@ -1230,11 +1236,11 @@
/// - close (bool): Closes the curve with a straight line between the start and end of the curve.
/// - name (none,str):
///
/// ## Styling
/// ## Styling
/// *Root*: `catmull`
///
/// Supports marks.
///
///
/// - tension (float) = 0.5: How tight the curve should fit to the points. The higher the tension the less curvy the curve.
///
/// ## Anchors
Expand Down Expand Up @@ -1305,11 +1311,11 @@
/// - ta (auto, array): Outgoing tension at `pts.at(n)` from `pts.at(n)` to `pts.at(n+1)`. The number given must be one less than the number of points.
/// - close (bool): Closes the curve with a proper smooth curve between the start and end of the curve.
/// - name (none,str):
///
///
/// ## Styling
/// *Root* `hobby`
///
/// Supports marks.
/// Supports marks.
/// - omega (array) = (1, 1): A tuple of floats that describe how curly the curve should be at each endpoint. When the curl is close to zero, the spline approaches a straight line near the endpoints. When the curl is close to one, it approaches a circular arc.
///
/// ## Anchors
Expand Down Expand Up @@ -1385,13 +1391,14 @@
///
/// Elements hidden via @@hide() are ignored.
///
/// ## Anchors
/// **centroid**: Centroid of the _closed and non self-intersecting_ shape. Only exists if `close` is true.
/// Supports path anchors and shapes where all vertices share the same z-value.
///
/// - body (elements): Elements with paths to be merged together.
/// - close (bool): Close the path with a straight line from the start of the path to its end.
/// - name (none,str):
/// - ..style (style):
///
/// ## Anchors
/// Supports path anchors.
#let merge-path(body, close: false, name: none, ..style) = {
// No extra positional arguments from the style sink
assert.eq(
Expand All @@ -1400,7 +1407,7 @@
message: "Unexpected positional arguments: " + repr(style.pos()),
)
let style = style.named()

return (
ctx => {
let ctx = ctx
Expand Down Expand Up @@ -1429,8 +1436,14 @@
let drawables = drawable.path(fill: style.fill, stroke: style.stroke, close: close, segments)

let (transform, anchors) = anchor_.setup(
auto,
(),
name => {
if name == "centroid" {
// Try finding a closed shapes center by
// Sampling it to a polygon.
return polygon.simple-centroid(polygon.from-segments(drawables.segments))
}
},
if close != none { ("centroid",) } else { () },
name: name,
transform: none,
path-anchors: true,
Expand Down
Loading
Loading