56 Commits

Author SHA1 Message Date
19c60c5ecf Release 0.2.0
Reviewed-on: #5
2024-11-09 14:00:05 +00:00
df3a2ddf68 updated README 2024-11-09 14:54:59 +01:00
42e8e26aa7 updated manual and gallery 2024-11-09 14:50:55 +01:00
b33531bef5 added max-width param to _col 2024-11-09 14:42:11 +01:00
ab1386e721 added comment wrapping if width is restricted 2024-11-09 14:41:44 +01:00
1f155063a8 updated CeTZ to 0.3.1 2024-11-09 14:19:14 +01:00
d986284e4e updated typst to 0.12.0 + bumped version to 0.2.0 2024-11-09 14:05:41 +01:00
a037727edf removed font set rule 2024-11-04 08:18:34 +01:00
b0950f5e68 fixed sequences with both create- and enable-dst 2024-10-27 11:24:03 +01:00
eb09a23fc1 added delay function 2024-10-08 11:44:53 +02:00
c4f09a0a3e added _ret for return sequences 2024-10-06 22:27:49 +02:00
9275169e8c added auto participant creation for notes 2024-10-06 15:14:34 +02:00
a198708743 fixed o sequence symbol not filled 2024-10-06 15:13:59 +02:00
b66634d44f added support for auto color with _note and _par 2024-10-06 15:12:50 +02:00
3bc103b9d7 updated manual with new group types 2024-10-05 01:52:31 +02:00
feff030510 added alt/else, loops and other group shortcuts 2024-10-04 19:19:17 +02:00
56cc1b11c0 added column width options 2024-10-04 15:11:45 +02:00
647d50e125 updated typst.toml + bumped to 0.1.1 2024-10-02 18:38:20 +02:00
522cd1537a added manual 2024-10-02 18:33:21 +02:00
6f563750fb published v0.1.0 to universe 2024-10-01 19:17:10 +02:00
5822d705f2 completed README 2024-09-30 15:19:36 +02:00
226efa46b7 added length support for width parmeter 2024-08-28 13:53:37 +02:00
3d9e045b56 fixed group sizing with syncs 2024-08-25 15:11:57 +02:00
eb05c41810 added show-top / -bottom options to participants 2024-08-20 23:19:43 +02:00
b9bbe6f93d added auto fit wrapper function 2024-08-04 00:22:20 +02:00
b5c34c154f fixed ignored comment width with sync 2024-07-31 22:46:24 +02:00
030fd3edba added comment alignment 2024-07-31 18:11:16 +02:00
cbba14ed04 fixed and completed arrow tips 2024-07-30 18:20:50 +02:00
77a65a3199 updated TODO.md 2024-07-30 14:28:23 +02:00
ff6a7e52bc added slanted sequences 2024-07-30 14:26:16 +02:00
ebd4d8f1ca added synched sequences 2024-07-30 12:38:23 +02:00
3b25d68f1e added TODO 2024-07-30 12:19:12 +02:00
0968436c8a implemented create event + create offset 2024-07-30 12:18:57 +02:00
2c724598e9 minor style improvements 2024-06-21 18:14:40 +02:00
0cff9d6799 implemented aligned notes 2024-06-21 17:26:50 +02:00
f08f30b9e2 added notes 2024-06-21 15:59:35 +02:00
81871e2aca updated examples 2024-06-21 02:56:49 +02:00
6500e5a4a3 added arrow tips (WIP) 2024-06-21 02:25:30 +02:00
6e7dc9913f fixed some spacing issues 2024-06-21 02:24:35 +02:00
9c83f810bb added events 2024-06-21 02:23:13 +02:00
fd5b147e86 minor style improvements 2024-06-20 22:44:15 +02:00
ef73f98dd0 minor fixes 2024-06-20 18:02:13 +02:00
3610c86884 removed useless invisible participants 2024-06-20 00:46:42 +02:00
29787ad8f1 fixed missing comment padding 2024-06-19 22:51:02 +02:00
0fbe1e0683 changed destruction color + default arrow style 2024-06-19 22:16:51 +02:00
816cb98491 added various participant types/icons 2024-06-19 22:01:29 +02:00
27ad9da458 added short/start/end arrows 2024-06-19 17:52:56 +02:00
8deb26441d improved seq arrow positioning 2024-06-19 00:39:55 +02:00
a9b467152a added participant creation 2024-06-18 23:01:53 +02:00
4dd940f584 added lifeline-style 2024-06-18 23:00:41 +02:00
ed84e06560 restructured code in separated files 2024-06-18 21:31:13 +02:00
94d0eb286e added basis for lifelines 2024-06-18 20:32:39 +02:00
0e0be4e76a added sequences with same endpoints 2024-06-18 17:08:06 +02:00
adc50124ad added gaps 2024-06-18 16:24:11 +02:00
b8d0c3468c added separators 2024-06-18 16:18:40 +02:00
dc5d868a5d added groups 2024-06-18 16:09:06 +02:00
45 changed files with 3356 additions and 105 deletions

149
README.md
View File

@ -1,3 +1,150 @@
# chronos
A Typst package to draw sequence diagrams with CeTZ
A Typst package to draw sequence diagrams with CeTZ
---
This package lets you render sequence diagrams directly in Typst. The following boilerplate code creates an empty sequence diagram with two participants:
<table>
<tr>
<td><strong>Typst</strong></td>
<td><strong>Result</strong></td>
</tr>
<tr>
<td>
```typst
#import "@preview/chronos:0.2.0"
#chronos.diagram({
import chronos: *
_par("Alice")
_par("Bob")
})
```
</td>
<td><img src="./gallery/readme/boilerplate.png"></td>
</tr>
</table>
> *Disclaimer*\
> The package cannot parse PlantUML syntax for the moment, and thus requires the use of element functions, as shown in the examples.
> A PlantUML parser is in the TODO list, just not the top priority
## Basic sequences
You can make basic sequences using the `_seq` function:
<table>
<tr>
<td><strong>Typst</strong></td>
<td><strong>Result</strong></td>
</tr>
<tr>
<td>
```typst
#chronos.diagram({
import chronos: *
_par("Alice")
_par("Bob")
_seq("Alice", "Bob", comment: "Hello")
_seq("Bob", "Bob", comment: "Think")
_seq("Bob", "Alice", comment: "Hi")
})
```
</td>
<td><img src="./gallery/readme/simple_sequence.png"></td>
</tr>
</table>
You can make lifelines using the following parameters of the `_seq` function:
- `enable-dst`: enables the destination lifeline
- `create-dst`: creates the destination lifeline and participant
- `disable-dst`: disables the destination lifeline
- `destroy-dst`: destroys the destination lifeline and participant
- `disable-src`: disables the source lifeline
- `destroy-src`: destroy the source lifeline and participant
<table>
<tr>
<td><strong>Typst</strong></td>
<td><strong>Result</strong></td>
</tr>
<tr>
<td>
```typst
#chronos.diagram({
import chronos: *
_par("A", display-name: "Alice")
_par("B", display-name: "Bob")
_par("C", display-name: "Charlie")
_par("D", display-name: "Derek")
_seq("A", "B", comment: "hello", enable-dst: true)
_seq("B", "B", comment: "self call", enable-dst: true)
_seq("C", "B", comment: "hello from thread 2", enable-dst: true, lifeline-style: (fill: rgb("#005500")))
_seq("B", "D", comment: "create", create-dst: true)
_seq("B", "C", comment: "done in thread 2", disable-src: true, dashed: true)
_seq("B", "B", comment: "rc", disable-src: true, dashed: true)
_seq("B", "D", comment: "delete", destroy-dst: true)
_seq("B", "A", comment: "success", disable-src: true, dashed: true)
})
```
</td>
<td><img src="./gallery/readme/lifelines.png"></td>
</tr>
</table>
## Showcase
Several features have already been implemented in Chronos. Don't hesitate to checkout the examples in the [gallery](./gallery) folder to see what you can do.
#### Quick example reference:
<table>
<tr>
<td><strong>Example</strong></td>
<td><strong>Features</strong></td>
</tr>
<tr>
<td>
`example1` <br>([PDF](./gallery/example1.pdf)|[Typst](./gallery/example1.typ))
</td>
<td>Simple cases, color sequences, groups, separators, gaps, self-sequences</td>
</tr>
<tr>
<td>
`example2` <br>([PDF](./gallery/example2.pdf)|[Typst](./gallery/example2.typ))
</td>
<td>Lifelines, found/lost messages, synchronized sequences, slanted sequences</td>
</tr>
<tr>
<td>
`example3` <br>([PDF](./gallery/example3.pdf)|[Typst](./gallery/example3.typ))
</td>
<td>Participant shapes, sequence tips, hidden partipicant ends</td>
</tr>
<tr>
<td>
`notes` <br>([PDF](./gallery/notes.pdf)|[Typst](./gallery/notes.typ))
</td>
<td>Notes (duh), deferred participant creation</td>
</tr>
</table>
> [!NOTE]
>
> Many examples were taken/adapted from the PlantUML [documentation](https://plantuml.com/sequence-diagram) on sequence diagrams

28
TODO.md Normal file
View File

@ -0,0 +1,28 @@
# TODO
- [x] Basic participants
- [x] Basic sequences
- [x] Separators
- [x] Gaps
- [x] Groups
- [x] Self arrows
- [x] Arrow from start / to end, small arrows
- [x] Lifelines
- [x] Different types of participants
- [x] Notes
- [x] Synchronized arrows
- [x] Slanted arrows
- [x] Different types of arrow tips
- [x] Sequence comment alignment
- [x] Fix group size with syncs
- [ ] Fix column spacing with notes over multiple columns
- [ ] Fix notes with arrows from start / to end / small arrows
- [ ] Fix group size with self arrows + notes
- [ ] Unify styling structure
- [ ] Add args verification to catch user errors + pretty error messages
- [ ] PlantUML parser
- [ ] (Message numbering)
- [ ] Mainframes
- [x] Different types of groups (alt/loop/etc.)
- [ ] Delays
- [ ] Auto-fit in parent

39
docs/example.typ Normal file
View File

@ -0,0 +1,39 @@
#import "../src/lib.typ" as chronos
#let example-preamble = "import \"../src/lib.typ\": *;"
#let example-scope = (
chronos: chronos
)
#let example(src, show-src: true, vertical: false, fill: true, wrap: true) = {
src = src.text
let full-src = example-preamble + src
let body = eval(full-src, scope: example-scope)
let img = if wrap { chronos.diagram(body) } else { body }
block(width: 100%,
align(center,
box(
stroke: black + 1pt,
radius: .5em,
fill: if fill {color.white.darken(5%)} else {none},
if show-src {
let src-block = align(left, raw(src, lang: "typc"))
table(
columns: if vertical {1} else {2},
inset: 1em,
align: horizon + center,
stroke: none,
img,
if vertical {table.hline()} else {table.vline()}, src-block
)
} else {
table(
inset: 1em,
img
)
}
)
)
)
}

155
docs/examples.typ Normal file
View File

@ -0,0 +1,155 @@
#import "example.typ": example
#let seq-comm-align = example(```
_par("p1", display-name: "Start participant")
_par("p2", display-name: "End participant")
let alignments = (
"start", "end",
"left", "right",
"center"
)
for a in alignments {
_seq(
"p2", "p1",
comment: raw(a),
comment-align: a
)
}
```)
#let seq-tips = example(```
let _seq = _seq.with(comment-align: "center")
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_seq("a", "b", comment: "Various tips", end-tip: "")
_seq("a", "b", end-tip: ">", comment: `->`)
_seq("a", "b", end-tip: ">>", comment: `->>`)
_seq("a", "b", end-tip: "\\", comment: `-\`)
_seq("a", "b", end-tip: "\\\\", comment: `-\\`)
_seq("a", "b", end-tip: "/", comment: `-/`)
_seq("a", "b", end-tip: "//", comment: `-//`)
_seq("a", "b", end-tip: "x", comment: `->x`)
_seq("a", "b", start-tip: "x", comment: `x->`)
_seq("a", "b", start-tip: "o", comment: `o->`)
_seq("a", "b", end-tip: ("o", ">"), comment: `->o`)
_seq("a", "b", start-tip: "o",
end-tip: ("o", ">"), comment: `o->o`)
_seq("a", "b", start-tip: ">",
end-tip: ">", comment: `<->`)
_seq("a", "b", start-tip: ("o", ">"),
end-tip: ("o", ">"), comment: `o<->o`)
_seq("a", "b", start-tip: "x",
end-tip: "x", comment: `x<->x`)
_seq("a", "b", end-tip: ("o", ">>"), comment: `->>o`)
_seq("a", "b", end-tip: ("o", "\\"), comment: `-\o`)
_seq("a", "b", end-tip: ("o", "\\\\"), comment: `-\\o`)
_seq("a", "b", end-tip: ("o", "/"), comment: `-/o`)
_seq("a", "b", end-tip: ("o", "//"), comment: `-//o`)
_seq("a", "b", start-tip: "x",
end-tip: ("o", ">"), comment: `x->o`)
```)
#let grp = example(```
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_grp("Group 1", desc: "Description", {
_seq("a", "b", comment: "Authentication")
_grp("loop", desc: "1000 times", {
_seq("a", "b", comment: "DoS Attack")
})
_seq("a", "b", end-tip: "x")
})
```)
#let alt = example(```
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_alt(
"first encounter", {
_seq("a", "b", comment: "Who are you ?")
_seq("b", "a", comment: "I'm Bob")
},
"know eachother", {
_seq("a", "b", comment: "Hello Bob")
_seq("b", "a", comment: "Hello Alice")
},
"best friends", {
_seq("a", "b", comment: "Hi !")
_seq("b", "a", comment: "Hi !")
}
)
```)
#let loop = example(```
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_loop("default loop", {
_seq("a", "b", comment: "Are you here ?")
})
_gap()
_loop("min loop", min: 1, {
_seq("a", "b", comment: "Are you here ?")
})
_gap()
_loop("min-max loop", min: 1, max: 5, {
_seq("a", "b", comment: "Are you still here ?")
})
```)
#let sync = example(```
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_par("craig", display-name: "Craig")
_seq("bob", "alice") // Unsynchronized
_seq("bob", "craig") // "
_sync({
_seq("bob", "alice") // Synchronized
_seq("bob", "craig") // "
})
_seq("alice", "bob") // Unsynchronized
_seq("craig", "bob") // "
_sync({
_seq("alice", "bob") // Synchronized
_seq("craig", "bob") // "
})
```)
#let gaps-seps = example(```
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_seq("alice", "bob", comment: "Hello")
_gap(size: 10)
_seq("bob", "alice", comment: "Hi")
_sep("Another day")
_seq("alice", "bob", comment: "Hello again")
```)
#let notes-shapes = example(```
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_note("over", `default`, pos: "alice")
_note("over", `rect`, pos: "bob", shape: "rect")
_note("over", `hex`, pos: ("alice", "bob"), shape: "hex")
```)
#let notes-sides = example(```
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_par("charlie", display-name: "Charlie")
_note("left", [`left` of Alice], pos: "alice")
_note("right", [`right` of Charlie], pos: "charlie")
_note("over", [`over` Alice and Bob], pos: ("alice", "bob"))
_note("across", [`across` all participants])
_seq("alice", "bob")
_note("left", [linked with sequence])
_note("over", [A note], pos: "alice")
_note("over", [Aligned note], pos: "charlie", aligned: true)
```, vertical: true)

8
docs/gaps_seps.typ Normal file
View File

@ -0,0 +1,8 @@
/// Creates a gap before the next element
/// - size (int): Size of the gap
#let _gap(size: 20) = {}
/// Creates a separator before the next element
/// #examples.gaps-seps
/// - name (content): Name to display in the middle of the separator
#let _sep(name) = {}

58
docs/groups.typ Normal file
View File

@ -0,0 +1,58 @@
/// Creates a group of sequences
/// #examples.grp
/// - name (content): The group's name
/// - desc (none, content): Optional description
/// - type (str): The groups's type (should only be set through other functions like @@_alt() or @@_loop() )
/// - elmts (array): Elements inside the group (can be sequences, other groups, notes, etc.)
#let _grp(
name,
desc: none,
type: "default",
elmts
) = {}
/// Creates an alt-else group of sequences
///
/// It contains at least one section but can have as many as needed
/// #examples.alt
/// - desc (content): The alt's label
/// - elmts (array): Elements inside the alt's first section
/// - ..args (content, array): Complementary "else" sections.\ You can add as many else sections as you need by passing a content (else section label) followed by an array of elements (see example)
#let _alt(
desc,
elmts,
..args
)
/// Creates a looped group of sequences
/// #examples.loop
/// - desc (content): Loop description
/// - min (none, number): Optional lower bound of the loop
/// - max (auto, number): Upper bound of the loop. If left as `auto` and `min` is set, it will be infinity (`'*'`)
/// - elmts (array): Elements inside the group
#let _loop(
desc,
min: none,
max: auto,
elmts
) = {}
/// Synchronizes multiple sequences\
/// All elements inside a synchronized group will start at the same time
/// #examples.sync
/// - elmts (array): Synchronized elements (generally sequences or notes)
#let _sync(
elmts
)
/// Creates an optional group\
/// This is a simple wrapper around @@_grp()
/// - desc (content): Group description
/// - elmts (array): Elements inside the group
#let _opt(desc, elmts) = {}
/// Creates a break group\
/// This is a simple wrapper around @@_grp()
/// - desc (content): Group description
/// - elmts (array): Elements inside the group
#let _break(desc, elmts) = {}

23
docs/notes.typ Normal file
View File

@ -0,0 +1,23 @@
/// Creates a note
/// - side (str): The side on which to place the note (see @@SIDES for accepted values)
/// - content (content): The note's content
/// - pos (none, str, array): Optional participant(s) on which to draw next to / over. If `side` is "left" or "right", sets next to which participant the note is placed. If `side` is "over", sets over which participant(s) it is placed
/// - color (color): The note's color
/// - shape (str): The note's shape (see @@SHAPES for accepted values)
/// - aligned (bool): True if the note is aligned with another note, in which case `side` must be `"over"`, false otherwise
#let _note(
side,
content,
pos: none,
color: rgb("#FEFFDD"),
shape: "default",
aligned: false
) = {}
/// Accepted values for `shape` argument of @@_note()
/// #examples.notes-shapes
#let SHAPES = ("default", "rect", "hex")
/// Accepted values for `side` argument of @@_note()
/// #examples.notes-sides
#let SIDES = ("left", "right", "over", "across")

69
docs/participants.typ Normal file
View File

@ -0,0 +1,69 @@
/// Possible participant shapes
/// #box(width: 100%, align(center)[
/// #chronos.diagram({
/// import chronos: *
/// let _par = _par.with(show-bottom: false)
/// _par("Foo", display-name: "participant", shape: "participant")
/// _par("Foo1", display-name: "actor", shape: "actor")
/// _par("Foo2", display-name: "boundary", shape: "boundary")
/// _par("Foo3", display-name: "control", shape: "control")
/// _par("Foo4", display-name: "entity", shape: "entity")
/// _par("Foo5", display-name: "database", shape: "database")
/// _par("Foo6", display-name: "collections", shape: "collections")
/// _par("Foo7", display-name: "queue", shape: "queue")
/// _par("Foo8", display-name: "custom", shape: "custom", custom-image: TYPST)
/// _gap()
/// })
/// ])
#let SHAPES = (
"participant",
"actor",
"boundary",
"control",
"entity",
"database",
"collections",
"queue",
"custom"
)
/// Creates a new participant
/// - name (str): Unique participant name used as reference in other functions
/// - display-name (auto, content): Name to display in the diagram. If set to `auto`, `name` is used
/// - from-start (bool): If set to true, the participant is created at the top of the diagram. Otherwise, it is created at the first reference
/// - invisible (bool): If set to true, the participant will not be shown
/// - shape (str): The shape of the participant. Possible values in @@SHAPES
/// - color (color): The participant's color
/// - custom-image (none, image): If shape is 'custom', sets the custom image to display
/// - show-bottom (bool): Whether to display the bottom shape
/// - show-top (bool): Whether to display the top shape
/// -> array
#let _par(
name,
display-name: auto,
from-start: true,
invisible: false,
shape: "participant",
color: rgb("#E2E2F0"),
custom-image: none,
show-bottom: true,
show-top: true,
) = {}
/// Sets some options for columns between participants
///
/// Parameters `p1` and `p2` MUST be consecutive participants (also counting found/lost messages), but they do not need to be in the left to right order
/// - p1 (str): The first neighbouring participant
/// - p2 (str): The second neighbouring participant
/// - width (auto, int, float, length): Optional fixed width of the column\ If the column's content (e.g. sequence comments) is larger, it will overflow
/// - margin (int, float, length): Additional margin to add to the column\ This margin is not included in `width` and `min-width`, but rather added separately
/// - min-width (int, float, length): Minimum width of the column\ If set to a larger value than `width`, the latter will be overriden
/// - max-width (int, float, length, none): Maximum width of the column\ If set to a lower value than `width`, the latter will be overriden\ If set to `none`, no restriction is applied
#let _col(
p1,
p2,
width: auto,
margin: 0,
min-width: 0,
max-width: none
) = {}

60
docs/sequences.typ Normal file
View File

@ -0,0 +1,60 @@
/// Manually adds an event to the given participant
/// - participant (str): The participant concerned by the event
/// - event (str): The event type (see @@EVENTS for ccepted values)
#let _evt(participant, event) = {}
/// Creates a sequence / message between two participants
/// - p1 (str): Start participant
/// - p2 (str): End participant
/// - comment (none, content): Optional comment to display along the arrow
/// - comment-align (str): Where to align the comment with respect to the arrow (see @@comment-align for accepted values)
/// - dashed (bool): Whether the arrow's stroke is dashed or not
/// - start-tip (str): Start arrow tip (see @@tips for accepted values)
/// - end-tip (str): End arrow tip (see @@tips for accepted values)
/// - color (color): Arrow's color
/// - flip (bool): If true, the arrow is flipped (goes from end to start). This is particularly useful for self calls, to change the side on which the arrow appears
/// - enable-dst (bool): If true, enables the destination lifeline
/// - create-dst (bool): If true, creates the destination lifeline and participant
/// - disable-dst (bool): If true, disables the destination lifeline
/// - destroy-dst (bool): If true, destroys the destination lifeline and participant
/// - disable-src (bool): If true, disables the source lifeline
/// - destroy-src (bool): If true, destroy the source lifeline and participant
/// - lifeline-style (auto, dict): Optional styling options for lifeline rectangles (see CeTZ documentation for more information on all possible values)
/// - slant (none, int): Optional slant of the arrow
/// -> array
#let _seq(
p1,
p2,
comment: none,
comment-align: "left",
dashed: false,
start-tip: "",
end-tip: ">",
color: black,
flip: false,
enable-dst: false,
create-dst: false,
disable-dst: false,
destroy-dst: false,
disable-src: false,
destroy-src: false,
lifeline-style: auto,
slant: none
) = {}
/// Accepted values for `event` argument of @@_evt()
///
/// `EVENTS = ("create", "destroy", "enable", "disable")`
#let EVENTS = ("create", "destroy", "enable", "disable")
/// Accepted values for `start-tip` and `end-tip` arguments of @@_seq()
/// #examples.seq-tips
#let tips = (
"", ">", ">>", "\\", "\\\\", "/", "//", "x", "o",
)
/// Accepted values for `comment-align` argument of @@_seq()
/// #examples.seq-comm-align
#let comment-align = (
"start", "end", "left", "center", "right"
)

27
gallery.bash Normal file
View File

@ -0,0 +1,27 @@
#!/bin/bash
echo
echo "Generating gallery PDFs"
set -- ./gallery/*.typ
cnt="$#"
i=1
for f
do
f2="${f/typ/pdf}"
echo "($i/$cnt) $f -> $f2"
typst c --root ./ "$f" "$f2"
i=$((i+1))
done
set -- ./gallery/readme/*.typ
cnt="$#"
i=1
for f
do
f2="${f/typ/png}"
echo "($i/$cnt) $f -> $f2"
typst c --root ./ "$f" "$f2"
i=$((i+1))
done

Binary file not shown.

View File

@ -10,7 +10,7 @@ Alice <-- Bob: Another authentication Response
#chronos.diagram({
import "/src/diagram.typ": *
import chronos: *
_seq("Alice", "Bob", comment: "Authentication Request")
_seq("Bob", "Alice", comment: "Authentication Response", dashed: true)
@ -19,13 +19,88 @@ Alice <-- Bob: Another authentication Response
})
#chronos.diagram({
import "/src/diagram.typ": *
import chronos: *
_seq("Bob", "Alice", comment: "bonjour", color: red)
_seq("Alice", "Bob", comment: "ok", color: blue)
})
#chronos.diagram({
import "/src/diagram.typ": *
import chronos: *
_seq("Alice", "Bob", comment: "This is a test")
_seq("Alice", "Callum", comment: "This is another test with a long text")
})
#chronos.diagram({
import chronos: *
_seq("Alice", "Bob", comment: "Authentication Request")
_alt(
"successful case", {
_seq("Bob", "Alice", comment: "Authentication Accepted")
},
"some kind of failure", {
_seq("Bob", "Alice", comment: "Authentication Failure")
_grp("My own label", desc: "My own label2", {
_seq("Alice", "Log", comment: "Log attack start")
_loop("1000 times", {
_seq("Alice", "Bob", comment: "DNS Attack")
})
_seq("Alice", "Log", comment: "Log attack end")
})
},
"Another type of failure", {
_seq("Bob", "Alice", comment: "Please repeat")
}
)
})
#chronos.diagram({
import chronos: *
_par("a", display-name: box(width: 1.5em, height: .5em), show-bottom: false)
_par("b", display-name: box(width: 1.5em, height: .5em), show-bottom: false)
_col("a", "b", width: 2cm)
_loop("a<1", min: 1, {
_seq("a", "b", end-tip: ">>")
_seq("b", "a", end-tip: ">>")
})
_seq("a", "b", end-tip: ">>")
})
#chronos.diagram({
import chronos: *
_sep("Initialization")
_seq("Alice", "Bob", comment: "Authentication Request")
_seq("Bob", "Alice", comment: "Authentication Response", dashed: true)
_sep("Repetition")
_seq("Alice", "Bob", comment: "Another authentication Request")
_seq("Bob", "Alice", comment: "another authentication Response", dashed: true)
})
#chronos.diagram({
import chronos: *
_seq("Alice", "Bob", comment: "Authentication Request")
_delay()
_seq("Bob", "Alice", comment: "Authentication Response")
_delay(name: "5 minutes later")
_seq("Bob", "Alice", comment: "Good Bye !")
})
#chronos.diagram({
import chronos: *
_seq("Alice", "Bob", comment: "message 1")
_seq("Bob", "Alice", comment: "ok", dashed: true)
_gap()
_seq("Alice", "Bob", comment: "message 2")
_seq("Bob", "Alice", comment: "ok", dashed: true)
_gap(size: 45)
_seq("Alice", "Bob", comment: "message 3")
_seq("Bob", "Alice", comment: "ok", dashed: true)
})
#chronos.diagram({
import chronos: *
_seq("Alice", "Alice", comment: "On the\nright")
_seq("Alice", "Alice", flip: true, comment: "On the\nleft")
})

BIN
gallery/example2.pdf Normal file

Binary file not shown.

169
gallery/example2.typ Normal file
View File

@ -0,0 +1,169 @@
#import "/src/lib.typ" as chronos
#chronos.diagram({
import chronos: *
_seq("User", "A", comment: "DoWork", enable-dst: true)
_seq("A", "B", comment: [#sym.quote.angle.l createRequest #sym.quote.angle.r], enable-dst: true)
_seq("B", "C", comment: "DoWork", enable-dst: true)
_seq("C", "B", comment: "WorkDone", destroy-src: true, disable-src: true, dashed: true)
_seq("B", "A", comment: "RequestCreated", disable-src: true, dashed: true)
_seq("A", "User", comment: "Done", disable-src: true)
})
#chronos.diagram({
import chronos: *
_seq("User", "A", comment: "DoWork", enable-dst: true, lifeline-style: (fill: rgb("#FFBBBB")))
_seq("A", "A", comment: "Internal call", enable-dst: true, lifeline-style: (fill: rgb("#E9967A")))
_seq("A", "B", comment: [#sym.quote.angle.l createRequest #sym.quote.angle.r], enable-dst: true)
_seq("B", "A", comment: "RequestCreated", disable-src: true, disable-dst: true, dashed: true)
_seq("A", "User", comment: "Done", disable-src: true)
})
#chronos.diagram({
import chronos: *
_seq("alice", "bob", comment: "hello", enable-dst: true)
_seq("bob", "bob", comment: "self call", enable-dst: true)
_seq("bill", "bob", comment: "hello from thread 2", enable-dst: true, lifeline-style: (fill: rgb("#005500")))
_seq("bob", "george", comment: "create", create-dst: true)
_seq("bob", "bill", comment: "done in thread 2", disable-src: true, dashed: true)
_seq("bob", "bob", comment: "rc", disable-src: true, dashed: true)
_seq("bob", "george", comment: "delete", destroy-dst: true)
_seq("bob", "alice", comment: "success", disable-src: true, dashed: true)
})
#chronos.diagram({
import chronos: *
_seq("alice", "bob", comment: "hello1", enable-dst: true)
_seq("bob", "charlie", comment: "hello2", enable-dst: true, disable-src: true)
_seq("charlie", "alice", comment: "ok", dashed: true, disable-src: true)
})
#chronos.diagram({
import chronos: *
_seq("?", "Alice", comment: [?->\ *short* to actor1])
_seq("[", "Alice", comment: [\[->\ *from start* to actor1])
_seq("[", "Bob", comment: [\[->\ *from start* to actor2])
_seq("?", "Bob", comment: [?->\ *short* to actor2])
_seq("Alice", "]", comment: [->\]\ from actor1 *to end*])
_seq("Alice", "?", comment: [->?\ *short* from actor1])
_seq("Alice", "Bob", comment: [->\ from actor1 to actor2])
})
#chronos.diagram({
import chronos: *
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_par("craig", display-name: "Craig")
_seq("bob", "alice")
_seq("bob", "craig")
_gap()
_sync({
_seq("bob", "alice", comment: "Synched", comment-align: "start")
_seq("bob", "craig", comment: "Synched", comment-align: "start")
})
_gap()
_seq("alice", "bob")
_seq("craig", "bob")
_gap()
_sync({
_seq("alice", "bob")
_seq("craig", "bob")
})
_gap()
_sync({
_seq("alice", "bob", enable-dst: true)
_seq("craig", "bob")
})
_gap()
_evt("bob", "disable")
})
#chronos.diagram({
import chronos: *
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_par("craig", display-name: "Craig")
_seq("alice", "bob")
_seq("bob", "craig", slant: auto)
_seq("alice", "craig", slant: 20)
_sync({
_seq("alice", "bob", slant: 10)
_seq("craig", "bob", slant: 20)
})
_sync({
_seq("alice", "bob", slant: auto)
_seq("bob", "alice", slant: auto)
})
_gap()
_evt("bob", "disable")
})
#grid(columns: 2, column-gutter: 2em,
chronos.diagram({
import chronos: *
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_seq("alice", "bob", comment: "This is a very long comment")
// Left to right
_seq("alice", "bob", comment: "Start aligned", comment-align: "start")
_seq("alice", "bob", comment: "End aligned", comment-align: "end")
_seq("alice", "bob", comment: "Left aligned", comment-align: "left")
_seq("alice", "bob", comment: "Right aligned", comment-align: "right")
_seq("alice", "bob", comment: "Centered", comment-align: "center")
_gap()
// Right to left
_seq("bob", "alice", comment: "Start aligned", comment-align: "start")
_seq("bob", "alice", comment: "End aligned", comment-align: "end")
_seq("bob", "alice", comment: "Left aligned", comment-align: "left")
_seq("bob", "alice", comment: "Right aligned", comment-align: "right")
_seq("bob", "alice", comment: "Centered", comment-align: "center")
_gap()
// Slant left to right
_seq("alice", "bob", comment: "Start aligned", comment-align: "start", slant: 10)
_seq("alice", "bob", comment: "End aligned", comment-align: "end", slant: 10)
_seq("alice", "bob", comment: "Left aligned", comment-align: "left", slant: 10)
_seq("alice", "bob", comment: "Right aligned", comment-align: "right", slant: 10)
_seq("alice", "bob", comment: "Centered", comment-align: "center", slant: 10)
_gap()
// Slant right to left
_seq("bob", "alice", comment: "Start aligned", comment-align: "start", slant: 10)
_seq("bob", "alice", comment: "End aligned", comment-align: "end", slant: 10)
_seq("bob", "alice", comment: "Left aligned", comment-align: "left", slant: 10)
_seq("bob", "alice", comment: "Right aligned", comment-align: "right", slant: 10)
_seq("bob", "alice", comment: "Centered", comment-align: "center", slant: 10)
}),
chronos.diagram({
import chronos: *
_par("alice", display-name: "Alice")
_seq("alice", "alice", comment: "Start aligned", comment-align: "start")
_seq("alice", "alice", comment: "End aligned", comment-align: "end")
_seq("alice", "alice", comment: "Left aligned", comment-align: "left")
_seq("alice", "alice", comment: "Right aligned", comment-align: "right")
_seq("alice", "alice", comment: "Centered", comment-align: "center")
_seq("alice", "alice", comment: "Start aligned", comment-align: "start", flip: true)
_seq("alice", "alice", comment: "End aligned", comment-align: "end", flip: true)
_seq("alice", "alice", comment: "Left aligned", comment-align: "left", flip: true)
_seq("alice", "alice", comment: "Right aligned", comment-align: "right", flip: true)
_seq("alice", "alice", comment: "Centered", comment-align: "center", flip: true)
})
)

BIN
gallery/example3.pdf Normal file

Binary file not shown.

166
gallery/example3.typ Normal file
View File

@ -0,0 +1,166 @@
#import "/src/lib.typ" as chronos
#set page(width: auto, height: auto)
#let TYPST = image("typst.png", width: 1.5cm, height: 1.5cm, fit: "contain")
#let FERRIS = image("ferris.png", width: 1.5cm, height: 1.5cm, fit: "contain")
#let ME = image("me.jpg", width: 1.5cm, height: 1.5cm, fit: "contain")
#chronos.diagram({
import chronos: *
_par("Foo", display-name: "Participant", shape: "participant")
_par("Foo1", display-name: "Actor", shape: "actor")
_par("Foo2", display-name: "Boundary", shape: "boundary")
_par("Foo3", display-name: "Control", shape: "control")
_par("Foo4", display-name: "Entity", shape: "entity")
_par("Foo5", display-name: "Database", shape: "database")
_par("Foo6", display-name: "Collections", shape: "collections")
_par("Foo7", display-name: "Queue", shape: "queue")
_par("Foo8", display-name: "Typst", shape: "custom", custom-image: TYPST)
_par("Foo9", display-name: "Ferris", shape: "custom", custom-image: FERRIS)
_par("Foo10", display-name: "Baryhobal", shape: "custom", custom-image: ME)
_seq("Foo", "Foo1", comment: "To actor")
_seq("Foo", "Foo2", comment: "To boundary")
_seq("Foo", "Foo3", comment: "To control")
_seq("Foo", "Foo4", comment: "To entity")
_seq("Foo", "Foo5", comment: "To database")
_seq("Foo", "Foo6", comment: "To collections")
_seq("Foo", "Foo7", comment: "To queue")
_seq("Foo", "Foo8", comment: "To Typst")
_seq("Foo", "Foo9", comment: "To ferris")
_seq("Foo", "Foo10", comment: "To Baryhobal")
})
#pagebreak()
#chronos.diagram({
import chronos: *
_par("me", display-name: "Me", shape: "custom", custom-image: ME)
_par("typst", display-name: "Typst", shape: "custom", custom-image: TYPST)
_par("rust", display-name: "Rust", shape: "custom", custom-image: FERRIS)
_seq("me", "typst", comment: "opens document", enable-dst: true)
_seq("me", "typst", comment: "types document")
_seq("typst", "rust", comment: "compiles content", enable-dst: true)
_seq("rust", "typst", comment: "renders document", disable-src: true)
_seq("typst", "me", comment: "displays document")
_evt("typst", "disable")
})
#pagebreak()
#stack(dir: ltr, spacing: 1em,
chronos.diagram({
import chronos: *
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_seq("a", "b", end-tip: ">", comment: `->`)
_seq("a", "b", end-tip: ">>", comment: `->>`)
_seq("a", "b", end-tip: "\\", comment: `-\`)
_seq("a", "b", end-tip: "\\\\", comment: `-\\`)
_seq("a", "b", end-tip: "/", comment: `-/`)
_seq("a", "b", end-tip: "//", comment: `-//`)
_seq("a", "b", end-tip: "x", comment: `->x`)
_seq("a", "b", start-tip: "x", comment: `x->`)
_seq("a", "b", start-tip: "o", comment: `o->`)
_seq("a", "b", end-tip: ("o", ">"), comment: `->o`)
_seq("a", "b", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`)
_seq("a", "b", start-tip: ">", end-tip: ">", comment: `<->`)
_seq("a", "b", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`)
_seq("a", "b", start-tip: "x", end-tip: "x", comment: `x<->x`)
_seq("a", "b", end-tip: ("o", ">>"), comment: `->>o`)
_seq("a", "b", end-tip: ("o", "\\"), comment: `-\o`)
_seq("a", "b", end-tip: ("o", "\\\\"), comment: `-\\o`)
_seq("a", "b", end-tip: ("o", "/"), comment: `-/o`)
_seq("a", "b", end-tip: ("o", "//"), comment: `-//o`)
_seq("a", "b", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`)
}),
chronos.diagram({
import chronos: *
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_seq("b", "a", end-tip: ">", comment: `->`)
_seq("b", "a", end-tip: ">>", comment: `->>`)
_seq("b", "a", end-tip: "\\", comment: `-\`)
_seq("b", "a", end-tip: "\\\\", comment: `-\\`)
_seq("b", "a", end-tip: "/", comment: `-/`)
_seq("b", "a", end-tip: "//", comment: `-//`)
_seq("b", "a", end-tip: "x", comment: `->x`)
_seq("b", "a", start-tip: "x", comment: `x->`)
_seq("b", "a", start-tip: "o", comment: `o->`)
_seq("b", "a", end-tip: ("o", ">"), comment: `->o`)
_seq("b", "a", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`)
_seq("b", "a", start-tip: ">", end-tip: ">", comment: `<->`)
_seq("b", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`)
_seq("b", "a", start-tip: "x", end-tip: "x", comment: `x<->x`)
_seq("b", "a", end-tip: ("o", ">>"), comment: `->>o`)
_seq("b", "a", end-tip: ("o", "\\"), comment: `-\o`)
_seq("b", "a", end-tip: ("o", "\\\\"), comment: `-\\o`)
_seq("b", "a", end-tip: ("o", "/"), comment: `-/o`)
_seq("b", "a", end-tip: ("o", "//"), comment: `-//o`)
_seq("b", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`)
}),
chronos.diagram({
import chronos: *
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_seq("a", "a", end-tip: ">", comment: `->`)
_seq("a", "a", end-tip: ">>", comment: `->>`)
_seq("a", "a", end-tip: "\\", comment: `-\`)
_seq("a", "a", end-tip: "\\\\", comment: `-\\`)
_seq("a", "a", end-tip: "/", comment: `-/`)
_seq("a", "a", end-tip: "//", comment: `-//`)
_seq("a", "a", end-tip: "x", comment: `->x`)
_seq("a", "a", start-tip: "x", comment: `x->`)
_seq("a", "a", start-tip: "o", comment: `o->`)
_seq("a", "a", end-tip: ("o", ">"), comment: `->o`)
_seq("a", "a", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`)
_seq("a", "a", start-tip: ">", end-tip: ">", comment: `<->`)
_seq("a", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`)
_seq("a", "a", start-tip: "x", end-tip: "x", comment: `x<->x`)
_seq("a", "a", end-tip: ("o", ">>"), comment: `->>o`)
_seq("a", "a", end-tip: ("o", "\\"), comment: `-\o`)
_seq("a", "a", end-tip: ("o", "\\\\"), comment: `-\\o`)
_seq("a", "a", end-tip: ("o", "/"), comment: `-/o`)
_seq("a", "a", end-tip: ("o", "//"), comment: `-//o`)
_seq("a", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`)
})
)
#chronos.diagram({
import chronos: *
_par("a", display-name: "Alice")
_par("b", display-name: "Bob", show-bottom: false)
_par("c", display-name: "Caleb", show-top: false)
_par("d", display-name: "Danny", show-bottom: false, show-top: false)
_gap()
})
#chronos.diagram({
import chronos: *
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_par("c", display-name: "Caleb")
_par("d", display-name: "Danny")
_par("e", display-name: "Erika")
_col("a", "b")
_col("b", "c", width: 2cm)
_col("c", "d", margin: .5cm)
_col("d", "e", min-width: 2cm)
//_seq("b", "c", comment: [Hello World !])
//_seq("c", "d", comment: [Hello World])
//_seq("d", "e", comment: [Hello World])
})

BIN
gallery/ferris.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
gallery/gitea.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
gallery/me.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

BIN
gallery/notes.pdf Normal file

Binary file not shown.

145
gallery/notes.typ Normal file
View File

@ -0,0 +1,145 @@
#import "/src/lib.typ" as chronos: *
#set page(width: auto, height: auto)
#chronos.diagram({
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_seq("a", "b", comment: [hello])
_note("left", [this is a first note])
_seq("b", "a", comment: [ok])
_note("right", [this is another note])
_seq("b", "b", comment: [I am thinking])
_note("left", [a note\ can also be defined\ on several lines])
})
#pagebreak()
#chronos.diagram({
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_note("left", [This is displayed\ left of Alice.], pos: "a", color: rgb("#00FFFF"))
_note("right", [This is displayed right of Alice.], pos: "a")
_note("over", [This is displayed over Alice.], pos: "a")
_note("over", [This is displayed\ over Bob and Alice.], pos: ("a", "b"), color: rgb("#FFAAAA"))
_note("over", [This is yet another\ example of\ a long note.], pos: ("a", "b"))
})
#pagebreak()
#chronos.diagram({
_par("caller")
_par("server")
_seq("caller", "server", comment: [conReq])
_note("over", [idle], pos: "caller", shape: "hex")
_seq("server", "caller", comment: [conConf])
_note("over", ["r" as rectangle\ "h" as hexagon], pos: "server", shape: "rect")
_note("over", [this is\ on several\ lines], pos: "server", shape: "rect")
_note("over", [this is\ on several\ lines], pos: "caller", shape: "hex")
})
#pagebreak()
#chronos.diagram({
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_par("c", display-name: "Charlie")
_seq("a", "b", comment: [m1])
_seq("b", "c", comment: [m2])
_note("over", [Old method for note over all part. with:\ `note over FirstPart, LastPart`.], pos: ("a", "c"))
_note("across", [New method with:\ `note across`.])
_seq("b", "a")
_note("across", [Note across all part.], shape: "hex")
})
#pagebreak()
#chronos.diagram({
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_note("over", [initial state of Alice], pos: "a")
_note("over", [initial state of Bob], pos: "b")
_seq("b", "a", comment: [hello])
})
#chronos.diagram({
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_par("c", display-name: "Charlie")
_par("d", display-name: "Donald")
_par("e", display-name: "Eddie")
_note("over", [initial state of Alice], pos: "a")
_note("over", [initial state of Bob the builder], pos: "b", aligned: true)
_note("over", [Note 1], pos: "a")
_note("over", [Note 2], pos: "b", aligned: true)
_note("over", [Note 3], pos: "c", aligned: true)
_seq("a", "d")
_note("over", [this is an extremely long note], pos: ("d", "e"))
})
#pagebreak()
#chronos.diagram({
_par("a", display-name: [Alice])
_par("b", display-name: [The *Famous* Bob])
_seq("a", "b", comment: [hello #strike([there])])
_gap()
_seq("b", "a", comment: [ok])
_note("left", [
This is *bold*\
This is _italics_\
This is `monospaced`\
This is #strike([stroked])\
This is #underline([underlined])\
This is #underline([waved])\
])
_seq("a", "b", comment: [A _well formatted_ message])
_note("right", [
This is #box(text([displayed], size: 18pt), fill: rgb("#5F9EA0"))\
#underline([left of]) Alice.
], pos: "a")
_note("left", [
#underline([This], stroke: red) is #text([displayed], fill: rgb("#118888"))\
*#text([left of], fill: rgb("#800080")) #strike([Alice], stroke: red) Bob.*
], pos: "b")
_note("over", [
#underline([This is hosted], stroke: rgb("#FF33FF")) by #box(baseline: 50%, image("gitea.png", width: 1cm, height: 1cm, fit: "contain"))
], pos: ("a", "b"))
})
// TODO
/*
#pagebreak()
#chronos.diagram({
_par("a", display-name: [Alice])
_par("b", display-name: [Bob])
_seq("a", "b", comment: [Hello])
_note("left", [This is a note])
_seq("[", "a", comment: [Test])
_note("left", [This is also a note])
})*/
#pagebreak()
#chronos.diagram({
_seq("Bob", "Alice", comment: [Hello])
_evt("Other", "create")
})

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,13 @@
#import "/src/lib.typ" as chronos
#set page(
width: auto,
height: auto,
margin: 0.5cm
)
#chronos.diagram({
import chronos: *
_par("Alice")
_par("Bob")
})

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1,24 @@
#import "/src/lib.typ" as chronos
#set page(
width: auto,
height: auto,
margin: 0.5cm
)
#chronos.diagram({
import chronos: *
_par("A", display-name: "Alice")
_par("B", display-name: "Bob")
_par("C", display-name: "Charlie")
_par("D", display-name: "Derek")
_seq("A", "B", comment: "hello", enable-dst: true)
_seq("B", "B", comment: "self call", enable-dst: true)
_seq("C", "B", comment: "hello from thread 2", enable-dst: true, lifeline-style: (fill: rgb("#005500")))
_seq("B", "D", comment: "create", create-dst: true)
_seq("B", "C", comment: "done in thread 2", disable-src: true, dashed: true)
_seq("B", "B", comment: "rc", disable-src: true, dashed: true)
_seq("B", "D", comment: "delete", destroy-dst: true)
_seq("B", "A", comment: "success", disable-src: true, dashed: true)
})

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,17 @@
#import "/src/lib.typ" as chronos
#set page(
width: auto,
height: auto,
margin: 0.5cm
)
#chronos.diagram({
import chronos: *
_par("Alice")
_par("Bob")
_seq("Alice", "Bob", comment: "Hello")
_seq("Bob", "Bob", comment: "Think")
_seq("Bob", "Alice", comment: "Hi")
})

BIN
gallery/typst.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
manual.pdf Normal file

Binary file not shown.

214
manual.typ Normal file
View File

@ -0,0 +1,214 @@
#import "@preview/tidy:0.3.0"
#import "src/lib.typ" as chronos
#import "src/participant.typ" as mod-par
#import "docs/examples.typ"
#import "docs/example.typ": example
#let TYPST = image("gallery/typst.png", width: 1.5cm, height: 1.5cm, fit: "contain")
#let doc-ref(target, full: false, var: false) = {
let (module, func) = target.split(".")
let label-name = module + func
let display-name = func
if full {
display-name = target
}
if not var {
label-name += "()"
display-name += "()"
}
link(label(label-name))[#display-name]
}
#set heading(numbering: (..num) => if num.pos().len() < 4 {
numbering("1.1", ..num)
})
#{
outline(indent: true, depth: 3)
}
#show link: set text(fill: blue)
#set page(numbering: "1/1", header: align(right)[chronos #sym.dash.em v#chronos.version])
#set page(
header: align(left)[chronos #sym.dash.em v#chronos.version],
footer: context align(center, counter(page).display("1/1", both: true))
)
= Introduction
This package lets you create nice sequence diagrams using the CeTZ package.
= Usage
Simply import #link("https://typst.app/universe/package/chronos/")[chronos] and call the `diagram` function:
#pad(left: 1em)[```typ
#import "@preview/chronos:0.1.0"
#chronos.diagram({
import chronos: *
...
})
```]
= Examples
You can find the following examples and more in the #link("https://git.kb28.ch/HEL/circuiteria/src/branch/main/gallery")[gallery] directory
== Some groups and sequences
#example(```
chronos.diagram({
import chronos: *
_seq("Alice", "Bob", comment: "Authentication Request")
_seq("Bob", "Alice", comment: "Authentication Failure")
_grp("My own label", desc: "My own label2", {
_seq("Alice", "Log", comment: "Log attack start")
_grp("loop", desc: "1000 times", {
_seq("Alice", "Bob", comment: "DNS Attack")
})
_seq("Alice", "Bob", comment: "Log attack end")
})
})
```, wrap: false, vertical: true)
#pagebreak(weak: true)
== Lifelines
#example(```
chronos.diagram({
import chronos: *
_seq("alice", "bob", comment: "hello", enable-dst: true)
_seq("bob", "bob", comment: "self call", enable-dst: true)
_seq(
"bill", "bob",
comment: "hello from thread 2",
enable-dst: true,
lifeline-style: (fill: rgb("#005500"))
)
_seq("bob", "george", comment: "create", create-dst: true)
_seq(
"bob", "bill",
comment: "done in thread 2",
disable-src: true,
dashed: true
)
_seq("bob", "bob", comment: "rc", disable-src: true, dashed: true)
_seq("bob", "george", comment: "delete", destroy-dst: true)
_seq("bob", "alice", comment: "success", disable-src: true, dashed: true)
})
```, wrap: false, vertical: true)
#pagebreak(weak: true)
== Found and lost messages
#example(```
chronos.diagram({
import chronos: *
_seq("?", "Alice", comment: [?->\ *short* to actor1])
_seq("[", "Alice", comment: [\[->\ *from start* to actor1])
_seq("[", "Bob", comment: [\[->\ *from start* to actor2])
_seq("?", "Bob", comment: [?->\ *short* to actor2])
_seq("Alice", "]", comment: [->\]\ from actor1 *to end*])
_seq("Alice", "?", comment: [->?\ *short* from actor1])
_seq("Alice", "Bob", comment: [->\ from actor1 to actor2])
})
```, wrap: false, vertical: true)
#pagebreak(weak: true)
== Custom images
#example(```
let load-img(path) = image(path, width: 1.5cm, height: 1.5cm, fit:"contain")
let TYPST = load-img("../gallery/typst.png")
let FERRIS = load-img("../gallery/ferris.png")
let ME = load-img("../gallery/me.jpg")
chronos.diagram({
import chronos: *
_par("me", display-name: "Me", shape: "custom", custom-image: ME)
_par("typst", display-name: "Typst", shape: "custom", custom-image: TYPST)
_par("rust", display-name: "Rust", shape: "custom", custom-image: FERRIS)
_seq("me", "typst", comment: "opens document", enable-dst: true)
_seq("me", "typst", comment: "types document")
_seq("typst", "rust", comment: "compiles content", enable-dst: true)
_seq("rust", "typst", comment: "renders document", disable-src: true)
_seq("typst", "me", comment: "displays document", disable-src: true)
})
```, wrap: false, vertical: true)
#pagebreak(weak: true)
= Reference
#let par-docs = tidy.parse-module(
read("docs/participants.typ"),
name: "Participants",
require-all-parameters: true,
scope: (
chronos: chronos,
mod-par: mod-par,
TYPST: TYPST,
doc-ref: doc-ref
)
)
#tidy.show-module(par-docs, show-outline: false, sort-functions: none)
#pagebreak(weak: true)
#let seq-docs = tidy.parse-module(
read("docs/sequences.typ"),
name: "Sequences",
require-all-parameters: true,
scope: (
chronos: chronos,
doc-ref: doc-ref,
examples: examples
)
)
#tidy.show-module(seq-docs, show-outline: false, sort-functions: none)
#pagebreak(weak: true)
#let grp-docs = tidy.parse-module(
read("docs/groups.typ"),
name: "Groups",
require-all-parameters: true,
scope: (
chronos: chronos,
doc-ref: doc-ref,
examples: examples
)
)
#tidy.show-module(grp-docs, show-outline: false, sort-functions: none)
#pagebreak(weak: true)
#let gap-sep-docs = tidy.parse-module(
read("docs/gaps_seps.typ"),
name: "Gaps and separators",
require-all-parameters: true,
scope: (
chronos: chronos,
doc-ref: doc-ref,
examples: examples
)
)
#tidy.show-module(gap-sep-docs, show-outline: false)
#pagebreak(weak: true)
#let notes-docs = tidy.parse-module(
read("docs/notes.typ"),
name: "Notes",
require-all-parameters: true,
scope: (
chronos: chronos,
doc-ref: doc-ref,
examples: examples
)
)
#tidy.show-module(notes-docs, show-outline: false)

30
src/consts.typ Normal file
View File

@ -0,0 +1,30 @@
#let Y-SPACE = 10
#let PAR-SPACE = 10
#let COMMENT-PAD = 8
#let LIFELINE-W = 10
#let CREATE-OFFSET = 15
#let DEFAULT-SLANT = 10
#let CROSS-TIP-SIZE = 4
#let CIRCLE-TIP-RADIUS = 3
#let SYM-GAP = 5
#let PAR-PAD = (5pt, 3pt)
#let ACTOR-WIDTH = 20
#let BOUNDARY-HEIGHT = 20
#let CONTROL-HEIGHT = 20
#let ENTITY-HEIGHT = 20
#let DATABASE-WIDTH = 24
#let COLLECTIONS-PAD = (5pt, 3pt)
#let COLLECTIONS-DX = 3
#let COLLECTIONS-DY = 3
#let QUEUE-PAD = (5pt, 3pt)
#let NOTE-PAD = (6, 3)
#let NOTE-CORNER-SIZE = 6
#let NOTE-GAP = 3
#let NOTE-HEX-PAD = (6, 8)
#let COL-DESTRUCTION = rgb("#A80238")
#let COL-GRP-NAME = rgb("#EEEEEE")
#let COL-SEP-NAME = rgb("#EEEEEE")
#let COL-NOTE = rgb("#FEFFDD")

View File

@ -1,58 +1,248 @@
#import "utils.typ": get-group-span, fit-canvas
#import "renderer.typ": render
#import "participant.typ" as participant: _par, PAR-SPECIALS
#import "sequence.typ": _seq
#let _seq(
p1,
p2,
comment: none,
dashed: false,
tip: "default",
color: black
) = {
#let _gap(size: 20) = {
return ((
type: "seq",
type: "gap",
size: size
),)
}
#let _evt(participant, event) = {
return ((
type: "evt",
participant: participant,
event: event,
lifeline-style: auto
),)
}
#let _col(p1, p2, width: auto, margin: 0, min-width: 0, max-width: none) = {
return ((
type: "col",
p1: p1,
p2: p2,
comment: comment,
dashed: dashed,
tip: tip,
color: color,
width: width,
margin: margin,
min-width: min-width,
max-width: max-width
),)
}
#let _par(name, display-name: auto, start-at: 0) = {
return ((
type: "par",
name: name,
display-name: if display-name == auto {name} else {display-name},
start-at: start-at
),)
}
#let _par-exists(participants, name) = {
for p in participants {
if name == p.name {
return true
}
#let diagram(elements, width: auto) = {
if elements == none {
return
}
return false
}
#let diagram(elements) = {
let participants = ()
for elmt in elements {
let elmts = elements
let i = 0
let activation-history = ()
// Flatten groups + convert returns
while i < elmts.len() {
let elmt = elmts.at(i)
if elmt.type == "grp" {
let grp-elmts = elmt.elmts
elmt.elmts = elmt.elmts.map(e => {
if e.type == "seq" {
if e.p1 == "?" {
e.p1 = "?" + e.p2
} else if e.p2 == "?" {
e.p2 = e.p1 + "?"
}
}
e
})
elmts.at(i) = elmt
elmts = (
elmts.slice(0, i + 1) +
grp-elmts +
((
type: "grp-end"
),) +
elmts.slice(i+1)
)
} else if elmt.type == "seq" {
if elmt.enable-dst {
activation-history.push(elmt)
}
} else if elmt.type == "evt" {
if elmt.event == "enable" {
for elmt2 in elmts.slice(0, i).rev() {
if elmt2.type == "seq" {
activation-history.push(elmt2)
break
}
}
}
} else if elmt.type == "ret" {
if activation-history.len() == 0 {
panic("Cannot return if no lifeline is activated")
}
let seq = activation-history.pop()
elmts.at(i) = _seq(
seq.p2, seq.p1,
comment: elmt.comment,
disable-src: true,
dashed: true
).first()
}
i += 1
}
// List participants
let linked = ()
let last-seq = none
let last-note = none
for (i, elmt) in elmts.enumerate() {
if elmt.type == "par" {
participants.push(elmt)
} else if elmt.type == "seq" {
if not _par-exists(participants, elmt.p1) {
if not participant._exists(participants, elmt.p1) {
participants.push(_par(elmt.p1).first())
}
if not _par-exists(participants, elmt.p2) {
participants.push(_par(elmt.p2).first())
if not participant._exists(participants, elmt.p2) {
let par = _par(elmt.p2, from-start: not elmt.create-dst).first()
participants.push(par)
} else if elmt.create-dst {
let i = participants.position(p => p.name == elmt.p2)
participants.at(i).from-start = false
}
let p1 = elmt.p1
let p2 = elmt.p2
if elmt.p1 == "?" {
p1 = "?" + elmt.p2
}
if elmt.p2 == "?" {
p2 = elmt.p1 + "?"
}
linked.push(p1)
linked.push(p2)
last-seq = (
elmt: elmt,
i: i,
p1: p1,
p2: p2
)
} else if elmt.type == "note" {
elmt.insert("linked", elmt.pos == none and elmt.side != "across")
if elmt.pos == none and elmt.side != "across" {
let names = participants.map(p => p.name)
let i1 = names.position(n => n == last-seq.p1)
let i2 = names.position(n => n == last-seq.p2)
let pars = ((i1, last-seq.p1), (i2, last-seq.p2)).sorted(key: p => p.first())
if elmt.side == "left" {
elmt.pos = pars.first().last()
} else if elmt.side == "right" {
elmt.pos = pars.last().last()
}
let seq = last-seq.elmt
seq.insert("linked-note", elmt)
elmts.at(last-seq.i) = seq
}
if elmt.aligned {
let n = last-note.elmt
n.aligned-with = elmt
elmts.at(last-note.i) = n
}
elmts.at(i) = elmt
if elmt.side == "left" {
linked.push("[")
} else if elmt.side == "right" {
linked.push("]")
}
let pars = none
if type(elmt.pos) == str {
pars = (elmt.pos,)
} else if type(elmt.pos) == array {
pars = elmt.pos
}
if pars != none {
for par in pars {
if not participant._exists(participants, par) {
participants.push(_par(par).first())
}
}
}
last-note = (
elmt: elmt,
i: i
)
} else if elmt.type == "evt" {
let par = elmt.participant
if not participant._exists(participants, par) {
let p = _par(par, from-start: elmt.event != "create").first()
participants.push(p)
} else if elmt.event == "create" {
let i = participants.position(p => p.name == par)
participants.at(i).from-start = false
}
}
}
linked = linked.dedup()
let pars = participants
participants = ()
if "[" in linked {
participants.push(_par("[", invisible: true).first())
}
for (i, p) in pars.enumerate() {
let before = _par("?" + p.name, invisible: true).first()
let after = _par(p.name + "?", invisible: true).first()
if before.name in linked {
if participants.len() == 0 or not participants.last().name.ends-with("?") {
participants.push(before)
} else {
participants.insert(-1, before)
}
}
participants.push(p)
if after.name in linked {
participants.push(after)
}
}
if "]" in linked {
participants.push(_par("]", invisible: true).first())
}
// Add index to participant
for (i, p) in participants.enumerate() {
p.insert("i", i)
participants.at(i) = p
}
// Compute groups spans (horizontal)
for (i, elmt) in elmts.enumerate() {
if elmt.type == "grp" or elmt.type == "alt" {
let (min-i, max-i) = get-group-span(participants, elmt)
elmts.at(i).insert("min-i", min-i)
elmts.at(i).insert("max-i", max-i)
} else if elmt.type == "seq" {
if elmt.p1 == "?" {
elmts.at(i).p1 = "?" + elmt.p2
} else if elmt.p2 == "?" {
elmts.at(i).p2 = elmt.p1 + "?"
}
}
}
render(participants, elements)
let canvas = render(participants, elmts)
fit-canvas(canvas, width: width)
}
#let from-plantuml(code) = {

99
src/group.typ Normal file
View File

@ -0,0 +1,99 @@
#import "@preview/cetz:0.3.1": draw
#import "consts.typ": *
#let _grp(name, desc: none, type: "default", elmts) = {
return ((
type: "grp",
name: name,
desc: desc,
grp-type: type,
elmts: elmts
),)
}
#let _alt(desc, elmts, ..args) = {
let all-elmts = ()
all-elmts += elmts
let args = args.pos()
for i in range(0, args.len(), step: 2) {
let else-desc = args.at(i)
let else-elmts = args.at(i + 1, default: ())
all-elmts.push((
type: "else",
desc: else-desc
))
all-elmts += else-elmts
}
return _grp("alt", desc: desc, type: "alt", all-elmts)
}
#let _loop(desc, min: none, max: auto, elmts) = {
let name = "loop"
if min != none {
if max == auto {
max = "*"
}
name += "(" + str(min) + "," + str(max) + ")"
}
_grp(name, desc: desc, type: "loop", elmts)
}
#let _opt(desc, elmts) = grp("opt", desc: desc, type: "opt", elmts)
#let _break(desc, elmts) = grp("break", desc: desc, type: "break", elmts)
#let render(x0, x1, y0, y1, group) = {
let shapes = ()
let name = text(group.name, weight: "bold")
let m = measure(box(name))
let w = m.width / 1pt + 15
let h = m.height / 1pt + 6
shapes += draw.rect(
(x0, y0),
(x1, y1)
)
shapes += draw.merge-path(
fill: COL-GRP-NAME,
close: true,
{
draw.line(
(x0, y0),
(x0 + w, y0),
(x0 + w, y0 - h / 2),
(x0 + w - 5, y0 - h),
(x0, y0 - h)
)
}
)
shapes += draw.content(
(x0, y0),
name,
anchor: "north-west",
padding: (left: 5pt, right: 10pt, top: 3pt, bottom: 3pt)
)
if group.desc != none {
shapes += draw.content(
(x0 + w, y0),
text([\[#group.desc\]], weight: "bold", size: .8em),
anchor: "north-west",
padding: 3pt
)
}
return shapes
}
#let render-else(x0, x1, y, elmt) = {
let shapes = draw.line(
(x0, y),
(x1, y),
stroke: (dash: (2pt, 1pt), thickness: .5pt)
)
shapes += draw.content(
(x0, y),
text([\[#elmt.desc\]], weight: "bold", size: .8em),
anchor: "north-west",
padding: 3pt
)
return shapes
}

View File

@ -1 +1,9 @@
#import "diagram.typ": diagram, from-plantuml
#let version = version(0, 2, 0)
#import "diagram.typ": diagram, from-plantuml, _gap, _evt, _col
#import "sequence.typ": _seq, _ret
#import "group.typ": _grp, _loop, _alt, _opt, _break
#import "participant.typ": _par
#import "separator.typ": _sep, _delay
#import "note.typ": _note
#import "sync.typ": _sync

185
src/note.typ Normal file
View File

@ -0,0 +1,185 @@
#import "@preview/cetz:0.3.1": draw
#import "consts.typ": *
#let SIDES = (
"left",
"right",
"over",
"across"
)
#let SHAPES = (
"default",
"rect",
"hex"
)
#let _note(side, content, pos: none, color: COL-NOTE, shape: "default", aligned: false) = {
if side == "over" {
if pos == none {
panic("Pos cannot be none with side 'over'")
}
}
if aligned {
if side != "over" {
panic("Aligned notes can only be over a participant (got side '" + side + "')")
}
}
if color == auto {
color = COL-NOTE
}
return ((
type: "note",
side: side,
content: content,
pos: pos,
color: color,
shape: shape,
aligned: aligned,
aligned-with: none
),)
}
#let get-note-box(note) = {
let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD}
let inset = (
left: PAD.last() * 1pt,
right: PAD.last() * 1pt,
top: PAD.first() * 1pt,
bottom: PAD.first() * 1pt,
)
if note.shape == "default" {
inset.right += NOTE-CORNER-SIZE * 1pt
}
if note.side == "left" {
inset.right += NOTE-GAP * 1pt
} else if note.side == "right" {
inset.left += NOTE-GAP * 1pt
}
return box(note.content, inset: inset)
}
#let get-size(note) = {
let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD}
let m = measure(box(note.content))
let w = m.width / 1pt + PAD.last() * 2
let h = m.height / 1pt + PAD.first() * 2
if note.shape == "default" {
w += NOTE-CORNER-SIZE
}
return (
width: w,
height: h
)
}
#let _get-base-x(pars-i, x-pos, note) = {
if note.side == "across" {
return (x-pos.first() + x-pos.last()) / 2
}
if note.side == "over" {
if type(note.pos) == array {
let xs = note.pos.map(par => x-pos.at(pars-i.at(par)))
return (calc.min(..xs) + calc.max(..xs)) / 2
}
}
return x-pos.at(pars-i.at(note.pos))
}
#let render(pars-i, x-pos, note, y, lifelines) = {
let shapes = ()
let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD}
let m = measure(box(note.content))
let w = m.width / 1pt + PAD.last() * 2
let h = m.height / 1pt + PAD.first() * 2
let total-w = w
if note.shape == "default" {
total-w += NOTE-CORNER-SIZE
}
let base-x = _get-base-x(pars-i, x-pos, note)
let i = none
if note.pos != none and type(note.pos) == str {
i = pars-i.at(note.pos)
}
let x0 = base-x
if note.side == "left" {
x0 -= NOTE-GAP
x0 -= total-w
if lifelines.at(i).level != 0 {
x0 -= LIFELINE-W / 2
}
} else if note.side == "right" {
x0 += NOTE-GAP
x0 += lifelines.at(i).level * LIFELINE-W / 2
} else if note.side == "over" or note.side == "across" {
x0 -= total-w / 2
}
let x1 = x0 + w
let x2 = x0 + total-w
let y0 = y
if note.linked {
y0 += h / 2
}
let y1 = y0 - h
if note.shape == "default" {
shapes += draw.merge-path(
stroke: black + .5pt,
fill: note.color,
close: true,
{
draw.line(
(x0, y0),
(x1, y0),
(x2, y0 - NOTE-CORNER-SIZE),
(x2, y1),
(x0, y1)
)
}
)
shapes += draw.line((x1, y0), (x1, y0 - NOTE-CORNER-SIZE), (x2, y0 - NOTE-CORNER-SIZE), stroke: black + .5pt)
} else if note.shape == "rect" {
shapes += draw.rect(
(x0, y0),
(x2, y1),
stroke: black + .5pt,
fill: note.color
)
} else if note.shape == "hex" {
let lx = x0 + PAD.last()
let rx = x2 - PAD.last()
let my = (y0 + y1) / 2
shapes += draw.merge-path(
stroke: black + .5pt,
fill: note.color,
close: true,
{
draw.line(
(lx, y0),
(rx, y0),
(x2, my),
(rx, y1),
(lx, y1),
(x0, my),
)
}
)
}
shapes += draw.content(
((x0 + x1)/2, (y0 + y1)/2),
note.content,
anchor: "center"
)
if note.aligned-with == none and (note.pos != none or note.side == "across") {
y -= h
}
let r = (y, shapes)
return r
}

361
src/participant.typ Normal file
View File

@ -0,0 +1,361 @@
#import "@preview/cetz:0.3.1": draw
#import "consts.typ": *
#let PAR-SPECIALS = "?[]"
#let SHAPES = (
"participant",
"actor",
"boundary",
"control",
"entity",
"database",
"collections",
"queue",
"custom"
)
#let DEFAULT-COLOR = rgb("#E2E2F0")
#let _par(
name,
display-name: auto,
from-start: true,
invisible: false,
shape: "participant",
color: DEFAULT-COLOR,
custom-image: none,
show-bottom: true,
show-top: true,
) = {
if color == auto {
color = DEFAULT-COLOR
}
return ((
type: "par",
name: name,
display-name: if display-name == auto {name} else {display-name},
from-start: from-start,
invisible: invisible,
shape: shape,
color: color,
custom-image: custom-image,
show-bottom: show-bottom,
show-top: show-top
),)
}
#let _exists(participants, name) = {
if name == "?" or name == "[" or name == "]" {
return true
}
for p in participants {
if name == p.name {
return true
}
}
return false
}
#let get-size(par) = {
if par.invisible {
return (width: 0pt, height: 0pt)
}
let m = measure(box(par.display-name))
let w = m.width
let h = m.height
let (shape-w, shape-h) = (
participant: (w + PAR-PAD.last() * 2, h + PAR-PAD.first() * 2),
actor: (ACTOR-WIDTH * 1pt, ACTOR-WIDTH * 2pt + SYM-GAP * 1pt + h),
boundary: (BOUNDARY-HEIGHT * 2pt, BOUNDARY-HEIGHT * 1pt + SYM-GAP * 1pt + h),
control: (CONTROL-HEIGHT * 1pt, CONTROL-HEIGHT * 1pt + SYM-GAP * 1pt + h),
entity: (ENTITY-HEIGHT * 1pt, ENTITY-HEIGHT * 1pt + 2pt + SYM-GAP * 1pt + h),
database: (DATABASE-WIDTH * 1pt, DATABASE-WIDTH * 4pt / 3 + SYM-GAP * 1pt + h),
collections: (
w + COLLECTIONS-PAD.last() * 2 + calc.abs(COLLECTIONS-DX) * 1pt,
h + COLLECTIONS-PAD.first() * 2 + calc.abs(COLLECTIONS-DY) * 1pt,
),
queue: (
w + QUEUE-PAD.last() * 2 + 3 * (h + QUEUE-PAD.first() * 2) / 4,
h + QUEUE-PAD.first() * 2
),
custom: (
measure(par.custom-image).width,
measure(par.custom-image).height + SYM-GAP * 1pt + h
)
).at(par.shape)
return (
width: calc.max(w, shape-w),
height: calc.max(h, shape-h)
)
}
#let _render-participant(x, y, p, m, bottom) = {
let w = m.width / 1pt
let h = m.height / 1pt
let x0 = x - w / 2 - PAR-PAD.last() / 1pt
let x1 = x + w / 2 + PAR-PAD.last() / 1pt
let y0 = y + h + PAR-PAD.first() / 1pt * 2
if bottom {
y0 = y
}
let y1 = y0 - h - PAR-PAD.first() / 1pt * 2
draw.rect(
(x0, y0),
(x1, y1),
radius: 2pt,
fill: p.color,
stroke: black + .5pt
)
draw.content(
((x0 + x1) / 2, (y0 + y1) / 2),
p.display-name,
anchor: "center"
)
}
#let _render-actor(x, y, p, m, bottom) = {
let w2 = ACTOR-WIDTH / 2
let head-r = ACTOR-WIDTH / 4
let height = ACTOR-WIDTH * 2
let arms-y = height * 0.375
let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + height + SYM-GAP}
draw.circle(
(x, y0 - head-r),
radius: head-r,
fill: p.color,
stroke: black + .5pt
)
draw.line((x, y0 - head-r * 2), (x, y0 - height + w2), stroke: black + .5pt)
draw.line((x - w2, y0 - arms-y), (x + w2, y0 - arms-y), stroke: black + .5pt)
draw.line((x - w2, y0 - height), (x, y0 - height + w2), (x + w2, y0 - height), stroke: black + .5pt)
draw.content(
(x, y),
p.display-name,
anchor: if bottom {"north"} else {"south"}
)
}
#let _render-boundary(x, y, p, m, bottom) = {
let circle-r = BOUNDARY-HEIGHT / 2
let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + BOUNDARY-HEIGHT + SYM-GAP}
let x0 = x - BOUNDARY-HEIGHT
let y1 = y0 - circle-r
let y2 = y0 - BOUNDARY-HEIGHT
draw.circle(
(x + circle-r, y1),
radius: circle-r,
fill: p.color,
stroke: black + .5pt
)
draw.line(
(x0, y0), (x0, y2),
stroke: black + .5pt
)
draw.line(
(x0, y1), (x, y1),
stroke: black + .5pt
)
draw.content(
(x, y),
p.display-name,
anchor: if bottom {"north"} else {"south"}
)
}
#let _render-control(x, y, p, m, bottom) = {
let r = CONTROL-HEIGHT / 2
let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + CONTROL-HEIGHT + SYM-GAP}
draw.circle(
(x, y0 - r),
radius: r,
fill: p.color,
stroke: black + .5pt
)
draw.mark((x, y0), (x - r / 2, y0), symbol: "stealth", fill: black)
draw.content(
(x, y),
p.display-name,
anchor: if bottom {"north"} else {"south"}
)
}
#let _render-entity(x, y, p, m, bottom) = {
let r = ENTITY-HEIGHT / 2
let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + ENTITY-HEIGHT + SYM-GAP}
let y1 = y0 - ENTITY-HEIGHT - 1.5
draw.circle(
(x, y0 - r),
radius: r,
fill: p.color,
stroke: black + .5pt
)
draw.line(
(x - r, y1),
(x + r, y1),
stroke: black + .5pt
)
draw.content(
(x, y),
p.display-name,
anchor: if bottom {"north"} else {"south"}
)
}
#let _render-database(x, y, p, m, bottom) = {
let height = DATABASE-WIDTH * 4 / 3
let rx = DATABASE-WIDTH / 2
let ry = rx / 2
let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + height + SYM-GAP}
let y1 = y0 - height
draw.merge-path(
close: true,
fill: p.color,
stroke: black + .5pt,
{
draw.bezier((x - rx, y0 - ry), (x, y0), (x - rx, y0 - ry/2), (x - rx/2, y0))
draw.bezier((), (x + rx, y0 - ry), (x + rx/2, y0), (x + rx, y0 - ry/2))
draw.line((), (x + rx, y1 + ry))
draw.bezier((), (x, y1), (x + rx, y1 + ry/2), (x + rx/2, y1))
draw.bezier((), (x - rx, y1 + ry), (x - rx/2, y1), (x - rx, y1 + ry/2))
}
)
draw.merge-path(
stroke: black + .5pt,
{
draw.bezier((x - rx, y0 - ry), (x, y0 - ry*2), (x - rx, y0 - 3*ry/2), (x - rx/2, y0 - ry*2))
draw.bezier((), (x + rx, y0 - ry), (x + rx/2, y0 - ry*2), (x + rx, y0 - 3*ry/2))
}
)
draw.content(
(x, y),
p.display-name,
anchor: if bottom {"north"} else {"south"}
)
}
#let _render-collections(x, y, p, m, bottom) = {
let w = m.width / 1pt
let h = m.height / 1pt
let dx = COLLECTIONS-DX
let dy = COLLECTIONS-DY
let total-w = w + PAR-PAD.last() * 2 / 1pt + calc.abs(dx)
let total-h = h + PAR-PAD.first() * 2 / 1pt + calc.abs(dy)
let x0 = x - total-w / 2
let x1 = x0 + calc.abs(dx)
let x3 = x0 + total-w
let x2 = x3 - calc.abs(dx)
let y0 = if bottom {y} else {y + total-h}
let y1 = y0 - calc.abs(dy)
let y3 = y0 - total-h
let y2 = y3 + calc.abs(dy)
let r1 = (x1, y0, x3, y2)
let r2 = (x0, y1, x2, y3)
if dx < 0 {
r1.at(0) = x0
r1.at(2) = x2
r2.at(0) = x1
r2.at(2) = x3
}
if dy < 0 {
r1.at(1) = y1
r1.at(3) = y3
r2.at(1) = y0
r2.at(3) = y2
}
draw.rect(
(r1.at(0), r1.at(1)),
(r1.at(2), r1.at(3)),
fill: p.color,
stroke: black + .5pt
)
draw.rect(
(r2.at(0), r2.at(1)),
(r2.at(2), r2.at(3)),
fill: p.color,
stroke: black + .5pt
)
draw.content(
((r2.at(0) + r2.at(2)) / 2, (r2.at(1) + r2.at(3)) / 2),
p.display-name,
anchor: "center"
)
}
#let _render-queue(x, y, p, m, bottom) = {
let w = (m.width + QUEUE-PAD.last() * 2) / 1pt
let h = (m.height + QUEUE-PAD.first() * 2) / 1pt
let total-h = h
let ry = total-h / 2
let rx = ry / 2
let total-w = w + 3 + 3 * rx
let x0 = x - total-w / 2
let y0 = if bottom {y} else {y + total-h}
let y1 = y0 - total-h
let x-left = x0 + rx
let x-right = x-left + w + rx
draw.merge-path(
close: true,
fill: p.color,
stroke: black + .5pt,
{
draw.bezier((x-right, y0), (x-right + rx, y0 - ry), (x-right + rx/2, y0), (x-right + rx, y0 - ry/2))
draw.bezier((), (x-right, y1), (x-right + rx, y1 + ry/2), (x-right + rx/2, y1))
draw.line((), (x-left, y1))
draw.bezier((), (x-left - rx, y0 - ry), (x-left - rx/2, y1), (x-left - rx, y1 + ry/2))
draw.bezier((), (x-left, y0), (x-left - rx, y0 - ry/2), (x-left - rx/2, y0))
}
)
draw.merge-path(
stroke: black + .5pt,
{
draw.bezier((x-right, y0), (x-right - rx, y0 - ry), (x-right - rx/2, y0), (x-right - rx, y0 - ry/2))
draw.bezier((), (x-right, y1), (x-right - rx, y1 + ry/2), (x-right - rx/2, y1))
}
)
draw.content(
((x-left + x-right - rx) / 2, y0 - ry),
p.display-name,
anchor: "center"
)
}
#let _render-custom(x, y, p, m, bottom) = {
let image-m = measure(p.custom-image)
let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + image-m.height / 1pt + SYM-GAP}
draw.content((x - image-m.width / 2pt, y0), p.custom-image, anchor: "north-west")
draw.content(
(x, y),
p.display-name,
anchor: if bottom {"north"} else {"south"}
)
}
#let render(x-pos, p, y: 0, bottom: false) = {
let m = measure(box(p.display-name))
let func = (
participant: _render-participant,
actor: _render-actor,
boundary: _render-boundary,
control: _render-control,
entity: _render-entity,
database: _render-database,
collections: _render-collections,
queue: _render-queue,
custom: _render-custom,
).at(p.shape)
func(x-pos.at(p.i), y, p, m, bottom)
}

View File

@ -1,20 +1,36 @@
#import "@preview/cetz:0.2.2": canvas, draw
#import "@preview/cetz:0.3.1": canvas, draw
#import "utils.typ": get-participants-i, get-style, normalize-units
#import "group.typ"
#import "participant.typ"
#import participant: PAR-SPECIALS
#import "sequence.typ"
#import "separator.typ"
#import "sync.typ"
#import "consts.typ": *
#import "note.typ" as note: get-note-box
#let Y-SPACE = 20
#let PAR-PAD = (5pt, 3pt)
#let PAR-SPACE = 10
#let get-participants-i(participants) = {
let pars-i = (:)
for (i, p) in participants.enumerate() {
pars-i.insert(p.name, i)
}
return pars-i
}
#let DEBUG-INVISIBLE = false
#let get-columns-width(participants, elements) = {
participants = participants.map(p => {
p.insert("lifeline-lvl", 0)
p.insert("max-lifelines", 0)
p
})
let pars-i = get-participants-i(participants)
let cells = ()
// Unwrap syncs
let i = 0
while i < elements.len() {
let elmt = elements.at(i)
if elmt.type == "sync" {
elements = elements.slice(0, i + 1) + elmt.elmts + elements.slice(i + 1)
}
i += 1
}
// Compute max lifeline levels
for elmt in elements {
if elmt.type == "seq" {
let com = if elmt.comment == none {""} else {elmt.comment}
@ -28,26 +44,134 @@
cell: box(com, inset: 3pt)
)
)
if elmt.disable-src or elmt.destroy-src {
let p = participants.at(i1)
p.lifeline-lvl -= 1
participants.at(i1) = p
}
if elmt.disable-dst {
let p = participants.at(i2)
p.lifeline-lvl -= 1
participants.at(i2) = p
}
if elmt.enable-dst {
let p = participants.at(i2)
p.lifeline-lvl += 1
p.max-lifelines = calc.max(p.max-lifelines, p.lifeline-lvl)
participants.at(i2) = p
}
} else if elmt.type == "evt" {
let par-name = elmt.participant
let i = pars-i.at(par-name)
let par = participants.at(i)
if elmt.event == "disable" or elmt.event == "destroy" {
par.lifeline-lvl -= 1
} else if elmt.event == "enable" {
par.lifeline-lvl += 1
par.max-lifelines = calc.max(par.max-lifelines, par.lifeline-lvl)
}
participants.at(i) = par
} else if elmt.type == "note" {
let (p1, p2) = (none, none)
let cell = none
if elmt.side == "left" {
p1 = "["
p2 = elmt.pos
cell = get-note-box(elmt)
} else if elmt.side == "right" {
p1 = elmt.pos
p2 = "]"
cell = get-note-box(elmt)
} else if elmt.side == "over" {
if elmt.aligned-with != none {
let box1 = get-note-box(elmt)
let box2 = get-note-box(elmt.aligned-with)
let m1 = measure(box1)
let m2 = measure(box2)
cell = box(width: (m1.width + m2.width) / 2, height: calc.max(m1.height, m2.height))
p1 = elmt.pos
p2 = elmt.aligned-with.pos
}
}
if p1 != none and p2 != none and cell != none {
let i1 = pars-i.at(p1)
let i2 = pars-i.at(p2)
cells.push(
(
elmt: elmt,
i1: calc.min(i1, i2),
i2: calc.max(i1, i2),
cell: cell
)
)
}
}
}
// Compute column widths
// Compute minimum widths for participant names and shapes
let widths = ()
for i in range(participants.len() - 1) {
let p1 = participants.at(i)
let p2 = participants.at(i + 1)
let w1 = measure(box(p1.display-name)).width + PAR-PAD.last() * 2
let w2 = measure(box(p2.display-name)).width + PAR-PAD.last() * 2
let m1 = participant.get-size(p1)
let m2 = participant.get-size(p2)
let w1 = m1.width
let w2 = m2.width
widths.push(w1 / 2pt + w2 / 2pt + PAR-SPACE)
}
// Compute minimum width for over notes
for n in elements.filter(e => (e.type == "note" and
e.side == "over" and
type(e.pos) == str)) {
let m = note.get-size(n)
let i = pars-i.at(n.pos)
if i < widths.len() {
widths.at(i) = calc.max(
widths.at(i),
m.width / 2 + NOTE-GAP
)
}
if i > 0 {
widths.at(i - 1) = calc.max(
widths.at(i - 1),
m.width / 2 + NOTE-GAP
)
}
}
// Compute minimum width for simple sequences (spanning 1 column)
for cell in cells.filter(c => c.i2 - c.i1 == 1) {
let m = measure(cell.cell)
widths.at(cell.i1) = calc.max(
widths.at(cell.i1),
m.width / 1pt
m.width / 1pt + COMMENT-PAD
)
}
// Compute minimum width for self sequences
for cell in cells.filter(c => c.elmt.type == "seq" and c.i1 == c.i2) {
let m = measure(cell.cell)
let i = cell.i1
if cell.elmt.flip {
i -= 1
}
if 0 <= i and i < widths.len() {
widths.at(i) = calc.max(
widths.at(i),
m.width / 1pt + COMMENT-PAD
)
}
}
// Compute remaining widths for longer sequences (spanning multiple columns)
let multicol-cells = cells.filter(c => c.i2 - c.i1 > 1)
multicol-cells = multicol-cells.sorted(key: c => {
c.i1 * 1000 + c.i2
@ -56,82 +180,343 @@
let m = measure(cell.cell)
widths.at(cell.i2 - 1) = calc.max(
widths.at(cell.i2 - 1),
m.width / 1pt - widths.slice(0, cell.i2 - 1).sum()
m.width / 1pt + COMMENT-PAD - widths.slice(cell.i1, cell.i2 - 1).sum()
)
}
// Add lifeline widths
for (i, w) in widths.enumerate() {
let p1 = participants.at(i)
let p2 = participants.at(i + 1)
let w = w + p1.max-lifelines * LIFELINE-W / 2
if p2.max-lifelines != 0 {
w += LIFELINE-W / 2
}
widths.at(i) = w
}
for elmt in elements {
if elmt.type == "col" {
let i1 = pars-i.at(elmt.p1)
let i2 = pars-i.at(elmt.p2)
if calc.abs(i1 - i2) != 1 {
let i-min = calc.min(i1, i2)
let i-max = calc.max(i1, i2)
let others = pars-i.pairs()
.sorted(key: p => p.last())
.slice(i-min + 1, i-max)
.map(p => "'" + p.first() + "'")
.join(", ")
panic(
"Column participants must be consecutive (participants (" +
others +
") are in between)"
)
}
let i = calc.min(i1, i2)
if elmt.width != auto {
widths.at(i) = normalize-units(elmt.width)
}
let width = widths.at(i)
width = calc.max(width, normalize-units(elmt.min-width))
if elmt.max-width != none {
width = calc.min(width, normalize-units(elmt.max-width))
}
widths.at(i) = width + normalize-units(elmt.margin)
}
}
return widths
}
#let render(participants, elements) = context canvas(length: 1pt, {
let shapes = ()
let pars-i = get-participants-i(participants)
let widths = get-columns-width(participants, elements)
// Compute each column's X position
let x-pos = (0,)
for width in widths {
x-pos.push(x-pos.last() + width)
}
let draw-seq = sequence.render.with(pars-i, x-pos, participants)
let draw-group = group.render.with()
let draw-else = group.render-else.with()
let draw-sep = separator.render.with(x-pos)
let draw-par = participant.render.with(x-pos)
let draw-note = note.render.with(pars-i, x-pos)
let draw-sync = sync.render.with(pars-i, x-pos, participants)
// Draw participants
for (i, p) in participants.enumerate() {
draw.content(
(x-pos.at(i), 0),
p.display-name,
name: p.name,
frame: "rect",
padding: PAR-PAD,
anchor: "south"
)
// Draw participants (start)
for p in participants {
if p.from-start and not p.invisible and p.show-top {
shapes += draw-par(p)
}
}
let y = -Y-SPACE
// Draw sequences
let y = 0
let groups = ()
let lifelines = participants.map(_ => (
level: 0,
lines: ()
))
// Draw elemnts
for elmt in elements {
// Sequences
if elmt.type == "seq" {
let x1 = x-pos.at(pars-i.at(elmt.p1))
let x2 = x-pos.at(pars-i.at(elmt.p2))
let style = (
mark: (end: "straight"),
stroke: (
dash: if elmt.dashed {"dashed"} else {"solid"},
paint: elmt.color
let shps
(y, lifelines, shps) = draw-seq(elmt, y, lifelines)
shapes += shps
// Groups (start) -> reserve space for labels + store position
} else if elmt.type == "grp" {
y -= Y-SPACE
let m = measure(
box(
elmt.name,
inset: (left: 5pt, right: 5pt, top: 3pt, bottom: 3pt),
)
)
groups = groups.map(g => {
if g.at(1).min-i == elmt.min-i { g.at(2) += 1 }
if g.at(1).max-i == elmt.max-i { g.at(3) += 1 }
g
})
if elmt.grp-type == "alt" {
elmt.insert("elses", ())
}
groups.push((y, elmt, 0, 0))
y -= m.height / 1pt
// Groups (end) -> actual drawing
} else if elmt.type == "grp-end" {
y -= Y-SPACE
let (start-y, group, start-lvl, end-lvl) = groups.pop()
let x0 = x-pos.at(group.min-i) - start-lvl * 10 - 20
let x1 = x-pos.at(group.max-i) + end-lvl * 10 + 20
shapes += draw-group(x0, x1, start-y, y, group)
draw.line(
(x1, y),
(x2, y),
..style
)
if elmt.comment != none {
draw.content(
(calc.min(x1, x2), y),
elmt.comment,
anchor: "south-west",
padding: 3pt
if group.grp-type == "alt" {
for (else-y, else-elmt) in group.elses {
shapes += draw-else(x0, x1, else-y, else-elmt)
}
}
// Alt's elses -> reserve space for label + store position
} else if elmt.type == "else" {
y -= Y-SPACE
let m = measure(text([\[#elmt.desc\]], weight: "bold", size: .8em))
groups.last().at(1).elses.push((
y, elmt
))
y -= m.height / 1pt
// Separator
} else if elmt.type == "sep" {
let shps
(y, shps) = draw-sep(elmt, y)
shapes += shps
// Gap
} else if elmt.type == "gap" {
y -= elmt.size
// Delay
} else if elmt.type == "delay" {
let y0 = y
let y1 = y - elmt.size
for (i, line) in lifelines.enumerate() {
line.lines.push(("delay-start", y0))
line.lines.push(("delay-end", y1))
lifelines.at(i) = line
}
if elmt.name != none {
let x0 = x-pos.first()
let x1 = x-pos.last()
shapes += draw.content(
((x0 + x1) / 2, (y0 + y1) / 2),
anchor: "center",
elmt.name
)
}
y -= Y-SPACE
y = y1
// Event
} else if elmt.type == "evt" {
let par-name = elmt.participant
let i = pars-i.at(par-name)
let par = participants.at(i)
let line = lifelines.at(i)
if elmt.event == "disable" {
line.level -= 1
line.lines.push(("disable", y))
} else if elmt.event == "destroy" {
line.lines.push(("destroy", y))
} else if elmt.event == "enable" {
line.level += 1
line.lines.push(("enable", y, elmt.lifeline-style))
} else if elmt.event == "create" {
y -= CREATE-OFFSET
shapes += participant.render(x-pos, par, y: y)
line.lines.push(("create", y))
}
lifelines.at(i) = line
// Note
} else if elmt.type == "note" {
if not elmt.linked {
if not elmt.aligned {
y -= Y-SPACE
}
let shps
(y, shps) = draw-note(elmt, y, lifelines)
shapes += shps
}
// Synched sequences
} else if elmt.type == "sync" {
let shps
(y, lifelines, shps) = draw-sync(elmt, y, lifelines)
shapes += shps
}
}
// Draw vertical lines + end participants
draw.on-layer(-1, {
for (i, p) in participants.enumerate() {
let x = x-pos.at(i)
y -= Y-SPACE
// Draw vertical lines + lifelines + end participants
shapes += draw.on-layer(-1, {
if DEBUG-INVISIBLE {
for p in participants.filter(p => p.invisible) {
let color = if p.name.starts-with("?") {green} else if p.name.ends-with("?") {red} else {blue}
let x = x-pos.at(p.i)
draw.line(
(x, 0),
(x, y),
stroke: (paint: color, dash: "dotted")
)
draw.content(
(x, 0),
p.display-name,
anchor: "west",
angle: 90deg
)
}
}
for p in participants.filter(p => not p.invisible) {
let x = x-pos.at(p.i)
// Draw vertical line
let last-y = 0
let rects = ()
let destructions = ()
let lines = ()
// Compute lifeline rectangles + destruction positions
for line in lifelines.at(p.i).lines {
let event = line.first()
if event == "create" {
last-y = line.at(1)
} else if event == "enable" {
if lines.len() == 0 {
draw.line(
(x, last-y),
(x, line.at(1)),
stroke: (
dash: "dashed",
paint: gray.darken(40%),
thickness: .5pt
)
)
}
lines.push(line)
} else if event == "disable" or event == "destroy" {
let lvl = 0
if lines.len() != 0 {
let l = lines.pop()
lvl = lines.len()
rects.push((
x + lvl * LIFELINE-W / 2,
l.at(1),
line.at(1),
l.at(2)
))
last-y = line.at(1)
}
if event == "destroy" {
destructions.push((x + lvl * LIFELINE-W / 2, line.at(1)))
}
} else if event == "delay-start" {
draw.line(
(x, last-y),
(x, line.at(1)),
stroke: (
dash: "dashed",
paint: gray.darken(40%),
thickness: .5pt
)
)
last-y = line.at(1)
} else if event == "delay-end" {
draw.line(
(x, last-y),
(x, line.at(1)),
stroke: (
dash: "loosely-dotted",
paint: gray.darken(40%),
thickness: .8pt
)
)
last-y = line.at(1)
}
}
draw.line(
(x, 0),
(x, last-y),
(x, y),
stroke: (dash: "dashed", paint: gray.darken(40%))
)
draw.content(
(x, y),
p.display-name,
name: p.name,
frame: "rect",
padding: PAR-PAD,
anchor: "north"
stroke: (
dash: "dashed",
paint: gray.darken(40%),
thickness: .5pt
)
)
// Draw lifeline rectangles (reverse for bottom to top)
for rect in rects.rev() {
let (cx, y0, y1, style) = rect
let style = get-style("lifeline", style)
draw.rect(
(cx - LIFELINE-W / 2, y0),
(cx + LIFELINE-W / 2, y1),
..style
)
}
// Draw lifeline destructions
for dest in destructions {
let (cx, cy) = dest
draw.line((cx - 8, cy - 8), (cx + 8, cy + 8), stroke: COL-DESTRUCTION + 2pt)
draw.line((cx - 8, cy + 8), (cx + 8, cy - 8), stroke: COL-DESTRUCTION + 2pt)
}
// Draw participants (end)
if p.show-bottom {
draw-par(p, y: y, bottom: true)
}
}
})
shapes
})

63
src/separator.typ Normal file
View File

@ -0,0 +1,63 @@
#import "@preview/cetz:0.3.1": draw
#import "consts.typ": *
#let _sep(name) = {
return ((
type: "sep",
name: name
),)
}
#let _delay(name: none, size: 30) = {
return ((
type: "delay",
name: name,
size: size
),)
}
#let render(x-pos, elmt, y) = {
let shapes = ()
y -= Y-SPACE
let x0 = x-pos.first() - 20
let x1 = x-pos.last() + 20
let m = measure(
box(
elmt.name,
inset: (left: 3pt, right: 3pt, top: 5pt, bottom: 5pt)
)
)
let w = m.width / 1pt
let h = m.height / 1pt
let cx = (x0 + x1) / 2
let xl = cx - w / 2
let xr = cx + w / 2
y -= h / 2
shapes += draw.rect(
(x0, y),
(x1, y - 3),
stroke: none,
fill: white
)
shapes += draw.line((x0, y), (x1, y))
//shapes += draw.line((x0, y), (xl, y))
//shapes += draw.line((xr, y), (x1, y))
y -= 3
shapes += draw.line((x0, y), (x1, y))
//shapes += draw.line((x0, y), (xl, y))
//shapes += draw.line((xr, y), (x1, y))
shapes += draw.content(
((x0 + x1) / 2, y + 1.5),
elmt.name,
anchor: "center",
padding: (5pt, 3pt),
frame: "rect",
fill: COL-SEP-NAME
)
y -= h / 2
let r = (y, shapes)
return r
}

387
src/sequence.typ Normal file
View File

@ -0,0 +1,387 @@
#import "@preview/cetz:0.3.1": draw, vector
#import "consts.typ": *
#import "participant.typ"
#import "note.typ"
#let get-arrow-marks(sym, color) = {
if sym == none {
return none
}
if type(sym) == array {
return sym.map(s => get-arrow-marks(s, color))
}
(
"": none,
">": (symbol: ">", fill: color),
">>": (symbol: "straight"),
"\\": (symbol: ">", fill: color, harpoon: true, flip: true),
"\\\\": (symbol: "straight", harpoon: true, flip: true),
"/": (symbol: ">", fill: color, harpoon: true),
"//": (symbol: "straight", harpoon: true),
"x": none,
"o": none,
).at(sym)
}
#let reverse-arrow-mark(mark) = {
if type(mark) == array {
return mark.map(m => reverse-arrow-mark(m))
}
let mark2 = mark
if type(mark) == dictionary and mark.at("harpoon", default: false) {
let flipped = mark.at("flip", default: false)
mark2.insert("flip", not flipped)
}
return mark2
}
#let is-tip-of-type(type_, tip) = {
if type(tip) == str and tip == type_ {
return true
}
if type(tip) == array and tip.contains(type_) {
return true
}
return false
}
#let is-circle-tip = is-tip-of-type.with("o")
#let is-cross-tip = is-tip-of-type.with("x")
#let _seq(
p1,
p2,
comment: none,
comment-align: "left",
dashed: false,
start-tip: "",
end-tip: ">",
color: black,
flip: false,
enable-dst: false,
create-dst: false,
disable-dst: false,
destroy-dst: false,
disable-src: false,
destroy-src: false,
lifeline-style: auto,
slant: none
) = {
return ((
type: "seq",
p1: p1,
p2: p2,
comment: comment,
comment-align: comment-align,
dashed: dashed,
start-tip: start-tip,
end-tip: end-tip,
color: color,
flip: flip,
enable-dst: enable-dst,
create-dst: create-dst,
disable-dst: disable-dst,
destroy-dst: destroy-dst,
disable-src: disable-src,
destroy-src: destroy-src,
lifeline-style: lifeline-style,
slant: slant
),)
}
#let _ret(comment: none) = {
return ((
type: "ret",
comment: comment
),)
}
#let render(pars-i, x-pos, participants, elmt, y, lifelines) = {
let shapes = ()
y -= Y-SPACE
let i1 = pars-i.at(elmt.p1)
let i2 = pars-i.at(elmt.p2)
let width = calc.abs(x-pos.at(i1) - x-pos.at(i2))
let h = 0
let comment = if elmt.comment == none {none} else {
let w = calc.min(width * 1pt, measure(elmt.comment).width)
box(
width: if i1 == i2 {auto} else {w},
elmt.comment
)
}
// Reserve space for comment
if comment != none {
h = calc.max(h, measure(comment).height / 1pt + 6)
}
if "linked-note" in elmt {
h = calc.max(h, note.get-size(elmt.linked-note).height / 2)
}
y -= h
let start-info = (
i: i1,
x: x-pos.at(i1),
y: y,
ll-lvl: lifelines.at(i1).level * LIFELINE-W / 2
)
let end-info = (
i: i2,
x: x-pos.at(i2),
y: y,
ll-lvl: lifelines.at(i2).level * LIFELINE-W / 2
)
let slant = if elmt.slant == auto {
DEFAULT-SLANT
} else if elmt.slant != none {
elmt.slant
} else {
0
}
end-info.y -= slant
if elmt.p1 == elmt.p2 {
end-info.y -= 10
}
if elmt.disable-src {
let src-line = lifelines.at(i1)
src-line.level -= 1
src-line.lines.push(("disable", start-info.y))
lifelines.at(i1) = src-line
}
if elmt.destroy-src {
let src-line = lifelines.at(i1)
src-line.lines.push(("destroy", start-info.y))
lifelines.at(i1) = src-line
}
if elmt.disable-dst {
let dst-line = lifelines.at(i2)
dst-line.level -= 1
dst-line.lines.push(("disable", end-info.y))
lifelines.at(i2) = dst-line
}
if elmt.destroy-dst {
let dst-line = lifelines.at(i2)
dst-line.lines.push(("destroy", end-info.y))
lifelines.at(i2) = dst-line
}
if elmt.enable-dst {
let dst-line = lifelines.at(i2)
dst-line.level += 1
lifelines.at(i2) = dst-line
}
if elmt.create-dst {
let par = participants.at(i2)
let m = measure(box(par.display-name))
let f = if i1 > i2 {-1} else {1}
end-info.x -= (m.width + PAR-PAD.last() * 2) / 2pt * f
shapes += participant.render(x-pos, par, y: end-info.y)
}
end-info.ll-lvl = lifelines.at(i2).level * LIFELINE-W / 2
// Compute left/right position at start/end
start-info.insert("lx", start-info.x)
if start-info.ll-lvl != 0 { start-info.lx -= LIFELINE-W / 2 }
end-info.insert("lx", end-info.x)
if end-info.ll-lvl != 0 { end-info.lx -= LIFELINE-W / 2 }
start-info.insert("rx", start-info.x + start-info.ll-lvl)
end-info.insert("rx", end-info.x + end-info.ll-lvl)
// Choose correct points to link
let x1 = start-info.rx
let x2 = end-info.lx
if (start-info.i > end-info.i) {
x1 = start-info.lx
x2 = end-info.rx
}
let style = (
mark: (
start: get-arrow-marks(elmt.start-tip, elmt.color),
end: get-arrow-marks(elmt.end-tip, elmt.color),
scale: 1.2
),
stroke: (
dash: if elmt.dashed {(2pt,2pt)} else {"solid"},
paint: elmt.color,
thickness: .5pt
)
)
let y0 = start-info.y
if "linked-note" in elmt {
let shps = note.render(pars-i, x-pos, elmt.linked-note, start-info.y, lifelines).last()
shapes += shps
}
let flip-mark = end-info.i <= start-info.i
if elmt.flip {
flip-mark = not flip-mark
}
if flip-mark {
style.mark.end = reverse-arrow-mark(style.mark.end)
}
let pts
let comment-pt
let comment-anchor
let comment-angle = 0deg
if elmt.p1 == elmt.p2 {
if elmt.flip {
x1 = start-info.lx
} else {
x2 = end-info.rx
}
let x-mid = if elmt.flip {
calc.min(x1, x2) - 20
} else {
calc.max(x1, x2) + 20
}
pts = (
(x1, start-info.y),
(x-mid, start-info.y),
(x-mid, end-info.y),
(x2, end-info.y)
)
if comment != none {
comment-anchor = (
start: if x-mid < x1 {"south-east"} else {"south-west"},
end: if x-mid < x1 {"south-west"} else {"south-east"},
left: "south-west",
right: "south-east",
center: "south",
).at(elmt.comment-align)
comment-pt = (
start: pts.first(),
end: pts.at(1),
left: if x-mid < x1 {pts.at(1)} else {pts.first()},
right: if x-mid < x1 {pts.first()} else {pts.at(1)},
center: (pts.first(), 50%, pts.at(1))
).at(elmt.comment-align)
}
} else {
pts = (
(x1, start-info.y),
(x2, end-info.y)
)
if comment != none {
let start-pt = pts.first()
let end-pt = pts.last()
if elmt.start-tip != "" {
start-pt = (pts.first(), COMMENT-PAD, pts.last())
}
if elmt.end-tip != "" {
end-pt = (pts.last(), COMMENT-PAD, pts.first())
}
comment-pt = (
start: start-pt,
end: end-pt,
left: if x2 < x1 {end-pt} else {start-pt},
right: if x2 < x1 {start-pt} else {end-pt},
center: (start-pt, 50%, end-pt)
).at(elmt.comment-align)
comment-anchor = (
start: if x2 < x1 {"south-east"} else {"south-west"},
end: if x2 < x1 {"south-west"} else {"south-east"},
left: "south-west",
right: "south-east",
center: "south",
).at(elmt.comment-align)
}
let (p1, p2) = pts
if x2 < x1 {
(p1, p2) = (p2, p1)
}
comment-angle = vector.angle2(p1, p2)
}
// Start circle tip
if is-circle-tip(elmt.start-tip) {
shapes += draw.circle(pts.first(), radius: CIRCLE-TIP-RADIUS, stroke: none, fill: elmt.color, name: "_circle-start-tip")
pts.at(0) = "_circle-start-tip"
// Start cross tip
} else if is-cross-tip(elmt.start-tip) {
let size = CROSS-TIP-SIZE
let cross-pt = (pts.first(), size * 2, pts.at(1))
shapes += draw.line(
(rel: (-size, -size), to: cross-pt),
(rel: (size, size), to: cross-pt),
stroke: elmt.color + 1.5pt
)
shapes += draw.line(
(rel: (-size, size), to: cross-pt),
(rel: (size, -size), to: cross-pt),
stroke: elmt.color + 1.5pt
)
pts.at(0) = cross-pt
}
// End circle tip
if is-circle-tip(elmt.end-tip) {
shapes += draw.circle(pts.last(), radius: 3, stroke: none, fill: elmt.color, name: "_circle-end-tip")
pts.at(pts.len() - 1) = "_circle-end-tip"
// End cross tip
} else if is-cross-tip(elmt.end-tip) {
let size = CROSS-TIP-SIZE
let cross-pt = (pts.last(), size * 2, pts.at(pts.len() - 2))
shapes += draw.line(
(rel: (-size, -size), to: cross-pt),
(rel: (size, size), to: cross-pt),
stroke: elmt.color + 1.5pt
)
shapes += draw.line(
(rel: (-size, size), to: cross-pt),
(rel: (size, -size), to: cross-pt),
stroke: elmt.color + 1.5pt
)
pts.at(pts.len() - 1) = cross-pt
}
shapes += draw.line(..pts, ..style)
if comment != none {
shapes += draw.content(
comment-pt,
comment,
anchor: comment-anchor,
angle: comment-angle,
padding: 3pt
)
}
if elmt.create-dst {
let dst-line = lifelines.at(i2)
dst-line.lines.push(("create", end-info.y))
lifelines.at(i2) = dst-line
}
if elmt.enable-dst {
let dst-line = lifelines.at(i2)
dst-line.lines.push(("enable", end-info.y, elmt.lifeline-style))
lifelines.at(i2) = dst-line
}
if "linked-note" in elmt {
let m = note.get-size(elmt.linked-note)
end-info.y = calc.min(end-info.y, y0 - m.height / 2)
}
let r = (end-info.y, lifelines, shapes)
return r
}

27
src/sync.typ Normal file
View File

@ -0,0 +1,27 @@
#import "sequence.typ"
#let _sync(elmts) = {
return ((
type: "sync",
elmts: elmts
),)
}
#let render(pars-i, x-pos, participants, elmt, y, lifelines) = {
let draw-seq = sequence.render.with(pars-i, x-pos, participants)
let shapes = ()
let end-y = y
for e in elmt.elmts {
let yi
let shps
(yi, lifelines, shps) = draw-seq(e, y, lifelines)
shapes += shps
end-y = calc.min(end-y, yi)
}
let r = (end-y, lifelines, shapes)
return r
}

79
src/utils.typ Normal file
View File

@ -0,0 +1,79 @@
#let normalize-units(value) = {
if type(value) == int or type(value) == float {
return value
}
if type(value) == length {
return value / 1pt
}
panic("Unsupported type '" + str(type(value)) + "'")
}
#let get-participants-i(participants) = {
let pars-i = (:)
for (i, p) in participants.enumerate() {
pars-i.insert(p.name, i)
}
return pars-i
}
#let get-group-span(participants, group) = {
let min-i = participants.len() - 1
let max-i = 0
let pars-i = get-participants-i(participants)
for elmt in group.elmts {
if elmt.type == "seq" {
let i1 = pars-i.at(elmt.p1)
let i2 = pars-i.at(elmt.p2)
min-i = calc.min(min-i, i1, i2)
max-i = calc.max(max-i, i1, i2)
} else if elmt.type == "grp" {
let (i0, i1) = get-group-span(participants, elmt)
min-i = calc.min(min-i, i0)
max-i = calc.max(max-i, i1)
} else if elmt.type == "sync" {
let (i0, i1) = get-group-span(participants, elmt)
min-i = calc.min(min-i, i0)
max-i = calc.max(max-i, i1)
}
}
return (min-i, max-i)
}
#let get-style(base-name, mods) = {
let style = if base-name == "lifeline" {(
fill: white,
stroke: black + 1pt
)}
if mods == auto {
return style
}
if type(mods) == dictionary {
return style + mods
}
panic("Invalid type for parameter mods, expected auto or dictionary, got " + str(type(mods)))
}
#let fit-canvas(canvas, width: auto) = layout(size => {
let m = measure(canvas)
let w = m.width
let h = m.height
let r = if w == 0pt {0} else {
if width == auto {1}
else if type(width) == length {
width / w
} else {
size.width * width / w
}
}
let new-w = w * r
let new-h = h * r
r *= 100%
box(
width: new-w,
height: new-h,
scale(x: r, y: r, reflow: true, canvas)
)
})

View File

@ -1,7 +1,7 @@
[package]
name = "chronos"
version = "0.0.1"
compiler = "0.11.0"
version = "0.2.0"
compiler = "0.12.0"
repository = "https://git.kb28.ch/HEL/chronos"
entrypoint = "src/lib.typ"
authors = [
@ -11,4 +11,4 @@ categories = ["visualization"]
license = "Apache-2.0"
description = "A package to draw sequence diagrams with CeTZ"
keywords = ["sequence", "diagram", "plantuml"]
exclude = [ "/gallery/*" ]
exclude = [ "gallery", "gallery.bash", "docs" ]