Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/assets/typst/packs/preview/cetz/0.3.2.pack

281 KiB, 1 run

created by r2519314175:1063, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1DAIMOND TYPST PACK 1
2namespace preview
3name cetz
4version 0.3.2
5entrypoint src/lib.typ
6file 7651 LICENSE
7file 1953 src/aabb.typ
8file 7179 src/anchor.typ
9file 18053 src/bezier.typ
10file 4758 src/canvas.typ
11file 2712 src/complex.typ
12file 8810 src/coordinate.typ
13file 32 src/deps.typ
14file 578 src/draw.typ
15file 18646 src/draw/grouping.typ
16file 5903 src/draw/projection.typ
17file 51771 src/draw/shapes.typ
18file 2059 src/draw/styling.typ
19file 8432 src/draw/transformations.typ
20file 871 src/draw/util.typ
21file 6319 src/drawable.typ
22file 9706 src/hobby.typ
23file 3674 src/intersection.typ
24file 488 src/lib.typ
25file 6855 src/lib/angle.typ
26file 157 src/lib/decorations.typ
27file 12984 src/lib/decorations/brace.typ
28file 15433 src/lib/decorations/path.typ
29file 4248 src/lib/palette.typ
30file 410 src/lib/pin.typ
31file 6944 src/lib/tree.typ
32file 8800 src/mark-shapes.typ
33file 11492 src/mark.typ
34file 9463 src/matrix.typ
35file 15548 src/path-util.typ
36file 2577 src/polygon.typ
37file 2632 src/process.typ
38file 867 src/sorting.typ
39file 8949 src/styles.typ
40file 13292 src/util.typ
41file 5821 src/vector.typ
42file 30 src/version.typ
43file 608 typst.toml
44
45 GNU LESSER GENERAL PUBLIC LICENSE
46 Version 3, 29 June 2007
47
48 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
49 Everyone is permitted to copy and distribute verbatim copies
50 of this license document, but changing it is not allowed.
51
52
53 This version of the GNU Lesser General Public License incorporates
54the terms and conditions of version 3 of the GNU General Public
55License, supplemented by the additional permissions listed below.
56
57 0. Additional Definitions.
58
59 As used herein, "this License" refers to version 3 of the GNU Lesser
60General Public License, and the "GNU GPL" refers to version 3 of the GNU
61General Public License.
62
63 "The Library" refers to a covered work governed by this License,
64other than an Application or a Combined Work as defined below.
65
66 An "Application" is any work that makes use of an interface provided
67by the Library, but which is not otherwise based on the Library.
68Defining a subclass of a class defined by the Library is deemed a mode
69of using an interface provided by the Library.
70
71 A "Combined Work" is a work produced by combining or linking an
72Application with the Library. The particular version of the Library
73with which the Combined Work was made is also called the "Linked
74Version".
75
76 The "Minimal Corresponding Source" for a Combined Work means the
77Corresponding Source for the Combined Work, excluding any source code
78for portions of the Combined Work that, considered in isolation, are
79based on the Application, and not on the Linked Version.
80
81 The "Corresponding Application Code" for a Combined Work means the
82object code and/or source code for the Application, including any data
83and utility programs needed for reproducing the Combined Work from the
84Application, but excluding the System Libraries of the Combined Work.
85
86 1. Exception to Section 3 of the GNU GPL.
87
88 You may convey a covered work under sections 3 and 4 of this License
89without being bound by section 3 of the GNU GPL.
90
91 2. Conveying Modified Versions.
92
93 If you modify a copy of the Library, and, in your modifications, a
94facility refers to a function or data to be supplied by an Application
95that uses the facility (other than as an argument passed when the
96facility is invoked), then you may convey a copy of the modified
97version:
98
99 a) under this License, provided that you make a good faith effort to
100 ensure that, in the event an Application does not supply the
101 function or data, the facility still operates, and performs
102 whatever part of its purpose remains meaningful, or
103
104 b) under the GNU GPL, with none of the additional permissions of
105 this License applicable to that copy.
106
107 3. Object Code Incorporating Material from Library Header Files.
108
109 The object code form of an Application may incorporate material from
110a header file that is part of the Library. You may convey such object
111code under terms of your choice, provided that, if the incorporated
112material is not limited to numerical parameters, data structure
113layouts and accessors, or small macros, inline functions and templates
114(ten or fewer lines in length), you do both of the following:
115
116 a) Give prominent notice with each copy of the object code that the
117 Library is used in it and that the Library and its use are
118 covered by this License.
119
120 b) Accompany the object code with a copy of the GNU GPL and this license
121 document.
122
123 4. Combined Works.
124
125 You may convey a Combined Work under terms of your choice that,
126taken together, effectively do not restrict modification of the
127portions of the Library contained in the Combined Work and reverse
128engineering for debugging such modifications, if you also do each of
129the following:
130
131 a) Give prominent notice with each copy of the Combined Work that
132 the Library is used in it and that the Library and its use are
133 covered by this License.
134
135 b) Accompany the Combined Work with a copy of the GNU GPL and this license
136 document.
137
138 c) For a Combined Work that displays copyright notices during
139 execution, include the copyright notice for the Library among
140 these notices, as well as a reference directing the user to the
141 copies of the GNU GPL and this license document.
142
143 d) Do one of the following:
144
145 0) Convey the Minimal Corresponding Source under the terms of this
146 License, and the Corresponding Application Code in a form
147 suitable for, and under terms that permit, the user to
148 recombine or relink the Application with a modified version of
149 the Linked Version to produce a modified Combined Work, in the
150 manner specified by section 6 of the GNU GPL for conveying
151 Corresponding Source.
152
153 1) Use a suitable shared library mechanism for linking with the
154 Library. A suitable mechanism is one that (a) uses at run time
155 a copy of the Library already present on the user's computer
156 system, and (b) will operate properly with a modified version
157 of the Library that is interface-compatible with the Linked
158 Version.
159
160 e) Provide Installation Information, but only if you would otherwise
161 be required to provide such information under section 6 of the
162 GNU GPL, and only to the extent that such information is
163 necessary to install and execute a modified version of the
164 Combined Work produced by recombining or relinking the
165 Application with a modified version of the Linked Version. (If
166 you use option 4d0, the Installation Information must accompany
167 the Minimal Corresponding Source and Corresponding Application
168 Code. If you use option 4d1, you must provide the Installation
169 Information in the manner specified by section 6 of the GNU GPL
170 for conveying Corresponding Source.)
171
172 5. Combined Libraries.
173
174 You may place library facilities that are a work based on the
175Library side by side in a single library together with other library
176facilities that are not Applications and are not covered by this
177License, and convey such a combined library under terms of your
178choice, if you do both of the following:
179
180 a) Accompany the combined library with a copy of the same work based
181 on the Library, uncombined with any other library facilities,
182 conveyed under the terms of this License.
183
184 b) Give prominent notice with the combined library that part of it
185 is a work based on the Library, and explaining where to find the
186 accompanying uncombined form of the same work.
187
188 6. Revised Versions of the GNU Lesser General Public License.
189
190 The Free Software Foundation may publish revised and/or new versions
191of the GNU Lesser General Public License from time to time. Such new
192versions will be similar in spirit to the present version, but may
193differ in detail to address new problems or concerns.
194
195 Each version is given a distinguishing version number. If the
196Library as you received it specifies that a certain numbered version
197of the GNU Lesser General Public License "or any later version"
198applies to it, you have the option of following the terms and
199conditions either of that published version or of any later version
200published by the Free Software Foundation. If the Library as you
201received it does not specify a version number of the GNU Lesser
202General Public License, you may choose any version of the GNU Lesser
203General Public License ever published by the Free Software Foundation.
204
205 If the Library as you received it specifies that a proxy can decide
206whether future versions of the GNU Lesser General Public License shall
207apply, that proxy's public statement of acceptance of any version is
208permanent authorization for you to choose that version for the
209Library.
210#import "vector.typ"
211
212/// Compute an axis aligned bounding box (aabb) for a list of <Type>vectors</Type>.
213///
214/// - pts (array): List of <Type>vector</Type>s.
215/// - init (aabb): Initial aabb
216/// -> aabb
217#let aabb(pts, init: none) = {
218 let bounds = init
219
220 if type(pts) == array {
221 for (i, pt) in pts.enumerate() {
222 if bounds == none and i == 0 {
223 bounds = (low: pt, high: pt)
224 } else {
225 assert(type(pt) == array and pt.len() == 3, message: repr(init) + repr(pts))
226 let (x, y, z) = pt
227
228 let (lo-x, lo-y, lo-z) = bounds.low
229 bounds.low = (calc.min(lo-x, x), calc.min(lo-y, y), calc.min(lo-z, z))
230
231 let (hi-x, hi-y, hi-z) = bounds.high
232 bounds.high = (calc.max(hi-x, x), calc.max(hi-y, y), calc.max(hi-z, z))
233 }
234 }
235 return bounds
236 } else if type(pts) == dictionary {
237 if init == none {
238 return pts
239 } else {
240 return aabb((pts.low, pts.high,), init: bounds)
241 }
242 }
243
244 panic("Expected array of vectors or bbox dictionary, got: " + repr(pts))
245}
246
247/// Get the mid-point of an AABB as vector.
248///
249/// - bounds (aabb): The AABB to get the mid-point of.
250/// -> vector
251#let mid(bounds) = {
252 return vector.scale(vector.add(bounds.low, bounds.high), .5)
253}
254
255/// Get the size of an aabb as vector. This is a vector from the aabb's low to high.
256///
257/// - bounds (aabb): The aabb to get the size of.
258/// -> vector
259#let size(bounds) = {
260 return vector.sub(bounds.high, bounds.low)
261}
262
263/// Pad AABB with padding from dictionary with keys top, left, right and bottom.
264///
265/// - bounds (aabb): The AABB to pad.
266/// - padding (none, dictionary): Padding values
267///
268/// -> aabb
269#let padded(bounds, padding) = {
270 if padding != none {
271 bounds.low.at(0) -= padding.at("left", default: 0)
272 bounds.low.at(1) -= padding.at("top", default: 0)
273 bounds.high.at(0) += padding.at("right", default: 0)
274 bounds.high.at(1) += padding.at("bottom", default: 0)
275 }
276 return bounds
277}
278#import "deps.typ"
279#import deps.oxifmt: strfmt
280
281#import "util.typ"
282#import "intersection.typ"
283#import "drawable.typ"
284#import "path-util.typ"
285#import "matrix.typ"
286#import "vector.typ"
287
288// Compass direction to angle
289#let named-border-anchors = (
290 east: 0deg,
291 north-east: 45deg,
292 north: 90deg,
293 north-west: 135deg,
294 west: 180deg,
295 south-west: 225deg,
296 south: 270deg,
297 south-east: 315deg,
298)
299
300// Path anchors
301#let named-path-anchors = (
302 start: 0%,
303 mid: 50%,
304 end: 100%,
305)
306
307/// Calculates a border anchor at the given angle by testing for an intersection between a line and the given drawables. Returns `none` if no intersection is found for better error reporting.
308///
309/// - center (vector): The position from which to start the test line.
310/// - x-dist (number): The furthest distance the test line should go in the x direction.
311/// - y-dist (number): The furthest distance the test line should go in the y direction.
312/// - drawables (drawables): Drawables to test for an intersection against. Ideally should be of type path but all others are ignored.
313/// - angle (angle): The angle to check for a border anchor at.
314/// -> vector,none
315#let border(center, x-dist, y-dist, drawables, angle) = {
316 x-dist += util.float-epsilon
317 y-dist += util.float-epsilon
318
319 if type(drawables) == dictionary {
320 drawables = (drawables,)
321 }
322
323 let test-line = (
324 center,
325 (
326 center.at(0) + x-dist * calc.cos(angle),
327 center.at(1) + y-dist * calc.sin(angle),
328 center.at(2),
329 )
330 )
331
332 let pts = ()
333 for drawable in drawables {
334 if drawable.type != "path" {
335 continue
336 }
337 pts += intersection.line-path(..test-line, drawable)
338 }
339
340 if pts.len() == 1 {
341 return pts.first()
342 }
343
344
345 return if pts.len() == 1 {
346 pts.first()
347 } else if pts.len() > 1 {
348 // Find the furthest intersection point from center
349 util.sort-points-by-distance(center, pts).last()
350 }
351}
352
353
354/// Setup an anchor calculation and handling function for an element. Unifies anchor error checking and calculation of the offset transform.
355///
356/// A tuple of a transformation matrix and function will be returned.
357/// The transform is calculated by translating the given transform by the distance between the position of `offset-anchor` and `default`. It can then be used to correctly transform an element's drawables. If either are none the calculation won't happen but the transform will still be returned.
358/// The function can be used to get the transformed anchors of an element by passing it a string. An empty array can be passed to get the list of valid anchors.
359///
360/// - callback (function, auto): The function to call to get a named anchor's position. The anchor's name will be passed and it should return a <Type>vector</Type> (`str => vector`). If no named anchors exist on the element `auto` can be given instead of a function.
361/// - anchor-names (array): A list of valid anchor names. This list will be used to validate an anchor exists before `callback` is used.
362/// - default (str,none): The name of the default anchor, if one exists.
363/// - transform (matrix,none): The current transformation matrix to apply to an anchor's position before returning it. If `offset-anchor` and `default` is set, it will be first translated by the distance between them.
364/// - name (str, none): The name of the element, this is only used in the error message in the event an anchor is invalid.
365/// - offset-anchor (str, none): The name of an anchor to offset the transform by.
366/// - border-anchors (bool): If true, add border anchors.
367/// - path-anchors (bool): If true, add path anchors.
368/// - radii (none,array): Radius tuple used for border anchor calculation.
369/// - path (none,drawable): Path used for path and border anchor calculation.
370/// -> array
371#let setup(
372 callback,
373 anchor-names,
374 default: none,
375 transform: none,
376 name: none,
377 offset-anchor: none,
378 border-anchors: false,
379 path-anchors: false,
380 radii: none,
381 path: none,
382 nested-anchors: false
383 ) = {
384 // Passing no callback is valid!
385 if callback == auto {
386 callback = (anchor) => {}
387 }
388
389 // Add enabled anchor names
390 if border-anchors {
391 assert("center" in anchor-names and radii != none and path != none,
392 message: "Border anchors need a center anchor, radii and the path set!")
393 }
394 if path-anchors {
395 assert(path != none,
396 message: "Path anchors need the path set!")
397 }
398
399 // Anchor callback
400 let calculate-anchor(anchor, transform: none) = {
401 if anchor == () {
402 return (anchor-names + if border-anchors { named-border-anchors.keys() } + if path-anchors { named-path-anchors.keys() }).dedup()
403 }
404
405 let out = none
406 let nested-anchors = if type(anchor) == array {
407 if not nested-anchors {
408 anchor = anchor.join(".")
409 } else {
410 if anchor.len() > 1 {
411 anchor
412 }
413 anchor = anchor.first()
414 }
415 } else if nested-anchors and type(anchor) == str {
416 anchor = anchor.split(".")
417 if anchor.len() > 1 {
418 anchor
419 }
420 anchor = anchor.first()
421 }
422
423
424 if type(anchor) == str {
425 if anchor in anchor-names or (anchor == "default" and default != none) {
426 if anchor == "default" {
427 anchor = default
428 }
429
430 out = callback(if nested-anchors != none { nested-anchors } else { anchor })
431 } else if path-anchors and anchor in named-path-anchors {
432 anchor = named-path-anchors.at(anchor)
433 } else if border-anchors and anchor in named-border-anchors {
434 anchor = named-border-anchors.at(anchor)
435 } else if util.str-is-number(anchor) {
436 anchor = util.str-to-number(if nested-anchors != none { nested-anchors.join(".") } else { anchor })
437 } else {
438 panic(
439 strfmt(
440 "Anchor '{}' not in anchors {} for element '{}'",
441 anchor,
442 repr(anchor-names),
443 name
444 )
445 )
446 }
447 }
448
449 if out == none {
450 if type(anchor) in (ratio, float, int) {
451 assert(path-anchors, message: strfmt("Element '{}' does not support path anchors.", name))
452 out = path-util.point-on-path(path.segments, anchor)
453 } else if type(anchor) == angle {
454 assert(border-anchors, message: strfmt("Element '{}' does not support border anchors.", name))
455 out = border(callback("center"), ..radii, path, anchor)
456 assert(out != none, message: strfmt("Element '{}' does not have a border for anchor '{}'.", name, anchor))
457 } else {
458 panic(strfmt("Unknown anchor '{}' for element '{}'", repr(anchor), name))
459 }
460 }
461
462 return if transform != none {
463 util.apply-transform(
464 transform,
465 out
466 )
467 } else {
468 out
469 }
470 }
471
472 if default != none and offset-anchor != none {
473 let offset = matrix.transform-translate(
474 ..vector.sub(calculate-anchor(default), calculate-anchor(offset-anchor)).slice(0, 3)
475 )
476 transform = if transform != none {
477 matrix.mul-mat(
478 transform,
479 offset
480 )
481 } else {
482 offset
483 }
484 }
485
486 return (if transform == none { matrix.ident() } else { transform }, calculate-anchor.with(transform: transform))
487}
488
489
490// This file contains functions related to bezier curve calculation
491// Many functions are ports from https://github.com/Pomax/bezierjs
492#import "vector.typ"
493
494// Map number v from range (ds, de) to (ts, te)
495#let _map(v, ds, de, ts, te) = {
496 let d1 = de - ds
497 let d2 = te - ts
498 let v2 = v - ds
499 let r = v2 / d1
500 return ts + d2 * r
501}
502
503/// Get the point on quadratic bezier at position `t`.
504///
505/// - a (vector): Start point
506/// - b (vector): End point
507/// - c (vector): Control point
508/// - t (float): Position on curve $[0, 1]$
509/// -> vector
510#let quadratic-point(a, b, c, t) = {
511 // (1-t)^2 * a + 2 * (1-t) * t * c + t^2 b
512 return vector.add(
513 vector.add(
514 vector.scale(a, calc.pow(1-t, 2)),
515 vector.scale(c, 2 * (1-t) * t)
516 ),
517 vector.scale(b, calc.pow(t, 2))
518 )
519}
520
521/// Get the derivative (dx/dt) of a quadratic bezier at position `t`.
522///
523/// - a (vector): Start point
524/// - b (vector): End point
525/// - c (vector): Control point
526/// - t (float): Position on curve [0, 1]
527/// -> vector
528#let quadratic-derivative(a, b, c, t) = {
529 // 2(-a(1-t) + bt - 2ct + c)
530 return vector.scale(
531 vector.add(
532 vector.sub(
533 vector.add(
534 vector.scale(vector.neg(a), (1 - t)),
535 vector.scale(b, t)),
536 vector.scale(c, 2 * t)),
537 c)
538 , 2)
539}
540
541/// Get the point on a cubic bezier curve at position `t`.
542///
543/// - a (vector): Start point
544/// - b (vector): End point
545/// - c1 (vector): Control point 1
546/// - c2 (vector): Control point 2
547/// - t (float): Position on curve [0, 1]
548/// -> vector
549#let cubic-point(a, b, c1, c2, t) = {
550 // (1-t)^3*a + 3*(1-t)^2*t*c1 + 3*(1-t)*t^2*c2 + t^3*b
551 vector.add(
552 vector.add(
553 vector.scale(a, calc.pow(1-t, 3)),
554 vector.scale(c1, 3 * calc.pow(1-t, 2) * t)
555 ),
556 vector.add(
557 vector.scale(c2, 3*(1-t)*calc.pow(t,2)),
558 vector.scale(b, calc.pow(t, 3))
559 )
560 )
561}
562
563/// Get the derivative (dx/dt) of a cubic bezier at position `t`.
564///
565/// - a (vector): Start point
566/// - b (vector): End point
567/// - c1 (vector): Control point 1
568/// - c2 (vector): Control point 2
569/// - t (float): Position on curve [0, 1]
570/// -> vector
571#let cubic-derivative(a, b, c1, c2, t) = {
572 // -3(a(1-t)^2 + t(-2c2 - bt + 3 c2 t) + c1(-1 + 4t - 3t^2))
573 vector.scale(
574 vector.add(
575 vector.add(
576 vector.scale(a, calc.pow((1 - t), 2)),
577 vector.scale(
578 vector.sub(
579 vector.add(
580 vector.scale(b, -1 * t),
581 vector.scale(c2, 3 * t)
582 ),
583 vector.scale(c2, 2)
584 ),
585 t
586 )
587 ),
588 vector.scale(c1, -3 * calc.pow(t, 2) + 4 * t - 1)
589 ),
590 -3
591 )
592}
593
594/// Get a bezier curve's ABC coordinates. Returns them as a respective <Type>array</Type> of <Type>vector</Type>s.
595/// ```
596/// /A\ <-- Control point of quadratic bezier
597/// / | \
598/// / | \
599/// /_.-B-._\ <-- Point on curve
600/// ,' | ',
601/// / | \
602/// s------C------e <-- Point on line between s and e
603/// ```
604/// - s (vector): Curve start
605/// - e (vector): Curve end
606/// - B (vector): Point on curve
607/// - t (float): Position on curve $[0, 1]$
608/// - deg (int): Bezier degree (2 or 3)
609/// -> array
610#let to-abc(s, e, B, t, deg: 2) = {
611 let abc-ratio(t) = {
612 if t == 0 or t == 1 { return t }
613 let bottom = calc.pow(t, deg) + calc.pow(1 - t, deg)
614 let top = bottom - 1
615 return calc.abs(top / bottom)
616 }
617
618 let projection-ratio(t) = {
619 if t == 0 or t == 1 { return t }
620 let top = calc.pow(1 - t, deg)
621 let bottom = calc.pow(t, deg) + top
622 return top / bottom
623 }
624
625 let u = projection-ratio(t)
626 let um = 1 - u
627
628 let C = vector.add(
629 vector.scale(s, u),
630 vector.scale(e, um))
631
632 let s = abc-ratio(t)
633 let A = vector.add(
634 B,
635 vector.scale(vector.sub(B, C), 1/s))
636
637 return (A, B, C)
638}
639
640
641/// Compute the control points for a quadratic bezier through 3 points.
642///
643/// - s (vector): Curve start
644/// - e (vector): Curve end
645/// - B (vector): A point which the curve passes through
646/// -> bezier
647#let quadratic-through-3points(s, B, e) = {
648 let d1 = vector.dist(s, B)
649 let d2 = vector.dist(e, B)
650 let t = d1 / (d1 + d2)
651
652 let (A, B, C) = to-abc(s, e, B, t, deg: 2)
653
654 return (s, e, A)
655}
656
657
658/// Convert a quadratic bezier to a cubic bezier.
659///
660/// - s (vector): Curve start
661/// - e (vector): Curve end
662/// - c (vector): Control point
663/// -> bezier
664#let quadratic-to-cubic(s, e, c) = {
665 let c1 = vector.add(s, vector.scale(vector.sub(c, s), 2/3))
666 let c2 = vector.add(e, vector.scale(vector.sub(c, e), 2/3))
667 return (s, e, c1, c2)
668}
669
670/// Compute the control points for a cubic bezier through 3 points.
671///
672/// - s (vector): Curve start
673/// - e (vector): Curve end
674/// - B (vector): A point which the curve passes through
675/// -> bezier
676#let cubic-through-3points(s, B, e) = {
677 if s == B or e == B {
678 return (s, e, s, e)
679 }
680
681 let d1 = vector.dist(s, B)
682 let d2 = vector.dist(e, B)
683 let t = d1 / (d1 + d2)
684
685 let (A, _, C) = to-abc(s, e, B, t, deg: 3)
686
687 let angle = vector.angle2(s, e) - vector.angle2(s, B)
688 if angle == 0deg or calc.abs(angle) == 180deg {
689 let se = vector.dist(s, e)
690 let sB = vector.dist(s, B)
691 let eB = vector.dist(e, B)
692
693 if sB >= se and sB >= eB {
694 return (s, B, s, B)
695 } else if eB >= se and eB >= sB {
696 return (e, B, e, B)
697 }
698 return (s, e, s, e)
699 }
700
701 let bc = (if angle < 0deg or angle > 180deg { -1 } else { 1 }) * vector.dist(s, e) / 3
702 let de1 = t * bc
703 let de2 = (1 - t) * bc
704
705 let (ax, ay, az) = A
706 let (sx, sy, sz) = s
707 let (ex, ey, ez) = e
708 let (bx, by, bz) = B
709
710 import "/src/util.typ": calculate-circle-center-3pt
711 let (cx, cy, cz) = calculate-circle-center-3pt(s, B, e)
712 let tangent = (
713 (bx - (by - cy), by + (bx - cx), bz),
714 (bx + (by - cy), by - (bx - cx), bz))
715 let (dx, dy, dz) = vector.norm(vector.sub(tangent.at(1), tangent.at(0)))
716
717 let (e1x, e1y) = (
718 bx + de1 * dx,
719 by + de1 * dy)
720 let (e2x, e2y) = (
721 bx - de2 * dx,
722 by - de2 * dy)
723 let (v1x, v1y) = (
724 ax + (e1x - ax) / (1 - t),
725 ay + (e1y - ay) / (1 - t))
726 let (v2x, v2y) = (
727 ax + (e2x - ax) / t,
728 ay + (e2y - ay) / t)
729 let c1 = (
730 sx + (v1x - sx) / t,
731 sy + (v1y - sy) / t)
732 let c2 = (
733 ex + (v2x - ex) / (1 - t),
734 ey + (v2y - ey) / (1 - t))
735
736 return (s, e, c1, c2)
737}
738
739/// Split a cubic bezier into two cubic beziers at the point `t`. Returns an <Type>array</Type> of two <Type>bezier</Type>. The first holds the original curve start `s`, and the second holds the original curve end `e`.
740///
741/// - s (vector): Curve start
742/// - e (vector): Curve end
743/// - c1 (vector): Control point 1
744/// - c2 (vector): Control point 2
745/// - t (float): The point on the bezier to split, $[0, 1]$
746/// -> array
747#let split(s, e, c1, c2, t) = {
748 t = calc.max(0, calc.min(t, 1))
749
750 let split-rec(pts, t, left, right) = {
751 if pts.len() == 1 {
752 left.push(pts.at(0))
753 right.push(pts.at(0))
754 } else {
755 let new-pts = ()
756 for i in range(0, pts.len() - 1) {
757 if i == 0 {
758 left.push(pts.at(i))
759 }
760 if i == pts.len() - 2 {
761 right.push(pts.at(i + 1))
762 }
763 new-pts.push(vector.add(vector.scale(pts.at(i), (1 - t)),
764 vector.scale(pts.at(i + 1), t)))
765 }
766 (left, right) = split-rec(new-pts, t, left, right)
767 }
768 return (left, right)
769 }
770 let (left, right) = split-rec((s, c1, c2, e), t, (), ())
771
772 return ((left.at(0), left.at(3), left.at(1), left.at(2)),
773 (right.at(3), right.at(0), right.at(2), right.at(1)))
774}
775
776/// Get the approximate cubic curve length
777/// - s (vector): Curve start
778/// - e (vector): Curve end
779/// - c1 (vector): Control point 1
780/// - c2 (vector): Control point 2
781/// -> float
782#let cubic-arclen(s, e, c1, c2, samples: 10) = {
783 let d = 0
784 let last = none
785 for t in range(0, samples + 1) {
786 let pt = cubic-point(s, e, c1, c2, t / samples)
787 if last != none {
788 d += vector.dist(last, pt)
789 }
790 last = pt
791 }
792 return d
793}
794
795/// Shorten the curve by offsetting s and c1 or e and c2 by distance d. If d is positive the curve gets shortened by moving s and c1 closer to e, if d is negative, e and c2 get moved closer to s.
796///
797/// - s (vector): Curve start
798/// - e (vector): Curve end
799/// - c1 (vector): Control point 1
800/// - c2 (vector): Control point 2
801/// - d (float): Distance to shorten by
802/// -> bezier
803#let cubic-shorten-linear(s, e, c1, c2, d) = {
804 if d == 0 { return (s, e, c1, c2) }
805
806 let t = if d < 0 { 1 } else { 0 }
807 let sign = if d < 0 { -1 } else { 1 }
808
809 let a = cubic-point(s, e, c1, c2, t)
810 let b = cubic-point(s, e, c1, c2, t + sign * 0.01)
811 let offset = vector.scale(vector.norm(vector.sub(b, a)),
812 calc.abs(d))
813 if d > 0 {
814 s = vector.add(s, offset)
815 c1 = vector.add(c1, offset)
816 } else {
817 e = vector.add(e, offset)
818 c2 = vector.add(c2, offset)
819 }
820 return (s, e, c1, c2)
821}
822
823/// Approximate bezier interval `t` for a given distance `d`. If `d` is positive, the functions starts from the curve's start `s`, if `d` is negative, it starts form the curve's end `e`.
824/// - s (vector): Curve start
825/// - e (vector): Curve end
826/// - c1 (vector): Control point 1
827/// - c2 (vector): Control point 2
828/// - d (float): The distance along the bezier to find `t`.
829/// -> float
830#let cubic-t-for-distance(s, e, c1, c2, d, samples: 20) = {
831 let travel-forwards(s, e, c1, c2, d) = {
832 let sum = 0
833 for n in range(1, samples + 1) {
834 let t0 = (n - 1) / samples
835 let t1 = n / samples
836
837 let segment-dist = vector.dist(cubic-point(s, e, c1, c2, t0),
838 cubic-point(s, e, c1, c2, t1))
839 if sum <= d and d <= sum + segment-dist {
840 return t0 + (d - sum) / segment-dist / samples
841 }
842 sum += segment-dist
843 }
844 return 1
845 }
846
847 if d == 0 {
848 return 0
849 }
850
851 if d > 0 {
852 return travel-forwards(s, e, c1, c2, d)
853 } else {
854 return 1 - travel-forwards(e, s, c2, c1, -d)
855 }
856}
857
858/// Shorten curve by distance `d`. This keeps the curvature of the curve by finding new values along the original curve. If `d` is positive the curve gets shortened by moving `s` closer to `e`, if `d` is negative, `e` is moved closer to `s`. The points `s` and `e` are moved along the curve, keeping the curve's curvature the same (the control points get recalculated).
859///
860/// - s (vector): Curve start
861/// - e (vector): Curve end
862/// - c1 (vector): Control point 1
863/// - c2 (vector): Control point 2
864/// - d (float): Distance to shorten by
865/// - samples (int): Maximum of samples/steps to use
866/// -> bezier
867#let cubic-shorten(s, e, c1, c2, d, samples: 15) = {
868 if d == 0 { return (s, e, c1, c2) }
869
870 let (left, right) = split(s, e, c1, c2, cubic-t-for-distance(s, e, c1, c2, d, samples: samples))
871 return if d > 0 {
872 right
873 } else {
874 left
875 }
876}
877
878/// Find cubic curve extrema by calculating the roots of the curve's first derivative. Returns an <Type>array</Type> of <Type>vector</Type> ordered by distance along the curve from the start to its end.
879/// - s (vector): Curve start
880/// - e (vector): Curve end
881/// - c1 (vector): Control point 1
882/// - c2 (vector): Control point 2
883/// -> array
884#let cubic-extrema(s, e, c1, c2) = {
885 // Compute roots of a single dimension (x, y, z) of the
886 // curve by using the abc formula for finding roots of
887 // the curves first derivative.
888 let dim-extrema(a, b, c1, c2) = {
889 let f0 = calc.round(3*(c1 - a), digits: 8)
890 let f1 = calc.round(6*(c2 - 2*c1 + a), digits: 8)
891 let f2 = calc.round(3*(b - 3*c2 + 3*c1 - a), digits: 8)
892
893 if f1 == 0 and f2 == 0 {
894 return ()
895 }
896
897 // Linear function
898 if f2 == 0 {
899 return (-f0 / f1,)
900 }
901
902 // No real roots
903 let discriminant = f1*f1 - 4*f0*f2
904 if discriminant < 0 {
905 return ()
906 }
907
908 if discriminant == 0 {
909 return (-f1 / (2*f2),)
910 }
911
912 return ((-f1 - calc.sqrt(discriminant)) / (2*f2),
913 (-f1 + calc.sqrt(discriminant)) / (2*f2))
914 }
915
916 let pts = ()
917 let dims = calc.max(s.len(), e.len())
918 for dim in range(dims) {
919 let ts = dim-extrema(
920 s.at(dim, default: 0),
921 e.at(dim, default: 0),
922 c1.at(dim, default: 0),
923 c2.at(dim, default: 0)
924 )
925 for t in ts {
926 // Discard any root outside the bezier range
927 if t >= 0 and t <= 1 {
928 pts.push(cubic-point(s, e, c1, c2, t))
929 }
930 }
931 }
932 return pts
933}
934
935/// Returns axis aligned bounding box coordinates `(bottom-left, top-right)` for a cubic bezier curve.
936///
937/// - s (vector): Curve start
938/// - e (vector): Curve end
939/// - c1 (vector): Control point 1
940/// - c2 (vector): Control point 2
941/// -> array
942#let cubic-aabb(s, e, c1, c2) = {
943 let (lo, hi) = (s, e)
944 for dim in range(lo.len()) {
945 if lo.at(dim) > hi.at(dim) {
946 (lo.at(dim), hi.at(dim)) = (hi.at(dim), lo.at(dim))
947 }
948 }
949 for pt in cubic-extrema(s, e, c1, c2) {
950 for dim in range(pt.len()) {
951 lo.at(dim) = calc.min(lo.at(dim), hi.at(dim), pt.at(dim))
952 hi.at(dim) = calc.max(lo.at(dim), hi.at(dim), pt.at(dim))
953 }
954 }
955 return (lo, hi)
956}
957
958/// Returns a cubic bezier between points `p2` and `p3` for a catmull-rom curve through all four points.
959///
960/// - p1 (vector): Point 1
961/// - p2 (vector): Point 2
962/// - p3 (vector): Point 3
963/// - p4 (vector): Point 4
964/// - k (float): The tension of the catmull-rom curve. Must be in the range $[0, 1]$
965/// -> bezier
966#let _catmull-section-to-cubic(p1, p2, p3, p4, k) = {
967 return (p2, p3,
968 vector.add(p2, vector.scale(vector.sub(p3, p1), 1/(k * 6))),
969 vector.sub(p3, vector.scale(vector.sub(p4, p2), 1/(k * 6))))
970}
971
972/// Returns an array of cubic <Type>bezier</Type> for a catmull curve through an array of points.
973///
974/// - points (array): Array of 2d points
975/// - k (float): Strength between 0 and 1
976/// - close (bool):
977/// -> array
978#let catmull-to-cubic(points, k, close: false) = {
979 k = calc.max(k, 0.1)
980 k = if k < .5 {
981 1 / _map(k, .5, 0, 1, 10)
982 } else {
983 _map(k, .5, 1, 1, 10)
984 }
985
986 let len = points.len()
987 if len == 2 {
988 return ((points.at(0), points.at(1),
989 points.at(0), points.at(1)),)
990 } else if len > 2 {
991 let curves = ()
992
993 let (i0, iN) = if close {
994 (-1, 0)
995 } else {
996 (0, -1)
997 }
998
999 curves.push(_catmull-section-to-cubic(points.at(i0), points.at(0),
1000 points.at(1), points.at(2), k))
1001 for i in range(1, len - 2, step: 1) {
1002 curves.push(_catmull-section-to-cubic(
1003 ..range(i - 1, i + 3).map(i => points.at(i)), k))
1004 }
1005
1006 curves.push(_catmull-section-to-cubic(
1007 points.at(-3), points.at(-2), points.at(-1), points.at(iN), k))
1008
1009 if close {
1010 curves.push(_catmull-section-to-cubic(
1011 points.at(-2), points.at(-1), points.at(0), points.at(1), k))
1012 }
1013
1014 return curves
1015 }
1016 return ()
1017}
1018
1019/// Find the roots of a cubic polynomial with the coefficients a, b, c and d.
1020///
1021/// -> array Array of roots
1022#let _cubic-roots(a, b, c, d) = {
1023 let epsilon = 0.000001
1024 if calc.abs(a) < 1e-6 {
1025 if calc.abs(b) < 1e-6 {
1026 // Constant
1027 if c == 0 {
1028 return ()
1029 }
1030
1031 // Linear
1032 let root = -1 * d / c
1033 if root < 0 - epsilon and root > 1 + epsilon {
1034 return ()
1035 }
1036 return (root,)
1037 }
1038
1039 // Quadratic
1040 let dq = calc.pow(c, 2) - 4 * b * d
1041 if dq >= 0 {
1042 dq = calc.sqrt(dq)
1043 let roots = (-1 * (dq + c) / (2 * b),
1044 (dq - c) / (2 * b))
1045 return roots.filter(t => t >= 0 - epsilon and t <= 1 + epsilon)
1046 }
1047 }
1048
1049 let (A, B, C) = (b/a, c/a, d/a)
1050 let Q = (3 * B - calc.pow(A, 2)) / 9
1051 let R = (9 * A * B - 27 * C - 2 * calc.pow(A, 3)) / 54
1052 let D = calc.pow(Q, 3) + calc.pow(R, 2)
1053 let aa = -A / 3
1054
1055 let sgn = x => { if x < 0 { -1 } else { 1 } }
1056 let roots = if D >= 0 {
1057 let S = sgn(R + calc.sqrt(D)) * calc.pow(calc.abs(R + calc.sqrt(D)), 1/3)
1058 let T = sgn(R - calc.sqrt(D)) * calc.pow(calc.abs(R - calc.sqrt(D)), 1/3)
1059
1060 if (S - T) != 0 {
1061 // Roots 2 and 3 are complex
1062 (aa + (S + T),)
1063 } else {
1064 (aa + (S + T), aa - (S + T) / 2)
1065 }
1066 } else {
1067 let th = calc.acos(R / calc.sqrt(-calc.pow(Q, 3))) / 1rad
1068 let qq = 2 * calc.sqrt(-Q)
1069 (qq * calc.cos(th / 3) + aa,
1070 qq * calc.cos((th + 2 * calc.pi) / 3) + aa,
1071 qq * calc.cos((th + 4 * calc.pi) / 3) + aa)
1072 }
1073
1074 return roots.filter(t => t >= 0 - epsilon and t <= 1 + epsilon)
1075}
1076
1077/// Calculate the intersection points between a 2D cubic-bezier and a straight line. Returns an array of <Type>vector</Type>
1078///
1079/// - s (vector): Bezier start point
1080/// - e (vector): Bezier end point
1081/// - c1 (vector): Bezier control point 1
1082/// - c2 (vector): Bezier control point 2
1083/// - la (vector): Line start point
1084/// - lb (vector): Line end point
1085/// - ray (bool): If set to true, ignore line length
1086/// -> array
1087#let line-cubic-intersections(la, lb, s, e, c1, c2, ray: false) = {
1088 // Based on:
1089 // http://www.particleincell.com/blog/2013/cubic-line-intersection/
1090 // with some rounding improvements
1091 let a = lb.at(1) - la.at(1)
1092 let b = la.at(0) - lb.at(0)
1093 let c = la.at(0) * (la.at(1) - lb.at(1)) + la.at(1) * (lb.at(0) - la.at(0))
1094
1095 /// Get cubic bezier function coefficients
1096 let _cubic-coeff(a, b, c, d) = (
1097 -a + 3*b - 3*c + d,
1098 3*a - 6*b + 3*c,
1099 -3*a +3*b,
1100 a)
1101
1102 let x-coeff = _cubic-coeff(s.at(0), c1.at(0), c2.at(0), e.at(0))
1103 let y-coeff = _cubic-coeff(s.at(1), c1.at(1), c2.at(1), e.at(1))
1104
1105 let roots = _cubic-roots(a * x-coeff.at(0) + b * y-coeff.at(0),
1106 a * x-coeff.at(1) + b * y-coeff.at(1),
1107 a * x-coeff.at(2) + b * y-coeff.at(2),
1108 a * x-coeff.at(3) + b * y-coeff.at(3) + c)
1109
1110 let pts = ()
1111 for t in roots {
1112 let pt = cubic-point(s, e, c1, c2, t)
1113 if ray {
1114 pts.push(pt)
1115 } else {
1116 let s = if calc.abs(lb.at(0) - la.at(0)) >= 1e-6 {
1117 (pt.at(0) - la.at(0)) / (lb.at(0) - la.at(0))
1118 } else {
1119 (pt.at(1) - la.at(1)) / (lb.at(1) - la.at(1))
1120 }
1121 if s >= 0 and s <= 1 {
1122 pts.push(pt)
1123 }
1124 }
1125 }
1126 return pts
1127}
1128#import "matrix.typ"
1129#import "vector.typ"
1130#import "util.typ"
1131#import "path-util.typ"
1132#import "aabb.typ"
1133#import "styles.typ"
1134#import "process.typ"
1135#import "version.typ"
1136
1137/// Sets up a canvas for drawing on.
1138///
1139/// - length (length, ratio): Used to specify what 1 coordinate unit is. If given a ratio, that ratio is relative to the containing elements width!
1140/// - body (none, array, element): A code block in which functions from the `draw` module have been called.
1141/// - background (none, color): A color to be used for the background of the canvas.
1142/// - padding (none, number, array, dictionary) = none: How much padding to add to the canvas. `none` applies no padding. A number applies padding to all sides equally. A dictionary applies padding following Typst's `pad` function: https://typst.app/docs/reference/layout/pad/. An array follows CSS like padding: `(y, x)`, `(top, x, bottom)` or `(top, right, bottom, left)`.
1143/// - debug (bool): Shows the bounding boxes of each element when `true`.
1144/// -> content
1145#let canvas(length: 1cm, debug: false, background: none, padding: none, body) = context { layout(ly => {
1146 if body == none {
1147 return []
1148 }
1149 assert(
1150 type(body) == array,
1151 message: "Incorrect type for body: " + repr(type(body)),
1152 )
1153
1154 assert(type(length) in (std.length, ratio), message: "Expected `length` to be of type length or ratio, got " + repr(length))
1155 let length = if type(length) == ratio {
1156 length * ly.width
1157 } else {
1158 length.to-absolute()
1159 }
1160 assert(length / 1cm != 0,
1161 message: "Canvas length must be != 0!")
1162
1163 let ctx = (
1164 version: version.version,
1165 length: length,
1166 debug: debug,
1167 // Previous element position & bbox
1168 prev: (pt: (0, 0, 0)),
1169 style: styles.default,
1170 // Current transformation matrix, a rhs coordinate system
1171 // where z is sheared by a half x and y.
1172 // +x = right, +y = up, +z = 1/2 (left + down)
1173 transform:
1174 ((1, 0,-.5, 0),
1175 (0,-1,+.5, 0),
1176 (0, 0, .0, 0),
1177 (0, 0, .0, 1)),
1178 // Nodes, stores anchors and paths
1179 nodes: (:),
1180 // group stack
1181 groups: (),
1182 // user defined marks
1183 marks: (
1184 mnemonics: (:),
1185 marks: (:),
1186 )
1187 )
1188
1189 let (ctx, bounds, drawables) = process.many(ctx, body)
1190 if bounds == none {
1191 return []
1192 }
1193
1194 // Filter hidden drawables
1195 drawables = drawables.filter(d => not d.hidden)
1196
1197 // Order draw commands by z-index
1198 drawables = drawables.sorted(key: (cmd) => {
1199 return cmd.at("z-index", default: 0)
1200 })
1201
1202 // Apply padding
1203 let padding = util.as-padding-dict(padding)
1204 bounds = aabb.padded(bounds, padding)
1205
1206 // Final canvas size
1207 let (width, height, ..) = vector.scale(aabb.size(bounds), length)
1208
1209 let relative = (orig, c) => {
1210 return vector.sub(c, orig)
1211 }
1212
1213 box(width: width, height: height, fill: background, align(top, {
1214 for drawable in drawables {
1215 // Typst path elements have strange bounding boxes. We need to
1216 // offset all paths to start at (0, 0) to make gradients work.
1217 let (x, y, _) = if drawable.type == "path" {
1218 vector.sub(
1219 aabb.aabb(path-util.bounds(drawable.segments)).low,
1220 bounds.low)
1221 } else {
1222 (0, 0, 0)
1223 }
1224
1225 place(top + left, float: false, if drawable.type == "path" {
1226 let vertices = ()
1227 for ((kind, ..pts)) in drawable.segments {
1228 pts = pts.map(c => {
1229 ((c.at(0) - bounds.low.at(0) - x) * length,
1230 (c.at(1) - bounds.low.at(1) - y) * length)
1231 })
1232 assert(
1233 kind in ("line", "cubic"),
1234 message: "Path segments must be of type line, cubic")
1235
1236 if kind == "cubic" {
1237 let a = pts.at(0)
1238 let b = pts.at(1)
1239 let ctrla = relative(a, pts.at(2))
1240 let ctrlb = relative(b, pts.at(3))
1241
1242 vertices.push((a, (0pt, 0pt), ctrla))
1243 vertices.push((b, ctrlb, (0pt, 0pt)))
1244 } else {
1245 vertices += pts
1246 }
1247 }
1248 if type(drawable.stroke) == dictionary and "thickness" in drawable.stroke and type(drawable.stroke.thickness) != std.length {
1249 drawable.stroke.thickness *= length
1250 }
1251 path(
1252 stroke: drawable.stroke,
1253 fill: drawable.fill,
1254 fill-rule: drawable.at("fill-rule", default: "non-zero"),
1255 closed: drawable.at("close", default: false),
1256 ..vertices,
1257 )
1258 } else if drawable.type == "content" {
1259 let (width, height) = std.measure(drawable.body)
1260 move(
1261 dx: (drawable.pos.at(0) - bounds.low.at(0)) * length - width / 2,
1262 dy: (drawable.pos.at(1) - bounds.low.at(1)) * length - height / 2,
1263 drawable.body,
1264 )
1265 }, dx: x * length, dy: y * length)
1266 }
1267 }))
1268})}
1269/// Returns the real part of a complex number.
1270/// - V (complex): A complex number.
1271/// -> float
1272#let re(V) = V.at(0)
1273
1274/// Returns the imaginary part of a complex number.
1275/// - V (complex): A complex number.
1276/// -> float
1277#let im(V) = V.at(1)
1278
1279
1280/// Multiplies two complex numbers together and returns the result $V W$.
1281/// - V (complex): The complex number on the left hand side.
1282/// - W (complex): The complex number on the right hand side.
1283#let mul(V, W) = (re(V) * re(W) - im(V) * im(W), im(V) * re(W) + re(V) * im(W))
1284
1285/// Calculates the conjugate of a complex number.
1286/// - V (complex): A complex number.
1287/// -> complex
1288#let conj(V) = (re(V),-im(V))
1289
1290// TODO: check what "in R^2" means.
1291/// Calculates the dot product of two complex numbers in R^2 $V \cdot W$.
1292/// - V (complex): The complex number on the left hand side.
1293/// - W (complex): The complex number on the right hand side.
1294/// -> float
1295#let dot(V,W) = re(mul(V,conj(W)))
1296
1297/// Calculates the squared normal of a complex number.
1298/// - V (complex): The complex number.
1299/// -> float
1300#let normsq(V) = dot(V,V)
1301
1302
1303/// Calculates the normal of a complex number
1304/// - V (complex): The complex number.
1305/// -> float
1306#let norm(V) = calc.sqrt(normsq(V))
1307
1308/// Multiplies a complex number by a scale factor.
1309/// - V (complex): The complex number to scale.
1310/// - t (float): The scale factor.
1311/// -> complex
1312#let scale(V,t) = mul(V,(t,0))
1313
1314/// Returns a unit vector in the direction of a complex number.
1315/// - V (complex): The complex number.
1316/// -> vector
1317#let unit(V) = scale(V, 1/norm(V))
1318
1319/// Inverts a complex number.
1320/// - V (complex): The complex number
1321/// -> complex
1322#let inv(V) = scale(conj(V), 1/normsq(V))
1323
1324/// Divides two complex numbers.
1325/// - V (complex): The complex number of the numerator.
1326/// - W (complex): The complex number of the denominator.
1327/// -> complex
1328#let div(V,W) = mul(V,inv(W))
1329
1330/// Adds two complex numbers together.
1331/// - V (complex): The complex number on the left hand side.
1332/// - W (complex): The complex number on the right hand side.
1333/// -> complex
1334#let add(V,W) = (re(V) + re(W),im(V) + im(W))
1335
1336/// Subtracts two complex numbers together.
1337/// - V (complex): The complex number on the left hand side.
1338/// - W (complex): The complex number on the right hand side.
1339/// -> complex
1340#let sub(V,W) = (re(V) - re(W),im(V) - im(W))
1341
1342/// Calculates the argument of a complex number.
1343/// - V (complex): The complex number.
1344#let arg(V) = calc.atan2(..V) / 1rad
1345
1346/// Get the signed angle of two complex numbers from V to W.
1347/// - V (complex): A complex number.
1348/// - W (complex): A complex number.
1349#let ang(V,W) = arg(div(W,V))
1350
1351// exp(i*a)
1352#let expi(a) = (calc.cos(a),calc.sin(a))
1353
1354// Rotate by angle a
1355#let rot(v,a) = mul(v,expi(a))
1356#import "vector.typ"
1357#import "util.typ"
1358#import "deps.typ"
1359#import deps.oxifmt: strfmt
1360
1361#let resolve-xyz(c) = {
1362 // (x: <number> or <none>, y: <number> or <none>, z: <number> or <none>)
1363 // (x, y)
1364 // (x, y, z)
1365
1366 return if type(c) == array {
1367 vector.as-vec(c)
1368 } else {
1369 (
1370 c.at("x", default: 0),
1371 c.at("y", default: 0),
1372 c.at("z", default: 0),
1373 )
1374 }
1375}
1376
1377
1378#let resolve-polar(c) = {
1379 // (angle: <angle>, radius: <number>)
1380 // (angle: <angle>, radius: (x, y))
1381 // (angle, radius)
1382 // (angle, (x-radius, y-radius))
1383
1384 let (angle, xr, yr) = if type(c) == array {
1385 (
1386 c.first(),
1387 ..if type(c.last()) == array {
1388 c.last()
1389 } else {
1390 (c.last(), c.last())
1391 }
1392 )
1393 } else {
1394 (
1395 c.angle,
1396 ..if type(c.radius) == array {
1397 c.radius
1398 } else {
1399 (c.radius, c.radius)
1400 }
1401 )
1402 }
1403 return (
1404 xr * calc.cos(angle),
1405 yr * calc.sin(angle),
1406 0
1407 )
1408}
1409
1410
1411#let resolve-anchor(ctx, c) = {
1412 // (name: <string>, anchor: <number, angle, string> or <none>)
1413 // "name.anchor"
1414 // "name"
1415 let (name, anchor) = if type(c) == str {
1416 let (name, ..anchor) = c.split(".")
1417 if anchor.len() == 0 {
1418 anchor = "default"
1419 }
1420 (name, anchor)
1421 } else {
1422 (c.name, c.at("anchor", default: "default"))
1423 }
1424
1425 // Check if node is known
1426 assert(name in ctx.nodes,
1427 message: "Unknown element '" + name + "' in elements " + repr(ctx.nodes.keys()))
1428
1429 // Resolve length anchors
1430 if type(anchor) == length {
1431 anchor = util.resolve-number(ctx, anchor)
1432 }
1433
1434 // Check if anchor is known
1435 let node = ctx.nodes.at(name)
1436 let pos = (node.anchors)(anchor)
1437
1438 let pos = util.revert-transform(
1439 ctx.transform,
1440 pos
1441 )
1442
1443 return pos
1444}
1445
1446#let resolve-barycentric(ctx, c) = {
1447 // dictionary of numbers
1448 return vector.scale(
1449 c.bary.pairs().fold(
1450 vector.new(3),
1451 (vec, (k, v)) => {
1452 vector.add(
1453 vec,
1454 vector.scale(
1455 resolve-anchor(ctx, k),
1456 v
1457 )
1458 )
1459 }
1460 ),
1461 1 / c.bary.values().sum()
1462 )
1463}
1464
1465#let resolve-relative(resolve, ctx, c) = {
1466 // (rel: <coordinate>, update: <bool> or <none>, to: <coordinate>)
1467 let update = c.at("update", default: true)
1468 let (ctx, rel) = resolve(ctx, c.rel, update: false)
1469 let (ctx, to) = if "to" in c {
1470 resolve(ctx, c.to, update: false)
1471 } else {
1472 (ctx, ctx.prev.pt)
1473 }
1474 c = vector.add(
1475 rel,
1476 to,
1477 )
1478 c.insert(0, update)
1479 return c
1480}
1481
1482#let resolve-tangent(resolve, ctx, c) = {
1483 // (element: <string>, point: <coordinate>, solution: <integer>)
1484
1485 // https://stackoverflow.com/a/69641745/7142815
1486 let C = resolve-anchor(ctx, c.element)
1487 let (ctx, P) = resolve(ctx, c.point, update: false)
1488 // Radius
1489 let r = vector.len(vector.sub(resolve-anchor(ctx, c.element + ".north"), C))
1490 // Vector between C and P
1491 let D = vector.sub(P, C) // C - P
1492 // Distance between C and P
1493 let pc = vector.len(D)
1494 if pc < r {
1495 panic("No tangent solution for element " + c.element + " and point " + repr(c.point))
1496 }
1497 // Distance between P and X0
1498 let d = r*r / pc
1499 // Distance between X0 and X1(X2)
1500 let h = calc.sqrt(r*r - d*d)
1501
1502 return if c.solution == 1 {
1503 (
1504 C.at(0) + (D.at(0) * d - D.at(1) * h) / pc,
1505 C.at(1) + (D.at(1) * d + D.at(0) * h) / pc,
1506 0
1507 )
1508 } else {
1509 (
1510 C.at(0) + (D.at(0) * d + D.at(1) * h) / pc,
1511 C.at(1) + (D.at(1) * d - D.at(0) * h) / pc,
1512 0
1513 )
1514 }
1515}
1516
1517#let resolve-perpendicular(resolve, ctx, c) = {
1518 // (horizontal: <coordinate>, vertical: <coordinate>)
1519 // (horizontal, "-|", vertical)
1520 // (vertical, "|-", horizontal)
1521
1522 let (ctx, horizontal, vertical) = resolve(ctx, ..if type(c) == array {
1523 if c.at(1) == "|-" {
1524 (c.first(), c.last())
1525 } else {
1526 // c.at(1) == "-|"
1527 (c.last(), c.first())
1528 }
1529 } else {
1530 (c.horizontal, c.vertical)
1531 }, update: false)
1532
1533 return (
1534 horizontal.at(0),
1535 vertical.at(1),
1536 0
1537 )
1538}
1539
1540#let resolve-lerp(resolve, ctx, c) = {
1541 // (a: <coordinate>, number: <number,ratio>,
1542 // angle?: <angle>, b: <coordinate>)
1543 // (a, <number, ratio>, b)
1544 // (a, <number, ratio>, angle, b)
1545
1546 let (a, number, angle, b) = if type(c) == array {
1547 if c.len() == 3 {
1548 (
1549 ..c.slice(0, 2),
1550 none, // angle
1551 c.last(),
1552 )
1553 } else {
1554 c
1555 }
1556 } else {
1557 (
1558 c.a,
1559 c.number,
1560 c.at("angle", default: 0deg),
1561 c.b
1562 )
1563 }
1564
1565 (ctx, a, b) = resolve(ctx, a, b)
1566
1567 if angle != none {
1568 let (x, y, _) = vector.sub(b,a)
1569 b = vector.add(
1570 (
1571 calc.cos(angle) * x - calc.sin(angle) * y,
1572 calc.sin(angle) * x + calc.cos(angle) * y,
1573 0
1574 ),
1575 a,
1576 )
1577 }
1578
1579 let ab = vector.sub(b, a)
1580
1581 let is-absolute = type(number) != ratio
1582 let distance = if is-absolute {
1583 let dist = vector.len(ab)
1584 if dist != 0 {
1585 util.resolve-number(ctx, number) / dist
1586 } else {
1587 0
1588 }
1589 } else {
1590 number / 100%
1591 }
1592
1593 return vector.add(a, vector.scale(ab, distance))
1594}
1595
1596#let resolve-function(resolve, ctx, c) = {
1597 let (func, ..c) = c
1598 (ctx, ..c) = resolve(ctx, ..c)
1599 func(..c)
1600}
1601
1602#let resolve-pos(ctx, c) = {
1603 // (name: str, pos: float, auto?: left|right, swap?: bool)
1604}
1605
1606/// Figures out what system a coordinate belongs to and returns the corresponding string.
1607/// - c (coordinate): The coordinate to find the system of.
1608/// -> str
1609#let resolve-system(c) = {
1610 let t = if type(c) == dictionary {
1611 let keys = c.keys()
1612 let len = c.len()
1613 if len in (1, 2, 3) and keys.all(k => k in ("x", "y", "z")) {
1614 "xyz"
1615 } else if len == 2 and keys.all(k => k in ("angle", "radius")) and (type(c.radius) in (int, float, length) or (type(c.radius) == array and c.radius.len() == 2)) {
1616 "polar"
1617 } else if len == 1 and keys == ("bary",) {
1618 "barycentric"
1619 } else if len in (1, 2) and keys.all(k => k in ("name", "anchor")) {
1620 "anchor"
1621 } else if len == 3 and keys.all(k => k in ("element", "point", "solution")) {
1622 "tangent"
1623 } else if len == 2 and keys.all(k => k in ("horizontal", "vertical")) {
1624 "perpendicular"
1625 } else if len in (1, 2, 3) and keys.all(k => k in ("rel", "to", "update")) {
1626 "relative"
1627 } else if len in (3, 4) and keys.all(k => k in ("a", "number", "angle", "abs", "b")) {
1628 "lerp"
1629 }
1630 } else if type(c) == array {
1631 let len = c.len()
1632 let types = c.map(type)
1633 if len == 0 {
1634 "previous"
1635 } else if len in (2, 3) and types.all(t => t in (int, float, length)) {
1636 "xyz"
1637 } else if len == 2 and types.first() == angle {
1638 "polar"
1639 } else if len == 3 and c.at(1) in ("-|", "|-") {
1640 "perpendicular"
1641 } else if len in (3, 4) and types.at(1) in (int, float, length, ratio) and (len == 3 or (len == 4 and types.at(2) == angle)) {
1642 "lerp"
1643 } else if len >= 2 and types.first() == function {
1644 "function"
1645 }
1646 } else if type(c) == str {
1647 if c.contains(".") {
1648 "anchor"
1649 } else {
1650 "element"
1651 }
1652 }
1653
1654 if t == none {
1655 panic("Failed to resolve coordinate: " + repr(c))
1656 }
1657 return t
1658}
1659
1660/// Resolve a list of coordinates to absolute vectors. Returns an array of the new <Type>context</Type> then the resolved coordinate vectors.
1661///
1662/// ```typc example
1663/// line((0,0), (1,1), name: "l")
1664/// get-ctx(ctx => {
1665/// // Get the vector of coordinate "l.start" and "l.end"
1666/// let (ctx, a, b) = cetz.coordinate.resolve(ctx, "l.start", "l.end")
1667/// content("l.start", [#a], frame: "rect", stroke: none, fill: white)
1668/// content("l.end", [#b], frame: "rect", stroke: none, fill: white)
1669/// })
1670/// ```
1671///
1672/// - ctx (context): Canvas context object
1673/// - ..coordinates (coordinate): List of coordinates
1674/// - update (bool): Update the context's last position
1675/// -> array
1676#let resolve(ctx, ..coordinates, update: true) = {
1677 let result = ()
1678 for c in coordinates.pos() {
1679 let t = resolve-system(c)
1680 let out = if t == "xyz" {
1681 resolve-xyz(c)
1682 } else if t == "previous" {
1683 ctx.prev.pt
1684 } else if t == "polar" {
1685 resolve-polar(c)
1686 } else if t == "barycentric" {
1687 resolve-barycentric(ctx, c)
1688 } else if t in ("element", "anchor") {
1689 resolve-anchor(ctx, c)
1690 } else if t == "tangent" {
1691 resolve-tangent(resolve, ctx, c)
1692 } else if t == "perpendicular" {
1693 resolve-perpendicular(resolve, ctx, c)
1694 } else if t == "relative" {
1695 (update, ..c) = resolve-relative(resolve, ctx, c)
1696 c
1697 } else if t == "lerp" {
1698 resolve-lerp(resolve, ctx, c)
1699 } else if t == "function" {
1700 resolve-function(resolve, ctx, c)
1701 } else {
1702 panic("Failed to resolve coordinate of format: " + repr(c))
1703 }.map(util.resolve-number.with(ctx))
1704
1705 if update {
1706 ctx.prev.pt = out
1707 }
1708 result.push(out)
1709 }
1710
1711 return (ctx, ..result)
1712}
1713#import "@preview/oxifmt:0.2.1"
1714#import "draw/grouping.typ": intersections, group, scope, anchor, copy-anchors, set-ctx, get-ctx, for-each-anchor, on-layer, hide, floating
1715#import "draw/transformations.typ": set-transform, rotate, translate, scale, set-origin, move-to, set-viewport
1716#import "draw/styling.typ": set-style, fill, stroke, register-mark
1717#import "draw/shapes.typ": circle, circle-through, arc, arc-through, mark, line, grid, content, rect, bezier, bezier-through, catmull, hobby, merge-path, polygon
1718#import "draw/projection.typ": ortho, on-xy, on-xz, on-yz
1719#import "draw/util.typ": assert-version
1720#import "/src/process.typ"
1721#import "/src/intersection.typ"
1722#import "/src/path-util.typ"
1723#import "/src/styles.typ"
1724#import "/src/drawable.typ"
1725#import "/src/vector.typ"
1726#import "/src/util.typ"
1727#import "/src/coordinate.typ"
1728#import "/src/aabb.typ"
1729#import "/src/anchor.typ" as anchor_
1730#import "/src/matrix.typ"
1731#import "/src/deps.typ"
1732#import deps.oxifmt: strfmt
1733
1734#import "transformations.typ": move-to
1735
1736/// Hides an element.
1737///
1738/// Hidden elements are not drawn to the canvas, are ignored when calculating bounding boxes and discarded by [merge-path](../shapes/merge-path). All other behaviours remain the same as a non-hidden element.
1739///
1740/// ```typc example
1741/// set-style(radius: .5)
1742/// intersections("i", {
1743/// circle((0,0), name: "a")
1744/// circle((1,2), name: "b")
1745/// // Use a hidden line to find the border intersections
1746/// hide(line("a.center", "b.center"))
1747/// })
1748/// line("i.0", "i.1")
1749/// ```
1750///
1751/// - body (element): One or more elements to hide
1752/// - bounds (bool): If true, respect the bounding box of the hidden elements for resizing the canvas
1753#let hide(body, bounds: false) = {
1754 if type(body) == array {
1755 return body.map(f => {
1756 (ctx) => {
1757 let element = f(ctx)
1758 if "drawables" in element {
1759 element.drawables = element.drawables.map(d => {
1760 d.hidden = true
1761 d.bounds = bounds
1762 return d
1763 })
1764 }
1765 return element
1766 }
1767 })
1768 }
1769 return body
1770}
1771
1772/// Places an element without affecting bounding boxes.
1773///
1774/// Floating elements are drawn to the canvas but are ignored when calculating bouding boxes. All other behaviours remain the same.
1775///
1776/// ```typc example
1777/// group(name: "g", {
1778/// content((1,0), [Normal])
1779/// content((0,1), [Normal])
1780/// floating(content((.5,1.5), [Floating]))
1781/// })
1782/// set-style(stroke: red)
1783/// rect("g.north-west", "g.south-east")
1784/// ```
1785///
1786/// - body (element): One or more elements to place
1787#let floating(body) = {
1788 if type(body) == array {
1789 return body.map(f => {
1790 ctx => {
1791 let element = f(ctx)
1792 if "drawables" in element {
1793 element.drawables = element.drawables.map(d => {
1794 d.bounds = false
1795 return d
1796 })
1797 }
1798 return element
1799 }
1800 })
1801 }
1802 return body
1803}
1804
1805/// Calculates the intersections between multiple paths and creates one anchor per intersection point.
1806///
1807/// All resulting anchors will be named numerically, starting at `0`. i.e., a call `intersections("a", ...)` will generate the anchors `"a.0"`, `"a.1"`, `"a.2"` to `"a.n"`, depending of the number of intersections.
1808///
1809/// ```typc example
1810/// intersections("i", {
1811/// circle((0, 0))
1812/// bezier((0,0), (3,0), (1,-1), (2,1))
1813/// line((0,-1), (0,1))
1814/// rect((1.5,-1),(2.5,1))
1815/// })
1816/// for-each-anchor("i", (name) => {
1817/// circle("i." + name, radius: .1, fill: blue)
1818/// })
1819/// ```
1820///
1821/// You can also use named elements:
1822///
1823/// ```typc example
1824/// circle((0,0), name: "a")
1825/// rect((0,0), (1,1), name: "b")
1826/// intersections("i", "a", "b")
1827/// for-each-anchor("i", (name) => {
1828/// circle("i." + name, radius: .1, fill: blue)
1829/// })
1830/// ```
1831///
1832/// You can calculate intersections with hidden elements by using [hide](./hide).
1833///
1834/// - name (str): Name to prepend to the generated anchors. (Not to be confused with other `name` arguments that allow the use of anchor coordinates.)
1835/// - ..elements (elements,str): Elements and/or element names to calculate intersections with. Elements referred to by name are (unlike elements passed) not drawn by the intersections function!
1836/// - samples (int): Number of samples to use for non-linear path segments. A higher sample count can give more precise results but worse performance.
1837/// - sort (none,function): A function of the form `(context, array<vector>) -> array<vector>`
1838/// that gets called with the list of intersection points.
1839///
1840/// CeTZ provides the following sorting functions:
1841/// - sorting.points-by-distace(points, reference: (0, 0, 0))
1842/// - sorting.points-by-angle(points, reference: (0, 0, 0))
1843#let intersections(name, ..elements, samples: 10, sort: none) = {
1844 samples = calc.clamp(samples, 2, 2500)
1845
1846 assert(type(name) == str and name != "",
1847 message: "Intersection must have a name, got:" + repr(name))
1848 assert(elements.pos() != (),
1849 message: "You must at least give one element to intersections.")
1850
1851 return (ctx => {
1852 let ctx = ctx
1853
1854 // List of drawables to calc intersections for;
1855 // grouped by element.
1856 let named-drawables = ()
1857 // List of drawables passed as elements to calc intersections for;
1858 // grouped by element.
1859 let drawables = ()
1860
1861 for elem in elements.pos() {
1862 if type(elem) == str {
1863 assert(elem in ctx.nodes,
1864 message: "No such element '" + elem + "' in elements " + repr(ctx.nodes.keys()))
1865 named-drawables.push(ctx.nodes.at(elem).drawables)
1866 } else {
1867 for sub in elem {
1868 let sub-drawables = ()
1869 (ctx: ctx, drawables: sub-drawables, ..) = process.element(ctx, sub)
1870 if sub-drawables != none and sub-drawables != () {
1871 drawables.push(sub-drawables)
1872 }
1873 }
1874 }
1875 }
1876
1877 let elems = named-drawables + drawables
1878 let pts = ()
1879 if elems.len() > 1 {
1880 for (i, elem-1) in elems.enumerate() {
1881 for j in range(i + 1, elems.len()) {
1882 let elem-2 = elems.at(j)
1883 for path-1 in elem-1 {
1884 for path-2 in elem-2 {
1885 for pt in intersection.path-path(
1886 path-1,
1887 path-2,
1888 samples: samples
1889 ) {
1890 if pt not in pts { pts.push(pt) }
1891 }
1892 }
1893 }
1894 }
1895 }
1896 }
1897
1898 if sort != none {
1899 pts = (sort)(ctx, pts)
1900 }
1901
1902 let anchors = (:)
1903 for (i, pt) in pts.enumerate() {
1904 anchors.insert(str(i), pt)
1905 }
1906
1907 return (
1908 ctx: ctx,
1909 name: name,
1910 anchors: anchor_.setup(
1911 anchor => {
1912 anchors.at(anchor)
1913 },
1914 anchors.keys(),
1915 transform: none,
1916 name: name
1917 ).last(),
1918 drawables: drawables.flatten()
1919 )
1920 },)
1921}
1922
1923/// Groups one or more elements together. This element acts as a scope, all state changes such as transformations and styling only affect the elements in the group. Elements after the group are not affected by the changes inside the group.
1924///
1925/// ```typc example
1926/// // Create group
1927/// group({
1928/// stroke(5pt)
1929/// scale(.5); rotate(45deg)
1930/// rect((-1,-1),(1,1))
1931/// })
1932/// rect((-1,-1),(1,1))
1933/// ```
1934///
1935/// - body (elements, function): Elements to group together. A least one is required. A function that accepts `ctx` and returns elements is also accepted.
1936/// - anchor (none, str): Anchor to position the group and it's children relative to. For translation the difference between the groups `"default"` anchor and the passed anchor is used.
1937/// - name (none, str):
1938/// - ..style (style):
1939///
1940/// ## Styling
1941/// *Root:* `group`
1942///
1943/// - padding (none, number, array, dictionary) = none: How much padding to add around the group's bounding box. `none` applies no padding. A number applies padding to all sides equally. A dictionary applies padding following Typst's `pad` function: https://typst.app/docs/reference/layout/pad/. An array follows CSS like padding: `(y, x)`, `(top, x, bottom)` or `(top, right, bottom, left)`.
1944///
1945/// ## Anchors
1946/// Supports border and path anchors of the axis aligned bounding box of all the child elements of the group.
1947///
1948/// You can add custom named anchors to the group by using the [anchor](./anchor) element while in the scope of said group, see [anchor](./anchor) for more details.
1949///
1950/// The default anchor is `"center"` but this can be overridden by using [anchor](./anchor) to place a new anchor called `"default"`.
1951///
1952/// When using named elements within a group, you can access the element's anchors outside of the group by using the implicit anchor coordinate. e.g. `"a.b.north"`
1953/// ```typc example
1954/// group(name: "a", {
1955/// circle((), name: "b")
1956/// })
1957/// circle("a.b.south", radius: 0.2)
1958/// circle((name: "a", anchor: "b.north"), radius: 0.2)
1959/// ```
1960#let group(body, name: none, anchor: none, ..style) = {
1961 // No extra positional arguments from the style sink
1962 assert.eq(style.pos(), (),
1963 message: "Unexpected positional arguments: " + repr(style.pos()),)
1964 util.assert-body(body)
1965
1966 (ctx => {
1967 let style = styles.resolve(ctx.style, merge: style.named(), root: "group")
1968
1969 let bounds = none
1970 let drawables = ()
1971 let group-ctx = ctx
1972 group-ctx.groups.push(())
1973
1974 (ctx: group-ctx, drawables, bounds) = process.many(group-ctx, util.resolve-body(group-ctx, body))
1975
1976 // Apply bounds padding
1977 bounds = if bounds != none {
1978 let padding = util.as-padding-dict(style.padding)
1979 padding = padding.pairs().map(
1980 ((k, v)) => (
1981 (k): util.resolve-number(ctx, v)
1982 )
1983 ).join()
1984
1985 aabb.padded(bounds, padding)
1986 }
1987
1988 // Calculate a bounding box path used for border
1989 // anchor calculation.
1990 let (center, width, height, path) = if bounds != none {
1991 (bounds.low.at(1), bounds.high.at(1)) = (bounds.high.at(1), bounds.low.at(1))
1992 let center = aabb.mid(bounds)
1993 let (width, height, _) = aabb.size(bounds)
1994 let path = drawable.path(
1995 path-util.line-segment((
1996 (bounds.low.at(0), bounds.high.at(1)),
1997 bounds.high,
1998 (bounds.high.at(0), bounds.low.at(1)),
1999 bounds.low,
2000 )), close: true)
2001 (center, width, height, path)
2002 } else { (none,) * 4 }
2003
2004 let children = group-ctx.groups.last().map(name => ((name): group-ctx.nodes.at(name))).join()
2005
2006 // Children can be none if the groups array is empty
2007 let anchors = if children != none {
2008 children.pairs().map(((name, child)) => {
2009 if "anchors" in child {
2010 ((name): child.anchors)
2011 }
2012 }).join()
2013 } else {
2014 (:)
2015 }
2016
2017 let (transform, anchors) = anchor_.setup(
2018 anchor => {
2019 let (name, ..nested-anchors) = if type(anchor) == array {
2020 anchor
2021 } else {
2022 (anchor,)
2023 }
2024 anchor = (
2025 if bounds != none {
2026 (default: center, center: center)
2027 } + anchors
2028 ).at(name)
2029 if type(anchor) == function {
2030 anchor(if nested-anchors == () { "default" } else { nested-anchors })
2031 } else {
2032 anchor
2033 }
2034 },
2035 (anchors.keys() + if bounds != none { ("center",) }).dedup(),
2036 name: name,
2037 default: if bounds != none or "default" in anchors { "default" },
2038 offset-anchor: anchor,
2039 path-anchors: bounds != none,
2040 border-anchors: bounds != none,
2041 radii: (width, height),
2042 path: path,
2043 nested-anchors: true
2044 )
2045
2046 return (
2047 ctx: ctx,
2048 name: name,
2049 anchors: anchors,
2050 drawables: drawable.apply-transform(transform, drawables),
2051 )
2052 },)
2053}
2054
2055/// This element acts as a scope, all state changes such as transformations and styling only affect the elements in the group. Elements after the scope are not affected by the changes inside the scope.
2056/// In contrast to `group`, the `scope` element does not create a named element itself and "leaks" body element to the outside.
2057///
2058/// - body (elements, function): Elements to group together. A least one is required. A function that accepts `ctx` and returns elements is also accepted.
2059#let scope(body) = (ctx => {
2060 let bounds = none
2061 let drawables = ()
2062 let group-ctx = ctx
2063 group-ctx.groups.push(())
2064
2065 (ctx: group-ctx, drawables, bounds) = process.many(group-ctx, util.resolve-body(group-ctx, body))
2066
2067 // Leak nodes
2068 ctx.nodes += group-ctx.nodes
2069
2070 return (
2071 ctx: ctx,
2072 drawables: drawables,
2073 )
2074},)
2075
2076/// Creates a new anchor for the current group. The new anchor will be accessible from inside the group by using just the anchor's name as a coordinate.
2077///
2078/// ```typc example
2079/// // Inside a group
2080/// group(name: "g", {
2081/// circle((0,0))
2082/// anchor("x", (.4, .1))
2083/// circle("x", radius: .2)
2084/// })
2085/// circle("g.x", radius: .1)
2086/// ```
2087
2088/// ```typc example
2089/// // At the root scope
2090/// anchor("x", (1, 1))
2091/// // ...
2092/// circle("x", radius: .1)
2093/// ```
2094///
2095/// - name (str): The name of the anchor
2096/// - position (coordinate): The position of the anchor
2097#let anchor(name, position) = {
2098 assert(name != none and name != "" and not name.starts-with("."),
2099 message: "Anchors must not be none, \"\" or start with \".\"!")
2100
2101 coordinate.resolve-system(position)
2102 return (ctx => {
2103 let (ctx, position) = coordinate.resolve(ctx, position)
2104 position = util.apply-transform(ctx.transform, position)
2105 return (
2106 ctx: ctx,
2107 name: name,
2108 anchors: anchor_.setup(
2109 anchor => position,
2110 ("default",),
2111 default: "default",
2112 name: name,
2113 transform: none
2114 ).last()
2115 )
2116 },)
2117}
2118
2119/// Copies multiple anchors from one element into the current group. Panics when used outside of a group. Copied anchors will be accessible in the same way anchors created by the `anchor` element are.
2120///
2121/// - element (str): The name of the element to copy anchors from.
2122/// - filter (auto,array): When set to `auto` all anchors will be copied to the group. An array of anchor names can instead be given so only the anchors that are in the element and the list will be copied over.
2123#let copy-anchors(element, filter: auto) = {
2124 (ctx => {
2125 assert(
2126 ctx.groups.len() > 0,
2127 message: "copy-anchors cannot be used outside of a group.",
2128 )
2129 assert(
2130 element in ctx.nodes,
2131 message: "copy-anchors: Could not find element '" + element + "'",
2132 )
2133
2134 let calc-anchors = ctx.nodes.at(element).anchors
2135 let anchors = calc-anchors(())
2136 if filter != auto {
2137 anchors = anchors.filter(a => a in filter)
2138 }
2139
2140 // Add each anchor as own element
2141 for anchor in anchors {
2142 ctx.nodes.insert(
2143 anchor,
2144 (anchors: name => {
2145 if name == "default" {
2146 calc-anchors(anchor)
2147 } else if name == () {
2148 ("default",)
2149 } else {
2150 calc-anchors((anchor,) + name)
2151 }
2152 })
2153 )
2154 ctx.groups.last().push(anchor)
2155 }
2156
2157 return (ctx: ctx)
2158 },)
2159}
2160
2161/// An advanced element that allows you to modify the current canvas {{context}}.
2162/// Note: The transformation matrix (`transform`) is rounded after calling the `callback` function and therefore might be not exactly the matrix specified. This is due to rounding errors and should not cause any problems.
2163///
2164/// ```typc example
2165/// // Setting a custom transformation matrix
2166/// set-ctx(ctx => {
2167/// let mat = ((1, 0, .5, 0),
2168/// (0, 1, 0, 0),
2169/// (0, 0, 1, 0),
2170/// (0, 0, 0, 1))
2171/// ctx.transform = mat
2172/// return ctx
2173/// })
2174/// circle((z: 0), fill: red)
2175/// circle((z: 1), fill: blue)
2176/// circle((z: 2), fill: green)
2177/// ```
2178///
2179/// - callback (function): A function that accepts the context dictionary and only returns a new one.
2180#let set-ctx(callback) = {
2181 assert(type(callback) == function)
2182 return (ctx => {
2183 let new-ctx = callback(ctx)
2184 assert(new-ctx != none, message: "set-ctx must return a context!")
2185
2186 if new-ctx.transform != ctx.transform {
2187 // User supplied matrices can cause rounding issues
2188 new-ctx.transform = matrix.round(new-ctx.transform)
2189 }
2190 (ctx: new-ctx)
2191 },)
2192}
2193
2194/// An advanced element that allows you to read the current {{context}} through a callback and return {{element}}s based on it.
2195///
2196/// ```typc example
2197/// // Print the transformation matrix
2198/// get-ctx(ctx => {
2199/// content((), [#repr(ctx.transform)])
2200/// })
2201/// ```
2202///
2203/// - callback (function): A function that accepts the {{context}} and can return elements.
2204#let get-ctx(callback) = {
2205 assert(type(callback) == function)
2206 (ctx => {
2207 let body = callback(ctx)
2208 if body != none {
2209 let (ctx, drawables) = process.many(ctx, callback(ctx))
2210 return (ctx: ctx, drawables: drawables)
2211 }
2212 return (ctx: ctx)
2213 },)
2214}
2215
2216/// Iterates through all named anchors of an element and calls a callback for each one.
2217///
2218/// ```typc example
2219/// // Label nodes anchors
2220/// rect((0, 0), (2,2), name: "my-rect")
2221/// for-each-anchor("my-rect", exclude: ("start", "mid", "end"), (name) => {
2222/// content((), box(inset: 1pt, fill: white, text(8pt, [#name])), angle: -30deg)
2223/// })
2224/// ```
2225///
2226/// - name (str): The name of the element with the anchors to loop through.
2227/// - callback (function): A function that takes the anchor name and can return elements.
2228/// - exclude (array): An array of anchor names to not include in the loop.
2229#let for-each-anchor(name, callback, exclude: ()) = {
2230 get-ctx(ctx => {
2231 assert(
2232 name in ctx.nodes,
2233 message: strfmt("Unknown element {} in elements {}", name, repr(ctx.nodes.keys()))
2234 )
2235 for anchor in (ctx.nodes.at(name).anchors)(()) {
2236 if anchor == none or (anchor in exclude) { continue }
2237 move-to(name + "." + anchor)
2238 callback(anchor)
2239 }
2240 })
2241}
2242
2243/// Places elements on a specific layer.
2244///
2245/// A layer determines the position of an element in the draw queue. A lower layer is drawn before a higher layer.
2246///
2247/// Layers can be used to draw behind or in front of other elements, even if the other elements were created before or after. An example would be drawing a background behind a text, but using the text's calculated bounding box for positioning the background.
2248///
2249/// ```typc example
2250/// // Draw something behind text
2251/// set-style(stroke: none)
2252/// content((0, 0), [This is an example.], name: "text")
2253/// on-layer(-1, {
2254/// circle("text.north-east", radius: .3, fill: red)
2255/// circle("text.south", radius: .4, fill: green)
2256/// circle("text.north-west", radius: .2, fill: blue)
2257/// })
2258/// ```
2259///
2260/// - layer (float, int): The layer to place the elements on. Elements placed without `on-layer` are always placed on layer 0.
2261/// - body (elements, function): Elements to draw on the layer specified. A function that accepts `ctx` and returns elements is also accepted.
2262#let on-layer(layer, body) = {
2263 util.assert-body(body)
2264 assert(type(layer) in (int, float),
2265 message: "Layer must be a float or integer, 0 being the default layer. Got: " + repr(layer))
2266
2267 return (ctx => {
2268 let (ctx, drawables, ..) = process.many(ctx, util.resolve-body(ctx, body))
2269 drawables = drawables.map(d => {
2270 if d.at("z-index", default: none) == none {
2271 d.z-index = layer
2272 }
2273 return d
2274 })
2275
2276 return (
2277 ctx: ctx,
2278 drawables: drawables
2279 )
2280 },)
2281}
2282#import "grouping.typ": group, get-ctx, set-ctx, scope
2283#import "transformations.typ": set-transform
2284#import "/src/process.typ"
2285#import "/src/matrix.typ"
2286#import "/src/drawable.typ"
2287#import "/src/util.typ"
2288#import "/src/polygon.typ"
2289
2290// Get an orthographic view matrix for 3 angles
2291#let ortho-matrix(x, y, z) = matrix.mul-mat(
2292 matrix.ident(),
2293 matrix.transform-rotate-x(x),
2294 matrix.transform-rotate-y(y),
2295 matrix.transform-rotate-z(z),
2296)
2297
2298#let ortho-projection-matrix = (
2299 (1, 0, 0, 0),
2300 (0, 1, 0, 0),
2301 (0, 0, 0, 0),
2302 (0, 0, 0, 1),
2303)
2304
2305#let _sort-by-distance(drawables) = {
2306 return drawables.sorted(key: d => {
2307 let z = none
2308 for ((kind, ..pts)) in d.segments {
2309 pts = pts.map(p => p.at(2))
2310 z = if z == none {
2311 calc.max(..pts)
2312 } else {
2313 calc.max(z, ..pts)
2314 }
2315 }
2316 return z
2317 })
2318}
2319
2320// Filter out all clock-wise polygons, or if `invert` is true,
2321// all counter clock-wise ones.
2322#let _filter-cw-faces(drawables, mode: "cw") = {
2323 return drawables.filter(d => {
2324 let poly = polygon.from-segments(d.segments)
2325 poly.first() != poly.last() or polygon.winding-order(poly) == mode
2326 })
2327}
2328
2329// Sets up a view matrix to transform all `body` elements. The current context
2330// transform is not modified.
2331//
2332// - body (element): Elements
2333// - view-matrix (matrix): View matrix
2334// - projection-matrix (matrix): Projection matrix
2335// - reset-transform (bool): Ignore the current transformation matrix
2336// - sorted (bool): Sort drawables by maximum distance (front to back)
2337// - cull-face (none,str): Enable back-face culling if set to `"cw"` for clockwise
2338// or `"ccw"` for counter-clockwise. Polygons of the specified order will not get drawn.
2339#let _projection(body, view-matrix, projection-matrix, reset-transform: true, sorted: true, cull-face: "cw") = {
2340 (ctx => {
2341 let transform = ctx.transform
2342 ctx.transform = view-matrix
2343
2344 let (ctx, drawables, bounds) = process.many(ctx, util.resolve-body(ctx, body))
2345
2346 if cull-face != none {
2347 assert(cull-face in ("cw", "ccw"),
2348 message: "cull-face must be none, cw or ccw.")
2349 drawables = _filter-cw-faces(drawables, mode: cull-face)
2350 }
2351 if sorted {
2352 drawables = _sort-by-distance(drawables)
2353 }
2354
2355 if projection-matrix != none {
2356 drawables = drawable.apply-transform(projection-matrix, drawables)
2357 }
2358
2359 ctx.transform = transform
2360 if not reset-transform {
2361 drawables = drawable.apply-transform(ctx.transform, drawables)
2362 }
2363
2364 return (
2365 ctx: ctx,
2366 bounds: bounds,
2367 drawables: drawables,
2368 )
2369 },)
2370}
2371
2372// Apply function `fn` to all vertices of all
2373// elements in `body`.
2374//
2375// - body (element): Elements
2376// - ..mat (matrix): Transformation matrices
2377#let scoped-transform(body, ..mat) = {
2378 scope({
2379 set-ctx(ctx => {
2380 ctx.transform = matrix.mul-mat(ctx.transform, ..mat.pos().filter(m => m != none))
2381 return ctx
2382 })
2383 body
2384 })
2385}
2386
2387/// Set-up an orthographic projection environment.
2388///
2389/// This is a transformation matrix that rotates elements around the x, the y and the z axis by the parameters given.
2390///
2391/// By default an isometric projection (x ≈ 35.264°, y = 45°) is set.
2392///
2393/// ```typc example
2394/// ortho({
2395/// on-xz({
2396/// rect((-1,-1), (1,1))
2397/// })
2398/// })
2399/// ```
2400///
2401/// - x (angle): X-axis rotation angle
2402/// - y (angle): Y-axis rotation angle
2403/// - z (angle): Z-axis rotation angle
2404/// - sorted (bool): Sort drawables by maximum distance (front to back)
2405/// - cull-face (none,str): Enable back-face culling if set to `"cw"` for clockwise
2406/// or `"ccw"` for counter-clockwise. Polygons of the specified order will not get drawn.
2407/// - reset-transform (bool): Ignore the current transformation matrix
2408/// - body (element): Elements to draw
2409#let ortho(x: 35.264deg, y: 45deg, z: 0deg, sorted: true, cull-face: none, reset-transform: false, body, name: none) = group(name: name, ctx => {
2410 _projection(body, ortho-matrix(x, y, z), ortho-projection-matrix,
2411 sorted: sorted,
2412 cull-face: cull-face,
2413 reset-transform: reset-transform)
2414})
2415
2416/// Draw elements on the xy-plane with optional z offset.
2417///
2418/// All vertices of all elements will be changed in the following way: $\begin{pmatrix} x \\ y \\ z_\text{argument}\end{pmatrix}$, where $z_\text{argument}$ is the z-value given as argument.
2419///
2420/// ```typc example
2421/// on-xy({
2422/// rect((-1, -1), (1, 1))
2423/// })
2424/// ```
2425///
2426/// - z (number): Z offset for all coordinates
2427/// - body (element): Elements to draw
2428#let on-xy(z: 0, body) = get-ctx(ctx => {
2429 let z = util.resolve-number(ctx, z)
2430 scoped-transform(body, if z != 0 {
2431 matrix.transform-translate(0, 0, z)
2432 }, matrix.ident())
2433})
2434
2435/// Draw elements on the xz-plane with optional y offset.
2436///
2437/// All vertices of all elements will be changed in the following way: $\begin{pmatrix} x \\ y_\text{argument} \\ y \end{pmatrix}$, where $y_\text{argument}$ is the y-value given as argument.
2438///
2439/// ```typc example
2440/// on-xz({
2441/// rect((-1, -1), (1, 1))
2442/// })
2443/// ```
2444///
2445/// - y (number): Y offset for all coordinates
2446/// - body (element): Elements to draw
2447#let on-xz(y: 0, body) = get-ctx(ctx => {
2448 let y = util.resolve-number(ctx, y)
2449 scoped-transform(body, if y != 0 {
2450 matrix.transform-translate(0, y, 0)
2451 }, matrix.transform-rotate-x(90deg))
2452})
2453
2454/// Draw elements on the yz-plane with optional x offset.
2455///
2456/// All vertices of all elements will be changed in the following way: $\begin{pmatrix} x_\text{argument} \\ x \\ y \end{pmatrix}$, where $x_\text{argument}$ is the x-value given as argument.
2457///
2458/// ```typc example
2459/// on-yz({
2460/// rect((-1, -1), (1, 1))
2461/// })
2462/// ```
2463///
2464/// - x (number): X offset for all coordinates
2465/// - body (element): Elements to draw
2466#let on-yz(x: 0, body) = get-ctx(ctx => {
2467 let x = util.resolve-number(ctx, x)
2468 scoped-transform(body, if x != 0 {
2469 matrix.transform-translate(x, 0, 0)
2470 }, matrix.transform-rotate-y(90deg))
2471})
2472#let typst-angle = angle
2473#let typst-rotate = rotate
2474
2475#import "/src/coordinate.typ"
2476#import "/src/drawable.typ"
2477#import "/src/styles.typ"
2478#import "/src/path-util.typ"
2479#import "/src/util.typ"
2480#import "/src/vector.typ"
2481#import "/src/matrix.typ"
2482#import "/src/process.typ"
2483#import "/src/bezier.typ" as bezier_
2484#import "/src/hobby.typ" as hobby_
2485#import "/src/anchor.typ" as anchor_
2486#import "/src/mark.typ" as mark_
2487#import "/src/mark-shapes.typ" as mark-shapes_
2488#import "/src/polygon.typ" as polygon_
2489#import "/src/aabb.typ"
2490
2491#import "transformations.typ": *
2492#import "styling.typ": *
2493#import "grouping.typ": *
2494
2495/// Draws a circle or ellipse.
2496///
2497/// ```typc example
2498/// circle((0,0))
2499/// // Draws an ellipse
2500/// circle((0,-2), radius: (0.75, 0.5))
2501/// ```
2502///
2503/// - position (coordinate): The position to place the circle on.
2504/// - name (none,str):
2505/// - anchor (none, str):
2506/// - ..style (style):
2507///
2508/// ### Styling
2509/// *Root*: `circle`
2510///
2511/// - 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.
2512///
2513/// ### Anchors
2514/// Supports border and path anchors. The `"center"` anchor is the default.
2515///
2516#let circle(position, name: none, anchor: none, ..style) = {
2517 // No extra positional arguments from the style sink
2518 assert.eq(
2519 style.pos(),
2520 (),
2521 message: "Unexpected positional arguments: " + repr(style.pos()),
2522 )
2523 let style = style.named()
2524
2525 (ctx => {
2526 let (ctx, pos) = coordinate.resolve(ctx, position)
2527 let style = styles.resolve(ctx.style, merge: style, root: "circle")
2528 let (rx, ry) = util.resolve-radius(style.radius).map(util.resolve-number.with(ctx))
2529 let (cx, cy, cz) = pos
2530
2531 let drawables = drawable.ellipse(
2532 cx, cy, cz,
2533 rx, ry,
2534 fill: style.fill,
2535 stroke: style.stroke
2536 )
2537
2538 let (transform, anchors) = anchor_.setup(
2539 (_) => pos,
2540 ("center",),
2541 default: "center",
2542 name: name,
2543 offset-anchor: anchor,
2544 transform: ctx.transform,
2545 border-anchors: true,
2546 path-anchors: true,
2547 radii: (rx*2, ry*2),
2548 path: drawables,
2549 )
2550
2551 return (
2552 ctx: ctx,
2553 name: name,
2554 anchors: anchors,
2555 drawables: drawable.apply-transform(transform, drawables),
2556 )
2557 },)
2558}
2559
2560/// Draws a circle through three coordinates.
2561///
2562/// ```typc example
2563/// let (a, b, c) = ((0,0), (2,-.5), (1,1))
2564/// line(a, b, c, close: true, stroke: gray)
2565/// circle-through(a, b, c, name: "c")
2566/// circle("c.center", radius: .05, fill: red)
2567/// ```
2568///
2569/// - a (coordinate): Coordinate a.
2570/// - b (coordinate): Coordinate b.
2571/// - c (coordinate): Coordinate c.
2572/// - name (none,str):
2573/// - anchor (none,str):
2574/// - ..style (style):
2575///
2576/// ### Styling
2577/// *Root*: `circle`
2578///
2579/// `circle-through` has the same styling as [circle](./circle#styling) except for `radius` as the circle's radius is calculated by the given coordinates.
2580///
2581/// ### Anchors
2582/// Supports the same anchors as [circle](./circle#anchors) as well as:
2583/// - **a**: Coordinate a
2584/// - **b**: Coordinate b
2585/// - **c**: Coordinate c
2586#let circle-through(a, b, c, name: none, anchor: none, ..style) = {
2587 assert.eq(style.pos(), (), message: "Unexpected positional arguments: " + repr(style.pos()))
2588 style = style.named()
2589
2590 (a, b, c).map(coordinate.resolve-system)
2591
2592 return (ctx => {
2593 let (ctx, a, b, c) = coordinate.resolve(ctx, a, b, c)
2594
2595 let center = util.calculate-circle-center-3pt(a, b, c)
2596
2597 let style = styles.resolve(ctx.style, merge: style, root: "circle")
2598 let (cx, cy, cz) = center
2599 let r = vector.dist(a, (cx, cy))
2600
2601 let drawables = drawable.ellipse(
2602 cx, cy, 0,
2603 r, r,
2604 fill: style.fill,
2605 stroke: style.stroke
2606 )
2607
2608 let (transform, anchors) = anchor_.setup(
2609 (anchor) => (
2610 center: center,
2611 a: a,
2612 b: b,
2613 c: c
2614 ).at(anchor),
2615 ("center", "a", "b", "c"),
2616 default: "center",
2617 name: name,
2618 offset-anchor: anchor,
2619 transform: ctx.transform,
2620 border-anchors: true,
2621 path-anchors: true,
2622 radii: (r*2, r*2),
2623 path: drawables,
2624 )
2625
2626 return (
2627 ctx: ctx,
2628 name: name,
2629 anchors: anchors,
2630 drawables: drawable.apply-transform(
2631 transform,
2632 drawables
2633 )
2634 )
2635 },)
2636}
2637
2638/// Draws a circular segment.
2639///
2640/// ```typc example
2641/// arc((0,0), start: 45deg, stop: 135deg)
2642/// arc((0,-0.5), start: 45deg, delta: 90deg, mode: "CLOSE")
2643/// arc((0,-1), stop: 135deg, delta: 90deg, mode: "PIE")
2644/// ```
2645///
2646/// Note that two of the three angle arguments (`start`, `stop` and `delta`) must be set.
2647/// The current position `()` gets updated to the arc's end coordinate (anchor `arc-end`).
2648///
2649/// - position (coordinate): Position to place the arc at.
2650/// - start (auto,angle): The angle at which the arc should start. Remember that `0deg` points directly towards the right and `90deg` points up.
2651/// - stop (auto,angle): The angle at which the arc should stop.
2652/// - delta (auto,angle): The change in angle away start or stop.
2653/// - name (none,str):
2654/// - anchor (none, str):
2655/// - ..style (style):
2656///
2657/// ## Styling
2658/// *Root*: `arc`\
2659/// - radius (number, array) = 1: The radius of the arc. An elliptical arc can be created by passing a tuple of numbers where the first element is the x radius and the second element is the y radius.
2660/// - mode (str) = "OPEN": The options are: `"OPEN"` no additional lines are drawn so just the arc is shown; `"CLOSE"` a line is drawn from the start to the end of the arc creating a circular segment; `"PIE"` lines are drawn from the start and end of the arc to the origin creating a circular sector.
2661/// - update-position (bool) = true: Update the current canvas position to the arc's end point (anchor `"arc-end"`). This overrides the default of `true`, that allows chaining of (arc) elements.
2662///
2663/// ## Anchors
2664/// Supports border and path anchors.
2665/// - **arc-start**: The position at which the arc's curve starts, this is the default.
2666/// - **arc-end**: The position of the arc's curve end.
2667/// - **arc-center**: The midpoint of the arc's curve.
2668/// - **center**: The center of the arc, this position changes depending on if the arc is closed or not.
2669/// - **chord-center**: Center of chord of the arc drawn between the start and end point.
2670/// - **origin**: The origin of the arc's circle.
2671#let arc(
2672 position,
2673 start: auto,
2674 stop: auto,
2675 delta: auto,
2676 name: none,
2677 anchor: none,
2678 ..style,
2679) = {
2680 // Start, stop, delta check
2681 assert(
2682 (start, stop, delta).filter(it => { it == auto }).len() == 1,
2683 message: "Exactly two of three options start, stop and delta should be defined.",
2684 )
2685
2686 // No extra positional arguments from the style sink
2687 assert.eq(
2688 style.pos(),
2689 (),
2690 message: "Unexpected positional arguments: " + repr(style.pos()),
2691 )
2692 let style = style.named()
2693
2694 // Coordinate check
2695 let t = coordinate.resolve-system(position)
2696
2697 let start-angle = if start == auto { stop - delta } else { start }
2698 let stop-angle = if stop == auto { start + delta } else { stop }
2699 // Border angles can break if the angle is 0.
2700 assert.ne(start-angle, stop-angle, message: "Angle must be greater than 0deg")
2701
2702 return (ctx => {
2703 let style = styles.resolve(ctx.style, merge: style, root: "arc")
2704 assert(style.mode in ("OPEN", "PIE", "CLOSE"))
2705
2706 let (ctx, arc-start) = coordinate.resolve(ctx, position)
2707 let (rx, ry) = util.resolve-radius(style.radius).map(util.resolve-number.with(ctx))
2708
2709 let (x, y, z) = arc-start
2710 let drawables = drawable.arc(
2711 ..arc-start,
2712 start-angle,
2713 stop-angle,
2714 rx,
2715 ry,
2716 stroke: style.stroke,
2717 fill: style.fill,
2718 mode: style.mode
2719 )
2720
2721 let sector-center = (
2722 x - rx * calc.cos(start-angle),
2723 y - ry * calc.sin(start-angle),
2724 z
2725 )
2726 let arc-end = (
2727 sector-center.first() + rx * calc.cos(stop-angle),
2728 sector-center.at(1) + ry * calc.sin(stop-angle),
2729 z
2730 )
2731 let chord-center = vector.lerp(arc-start, arc-end, 0.5)
2732 let arc-center = (
2733 sector-center.first() + rx * calc.cos((stop-angle + start-angle)/2),
2734 sector-center.at(1) + ry * calc.sin((stop-angle + start-angle)/2),
2735 z
2736 )
2737
2738 // Set the last position to arc-end
2739 if style.update-position {
2740 ctx.prev.pt = arc-end
2741 }
2742
2743 // Center is calculated based on observations of tikz's circular sector and semi circle shapes.
2744 let center = if style.mode != "CLOSE" {
2745 // 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.
2746 vector.lerp(
2747 arc-center,
2748 sector-center,
2749 if (stop-angle + start-angle) > 180deg { (stop-angle + start-angle) } else { (stop-angle + start-angle) + 180deg } / 720deg
2750 )
2751 } else {
2752 // A semi circle's center anchor is placed half way between the sector-center and arc-center, so that is always `center` when the arc is closed. Otherwise the point at which compass anchors are calculated from will be outside the lines.
2753 vector.lerp(
2754 arc-center,
2755 chord-center,
2756 0.5
2757 )
2758 }
2759
2760 let (transform, anchors) = anchor_.setup(
2761 anchor => (
2762 arc-start: arc-start,
2763 origin: sector-center,
2764 arc-end: arc-end,
2765 arc-center: arc-center,
2766 chord-center: chord-center,
2767 center: center,
2768 ).at(anchor),
2769 ("arc-center", "chord-center", "origin", "arc-start", "arc-end", "center"),
2770 default: "arc-start",
2771 name: name,
2772 offset-anchor: anchor,
2773 transform: ctx.transform,
2774 border-anchors: true,
2775 path-anchors: true,
2776 radii: (rx, ry), // Don't multiply as its not from the arc's center
2777 path: drawables
2778 )
2779
2780 if mark_.check-mark(style.mark) {
2781 drawables = mark_.place-marks-along-path(ctx, style.mark, transform, drawables)
2782 } else {
2783 drawables = drawable.apply-transform(transform, drawables)
2784 }
2785
2786 return (
2787 ctx: ctx,
2788 name: name,
2789 anchors: anchors,
2790 drawables: drawables,
2791 )
2792 },)
2793}
2794
2795/// Draws an arc that passes through three points a, b and c.
2796///
2797/// Note that all three points must not lie on a straight line, otherwise
2798/// the function fails.
2799///
2800/// ```typc example
2801/// arc-through((0,1), (1,1), (1,0))
2802/// ```
2803///
2804/// - a (coordinate): Start position of the arc
2805/// - b (coordinate): Position the arc passes through
2806/// - c (coordinate): End position of the arc
2807/// - name (none, str):
2808/// - ..style (style):
2809///
2810/// ### Styling
2811/// *Root*: `arc`
2812///
2813/// Uses the same styling as [arc](./arc#styling)
2814///
2815/// ### Anchors
2816/// For anchors see [arc](./arc#anchors).
2817///
2818#let arc-through(
2819 a,
2820 b,
2821 c,
2822 name: none,
2823 ..style,
2824) = get-ctx(ctx => {
2825 let (ctx, a, b, c) = coordinate.resolve(ctx, a, b, c)
2826 assert(a.at(2) == b.at(2) and b.at(2) == c.at(2),
2827 message: "The z coordinate of all points must be equal, but is: " + repr((a, b, c).map(v => v.at(2))))
2828
2829 // Calculate the circle center from three points or fails if all
2830 // three points are on one straight line.
2831 let center = util.calculate-circle-center-3pt(a, b, c)
2832 let radius = vector.dist(center, a)
2833
2834 // Find the start and inner angle between a-center-c
2835 let start = vector.angle2(center, a)
2836 let delta = vector.angle(a, center, c)
2837
2838 // Returns a negative number if pt is left of the line a-b,
2839 // if pt is right to a-b, a positive number is returned,
2840 // otherwise zero.
2841 let side-on-line(a, b, pt) = {
2842 let (x1, y1, ..) = a
2843 let (x2, y2, ..) = b
2844 let (x, y, ..) = pt
2845 return (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1)
2846 }
2847
2848 // Center & b b is left,
2849 // are left center not
2850 //
2851 // +-b-+ +-b-+
2852 // / \ / \
2853 // | C | --a-------c--
2854 // \ / \ C /
2855 // ---a---c--- +---+
2856 //
2857 // If b and C are on the same side of a-c, the arcs radius is >= 180deg,
2858 // otherwise the radius is < 180deg.
2859 let center-is-left = side-on-line(a, c, center) < 0
2860 let b-is-left = side-on-line(a, c, b) < 0
2861
2862 // If the center and point b are on the same side of a-c,
2863 // the arcs delta must be > 180deg. Note, that delta is
2864 // the inner angle between a-center-c, so we need to calculate
2865 // the outer angle by subtracting from 360deg.
2866 if center-is-left == b-is-left {
2867 delta = 360deg - delta
2868 }
2869
2870 // If b is left of a-c, swap a-c to c-a by using a negative delta
2871 if b-is-left {
2872 delta *= -1
2873 }
2874
2875 return arc(
2876 a,
2877 start: start,
2878 delta: delta,
2879 radius: radius,
2880 anchor: "arc-start",
2881 name: name,
2882 ..style
2883 )
2884})
2885
2886/// Draws a single mark pointing towards a target coordinate.
2887///
2888/// ```typc example
2889/// mark((0,0), (1,0), symbol: ">", fill: black)
2890/// mark((0,0), (1,1), symbol: "stealth", scale: 3, fill: black)
2891/// ```
2892///
2893/// Note: To place a mark centered at the first coodinate (`from`) use
2894/// the marks `anchor: "center"` style.
2895///
2896/// - from (coordinate): The position to place the mark.
2897/// - to (coordinate,angle): The position or angle the mark should point towards.
2898/// - ..style (style):
2899///
2900/// ## Styling
2901/// *Root*: `mark`
2902///
2903/// You can directly use the styling from [Mark Styling](/docs/basics/marks).
2904#let mark(from, to, ..style) = {
2905 assert.eq(
2906 style.pos(),
2907 (),
2908 message: "Unexpected positional arguments: " + repr(style.pos()),
2909 )
2910
2911 let style = style.named()
2912
2913 if type(to) == angle {
2914 // Construct a coordinate pointing (+1, 0) away from
2915 // `from`, rotated by the angle given.
2916 to = ((rel: (to, 1), to: from))
2917 }
2918
2919 (from, to).map(coordinate.resolve-system)
2920
2921 return (ctx => {
2922 let (ctx, ..pts) = coordinate.resolve(ctx, from, to)
2923 let style = styles.resolve(ctx.style, merge: style, root: "mark")
2924
2925 if style.end == none {
2926 style.end = style.symbol
2927 }
2928 style.start = none
2929 style.symbol = none
2930
2931 let (to, from) = (..pts)
2932 from = vector.sub(to, vector.sub(from, to))
2933
2934 let drawables = drawable.path((path-util.line-segment((from, to)),))
2935 drawables = mark_.place-marks-along-path(ctx, style, none, drawables, add-path: false)
2936 return (
2937 ctx: ctx,
2938 drawables: drawable.apply-transform(ctx.transform, drawables)
2939 )
2940 },)
2941}
2942
2943/// Draws a line, more than two points can be given to create a line-strip.
2944///
2945/// ```typc example
2946/// line((-1.5, 0), (1.5, 0))
2947/// line((0, -1.5), (0, 1.5))
2948/// line((-1, -1), (-0.5, 0.5), (0.5, 0.5), (1, -1), close: true)
2949/// ```
2950///
2951/// If the first or last coordinates are given as the name of an element,
2952/// that has a `"default"` anchor, the intersection of that element's border
2953/// and a line from the first or last two coordinates given is used as coordinate.
2954/// This is useful to span a line between the borders of two elements.
2955///
2956/// ```typc example
2957/// circle((1,2), radius: .5, name: "a")
2958/// rect((2,1), (rel: (1,1)), name: "b")
2959/// line("a", "b")
2960/// ```
2961/// - ..pts-style (coordinate,style): Positional two or more coordinates to draw lines between. Accepts style key-value pairs.
2962/// - close (bool): If true, the line-strip gets closed to form a polygon
2963/// - name (none,str):
2964///
2965/// ## Styling
2966/// *Root:* `line`
2967///
2968/// Supports mark styling.
2969///
2970/// ## Anchors
2971/// Supports path anchors.
2972/// - **centroid**: The centroid anchor is calculated for _closed non self-intersecting_ polygons if all vertices share the same z value.
2973#let line(..pts-style, close: false, name: none) = {
2974 // Extra positional arguments from the pts-style sink are interpreted as coordinates.
2975 let pts = pts-style.pos()
2976 let style = pts-style.named()
2977
2978 assert(pts.len() >= 2, message: "Line must have a minimum of two points")
2979
2980 // Coordinate check
2981 let pts-system = pts.map(coordinate.resolve-system)
2982
2983 // Find the intersection between line a-b next to b
2984 // if no intersection could be found, return a.
2985 let element-line-intersection(ctx, elem, a, b) = {
2986 // Vectors a and b are not transformed yet, but the vectors of the
2987 // drawable are.
2988 let (ta, tb) = util.apply-transform(ctx.transform, a, b)
2989
2990 let pts = ()
2991 for drawable in elem.at("drawables", default: ()).filter(d => d.type == "path") {
2992 pts += intersection.line-path(ta, tb, drawable)
2993 }
2994 return if pts == () {
2995 a
2996 } else {
2997 // Find the nearest point
2998 let pt = util.sort-points-by-distance(tb, pts).first()
2999
3000 // Reverse the transformation
3001 return util.revert-transform(ctx.transform, pt)
3002 }
3003 }
3004
3005 return (ctx => {
3006 let first-elem = pts.first()
3007 let last-elem = pts.last()
3008 let (ctx, ..pts) = coordinate.resolve(ctx, ..pts)
3009
3010 // If the first/last element, test for intersection
3011 // of that element and a line from the two first/last coordinates of this
3012 // line strip.
3013 if pts-system.first() == "element" {
3014 let elem = ctx.nodes.at(first-elem)
3015 pts.first() = element-line-intersection(ctx, elem, ..pts.slice(0, 2))
3016 }
3017 if pts-system.last() == "element" {
3018 let elem = ctx.nodes.at(last-elem)
3019 pts.last() = element-line-intersection(ctx, elem, ..pts.slice(-2).rev())
3020 }
3021
3022 let style = styles.resolve(ctx.style, merge: style, root: "line")
3023
3024 let drawables = drawable.path(
3025 (path-util.line-segment(pts),),
3026 fill: style.fill,
3027 fill-rule: style.fill-rule,
3028 stroke: style.stroke,
3029 close: close
3030 )
3031
3032 // Get bounds
3033 let (transform, anchors) = anchor_.setup(
3034 name => {
3035 if name == "centroid" {
3036 return polygon_.simple-centroid(pts)
3037 }
3038 },
3039 if close != none { ("centroid",) } else { () },
3040 default: if close != none { "centroid" },
3041 name: name,
3042 transform: ctx.transform,
3043 path-anchors: true,
3044 path: drawables
3045 )
3046
3047 // Place marks and adjust segments
3048 if mark_.check-mark(style.mark) {
3049 drawables = mark_.place-marks-along-path(ctx, style.mark, transform, drawables)
3050 } else {
3051 drawables = drawable.apply-transform(transform, drawables)
3052 }
3053
3054 return (
3055 ctx: ctx,
3056 name: name,
3057 anchors: anchors,
3058 drawables: drawables,
3059 )
3060 },)
3061}
3062
3063/// Draws a regular polygon.
3064///
3065/// - origin (coordinate): Coordinate to draw the polygon at
3066/// - sides (int): Number of sides of the polygon (>= 3)
3067/// - angle (angle) = 0deg: Angle angle to rotate the polygon arround its origin
3068/// - name (none, str):
3069///
3070/// ## Styling
3071/// *Root*: `polygon`
3072/// - radius (number) = 1: Radius of the polygon
3073#let polygon(origin, sides, angle: 0deg, name: none, anchor: none, ..style) = {
3074 coordinate.resolve-system(origin)
3075
3076 assert(type(sides) == int and sides >= 3,
3077 message: "Invalid number of sides: " + repr(sides))
3078
3079 let style = style.named()
3080 return (ctx => {
3081 let anchors = ()
3082
3083 let style = styles.resolve(ctx.style, merge: style, root: "polygon")
3084
3085 let (ctx, origin) = coordinate.resolve(ctx, origin)
3086 let (rx, ry) = util.resolve-radius(style.radius)
3087
3088 let points = range(0, sides).map(i => {
3089 let alpha = angle + 360deg / sides * i
3090 vector.add(origin, (calc.cos(alpha) * rx, calc.sin(alpha) * ry, 0))
3091 })
3092
3093 let drawables = drawable.path(
3094 (path-util.line-segment(points),),
3095 fill: style.fill,
3096 stroke: style.stroke,
3097 close: true)
3098
3099 let edge-anchors = range(0, sides).map(i => "edge-" + str(i))
3100 let corner-anchors = range(0, sides).map(i => "corner-" + str(i))
3101
3102 let (transform, anchors) = anchor_.setup(
3103 name => {
3104 return if name == "center" or name == "default" {
3105 origin
3106 } else if name in edge-anchors {
3107 let idx = edge-anchors.position(item => item == name)
3108 vector.lerp(
3109 points.at(idx),
3110 points.at(idx + 1, default: points.first()),
3111 .5)
3112 } else if name in corner-anchors {
3113 let idx = corner-anchors.position(item => item == name)
3114 points.at(idx)
3115 }
3116 },
3117 ("center",) + edge-anchors + corner-anchors,
3118 name: name,
3119 transform: ctx.transform,
3120 path-anchors: false,
3121 border-anchors: true,
3122 radii: (rx*2, ry*2),
3123 path: drawables,
3124 offset-anchor: anchor,
3125 default: "center",
3126 )
3127
3128 return (
3129 ctx: ctx,
3130 name: name,
3131 anchors: anchors,
3132 drawables: drawable.apply-transform(transform, drawables),
3133 )
3134 },)
3135}
3136
3137/// Draws a grid between two coordinates
3138///
3139/// ```typc example
3140/// // Draw a grid
3141/// grid((0,0), (2,2))
3142///
3143/// // Draw a smaller blue grid
3144/// grid((1,1), (2,2), stroke: blue, step: .25)
3145/// ```
3146///
3147/// - from (coordinate): The top left of the grid
3148/// - to (coordinate): The bottom right of the grid
3149/// - name (none,str):
3150/// - ..style (style):
3151///
3152/// ## Styling
3153/// *Root*: `grid`
3154/// - step (number, array, dictionary) = 1: Distance between grid lines. A distance of $1$ means to draw a grid line every $1$ length units in x- and y-direction. If given a dictionary with `x` and `y` keys or a tuple, the step is set per axis.
3155/// - help-lines (bool) = false: If true, force the stroke style to `gray + 0.2pt`
3156///
3157/// ## Anchors
3158/// Supports border anchors.
3159#let grid(from, to, name: none, ..style) = {
3160 (from, to).map(coordinate.resolve-system)
3161
3162 assert.eq(style.pos(), (), message: "Unexpected positional arguments: " + repr(style.pos()))
3163 style = style.named()
3164
3165 return (ctx => {
3166 let (ctx, from, to) = coordinate.resolve(ctx, from, to)
3167
3168 (from, to) = {
3169 let pairs = ((from.at(0), to.at(0)), (from.at(1), to.at(1)), (from.at(2), from.at(2)))
3170 (
3171 pairs.map(e => calc.min(..e)),
3172 pairs.map(e => calc.max(..e))
3173 )
3174 }
3175
3176 let style = styles.resolve(ctx.style, merge: style, root: "grid", base: (
3177 step: 1,
3178 stroke: auto,
3179 help-lines: false,
3180 ))
3181 if style.help-lines {
3182 style.stroke = 0.2pt + gray
3183 }
3184
3185 let (x-step, y-step) = if type(style.step) == dictionary {
3186 (style.step.at("x", default: 1), style.step.at("y", default: 1))
3187 } else if type(style.step) == array {
3188 style.step
3189 } else {
3190 (style.step, style.step)
3191 }.map(util.resolve-number.with(ctx))
3192
3193 let drawables = {
3194 if x-step != 0 {
3195 range(int((to.at(0) - from.at(0)) / x-step)+1).map(x => {
3196 x *= x-step
3197 x += from.at(0)
3198 drawable.path(
3199 path-util.line-segment(((x, from.at(1)), (x, to.at(1)))),
3200 stroke: style.stroke
3201 )
3202 })
3203 } else {
3204 ()
3205 }
3206 if y-step != 0 {
3207 range(int((to.at(1) - from.at(1)) / y-step)+1).map(y => {
3208 y *= y-step
3209 y += from.at(1)
3210 drawable.path(
3211 path-util.line-segment(((from.at(0), y), (to.at(0), y))),
3212 stroke: style.stroke
3213 )
3214 })
3215 } else {
3216 ()
3217 }
3218 }
3219
3220 let center = vector.lerp(from, to, .5)
3221 let (transform, anchors) = anchor_.setup(
3222 _ => center,
3223 ("center",),
3224 name: name,
3225 transform: ctx.transform,
3226 border-anchors: true,
3227 radii: (vector.dist(center, from) * 2,) * 2,
3228 path: drawable.path(
3229 path-util.line-segment((
3230 from,
3231 (from.first(), to.at(1), 0),
3232 to,
3233 (to.first(), from.at(1), 0)
3234 )),
3235 close: true
3236 )
3237 )
3238
3239 return (
3240 ctx: ctx,
3241 name: name,
3242 anchors: anchors,
3243 drawables: drawable.apply-transform(
3244 transform,
3245 drawables
3246 )
3247 )
3248 },)
3249}
3250
3251/// Positions Typst content in the canvas. Note that the content itself is not transformed only its position is.
3252///
3253/// ```typc example
3254/// content((0,0), [Hello World!])
3255/// ```
3256/// 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`:
3257///
3258/// ```typc example
3259/// line((0, 0), (3, 1), name: "line")
3260/// content(
3261/// ("line.start", 50%, "line.end"),
3262/// angle: "line.end",
3263/// padding: .1,
3264/// anchor: "south",
3265/// [Text on a line]
3266/// )
3267/// ```
3268///
3269/// ```typc example
3270/// // Place content in a rect between two coordinates
3271/// content(
3272/// (0, 0),
3273/// (2, 2),
3274/// box(
3275/// par(justify: false)[This is a long text.],
3276/// stroke: 1pt,
3277/// width: 100%,
3278/// height: 100%,
3279/// inset: 1em
3280/// )
3281/// )
3282/// ```
3283///
3284/// - ..args-style (coordinate, content, style): When one coordinate is given as a positional argument, the content will be placed at that position. When two coordinates are given as positional arguments, the content will be placed inside a rectangle between the two positions. All named arguments are styling and any additional positional arguments will panic.
3285/// - angle (angle,coordinate): Rotates the content by the given angle. A coordinate can be given to rotate the content by the angle between it and the first coordinate given in `args`. This effectively points the right hand side of the content towards the coordinate. This currently exists because Typst's rotate function does not change the width and height of content.
3286/// - anchor (none, str):
3287/// - name (none, str):
3288///
3289/// ## Styling
3290/// *Root*: `content`
3291/// - 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/
3292/// - frame (str, none) = none: Sets the frame style. Can be {{none}}, `"rect"` or `"circle"` and inherits the `stroke` and `fill` style.
3293/// - auto-scale (bool): If `true`, apply current canvas scaling to the content. Defaults to `false`.
3294///
3295/// ## Anchors
3296/// Supports border anchors, the default anchor is set to **center**.
3297/// - **mid**: Content center, from baseline to top bounds
3298/// - **mid-east**: Content center extended to the east
3299/// - **mid-west**: Content center extended to the west
3300/// - **base**: Horizontally centered baseline of the content
3301/// - **base-east**: Baseline height extended to the east
3302/// - **base-west**: Baseline height extended to the west
3303/// - **text**: Position at the content start on the baseline of the content
3304#let content(
3305 ..args-style,
3306 angle: 0deg,
3307 anchor: none,
3308 name: none,
3309 ) = {
3310 let (args, style) = (args-style.pos(), args-style.named())
3311
3312 let (a, b, body) = if args.len() == 2 {
3313 args.insert(1, auto)
3314 args
3315 } else if args.len() == 3 {
3316 args
3317 } else {
3318 panic("Expected 2 or 3 positional arguments, got " + str(args.len()))
3319 }
3320
3321 coordinate.resolve-system(a)
3322
3323 if b != auto {
3324 coordinate.resolve-system(b)
3325 }
3326
3327 if type(angle) != typst-angle {
3328 coordinate.resolve-system(angle)
3329 }
3330
3331 return (ctx => {
3332 let body = body
3333 let style = styles.resolve(ctx.style, merge: style, root: "content")
3334 let padding = util.as-padding-dict(style.padding)
3335 for (k, v) in padding {
3336 padding.insert(k, util.resolve-number(ctx, v))
3337 }
3338
3339 let (ctx, a) = coordinate.resolve(ctx, a)
3340 let b = b
3341 let auto-size = b == auto
3342 if not auto-size {
3343 (ctx, b) = coordinate.resolve(ctx, b)
3344 }
3345
3346 let angle = if type(angle) != typst-angle {
3347 let c
3348 (ctx, c) = coordinate.resolve(ctx, angle)
3349 vector.angle2(a, c)
3350 } else {
3351 angle
3352 }
3353
3354 // Typst's `rotate` function is clockwise relative to x-axis, which is backwards from us
3355 angle = angle * -1
3356
3357 // Optionally scale content with current canvas scaling
3358 if style.auto-scale == true {
3359 let sx = vector.len(matrix.column(ctx.transform, 0))
3360 let sy = vector.len(matrix.column(ctx.transform, 1))
3361
3362 body = std.scale(x: sx * 100%, y: sy * 100%, body, reflow: true)
3363 }
3364
3365 // Height from the baseline to content-north
3366 let (content-width, baseline-height) = util.measure(ctx, text(top-edge: "cap-height", bottom-edge: "baseline", body))
3367
3368 // Size of the bounding box
3369 let (width, height, ..) = if auto-size {
3370 util.measure(ctx, text(top-edge: "cap-height", bottom-edge: "bounds", body))
3371 } else {
3372 vector.sub(b, a)
3373 }
3374
3375 let bounds-width = calc.abs(width)
3376 let bounds-height = calc.abs(height)
3377 baseline-height = bounds-height - baseline-height
3378
3379 width = calc.max(0, bounds-width + padding.left + padding.right)
3380 height = calc.max(0, bounds-height + padding.top + padding.bottom)
3381
3382 let anchors = {
3383 let w = width / 2
3384 let h = height / 2
3385 let bh = (baseline-height - padding.top - padding.bottom) / 2
3386
3387 let bounds-center = if auto-size {
3388 a
3389 } else {
3390 vector.lerp(a, b, .5)
3391 }
3392
3393 // Only the center anchor gets transformed. All other anchors
3394 // must be calculated relative to the transformed center!
3395 bounds-center = matrix.mul4x4-vec3(ctx.transform,
3396 vector.as-vec(bounds-center, init: (0,0,0)))
3397
3398 let east-dir = vector.rotate-z((1, 0, 0), angle)
3399 let north-dir = vector.rotate-z((-1, 0, 0), angle + 90deg)
3400 let east-scaled = vector.scale(east-dir, +w)
3401 let west-scaled = vector.scale(east-dir, -w)
3402 let north-scaled = vector.scale(north-dir, +h)
3403 let south-scaled = vector.scale(north-dir, -h)
3404
3405 let north = vector.add(bounds-center, north-scaled)
3406 let south = vector.add(bounds-center, south-scaled)
3407 let east = vector.add(bounds-center, east-scaled)
3408 let west = vector.add(bounds-center, west-scaled)
3409 let north-east = vector.add(bounds-center, vector.add(north-scaled, east-scaled))
3410 let north-west = vector.sub(bounds-center, vector.add(south-scaled, east-scaled))
3411 let south-east = vector.add(bounds-center, vector.add(south-scaled, east-scaled))
3412 let south-west = vector.sub(bounds-center, vector.add(north-scaled, east-scaled))
3413
3414 let base = vector.add(south,
3415 vector.scale(north-dir, padding.bottom + baseline-height))
3416 let mid = vector.lerp(
3417 vector.sub(north, vector.scale(north-dir, padding.top)),
3418 base,
3419 0.5)
3420 let base-east = vector.add(base, east-scaled)
3421 let base-west = vector.add(base, west-scaled)
3422 let text = vector.add(base, vector.scale(east-dir, -content-width / 2))
3423 let mid-east = vector.add(mid, east-scaled)
3424 let mid-west = vector.add(mid, west-scaled)
3425
3426 (
3427 center: bounds-center,
3428 mid: mid,
3429 mid-east: mid-east,
3430 mid-west: mid-west,
3431 base: base,
3432 base-east: base-east,
3433 base-west: base-west,
3434 text: text,
3435 north: north,
3436 north-east: north-east,
3437 north-west: north-west,
3438 south: south,
3439 south-east: south-east,
3440 south-west: south-west,
3441 east: east,
3442 west: west,
3443 )
3444 }
3445
3446 let frame-stroke = if style.frame != none {
3447 style.stroke
3448 }
3449 let frame-fill = if style.frame != none {
3450 style.fill
3451 }
3452 let frame-shape = if style.frame in (none, "rect") {
3453 drawable.path(
3454 path-util.line-segment((
3455 anchors.north-west,
3456 anchors.north-east,
3457 anchors.south-east,
3458 anchors.south-west
3459 )),
3460 close: true,
3461 stroke: frame-stroke,
3462 fill: frame-fill,)
3463 } else if style.frame == "circle" {
3464 let (x, y, z) = util.calculate-circle-center-3pt(anchors.north-west, anchors.south-west, anchors.south-east)
3465 let r = vector.dist((x, y, z), anchors.north-west)
3466 drawable.ellipse(
3467 x, y, z,
3468 r, r,
3469 stroke: frame-stroke,
3470 fill: frame-fill,)
3471 }
3472
3473 let (aabb-width, aabb-height, ..) = aabb.size(aabb.aabb(
3474 (anchors.north-west, anchors.north-east,
3475 anchors.south-west, anchors.south-east)))
3476
3477 let drawables = ()
3478 if frame-shape != none {
3479 drawables.push(frame-shape)
3480 }
3481
3482 // Because of precision problems with some fonts (e.g. "Source Sans 3")
3483 // we need to round the block sizes up. Otherwise, unwanted hyphenation
3484 // gets introduced.
3485 let round-up(v, digits: 8) = {
3486 calc.ceil(v * calc.pow(10, digits)) / calc.pow(10, digits)
3487 }
3488
3489 drawables.push(
3490 drawable.content(
3491 anchors.center,
3492 aabb-width,
3493 aabb-height,
3494 frame-shape.segments,
3495 typst-rotate(angle,
3496 reflow: true,
3497 origin: center + horizon,
3498 block(
3499 width: round-up(width) * ctx.length,
3500 height: round-up(height) * ctx.length,
3501 inset: (
3502 top: padding.at("top", default: 0) * ctx.length,
3503 left: padding.at("left", default: 0) * ctx.length,
3504 bottom: padding.at("bottom", default: 0) * ctx.length,
3505 right: padding.at("right", default: 0) * ctx.length,
3506 ),
3507 text(top-edge: "cap-height", bottom-edge: "baseline", body)
3508 )
3509 )
3510 )
3511 )
3512
3513 let (transform, anchors) = anchor_.setup(
3514 anchor => {
3515 if type(anchor) == str {
3516 anchors.at(anchor)
3517 }
3518 },
3519 anchors.keys(),
3520 default: if auto-size { "center" } else { "north-west" },
3521 offset-anchor: anchor,
3522 transform: none, // Content does not get transformed, see the calculation of anchors.
3523 name: name,
3524 )
3525
3526 return (
3527 ctx: ctx,
3528 name: name,
3529 anchors: anchors,
3530 drawables: drawable.apply-transform(
3531 transform,
3532 drawables
3533 )
3534 )
3535 },)
3536}
3537
3538/// Draws a rectangle between two coordinates.
3539/// ```typc example
3540/// rect((0,0), (1,1))
3541/// rect(
3542/// (-.5, -.5),
3543/// (rel: (2, 2)),
3544/// radius: (
3545/// north-east: (100%, .5),
3546/// south-west: (100%, .5),
3547/// rest: .2
3548/// ),
3549/// stroke: red
3550/// )
3551/// rect((-1, -1), (rel: (3, 3)), radius: .5, stroke: blue)
3552/// ```
3553///
3554/// - a (coordinate): Coordinate of the bottom left corner of the rectangle.
3555/// - b (coordinate): Coordinate of the top right corner of the rectangle. You can draw a rectangle with a specified width and height by using relative coordinates for this parameter `(rel: (width, height))`.
3556/// - name (none,str):
3557/// - anchor (none, str):
3558/// - ..style (style):
3559///
3560/// ## Styling
3561/// *Root*: `rect`
3562/// <Parameter name="radius" types="number,ratio,dictionary" default_value="0">
3563/// The rectangle's corner radius. If set to a single number, that radius is applied to all four corners of the rectangle. If passed a dictionary you can set the radii per corner. The following keys support either a <Type>number</Type>, <Type>ratio</Type> or an array of <Type>number</Type> or <Type>ratio</Type> for specifying a different x- and y-radius: `north`, `east`, `south`, `west`, `north-west`, `north-east`, `south-west` and `south-east`. To set a default value for remaining corners, the `rest` key can be used.
3564///
3565/// Ratio values are relative to the rectangle's width and height.
3566///
3567/// ```typc example vertical
3568/// rect((0,0), (rel: (1,1)), radius: 0)
3569/// rect((2,0), (rel: (1,1)), radius: 25%)
3570/// rect((4,0), (rel: (1,1)), radius: (north: 50%))
3571/// rect((6,0), (rel: (1,1)), radius: (north-east: 50%))
3572/// rect((8,0), (rel: (1,1)), radius: (south-west: 0, rest: 50%))
3573/// rect((10,0), (rel: (1,1)), radius: (rest: (20%, 50%)))
3574/// ```
3575/// </Parameter>
3576///
3577/// ## Anchors
3578/// Supports border and path anchors. It's default is the `"center"` anchor.
3579///
3580#let rect(a, b, name: none, anchor: none, ..style) = {
3581 // Coordinate check
3582 let t = (a, b).map(coordinate.resolve-system)
3583
3584 // No extra positional arguments from the style sink
3585 assert.eq(
3586 style.pos(),
3587 (),
3588 message: "Unexpected positional arguments: " + repr(style.pos()),
3589 )
3590 let style = style.named()
3591
3592 return (
3593 ctx => {
3594 let ctx = ctx
3595 let (ctx, a, b) = coordinate.resolve(ctx, a, b)
3596 assert(a.at(2) == b.at(2),
3597 message: "Both rectangle points must have the same z value.")
3598 (a, b) = {
3599 let lo = (
3600 calc.min(a.at(0), b.at(0)),
3601 calc.min(a.at(1), b.at(1)),
3602 calc.min(a.at(2), b.at(2)),
3603 )
3604 let hi = (
3605 calc.max(a.at(0), b.at(0)),
3606 calc.max(a.at(1), b.at(1)),
3607 calc.max(a.at(2), b.at(2)),
3608 )
3609 (lo, hi)
3610 }
3611
3612 let style = styles.resolve(ctx.style, merge: style, root: "rect")
3613 let (x1, y1, z1) = a
3614 let (x2, y2, z2) = b
3615
3616 let size = (calc.abs(x2 - x1), calc.abs(y2 - y1))
3617 let (north-west: nw, north-east: ne,
3618 south-west: sw, south-east: se) = util.as-corner-radius-dict(ctx, style.radius, size)
3619
3620 let drawables = {
3621 let z = z1
3622
3623 // Compute two corner points offset by radius from origin pt.
3624 //
3625 // x radius * a
3626 // |----|
3627 // --p1←--pt ---
3628 // | | y radius * b
3629 // ↓ |
3630 // p2 ---
3631 // |
3632 //
3633 // parameters a and b function as direction vectors in which
3634 // direction the resulting points p1 and p2 should get offset to.
3635 //
3636 // The point pt is the corner point of the non-rounded rectangle.
3637 // If the radius is zero, we can just return that point for both
3638 // new corners.
3639 let get-corner-pts(radius, pt, a, b) = {
3640 let (rx, ry) = radius
3641 if rx > 0 or ry > 0 {
3642 let (xa, ya) = a
3643 let (xb, yb) = b
3644 (vector.add(pt, (xa * rx, ya * ry)),
3645 vector.add(pt, (xb * rx, yb * ry)))
3646 } else {
3647 (pt, pt)
3648 }
3649 }
3650
3651 // Get segments for arc between start- and stop angle, starting
3652 // at point. If radius is zero for both axes, x and y, nothing
3653 // gets returned.
3654 //
3655 // s----p0/
3656 // p1
3657 // |
3658 // e
3659 //
3660 // Returns a cubic bezier curve between s and e
3661 // with the control points pointing from s in direction
3662 // p0 * radius and from e in direction p1 * radius.
3663 // The bezier approximates a 90 degree arc.
3664 let corner-arc(radius, s, e, p0, p1) = {
3665 let (rx, ry) = radius
3666 if rx > 0 or ry > 0 {
3667 let m = 0.551784
3668 let p0 = (p0.at(0) * m * rx,
3669 p0.at(1) * m * ry)
3670 let p1 = (p1.at(0) * m * rx,
3671 p1.at(1) * m * ry)
3672 (path-util.cubic-segment(s, e,
3673 vector.add(s, p0),
3674 vector.add(e, p1)),)
3675 }
3676 }
3677
3678 // Compute all eight corner points:
3679 //
3680 // p1-------p2
3681 // / | | \
3682 // p0--+ +--p3
3683 // | |
3684 // p7--+ +--p4
3685 // \ | | /
3686 // p6-------p5
3687 //
3688 // If a corner has radius (0,0), both of its
3689 // corner points are the same. See the comment on get-corner-pts
3690 // on how the corners get computed.
3691 let (p0, p1) = get-corner-pts(nw, (x1, y2, z), ( 0,-1), ( 1, 0))
3692 let (p2, p3) = get-corner-pts(ne, (x2, y2, z), (-1, 0), ( 0,-1))
3693 let (p4, p5) = get-corner-pts(se, (x2, y1, z), ( 0, 1), (-1, 0))
3694 let (p6, p7) = get-corner-pts(sw, (x1, y1, z), ( 1, 0), ( 0, 1))
3695
3696 let segments = ()
3697 segments += corner-arc(nw, p1, p0, (-1,0), (0, 1))
3698 if p0 != p7 { segments += (path-util.line-segment((p0, p7)),) }
3699 segments += corner-arc(sw, p7, p6, (0,-1), (-1,0))
3700 if p6 != p5 { segments += (path-util.line-segment((p6, p5)),) }
3701 segments += corner-arc(se, p5, p4, (1, 0), (0,-1))
3702 if p4 != p3 { segments += (path-util.line-segment((p4, p3)),) }
3703 segments += corner-arc(ne, p3, p2, (0, 1), (1, 0))
3704 if p2 != p1 { segments += (path-util.line-segment((p2, p1)),) }
3705
3706 drawable.path(segments, fill: style.fill, stroke: style.stroke, close: true)
3707 }
3708
3709 // Calculate border anchors
3710 let center = vector.lerp(a, b, .5)
3711 let (width, height, ..) = size
3712 let (transform, anchors) = anchor_.setup(
3713 _ => center,
3714 ("center",),
3715 default: "center",
3716 name: name,
3717 offset-anchor: anchor,
3718 transform: ctx.transform,
3719 border-anchors: true,
3720 path-anchors: true,
3721 radii: (width, height),
3722 path: drawables,
3723 )
3724
3725 return (
3726 ctx: ctx,
3727 name: name,
3728 anchors: anchors,
3729 drawables: drawable.apply-transform(transform, drawables),
3730 )
3731 },
3732 )
3733}
3734
3735/// Draws a quadratic or cubic bezier curve
3736///
3737/// ```typc example
3738/// let (a, b, c) = ((0, 0), (2, 0), (1, 1))
3739/// line(a, c, b, stroke: gray)
3740/// bezier(a, b, c)
3741///
3742/// let (a, b, c, d) = ((0, -1), (2, -1), (.5, -2), (1.5, 0))
3743/// line(a, c, d, b, stroke: gray)
3744/// bezier(a, b, c, d)
3745/// ```
3746///
3747/// - start (coordinate): Start position
3748/// - end (coordinate): End position (last coordinate)
3749/// - name (none,str):
3750/// - ..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.
3751///
3752/// ## Styling
3753/// *Root* `bezier`
3754///
3755/// Supports marks.
3756///
3757/// ## Anchors
3758/// Supports path anchors.
3759/// - **ctrl-n**: nth control point where n is an integer starting at 0
3760///
3761#let bezier(start, end, ..ctrl-style, name: none) = {
3762 // Extra positional arguments are treated like control points.
3763 let (ctrl, style) = (ctrl-style.pos(), ctrl-style.named())
3764
3765 // Control point check
3766 let len = ctrl.len()
3767 assert(
3768 len in (1, 2),
3769 message: "Bezier curve expects 1 or 2 control points. Got " + str(len),
3770 )
3771 let coordinates = (start, ..ctrl, end)
3772
3773 // Coordinates check
3774 let t = coordinates.map(coordinate.resolve-system)
3775
3776 return (
3777 ctx => {
3778 let (ctx, start, ..ctrl, end) = coordinate.resolve(ctx, ..coordinates)
3779
3780 if ctrl.len() == 1 {
3781 (start, end, ..ctrl) = bezier_.quadratic-to-cubic(start, end, ..ctrl)
3782 }
3783
3784 let style = styles.resolve(ctx.style, merge: style, root: "bezier")
3785 let drawables = drawable.path(
3786 (path-util.cubic-segment(start, end, ..ctrl),),
3787 fill: style.fill,
3788 fill-rule: style.fill-rule,
3789 stroke: style.stroke,
3790 )
3791
3792 let (transform, anchors) = anchor_.setup(
3793 anchor => (
3794 ctrl-0: ctrl.at(0),
3795 ctrl-1: ctrl.at(1),
3796 ).at(anchor),
3797 ("ctrl-0", "ctrl-1"),
3798 default: "start",
3799 name: name,
3800 transform: ctx.transform,
3801 path-anchors: true,
3802 path: drawables,
3803 )
3804
3805 if mark_.check-mark(style.mark) {
3806 drawables = mark_.place-marks-along-path(ctx, style.mark, transform, drawables)
3807 } else {
3808 drawables = drawable.apply-transform(transform, drawables)
3809 }
3810
3811 return (
3812 ctx: ctx,
3813 name: name,
3814 anchors: anchors,
3815 drawables: drawables,
3816 )
3817 },
3818 )
3819}
3820
3821/// Draws a cubic bezier curve through a set of three points. See [bezier](./bezier) for style and anchor details.
3822///
3823/// ```typc example
3824/// let (a, b, c) = ((0, 0), (1, 1), (2, -1))
3825/// line(a, b, c, stroke: gray)
3826/// bezier-through(a, b, c, name: "b")
3827///
3828/// // Show calculated control points
3829/// line(a, "b.ctrl-0", "b.ctrl-1", c, stroke: gray)
3830/// ```
3831///
3832/// - start (coordinate): The position to start the curve.
3833/// - pass-through (coordinate): The position to pass the curve through.
3834/// - end (coordinate): The position to end the curve.
3835/// - name (none,str):
3836/// - ..style (style):
3837#let bezier-through(start, pass-through, end, name: none, ..style) = {
3838 assert.eq(style.pos(), (), message: "Unexpected positional arguments: " + repr(style.pos()))
3839 style = style.named()
3840
3841 return (ctx => {
3842 let (ctx, start, pass-through, end) = coordinate.resolve(ctx, start, pass-through, end)
3843
3844 let (start, end, ..control) = bezier_.cubic-through-3points(start, pass-through, end)
3845
3846 return bezier(start, end, ..control, ..style, name: name).first()(ctx)
3847 },)
3848}
3849
3850/// Draws a Catmull-Rom curve through a set of points.
3851///
3852/// ```typc example
3853/// catmull((0,0), (1,1), (2,-1), (3,0), tension: .4, stroke: blue)
3854/// catmull((0,0), (1,1), (2,-1), (3,0), tension: .5, stroke: red)
3855/// ```
3856///
3857/// - ..pts-style (coordinate,style): Positional arguments should be coordinates that the curve should pass through. Named arguments are for styling.
3858/// - close (bool): Closes the curve with a straight line between the start and end of the curve.
3859/// - name (none,str):
3860///
3861/// ## Styling
3862/// *Root*: `catmull`
3863///
3864/// Supports marks.
3865///
3866/// - tension (float) = 0.5: How tight the curve should fit to the points. The higher the tension the less curvy the curve.
3867///
3868/// ## Anchors
3869/// Supports path anchors.
3870/// - **pt-n**: The nth given position (0 indexed so "pt-0" is equal to "start")
3871#let catmull(..pts-style, close: false, name: none) = {
3872 let (pts, style) = (pts-style.pos(), pts-style.named())
3873
3874 assert(pts.len() >= 2, message: "Catmull-rom curve requires at least two points. Got " + repr(pts.len()) + "instead.")
3875
3876 pts.map(coordinate.resolve-system)
3877
3878 return (ctx => {
3879 let (ctx, ..pts) = coordinate.resolve(ctx, ..pts)
3880 let style = styles.resolve(ctx.style, merge: style, root: "catmull")
3881
3882 let curves = bezier_.catmull-to-cubic(
3883 pts,
3884 style.tension,
3885 close: close)
3886
3887 let segments = curves.map(c => path-util.cubic-segment(..c))
3888 let drawables = drawable.path(
3889 segments,
3890 fill: style.fill,
3891 fill-rule: style.fill-rule,
3892 stroke: style.stroke,
3893 close: close)
3894
3895 let (transform, anchors) = {
3896 let a = for (i, pt) in pts.enumerate() {
3897 (("pt-" + str(i)): pt)
3898 }
3899 anchor_.setup(
3900 anchor => a.at(anchor), // Would like to return just `a.at` but Typst is mean :<
3901 a.keys(),
3902 name: name,
3903 default: "start",
3904 transform: ctx.transform,
3905 path-anchors: true,
3906 path: drawables,
3907 )
3908 }
3909
3910 if mark_.check-mark(style.mark) {
3911 drawables = mark_.place-marks-along-path(ctx, style.mark, transform, drawables)
3912 } else {
3913 drawables = drawable.apply-transform(transform, drawables)
3914 }
3915
3916 return (
3917 ctx: ctx,
3918 name: name,
3919 anchors: anchors,
3920 drawables: drawables,
3921 )
3922 },)
3923}
3924
3925/// Draws a Hobby curve through a set of points.
3926///
3927/// ```typc example
3928/// hobby((0, 0), (1, 1), (2, -1), (3, 0), omega: 0, stroke: blue)
3929/// hobby((0, 0), (1, 1), (2, -1), (3, 0), omega: 1, stroke: red)
3930/// ```
3931///
3932/// - ..pts-style (coordinate,style): Positional arguments are the coordinates to use to draw the curve with, a minimum of two is required. Named arguments are for styling.
3933/// - tb (auto,array): Incoming tension at `pts.at(n+1)` from `pts.at(n)` to `pts.at(n+1)`. The number given must be one less than the number of points.
3934/// - 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.
3935/// - close (bool): Closes the curve with a proper smooth curve between the start and end of the curve.
3936/// - name (none,str):
3937///
3938/// ## Styling
3939/// *Root* `hobby`
3940///
3941/// Supports marks.
3942/// - 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.
3943///
3944/// ## Anchors
3945/// Supports path anchors.
3946/// - **pt-n**: The nth given position (0 indexed, so "pt-0" is equal to "start")
3947#let hobby(..pts-style, ta: auto, tb: auto, close: false, name: none) = {
3948 let (pts, style) = (pts-style.pos(), pts-style.named())
3949
3950 assert(pts.len() >= 2, message: "Hobby curve requires at least two points. Got " + repr(pts.len()) + "instead.")
3951
3952 pts.map(coordinate.resolve-system)
3953
3954 return (ctx => {
3955 let (ctx, ..pts) = coordinate.resolve(ctx, ..pts)
3956 let style = styles.resolve(ctx.style, merge: style, root: "hobby")
3957
3958 let curves = hobby_.hobby-to-cubic(
3959 pts,
3960 ta: ta,
3961 tb: tb,
3962 omega: style.omega,
3963 close: close)
3964
3965 let segments = curves.map(c => path-util.cubic-segment(..c))
3966 let drawables = drawable.path(
3967 segments,
3968 fill: style.fill,
3969 fill-rule: style.fill-rule,
3970 stroke: style.stroke,
3971 close: close)
3972
3973 let (transform, anchors) = {
3974 let a = for (i, pt) in pts.enumerate() {
3975 (("pt-" + str(i)): pt)
3976 }
3977 anchor_.setup(
3978 anchor => {
3979 if type(anchor) == str and anchor in a {
3980 return a.at(anchor)
3981 }
3982 },
3983 a.keys(),
3984 name: name,
3985 default: "start",
3986 transform: ctx.transform,
3987 path-anchors: true,
3988 path: drawables,
3989 )
3990 }
3991
3992 if mark_.check-mark(style.mark) {
3993 drawables = mark_.place-marks-along-path(ctx, style.mark, transform, drawables)
3994 } else {
3995 drawables = drawable.apply-transform(transform, drawables)
3996 }
3997
3998 return (
3999 ctx: ctx,
4000 name: name,
4001 anchors: anchors,
4002 drawables: drawables,
4003 )
4004 },)
4005}
4006
4007/// Merges two or more paths by concattenating their elements. Anchors and visual styling, such as `stroke` and `fill`, are not preserved. When an element's path does not start at the same position the previous element's path ended, a straight line is drawn between them so that the final path is continuous. You must then pay attention to the direction in which element paths are drawn.
4008///
4009/// ```typc example
4010/// merge-path(fill: white, {
4011/// line((0, 0), (1, 0))
4012/// bezier((), (0, 0), (1,1), (0,1))
4013/// })
4014/// ```
4015///
4016/// Elements hidden via @@hide() are ignored.
4017///
4018/// ## Anchors
4019/// **centroid**: Centroid of the _closed and non self-intersecting_ shape. Only exists if `close` is true.
4020/// Supports path anchors and shapes where all vertices share the same z-value.
4021///
4022/// - body (elements): Elements with paths to be merged together.
4023/// - close (bool): Close the path with a straight line from the start of the path to its end.
4024/// - name (none,str):
4025/// - ..style (style):
4026#let merge-path(body, close: false, name: none, ..style) = {
4027 // No extra positional arguments from the style sink
4028 assert.eq(
4029 style.pos(),
4030 (),
4031 message: "Unexpected positional arguments: " + repr(style.pos()),
4032 )
4033 let style = style.named()
4034
4035 return (
4036 ctx => {
4037 let ctx = ctx
4038 let segments = ()
4039 for element in body {
4040 let r = process.element(ctx, element)
4041 if r != none {
4042 ctx = r.ctx
4043 if segments != () and r.drawables != () {
4044 assert.eq(r.drawables.first().type, "path")
4045 let start = path-util.segment-end(segments.last())
4046 let end = path-util.segment-start(r.drawables.first().segments.first())
4047 if vector.dist(start, end) > 0 {
4048 segments.push(path-util.line-segment((start, end,)))
4049 }
4050 }
4051 for drawable in r.drawables {
4052 if drawable.hidden { continue }
4053 assert.eq(drawable.type, "path")
4054 segments += drawable.segments
4055 }
4056 }
4057 }
4058
4059 let style = styles.resolve(ctx.style, merge: style)
4060 let drawables = drawable.path(fill: style.fill, fill-rule: style.fill-rule, stroke: style.stroke, close: close, segments)
4061
4062 let (transform, anchors) = anchor_.setup(
4063 name => {
4064 if name == "centroid" {
4065 // Try finding a closed shapes center by
4066 // Sampling it to a polygon.
4067 return polygon_.simple-centroid(polygon_.from-segments(drawables.segments))
4068 }
4069 },
4070 if close != none { ("centroid",) } else { () },
4071 name: name,
4072 transform: none,
4073 path-anchors: true,
4074 path: drawables,
4075 )
4076
4077 return (
4078 ctx: ctx,
4079 name: name,
4080 anchors: anchors,
4081 drawables: drawables,
4082 )
4083 },
4084 )
4085}
4086#import "/src/util.typ"
4087
4088/// Set current style
4089///
4090/// - ..style (style): Style key-value pairs
4091#let set-style(..style) = {
4092 assert.eq(
4093 style.pos().len(),
4094 0,
4095 message: "set-style takes no positional arguments",
4096 )
4097
4098 (ctx => {
4099 ctx.style = util.merge-dictionary(ctx.style, style.named())
4100
4101 return (ctx: ctx)
4102 },)
4103}
4104
4105/// Set current fill style
4106///
4107/// Shorthand for `set-style(fill: <fill>)`
4108///
4109/// - fill (paint): Fill style
4110#let fill(fill) = set-style(fill: fill)
4111
4112/// Set current stroke style
4113///
4114/// Shorthand for `set-style(stroke: <fill>)`
4115///
4116/// - stroke (stroke): Stroke style
4117#let stroke(stroke) = set-style(stroke: stroke)
4118
4119/// Register a custom mark to the canvas
4120///
4121/// The mark should contain both anchors called **tip** and **base** that are used to determine the marks orientation. If unset both default to `(0, 0)`.
4122/// An anchor named **center** is used as center of the mark, if present. Otherwise the mid between **tip** and **base** is used.
4123///
4124/// ```typc example
4125/// register-mark(":)", style => {
4126/// circle((0,0), radius: .5, fill: yellow)
4127/// arc((0,0), start: 180deg + 30deg, delta: 180deg - 60deg, anchor: "origin", radius: .3)
4128/// circle((-0.15, 0.15), radius: .1, fill: white)
4129/// circle((-0.10, 0.10), radius: .025, fill: black)
4130/// circle(( 0.15, 0.15), radius: .1, fill: white)
4131/// circle(( 0.20, 0.10), radius: .025, fill: black)
4132///
4133/// anchor("tip", ( 0.5, 0))
4134/// anchor("base", (-0.5, 0))
4135/// })
4136///
4137/// line((0,0), (3,0), mark: (end: ":)"))
4138/// ```
4139///
4140/// - symbol (str): Mark name
4141/// - mnemonic (none,str): Mark short name
4142/// - body (function): Mark drawing callback, receiving the mark style as argument and returning elements. Format `(styles) => elements`.
4143#let register-mark(symbol, body, mnemonic: none) = {
4144 assert(type(symbol) == str)
4145 assert(type(body) == function)
4146
4147 (ctx => {
4148 ctx.marks.marks.insert(symbol, body)
4149 if type(mnemonic) == str and mnemonic.len() > 0 {
4150 ctx.marks.mnemonics.insert(mnemonic, symbol)
4151 }
4152 return (ctx: ctx)
4153 },)
4154}
4155#import "/src/coordinate.typ"
4156#import "/src/matrix.typ"
4157#import "/src/vector.typ"
4158#import "/src/util.typ"
4159
4160// Utility for applying translation to and from
4161// the origin to apply a transformation matrix to.
4162//
4163// - ctx (context): Context
4164// - transform (matrix): Transformation matrix
4165// - origin (coordinate): Origin coordinate or none
4166#let _transform-around-origin(ctx, transform, origin) = {
4167 if origin != none {
4168 let (_, origin) = coordinate.resolve(ctx, origin, update: false)
4169 let a = matrix.transform-translate(..origin)
4170 let b = matrix.transform-translate(..vector.scale(origin, -1))
4171
4172 matrix.mul-mat(a, matrix.mul-mat(transform, b))
4173 } else {
4174 transform
4175 }
4176}
4177
4178/// Sets the transformation matrix.
4179///
4180/// - mat (none, matrix): The 4x4 transformation matrix to set. If `none` is passed, the transformation matrix is set to the identity matrix (`matrix.ident()`).
4181#let set-transform(mat) = {
4182 let mat = if mat == none {
4183 matrix.ident()
4184 } else {
4185 matrix.round(mat)
4186 }
4187
4188 assert(
4189 type(mat) == array,
4190 message: "Transformtion matrix must be of type array, got: " + repr(mat)
4191 )
4192 assert.eq(
4193 mat.len(),
4194 4,
4195 message: "Transformation matrix must be of size 4x4, got: " + repr(mat)
4196 )
4197
4198 (ctx => {
4199 ctx.transform = mat
4200 return (ctx: ctx)
4201 },)
4202}
4203
4204/// Rotates the transformation matrix on the z-axis by a given angle or other axes when specified.
4205///
4206/// ```typc example
4207/// // Rotate on z-axis
4208/// rotate(z: 45deg)
4209/// rect((-1,-1), (1,1))
4210/// // Rotate on y-axis
4211/// rotate(y: 80deg)
4212/// circle((0,0))
4213/// ```
4214///
4215/// - ..angles (angle): A single angle as a positional argument to rotate on the z-axis by.
4216/// Named arguments of `x`, `y` or `z` can be given to rotate on their respective axis.
4217/// You can give named arguments of `yaw`, `pitch` or `roll`, too.
4218/// - origin (none,coordinate): Origin to rotate around, or (0, 0, 0) if set to `none`.
4219#let rotate(..angles, origin: none) = {
4220 assert(angles.pos().len() == 1 or angles.named().len() > 0,
4221 message: "Rotate takes a single z-angle or angles " +
4222 "(x, y, z or yaw, pitch, roll) as named arguments, got: " + repr(angles))
4223
4224 let named = angles.named()
4225 let names = named.keys()
4226
4227 let mat = if angles.pos().len() == 1 {
4228 matrix.transform-rotate-z(angles.pos().at(0))
4229 } else if names.all(n => n in ("x", "y", "z")) {
4230 matrix.transform-rotate-xyz(named.at("x", default: 0deg),
4231 named.at("y", default: 0deg),
4232 named.at("z", default: 0deg))
4233 } else if names.all(n => n in ("yaw", "pitch", "roll")) {
4234 matrix.transform-rotate-ypr(named.at("yaw", default: 0deg),
4235 named.at("pitch", default: 0deg),
4236 named.at("roll", default: 0deg))
4237 } else {
4238 panic("Invalid rotate arguments." +
4239 "Rotate expects: A single (z-axis) angle or any combination of x, y,z or any combination of yaw, pitch, roll. " +
4240 "Got: " + repr(named))
4241 }
4242
4243 (ctx => {
4244 ctx.transform = matrix.mul-mat(ctx.transform,
4245 _transform-around-origin(ctx, mat, origin))
4246 return (ctx: ctx)
4247 },)
4248}
4249
4250/// Translates the transformation matrix by the given vector or dictionary.
4251///
4252/// ```typc example
4253/// // Outer rect
4254/// rect((0, 0), (2, 2))
4255/// // Inner rect
4256/// translate(x: .5, y: .5)
4257/// rect((0, 0), (1, 1))
4258/// ```
4259///
4260/// - ..args (vector, float, length): A single vector or any combination of the named arguments `x`, `y` and `z` to translate by.
4261/// A translation matrix with the given offsets gets multiplied with the current transformation depending on the value of `pre`.
4262/// - pre (bool): Specify matrix multiplication order
4263/// - false: `World = World * Translate`
4264/// - true: `World = Translate * World`
4265#let translate(..args, pre: false) = {
4266 assert((args.pos().len() == 1 and args.named() == (:)) or
4267 (args.pos() == () and args.named() != (:)),
4268 message: "Expected a single positional argument or one or more named arguments, got: " + repr(args))
4269
4270 let pos = args.pos()
4271 let named = args.named()
4272
4273 let vec = if named != (:) {
4274 (named.at("x", default: 0), named.at("y", default: 0), named.at("z", default: 0))
4275 } else {
4276 vector.as-vec(pos.at(0), init: (0, 0, 0))
4277 }
4278
4279 (ctx => {
4280 // Allow translating by length values
4281 let vec = vec.map(v => if type(v) == length {
4282 util.resolve-number(ctx, v)
4283 } else {
4284 v
4285 })
4286
4287 let t = matrix.transform-translate(..vec)
4288 if pre {
4289 ctx.transform = matrix.mul-mat(t, ctx.transform)
4290 } else {
4291 ctx.transform = matrix.mul-mat(ctx.transform, t)
4292 }
4293 return (ctx: ctx)
4294 },)
4295}
4296
4297/// Scales the transformation matrix by the given factor(s).
4298///
4299/// ```typc example
4300/// // Scale the y-axis
4301/// scale(y: 50%)
4302/// circle((0,0))
4303/// ```
4304///
4305/// - ..args (float, ratio): A single value to scale the transformation matrix by or per axis
4306/// scaling factors. Accepts a single float or ratio value or any combination of the named arguments
4307/// `x`, `y` and `z` to set per axis scaling factors. A ratio of 100% is the same as the value $1$.
4308/// - origin (none,coordinate): Origin to rotate around, or (0, 0, 0) if set to `none`.
4309#let scale(..args, origin: none) = {
4310 assert((args.pos().len() == 1 and args.named() == (:)) or
4311 (args.pos() == () and args.named() != (:)),
4312 message: "Expected a single positional argument or one or more named arguments, got: " + repr(args))
4313
4314 let pos = args.pos()
4315 let named = args.named()
4316
4317 let vec = if args.named() != (:) {
4318 (named.at("x", default: 1), named.at("y", default: 1), named.at("z", default: 1))
4319 } else if type(pos.at(0)) == array {
4320 vector.as-vec(pos, init: (1, 1, 1))
4321 } else {
4322 let factor = pos.at(0)
4323 (factor, factor, factor)
4324 }
4325
4326 // Allow scaling using ratio values
4327 vec = vec.map(v => if type(v) == ratio {
4328 v / 100%
4329 } else {
4330 v
4331 })
4332
4333 (ctx => {
4334 let mat = matrix.transform-scale(vec)
4335 ctx.transform = matrix.mul-mat(ctx.transform,
4336 _transform-around-origin(ctx, mat, origin))
4337 return (ctx: ctx)
4338 },)
4339}
4340
4341/// Sets the given position as the new origin `(0, 0, 0)`
4342///
4343/// ```typc example
4344/// // Outer rect
4345/// rect((0,0), (2,2), name: "r")
4346/// // Move origin to top edge
4347/// set-origin("r.north")
4348/// circle((0, 0), radius: .1)
4349/// ```
4350///
4351/// - origin (coordinate): Coordinate to set as new origin `(0,0,0)`
4352#let set-origin(origin) = {
4353 (
4354 ctx => {
4355 let (ctx, c) = coordinate.resolve(ctx, origin)
4356 let (x, y, z) = vector.sub(
4357 util.apply-transform(ctx.transform, c),
4358 util.apply-transform(ctx.transform, (0, 0, 0)),
4359 )
4360 ctx.transform = matrix.mul-mat(matrix.transform-translate(x, y, z), ctx.transform)
4361 return (ctx: ctx)
4362 },
4363 )
4364}
4365
4366/// Sets the previous coordinate.
4367///
4368/// The previous coordinate can be used via `()` (empty coordinate).
4369/// It is also used as base for relative coordinates if not specified
4370/// otherwise.
4371///
4372/// ```typc example
4373/// circle((), radius: .25)
4374/// move-to((1,0))
4375/// circle((), radius: .15)
4376/// ```
4377///
4378/// - pt (coordinate): The coordinate to move to.
4379#let move-to(pt) = {
4380 let t = coordinate.resolve-system(pt)
4381
4382 return (ctx => {
4383 let (ctx, pt) = coordinate.resolve(ctx, pt)
4384 return (ctx: ctx)
4385 },)
4386}
4387
4388/// Span viewport between two coordinates and set-up scaling and translation
4389///
4390/// ```typc example
4391/// rect((0,0), (2,2))
4392/// set-viewport((0,0), (2,2), bounds: (10, 10))
4393/// circle((5,5))
4394/// ```
4395///
4396/// - from (coordinate): Bottom left corner coordinate
4397/// - to (coordinate): Top right corner coordinate
4398/// - bounds (vector): Viewport bounds vector that describes the inner width,
4399/// height and depth of the viewport
4400#let set-viewport(from, to, bounds: (1, 1, 1)) = {
4401 (from, to).map(coordinate.resolve-system)
4402
4403 return (ctx => {
4404 let bounds = vector.as-vec(bounds, init: (1, 1, 1))
4405
4406 let (ctx, from, to) = coordinate.resolve(ctx, from, to)
4407 let (fx, fy, fz) = from
4408 let (tx, ty, tz) = to
4409
4410 // Compute scaling
4411 let (sx, sy, sz) = vector.sub((tx, ty, tz),
4412 (fx, fy, fz)).enumerate().map(((i, v)) => if bounds.at(i) == 0 {
4413 0
4414 } else {
4415 v / bounds.at(i)
4416 })
4417
4418 ctx.transform = matrix.mul-mat(ctx.transform,
4419 matrix.transform-translate(fx, fy, fz))
4420 ctx.transform = matrix.mul-mat(ctx.transform,
4421 matrix.transform-scale((sx, sy, sz)))
4422 return (ctx: ctx)
4423 },)
4424}
4425
4426/// Assert that the cetz version of the canvas matches the given version (range).
4427///
4428/// min (version): Minimum version (current >= min)
4429/// max (none, version): First unsupported version (current < max)
4430/// hint (string): Name of the function/module this assert is called from
4431#let assert-version(min, max: none, hint: "") = {
4432 if hint != "" { hint = " by " + hint }
4433 (ctx => {
4434 /* Default to 2.0.0, as this is the first version that had elements as single functions. */
4435 let v = ctx.at("version", default: version(0,2,0))
4436 assert(min <= v,
4437 message: "CeTZ canvas version is " + str(v) + ", but the minimum required version" + hint + " is " + str(min))
4438 if max != none {
4439 assert(max > v,
4440 message: "CeTZ canvas version is " + str(v) + ", but the maximum supported version" + hint + " is " + str(min))
4441 }
4442
4443 return (ctx: ctx)
4444 },)
4445}
4446#import "vector.typ"
4447#import "util.typ"
4448#import "path-util.typ"
4449
4450/// Applies a transform to drawables. If a single drawable is given it will be returned in a single element <Type>array</Type>.
4451/// - transform (matrix): The transformation matrix.
4452/// - drawables (drawable): The drawables to transform.
4453/// -> drawable
4454#let apply-transform(transform, drawables) = {
4455 if type(drawables) == dictionary {
4456 drawables = (drawables,)
4457 }
4458 if drawables.len() == 0 {
4459 return ()
4460 }
4461 if transform == none {
4462 return drawables
4463 }
4464 for drawable in drawables {
4465 assert(type(drawable) != array,
4466 message: "Expected drawable, got array: " + repr(drawable))
4467 if drawable.type == "path" {
4468 drawable.segments = drawable.segments.map(((kind, ..pts)) => {
4469 return (kind,) + util.apply-transform(transform, ..pts)
4470 })
4471 } else if drawable.type == "content" {
4472 drawable.pos = util.apply-transform(transform, drawable.pos)
4473 } else {
4474 panic()
4475 }
4476 (drawable,)
4477 }
4478}
4479
4480/// Creates a path drawable from path segements.
4481/// - segments (array): The segments to create the path from.
4482/// - close (bool): If `true` the path will be closed.
4483/// - fill (color,none): The color to fill the path with.
4484/// - fill-rule (string): One of "even-odd" or "non-zero".
4485/// - stroke (stroke): The stroke of the path.
4486/// -> drawable
4487#let path(close: false, fill: none, stroke: none, fill-rule: "non-zero", segments) = {
4488 let segments = segments
4489 // Handle case where only one segment has been passed
4490 if type(segments.first()) == str {
4491 segments = (segments,)
4492 }
4493
4494 segments = path-util.normalize(segments)
4495 if close and path-util.segment-end(segments.last()) != path-util.segment-start(segments.first()) {
4496 segments.push(path-util.line-segment((
4497 path-util.segment-end(segments.last()),
4498 path-util.segment-start(segments.first()),
4499 )))
4500 }
4501
4502 return (
4503 type: "path",
4504 close: close,
4505 segments: segments,
4506 fill: fill,
4507 fill-rule: fill-rule,
4508 stroke: stroke,
4509 hidden: false,
4510 bounds: true,
4511 )
4512}
4513
4514
4515/// Creates a content drawable.
4516/// - pos (vector): The position of the drawable.
4517/// - width (float): The width of the drawable.
4518/// - height (float): The height of the drawable.
4519/// - border (segment): A segment to define the border of the drawable with.
4520/// - body (content): The content of the drawable.
4521/// -> drawable
4522#let content(pos, width, height, border, body) = {
4523 return (
4524 type: "content",
4525 pos: pos,
4526 width: width,
4527 height: height,
4528 segments: border,
4529 body: body,
4530 hidden: false,
4531 bounds: true,
4532 )
4533}
4534
4535/// Creates a path drawable in the shape of an ellipse.
4536/// - x (float): The $x$ position of the ellipse.
4537/// - y (float): The $y$ position of the ellipse.
4538/// - z (float): The $z$ position of the ellipse.
4539/// - rx (float): The radius of the ellipse in the $x$ axis.
4540/// - ry (float): The radius of the ellipse in the $y$ axis.
4541/// - fill (color,none): The color to fill the ellipse with.
4542/// - stroke (stroke): The stroke of the ellipse's path.
4543/// -> drawable
4544#let ellipse(x, y, z, rx, ry, fill: none, stroke: none) = {
4545 let m = 0.551784
4546 let mx = m * rx
4547 let my = m * ry
4548 let left = x - rx
4549 let right = x + rx
4550 let top = y + ry
4551 let bottom = y - ry
4552
4553 path(
4554 (
4555 path-util.cubic-segment(
4556 (x, top, z),
4557 (left, y, z),
4558 (x - m * rx, top, z),
4559 (left, y + m * ry, z),
4560 ),
4561 path-util.cubic-segment(
4562 (left, y, z),
4563 (x, bottom, z),
4564 (left, y - m * ry, z),
4565 (x - m * rx, bottom, z),
4566 ),
4567 path-util.cubic-segment(
4568 (x, bottom, z),
4569 (right, y, z),
4570 (x + m * rx, bottom, z),
4571 (right, y - m * ry, z),
4572 ),
4573 path-util.cubic-segment(
4574 (right, y, z),
4575 (x, top, z),
4576 (right, y + m * ry, z),
4577 (x + m * rx, top, z)
4578 ),
4579 ),
4580 stroke: stroke,
4581 fill: fill,
4582 close: true,
4583 )
4584}
4585
4586/// Creates a path drawable in the shape of an arc.
4587/// - x (float): The $x$ position of the start of the arc.
4588/// - y (float): The $y$ position of the start of the arc.
4589/// - z (float): The $z$ position of the start of the arc.
4590/// - start (angle): The angle along an ellipse to start drawing the arc from.
4591/// - stop (angle): The angle along an ellipse to stop drawing the arc at.
4592/// - rx (float): The radius of the arc in the $x$ axis.
4593/// - ry (float): The radius of the arc in the $y$ axis.
4594/// - mode (str): How to draw the arc: `"OPEN"` leaves the path open, `"CLOSED"` closes the arc by drawing a straight line between the end of the arc and its start, `"PIE"` also closes the arc by drawing a line from its end to its origin then to its start.
4595/// - fill (color,none): The color to fill the arc with.
4596/// - stroke (stroke): The stroke of the arc's path.
4597/// -> drawable
4598#let arc(x, y, z, start, stop, rx, ry, mode: "OPEN", fill: none, stroke: none) = {
4599 let delta = calc.max(-360deg, calc.min(stop - start, 360deg))
4600 let num-curves = calc.max(1, calc.min(calc.ceil(calc.abs(delta) / 90deg), 4))
4601
4602 // Move x/y to the center
4603 x -= rx * calc.cos(start)
4604 y -= ry * calc.sin(start)
4605
4606 // Calculation of control points is based on the method described here:
4607 // https://pomax.github.io/bezierinfo/#circles_cubic
4608 let segments = ()
4609 for n in range(0, num-curves) {
4610 let start = start + delta / num-curves * n
4611 let stop = start + delta / num-curves
4612
4613 let d = delta / num-curves
4614 let k = 4 / 3 * calc.tan(d / 4)
4615
4616 let sx = x + rx * calc.cos(start)
4617 let sy = y + ry * calc.sin(start)
4618 let ex = x + rx * calc.cos(stop)
4619 let ey = y + ry * calc.sin(stop)
4620
4621 let s = (sx, sy, z)
4622 let c1 = (
4623 x + rx * (calc.cos(start) - k * calc.sin(start)),
4624 y + ry * (calc.sin(start) + k * calc.cos(start)),
4625 z,
4626 )
4627 let c2 = (
4628 x + rx * (calc.cos(stop) + k * calc.sin(stop)),
4629 y + ry * (calc.sin(stop) - k * calc.cos(stop)),
4630 z,
4631 )
4632 let e = (ex, ey, z)
4633
4634 segments.push(path-util.cubic-segment(s, e, c1, c2))
4635 }
4636
4637 if mode == "PIE" and calc.abs(delta) < 360deg {
4638 segments.push(path-util.line-segment((
4639 path-util.segment-end(segments.last()),
4640 (x, y, z),
4641 path-util.segment-start(segments.first()))))
4642 }
4643
4644 return path(
4645 fill: fill,
4646 stroke: stroke,
4647 close: mode != "OPEN",
4648 segments
4649 )
4650}
4651#import "/src/vector.typ"
4652#import "/src/complex.typ"
4653
4654// Implementation by @Enivex
4655//
4656// Notation comes from https://tug.org/TUGboat/tb34-2/tb107jackowski.pdf
4657//
4658// points = (P_0, P_1, ... , P_n)
4659//
4660// ta = (tau_(a,0),tau_(a,1), ... , tau_(a,n-1))
4661// ta(i) = tau_(a,i) is the outgoing tension at P_i on the curve from P_i to P_(i+1)
4662//
4663// tb = (tau_(b,0),tau(b,1), ..., tau_(b,n-1))
4664// tb(i) = tau_(b,i) is the incoming tension at P_(i+1) on the curve from P_i to P_(i+1)
4665//
4666// omega = (omega_0,omega_n)
4667// curl at the start and end of curve (size of mock curvature relative to nearest point)
4668//
4669// v(i) = P_(i+1) - P_i
4670// vector pointing from point i to point i + 1 (direction of ith chord)
4671//
4672// d(i) = |v(i)|
4673// length of the ith chord
4674//
4675// gamma(i) = signed angle from v(i - 1) to v(i) (change in angle at P_i)
4676// Note: gamma(0) = gamma(n) = 0
4677//
4678// ca(i) = first control point on chord i
4679// cb(i) = second control point on chord i
4680//
4681// alpha(i) = signed angle from v(i) to ca(i) - P(i)
4682// beta(i) = signed angle from P(i + 1) - cb(i) to v(i)
4683
4684// Solve tridiagonal system
4685//
4686// - a,b,c,d have the same length, n + 1
4687// - a(0) and c(n) are not used
4688//
4689// Solves Ax=d
4690// where A=
4691// [ b_0 c_0
4692// a_1 b_1 c_1
4693// ......
4694// a_(n-1) b_(n-1) c_(n-1)
4695// a_n b_n ]
4696#let thomas(a, b, c, d) = {
4697 let n = a.len() - 1
4698
4699 for i in range(1,n + 1) {
4700 let w = a.at(i) / b.at(i - 1)
4701 b.at(i) = b.at(i) - w*c.at(i - 1)
4702 d.at(i) = d.at(i) - w*d.at(i - 1)
4703 }
4704
4705 let x = (0,)*(n + 1)
4706 x.last() = d.last() / b.last()
4707 for i in range(n - 1, -1 , step: -1) {
4708 x.at(i) = (d.at(i) - c.at(i)*x.at(i + 1)) / b.at(i)
4709 }
4710 return x
4711}
4712
4713// Solve cyclic tridiagonal system
4714//
4715// - a,b,c,d have the same length, n+1
4716//
4717// Solves Ax=d
4718// where A=
4719// [ b_0 c_0 a_0
4720// a_1 b_1 c_1
4721// ......
4722// a_(n-1) b_(n-1) c_(n-1)
4723// c_n a_n b_n ]
4724#let thomas-cyclic(a, b, c, d) = {
4725 let n = a.len() - 1
4726
4727 let u = (a.first(),) + (0,)*(n - 1) + (c.last(),)
4728 let v = (1,) + (0,)*(n - 1) + (1,)
4729
4730 let bp = array.zip(b,u).map(((s,t)) => s - t)
4731 let y = thomas(a, bp, c, d)
4732 let z = thomas(a, bp, c, u)
4733
4734 // Sherman-Morrison formula
4735 return vector.sub(y, vector.scale(z, vector.dot(v,y) / (1 + vector.dot(v, z))))
4736}
4737
4738/// Calculates a bezier spline for an open Hobby curve through a list of points. Returns an {{array}} of {{bezier}}s
4739///
4740/// - points (array): List of points
4741/// - ta (auto,array): Outgoing tension per point
4742/// - tb (auto,array): Incoming tension per point
4743/// - rho (auto,function): The rho function of the form `(float, float) => float`
4744/// - omega (auto,array): Tuple of the curl at the start end end of the curve `(start, end)` as floats
4745///
4746/// -> array
4747#let hobby-to-cubic-open(points, ta: auto, tb: auto, rho: auto, omega: auto) = {
4748 let n = points.len() - 1
4749
4750 if ta == auto {
4751 ta = (1,)*n
4752 } else {
4753 assert.eq(type(ta), array, message: "ta must be an array")
4754 assert.eq(ta.len(), n, message: "ta must have length n for n + 1 points")
4755 assert(ta.all(x => x > 0), message: "ta must contain only positive numbers")
4756 }
4757 if tb == auto {
4758 tb = (1,)*n
4759 } else {
4760 assert.eq(type(tb), array, message: "tb must be an array")
4761 assert.eq(tb.len(), n, message: "tb must have length n for n + 1 points")
4762 assert(tb.all(x => x > 0), message: "tb must contain only positive numbers")
4763 }
4764 if rho == auto {
4765 rho = (a,b) => {
4766 (2 + calc.sqrt(2)*(calc.sin(a) - calc.sin(b)/16)*(calc.sin(b)-calc.sin(a)/16)*(calc.cos(a)-calc.cos(b)))/(1 + calc.cos(a)*(calc.sqrt(5)-1)/2 + calc.cos(b)*(3-calc.sqrt(5))/2)
4767 }
4768 } else {
4769 assert.eq(type(rho), function,
4770 message: "rho must be a function")
4771 }
4772
4773 let v = range(n).map(i => complex.sub(points.at(i + 1),points.at(i)))
4774 let d = v.map(complex.norm)
4775
4776 let gamma = (0,) + range(n - 1).map(i => complex.ang(v.at(i),v.at(i + 1))) + (0,)
4777
4778 let ita = ta.map(x => 1/x)
4779 let itasq = ita.map(x => x*x)
4780
4781 let itb = tb.map(x => 1/x)
4782 let itbsq = itb.map(x => x*x)
4783
4784 let (omega0, omegan) = omega
4785
4786 let A = (0,) * (n + 1); let B = A; let C = A; let D = A; let E = A
4787
4788 C.at(0) = omega0 * ita.at(0) * itasq.at(0) / itbsq.at(0) + 3 - itb.at(0)
4789 D.at(0) = omega0 * itasq.at(0) / itbsq.at(0) * (3 - ita.at(0)) + itb.at(0)
4790 E.at(0) = - D.at(0) * gamma.at(1)
4791
4792 for i in range(1, n) {
4793 A.at(i) = ita.at(i - 1) / (d.at(i - 1) * itbsq.at(i - 1))
4794 B.at(i) = (3 - ita.at(i - 1))/(d.at(i - 1) * itbsq.at(i - 1))
4795 C.at(i) = (3 - itb.at(i))/(d.at(i) * itasq.at(i))
4796 D.at(i) = itb.at(i) / (d.at(i) * itasq.at(i))
4797 E.at(i) = - B.at(i) * gamma.at(i) - D.at(i) * gamma.at(i + 1)
4798 }
4799
4800 A.at(n) = omegan * itbsq.at(n - 1) / itasq.at(n - 1) * ( 3 - itb.at(n - 1)) + ita.at(n - 1)
4801 B.at(n) = omegan * itb.at(n - 1) * itbsq.at(n - 1) / itasq.at(n - 1) + 3 - ita.at(n - 1)
4802
4803 let alpha = thomas(A,vector.add(B,C), D, E)
4804 let beta = vector.scale(vector.add(alpha,gamma).slice(1), -1)
4805
4806 let ca = (0,)*n; let cb = ca
4807 for i in range(n) {
4808 let a = rho(alpha.at(i),beta.at(i)) * d.at(i) / 3
4809 let b = rho(beta.at(i),alpha.at(i)) * d.at(i) / 3
4810 ca.at(i) = complex.add(points.at(i), complex.scale(complex.unit(complex.rot(v.at(i),alpha.at(i))), a))
4811 cb.at(i) = complex.sub(points.at(i + 1), complex.scale(complex.unit(complex.rot(v.at(i),-beta.at(i))), b))
4812 }
4813 return range(n).map(i => (points.at(i), points.at(i+1), ca.at(i), cb.at(i)))
4814}
4815
4816/// Calculates a bezier spline for a closed Hobby curve through a list of points. Returns an {{array}} of {{bezier}}s.
4817///
4818/// - points (array): List of points
4819/// - ta (auto,array): Outgoing tension per point
4820/// - tb (auto,array): Incoming tension per point
4821/// - rho (auto,array): The rho function of the form `(float, b) => float`
4822///
4823/// -> array
4824#let hobby-to-cubic-closed(points, ta: auto, tb: auto, rho: auto) = {
4825 if points.first() != points.last() {
4826 points.push(points.first())
4827 }
4828
4829 let n = points.len() - 1
4830 points.push(points.at(1))
4831
4832 if ta == auto {
4833 ta = (1,)*n
4834 } else {
4835 assert.eq(type(ta), array, message: "ta must be an array")
4836 assert.eq(ta.len(), n, message: "ta must have length n for n + 1 points")
4837 assert(ta.all(x => x > 0), message: "ta must contain only positive numbers")
4838 }
4839 if tb == auto {
4840 tb = (1,)*n
4841 } else {
4842 assert.eq(type(tb), array, message: "tb must be an array")
4843 assert.eq(tb.len(), n, message: "tb must have length n for n + 1 points")
4844 assert(tb.all(x => x > 0), message: "tb must contain only positive numbers")
4845 }
4846 if rho == auto {
4847 rho = (a,b) => {
4848 (2 + calc.sqrt(2)*(calc.sin(a) - calc.sin(b)/16)*(calc.sin(b)-calc.sin(a)/16)*(calc.cos(a)-calc.cos(b)))/(1 + calc.cos(a)*(calc.sqrt(5)-1)/2 + calc.cos(b)*(3-calc.sqrt(5))/2)
4849 }
4850 } else {
4851 assert.eq(type(rho), function,
4852 message: "rho must be a function")
4853 }
4854
4855 let v = range(n + 1).map(i => complex.sub(points.at(i + 1),points.at(i)))
4856 let d = v.map(complex.norm)
4857
4858 let gamma = range(n).map(i => complex.ang(v.at(i),v.at(i + 1)))
4859 gamma = (gamma.last(),..gamma)
4860
4861 let ita = ta.map(x => 1/x)
4862 let itasq = ita.map(x => x*x)
4863
4864 let itb = tb.map(x => 1/x)
4865 let itbsq = itb.map(x => x*x)
4866
4867 let A = (0,) * n; let B = A; let C = A; let D = A; let E = A
4868
4869 A.at(0) = ita.at(n - 1) / (d.at(n - 1) * itbsq.at(n - 1))
4870 B.at(0) = (3 - ita.at(n - 1))/(d.at(n - 1) * itbsq.at(n - 1))
4871 C.at(0) = (3 - itb.at(0))/(d.at(0) * itasq.at(0))
4872 D.at(0) = itb.at(0) / (d.at(0) * itasq.at(0))
4873 E.at(0) = - B.at(0) * gamma.at(0) - D.at(0) * gamma.at(1)
4874
4875 for i in range(1, n) {
4876 A.at(i) = ita.at(i - 1) / (d.at(i - 1) * itbsq.at(i - 1))
4877 B.at(i) = (3 - ita.at(i - 1))/(d.at(i - 1) * itbsq.at(i - 1))
4878 C.at(i) = (3 - itb.at(i))/(d.at(i) * itasq.at(i))
4879 D.at(i) = itb.at(i) / (d.at(i) * itasq.at(i))
4880 E.at(i) = - B.at(i) * gamma.at(i) - D.at(i) * gamma.at(i + 1)
4881 }
4882
4883 let alpha = thomas-cyclic(A,vector.add(B,C), D, E)
4884 alpha.push(alpha.at(0))
4885 let beta = vector.scale(vector.add(alpha,gamma), -1)
4886 beta = (..beta.slice(1),beta.at(0))
4887
4888 let ca = (0,) * n; let cb = ca
4889 for i in range(n) {
4890 let a = rho(alpha.at(i),beta.at(i)) * d.at(i) / 3
4891 let b = rho(beta.at(i),alpha.at(i)) * d.at(i) / 3
4892 ca.at(i) = complex.add(points.at(i), complex.scale(complex.unit(complex.rot(v.at(i),alpha.at(i))), a))
4893 cb.at(i) = complex.sub(points.at(i + 1), complex.scale(complex.unit(complex.rot(v.at(i),-beta.at(i))), b))
4894 }
4895
4896 return range(n).map(i => (points.at(i), points.at(i+1), ca.at(i), cb.at(i)))
4897}
4898
4899/// Calculates a bezier spline for a Hobby curve through a list of points. Returns an {{array}} of {{bezier}}s.
4900///
4901/// - points (array): List of points
4902/// - ta (auto,array): Outgoing tension per point
4903/// - tb (auto,array): Incoming tension per point
4904/// - rho (auto,array): The rho function of the form `(float, float) => float`
4905/// - omega (auto,array): Tuple of the curl at the start end end of the curve `(start, end)` as floats
4906/// - close (bool): Close the curve
4907///
4908/// -> array
4909#let hobby-to-cubic(points, ta: auto, tb: auto, rho: auto, omega: auto, close: false) = {
4910 let omega = if omega == auto {
4911 (1, 1)
4912 } else if type(omega) == array {
4913 omega
4914 } else {
4915 (omega, omega)
4916 }
4917 assert.eq(type(omega), array,
4918 message: "Omega must be of type array")
4919 assert.eq(omega.len(), 2,
4920 message: "Omega must be of length 2")
4921 assert(omega.all(x => x >= 0),
4922 message: "Omega must contain positive values only")
4923
4924 if not close and points.len() == 2 {
4925 let (a, b) = points
4926 return ((a, b, a, b),)
4927 }
4928
4929 return if close {
4930 hobby-to-cubic-closed(points, ta: ta, tb: tb, rho: rho)
4931 } else {
4932 hobby-to-cubic-open(points, ta: ta, tb: tb, rho: rho, omega: omega)
4933 }
4934}
4935#import "vector.typ"
4936#import "util.typ"
4937
4938/// Checks for a line-line intersection between the given points and returns its position, otherwise {{none}}.
4939///
4940/// - a (vector): Line 1 point 1
4941/// - b (vector): Line 1 point 2
4942/// - c (vector): Line 2 point 1
4943/// - d (vector): Line 2 point 2
4944/// - ray (bool): When `true`, intersections will be found for the whole line instead of inbetween the given points.
4945/// -> vector,none
4946#let line-line(a, b, c, d, ray: false) = {
4947 let lli8(x1, y1, x2, y2, x3, y3, x4, y4) = {
4948 let nx = (x1*y2 - y1*x2)*(x3 - x4)-(x1 - x2)*(x3*y4 - y3*x4)
4949 let ny = (x1*y2 - y1*x2)*(y3 - y4)-(y1 - y2)*(x3*y4 - y3*x4)
4950 let d = (x1 - x2)*(y3 - y4)-(y1 - y2)*(x3 - x4)
4951 if d == 0 {
4952 return none
4953 }
4954 return (nx / d, ny / d, 0)
4955 }
4956 let pt = lli8(a.at(0), a.at(1), b.at(0), b.at(1),
4957 c.at(0), c.at(1), d.at(0), d.at(1))
4958 if pt != none {
4959 let on-line(pt, a, b) = {
4960 let (x, y, ..) = pt
4961 let epsilon = util.float-epsilon
4962 let mx = calc.min(a.at(0), b.at(0)) - epsilon
4963 let my = calc.min(a.at(1), b.at(1)) - epsilon
4964 let Mx = calc.max(a.at(0), b.at(0)) + epsilon
4965 let My = calc.max(a.at(1), b.at(1)) + epsilon
4966 return mx <= x and Mx >= x and my <= y and My >= y
4967 }
4968 if ray or (on-line(pt, a, b) and on-line(pt, c, d)) {
4969 return pt
4970 }
4971 }
4972}
4973
4974/// Finds the intersections of a line and cubic bezier.
4975///
4976/// - s (vector): Bezier start point
4977/// - e (vector): Bezier end point
4978/// - c1 (vector): Bezier control point 1
4979/// - c2 (vector): Bezier control point 2
4980/// - la (vector): Line start point
4981/// - lb (vector): Line end point
4982/// - ray (bool): When `true`, intersections will be found for the whole line instead of inbetween the given points.
4983/// -> array
4984#let line-cubic(la, lb, s, e, c1, c2) = {
4985 import "/src/bezier.typ": line-cubic-intersections as line-cubic
4986 return line-cubic(la, lb, s, e, c1, c2)
4987}
4988
4989/// Finds the intersections of a line and linestrip.
4990/// - la (vector): Line start point.
4991/// - lb (vector): Line end point.
4992/// - v (array): An {{array}} of {{vector}}s that define each point on the linestrip.
4993/// -> array
4994#let line-linestrip(la, lb, v) = {
4995 let pts = ()
4996 for i in range(0, v.len() - 1) {
4997 let pt = line-line(la, lb, v.at(i), v.at(i + 1))
4998 if pt != none {
4999 pts.push(pt)
5000 }
5001 }
5002 return pts
5003}
5004
5005/// Finds the intersections of a line and path in 2D. The path should be given as a {{drawable}} of type `path`.
5006///
5007/// - la (vector): Line start
5008/// - lb (vector): Line end
5009/// - path (drawable): The path.
5010/// -> array
5011#let line-path(la, lb, path) = {
5012 let segment(s) = {
5013 let (k, ..v) = s
5014 if k == "line" {
5015 return line-linestrip(la, lb, v)
5016 } else if k == "cubic" {
5017 return line-cubic(la, lb, ..v)
5018 } else {
5019 return ()
5020 }
5021 }
5022
5023 let pts = ()
5024 for s in path.at("segments", default: ()) {
5025 pts += segment(s)
5026 }
5027 return pts
5028}
5029
5030/// Finds the intersections between two path {{drawable}}s in 2D.
5031///
5032/// - a (path): Path a
5033/// - b (path): Path b
5034/// - samples (int): Number of samples to use for bezier curves
5035/// -> array
5036#let path-path(a, b, samples: 8) = {
5037 import "bezier.typ": cubic-point
5038
5039 // Convert segment to vertices by sampling curves
5040 let linearize-segment(s) = {
5041 let t = s.at(0)
5042 if t == "line" {
5043 return s.slice(1)
5044 } else if t == "cubic" {
5045 return range(samples + 1).map(
5046 t => cubic-point(..s.slice(1), t/samples)
5047 )
5048 }
5049 }
5050
5051 let pts = ()
5052 for s in a.at("segments", default: ()) {
5053 let sv = linearize-segment(s)
5054 for ai in range(0, sv.len() - 1) {
5055 pts += line-path(sv.at(ai), sv.at(ai + 1), b)
5056 }
5057 }
5058 return pts
5059}
5060#import "version.typ": version
5061
5062#import "canvas.typ": canvas
5063#import "draw.typ"
5064
5065// Expose utilities
5066#import "vector.typ"
5067#import "matrix.typ"
5068#import "styles.typ"
5069#import "coordinate.typ"
5070#import "intersection.typ"
5071#import "drawable.typ"
5072#import "process.typ"
5073#import "util.typ"
5074#import "path-util.typ"
5075#import "mark.typ"
5076#import "mark-shapes.typ"
5077#import "sorting.typ"
5078
5079// Libraries
5080#import "lib/palette.typ"
5081#import "lib/angle.typ"
5082#import "lib/tree.typ"
5083#import "lib/decorations.typ"
5084#import "/src/drawable.typ"
5085#import "/src/styles.typ"
5086#import "/src/vector.typ"
5087#import "/src/util.typ"
5088#import "/src/coordinate.typ"
5089#import "/src/anchor.typ" as anchor_
5090#import "/src/draw.typ"
5091
5092// Angle default-style
5093#let default-style = (
5094 fill: none,
5095 stroke: auto,
5096 radius: .5,
5097 label-radius: 50%,
5098 mark: auto,
5099)
5100
5101/// Draw an angle counter-clock-wise between `a` and `b` through origin `origin`
5102///
5103/// ```typc example
5104/// line((0,0), (1,1.5), name: "a")
5105/// line((0,0), (2,-1), name: "b")
5106///
5107/// // Draw an angle between the two lines
5108/// cetz.angle.angle("a.start", "a.end", "b.end", label: $ alpha $,
5109/// mark: (end: ">"), radius: 1.5)
5110/// cetz.angle.angle("a.start", "b.end", "a.end", label: $ alpha' $,
5111/// radius: 50%, direction: "cw")
5112/// ```
5113///
5114/// - origin (coordinate): Angle origin
5115/// - a (coordinate): Coordinate of side `a`, containing an angle between `origin` and `b`.
5116/// - b (coordinate): Coordinate of side `b`, containing an angle between `origin` and `a`.
5117/// - direction (string): Direction of the angle. Accepts "cw" (clockwise) and "ccw" (counter-clockwise), the latter being the default.
5118/// - label (none,content,function): Draw a label at the angles "label" anchor. If label is a function, it gets the angle value passed as argument. The function must be of the format `angle => content`.
5119/// - name (none,str): Element name, used for querying anchors.
5120/// - ..style (style): Style key-value pairs.
5121///
5122/// ## Styling
5123/// *Root:* `angle` \
5124///
5125/// - radius (number) = 0.5: The radius of the angles arc. If of type `ratio`, it is relative to the smaller distance of either origin to a or origin to b.
5126/// - label-radius (number, ratio) = 50%: The radius of the angles label origin. If of type `ratio`, it is relative to `radius`.
5127///
5128/// ## Anchors
5129/// - **a** Point a
5130/// - **b** Point b
5131/// - **origin** Origin
5132/// - **label** Label center
5133/// - **start** Arc start
5134/// - **end** Arc end
5135#let angle(
5136 origin,
5137 a,
5138 b,
5139 direction: "ccw",
5140 label: none,
5141 name: none,
5142 ..style
5143) = draw.group(name: name, ctx => {
5144 let style = styles.resolve(ctx.style, merge: style.named(), base: default-style, root: "angle")
5145 let radius = util.resolve-number(ctx, style.radius)
5146 let label-radius = util.resolve-number(ctx, style.label-radius)
5147
5148 let (ctx, origin) = coordinate.resolve(ctx, origin)
5149 let (ctx, a, b) = coordinate.resolve(ctx, a, b, update: false)
5150
5151 assert(origin.at(2) == a.at(2) and a.at(2) == b.at(2),
5152 message: "Angle z coordinates of all three points must be equal")
5153
5154 assert(direction in ("cw", "ccw"),
5155 message: "Invalid angle direction " + repr(direction))
5156
5157 let (start, delta, ccw) = {
5158 let ccw = direction == "ccw"
5159
5160 let s = vector.angle2(origin, a)
5161 if s < 0deg { s += 360deg }
5162
5163 let e = vector.angle2(origin, b)
5164 if e < 0deg { e += 360deg }
5165
5166 if e < s {
5167 e += 360deg
5168 }
5169
5170 if ccw {
5171 (s, (e - s), ccw)
5172 } else {
5173 (s, -(360deg - (e - s)), ccw)
5174 }
5175 }
5176
5177 let mid = start + delta / 2
5178
5179 // Radius can be relative to the min-distance between origin-a and origin-b
5180 if type(radius) == ratio {
5181 radius = radius * calc.min(vector.dist(origin, a), vector.dist(origin, b)) / 100%
5182 }
5183
5184 // Label radius can be relative to radius
5185 if type(label-radius) == ratio {
5186 label-radius = label-radius * radius / 100%
5187 }
5188
5189 let label-pt = vector.add(origin, (calc.cos(mid) * label-radius, calc.sin(mid) * label-radius, 0))
5190 let start-pt = vector.add(origin, (calc.cos(start) * radius, calc.sin(start) * radius, 0))
5191 let end-pt = vector.add(origin, (calc.cos(start + delta) * radius, calc.sin(start + delta) * radius, 0))
5192 draw.anchor("origin", origin)
5193 draw.anchor("label", label-pt)
5194 draw.anchor("start", start-pt)
5195 draw.anchor("end", end-pt)
5196 draw.anchor("a", a)
5197 draw.anchor("b", b)
5198
5199 if delta != 0deg {
5200 if style.fill != none {
5201 draw.arc(origin, start: start, delta: delta, anchor: "origin",
5202 name: "arc", ..style, radius: radius, mode: "PIE", mark: none, stroke: none)
5203 }
5204 if style.stroke != none {
5205 draw.arc(origin, start: start, delta: delta, anchor: "origin",
5206 name: "arc", ..style, radius: radius, fill: none)
5207 }
5208 }
5209
5210 let label = if type(label) == function { label(calc.abs(delta)) } else { label }
5211 if label != none {
5212 draw.content(label-pt, label)
5213 }
5214})
5215
5216/// Draw a right angle between `a` and `b` through origin `origin`
5217///
5218/// ```typc example
5219/// line((0,0), (1,2), name: "a")
5220/// line((0,0), (2,-1), name: "b")
5221///
5222/// // Draw an angle between the two lines
5223/// cetz.angle.right-angle(
5224/// "a.start",
5225/// "a.end",
5226/// "b.end",
5227/// radius: 1.5
5228/// )
5229/// ```
5230///
5231/// - origin (coordinate): Angle origin
5232/// - a (coordinate): Coordinate of side `a`, containing an angle between `origin` and `b`.
5233/// - b (coordinate): Coordinate of side `b`, containing an angle between `origin` and `a`.
5234/// - label (none,content): Draw a label at the angles "label" anchor.
5235/// - name (none,str): Element name, used for querying anchors.
5236/// - ..style (style): Style key-value pairs.
5237///
5238/// ## Styling
5239/// Styling is the same as the `angle` function.
5240///
5241/// ## Anchors
5242/// Anchors are the same as the `angle` function
5243///
5244#let right-angle(
5245 origin,
5246 a,
5247 b,
5248 label: "•",
5249 name: none,
5250 ..style
5251) = draw.group(name: name, ctx => {
5252 let style = styles.resolve(ctx.style, merge: style.named(), base: default-style, root: "angle")
5253 let (ctx, origin) = coordinate.resolve(ctx, origin)
5254 let (ctx, a, b) = coordinate.resolve(ctx, a, b, update: false)
5255 let vo = origin; let va = a; let vb = b
5256
5257 // Radius can be relative to the min-distance between origin-a and origin-b
5258 if type(style.radius) == ratio {
5259 style.radius = style.radius * calc.min(vector.dist(vo, va), vector.dist(vo, vb)) / 100%
5260 }
5261 let (r, _) = util.resolve-radius(style.radius).map(util.resolve-number.with(ctx))
5262
5263 let va = vector.add(vo, vector.scale(vector.norm(vector.sub(va, vo)), r))
5264 let vb = vector.add(vo, vector.scale(vector.norm(vector.sub(vb, vo)), r))
5265 let angle-b = vector.angle2(vo, vb)
5266 let vm = vector.add(va, (calc.cos(angle-b) * r, calc.sin(angle-b) * r, 0))
5267
5268 // Label radius can be relative to the distance between origin and the
5269 // angle corner
5270 if type(style.label-radius) == ratio {
5271 style.label-radius = style.label-radius * vector.dist(vm, vo) / 100%
5272 }
5273 let (ra, _) = util.resolve-radius(style.label-radius).map(util.resolve-number.with(ctx))
5274
5275 if style.fill != none {
5276 draw.line(vo, va, vm, vb, close: true, stroke: none, fill: style.fill)
5277 }
5278 draw.line(va, vm, vb, ..style, fill: none)
5279
5280 let label-pt = vector.add(vo, vector.scale(vector.norm(vector.sub(vm, vo)), ra))
5281 if label != none {
5282 draw.content(label-pt, label)
5283 }
5284
5285 draw.anchor("a", a)
5286 draw.anchor("b", b)
5287 draw.anchor("origin", origin)
5288 draw.anchor("corner", vm)
5289 draw.anchor("label", label-pt)
5290})
5291#import "decorations/brace.typ": brace, brace-default-style, flat-brace, flat-brace-default-style
5292#import "decorations/path.typ": zigzag, wave, coil, square
5293#import "/src/vector.typ"
5294#import "/src/matrix.typ"
5295#import "/src/util.typ"
5296#import "/src/draw.typ": *
5297#import "/src/coordinate.typ"
5298#import "/src/styles.typ"
5299
5300// Rotates the vector 'ab' around 'a' and scales it to 'len', returns the absolute point 'c'.
5301#let _rotate-around(a, b, angle: 90deg, len: auto) = {
5302 let rel = vector.sub(b, a)
5303 let rotated = util.apply-transform(matrix.transform-rotate-z(angle), rel)
5304 let scaled = if len == auto {
5305 rotated
5306 } else {
5307 vector.scale(vector.norm(rotated), len)
5308 }
5309 return vector.add(a, scaled)
5310}
5311
5312#let brace-default-style = (
5313 amplitude: .5,
5314 pointiness: 15deg,
5315 outer-pointiness: 0deg,
5316 content-offset: .3,
5317 flip: false,
5318 stroke: auto,
5319 fill: none,
5320)
5321
5322/// Draw a curly brace between two points.
5323///
5324/// ```typc example
5325/// cetz.decorations.brace((0,1),(2,1))
5326///
5327/// cetz.decorations.brace((0,0),(2,0),
5328/// pointiness: 45deg, outer-pointiness: 45deg)
5329/// cetz.decorations.brace((0,-1),(2,-1),
5330/// pointiness: 90deg, outer-pointiness: 90deg)
5331/// ```
5332///
5333/// - start (coordinate): Start point
5334/// - end (coordinate): End point
5335/// - name (string, none): Element name used for querying anchors
5336/// - ..style (style): Style key-value pairs
5337///
5338/// ## Styling
5339///
5340/// *Root:* `brace`
5341/// - amplitude (number) = 0.5: Sets the height of the brace, from its baseline to its middle tip.
5342/// - pointiness (ratio, angle) = 15deg: How pointy the spike should be. `0deg` or `100%` for maximum pointiness, `90deg` or `0%` for minimum.
5343/// - outer-pointiness (ratio, angle) = 15deg: How pointy the outer edges should be. `0deg` or `100%` for maximum pointiness (allowing for a smooth transition to a straight line), `90deg` or `0%` for minimum. Setting this to <Type>auto</Type> will use the value set for `pointiness`.
5344/// - content-offset (number) = 0.3: Offset of the `"content"` anchor from the spike of the brace.
5345/// - flip (bool) = false: Mirror the brace along the line between start and end.
5346///
5347/// ## Anchors
5348/// - **start** Where the brace starts, same as the `start` parameter.
5349/// - **end** Where the brace end, same as the `end` parameter.
5350/// - **spike** Point of the spike, halfway between `start` and `end` and shifted by `amplitude` towards the pointing direction.
5351/// - **content** Point to place content/text at, in front of the spike.
5352/// - **center** Center of the enclosing rectangle.
5353#let brace(start, end, ..style, name: none) = {
5354 assert.eq(style.pos().len(), 0,
5355 message: "Brace takes no additional positional arugments.")
5356
5357 // Validate coordinates
5358 let _ = (start, end).map(coordinate.resolve-system)
5359
5360 group(name: name, ctx => {
5361 // Resolve all coordinates
5362 let (ctx, start, end) = coordinate.resolve(ctx, start, end)
5363
5364 // Query and resolve style
5365 let style = styles.resolve(ctx.style, root: "brace", base: brace-default-style, merge: style.named())
5366
5367 let amplitude = util.resolve-number(ctx, style.amplitude)
5368 let content-offset = util.resolve-number(ctx, style.content-offset)
5369 let pointiness = if type(style.pointiness) == ratio {
5370 (1 - style.pointiness / 100%) * 90deg
5371 } else { style.pointiness }
5372 pointiness = calc.max(0deg, calc.min(pointiness, 90deg))
5373
5374 let outer-pointiness = if type(style.outer-pointiness) == ratio {
5375 (1 - style.outer-pointiness / 100%) * 90deg
5376 } else { style.outer-pointiness }
5377 outer-pointiness = calc.max(0deg, calc.min(outer-pointiness, 90deg)) * -1
5378
5379 let up = (0, 0, -1)
5380 let mid = vector.lerp(start, end, .5)
5381
5382 let dir = vector.norm(vector.sub(end, start))
5383 let normal = vector.cross(dir, up)
5384 if style.flip {
5385 normal = vector.scale(normal, -1)
5386 pointiness *= -1
5387 outer-pointiness *= -1
5388 }
5389
5390 // Compute tip coordinate
5391 let tip = vector.add(mid, vector.scale(normal, calc.abs(amplitude)))
5392
5393 // Measure distance between midpoint on start-end and tip
5394 let amplitude = vector.dist(mid, tip)
5395
5396 // Add anchors
5397 anchor("start", start)
5398 anchor("end", end)
5399 anchor("default", mid)
5400 anchor("spike", tip)
5401
5402 // Offset content anchor
5403 anchor("content", vector.add(tip, vector.scale(normal, content-offset)))
5404
5405 merge-path({
5406 let scale-amplitude(v) = {
5407 let max = vector.dist(start, end) / 2
5408 vector.scale(v, calc.min(amplitude, max))
5409 }
5410
5411 let rotate-inner(factor) = {
5412 vector.rotate-z(normal, pointiness * factor)
5413 }
5414
5415 let rotate-outer(factor) = {
5416 vector.rotate-z(normal, outer-pointiness * factor)
5417 }
5418
5419 let dist = vector.dist(start, tip) + vector.dist(tip, end)
5420 let ratio = vector.dist(start, tip) / dist
5421 let b = vector.dist(end, tip) / dist
5422
5423 bezier(start, tip,
5424 vector.add(start, scale-amplitude(rotate-outer(+1))),
5425 vector.sub(tip, scale-amplitude(rotate-inner(-1))))
5426 bezier(tip, end,
5427 vector.sub(tip, scale-amplitude(rotate-inner(+1))),
5428 vector.add(end, scale-amplitude(rotate-outer(-1))))
5429 }, stroke: style.stroke, fill: style.fill)
5430
5431 move-to(end)
5432 })
5433}
5434
5435
5436#let flat-brace-default-style = (
5437 stroke: auto,
5438 fill: none,
5439 amplitude: .3,
5440 aspect: 50%,
5441 curves: (1, .5, .6, .15),
5442 outer-curves: auto,
5443 content-offset: .3,
5444 debug-text-size: 6pt,
5445)
5446
5447/// Draw a flat curly brace between two points.
5448///
5449/// ```typc example
5450/// cetz.decorations.flat-brace((0,1),(2,1))
5451///
5452/// cetz.decorations.flat-brace((0,0),(2,0),
5453/// curves: .2,
5454/// aspect: 25%)
5455/// cetz.decorations.flat-brace((0,-1),(2,-1),
5456/// outer-curves: 0,
5457/// aspect: 75%)
5458/// ```
5459///
5460/// This mimics the braces from TikZ's [`decorations.pathreplacing` library](https://github.com/pgf-tikz/pgf/blob/6e5fd71581ab04351a89553a259b57988bc28140/tex/generic/pgf/libraries/decorations/pgflibrarydecorations.pathreplacing.code.tex#L136-L185).
5461/// In contrast to the `brace` function, these braces use straight line segments, resulting in better looks for long braces with a small amplitude.
5462///
5463/// - start (coordinate): Start point
5464/// - end (coordinate): End point
5465/// - flip (bool): Flip the brace around
5466/// - name (str, none): Element name for querying anchors
5467/// - debug (bool):
5468/// - ..style (style): Style key-value pairs
5469///
5470/// ## Styling
5471///
5472/// *Root:* `flat-brace`
5473/// - amplitude (number) = 0.3: Determines how much the brace rises above the base line.
5474/// - aspect (ratio) = 50% Determines the fraction of the total length where the spike will be placed.
5475/// - curves (number, auto, array) = auto: Curviness factor of the brace, a factor of 0 means no curves.
5476/// - outer-curves (number, auto, array) = auto: Curviness factor of the outer curves of the brace. A factor of 0 means no curves.
5477///
5478/// ## Anchors
5479/// - **start** Where the brace starts, same as the `start` parameter.
5480/// - **end** Where the brace end, same as the `end` parameter.
5481/// - **spike** Point of the spike's top.
5482/// - **content** Point to place content/text at, in front of the spike.
5483/// - **center** Center of the enclosing rectangle.
5484#let flat-brace(
5485 start,
5486 end,
5487 flip: false,
5488 debug: false,
5489 name: none,
5490 ..style,
5491) = {
5492 // Validate coordinates
5493 let _ = (start, end).map(coordinate.resolve-system)
5494
5495 group(name: name, ctx => {
5496 // Get styles and validate their types and values
5497 let style = styles.resolve(ctx.style, merge: style.named(),
5498 root: "flat-brace", base: flat-brace-default-style)
5499
5500 let amplitude = style.amplitude
5501 assert(
5502 type(amplitude) in (int, float),
5503 message: "amplitude must be a number, got " + repr(amplitude),
5504 )
5505
5506 let aspect = style.aspect
5507 assert(
5508 (type(aspect) == ratio
5509 and aspect >= 0% and aspect <= 100%)
5510 or (type(aspect) in (int, float)
5511 and aspect >= 0 and aspect <= 1),
5512 message: "aspect must be a ratio between 0% and 100%, got " + repr(aspect),
5513 )
5514 if type(aspect) == ratio { aspect /= 100% }
5515
5516 let inner-curves = style.curves
5517 assert(
5518 type(inner-curves) in (int, float)
5519 or type(inner-curves) == array
5520 and inner-curves.all(v => type(v) in (int, float, type(auto))),
5521 message: "curves must be a number, or an array of numbers or auto, got " + repr(inner-curves),
5522 )
5523 if type(inner-curves) in (int, float) { inner-curves = (inner-curves,) }
5524 while inner-curves.len() < flat-brace-default-style.curves.len() {
5525 inner-curves.push(auto)
5526 }
5527 inner-curves = inner-curves.enumerate().map(((idx, v)) => if v == auto {
5528 flat-brace-default-style.curves.at(idx)
5529 } else { v })
5530
5531 let outer-curves = style.outer-curves
5532 assert(
5533 type(outer-curves) in (int, float, type(auto))
5534 or type(outer-curves) == array
5535 and outer-curves.all(v => type(v) in (int, float, type(auto))),
5536 message: "outer-curves must be auto, a number, or an array of numbers or auto, got " + repr(outer-curves),
5537 )
5538 if outer-curves == auto {
5539 outer-curves = inner-curves
5540 } else {
5541 if type(outer-curves) in (int, float) { outer-curves = (outer-curves,) }
5542 while outer-curves.len() < inner-curves.len() { outer-curves.push(auto) }
5543 outer-curves = outer-curves.enumerate()
5544 .map(((idx, v)) => if v == auto { inner-curves.at(idx) } else { v })
5545 }
5546
5547 let content-offset = style.content-offset
5548 assert(
5549 type(content-offset) in (int, float),
5550 message: "content-offset must be a number, got " + repr(content-offset),
5551 )
5552
5553 // all the following code assumes the brace to start at (0, 0), growing to the right,
5554 // pointing upwards, so we set the origin and rotate the entire group accordingly
5555 let (_, start, end) = coordinate.resolve(ctx, start, end)
5556 set-origin(start)
5557 rotate(vector.angle2(start, end))
5558
5559 // we achieve flipping by inverting the amplitude
5560 if flip {
5561 amplitude *= -1
5562 content-offset *= -1
5563 }
5564
5565 let length = vector.dist(start, end)
5566 let middle = aspect * length
5567 let horizon = amplitude / 2
5568
5569 let normal-outer = calc.abs(amplitude * outer-curves.at(0))
5570 let normal-inner = calc.abs(amplitude * inner-curves.at(0))
5571 let length-left = middle
5572 let length-right = length - middle
5573
5574 // width of left-outer, left-inner, right-inner, right-outer curve segments
5575 let lo = if 2 * normal-outer > length-left { length-left / 2 } else { normal-outer }
5576 let li = if 2 * normal-inner > length-left { length-left / 2 } else { normal-inner }
5577 let ri = if 2 * normal-inner > length-right { length-right / 2 } else { normal-inner }
5578 let ro = if 2 * normal-outer > length-right { length-right / 2 } else { normal-outer }
5579
5580 // 'a' and 'b' are start and end
5581 let a = ( 0, 0)
5582 let b = (length, 0)
5583 // 'c' is the spike's top
5584 let c = (middle, amplitude)
5585 // 'de' is the left line, 'fg' is the right line
5586 let d = ( lo, horizon)
5587 let e = (middle - li, horizon)
5588 let f = (middle + ri, horizon)
5589 let g = (length - ro, horizon)
5590 // 'h' is where to place content, above the spike
5591 let h = (middle, amplitude + content-offset)
5592
5593 // list of all named points to show in debug mode
5594 let points = (a: a, b: b, c: c, d: d, e: e, f: f, g: g, h: h)
5595
5596 // bezier control points: in 'dlc' 'd' stands for the point 'd' where the control point is used,
5597 // 'l' stands for left of spike, 'c' stands for control point
5598 let dlc = ( (1 - outer-curves.at(1)) * lo, horizon)
5599 let elc = (middle - (1 - inner-curves.at(1)) * li, horizon)
5600 let frc = (middle + (1 - inner-curves.at(1)) * ri, horizon)
5601 let grc = (length - (1 - outer-curves.at(1)) * ro, horizon)
5602 let alc = ( outer-curves.at(3) * lo, outer-curves.at(2) / 2 * amplitude)
5603 let clc = (middle - inner-curves.at(3) * li, (1 - inner-curves.at(2) / 2) * amplitude)
5604 let crc = (middle + inner-curves.at(3) * ri, (1 - inner-curves.at(2) / 2) * amplitude)
5605 let brc = (length - outer-curves.at(3) * ro, outer-curves.at(2) / 2 * amplitude)
5606
5607 merge-path({
5608 bezier(a, d, alc, dlc)
5609 bezier(e, c, elc, clc)
5610 bezier(c, f, crc, frc)
5611 bezier(g, b, grc, brc)
5612 }, stroke: style.stroke, fill: style.fill)
5613
5614 // Define some named anchors
5615 anchor("spike", c)
5616 anchor("content", h)
5617 anchor("start", a)
5618 anchor("end", b)
5619 anchor("default", (d, 50%, g))
5620
5621 // Define anchors for all points
5622 for (name, point) in points {
5623 anchor(name, point)
5624 }
5625
5626 if debug {
5627 // Show bezier control points using colored lines
5628 line(stroke: purple, a, alc)
5629 line(stroke: blue, d, dlc)
5630 line(stroke: olive, e, elc)
5631 line(stroke: red, c, clc)
5632 line(stroke: red, c, crc)
5633 line(stroke: olive, f, frc)
5634 line(stroke: blue, g, grc)
5635 line(stroke: purple, b, brc)
5636 // Show all named points
5637 for (name, point) in points {
5638 content(point, box(fill: luma(240), inset: .5pt, text(style.debug-text-size, raw(name))))
5639 }
5640 }
5641 })
5642
5643 // Move to end point so the current position after this is the end position
5644 move-to(end)
5645}
5646// Library for drawing springs
5647#import "/src/draw.typ"
5648#import "/src/styles.typ"
5649#import "/src/coordinate.typ"
5650#import "/src/vector.typ"
5651#import "/src/process.typ"
5652#import "/src/path-util.typ"
5653#import "/src/util.typ"
5654#import "/src/bezier.typ"
5655
5656#let default-style = (
5657 /// Number of segments
5658 segments: 10,
5659 /// Length of a single segments
5660 segment-length: none,
5661
5662 /// Amplitude of a segment in the direction of the segments normal.
5663 /// The following types are supported:
5664 /// - float
5665 /// - function ratio -> float (the segment ratio is given as argument)
5666 /// - array of floats (the rounded down segment number is used as index modulo the array length)
5667 amplitude: 1,
5668 /// Decoration start
5669 start: 0%,
5670 /// Decoration stop
5671 stop: 100%,
5672 /// Decoration alignment on the target path
5673 align: "START",
5674 /// Draw remaining space as line ("LINE") or none
5675 rest: "LINE",
5676
5677 /// Up-vector for 3D lines
5678 z-up: (0, 1, 0),
5679 /// Up-vector for 2D lines
5680 xy-up: (0, 0, -1),
5681
5682 stroke: auto,
5683 fill: none,
5684 mark: auto,
5685)
5686
5687// Zig-Zag default style
5688#let zigzag-default-style = (
5689 ..default-style,
5690 /// Midpoint factor
5691 /// 0%: Sawtooth (up-down)
5692 /// 50%: Triangle
5693 /// 100%: Sawtooth (down-up)
5694 factor: 50%,
5695)
5696
5697// Wave default style
5698#let wave-default-style = (
5699 ..default-style,
5700 /// Wave (catmull-rom) tension
5701 tension: .5,
5702)
5703
5704// Coil default style
5705#let coil-default-style = (
5706 ..default-style,
5707 /// Coil "overshoot" factor
5708 factor: 150%,
5709)
5710
5711// Square default style
5712#let square-default-style = (
5713 ..default-style,
5714 /// Midpoint factor
5715 factor: 50%,
5716)
5717
5718#let resolve-amplitude(ctx, amplitude, segment, num-segments) = {
5719 segment = calc.max(0, calc.min(segment, num-segments))
5720 let amp = if type(amplitude) == function {
5721 (amplitude)(segment / num-segments * 100%)
5722 } else if type(amplitude) == array {
5723 amplitude.at(calc.rem(int(2*segment), amplitude.len()), default: 0)
5724 } else {
5725 amplitude
5726 }
5727 return util.resolve-number(ctx, amp)
5728}
5729
5730#let resolve-style(ctx, segments, style) = {
5731 assert(not (style.segments == none and style.segment-length == none),
5732 message: "Only one of segments or segment-length must be set, while the other must be auto")
5733 assert(style.segments != none or style.segment-length != none,
5734 message: "Either segments or segment-length must be not equal to none")
5735
5736 // Calculate absolute start/stop distances
5737 let len = path-util.length(segments)
5738 if type(style.start) == ratio {
5739 style.start = len * style.start / 100%
5740 }
5741 style.start = calc.max(0, calc.min(style.start, len))
5742 if type(style.stop) == ratio {
5743 style.stop = len * style.stop / 100%
5744 }
5745 style.stop = calc.max(0, calc.min(style.stop, len))
5746
5747 if style.segment-length != none {
5748 // Calculate number of divisions
5749 let n = (style.stop - style.start) / style.segment-length
5750 style.segments = calc.floor(n)
5751
5752 // Divides the rest between start, stop or both
5753 let r = (n - calc.floor(n)) * style.segment-length
5754 if style.align == "MID" {
5755 let m = (style.start + style.stop) / 2
5756 style.start = m - n * style.segment-length / 2
5757 style.stop = m + n * style.segment-length / 2
5758 } else if style.align == "STOP" {
5759 style.start = style.stop - n * style.segment-length
5760 } else if style.align == "START" {
5761 style.stop = style.start + n * style.segment-length
5762 }
5763 }
5764
5765 return style
5766}
5767
5768#let get-segments(ctx, target) = {
5769 if type(target) == array {
5770 assert.eq(target.len(), 1,
5771 message: "Expected a single element, got " + str(target.len()))
5772 target = target.first()
5773 }
5774
5775 let (ctx, drawables, ..) = process.element(ctx, target)
5776 if drawables == none or drawables == () {
5777 return ()
5778 }
5779
5780 let first = drawables.first()
5781 return (segments: first.segments, close: first.close)
5782}
5783
5784// Add optional line elements from segments start to mid-path start
5785// and mid-path end to sgements end
5786#let finalize-path(ctx, segments, style, mid-path, close: false) = {
5787 let add = style.rest == "LINE" and not close
5788
5789 let (ctx, drawables, ..) = process.many(ctx, mid-path)
5790 let mid-first = drawables.first().segments.first()
5791 let mid-last = drawables.last().segments.last()
5792
5793 if add {
5794 let start = path-util.segment-start(segments.first())
5795 start = util.revert-transform(ctx.transform, start)
5796
5797 let mid-start = path-util.segment-start(mid-first)
5798 mid-start = util.revert-transform(ctx.transform, mid-start)
5799 draw.line(start, mid-start, mark: none)
5800 }
5801 mid-path;
5802 if add {
5803 let end = path-util.segment-end(segments.last())
5804 end = util.revert-transform(ctx.transform, end)
5805
5806 let mid-end = path-util.segment-end(mid-last)
5807 mid-end = util.revert-transform(ctx.transform, mid-end)
5808 draw.line(mid-end, end, mark: none)
5809 }
5810 // TODO: Add marks on path.
5811}
5812
5813// Call callback `fn` for each decoration segment
5814// on path `segments`.
5815//
5816// The callback gets called with the following arguments:
5817// - i Segment index
5818// - start Segment start point
5819// - end Segment end point
5820// - norm Normal vector (length 1)
5821// Result values get returned as an array
5822#let _path-effect(ctx, segments, fn, close: false, style) = {
5823 let n = style.segments
5824 assert(n > 0,
5825 message: "Number of segments must be greater than 0")
5826
5827 let (start, stop) = (style.start, style.stop)
5828 let inc = (stop - start) / n
5829 let pts = ()
5830 let len = path-util.length(segments)
5831 for i in range(0, n) {
5832 let p0 = path-util.point-on-path(segments, calc.max(start,
5833 start + inc * i))
5834 let p1 = path-util.point-on-path(segments, calc.min(stop,
5835 start + inc * (i + 1)))
5836 if p0 == p1 { continue }
5837
5838 (p0, p1) = util.revert-transform(ctx.transform, p0, p1)
5839
5840 let dir = vector.norm(vector.sub(p1, p0))
5841 let norm = vector.cross(dir, if p0.at(2) != p1.at(2) {
5842 style.z-up
5843 } else {
5844 style.xy-up
5845 })
5846
5847 pts += fn(i, p0, p1, norm)
5848 }
5849 return pts
5850}
5851
5852/// Draw a zig-zag or saw-tooth wave along a path.
5853///
5854/// The number of tooths can be controlled via the `segments` or `segment-length` style key, and the width via `amplitude`.
5855///
5856/// ```typc example
5857/// line((0,0), (2,1), stroke: gray)
5858/// cetz.decorations.zigzag(line((0,0), (2,1)), amplitude: .25, start: 10%, stop: 90%)
5859/// ```
5860///
5861/// - target (drawable): Target path
5862/// - close (auto,bool): Close the path
5863/// - name (none,string): Element name
5864/// - ..style (style): Style
5865///
5866/// ## Styling
5867/// *Root*: `zigzag`
5868/// - factor (ratio) = 100%: Triangle mid between its start and end. Setting this to 0% leads to a falling sawtooth shape, while 100% results in a raising sawtooth.
5869#let zigzag(target, name: none, close: auto, ..style) = draw.get-ctx(ctx => {
5870 let style = styles.resolve(ctx, merge: style.named(),
5871 base: zigzag-default-style, root: "zigzag")
5872
5873 let (segments, close) = get-segments(ctx, target)
5874 let style = resolve-style(ctx, segments, style)
5875 let num-segments = style.segments
5876
5877 // Return points for a zigzag line
5878 //
5879 // m1 ▲
5880 // / \ │ Up
5881 // ..a....\....b.. '
5882 // \ /
5883 // m2
5884 // |--|
5885 // q-dir (quarter length between a and b)
5886 //
5887 // For the first/last segment, a/b get added. For all
5888 // other segments we only have to add m1 and m2 to the
5889 // list of points for the line-strip.
5890 let fn(i, a, b, norm) = {
5891 let ab = vector.sub(b, a)
5892 let f = .25 - (50% - style.factor) / 50% * .25
5893 let q-dir = vector.scale(ab, f)
5894 let up = vector.scale(norm, resolve-amplitude(ctx, style.amplitude, i + .25, num-segments) / 2)
5895 let down = vector.scale(norm, -resolve-amplitude(ctx, style.amplitude, i + .75, num-segments) / 2)
5896
5897 let m1 = vector.add(vector.add(a, q-dir), up)
5898 let m2 = vector.add(vector.sub(b, q-dir), down)
5899
5900 return if not close and i == 0 {
5901 (a, m1, m2) // First segment: add a
5902 } else if not close and i == num-segments - 1 {
5903 (m1, m2, b) // Last segment: add b
5904 } else {
5905 (m1, m2)
5906 }
5907 }
5908
5909 let pts = _path-effect(ctx, segments, fn, close: close, style)
5910 return draw.merge-path(
5911 finalize-path(ctx, segments, style,
5912 draw.line(..pts, name: name, ..style, mark: none),
5913 close: close),
5914 close: close,
5915 ..style)
5916})
5917
5918/// Draw a stretched coil/loop spring along a path
5919///
5920/// The number of windings can be controlled via the `segments` or `segment-length` style key, and the width via `amplitude`.
5921///
5922/// ```typc example
5923/// line((0,0), (2,1), stroke: gray)
5924/// cetz.decorations.coil(line((0,0), (2,1)), amplitude: .25, start: 10%, stop: 90%)
5925/// ```
5926/// - target (drawable): Target path
5927/// - close (auto,bool): Close the path
5928/// - name (none,string): Element name
5929/// - ..style (style): Style
5930///
5931/// ## Styling
5932/// *Root*: `coil`
5933/// - factor (ratio) = 150%: Factor of how much the coil overextends its length to form a curl.
5934#let coil(target, close: auto, name: none, ..style) = draw.get-ctx(ctx => {
5935 let style = styles.resolve(ctx, merge: style.named(),
5936 base: coil-default-style, root: "coil")
5937
5938 let (segments, close) = get-segments(ctx, target)
5939 let style = resolve-style(ctx, segments, style)
5940
5941 let num-segments = calc.max(style.segments, 1)
5942 let length = path-util.length(segments)
5943 let phase-length = length / num-segments
5944 let overshoot = calc.max(0, (style.factor - 100%) / 100% * phase-length)
5945
5946 // Offset both control points so the curve approximates
5947 // an elliptic arc
5948 let ellipsize-cubic(s, e, c1, c2) = {
5949 let m = vector.scale(vector.add(c1, c2), .5)
5950 let d = vector.sub(e, s)
5951
5952 c1 = vector.sub(m, vector.scale(d, .5))
5953 c2 = vector.add(m, vector.scale(d, .5))
5954
5955 return (s, e, c1, c2)
5956 }
5957
5958 // Return a list of drawables to form a coil-like loop
5959 //
5960 // ____ ┐
5961 // / \ │ Upper curve
5962 // | | ┘
5963 // ..a...b..|.. ┐ Lower curve
5964 // \_/ ┘
5965 //
5966 // └──┘
5967 // Overshoot
5968 //
5969 let fn(i, a, b, norm) = {
5970 let ab = vector.sub(b, a)
5971 let amplitude = resolve-amplitude(ctx, style.amplitude, i, num-segments)
5972 let up = vector.scale(norm, amplitude / 2)
5973 let dist = vector.dist(a, b)
5974
5975 let d = vector.norm(ab)
5976 let overshoot-at(i) = if num-segments <= 1 {
5977 0
5978 } else if close {
5979 overshoot / 2
5980 } else {
5981 i / (num-segments - 1) * overshoot
5982 }
5983
5984 let next-a = vector.sub(b, vector.scale(d, overshoot-at(i + 1)))
5985 let a = vector.sub(a, vector.scale(d, overshoot-at(i)))
5986 let b = vector.add(b, vector.scale(d, overshoot-at(num-segments - i)))
5987 let m = vector.scale(vector.add(a, b), .5)
5988 let m-up = vector.add(m, up)
5989 let m-down = vector.sub(vector.scale(vector.add(next-a, b), .5), up)
5990
5991 let upper = bezier.cubic-through-3points(a, m-up, b)
5992 upper = ellipsize-cubic(..upper)
5993
5994 let lower = bezier.cubic-through-3points(b, m-down, next-a)
5995 lower = ellipsize-cubic(..lower)
5996
5997 if i < num-segments - 1 or close {
5998 return (
5999 draw.bezier(..upper, mark: none),
6000 draw.bezier(..lower, mark: none),
6001 )
6002 } else {
6003 return (draw.bezier(..upper, mark: none),)
6004 }
6005 }
6006
6007 return draw.merge-path(
6008 finalize-path(ctx, segments, style,
6009 _path-effect(ctx, segments, fn, close: close, style).flatten(),
6010 close: close),
6011 ..style,
6012 name: name,
6013 close: close)
6014})
6015
6016/// Draw a wave along a path using a catmull-rom curve
6017///
6018/// The number of phases can be controlled via the `segments` or `segment-length` style key, and the width via `amplitude`.
6019///
6020/// ```typc example
6021/// line((0,0), (2,1), stroke: gray)
6022/// cetz.decorations.wave(line((0,0), (2,1)), amplitude: .25, start: 10%, stop: 90%)
6023/// ```
6024///
6025/// - target (drawable): Target path
6026/// - close (auto,bool): Close the path
6027/// - name (none,string): Element name
6028/// - ..style (style): Style
6029///
6030/// ## Styling
6031/// *Root*: `wave`
6032///
6033/// - tension (float) = 0.5 Catmull-Rom curve tension, see [Catmull](/api/draw-functions/shapes/catmull)
6034#let wave(target, close: auto, name: none, ..style) = draw.get-ctx(ctx => {
6035 let style = styles.resolve(ctx, merge: style.named(),
6036 base: wave-default-style, root: "wave")
6037
6038 let (segments, close) = get-segments(ctx, target)
6039 let style = resolve-style(ctx, segments, style)
6040 let num-segments = style.segments
6041
6042 // Return a list of points for the catmull-rom curve
6043 //
6044 // ╭ ma ╮ ▲
6045 // │ │ │ Up
6046 // ..a....m....b.. '
6047 // │ │
6048 // ╰ mb ╯
6049 //
6050 let fn(i, a, b, norm) = {
6051 let ab = vector.sub(b, a)
6052 let up = vector.scale(norm, +resolve-amplitude(ctx, style.amplitude, i + .25, num-segments) / 2)
6053 let down = vector.scale(norm, -resolve-amplitude(ctx, style.amplitude, i + .75, num-segments) / 2)
6054
6055 let ma = vector.add(vector.add(a, vector.scale(ab, .25)), up)
6056 let m = vector.add(a, vector.scale(ab, .50))
6057 let mb = vector.add(vector.sub(b, vector.scale(ab, .25)), down)
6058
6059 if not close {
6060 if num-segments == 1 {
6061 return (a, ma, mb, b)
6062 } else if i == 0 {
6063 return (a, ma, mb)
6064 } else if i == num-segments - 1 {
6065 return (ma, mb, b,)
6066 }
6067 }
6068
6069 return (ma, mb)
6070 }
6071
6072 return draw.merge-path(
6073 finalize-path(ctx, segments, style, draw.catmull(
6074 .._path-effect(ctx, segments, fn, close: close, style),
6075 close: close), close: close) ,
6076 name: name,
6077 close: close,
6078 ..style)
6079})
6080
6081/// Draw a square-wave along a path using a line-strip
6082///
6083/// The number of phases can be controlled via the `segments` or `segment-length` style key, and the width via `amplitude`.
6084///
6085/// ```typc example
6086/// line((0,0), (2,1), stroke: gray)
6087/// cetz.decorations.square(line((0,0), (2,1)), amplitude: .25, start: 10%, stop: 90%)
6088/// ```
6089///
6090/// - target (drawable): Target path
6091/// - close (auto,bool): Close the path
6092/// - name (none,string): Element name
6093/// - ..style (style): Style
6094///
6095/// ## Styling
6096/// *Root*: `squre`
6097///
6098/// - factor (ratio) = 50% Square-Wave midpoint
6099#let square(target, close: auto, name: none, ..style) = draw.get-ctx(ctx => {
6100 let style = styles.resolve(ctx, merge: style.named(),
6101 base: square-default-style, root: "square")
6102
6103 let (segments, close) = get-segments(ctx, target)
6104 let style = resolve-style(ctx, segments, style)
6105 let num-segments = style.segments
6106 let factor = calc.max(0, calc.min(style.factor / 100%, 1))
6107
6108 // Return a list of points for the line-strip
6109 //
6110 // +----+ ▲
6111 // | | │ Up
6112 // ..a....m....b.. '
6113 // | |
6114 // +----+
6115 //
6116 let fn(i, a, b, norm) = {
6117 let ab = vector.sub(b, a)
6118 let up = vector.scale(norm, +resolve-amplitude(ctx, style.amplitude, i + .25, num-segments) / 2)
6119 let down = vector.scale(norm, -resolve-amplitude(ctx, style.amplitude, i + .75, num-segments) / 2)
6120 let m = vector.add(a, vector.scale(ab, factor))
6121
6122 if not close {
6123 if i == 0 {
6124 return (a, vector.add(a, up),
6125 vector.add(m, up), vector.add(m, down),
6126 vector.add(b, down))
6127 } else if i == num-segments - 1 {
6128 return (vector.add(a, up),
6129 vector.add(m, up), vector.add(m, down),
6130 vector.add(b, down), b)
6131 }
6132 }
6133
6134 return (vector.add(a, up),
6135 vector.add(m, up), vector.add(m, down),
6136 vector.add(b, down))
6137 }
6138
6139 return draw.merge-path(
6140 finalize-path(ctx, segments, style, draw.line(
6141 .._path-effect(ctx, segments, fn, close: close, style),
6142 close: close), close: close) ,
6143 name: name,
6144 close: close,
6145 ..style)
6146})
6147#let base-style = (stroke: (paint: black), fill: none)
6148
6149/// Create a new palette based on a base style
6150///
6151/// ```typc example
6152/// let p = cetz.palette.new(colors: (red, blue, green))
6153/// for i in range(0, p("len")) {
6154/// set-style(..p(i))
6155/// circle((0,0), radius: .5)
6156/// set-origin((1.1, 0))
6157/// }
6158/// ```
6159///
6160/// The functions returned by this function have the following named arguments:
6161/// - fill (bool) = true: If true, the returned fill color is one of the colors from the `colors` list, otherwise the base styles fill is used.
6162/// - stroke (bool) = false: If true, the returned stroke color is one of the colors from the `colors` list, otherwise the base styles stroke color is used.
6163///
6164/// You can use a palette for stroking via: `red.with(stroke: true)`.
6165///
6166/// - base (style): Style dictionary to use as base style for the styles generated per color
6167/// - colors (none, array): List of colors the returned palette should return styles with.
6168/// - dash (none, array): List of stroke dash patterns the returned palette should return styles with.
6169/// -> function
6170#let new(base: base-style, colors: (), dash: ()) = {
6171 if not "stroke" in base { base.stroke = (paint: black, thickness: 1pt, dash: "solid") }
6172 if not "fill" in base { base.fill = none }
6173
6174 let color-n = colors.len()
6175 let pattern-n = dash.len()
6176 return (index, fill: true, stroke: false) => {
6177 if index == "len" { return calc.max(color-n, pattern-n, 1) }
6178
6179 let style = base
6180 if pattern-n > 0 {
6181 style.stroke.dash = dash.at(calc.rem(index, pattern-n))
6182 }
6183 if color-n > 0 {
6184 if stroke {
6185 style.stroke.paint = colors.at(calc.rem(index, color-n))
6186 }
6187 if fill {
6188 style.fill = colors.at(calc.rem(index, color-n))
6189 }
6190 }
6191 return style
6192 }
6193}
6194
6195// Predefined color themes
6196#let tango-colors = (
6197 "edd400", "f57900", "c17d11",
6198 "73d216", "3465a4", "75507b",
6199 "cc0000", "d3d7cf", "555753").map(rgb)
6200#let tango-light-colors = (
6201 "fce94f", "fcaf3e", "e9b96e",
6202 "8ae234", "729fcf", "ad7fa8",
6203 "ef2929", "eeeeec", "888a85").map(rgb)
6204#let tango-dark-colors = (
6205 "c4a000", "ce5c00", "8f5902",
6206 "4e9a06", "204a87", "5c3566",
6207 "a40000", "babdb6", "2e3436").map(rgb)
6208#let rainbow-colors = (
6209 "#9400D4", "#4B0082", "#0000FF",
6210 "#00FF00", "#FFFF00", "#FF7F00",
6211 "#FF0000").map(rgb)
6212
6213#let red-colors = (
6214 "#FFCCCC", "#FF9999", "#FF6666",
6215 "#FF3333", "#CC0000").map(rgb)
6216#let orange-colors = (
6217 "#FFE5CC", "#FFCC99", "#FFB266",
6218 "#FF9933", "#FF8000").map(rgb)
6219#let light-green-colors = (
6220 "#E5FFCC", "#CCFF99", "#B2FF66",
6221 "#99FF33", "#72E300", "#66CC00",
6222 "#55A800", "#478F00", "#3A7300",
6223 "#326300").map(rgb)
6224#let dark-green-colors = (
6225 "#80E874", "#5DD45D", "#3CC23C",
6226 "#009900", "#006E00").map(rgb)
6227#let turquoise-colors = (
6228 "#C0FFD3", "#99FFCC", "#66FFB2",
6229 "#33FF99", "#4BD691").map(rgb)
6230#let cyan-colors = (
6231 "#CCFFFF", "#99FFFF", "#66FFFF",
6232 "#00F3F3", "#00DADA").map(rgb)
6233#let blue-colors = (
6234 "#BABAFF", "#9999FF", "#6666FF",
6235 "#3333FF", "#0000CC").map(rgb)
6236#let indigo-colors = (
6237 "#BABAFF", "#9999FF", "#6666FF",
6238 "#3333FF", "#0000CC").map(rgb)
6239#let purple-colors = (
6240 "#E0C2FF", "#CC99FF", "#B266FF",
6241 "#9933FF", "#7F00FF").map(rgb)
6242#let magenta-colors = (
6243 "#FFD4FF", "#FF99FF", "#FF66FF",
6244 "#F331F3", "#DA00DA").map(rgb)
6245#let pink-colors = (
6246 "#FFCCE5", "#FF99CC", "#FF66B2",
6247 "#FF3399", "#F20C7F", "#DB006B",
6248 "#C30061", "#99004C", "#800040",
6249 "#660033").map(rgb)
6250
6251
6252// Predefined palettes
6253#let gray = new(colors: range(90, 40, step: -12).map(v => luma(v * 1%)))
6254
6255#let red = new(colors: red-colors)
6256#let orange = new(colors: orange-colors)
6257#let light-green = new(colors: light-green-colors)
6258#let dark-green = new(colors: dark-green-colors)
6259#let turquoise = new(colors: turquoise-colors)
6260#let cyan = new(colors: cyan-colors)
6261#let blue = new(colors: blue-colors)
6262#let indigo = new(colors: indigo-colors)
6263#let purple = new(colors: purple-colors)
6264#let magenta = new(colors: magenta-colors)
6265#let pink = new(colors: pink-colors)
6266
6267#let rainbow = new(colors: rainbow-colors)
6268
6269#let tango = new(colors: tango-colors)
6270#let tango-light = new(colors: tango-light-colors)
6271#let tango-dark = new(colors: tango-dark-colors)
6272#let _pin-name(name) = "cetz-pin-" + name
6273
6274/// Place a pin aka. cetz anchor in the document
6275#let pin(name) = [ #metadata("cetz-pin-tracker") #label(_pin-name(name)) ]
6276
6277/// Returns all pins
6278#let get-pin() = context {
6279 let result = ()
6280 for item in locate(metadata) {
6281 if item.value.starts-with("cetz-pin-") {
6282 let name = item.value.slice(9)
6283
6284 result.insert(name, item)
6285 }
6286 }
6287 panic(result)
6288}
6289// CeTZ Library for Layouting Tree-Nodes
6290#import "/src/util.typ"
6291#import "/src/draw.typ"
6292#import "/src/coordinate.typ"
6293#import "/src/vector.typ"
6294#import "/src/matrix.typ"
6295#import "/src/process.typ"
6296#import "/src/anchor.typ" as anchor_
6297
6298#let typst-content = content
6299
6300// Default edge draw callback
6301//
6302// - from (string): Source element name
6303// - to (string): Target element name
6304// - parent (node): Parent (source) tree node
6305// - child (node): Child (target) tree node
6306#let default-draw-edge(from, to, parent, child) = {
6307 draw.line(from, to)
6308}
6309
6310// Default node draw callback
6311//
6312// - node (node): The node to draw
6313#let default-draw-node(node, _) = {
6314 let text = if type(node) in (content, str, int, float) {
6315 [#node]
6316 } else if type(node) == dictionary {
6317 node.content
6318 }
6319
6320 draw.get-ctx(ctx => {
6321 draw.content((), text)
6322 })
6323}
6324
6325/// Lays out and renders tree nodes.
6326///
6327/// For each node, the `tree` function creates an anchor of the format `"node-<depth>-<child-index>"` that can be used to query a nodes position on the canvas.
6328///
6329/// ```typc example
6330/// import cetz.tree
6331/// set-style(content: (padding: .1))
6332/// tree.tree(([Root], ([A], [A.A], [A.B]), ([B], [B.A])))
6333/// ```
6334///
6335/// - root (array): A nested array of content that describes the structure the tree should take. Example: `([root], [child 1], ([child 2], [grandchild 1]))`
6336/// - draw-node (auto,function): The function to call to draw a node. The function will be passed two positional arguments, the node to draw and the node's parent, and is expected to return elements (`(node, parent-node) => elements`). The node's position is accessible through the "center" anchor or by using the previous position coordinate `()`. If `auto` is given, just the node's value will be drawn as content. The following predefined styles can be used:
6337/// - draw-edge (none,auto,function): The function to call draw an edge between two nodes. The function will be passed the name of the starting node, the name of the ending node, the start node, the end node, and is expected to return elements (`(source-name, target-name, parent-node, child-node) => elements`). If `auto` is given, a straight line will be drawn between nodes.
6338/// - direction (str): A string describing the direction the tree should grow in ("up", "down", "left", "right")
6339/// - parent-position (str): Positioning of parent nodes (begin, center, end)
6340/// - grow (float): Depth grow factor
6341/// - spread (float): Sibling spread factor
6342/// - name (none,str): The tree element's name
6343/// - node-layer (int): Layer to draw nodes on
6344/// - edge-layer (int): Layer to draw edges on
6345#let tree(
6346 root,
6347 draw-node: auto,
6348 draw-edge: auto,
6349 direction: "down",
6350 parent-position: "center",
6351 grow: 1,
6352 spread: 1,
6353 name: none,
6354 node-layer: 1,
6355 edge-layer: 0
6356 ) = {
6357 assert(parent-position in ("begin", "center","end", "after-end"))
6358 assert(grow > 0)
6359 assert(spread > 0)
6360
6361 direction = (
6362 up: "north",
6363 down: "south",
6364 right: "east",
6365 left: "west"
6366 ).at(direction)
6367
6368 if draw-edge == auto {
6369 draw-edge = default-draw-edge
6370 } else if draw-edge == none {
6371 draw-edge = (..) => ()
6372 }
6373
6374 if draw-node == auto {
6375 draw-node = default-draw-node
6376 }
6377 assert(draw-node != none, message: "Node draw callback must be set!")
6378
6379 let build-node(tree, depth: 0, sibling: 0) = {
6380 let children = ()
6381 let content = none
6382 if type(tree) == array {
6383 children = tree.slice(1).enumerate().map(
6384 ((n, c)) => build-node(c, depth: depth + 1, sibling: n)
6385 )
6386 content = tree.at(0)
6387 } else {
6388 content = tree
6389 }
6390
6391 return (
6392 x: 0,
6393 y: depth * grow,
6394 n: sibling,
6395 depth: depth,
6396 children: children,
6397 content: content
6398 )
6399 }
6400
6401 // Layout node recursive
6402 //
6403 // return:
6404 // (node, left-x, right-x)
6405 let layout-node(node, shift-x) = {
6406 if node.children.len() == 0 {
6407 node.x = shift-x
6408 return (node, node.x, node.x)
6409 } else {
6410 let (min-x, max-x) = (none, none)
6411 let (left, right) = (none, none)
6412
6413 let n-children = node.children.len()
6414 for i in range(0, n-children) {
6415 let child = node.children.at(i)
6416 let (child-min-x, child-max-x) = (none, none)
6417
6418 (child, child-min-x, child-max-x) = layout-node(child, shift-x)
6419 node.children.at(i) = child
6420
6421 left = util.min(child.x, left)
6422 right = util.max(child.x, right)
6423
6424 min-x = util.min(min-x, child-min-x)
6425 max-x = util.max(max-x, child-max-x)
6426
6427 shift-x = child-max-x + spread
6428 }
6429
6430 if parent-position == "begin" {
6431 node.x = left
6432 } else if parent-position == "center" {
6433 node.x = left + (right - left) / 2
6434 } else if parent-position == "end" {
6435 node.x = right
6436 } else { //after-end
6437 node.x = right+spread
6438 max-x = max-x + spread
6439 }
6440
6441 node.direct-min-x = left
6442 node.direct-max-x = right
6443 node.min-x = min-x
6444 node.max-x = max-x
6445
6446 return (node, min-x, max-x)
6447 }
6448 }
6449
6450 let node-position(node) = {
6451 if direction == "south" {
6452 return (node.x, -node.y)
6453 } else if direction == "north" {
6454 return (node.x, node.y)
6455 } else if direction == "west" {
6456 return (-node.y, node.x)
6457 } else if direction == "east" {
6458 return (node.y, node.x)
6459 } else {
6460 panic(message: "Invalid tree direction.")
6461 }
6462 }
6463
6464 let anchors(node, parent-path) = {
6465 if parent-path != none {
6466 parent-path += "-"
6467 } else {
6468 parent-path = ""
6469 }
6470
6471 let d = (:)
6472 d.insert(parent-path + str(node.n), node-position(node))
6473 for child in node.children {
6474 d += anchors(child, parent-path + str(node.n))
6475 }
6476 return d
6477 }
6478
6479 let build-element(node, parent-name) = {
6480 let name = if parent-name != none {
6481 parent-name + "-" + str(node.n)
6482 } else {
6483 "0"
6484 }
6485
6486 // Render element
6487 node.name = name
6488 node.group-name = "g" + name
6489 node.element = {
6490 draw.anchor(node.name, node-position(node))
6491 draw.group(name: node.group-name, {
6492 draw.move-to(node-position(node))
6493 draw.anchor("default", ())
6494 draw-node(node, parent-name)
6495 })
6496 }
6497
6498 // Render children
6499 node.children = node.children.map(c => build-element(c, name))
6500
6501 // Render edges
6502 node.edges = if node.children != () {
6503 draw.group({
6504 for child in node.children {
6505 draw-edge(node.group-name, child.group-name, node, child)
6506 }
6507 })
6508 } else { () }
6509
6510 return node
6511 }
6512
6513 let root = build-node(root)
6514 let (nodes, ..) = layout-node(root, 0)
6515 let node = build-element(nodes, none)
6516
6517 // Render node recursive
6518 let render(node) = {
6519 if node.element != none {
6520 draw.on-layer(node-layer, node.element)
6521 if "children" in node {
6522 for child in node.children {
6523 render(child)
6524 }
6525 }
6526 draw.on-layer(edge-layer, node.edges)
6527 }
6528 }
6529
6530 draw.group(name: name, render(node))
6531}
6532#import "drawable.typ"
6533#import "path-util.typ"
6534#import "vector.typ"
6535
6536// Calculate triangular tip offset, depending on the strokes
6537// join type.
6538//
6539// The angle is calculated for an isosceles triangle of base style.widh
6540// and height style.length
6541#let _calculate-tip-offset(style) = {
6542 if style.stroke.join == "round" {
6543 return style.stroke.thickness / 2
6544 }
6545
6546 if style.length == 0 {
6547 return 0
6548 }
6549
6550 let angle = calc.atan(style.width / (2 * style.length) / if style.harpoon { 2 } else { 1 } ) * 2
6551 // If the miter length divided by the stroke width exceeds
6552 // the stroke miter limit then the miter join is converted to a bevel.
6553 // See: https://svgwg.org/svg2-draft/painting.html#LineJoin
6554 if style.stroke.join == "miter" {
6555 let angle = calc.abs(angle)
6556 if angle > 0deg {
6557 let miter-limit = 1 / calc.sin(angle / 2)
6558 if miter-limit <= style.stroke.miter-limit {
6559 return miter-limit * (style.stroke.thickness / 2)
6560 }
6561 }
6562 }
6563
6564 // style.stroke.join must be "bevel"
6565 return calc.sin(angle/2) * (style.stroke.thickness / 2)
6566}
6567
6568#let create-tip-and-base-anchor(style, tip, base, center: none) = {
6569 if base == tip { base = vector.add(tip, (1e-8, 0, 0)) }
6570 let dir = vector.norm(vector.sub(tip, base))
6571
6572 import "/src/draw.typ": *
6573 anchor("tip", vector.add(tip, vector.scale(dir, style.stroke.thickness / 2)))
6574 anchor("base", vector.sub(base, vector.scale(dir, style.stroke.thickness / 2)))
6575}
6576
6577#let create-triangle-tip-and-base-anchor(style, tip, base, center: none) = {
6578 if base == tip { base = vector.add(tip, (1e-8, 0, 0)) }
6579 let dir = vector.norm(vector.sub(tip, base))
6580
6581 import "/src/draw.typ": *
6582 if style.reverse {
6583 // Since tip and base are now "swapped", we add the stroke thickness to the triangle
6584 // base. To get smooth looking connections between the triangle tip and a connecting line,
6585 // we do not add the tip-offset.
6586 anchor("tip", vector.add(tip, vector.scale(dir, style.stroke.thickness / 2)))
6587 anchor("base", base)
6588 } else {
6589 anchor("tip", vector.add(tip, vector.scale(dir, _calculate-tip-offset(style))))
6590 anchor("base", vector.sub(base, vector.scale(dir, style.stroke.thickness / 2)))
6591 }
6592}
6593
6594#let create-diamond-tip-and-base-anchor(style, tip, base, center: none, ratio: 50%) = {
6595 if base == tip { base = vector.add(tip, (1e-8, 0, 0)) }
6596 let dir = vector.norm(vector.sub(tip, base))
6597
6598 import "/src/draw.typ": *
6599
6600 let tip-style = style
6601 tip-style.length = style.length * (ratio / 100%)
6602 if style.reverse {
6603 anchor("tip", tip)
6604 } else {
6605 anchor("tip", vector.add(tip, vector.scale(dir, _calculate-tip-offset(tip-style))))
6606 }
6607 if style.reverse {
6608 anchor("base", vector.sub(base, vector.scale(dir, _calculate-tip-offset(tip-style))))
6609 } else {
6610 anchor("base", base)
6611 }
6612}
6613
6614// Dictionary of built-in mark styles
6615//
6616// (style) => (<elements..>)
6617#let marks = (
6618 triangle: (style) => {
6619 import "/src/draw.typ": *
6620
6621 if style.harpoon {
6622 line((0,0), (style.length, 0), (style.length, +style.width / 2), close: true)
6623 } else {
6624 line((0,0), (style.length, -style.width / 2), (style.length, +style.width / 2), close: true)
6625 }
6626
6627 create-triangle-tip-and-base-anchor(style, (0, 0), (style.length, 0))
6628 },
6629 // A mark in the shape of an arrow tip.
6630 stealth: (style) => {
6631 import "/src/draw.typ": *
6632
6633 let (l, w, i) = (style.length, style.width, style.inset)
6634
6635 if style.harpoon {
6636 line((0,0), (l, w / 2), (l - i, 0), close: true)
6637 } else {
6638 line((0,0), (l, w / 2), (l - i, 0), (l, -w / 2), close: true)
6639 }
6640
6641 create-triangle-tip-and-base-anchor(style, (0, 0), (l - i, 0))
6642 },
6643 bar: (style) => {
6644 import "/src/draw.typ": *
6645
6646 let w = style.width
6647
6648 if style.harpoon {
6649 line((0, w / 2), (0, 0))
6650 } else {
6651 line((0, w / 2), (0, -w / 2))
6652 }
6653
6654 create-tip-and-base-anchor(style, (0, 0), (0, 0))
6655 },
6656 ellipse: (style) => {
6657 import "/src/draw.typ": *
6658
6659 let r = (style.length / 2, style.width / 2)
6660
6661 if style.harpoon {
6662 arc((0, 0), delta: -180deg, start: 0deg, radius: r, anchor: "origin", mode: "PIE")
6663 } else {
6664 circle((0, 0), radius: r)
6665 }
6666
6667 create-tip-and-base-anchor(style, (r.at(0), 0), (-r.at(0), 0))
6668 },
6669 circle: (style) => {
6670 import "/src/draw.typ": *
6671
6672 let r = calc.min(style.length, style.width) / 2
6673
6674 if style.harpoon {
6675 arc((0, 0), delta: -180deg, start: 0deg, radius: r, anchor: "origin", mode: "PIE")
6676 } else {
6677 circle((0, 0), radius: r)
6678 }
6679
6680 create-tip-and-base-anchor(style, (r, 0), (-r, 0))
6681 },
6682 bracket: (style) => {
6683 import "/src/draw.typ": *
6684
6685 let (l, w, i) = (style.length, style.width, style.inset)
6686
6687 if style.harpoon {
6688 line((-l - i, w / 2), (0, w / 2), (0, 0), fill: none)
6689 } else {
6690 line((-l - i, w / 2), (0, w / 2), (0, -w / 2), (-l - i, -w / 2), fill: none)
6691 }
6692
6693 create-tip-and-base-anchor(style, (0, 0), (-1e-8, 0), center: ((-l - i) / 2, 0))
6694 },
6695 diamond: (style) => {
6696 import "/src/draw.typ": *
6697
6698 let (l, w) = (style.length, style.width)
6699
6700 if style.harpoon {
6701 line((0,0), (l / 2, w / 2), (l, 0), close: true)
6702 } else {
6703 line((0,0), (l / 2, w / 2), (l, 0), (l / 2, -w / 2), close: true)
6704 }
6705
6706 create-diamond-tip-and-base-anchor(style, (0, 0), (l, 0))
6707 },
6708 rect: (style) => {
6709 import "/src/draw.typ": *
6710
6711 let (l, w) = (style.length, style.width)
6712
6713 if style.harpoon {
6714 rect((0, -w / 2), (-l, +w / 2))
6715 } else {
6716 rect((0, -w / 2), (-l, +w / 2))
6717 }
6718
6719 create-tip-and-base-anchor(style, (0, 0), (-l, 0))
6720 },
6721 hook: (style) => {
6722 import "/src/draw.typ": *
6723
6724 let r = calc.min(style.length, style.width / 2) / 2
6725 let (l, i) = (style.length, style.inset)
6726
6727 merge-path({
6728 line((i, -2 * r), (0, -2 * r))
6729 arc((0, 0), delta: -180deg, start: -90deg, radius: r, anchor: "end")
6730 if not style.harpoon {
6731 arc((0, 0), delta: -180deg, start: -90deg, radius: r, anchor: "start")
6732 line((i, +2 * r), (0, +2 * r))
6733 }
6734 }, fill: none)
6735
6736 line((0, 0), (l - r, 0))
6737
6738 create-tip-and-base-anchor(style, (-r, 0), (l - r, 0), center: ((-r + i) / 2, 0))
6739 },
6740 // An unfilled mark in the shape of an angle bracket (>).
6741 straight: (style) => {
6742 import "/src/draw.typ": *
6743
6744 let (l, w) = (style.length, style.width)
6745
6746 if style.harpoon {
6747 line((l, w / 2), (0, 0), fill: none)
6748 } else {
6749 line((l, w / 2), (0, 0), (l, -w / 2), fill: none)
6750 }
6751
6752 create-triangle-tip-and-base-anchor(style, (0, 0), (0, 0))
6753 },
6754 barbed: (style) => {
6755 import "/src/draw.typ": *
6756
6757 let style = style
6758 style.stroke.join = "round"
6759
6760 let (l, w) = (style.length, style.width)
6761
6762 let ctrl-a = (l, 0)
6763 let ctrl-b = (0, 0)
6764
6765 merge-path({
6766 bezier((l, w / 2), (0, 0), ctrl-a, ctrl-b)
6767 if not style.harpoon {
6768 bezier((0, 0), (l, -w / 2), ctrl-b, ctrl-a)
6769 }
6770 }, ..style)
6771
6772 create-tip-and-base-anchor(style, (0, 0), (1e-6, 0))
6773 },
6774 plus: (style) => {
6775 import "/src/draw.typ": *
6776
6777 let style = style
6778 style.stroke.join = "round"
6779
6780 let (l, w) = (style.length, style.width)
6781
6782 line((-l / 2, 0), (+l / 2, 0))
6783 line((0, -w / 2), (0, +w / 2))
6784
6785 create-tip-and-base-anchor(style, (0, 0), (l / 2, 0))
6786 },
6787 x: (style) => {
6788 import "/src/draw.typ": *
6789
6790 let style = style
6791 style.stroke.join = "round"
6792
6793 let (l, w) = (style.length, style.width)
6794
6795 line((-l / 2, w / 2), (+l / 2, -w / 2))
6796 line((-l / 2, -w / 2), (+l / 2, +w / 2))
6797
6798 create-tip-and-base-anchor(style, (0, 0), (0, 0))
6799 },
6800 star: (style) => {
6801 import "/src/draw.typ": *
6802
6803 let (l, w) = (style.length, style.width)
6804
6805 let n = 5
6806 for i in range(0, n) {
6807 let a = 360deg / n * i
6808 line((0, 0), (calc.cos(a) * l / 2, calc.sin(a) * w / 2))
6809 }
6810
6811 create-tip-and-base-anchor(style, (0, 0), (l / 2, 0))
6812 },
6813)
6814#let names = marks.keys()
6815
6816// Mark mnemonics
6817// Each mnemonic maps to a dictionary of:
6818// - reverse (bool)
6819// - flip (bool)
6820// - harpoon (bool)
6821// TODO: Resolve mark styles at a later point, to support all style keys here
6822#let mnemonics = (
6823 ">": ("triangle", (:)),
6824 "<": ("triangle", (reverse: true)),
6825 "<>": ("diamond", (:)),
6826 "[]": ("rect", (:)),
6827 "]": ("bracket", (:)),
6828 "[": ("bracket", (reverse: true)),
6829 "|": ("bar", (:)),
6830 "o": ("circle", (:)),
6831 "+": ("plus", (:)),
6832 "x": ("x", (:)),
6833 "*": ("star", (:)),
6834)
6835
6836// Get a mark shape + reverse tuple for a mark name
6837#let get-mark(ctx, symbol) = {
6838 symbol = ctx.marks.mnemonics.at(symbol, default: symbol)
6839 if symbol in ctx.marks.marks {
6840 return (ctx.marks.marks.at(symbol), (:))
6841 }
6842
6843 let (symbol, defaults) = mnemonics.at(symbol, default: (symbol, (:)))
6844 assert(symbol in marks, message: "Unknown mark '" + symbol + "'")
6845 return (marks.at(symbol), defaults)
6846}
6847#import "drawable.typ"
6848#import "vector.typ"
6849#import "matrix.typ"
6850#import "util.typ"
6851#import "path-util.typ"
6852#import "styles.typ"
6853#import "mark-shapes.typ": get-mark
6854#import "process.typ"
6855
6856/// Checks if a mark should be drawn according to the current style.
6857/// - style (style): The current style.
6858/// -> bool
6859#let check-mark(style) = style != none and (style.start, style.end, style.symbol).any(v => v != none)
6860
6861/// Processes the mark styling.
6862/// TODO: remember what is actually going on here.
6863///
6864/// - ctx (context): The context object.
6865/// - style (style): The current style.
6866/// - root (str): Where the mark is being placed, normally either `"start"` or `"end"`. Allows different styling for marks in different directions.
6867/// - path-length (float): The length of the path. This is used for relative offsets.
6868#let process-style(ctx, style, root, path-length) = {
6869 let base-style = (
6870 symbol: auto,
6871 fill: auto,
6872 stroke: auto,
6873 slant: auto,
6874 harpoon: auto,
6875 flip: auto,
6876 reverse: auto,
6877 inset: auto,
6878 width: auto,
6879 scale: auto,
6880 length: auto,
6881 sep: auto,
6882 pos: auto,
6883 offset: auto,
6884 flex: auto,
6885 xy-up: auto,
6886 z-up: auto,
6887 shorten-to: auto,
6888 position-samples: auto,
6889 anchor: auto,
6890 )
6891
6892 if type(style.at(root)) != array {
6893 style.at(root) = (style.at(root),)
6894 }
6895 if type(style.symbol) != array {
6896 style.symbol = (style.symbol,)
6897 }
6898
6899 let out = ()
6900 for i in range(calc.max(style.at(root).len(), style.symbol.len())) {
6901 let style = style
6902 style.symbol = style.symbol.at(i, default: auto)
6903 style.at(root) = style.at(root).at(i, default: auto)
6904
6905 if type(style.symbol) == dictionary {
6906 style = styles.resolve(style, merge: style.symbol)
6907 }
6908
6909 if type(style.at(root)) == str {
6910 style.symbol = style.at(root)
6911 } else if type(style.at(root)) == dictionary {
6912 style = styles.resolve(style, root: root, base: base-style)
6913 }
6914
6915 style.stroke = util.resolve-stroke(style.stroke)
6916 style.stroke.thickness = util.resolve-number(ctx, style.stroke.thickness)
6917
6918 if "angle" in style and type(style.angle) == angle {
6919 style.width = calc.tan(style.angle / 2) * style.length * 2
6920 }
6921
6922 // Stroke thickness relative attributes
6923 for (k, v) in style {
6924 if k in ("length", "width", "inset", "sep") {
6925 style.insert(k, if type(v) == ratio {
6926 style.stroke.thickness * v / 100%
6927 } else {
6928 util.resolve-number(ctx, v)
6929 } * style.scale)
6930 }
6931 }
6932
6933 // Path length relative attributes
6934 for k in ("offset", "pos",) {
6935 let v = style.at(k)
6936 if v != none and v != auto {
6937 style.insert(k, if type(v) == ratio {
6938 v * path-length / 100%
6939 } else {
6940 util.resolve-number(ctx, v)
6941 })
6942 }
6943 }
6944
6945 out.push(style)
6946 }
6947 return out
6948}
6949
6950#let transform-mark(style, mark, pos, dir, flip: false, reverse: false, slant: none, harpoon: false) = {
6951 let up = style.xy-up
6952 if dir.at(2) != 0 {
6953 up = style.z-up
6954 }
6955
6956 assert(style.anchor in ("tip", "base", "center"))
6957 let tip = mark.tip
6958 let base = mark.base
6959 let origin = mark.at(style.anchor)
6960
6961 // Mirror anchors on mark center
6962 if reverse {
6963 (tip, base) = (base, tip)
6964 origin = vector.sub(mark.center, vector.sub(origin, mark.center))
6965 }
6966
6967 mark.offset = vector.dist(origin, tip)
6968
6969 let t = (
6970 // Translate & rotate to the target coordinate & direction
6971 matrix.transform-translate(..pos),
6972 matrix.transform-rotate-dir(dir, up),
6973 matrix.transform-rotate-z(-90deg),
6974
6975 // Rotate mark to have base->tip on the x-axis
6976 matrix.transform-rotate-z(if reverse {
6977 vector.angle2(tip, base)
6978 } else {
6979 vector.angle2(base, tip)
6980 }),
6981
6982 // Translate mark to have its anchor (tip, base, center) at (0,0)
6983 matrix.transform-translate(..vector.scale(origin, if reverse {1} else {-1})),
6984
6985 // Mirror on x and/or y axis
6986 if flip or reverse {
6987 matrix.transform-scale({
6988 if flip {
6989 (y: -1)
6990 }
6991 if reverse {
6992 (x: -1)
6993 }
6994 })
6995 },
6996
6997 // Slant on x axis
6998 if slant not in (none, 0%) {
6999 if type(slant) == ratio {
7000 slant /= 100%
7001 }
7002 matrix.transform-shear-x(slant)
7003 },
7004 )
7005
7006 mark.drawables = drawable.apply-transform(
7007 matrix.mul-mat(..t.filter(m => m != none)),
7008 mark.drawables
7009 )
7010
7011 return mark
7012}
7013
7014#let _eval-mark-shape-and-anchors(ctx, mark, style) = {
7015 if "eval-mark-guard" in ctx {
7016 panic("Recursive mark drawing is not allowed")
7017 }
7018 ctx.eval-mark-guard = true
7019
7020 ctx.groups = ()
7021 ctx.nodes = (:)
7022 ctx.transform = matrix.ident()
7023
7024 import "/src/draw.typ"
7025 let body = draw.group({
7026 draw.set-style(
7027 stroke: style.at("stroke", default: none),
7028 fill: style.at("fill", default: none),
7029 mark: none,
7030 line: (mark: none),
7031 bezier: (mark: none),
7032 arc: (mark: none),
7033 )
7034 mark
7035 }, name: "mark")
7036 let (ctx: ctx, bounds: bounds, drawables: drawables) = process.many(ctx, body)
7037 let anchor-fn = ctx.nodes.at("mark").anchors
7038
7039 // Check if the mark has named anchor
7040 let has-anchor(name) = {
7041 return name in (anchor-fn)(())
7042 }
7043
7044 // Fetch special mark anchors
7045 let get-anchor(name, default: none) = {
7046 if default != none {
7047 if not has-anchor(name) {
7048 return default
7049 }
7050 }
7051 return (anchor-fn)(name)
7052 }
7053
7054 let tip = get-anchor("tip", default: (0, 0, 0))
7055 let base = get-anchor("base", default: tip)
7056 let center = get-anchor("center", default: vector.lerp(tip, base, .5))
7057
7058 return (
7059 tip: tip,
7060 base: base,
7061 center: center,
7062 length: vector.dist(tip, base),
7063 drawables: drawables,
7064 )
7065}
7066
7067/// Places a mark on the given path. Returns a {{dictionary}} with the following keys:
7068/// - drawables (drawable): The mark drawables.
7069/// - distance (float): The length to shorten the path by.
7070/// - pos (float): The position of the mark, can be used to snap the end of the path to after shortening.
7071///
7072/// ---
7073///
7074/// - ctx (context): The canvas context object.
7075/// - styles (style): A processed mark styling.
7076/// - segments (drawable): The path to place the mark on.
7077/// - is-end (bool): TODO
7078/// -> dictionary
7079#let place-mark-on-path(ctx, styles, segments, is-end: false) = {
7080 if type(styles) != array {
7081 styles = (styles,)
7082 }
7083 let distance = 0
7084 let shorten-distance = 0
7085 let shorten-pos = none
7086 let drawables = ()
7087 for (i, style) in styles.enumerate() {
7088 let is-last = i + 1 == styles.len()
7089 if style.symbol == none {
7090 continue
7091 }
7092
7093 // Override position, if set
7094 if style.pos != none {
7095 distance = style.pos
7096 }
7097
7098 // Apply mark offset
7099 distance += style.offset
7100
7101 let (mark-fn, defaults) = get-mark(ctx, style.symbol)
7102
7103 let merge-flag(style, key, default: false) = {
7104 let old = style.at(key)
7105 let def = defaults.at(key, default: default)
7106 style.insert(key, (old or def) and not (old and def))
7107 return style
7108 }
7109
7110 style = merge-flag(style, "reverse")
7111 style = merge-flag(style, "flip")
7112 style = merge-flag(style, "harpoon")
7113
7114 let mark = _eval-mark-shape-and-anchors(ctx, mark-fn(style), style)
7115
7116 let pos = if style.flex {
7117 path-util.point-on-path(
7118 segments,
7119 if distance != 0 {
7120 distance * if is-end { -1 } else { 1 }
7121 } else {
7122 if is-end {
7123 100%
7124 } else {
7125 0%
7126 }
7127 }, extrapolate: true)
7128 } else {
7129 let (_, dir) = path-util.direction(
7130 segments,
7131 if is-end {
7132 100%
7133 } else {
7134 0%
7135 },
7136 clamp: true)
7137 let pt = if is-end {
7138 path-util.segment-end(segments.last())
7139 } else {
7140 path-util.segment-start(segments.first())
7141 }
7142 vector.sub(pt, vector.scale(vector.norm(dir), distance * if is-end { 1 } else { -1 }))
7143 }
7144 assert.ne(pos, none,
7145 message: "Could not determine mark position")
7146
7147 let dir = if style.flex {
7148 let a = pos
7149 let b = path-util.point-on-path(
7150 segments,
7151 (mark.length + distance) * if is-end { -1 } else { 1 },
7152 samples: style.position-samples,
7153 extrapolate: true)
7154 if b != none and a != b {
7155 vector.sub(b, a)
7156 } else {
7157 let (_, dir) = path-util.direction(
7158 segments,
7159 distance,
7160 clamp: true)
7161 vector.scale(dir, if is-end { -1 } else { 1 })
7162 }
7163 } else {
7164 let (_, dir) = path-util.direction(
7165 segments,
7166 if is-end {
7167 100%
7168 } else {
7169 0%
7170 },
7171 clamp: true)
7172 if dir != none {
7173 vector.scale(dir, if is-end { -1 } else { 1 })
7174 }
7175 }
7176 assert.ne(pos, none,
7177 message: "Could not determine mark direction")
7178
7179 mark = transform-mark(
7180 style,
7181 mark,
7182 pos,
7183 dir,
7184 reverse: style.reverse,
7185 slant: style.slant,
7186 flip: style.flip,
7187 harpoon: style.harpoon,
7188 )
7189
7190 // Shorten path to this mark
7191 let inset = mark.at("inset", default: 0)
7192 if style.shorten-to != none and (style.shorten-to == auto or i <= style.shorten-to) {
7193 let offset = mark.offset
7194 inset += offset
7195
7196 shorten-distance = distance + mark.length - inset
7197 shorten-pos = vector.add(pos,
7198 vector.scale(vector.norm(dir), mark.length - inset))
7199 }
7200
7201 drawables += mark.drawables
7202 distance += mark.length
7203
7204 // Add separator
7205 distance += style.sep
7206 }
7207
7208 return (
7209 drawables: drawables,
7210 distance: shorten-distance,
7211 pos: shorten-pos
7212 )
7213}
7214
7215/// Places marks along a path. Returns them as an {{array}} of {{drawable}}.
7216///
7217/// - ctx (context): The context object.
7218/// - style (style): The current mark styling.
7219/// - transform (matrix): The current transformation matrix.
7220/// - path (drawable): The path to place the marks on.
7221/// - add-path (bool): When `true` the shortened path will returned as the first {{drawable}} in the {{array}}
7222/// -> array
7223#let place-marks-along-path(ctx, style, transform, path, add-path: true) = {
7224 let distance = (0, 0)
7225 let snap-to = (none, none)
7226 let drawables = ()
7227
7228 let (path, is-transformed) = if not style.transform-shape and transform != none {
7229 (drawable.apply-transform(transform, path).first(), true)
7230 } else {
7231 (path, false)
7232 }
7233
7234 let segments = path.segments
7235 if style.start != none or style.symbol != none {
7236 let (drawables: start-drawables, distance: start-distance, pos: pt) = place-mark-on-path(
7237 ctx,
7238 process-style(ctx, style, "start", path-util.length(segments)),
7239 segments
7240 )
7241 drawables += start-drawables
7242 distance.first() = start-distance
7243 snap-to.first() = pt
7244 }
7245 if style.end != none or style.symbol != none {
7246 let (drawables: end-drawables, distance: end-distance, pos: pt) = place-mark-on-path(
7247 ctx,
7248 process-style(ctx, style, "end", path-util.length(segments)),
7249 segments,
7250 is-end: true
7251 )
7252 drawables += end-drawables
7253 distance.last() = end-distance
7254 snap-to.last() = pt
7255 }
7256 if distance != (0, 0) {
7257 segments = path-util.shorten-path(
7258 segments,
7259 ..distance,
7260 mode: if style.flex { "CURVED" } else { "LINEAR" },
7261 samples: style.position-samples,
7262 snap-to: snap-to)
7263 }
7264
7265 if add-path {
7266 path.segments = segments
7267 drawables.insert(0, path)
7268 }
7269
7270 // If not transformed pre mark placement,
7271 // transform everything after mark placement.
7272 if not is-transformed {
7273 drawables = drawable.apply-transform(transform, drawables)
7274 }
7275
7276 return drawables
7277}
7278#import "vector.typ"
7279
7280// Global rounding precision
7281#let precision = 8
7282
7283#let _round = calc.round.with(digits: precision)
7284
7285#let cos(x) = {
7286 _round(calc.cos(x))
7287}
7288
7289#let sin(x) = {
7290 _round(calc.sin(x))
7291}
7292
7293#let pi = calc.pi
7294
7295/// Create identity matrix with dimensions $m \times n$
7296///
7297/// - m (int): The number of rows
7298/// - n (int): The number of columns
7299/// - one (float): Value to set as $1$
7300/// - zero (float): Value to set as $0$
7301/// -> matrix
7302#let ident(m: 4, n: 4, one: 1, zero: 0) = {
7303 ({for m in range(0, m) {
7304 ({for n in range(0, n) {
7305 if m == n { (one,) } else { (zero,) }
7306 }},)
7307 }})
7308}
7309
7310/// Returns the dimension of the given matrix as `(m, n)`
7311/// - m (matrix): The matrix
7312/// -> array
7313#let dim(m) = {
7314 return (m.len(), if m.len() > 0 {m.at(0).len()} else {0})
7315}
7316
7317/// Returns the nth column of a matrix as a {{vector}}
7318/// - mat (matrix): Input matrix
7319/// - n (int): The column's index
7320/// -> vector
7321#let column(mat, n) = {
7322 range(0, mat.len()).map(m => mat.at(m).at(n))
7323}
7324
7325/// Replaces the nth column of a matrix with the given vector.
7326/// - mat (matrix): Input matrix.
7327/// - n (int): The index of the column to replace
7328/// - vec (vector): The column data to insert.
7329/// -> matrix
7330#let set-column(mat, n, vec) = {
7331 assert(vec.len() == matrix.len())
7332 for m in range(0, mat.len()) {
7333 mat.at(m).at(n) = vec.at(n)
7334 }
7335}
7336
7337/// Rounds each value in the matrix to a precision.
7338/// - mat (matrix): Input matrix
7339/// - precision (int) = 8: Rounding precision (digits)
7340/// -> matrix
7341#let round(mat, precision: precision) = {
7342 mat.map(r => r.map(v => _round(v, digits: precision)))
7343}
7344
7345/// Returns a $4 \times 4$ translation matrix
7346/// - x (float): The translation in the $x$ direction.
7347/// - y (float): The translation in the $y$ direction.
7348/// - z (float): The translation in the $x$ direction.
7349/// -> matrix
7350#let transform-translate(x, y, z) = {
7351 ((1, 0, 0, x),
7352 (0, 1, 0, y),
7353 (0, 0, 1, z),
7354 (0, 0, 0, 1))
7355}
7356
7357/// Returns a $4 \times 4$ x-shear matrix
7358/// - factor (float): The shear in the $x$ direction.
7359/// -> matrix
7360#let transform-shear-x(factor) = {
7361 ((1, factor, 0, 0),
7362 (0, 1, 0, 0),
7363 (0, 0, 1, 0),
7364 (0, 0, 0, 1))
7365}
7366
7367
7368/// Returns a $4 \times 4$ z-shear matrix
7369/// - factor (float): The shear in the $z$ direction.
7370/// -> matrix
7371#let transform-shear-z(factor) = {
7372 ((1, 0, factor, 0),
7373 (0, 1,-factor, 0),
7374 (0, 0, 1, 0),
7375 (0, 0, 0, 1))
7376}
7377
7378/// Returns a $4 \times 4$ scale matrix
7379/// - f (float,array,dictionary): The scale factor(s) of the matrix. An {{array}} of at least 3 {{float}}s sets the x, y and z scale factors. A {{dictionary}} sets the scale in the direction of the corresponding x, y and z keys. A single {{float}} sets the scale for all directions.
7380/// -> matrix
7381#let transform-scale(f) = {
7382 let (x, y, z) = if type(f) == array {
7383 vector.as-vec(f, init: (1, 1, 1))
7384 } else if type(f) == dictionary {
7385 (f.at("x", default: 1),
7386 f.at("y", default: 1),
7387 f.at("z", default: 1))
7388 } else {
7389 (f, f, f)
7390 }
7391 return(
7392 (x, 0, 0, 0),
7393 (0, y, 0, 0),
7394 (0, 0, z, 0),
7395 (0, 0, 0, 1))
7396}
7397
7398/// Returns a $4 \times 4$ rotation xyz matrix for a direction and up vector
7399/// - dir (vector): idk
7400/// - up (vector): idk
7401/// -> matrix
7402#let transform-rotate-dir(dir, up) = {
7403 dir = vector.norm(dir)
7404 up = vector.norm(up)
7405
7406 let (dx, dy, dz) = dir
7407 let (ux, uy, uz) = up
7408 let (rx, ry, rz) = vector.norm(vector.cross(dir, up))
7409
7410 ((rx, dx, ux, 0),
7411 (ry, dy, uy, 0),
7412 (rz, dz, uz, 0),
7413 (0, 0, 0, 1))
7414}
7415
7416// Return 4x4 rotate x matrix
7417/// Returns a $4 \times 4$ $x$ rotation matrix
7418/// - angle (angle): The angle to rotate around the $x$ axis
7419/// -> matrix
7420#let transform-rotate-x(angle) = {
7421 ((1, 0, 0, 0),
7422 (0, cos(angle), -sin(angle), 0),
7423 (0, sin(angle), cos(angle), 0),
7424 (0, 0, 0, 1))
7425}
7426
7427// Return 4x4 rotate y matrix
7428/// Returns a $4 \times 4$ $y$ rotation matrix
7429/// - angle (angle): The angle to rotate around the $y$ axis
7430/// -> matrix
7431#let transform-rotate-y(angle) = {
7432 ((cos(angle), 0, -sin(angle), 0),
7433 (0, 1, 0, 0),
7434 (sin(angle), 0, cos(angle), 0),
7435 (0, 0, 0, 1))
7436}
7437
7438// Return 4x4 rotate z matrix
7439/// Returns a $4 \times 4$ $z$ rotation matrix
7440/// - angle (angle): The angle to rotate around the $z$ axis
7441/// -> matrix
7442#let transform-rotate-z(angle) = {
7443 ((cos(angle), -sin(angle), 0, 0),
7444 (sin(angle), cos(angle), 0, 0),
7445 (0, 0, 1, 0),
7446 (0, 0, 0, 1))
7447}
7448
7449// Return 4x4 rotate xz matrix
7450/// Returns a $4 \times 4$ $x z$ rotation matrix
7451/// - x (angle): The angle to rotate around the $x$ axis
7452/// - z (angle): The angle to rotate around the $z$ axis
7453/// -> matrix
7454#let transform-rotate-xz(x, z) = {
7455 ((cos(z), sin(z), 0, 0),
7456 (-cos(x)*sin(z), cos(x)*cos(z), -sin(x), 0),
7457 (sin(x)*sin(z), -sin(x)*cos(z), cos(x), 1),
7458 (0, 0, 0, 1))
7459}
7460
7461/// Returns a $4 \times 4$ rotation matrix - yaw-pitch-roll
7462///
7463/// Calculates the product of the three rotation matrices
7464/// $R = Rz(a) Ry(b) Rx(c)$
7465///
7466/// - a (angle): Yaw
7467/// - b (angle): Pitch
7468/// - c (angle): Roll
7469/// -> matrix
7470#let transform-rotate-ypr(a, b, c) = {
7471 ((cos(a)*cos(b), cos(a)*sin(b)*sin(c) - sin(a)*cos(c), cos(a)*sin(b)*cos(c) + sin(a)*sin(c), 0),
7472 (sin(a)*cos(b), sin(a)*sin(b)*sin(c) + cos(a)*cos(c), sin(a)*sin(b)*cos(c) - cos(a)*sin(c), 0),
7473 (-sin(b), cos(b)*sin(c), cos(b)*cos(c), 1),
7474 (0,0,0,1))
7475}
7476
7477/// Returns a $4 \times 4$ rotation matrix - euler angles
7478///
7479/// Calculates the product of the three rotation matrices
7480/// $R = Rz(z) Ry(y) Rx(x)$
7481///
7482/// - x (angle): Rotation about x
7483/// - y (angle): Rotation about y
7484/// - z (angle): Rotation about z
7485/// -> matrix
7486#let transform-rotate-xyz(x, y, z) = {
7487 ((cos(y)*cos(z), sin(x)*sin(y)*cos(z) - cos(x)*sin(z), cos(x)*sin(y)*cos(z) + sin(x)*sin(z), 0),
7488 (cos(y)*sin(z), sin(x)*sin(y)*sin(z) + cos(x)*cos(z), cos(x)*sin(y)*sin(z) - sin(x)*cos(z), 0),
7489 (-sin(y), sin(x)*cos(y), cos(x)*cos(y), 0),
7490 (0,0,0,1))
7491}
7492
7493/// Multiplies matrices on top of each other.
7494/// - ..matrices (matrix): The matrices to multiply from left to right.
7495/// -> matrix
7496#let mul-mat(..matrices) = {
7497 matrices = matrices.pos()
7498 let out = matrices.remove(0)
7499 for matrix in matrices {
7500 let (m, n, p) = (
7501 ..dim(out),
7502 dim(matrix).last()
7503 )
7504 out = (
7505 for i in range(m) {
7506 (
7507 for j in range(p) {
7508 (_round(range(n).map(k => out.at(i).at(k) * matrix.at(k).at(j)).sum(), digits: precision),)
7509 }
7510 ,)
7511 }
7512 )
7513 }
7514 return out
7515}
7516
7517// Multiply 4x4 matrix with vector of size 3 or 4.
7518// The value of vec_4 defaults to w (1).
7519//
7520// The resulting vector is of dimension 3
7521/// Multiplies a $4 \times 4$ matrix with a vector of size 3 or 4. The resulting is three dimensional
7522/// - mat (matrix): The matrix to multiply
7523/// - vec (vector): The vector to multiply
7524/// - w (float): The default value for the fourth element of the vector if it is three dimensional.
7525/// -> vector
7526#let mul4x4-vec3(mat, vec, w: 1) = {
7527 assert(vec.len() <= 4)
7528 let out = (0, 0, 0)
7529 for m in range(0, 3) {
7530 let v = (mat.at(m).at(0) * vec.at(0, default: 0)
7531 + mat.at(m).at(1) * vec.at(1, default: 0)
7532 + mat.at(m).at(2) * vec.at(2, default: 0)
7533 + mat.at(m).at(3) * vec.at(3, default: w))
7534 out.at(m) = v
7535 }
7536 return out
7537}
7538
7539// Multiply matrix with vector
7540/// Multiplies an $m \times n$ matrix with an $m$th dimensional vector where $m \lte 4$. Prefer the use of `mul4x4-vec3` when possible as it does not use loops.
7541/// - mat (matrix): The matrix to multiply
7542/// - vec (vector): The vector to multiply
7543/// -> vector
7544#let mul-vec(mat, vec) = {
7545 let m = mat.len()
7546 let n = mat.at(0).len()
7547 assert(n == vec.len(), message: "Matrix columns must be equal to vector rows")
7548
7549 let new = (0,) * m
7550 for i in range(0, m) {
7551 for j in range(0, n) {
7552 new.at(i) = new.at(i) + mat.at(i).at(j) * vec.at(j)
7553 }
7554 }
7555 return new
7556}
7557
7558/// Calculates the inverse matrix of any size.
7559/// - matrix (matrix): The matrix to inverse.
7560/// -> matrix
7561#let inverse(matrix) = {
7562 let n = {
7563 let size = dim(matrix)
7564 assert.eq(size.first(), size.last(), message: "Matrix must be square to perform inversion.")
7565 size.first()
7566 }
7567
7568 let N = range(n)
7569 let inverted = ident(m: n, n: n)
7570 let p
7571 for j in N {
7572 for i in range(j, n) {
7573 if matrix.at(i).at(j) != 0 {
7574 (matrix.at(j), matrix.at(i)) = (matrix.at(i), matrix.at(j))
7575 (inverted.at(j), inverted.at(i)) = (inverted.at(i), inverted.at(j))
7576
7577 p = 1 / matrix.at(j).at(j)
7578 for k in N {
7579 matrix.at(j).at(k) *= p
7580 inverted.at(j).at(k) *= p
7581 }
7582
7583 for L in N {
7584 if L != j {
7585 p = -matrix.at(L).at(j)
7586 for k in N {
7587 matrix.at(L).at(k) += _round(p * matrix.at(j).at(k))
7588 inverted.at(L).at(k) += _round(p * inverted.at(j).at(k))
7589 }
7590 }
7591 }
7592 }
7593 }
7594 }
7595
7596 return inverted
7597}
7598
7599/// Swaps the ath column with the bth column.
7600///
7601/// - mat (matrix): Matrix
7602/// - a (int): The index of column a.
7603/// - b (int): The index of column b.
7604/// -> matrix
7605#let swap-cols(mat, a, b) = {
7606 let new = mat
7607 for m in range(mat.len()) {
7608 new.at(m).at(a) = mat.at(m).at(b)
7609 new.at(m).at(b) = mat.at(m).at(a)
7610 }
7611 return new
7612}
7613
7614/// Translates a matrix by a vector.
7615/// - mat (matrix): The matrix to translate
7616/// - vec (vector): The vector to translate by.
7617#let translate(mat, vec) = {
7618 return mul-mat(
7619 mat,
7620 transform-translate(
7621 vec.at(0),
7622 -vec.at(1),
7623 vec.at(2),
7624 ),
7625 )
7626}
7627// This file contains utility functions for path calculation
7628#import "util.typ"
7629#import "vector.typ"
7630#import "bezier.typ"
7631#import "deps.typ"
7632#import deps.oxifmt: strfmt
7633
7634#let default-samples = 25
7635
7636/// Returns the first position vector of a path segment.
7637///
7638/// - s (segment): Path segment
7639/// -> vector
7640#let segment-start(s) = {
7641 return s.at(1)
7642}
7643
7644/// Returns the last position vector of a path segment
7645///
7646/// - s (segment): Path segment
7647/// -> vector
7648#let segment-end(s) = {
7649 if s.at(0) == "line" {
7650 return s.last()
7651 }
7652 return s.at(2)
7653}
7654
7655/// Calculates the bounding points for a list of path segments
7656///
7657/// - segments (array): List of path segments
7658/// -> array
7659#let bounds(segments) = {
7660 let bounds = ()
7661
7662 for s in segments {
7663 let (kind, ..pts) = s
7664 if kind == "line" {
7665 bounds += pts
7666 } else if kind == "cubic" {
7667 bounds.push(pts.at(0))
7668 bounds.push(pts.at(1))
7669 bounds += bezier.cubic-extrema(..pts)
7670 }
7671 }
7672 return bounds
7673}
7674
7675/// Calculates the length of a single path segment
7676///
7677/// - s (array): Path segment
7678/// -> float
7679#let _segment-length(s, samples: default-samples) = {
7680 let (kind, ..pts) = s
7681 if kind == "line" {
7682 let len = 0
7683 for i in range(1, pts.len()) {
7684 len += vector.len(vector.sub(pts.at(i - 1), pts.at(i)))
7685 }
7686 return len
7687 } else if kind == "cubic" {
7688 return bezier.cubic-arclen(..pts, samples: samples)
7689 } else {
7690 panic("Invalid segment: " + kind, s)
7691 }
7692}
7693
7694/// Calculates the length of a path
7695///
7696/// - segments (array): List of path segments
7697/// -> float
7698#let length(segments) = {
7699 return segments.map(_segment-length).sum()
7700}
7701
7702/// Finds the two points that enclose a distance along a line segment.
7703///
7704/// Returns a {{dictionary}} with the following key values:
7705/// - start (int): The index of the point that is before the distance.
7706/// - end (int): The index of the point that is after the distance.
7707/// - distance (float): The distance along the line segment to the point with the `start` index.
7708/// - length (float): The distance between the found points.
7709///
7710/// ---
7711///
7712/// - pts (array): The array of points of a line segment.
7713/// - distance (float): The distance along the line segment.
7714/// -> dictionary
7715#let _points-between-distance(pts, distance) = {
7716 let travelled = 0
7717 let length = 0
7718 for i in range(1, pts.len()) {
7719 length = vector.dist(pts.at(i - 1), pts.at(i))
7720 if travelled <= distance and distance <= travelled + length {
7721 return (
7722 start: i - 1,
7723 end: i,
7724 distance: distance - travelled,
7725 length: length
7726 )
7727 }
7728 travelled += length
7729 }
7730 return (
7731 start: pts.len() - 2,
7732 end: pts.len() - 1,
7733 distance: length,
7734 length: length
7735 )
7736}
7737
7738/// Finds the point at a given distance from the start of a line segment. Distances greater than the length of the segment return the end of the line segment. Distances less than zero return the start of the segment.
7739///
7740/// - segment (array): The line segment
7741/// - distance (float): The distance along the line segment to find the point
7742/// -> vector
7743#let _point-on-line-segment(segment, distance) = {
7744 let pts = segment.slice(1)
7745
7746 let (start, end, distance, length) = _points-between-distance(pts, distance)
7747 return if length == 0 {
7748 // length can be zero if start and end are at the same position
7749 // this can occur for several reasons, user input, group has zero width or height (not both)
7750 pts.at(end)
7751 } else {
7752 vector.lerp(pts.at(start), pts.at(end), distance / length)
7753 }
7754}
7755
7756/// Finds the point at a given distance from the start of a path segment. Distances greater than the length of the segment return the end of the path segment. Distances less than zero return the start of the segment.
7757///
7758/// - segment (segment): Path segment
7759/// - distance (float): The distance along the path segment to find the point
7760/// - extrapolate (bool): If true, use linear extrapolation for distances outsides the path
7761/// -> vector
7762#let _point-on-segment(segment, distance, samples: default-samples, extrapolate: false) = {
7763 let (kind, ..pts) = segment
7764 if kind == "line" {
7765 return _point-on-line-segment(segment, distance)
7766 } else if kind == "cubic" {
7767 return bezier.cubic-point(
7768 ..pts,
7769 bezier.cubic-t-for-distance(
7770 ..pts,
7771 distance,
7772 samples: samples
7773 )
7774 )
7775 }
7776}
7777
7778/// Finds the segment that contains a point that is distance `t` from the start of a path.
7779/// - segments (array): The array of segments that make up the path.
7780/// - t (float,ratio): The distance to find the segment with. A {{float}} will be in absolute distance along the path. A {{ratio}} will be relative to the length of the path. Will panic if the distance is greater than the length of the path.
7781/// - rev (bool): When true the path will be reversed, effectively looking for the segment from the end of the path.
7782/// - samples (int): The number of samples to use when calculating the length of a cubic segment.
7783/// - clamp (bool): Clamps the distance to the length of the path, so the function won't panic.
7784/// -> dictionary
7785/// ---
7786/// Returns a {{dictionary}} with the folloing key-value pairs:
7787/// - index (int): The index of the segment in the given array of segments.
7788/// - segment (segment): The found segment.
7789/// - travelled (float): The absolute distance travelled along the path to find the segment.
7790/// - distance (float): The distance left to travel along the path.
7791/// - length (float): The length of the returned segment.
7792#let segment-at-t(segments, t, rev: false, samples: default-samples, clamp: false) = {
7793 let lengths = segments.map(_segment-length.with(samples: samples))
7794 let total = lengths.sum()
7795
7796 if type(t) == ratio {
7797 t = total * t / 100%
7798 }
7799 if not clamp {
7800 assert(t >= 0 and t <= total,
7801 message: strfmt("t is expected to be between 0 and the length of the path ({}), got: {}", total, t))
7802 }
7803
7804 if rev {
7805 segments = segments.rev()
7806 lengths = lengths.rev()
7807 }
7808 let travelled = 0
7809 for (i, segment-length) in segments.zip(lengths).enumerate() {
7810 let (segment, length) = segment-length
7811 if travelled <= t and t <= travelled + length {
7812 return (
7813 index: if rev { segments.len() - i } else { i },
7814 segment: segment,
7815 // Distance travelled
7816 travelled: travelled,
7817 // Distance left
7818 distance: t - travelled,
7819 // The length of the segment
7820 length: length
7821 )
7822 }
7823 travelled += length
7824 }
7825 return (index: if rev { 0 } else { segments.len() - 1 },
7826 segment: segments.last(),
7827 travelled: total,
7828 distance: t,
7829 length: lengths.last())
7830}
7831
7832/// Extrapolates a point from a segment. It finds the direction the end of the segment is pointing and scales the normalised vector from the end of the segment. Returns {{none}} if the segment has no direction.
7833/// - segment (segment): The segment to extrapolate from.
7834/// - distance (float): The distance to extrapolate the point to.
7835/// - rev (bool): If `true` the segment will be reversed, effectively extrapolating the point from its start.
7836/// - samples (int): This isn't used.
7837#let _extrapolated-point-on-segment(segment, distance, rev: false, samples: 100) = {
7838 let (kind, ..pts) = segment
7839 let (pt, dir) = if kind == "line" {
7840 let (a, b) = if rev {
7841 (pts.at(0), pts.at(1))
7842 } else {
7843 (pts.at(-2), pts.at(-1))
7844 }
7845 (if rev {a} else {b}, vector.sub(b, a))
7846 } else {
7847 let dir = bezier.cubic-derivative(..pts, if rev { 0 } else { 1 })
7848 if vector.len(dir) == 0 {
7849 dir = vector.sub(pts.at(1), pts.at(0))
7850 }
7851 (if rev {pts.at(0)} else {pts.at(1)}, dir)
7852 }
7853
7854 if vector.len(dir) != 0 {
7855 return vector.add(pt, vector.scale(vector.norm(dir), distance * if rev { -1 } else { 1 }))
7856 }
7857 return none
7858}
7859
7860/// Finds the position of a point a distance from the start of a path. If the path is empty {{none}} will be returned.
7861///
7862/// - segments (array): List of path segments
7863/// - t (int,float,ratio): Absolute position on the path if given an float or integer, or relative position if given a ratio from 0% to 100%. When this value is negative, the point will be found from the end of the path instaed of the start.
7864/// - extrapolate (bool): If true, use linear extrapolation if distance is outsides the path's range
7865/// -> none,vector
7866#let point-on-path(segments, t, samples: default-samples, extrapolate: false) = {
7867 assert(
7868 type(t) in (int, float, ratio),
7869 message: "Distance t must be of type int, float or ratio"
7870 )
7871 let rev = if type(t) == ratio and t < 0% or type(t) in (int, float) and t < 0 {
7872 true
7873 } else {
7874 false
7875 }
7876 if rev {
7877 t *= -1
7878 }
7879
7880 // Extrapolate at path boundaries if enabled
7881 if extrapolate {
7882 let total = length(segments)
7883 let absolute-t = if type(t) == ratio { t / 100% * total } else { t }
7884 if absolute-t > total {
7885 return _extrapolated-point-on-segment(segments.first(), absolute-t - total, rev: rev, samples: samples)
7886 }
7887 }
7888
7889 let segment = segment-at-t(segments, t, samples: samples, rev: rev)
7890 return if segment != none {
7891 let (distance, segment, length, ..) = segment
7892 _point-on-segment(segment, if rev { length - distance } else { distance }, samples: samples, extrapolate: extrapolate)
7893 }
7894}
7895
7896/// Finds the position and direction of a point a distance along from the start of a path. Returns an {{array}} of two vectors where the first is the position, and the second is the direction.
7897///
7898/// - segments (array): List of path segments
7899/// - t (int,float,ratio): Absolute position on the path if given an float or integer, or relative position if given a ratio from 0% to 100%. When this value is negative, the point will be found from the end of the path instaed of the start.
7900/// - extrapolate (bool): If true, use linear extrapolation if distance is outsides the path's range
7901/// - clamp (bool): Clamps the distance between the start and end of a path.
7902/// -> array
7903#let direction(segments, t, samples: default-samples, clamp: false) = {
7904 let (segment, distance, length, ..) = segment-at-t(segments, t, samples: samples, clamp: clamp)
7905 let (kind, ..pts) = segment
7906 return (
7907 _point-on-segment(segment, distance, samples: samples),
7908 if kind == "line" {
7909 let (start, end, distance, length) = _points-between-distance(pts, distance)
7910 vector.norm(vector.sub(segment.at(end+1), segment.at(start+1)))
7911 } else {
7912 let t = bezier.cubic-t-for-distance(..pts, distance, samples: samples)
7913 let dir = bezier.cubic-derivative(..pts, t)
7914 if vector.len(dir) == 0 {
7915 vector.norm(vector.sub(pts.at(1), pts.at(0)))
7916 } else {
7917 dir
7918 }
7919 }
7920 )
7921}
7922
7923/// Creates a line segment with points
7924///
7925/// - points (array): List of points
7926/// -> segment
7927#let line-segment(points) = {
7928 ("line",) + points
7929}
7930
7931/// Creates a cubic bezier segment
7932///
7933/// - a (vector): Start
7934/// - b (vector): End
7935/// - ctrl-a (vector): Control point a
7936/// - ctrl-b (vector): Control point b
7937/// -> segment
7938#let cubic-segment(a, b, ctrl-a, ctrl-b) = {
7939 ("cubic", a, b, ctrl-a, ctrl-b)
7940}
7941
7942/// Normalize segments by connecting gaps via straight line segments and merging multiple line segments into a single one.
7943///
7944/// - segments (array): The path segments to normalize.
7945/// -> array
7946#let normalize(segments) = {
7947 let new = ()
7948 for s in segments {
7949 if new == () {
7950 new.push(s)
7951 } else {
7952 let head = new.last()
7953 let (kind, ..pts) = s
7954
7955 if kind == "line" and head.at(0) == kind {
7956 // Merge consecutive line segments
7957 if new.last().len() > 0 and new.last().last() == pts.first() {
7958 new.last() += pts.slice(1)
7959 } else {
7960 new.last() += pts
7961 }
7962 } else if segment-start(s) != segment-end(head) {
7963 // Push a new line or line point if the current segment
7964 // does not start where the previous segment ended
7965 if head.at(0) == "line" {
7966 new.last().push(pts.first())
7967 } else {
7968 new.push(line-segment((segment-end(head), segment-start(s))))
7969 }
7970 // Push the segment
7971 new.push(s)
7972 } else {
7973 new.push(s)
7974 }
7975 }
7976 }
7977 return new
7978}
7979
7980/// Shortens a segment by a given distance.
7981/// - segment (segment): The segment to shorten.
7982/// - distance (float): The distance to move the start of the segment towards the end of the segment. If this value is negative, the end of the segment will be moved towards the start.
7983/// - snap-to (none, vector): Shortening bezier curves suffers from rounding and precision errors so a position can be given to "snap" a curve's start/end point to.
7984/// - mode (str): How cubic segments should be shortned. Can be `"LINEAR"` to use `bezier.cubic-shorten-linear` or `"CURVED"` to use `bezier.cubic-shorten`.
7985/// - samples (int): The number of samples to use when shortening a cubic segment.
7986/// -> segment
7987#let shorten-segment(segment, distance, snap-to: none, mode: "CURVED", samples: default-samples) = {
7988 let rev = distance < 0
7989 if distance >= _segment-length(segment) {
7990 return line-segment(if rev {
7991 (segment-start(segment), segment-start(segment))
7992 } else {
7993 (segment-end(segment), segment-end(segment))
7994 })
7995 }
7996
7997 let (kind, ..s) = segment
7998 if kind == "line" {
7999 if rev {
8000 distance *= -1
8001 s = s.rev()
8002 }
8003 let (start, end, distance, length) = _points-between-distance(s, distance)
8004 if length != 0 {
8005 s = (vector.lerp(s.at(start), s.at(end), distance / length),) + s.slice(end)
8006 }
8007
8008 if rev {
8009 s = s.rev()
8010 }
8011 } else {
8012 s = if mode == "LINEAR" {
8013 bezier.cubic-shorten-linear(..s, distance)
8014 } else {
8015 bezier.cubic-shorten(..s, distance, samples: samples)
8016 }
8017
8018 // Shortening beziers suffers from rounding or precision errors
8019 // so we "snap" the curve start/end to the snap-points, if provided.
8020 if snap-to != none {
8021 if rev { s.at(1) = snap-to } else { s.at(0) = snap-to }
8022 }
8023 }
8024 return (kind,) + s
8025}
8026
8027/// Shortens a path's segments by the given distances. The start of the path is shortened first by moving the point along the path towards the end. The end of the path is then shortened in the same way. When a distance is 0 no other calculations are made.
8028///
8029/// - segments (segments): The segments of the path to shorten.
8030/// - start-distance (int, float): The distance to shorten from the start of the path.
8031/// - end-distance (int, float): The distance to shorten from the end of the path
8032/// - pos (none, tuple): Tuple of points to "snap" the path ends to
8033/// -> segments Segments of the path that have been shortened
8034#let shorten-path(segments, start-distance, end-distance, snap-to: none, mode: "CURVED", samples: default-samples) = {
8035 let total = length(segments)
8036 let (snap-start, snap-end) = if snap-to == none {
8037 (none, none)
8038 } else {
8039 snap-to
8040 }
8041
8042 if start-distance > 0 {
8043 let (segment, distance, index, ..) = segment-at-t(
8044 segments,
8045 start-distance,
8046 clamp: true,
8047 )
8048 segments = segments.slice(index + 1)
8049 segments.insert(0,
8050 shorten-segment(
8051 segment,
8052 distance,
8053 mode: mode,
8054 samples: samples,
8055 snap-to: snap-start
8056 )
8057 )
8058 }
8059 if end-distance > 0 {
8060 let (segment, distance, index, ..) = segment-at-t(
8061 segments,
8062 end-distance,
8063 rev: true,
8064 clamp: true,
8065 )
8066 segments = segments.slice(0, index - 1)
8067 segments.push(
8068 shorten-segment(
8069 segment,
8070 -distance,
8071 mode: mode,
8072 samples: samples,
8073 snap-to: snap-end
8074 )
8075 )
8076 }
8077 return segments
8078}
8079#import "/src/vector.typ"
8080
8081/// Returns a list of polygon points from
8082/// a list of segments.
8083///
8084/// Cubic segments get linearized by sampling.
8085///
8086/// - segment (array): List of segments
8087/// - samples (int): Number of samples
8088/// -> array
8089#let from-segments(segments, samples: 10) = {
8090 import "/src/bezier.typ": cubic-point
8091 let poly = ()
8092 for ((kind, ..pts)) in segments {
8093 if kind == "cubic" {
8094 poly += range(0, samples).map(t => {
8095 cubic-point(..pts, t / (samples - 1))
8096 })
8097 } else {
8098 poly += pts
8099 }
8100 }
8101 return poly
8102}
8103
8104/// Computes the signed area of a 2D polygon.
8105///
8106/// The formula used is the following:
8107/// $ 1/2 \sum_{i}=0^{n-1} x_i*y_i+1 - x_i+1*y_i $
8108///
8109/// - points (array): List of Vectors of dimension >= 2
8110/// -> float
8111#let signed-area(points) = {
8112 let a = 0
8113 let n = points.len()
8114 let (cx, cy) = (0, 0)
8115 for i in range(0, n) {
8116 let (x0, y0, ..) = points.at(i)
8117 let (x1, y1, ..) = points.at(calc.rem(i + 1, n))
8118 cx += (x0 + x1) * (x0 * y1 - x1 * y0)
8119 cy += (y0 + y1) * (x0 * y1 - x1 * y0)
8120 a += x0 * y1 - x1 * y0
8121 }
8122 return .5 * a
8123}
8124
8125/// Returns the winding order of a 2D polygon
8126/// by using it's signed area.
8127///
8128/// Returns either "ccw" (counter clock-wise) or "cw" (clock-wise) or none.
8129///
8130/// - point (array): List of polygon points
8131/// -> str,none
8132#let winding-order(points) = {
8133 let area = signed-area(points)
8134 if area > 0 {
8135 "cw"
8136 } else if area < 0 {
8137 "ccw"
8138 } else {
8139 none
8140 }
8141}
8142
8143// Calculate triangle centroid
8144#let triangle-centroid(points) = {
8145 assert.eq(points.len(), 3)
8146
8147 let (mx, my, mz) = (0, 0, 0)
8148 for p in points {
8149 let (x, y, z) = p
8150 mx += x
8151 my += y
8152 mz += z
8153 }
8154 return (mx / 3, my / 3, mz / 3)
8155}
8156
8157// Calculate the centroid of a line, triangle or simple polygon
8158// Formulas:
8159// https://en.wikipedia.org/wiki/Centroid
8160#let simple-centroid(points) = {
8161 return if points.len() <= 1 {
8162 none
8163 } else if points.len() == 2 {
8164 vector.lerp(..points, .5)
8165 } else if points.len() == 3 {
8166 triangle-centroid(points)
8167 } else if points.len() >= 3 {
8168 // Skip polygons with multiple z values
8169 let z = points.first().at(2, default: 0)
8170 if points.any(p => p.at(2) != z) {
8171 return none
8172 }
8173
8174 let a = 0
8175 let n = points.len()
8176 let (cx, cy) = (0, 0)
8177 for i in range(0, n) {
8178 let (x0, y0, ..) = points.at(i)
8179 let (x1, y1, ..) = points.at(calc.rem(i + 1, n))
8180 cx += (x0 + x1) * (x0 * y1 - x1 * y0)
8181 cy += (y0 + y1) * (x0 * y1 - x1 * y0)
8182 a += x0 * y1 - x1 * y0
8183 }
8184 return (cx/(3*a), cy/(3*a), z)
8185 }
8186}
8187#import "util.typ"
8188#import "path-util.typ"
8189#import "aabb.typ"
8190#import "drawable.typ"
8191#import "vector.typ"
8192
8193
8194/// Processes an element's function to get its drawables and bounds. Returns a {{dictionary}} with the key-values: `ctx` The modified context object, `bounds` The {{aabb}} of the element's drawables, `drawables` An {{array}} of the element's {{drawable}}s.
8195///
8196/// - ctx (ctx): The current context object.
8197/// - element-func (function): A function that when passed {{ctx}}, it should return an element dictionary.
8198#let element(ctx, element-func) = {
8199 let bounds = none
8200 let element
8201 let anchors = (:)
8202
8203 (ctx, ..element,) = element-func(ctx)
8204 if "drawables" in element {
8205 if type(element.drawables) == dictionary {
8206 element.drawables = (element.drawables,)
8207 }
8208 for drawable in element.drawables {
8209 if drawable.bounds {
8210 bounds = aabb.aabb(
8211 if drawable.type == "path" {
8212 path-util.bounds(drawable.segments)
8213 } else if drawable.type == "content" {
8214 let (x, y, _, w, h,) = drawable.pos + (drawable.width, drawable.height)
8215 ((x + w / 2, y - h / 2, 0), (x - w / 2, y + h / 2, 0))
8216 },
8217 init: bounds
8218 )
8219 }
8220 }
8221 }
8222
8223 let name = element.at("name", default: none)
8224 if name != none {
8225 assert.eq(type(name), str,
8226 message: "Element name must be a string")
8227 assert(not name.contains("."),
8228 message: "Invalid name for element '" + element.name + "'; name must not contain '.'")
8229
8230 if "anchors" in element {
8231 ctx.nodes.insert(name, element)
8232 if ctx.groups.len() > 0 {
8233 ctx.groups.last().push(name)
8234 }
8235 }
8236 }
8237
8238 if ctx.debug and bounds != none {
8239 element.drawables.push(drawable.path(
8240 path-util.line-segment((
8241 bounds.low,
8242 (bounds.high.at(0), bounds.low.at(1), 0),
8243 bounds.high,
8244 (bounds.low.at(0), bounds.high.at(1), 0)
8245 )),
8246 stroke: red,
8247 close: true
8248 ))
8249 }
8250
8251 return (
8252 ctx: ctx,
8253 bounds: bounds,
8254 drawables: element.at("drawables", default: ()),
8255 )
8256}
8257
8258/// Runs the `element` function for a list of element functions and aggregates the results.
8259/// - ctx (ctx): The current context object.
8260/// - body (array): The array of element functions to process.
8261/// -> dictionary
8262#let many(ctx, body) = {
8263 let drawables = ()
8264 let bounds = none
8265
8266 for el in body {
8267 let r = element(ctx, el)
8268 if r != none {
8269 if r.bounds != none {
8270 bounds = aabb.aabb(r.bounds, init: bounds)
8271 }
8272 ctx = r.ctx
8273 drawables += r.drawables
8274 }
8275 }
8276 return (ctx: ctx, bounds: bounds, drawables: drawables)
8277}
8278#import "/src/vector.typ"
8279#import "/src/util.typ"
8280
8281/// Sort list of points by distance to a
8282/// reference point.
8283///
8284/// - points (array): List of points to sort
8285/// - reference (vec): Reference point
8286/// -> List of points
8287#let points-by-distance(ctx, points, reference: (0, 0, 0)) = {
8288 let reference = util.apply-transform(ctx.transform, reference)
8289 return points.sorted(key: pt => {
8290 vector.dist(pt, reference)
8291 })
8292}
8293
8294/// Sort list of 2D points by angle to a
8295/// reference 2D point in CCW order.
8296/// Z component is ignored.
8297///
8298/// - points (array): List of points to sort
8299/// - reference (vec): Reference point
8300/// -> List of points
8301#let points-by-angle(ctx, points, reference: (0, 0, 0)) = {
8302 let (rx, ry, ..) = util.apply-transform(ctx.transform, reference)
8303 return points.sorted(key: ((px, py, ..)) => {
8304 360deg - calc.atan2(rx - px, ry - py)
8305 })
8306}
8307#import "util.typ"
8308
8309#let default = (
8310 fill: none,
8311 fill-rule: "non-zero",
8312 stroke: black + 1pt,
8313 radius: 1,
8314 /// Bezier shortening mode:
8315 /// - "LINEAR" Moving the affected point and it's next control point (like TikZ "quick" key)
8316 /// - "CURVED" Preserving the bezier curve by calculating new control points
8317 shorten: "LINEAR",
8318
8319 // Allowed values:
8320 // - none
8321 // - Number
8322 // - Array: (y, x), (top, y, bottom), (top, right, bottom, left)
8323 // - Dictionary: (top:, right:, bottom:, left:)
8324 padding: none,
8325 mark: (
8326 scale: 1, // A factor that is applied to length, width, and inset.
8327 length: .2cm, // The size of the mark along its direction
8328 width: 0.15cm, // The size of the mark along the normal of its direction
8329 inset: .05cm, // The inner length of some mark shapes, like triangles and brackets
8330 sep: .1cm, // The distance between multiple marks along their path
8331 pos: none, // Position override on the path (none, number or path-length ratio)
8332 offset: 0, // Mark extra offset (number or path-length ratio)
8333 start: none, // Mark start symbol(s)
8334 end: none, // Mark end symbol(s)
8335 symbol: none, // Mark symbol
8336 xy-up: (0, 0, 1), // Up vector for 2D marks
8337 z-up: (0, 1, 0), // Up vector for 3D marks
8338 stroke: auto,
8339 fill: auto,
8340 slant: none, // Slant factor - 0%: no slant, 100%: 45 degree slant
8341 harpoon: false,
8342 flip: false,
8343 reverse: false,
8344 /// If false, the mark points in the direction of the paths start/end direction.
8345 /// Curved paths get shortened linearly.
8346 flex: true,
8347 /// Max. number of samples to use for calculating curve positions
8348 /// a higher number gives better results but may slow down compilation.
8349 position-samples: 30,
8350 /// Index of the mark the path should get shortened to, or auto
8351 /// to shorten to the last mark. To apply different values per side,
8352 /// set the default to `0` and to `auto` for the mark you want to
8353 /// shorten the path to. Set to `none` to disable path shortening.
8354 shorten-to: auto,
8355 /// Apply shape transforms for marks. This is not honored per mark, but
8356 /// for all marks on a path. If set to false, marks get placed after the
8357 /// shape they are placed on got transformed.
8358 transform-shape: true,
8359 /// Mark anchor used for placement
8360 /// Possible values are:
8361 /// - "tip"
8362 /// - "center"
8363 /// - "base"
8364 anchor: "tip",
8365 ),
8366 circle: (
8367 radius: auto,
8368 stroke: auto,
8369 fill: auto
8370 ),
8371 group: (
8372 padding: auto,
8373 fill: auto,
8374 stroke: auto
8375 ),
8376 line: (
8377 mark: auto,
8378 fill: auto,
8379 fill-rule: auto,
8380 stroke: auto,
8381 ),
8382 bezier: (
8383 stroke: auto,
8384 fill: auto,
8385 fill-rule: auto,
8386 mark: auto,
8387 shorten: auto,
8388 ),
8389 catmull: (
8390 tension: .5,
8391 mark: auto,
8392 shorten: auto,
8393 stroke: auto,
8394 fill: auto,
8395 fill-rule: auto,
8396 ),
8397 hobby: (
8398 /// Curve start and end omega (curlyness)
8399 omega: (0,0),
8400 mark: auto,
8401 shorten: auto,
8402 stroke: auto,
8403 fill: auto,
8404 fill-rule: auto,
8405 ),
8406 rect: (
8407 /// Rect corner radius that supports the following types:
8408 /// - <radius>: Same x and y radius for all corners
8409 /// - (west: <radius>, east: <radius>, north: <radius>, south: <radius>,
8410 /// north-west: <radius>, north-east: <radius>, south-west: <radius>, south-east: <radius>,
8411 /// rest: <radius: 0>)
8412 ///
8413 /// A radius can be either a number, a ratio or a tuple of numbers or ratios.
8414 /// Ratios represent a value relative to the rects height or width.
8415 /// E.g. the radius `50%` is equal to `(50%, 50%)` and represents a x and y radius
8416 /// of 50% of the rects width/height.
8417 radius: 0,
8418 stroke: auto,
8419 fill: auto,
8420 ),
8421 arc: (
8422 // Supported values:
8423 // - "OPEN"
8424 // - "CLOSE"
8425 // - "PIE"
8426 mode: "OPEN",
8427 update-position: true,
8428 mark: auto,
8429 stroke: auto,
8430 fill: auto,
8431 radius: auto
8432 ),
8433 polygon: (
8434 radius: auto,
8435 stroke: auto,
8436 fill: auto,
8437 ),
8438 content: (
8439 padding: auto,
8440 // Supported values
8441 // - none
8442 // - "rect"
8443 // - "circle"
8444 frame: none,
8445 fill: auto,
8446 stroke: auto,
8447 // Apply canvas scaling to content
8448 auto-scale: false,
8449 ),
8450)
8451
8452/// You can use this to combine the style in `ctx`, the style given by a user for a single element and an element's default style.
8453///
8454/// `base` is first merged onto `dict` without overwriting existing values, and if `root` is given it is merged onto that key of `dict`. `merge` is then merged onto `dict` but does overwrite existing entries, if `root` is given it is merged onto that key of `dict`. Then entries in `dict` that are {{auto}} inherit values from their nearest ancestor and entries of type {{dictionary}} are merged with their closest ancestor.
8455/// ```typ example
8456/// #let dict = (
8457/// stroke: "black",
8458/// fill: none,
8459/// mark: (stroke: auto, fill: "blue"),
8460/// line: (stroke: auto, mark: auto, fill: "red")
8461/// )
8462/// #cetz.styles.resolve(dict, merge: (mark: (stroke: "yellow")), root: "line")
8463/// ```
8464/// The following is a more detailed explanation of how the algorithm works to use as a reference if needed. It should be updated whenever changes are made.
8465/// Remember that dictionaries are recursively merged, if an entry is any other type it is simply updated. (dict + dict = merged dict, value + dict = dict, dict + value = value)
8466/// First if `base` is given, it will be merged without overwriting values onto `dict`. If `root` is given it will be merged onto that key of `dict`.
8467/// Each level of `dict` is then processed with these steps. If `root` is given the level with that key will be the first, otherwise the whole of `dict` is processed.
8468/// + Values on the corresponding level of `merge` are inserted into the level if the key does not exist on the level or if they are not both dictionaries. If they are both dictionaries their values will be inserted in the same stage at a lower level.
8469/// + If an entry is `auto` or a dictionary, the tree is travelled back up until an entry with the same key is found. If the current entry is `auto` the value of the ancestor's entry is copied. Or if the current entry and ancestor entry is a dictionary, they are merged with the current entry overwriting any values in it's ancestors.
8470/// + Each entry that is a dictionary is then resolved from step 1.
8471///
8472/// ```typc example
8473/// get-ctx(ctx => {
8474/// // Get the current "mark" style
8475/// content((0,0), [#cetz.styles.resolve(ctx.style, root: "mark")])
8476/// })
8477/// ```
8478///
8479/// - dict (style): Current context style from `ctx.style`.
8480/// - merge (style): Style values overwriting the current style. I.e. inline styles passed with an element: `line(.., stroke: red)`.
8481/// - root (none, str): Style root element name.
8482/// - base (none, style): Style values to merge into `dict` without overwriting it.
8483/// -> style
8484#let resolve(dict, root: none, merge: (:), base: (:)) = {
8485 let resolve(dict, ancestors, merge) = {
8486 // Merge. If both values are dictionaries, merge's values will be inserted at a lower level in this step.
8487 for (k, v) in merge {
8488 if k not in dict or not (type(v) == dictionary and type(dict.at(k)) == dictionary) {
8489 dict.insert(k, v)
8490 }
8491 }
8492
8493 // For each entry that is a dictionary or `auto`, travel back up the tree until it finds an entry with the same key.
8494 for (k, v) in dict {
8495 let is-dict = type(v) == dictionary
8496 if is-dict or v == auto {
8497 for ancestor in ancestors {
8498 if k in ancestor {
8499 // If v is auto and the ancestor's value is not auto, update v.
8500 if ancestor.at(k) != auto and v == auto {
8501 v = ancestor.at(k)
8502 // If both values are dictionaries, merge them. Values in v overwrite its ancestor's value.
8503 } else if is-dict and type(ancestor.at(k)) == dictionary {
8504 v = util.merge-dictionary(ancestor.at(k), v)
8505 }
8506 // Retain the updated value. Because all of the ancestors have already been processed even if a v is still auto that just means the key at the highest level either is auto or doesn't exist.
8507 dict.insert(k, v)
8508 break
8509 }
8510 }
8511 }
8512 }
8513
8514 // Record history here so it doesn't change.
8515 ancestors = (dict,) + ancestors
8516 // Because only keys on this level have been processed, process all children of this level.
8517 for (k, v) in dict {
8518 if type(v) == dictionary {
8519 dict.insert(k, resolve(v, ancestors, merge.at(k, default: (:))))
8520 }
8521 }
8522 return dict
8523 }
8524
8525 if base != (:) {
8526 if root != none {
8527 let a = (:)
8528 a.insert(root, base)
8529 base = a
8530 }
8531 dict = util.merge-dictionary(dict, base, overwrite: false)
8532 }
8533 return resolve(
8534 if root != none { dict.at(root) } else { dict },
8535 if root != none {(dict,)} else {()},
8536 merge
8537 )
8538}
8539#import "deps.typ"
8540#import deps.oxifmt: strfmt
8541
8542#import "matrix.typ"
8543#import "vector.typ"
8544#import "bezier.typ"
8545
8546/// Constant to be used as float rounding error
8547#let float-epsilon = 0.000001
8548
8549/// Multiplies vectors by a transformation matrix. If multiple vectors are given they are returned as an array, if only one vector is given only one will be returned, if a dictionary is given they will be returned in the dictionary with the same keys.
8550///
8551/// - transform (matrix,function): The $4 \times 4$ transformation matrix or a function that accepts and returns a vector.
8552/// - ..vecs (vector): Vectors to get transformed. Only the positional part of the sink is used. A dictionary of vectors can also be passed and all will be transformed.
8553/// -> vector,array,dictionary
8554#let apply-transform(transform, ..vecs) = {
8555 let t = if type(transform) != function {
8556 matrix.mul4x4-vec3.with(transform)
8557 } else {
8558 transform
8559 }
8560 if type(vecs.pos().first()) == dictionary {
8561 vecs = vecs.pos().first()
8562 for (k, vec) in vecs {
8563 vecs.insert(k, t(vec))
8564 }
8565 } else {
8566 vecs = vecs.pos().map(t)
8567 if vecs.len() == 1 {
8568 return vecs.first()
8569 }
8570 }
8571 return vecs
8572}
8573
8574/// Reverts the transform of the given vector
8575///
8576/// - transform (matrix): Transformation matrix
8577/// - vec (vector): Vector to be transformed
8578/// -> vector
8579#let revert-transform(transform, ..vecs) = {
8580 apply-transform(matrix.inverse(transform), ..vecs)
8581}
8582
8583/// Linearly interpolates between two points and returns its position
8584///
8585/// - a (vector): Start point
8586/// - b (vector): End point
8587/// - t (float): Position on the line $[0, 1]$
8588/// -> vector
8589#let line-pt(a, b, t) = {
8590 return vector.add(a, vector.scale(vector.sub(b, a), t))
8591}
8592
8593/// Get orthogonal vector to line
8594///
8595/// - a (vector): Start point
8596/// - b (vector): End point
8597/// -> vector
8598#let line-normal(a, b) = {
8599 let v = vector.norm(vector.sub(b, a))
8600 return (0 - v.at(1), v.at(0), v.at(2, default: 0))
8601}
8602
8603/// Calculates the arc-length of a circle or arc
8604///
8605/// - radius (float): Circle or arc radius
8606/// - angle (angle): The angle of the arc.
8607/// -> float
8608#let circle-arclen(radius, angle: 360deg) = {
8609 calc.abs(angle / 360deg * 2 * calc.pi)
8610}
8611
8612/// Get point on an ellipse for an angle
8613///
8614/// - center (vector): Center
8615/// - radius (float,array): Radius or tuple of x/y radii
8616/// - angled (angle): Angle to get the point at
8617/// -> vector
8618#let ellipse-point(center, radius, angle) = {
8619 let (rx, ry) = if type(radius) == array {
8620 radius
8621 } else {
8622 (radius, radius)
8623 }
8624
8625 let (x, y, z) = center
8626 return (calc.cos(angle) * rx + x, calc.sin(angle) * ry + y, z)
8627}
8628
8629/// Calculates the center of a circle from 3 points. The z coordinate is taken from point a.
8630///
8631/// - a (vector): Point 1
8632/// - b (vector): Point 2
8633/// - c (vector): Point 3
8634/// -> vector
8635#let calculate-circle-center-3pt(a, b, c) = {
8636 let m-ab = line-pt(a, b, .5)
8637 let m-bc = line-pt(b, c, .5)
8638 let m-cd = line-pt(c, a, .5)
8639
8640 let args = () // a, c, b, d
8641 for i in range(0, 3) {
8642 let (p1, p2) = ((a,b,c).at(calc.rem(i,3)),
8643 (b,c,a).at(calc.rem(i,3)))
8644 let m = line-pt(p1, p2, .5)
8645 let n = line-normal(p1, p2)
8646
8647 // Find a line with a non upwards normal
8648 if n.at(0) == 0 { continue }
8649
8650 let la = n.at(1) / n.at(0)
8651 args.push(la)
8652 args.push(m.at(1) - la * m.at(0))
8653
8654 // We need only 2 lines
8655 if args.len() == 4 { break }
8656 }
8657
8658 // Find intersection point of two 2d lines
8659 // L1: a*x + c
8660 // L2: b*x + d
8661 let line-intersection-2d(a, c, b, d) = {
8662 if a - b == 0 {
8663 if c == d {
8664 return (0, c, 0)
8665 }
8666 return none
8667 }
8668 let x = (d - c)/(a - b)
8669 let y = a * x + c
8670 return (x, y)
8671 }
8672
8673 assert(args.len() == 4, message: "Could not find circle center")
8674 return vector.as-vec(line-intersection-2d(..args), init: (0, 0, a.at(2)))
8675}
8676
8677/// Converts a {{number}} to "canvas units"
8678/// - ctx (context): The current context object.
8679/// - num (number): The number to resolve.
8680/// -> float
8681#let resolve-number(ctx, num) = {
8682 return if type(num) == length {
8683 float(num.to-absolute() / ctx.length)
8684 } else if type(num) == ratio {
8685 num
8686 } else {
8687 float(num)
8688 }
8689}
8690
8691/// Ensures that a radius has an `x` and `y` component.
8692/// - radius (number, array):
8693/// -> array
8694#let resolve-radius(radius) = {
8695 return if type(radius) == array {radius} else {(radius, radius)}
8696}
8697
8698/// Finds the minimum of a set of values while ignoring {{none}} values.
8699/// - a (float,none):
8700/// -> float
8701#let min(..a) = {
8702 let a = a.pos().filter(v => v != none)
8703 return calc.min(..a)
8704}
8705
8706/// Finds the maximum of a set of values while ignoring {{none}} values.
8707/// - ..a (float,none):
8708/// -> float
8709#let max(..a) = {
8710 let a = a.pos().filter(v => v != none)
8711 return calc.max(..a)
8712}
8713
8714/// Merges dictionary `b` onto dictionary `a`. If a key does not exist in `a` but does in `b`, it is inserted into `a` with `b`'s value. If a key does exist in `a` and `b`, the value in `b` is only inserted into `a` if the `overwrite` argument is `true`. If a key does exist both in `a` and `b` and both values are of type {{dictionary}} they will be recursively merged with this same function.
8715///
8716/// - a (dictionary): Dictionary a
8717/// - b (dictionary): Dictionary b
8718/// - overwrite (bool): Whether to override an entry in `a` that also exists in `b` with the value in `b`.
8719/// -> dictionary
8720#let merge-dictionary(a, b, overwrite: true) = {
8721 for (k, v) in b {
8722 if type(a) == dictionary and k in a and type(v) == dictionary and type(a.at(k)) == dictionary {
8723 a.insert(k, merge-dictionary(a.at(k), v, overwrite: overwrite))
8724 } else if overwrite or k not in a {
8725 a.insert(k, v)
8726 }
8727 }
8728 return a
8729}
8730
8731/// Measures the size of some {{content}} in canvas coordinates.
8732/// - ctx (context): The current context object.
8733/// - cnt (content): The content to measure.
8734/// -> vector
8735#let measure(ctx, cnt) = {
8736 let size = std.measure(cnt)
8737 return (
8738 calc.abs(size.width / ctx.length),
8739 calc.abs(size.height / ctx.length)
8740 )
8741}
8742
8743/// Get a padding/margin dictionary with keys `(top, left, bottom, right)` from a padding value.
8744///
8745///
8746/// Type of `padding`:
8747/// - {{none}}: All sides padded by 0
8748/// - {{number}}: All sides are padded by the same value
8749/// - {{array}}: CSS like padding: `(y, x)`, `(top, x, bottom)` or `(top, right, bottom, left)`
8750/// - {{dictionary}}: Converts a Typst padding dictionary (top, left, bottom, right, x, y, rest) to a dictionary containing top, left, bottom and right.
8751///
8752/// - padding (none, number, array, dictionary): Padding specification
8753///
8754/// -> dictionary
8755#let as-padding-dict(padding) = {
8756 if padding == none {
8757 padding = 0
8758 }
8759
8760 if type(padding) == array {
8761 // Allow CSS like padding array
8762 assert(padding.len() in (2, 3, 4),
8763 message: "Padding array formats are: (y, x), (top, x, bottom), (top, right, bottom, left)")
8764 if padding.len() == 2 {
8765 let (y, x) = padding
8766 return (top: y, right: x, bottom: y, left: x)
8767 } else if padding.len() == 3 {
8768 let (top, x, bottom) = padding
8769 return (top: top, right: x, bottom: bottom, left: x)
8770 } else if padding.len() == 4 {
8771 let (top, right, bottom, left) = padding
8772 return (top: top, right: right, bottom: bottom, left: left)
8773 }
8774 } else if type(padding) == dictionary {
8775 // Support typst padding dictionary
8776 let rest = padding.at("rest", default: 0)
8777 let x = padding.at("x", default: rest)
8778 let y = padding.at("y", default: rest)
8779 if not "left" in padding { padding.left = x }
8780 if not "right" in padding { padding.right = x }
8781 if not "top" in padding { padding.top = y }
8782 if not "bottom" in padding { padding.bottom = y }
8783
8784 return padding
8785 } else {
8786 return (top: padding, left: padding, bottom: padding, right: padding)
8787 }
8788}
8789
8790/// Creates a corner-radius dictionary with keys `north-east`, `north-west`, `south-east` and `south-west` with values of a two element {{array}} of the radius in the `x` and `y` direction. Returns none if all radii are zero or none.
8791///
8792/// - ctx (context): The current canvas context object
8793/// - radii (none, number, dictionary): The radius specification. A {{number}} will cause all corners to have the same radius. An {{array}} with two items will cause all corners to have the same rx and ry radius. A {{dictionary}} can be given where the key specifies the corner and the value specifies the radius. The value can be either {{number}} for a circle radius or {{array}} for an x and y radius. The keys `north`, `south`, `east` and `west` targets both corners in that cardinal direction e.g. `south` sets the south west and south east corners. The keys `north-east`, `north-west`, `south-east` and `south-west` targets the corresponding corner. The key `rest` targets all other corners that have not been target by other keys.
8794/// - size (???): I'm not sure what this does.
8795///
8796/// -> dictionary
8797#let as-corner-radius-dict(ctx, radii, size) = {
8798 if radii == none or radii == 0 {
8799 return (north-west: (0,0), north-east: (0,0),
8800 south-west: (0,0), south-east: (0,0))
8801 }
8802
8803 let radii = (if type(radii) == dictionary {
8804 let rest = radii.at("rest", default: (0,0))
8805 let north = radii.at("north", default: auto)
8806 let south = radii.at("south", default: auto)
8807 let west = radii.at("west", default: auto)
8808 let east = radii.at("east", default: auto)
8809
8810 if north != auto or south != auto {
8811 assert(west == auto and east == auto,
8812 message: "Corner radius north/south and west/east are mutually exclusive! Use per corner radii: north-west, .. instead.")
8813 }
8814 if west != auto or east != auto {
8815 assert(north == auto and south == auto,
8816 message: "Corner radius north/south and west/east are mutually exclusive! Use per corner radii: north-west, .. instead.")
8817 }
8818
8819 let north-east = if north != auto { north } else if east != auto { east } else {rest}
8820 let north-west = if north != auto { north } else if west != auto { west } else {rest}
8821 let south-east = if south != auto { south } else if east != auto { east } else {rest}
8822 let south-west = if south != auto { south } else if west != auto { west } else {rest}
8823
8824 (radii.at("north-west", default: north-west),
8825 radii.at("north-east", default: north-east),
8826 radii.at("south-west", default: south-west),
8827 radii.at("south-east", default: south-east))
8828 } else if type(radii) == array {
8829 panic("Invalid corner radius type: " + type(radii))
8830 } else {
8831 (radii, radii, radii, radii)
8832 }).map(v => if type(v) != array { (v, v) } else { v })
8833
8834 // Resolve lengths to floats
8835 radii = radii.map(t => t.map(resolve-number.with(ctx)))
8836
8837 // Clamp radii to half the size
8838 radii = radii.map(t => t.enumerate().map(((i, v)) => {
8839 calc.max(0, calc.min(if type(v) == ratio {
8840 v * size.at(i) / 100%
8841 } else { v }, size.at(i) / 2))
8842 }))
8843
8844 let (nw, ne, sw, se) = radii
8845 return (
8846 north-west: nw,
8847 north-east: ne,
8848 south-west: sw,
8849 south-east: se,
8850 )
8851}
8852
8853/// Sorts an array of vectors by distance to a common position.
8854/// - base (vector): The position to measure the distance of the other vectors from.
8855/// - pts (array): The array of vectors to sort.
8856/// -> array
8857#let sort-points-by-distance(base, pts) = {
8858 if pts.len() == 1 {
8859 return pts
8860 }
8861
8862 // Sort by transforming points into tuples of (point, distance),
8863 // sorting them by key 1 and then transforming them back to points.
8864 return pts.map(p => {
8865 return (p, vector.dist(p, base))
8866 })
8867 .sorted(key: t => t.at(1))
8868 .map(t => t.at(0))
8869}
8870
8871/// Resolves a stroke into a usable dictionary with all fields that are missing or auto set to their Typst defaults.
8872/// - stroke (none, stroke): The stroke to resolve.
8873/// -> dictionary
8874#let resolve-stroke(stroke) = {
8875 if stroke == none {
8876 return (paint: none, thickness: 0pt, join: none, cap: none, miter-limit: 4)
8877 }
8878
8879 let default = (
8880 paint: black,
8881 thickness: 1pt,
8882 join: "miter",
8883 cap: "butt",
8884 miter-limit: 4
8885 )
8886 let s = line(stroke: stroke).stroke
8887 let stroke = (:)
8888 for (k, v) in (paint: s.paint, thickness: s.thickness, join: s.join, cap: s.cap, miter-limit: s.miter-limit) {
8889 if v == auto {
8890 stroke.insert(k, default.at(k))
8891 } else {
8892 stroke.insert(k, v)
8893 }
8894 }
8895 return stroke
8896}
8897
8898/// Asserts whether a "body" has the correct type.
8899#let assert-body(body) = {
8900 assert(body == none or type(body) in (array, function),
8901 message: "Body must be of type none, array or function")
8902}
8903
8904// Returns body if of type array, an
8905// empty array if body is none or
8906// the result of body called with ctx if of type
8907// function. A function result of none will return
8908// an empty array.
8909#let resolve-body(ctx, body) = {
8910 if type(body) == function {
8911 body = body(ctx)
8912 }
8913 if body == none {
8914 body = ()
8915 }
8916 return body
8917}
8918
8919
8920#let str-to-number-regex = regex("^(-?\d*\.?\d+)(cm|mm|pt|em|in|%|deg|rad)?$")
8921#let number-units = (
8922 "%": 1%,
8923 "cm": 1cm,
8924 "mm": 1mm,
8925 "pt": 1pt,
8926 "em": 1em,
8927 "in": 1in,
8928 "deg": 1deg,
8929 "rad": 1rad
8930)
8931#let str-is-number(string) = string.match(str-to-number-regex) != none
8932#let str-to-number(string) = {
8933 let (num, unit) = string.match(str-to-number-regex).captures
8934 num = float(num)
8935 if unit != none and unit in number-units {
8936 num *= number-units.at(unit)
8937 }
8938 return num
8939}
8940/// Returns a new vector of dimension `dim` with all fields set to `init` (defaults to 0).
8941///
8942/// - dim (int): Vector dimension
8943/// - init (float): Initial value of all fields
8944/// -> vector
8945#let new(dim, init: 0) = {
8946 return range(0, dim).map(x => init)
8947}
8948
8949/// Returns the dimension of a vector.
8950///
8951/// - v (vector): The vector to find the dimension of.
8952/// -> int
8953#let dim(v) = {
8954 assert(
8955 type(v) == array,
8956 message: "Expected vector to be of array type, got: " + repr(v)
8957 )
8958 return v.len()
8959}
8960
8961
8962/// Converts a vector to a row or column matrix.
8963///
8964/// - v (vector): The vector to convert.
8965/// - mode (str): The type of matrix to convert into. Must be one of `"row"` or `"column"`.
8966/// -> matrix
8967#let as-mat(v, mode: "row") = {
8968 if mode == "column" {
8969 return (v,)
8970 } else if mode == "row" {
8971 return (for c in v { (c,) }, )
8972 } else {
8973 panic("Invalid mode " + mode)
8974 }
8975}
8976
8977/// Ensures a vector has an exact dimension. This is done by passing another vector `init` that has the required dimension. If the original vector does not have enough dimensions, the values from `init` will be inserted. It is recommended to use a zero vector for `init`.
8978///
8979/// - v (vector): The vector to ensure.
8980/// - init (vector): The vector to check the dimension against.
8981/// -> vector
8982#let as-vec(v, init: (0, 0, 0)) = {
8983 for i in range(0, calc.min(dim(v), dim(init))) {
8984 init.at(i) = v.at(i)
8985 }
8986 return init
8987}
8988
8989
8990/// Return length/magnitude of a vector.
8991///
8992/// - v (vector): The vector to find the magnitude of.
8993/// -> float
8994#let len(v) = {
8995 return calc.sqrt(v.fold(0, (s, c) => s + c * c))
8996}
8997
8998/// Adds two vectors of the same dimension
8999///
9000/// - v1 (vector): The vector on the left hand side.
9001/// - v2 (vector): The vector on the right hand side.
9002/// -> vector
9003#let add(v1, v2) = {
9004 if dim(v1) != dim(v2) {
9005 v1 = as-vec(v1)
9006 v2 = as-vec(v2)
9007 }
9008 assert(dim(v1) == dim(v2), message: "Cannot add vectors, " + repr(v1) + " and " + repr(v2) + " are not of the same dimensions.")
9009 return v1.zip(v2).map(((a, b)) => a + b)
9010}
9011
9012/// Subtracts two vectors of the same dimension
9013///
9014/// - v1 (vector): The vector on the left hand side.
9015/// - v2 (vector): The vector on the right hand side.
9016/// -> vector
9017#let sub(v1, v2) = {
9018 if dim(v1) != dim(v2) {
9019 v1 = as-vec(v1)
9020 v2 = as-vec(v2)
9021 }
9022 assert(dim(v1) == dim(v2), message: "Cannot subtract vectors, " + repr(v1) + " and " + repr(v2) + " are not of the same dimensions.")
9023 return v1.zip(v2).map(((a, b)) => a - b)
9024}
9025
9026/// Calculates the distance between two vectors by subtracting the length of vector `a` from vector `b`.
9027///
9028/// - a (vector): Vector a
9029/// - b (vector): Vector b
9030/// -> float
9031#let dist(a, b) = len(sub(b, a))
9032
9033/// Multiplys a vector with scalar `x`
9034/// - v (vector): The vector to scale.
9035/// - x (float): The scale factor.
9036/// -> vector
9037#let scale(v, x) = v.map(s => s * x)
9038
9039/// Divides a vector by scalar `x`
9040/// - v (vector): The vector to be divded.
9041/// - x (float): The inverse scale factor.
9042#let div(v, x) = v.map(s => s / x)
9043
9044/// Negates each value in a vector
9045/// - v (vector): The vector to negate.
9046/// -> vector
9047#let neg(v) = scale(v, -1)
9048
9049/// Normalizes a vector (divide by its length)
9050/// - v (vector): The vector to normalize.
9051/// -> vector
9052#let norm(v) = div(v, len(v))
9053
9054/// Multiply two vectors component-wise
9055/// - a (vector): First vector.
9056/// - b (vector): Second vector.
9057#let element-product(a, b) = a.enumerate().map(((i, v)) => v * b.at(i))
9058
9059/// Calculates the dot product between two vectors.
9060/// - v1 (vector): The vector on the left hand side.
9061/// - v2 (vector): The vector on the right hand side.
9062/// -> float
9063#let dot(v1, v2) = {
9064 assert(dim(v1) == dim(v2))
9065 return v1.enumerate().fold(0, (s, t) => s + t.at(1) * v2.at(t.at(0)))
9066}
9067
9068/// Calculates the cross product of two vectors with a dimension of three.
9069/// - v1 (vector): The vector on the left hand side.
9070/// - v2 (vector): The vector on the right hand side.
9071/// -> vector
9072#let cross(v1, v2) = {
9073 assert(dim(v1) == 3 and dim(v2) == 3)
9074 let x = v1.at(1) * v2.at(2) - v1.at(2) * v2.at(1)
9075 let y = v1.at(2) * v2.at(0) - v1.at(0) * v2.at(2)
9076 let z = v1.at(0) * v2.at(1) - v1.at(1) * v2.at(0)
9077 return (x, y, z)
9078}
9079
9080/// Calculates the angle between two vectors and the x-axis in 2d space
9081/// - a (vector): The vector to measure the angle from.
9082/// - b (vector): The vector to measure the angle to.
9083/// -> angle
9084#let angle2(a, b) = {
9085 // Typst's atan2 is (x, y) order, not (y, x)
9086 return calc.atan2(b.at(0) - a.at(0), b.at(1) - a.at(1))
9087}
9088
9089/// Calculates the angle between three vectors
9090/// - v1 (vector): The vector to measure the angle from.
9091/// - c (vector): The vector to measure the angle at.
9092/// - v2 (vector): The vector to measure the angle to.
9093#let angle(v1, c, v2) = {
9094 assert(dim(v1) == dim(v2), message: "Vectors " + repr(v1) + " and " + repr(v2) + " do not have the same dimensions.")
9095 if dim(v1) == 2 or dim(v1) == 3 {
9096 v1 = sub(v1, c)
9097 v2 = sub(v2, c)
9098 return calc.acos(dot(norm(v1), norm(v2)))
9099 } else {
9100 panic("Invalid vector dimension")
9101 }
9102}
9103
9104/// Linear interpolation between two vectors.
9105/// - v1 (vector): The vector to interpolate from.
9106/// - v2 (vector): The vector to interpolate to.
9107/// - t (float): The factor to interpolate by. A value of `0` is `v1` and a value of `1` is `v2`.
9108#let lerp(v1, v2, t) = {
9109 return add(
9110 v1,
9111 scale(
9112 sub(
9113 v2,
9114 v1
9115 ),
9116 t,
9117 )
9118 )
9119}
9120
9121/// Rotates a vector of dimension 2 or 3 around the z-axis by an angle.
9122/// - v (vector): The vector to rotate.
9123/// - angle (angle): The angle to rotate by.
9124/// -> vector
9125#let rotate-z(v, angle) = {
9126 assert(v.len() >= 2,
9127 message: "Vector size must be >= 2")
9128 let (x, y, ..) = v
9129 v.at(0) = x * calc.cos(angle) - y * calc.sin(angle)
9130 v.at(1) = x * calc.sin(angle) + y * calc.cos(angle)
9131 return v
9132}
9133#let version = version(0,3,2)
9134[package]
9135name = "cetz"
9136version = "0.3.2"
9137compiler = "0.12.0"
9138repository = "https://github.com/cetz-package/cetz"
9139homepage = "https://cetz-package.github.io/"
9140entrypoint = "src/lib.typ"
9141authors = [
9142 "Johannes Wolf <https://github.com/johannes-wolf>",
9143 "fenjalien <https://github.com/fenjalien>"
9144]
9145categories = [ "visualization" ]
9146license = "LGPL-3.0-or-later"
9147description = "Drawing with Typst made easy, providing an API inspired by TikZ and Processing. Includes modules for plotting, charts and tree layout."
9148keywords = [ "draw", "canvas", "tree" ]
9149exclude = [ "/gallery/*", "manual.pdf", "manual.typ" ]