I've been obsessing over how to best implement customisable arrowheads (or "marks") for diagramming. I have a design which I think works well, and have a prototype in the master branch of https://github.com/Jollywatt/typst-fletcher. It's not published, so there's time for feedback and new ideas 🙂 It has three parts:
-
How many "centres" does an arrowhead have? You might say one — the tip — but this design has four. Two to control where the mark is "pointing on", one when facing forward (
tip-origin) and one when reversed (tail-origin); two more to control where the line stroke ends, one for forwards (tip-end) and one for backwards (tail-end). These four degrees of freedom allow for all sorts of mark alignment behaviours, although it's a bit complicated! -
What's the best way to represent marks as data in typst? You might suggest "functions which draw cetz objects at the given position and angle." I've settled on representing mark objects as dictionaries with a
drawentry, and any other parameters you want, likesizeorangleoraspect. Taking advantage of the fact that dictionaries are ordered, entries can depend on ones before them, like so:
let example-mark-object = (
size: 2,
tip-end: mark => mark.size,
draw: mark => cetz.draw.line((0, -mark.size), (0, +mark.size)),
)
- Lastly, how can marks be made user-customisable? Currently, all the marks are stored in a global state, which can be directly modified by the user. This means you have to understand and use contextual getter/setter functions, though. Perhaps there is a better user experience for this.
Points 2. and 3. appear in a little more detail in the fletcher manual: https://github.com/Jollywatt/typst-fletcher/blob/master/docs/manual.pdf?raw=true
A question for you
To anyone interested in diagramming with CeTZ or fletcher, what sorts of things to you want or need when it comes to arrows??