Oregami
Repositories/oxedyne/daimond

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

283 KiB, 1 run

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