Skip to content

Commit 74bd0d4

Browse files
line: Add centroid anchor (#602)
* tests: Update refs * PR Fixes * Update reference images
1 parent 7141c4e commit 74bd0d4

File tree

41 files changed

+180
-55
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+180
-55
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ package called `cetz-plot`.
2727
- Added `floating` function for drawing elements without affecting bounding boxes.
2828
- The `ortho` function gained a `sorted` and `cull-face` argument to enable
2929
depth ordering and face culling of drawables. Ordering is enabled by default.
30+
- Closed `line` and `merge-path` elements now have a `"centroid"` anchor that
31+
is the calculated centroid of the (non self-intersecting!) shape.
3032

3133
## Marks
3234
- Added support for mark `anchor` style key, to adjust mark placement and

src/draw/shapes.typ

Lines changed: 66 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#import "/src/anchor.typ" as anchor_
1515
#import "/src/mark.typ" as mark_
1616
#import "/src/mark-shapes.typ" as mark-shapes_
17+
#import "/src/polygon.typ"
1718
#import "/src/aabb.typ"
1819

1920
#import "transformations.typ": *
@@ -37,19 +38,19 @@
3738
/// *Root*: `circle`
3839
///
3940
/// - 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.
40-
///
41+
///
4142
/// ### Anchors
4243
/// Supports border and path anchors. The "center" anchor is the default.
4344
///
44-
#let circle(position, name: none, anchor: none, ..style) = {
45+
#let circle(position, name: none, anchor: none, ..style) = {
4546
// No extra positional arguments from the style sink
4647
assert.eq(
4748
style.pos(),
4849
(),
4950
message: "Unexpected positional arguments: " + repr(style.pos()),
5051
)
5152
let style = style.named()
52-
53+
5354
(ctx => {
5455
let (ctx, pos) = coordinate.resolve(ctx, position)
5556
let style = styles.resolve(ctx.style, merge: style, root: "circle")
@@ -163,7 +164,7 @@
163164
},)
164165
}
165166

166-
/// Draws a circular segment.
167+
/// Draws a circular segment.
167168
///
168169
/// ```typc example
169170
/// arc((0,0), start: 45deg, stop: 135deg)
@@ -190,12 +191,12 @@
190191
///
191192
/// ## Anchors
192193
/// Supports border and path anchors.
193-
/// - **arc-start** The position at which the arc's curve starts, this is the default.
194-
/// - **arc-end** The position of the arc's curve end.
195-
/// - **arc-center** The midpoint of the arc's curve.
196-
/// - **center** The center of the arc, this position changes depending on if the arc is closed or not.
197-
/// - **chord-center** Center of chord of the arc drawn between the start and end point.
198-
/// - **origin** The origin of the arc's circle.
194+
/// - **arc-start**: The position at which the arc's curve starts, this is the default.
195+
/// - **arc-end**: The position of the arc's curve end.
196+
/// - **arc-center**: The midpoint of the arc's curve.
197+
/// - **center**: The center of the arc, this position changes depending on if the arc is closed or not.
198+
/// - **chord-center**: Center of chord of the arc drawn between the start and end point.
199+
/// - **origin**: The origin of the arc's circle.
199200
#let arc(
200201
position,
201202
start: auto,
@@ -210,18 +211,18 @@
210211
(start, stop, delta).filter(it => { it == auto }).len() == 1,
211212
message: "Exactly two of three options start, stop and delta should be defined.",
212213
)
213-
214+
214215
// No extra positional arguments from the style sink
215216
assert.eq(
216217
style.pos(),
217218
(),
218219
message: "Unexpected positional arguments: " + repr(style.pos()),
219220
)
220221
let style = style.named()
221-
222+
222223
// Coordinate check
223224
let t = coordinate.resolve-system(position)
224-
225+
225226
let start-angle = if start == auto { stop - delta } else { start }
226227
let stop-angle = if stop == auto { start + delta } else { stop }
227228
// Border angles can break if the angle is 0.
@@ -272,7 +273,7 @@
272273
let center = if style.mode != "CLOSE" {
273274
// 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.
274275
vector.lerp(
275-
arc-center,
276+
arc-center,
276277
sector-center,
277278
if (stop-angle + start-angle) > 180deg { (stop-angle + start-angle) } else { (stop-angle + start-angle) + 180deg } / 720deg
278279
)
@@ -335,9 +336,9 @@
335336
/// - name (none, str):
336337
/// - ..style (style):
337338
///
338-
/// ### Styling
339+
/// ### Styling
339340
/// *Root*: `arc`
340-
///
341+
///
341342
/// Uses the same styling as @@arc()
342343
///
343344
/// ### Anchors
@@ -435,7 +436,7 @@
435436
(),
436437
message: "Unexpected positional arguments: " + repr(style.pos()),
437438
)
438-
439+
439440
let style = style.named()
440441

441442
if type(to) == angle {
@@ -445,7 +446,7 @@
445446
}
446447

447448
(from, to).map(coordinate.resolve-system)
448-
449+
449450
return (ctx => {
450451
let (ctx, ..pts) = coordinate.resolve(ctx, from, to)
451452
let style = styles.resolve(ctx.style, merge: style, root: "mark")
@@ -469,7 +470,7 @@
469470
}
470471

471472
/// Draws a line, more than two points can be given to create a line-strip.
472-
///
473+
///
473474
/// ```typc example
474475
/// line((-1.5, 0), (1.5, 0))
475476
/// line((0, -1.5), (0, 1.5))
@@ -490,20 +491,21 @@
490491
/// - close (bool): If true, the line-strip gets closed to form a polygon
491492
/// - name (none,str):
492493
///
493-
/// ## Styling
494+
/// ## Styling
494495
/// *Root:* `line`
495496
///
496497
/// Supports mark styling.
497-
///
498+
///
498499
/// ## Anchors
499-
/// Supports path anchors.
500+
/// Supports path anchors.
501+
/// - **centroid**: The centroid anchor is calculated for _closed non self-intersecting_ polygons if all vertices share the same z value.
500502
#let line(..pts-style, close: false, name: none) = {
501503
// Extra positional arguments from the pts-style sink are interpreted as coordinates.
502504
let pts = pts-style.pos()
503505
let style = pts-style.named()
504-
506+
505507
assert(pts.len() >= 2, message: "Line must have a minimum of two points")
506-
508+
507509
// Coordinate check
508510
let pts-system = pts.map(coordinate.resolve-system)
509511

@@ -528,7 +530,7 @@
528530
return util.revert-transform(ctx.transform, pt)
529531
}
530532
}
531-
533+
532534
return (ctx => {
533535
let first-elem = pts.first()
534536
let last-elem = pts.last()
@@ -557,8 +559,12 @@
557559

558560
// Get bounds
559561
let (transform, anchors) = anchor_.setup(
560-
auto,
561-
(),
562+
name => {
563+
if name == "centroid" {
564+
return polygon.simple-centroid(pts)
565+
}
566+
},
567+
if close != none { ("centroid",) } else { () },
562568
name: name,
563569
transform: ctx.transform,
564570
path-anchors: true,
@@ -586,7 +592,7 @@
586592
/// ```typc example
587593
/// // Draw a grid
588594
/// grid((0,0), (2,2))
589-
///
595+
///
590596
/// // Draw a smaller blue grid
591597
/// grid((1,1), (2,2), stroke: blue, step: .25)
592598
/// ```
@@ -700,15 +706,15 @@
700706
/// ```typc example
701707
/// content((0,0), [Hello World!])
702708
/// ```
703-
/// 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`:
709+
/// 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`:
704710
///
705711
/// ```typc example
706712
/// line((0, 0), (3, 1), name: "line")
707713
/// content(
708714
/// ("line.start", 50%, "line.end"),
709715
/// angle: "line.end",
710716
/// padding: .1,
711-
/// anchor: "south",
717+
/// anchor: "south",
712718
/// [Text on a line]
713719
/// )
714720
/// ```
@@ -727,19 +733,19 @@
727733
/// *Root*: `content`
728734
/// - 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/
729735
/// - frame (str, none) = none: Sets the frame style. Can be `none`, "rect" or "circle" and inherits the `stroke` and `fill` style.
730-
///
736+
///
731737
/// ## Anchors
732738
/// Supports border anchors.
733739
#let content(
734740
..args-style,
735741
angle: 0deg,
736-
anchor: none,
737-
name: none,
742+
anchor: none,
743+
name: none,
738744
) = {
739745
let (args, style) = (args-style.pos(), args-style.named())
740746

741747
let (a, b, body) = if args.len() == 2 {
742-
args.insert(1, auto)
748+
args.insert(1, auto)
743749
args
744750
} else if args.len() == 3 {
745751
args
@@ -961,7 +967,7 @@
961967
message: "Unexpected positional arguments: " + repr(style.pos()),
962968
)
963969
let style = style.named()
964-
970+
965971
return (
966972
ctx => {
967973
let ctx = ctx
@@ -1111,7 +1117,7 @@
11111117
/// let (a, b, c) = ((0, 0), (2, 0), (1, 1))
11121118
/// line(a, c, b, stroke: gray)
11131119
/// bezier(a, b, c)
1114-
///
1120+
///
11151121
/// let (a, b, c, d) = ((0, -1), (2, -1), (.5, -2), (1.5, 0))
11161122
/// line(a, c, d, b, stroke: gray)
11171123
/// bezier(a, b, c, d)
@@ -1121,28 +1127,28 @@
11211127
/// - end (coordinate): End position (last coordinate)
11221128
/// - name (none,str):
11231129
/// - ..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.
1124-
///
1125-
/// ## Styling
1130+
///
1131+
/// ## Styling
11261132
/// *Root* `bezier`
1127-
///
1133+
///
11281134
/// Supports marks.
1129-
///
1135+
///
11301136
/// ## Anchors
11311137
/// Supports path anchors.
11321138
/// - **ctrl-n**: nth control point where n is an integer starting at 0
11331139
///
11341140
#let bezier(start, end, ..ctrl-style, name: none) = {
11351141
// Extra positional arguments are treated like control points.
11361142
let (ctrl, style) = (ctrl-style.pos(), ctrl-style.named())
1137-
1143+
11381144
// Control point check
11391145
let len = ctrl.len()
11401146
assert(
11411147
len in (1, 2),
11421148
message: "Bezier curve expects 1 or 2 control points. Got " + str(len),
11431149
)
11441150
let coordinates = (start, ..ctrl, end)
1145-
1151+
11461152
// Coordinates check
11471153
let t = coordinates.map(coordinate.resolve-system)
11481154

@@ -1181,7 +1187,7 @@
11811187
}
11821188

11831189
return (
1184-
ctx: ctx,
1190+
ctx: ctx,
11851191
name: name,
11861192
anchors: anchors,
11871193
drawables: drawables,
@@ -1230,11 +1236,11 @@
12301236
/// - close (bool): Closes the curve with a straight line between the start and end of the curve.
12311237
/// - name (none,str):
12321238
///
1233-
/// ## Styling
1239+
/// ## Styling
12341240
/// *Root*: `catmull`
12351241
///
12361242
/// Supports marks.
1237-
///
1243+
///
12381244
/// - tension (float) = 0.5: How tight the curve should fit to the points. The higher the tension the less curvy the curve.
12391245
///
12401246
/// ## Anchors
@@ -1305,11 +1311,11 @@
13051311
/// - 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.
13061312
/// - close (bool): Closes the curve with a proper smooth curve between the start and end of the curve.
13071313
/// - name (none,str):
1308-
///
1314+
///
13091315
/// ## Styling
13101316
/// *Root* `hobby`
13111317
///
1312-
/// Supports marks.
1318+
/// Supports marks.
13131319
/// - 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.
13141320
///
13151321
/// ## Anchors
@@ -1385,13 +1391,14 @@
13851391
///
13861392
/// Elements hidden via @@hide() are ignored.
13871393
///
1394+
/// ## Anchors
1395+
/// **centroid**: Centroid of the _closed and non self-intersecting_ shape. Only exists if `close` is true.
1396+
/// Supports path anchors and shapes where all vertices share the same z-value.
1397+
///
13881398
/// - body (elements): Elements with paths to be merged together.
13891399
/// - close (bool): Close the path with a straight line from the start of the path to its end.
13901400
/// - name (none,str):
13911401
/// - ..style (style):
1392-
///
1393-
/// ## Anchors
1394-
/// Supports path anchors.
13951402
#let merge-path(body, close: false, name: none, ..style) = {
13961403
// No extra positional arguments from the style sink
13971404
assert.eq(
@@ -1400,7 +1407,7 @@
14001407
message: "Unexpected positional arguments: " + repr(style.pos()),
14011408
)
14021409
let style = style.named()
1403-
1410+
14041411
return (
14051412
ctx => {
14061413
let ctx = ctx
@@ -1429,8 +1436,14 @@
14291436
let drawables = drawable.path(fill: style.fill, stroke: style.stroke, close: close, segments)
14301437

14311438
let (transform, anchors) = anchor_.setup(
1432-
auto,
1433-
(),
1439+
name => {
1440+
if name == "centroid" {
1441+
// Try finding a closed shapes center by
1442+
// Sampling it to a polygon.
1443+
return polygon.simple-centroid(polygon.from-segments(drawables.segments))
1444+
}
1445+
},
1446+
if close != none { ("centroid",) } else { () },
14341447
name: name,
14351448
transform: none,
14361449
path-anchors: true,

0 commit comments

Comments
 (0)