diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
new file mode 100644
index 0000000..ce678b7
--- /dev/null
+++ b/.gitea/workflows/ci.yaml
@@ -0,0 +1,31 @@
+name: CI
+on:
+ push:
+ branches: [ main, dev ]
+ pull_request:
+ branches: [ main, dev ]
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+ container: catthehacker/ubuntu:rust-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install tytanic
+ run: cargo binstall tytanic@0.3.3
+
+ - name: Run test suite
+ run: tt run
+
+ - name: Archive artifacts
+ uses: https://gitea.com/actions/gitea-upload-artifact@v4
+ if: always()
+ with:
+ name: artifacts
+ path: |
+ tests/**/diff/*.png
+ tests/**/out/*.png
+ tests/**/ref/*.png
+ retention-days: 5
\ No newline at end of file
diff --git a/README.md b/README.md
index 08ce598..db751c8 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ This package lets you render sequence diagrams directly in Typst. The following
```typst
-#import "@preview/chronos:0.2.1"
+#import "@preview/chronos:0.3.0"
#chronos.diagram({
import chronos: *
_par("Alice")
diff --git a/docs/notes.typ b/docs/notes.typ
index 3b783eb..3c4b72e 100644
--- a/docs/notes.typ
+++ b/docs/notes.typ
@@ -5,13 +5,15 @@
/// - 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
+/// - allow-overlap (bool): If set to `false`, the note will try to reserve space in the column to avoid overlapping with neighboring participants. If set to `true`, the not will overlap other participants
#let _note(
side,
content,
pos: none,
color: rgb("#FEFFDD"),
shape: "default",
- aligned: false
+ aligned: false,
+ allow-overlap: true
) = {}
/// Accepted values for `shape` argument of @@_note()
diff --git a/docs/participants.typ b/docs/participants.typ
index 30db854..6610c0d 100644
--- a/docs/participants.typ
+++ b/docs/participants.typ
@@ -34,6 +34,7 @@
/// - 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
+/// - line-stroke (stroke): The participant's line style (defaults to a light gray dashed line)
/// - 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
@@ -45,6 +46,11 @@
invisible: false,
shape: "participant",
color: rgb("#E2E2F0"),
+ line-stroke: (
+ dash: "dashed",
+ paint: gray.darken(40%),
+ thickness: .5pt
+ ),
custom-image: none,
show-bottom: true,
show-top: true,
diff --git a/docs/sequences.typ b/docs/sequences.typ
index cc5fc1d..d1f9b37 100644
--- a/docs/sequences.typ
+++ b/docs/sequences.typ
@@ -1,7 +1,8 @@
/// 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) = {}
+/// - lifeline-style (auto, dict): See @@_seq()
+#let _evt(participant, event, lifeline-style: auto) = {}
/// Creates a sequence / message between two participants
/// - p1 (str): Start participant
@@ -21,6 +22,7 @@
/// - 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
+/// - outer-lifeline-connect (bool): If true, enables legacy anchoring, making sequences connect to the leftmost lifeline when arriving from the left side. If false, all connections are made with the latest/rightmost lifeline
/// -> array
#let _seq(
p1,
@@ -39,7 +41,8 @@
disable-src: false,
destroy-src: false,
lifeline-style: auto,
- slant: none
+ slant: none,
+ outer-lifeline-connect: false
) = {}
/// Creates a return sequence
diff --git a/gallery/example1.pdf b/gallery/example1.pdf
index 17fe065..7d10576 100644
Binary files a/gallery/example1.pdf and b/gallery/example1.pdf differ
diff --git a/gallery/example2.pdf b/gallery/example2.pdf
index c13a497..d1927e5 100644
Binary files a/gallery/example2.pdf and b/gallery/example2.pdf differ
diff --git a/gallery/example2.typ b/gallery/example2.typ
index 9a49d5d..1c41e87 100644
--- a/gallery/example2.typ
+++ b/gallery/example2.typ
@@ -4,7 +4,7 @@
#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("A", "B", comment: [#sym.quote.chevron.l createRequest #sym.quote.chevron.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)
@@ -15,7 +15,7 @@
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("A", "B", comment: [#sym.quote.chevron.l createRequest #sym.quote.chevron.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)
})
diff --git a/gallery/example3.pdf b/gallery/example3.pdf
index b6ee5b1..db0d82b 100644
Binary files a/gallery/example3.pdf and b/gallery/example3.pdf differ
diff --git a/gallery/notes.pdf b/gallery/notes.pdf
index df8c796..c47b3ac 100644
Binary files a/gallery/notes.pdf and b/gallery/notes.pdf differ
diff --git a/gallery/readme/lifelines.png b/gallery/readme/lifelines.png
index 2e33758..cc72c36 100644
Binary files a/gallery/readme/lifelines.png and b/gallery/readme/lifelines.png differ
diff --git a/gallery/readme/simple_sequence.png b/gallery/readme/simple_sequence.png
index e55bc0d..eedde29 100644
Binary files a/gallery/readme/simple_sequence.png and b/gallery/readme/simple_sequence.png differ
diff --git a/justfile b/justfile
index de19b92..5ad4926 100644
--- a/justfile
+++ b/justfile
@@ -4,9 +4,22 @@
gallery_dir := "./gallery"
set shell := ["bash", "-uc"]
+@version:
+ echo $'\e[1mTypst:\e[0m'
+ typst --version
+ echo
+ echo $'\e[1mTytanic:\e[0m'
+ tt util about
+
manual:
typst c manual.typ manual.pdf
gallery:
for f in "{{gallery_dir}}"/*.typ; do typst c --root . "$f" "${f%typ}pdf"; done
- for f in "{{gallery_dir}}"/readme/*.typ; do typst c --root . "$f" "${f%typ}png"; done
\ No newline at end of file
+ for f in "{{gallery_dir}}"/readme/*.typ; do typst c --root . "$f" "${f%typ}png"; done
+
+test *filter:
+ tt run {{filter}}
+
+update-test *filter:
+ tt update {{filter}}
\ No newline at end of file
diff --git a/manual.pdf b/manual.pdf
index b3a1bf0..8729fb0 100644
Binary files a/manual.pdf and b/manual.pdf differ
diff --git a/manual.typ b/manual.typ
index 65dc6e5..2876163 100644
--- a/manual.typ
+++ b/manual.typ
@@ -71,7 +71,7 @@ $import
= Examples
-You can find the following examples and more in the #link("https://git.kb28.ch/HEL/circuiteria/src/branch/main/gallery")[gallery] directory
+You can find the following examples and more in the #link("https://git.kb28.ch/HEL/chronos/src/branch/main/gallery")[gallery] directory
== Some groups and sequences
diff --git a/src/cetz.typ b/src/cetz.typ
new file mode 100644
index 0000000..670e565
--- /dev/null
+++ b/src/cetz.typ
@@ -0,0 +1 @@
+#import "@preview/cetz:0.4.2": *
\ No newline at end of file
diff --git a/src/core/draw/delay.typ b/src/core/draw/delay.typ
new file mode 100644
index 0000000..3b0a000
--- /dev/null
+++ b/src/core/draw/delay.typ
@@ -0,0 +1,28 @@
+#import "/src/cetz.typ": draw
+
+#import "/src/core/utils.typ": get-ctx, set-ctx
+
+#let render(delay) = get-ctx(ctx => {
+ let y0 = ctx.y
+ let y1 = ctx.y - delay.size
+ for (i, line) in ctx.lifelines.enumerate() {
+ line.lines.push(("delay-start", y0))
+ line.lines.push(("delay-end", y1))
+ ctx.lifelines.at(i) = line
+ }
+ if delay.name != none {
+ let x0 = ctx.x-pos.first()
+ let x1 = ctx.x-pos.last()
+ draw.content(
+ ((x0 + x1) / 2, (y0 + y1) / 2),
+ anchor: "center",
+ delay.name
+ )
+ }
+ ctx.y = y1
+ set-ctx(c => {
+ c.y = ctx.y
+ c.lifelines = ctx.lifelines
+ return c
+ })
+})
\ No newline at end of file
diff --git a/src/core/draw/event.typ b/src/core/draw/event.typ
new file mode 100644
index 0000000..bc39297
--- /dev/null
+++ b/src/core/draw/event.typ
@@ -0,0 +1,31 @@
+#import "/src/consts.typ": *
+#import "/src/core/utils.typ": get-ctx, set-ctx
+
+#let render(evt) = get-ctx(ctx => {
+ let par-name = evt.participant
+ let i = ctx.pars-i.at(par-name)
+ let par = ctx.participants.at(i)
+ let line = ctx.lifelines.at(i)
+ let entry = (evt.event, ctx.y)
+
+ if evt.event == "disable" {
+ line.level -= 1
+ } else if evt.event == "enable" {
+ line.level += 1
+ entry.push(evt.lifeline-style)
+ } else if evt.event == "create" {
+ ctx.y -= CREATE-OFFSET
+ entry.at(1) = ctx.y
+ (par.draw)(par, y: ctx.y)
+ } else if evt.event == "destroy" {
+ } else {
+ panic("Unknown event '" + evt.event + "'")
+ }
+
+ line.lines.push(entry)
+ set-ctx(c => {
+ c.lifelines.at(i) = line
+ c.y = ctx.y
+ return c
+ })
+})
\ No newline at end of file
diff --git a/src/core/draw/group.typ b/src/core/draw/group.typ
new file mode 100644
index 0000000..a182594
--- /dev/null
+++ b/src/core/draw/group.typ
@@ -0,0 +1,152 @@
+#import "/src/cetz.typ": draw
+
+#import "/src/consts.typ": *
+#import "/src/core/utils.typ": get-ctx, set-ctx, expand-parent-group
+
+#let display-name(name) = text(name, weight: "bold")
+#let display-desc(desc) = text([\[#desc\]], weight: "bold", size: .8em)
+
+#let render-start(grp) = get-ctx(ctx => {
+ let grp = grp
+ ctx.y -= Y-SPACE
+ let m = measure(
+ box(
+ grp.name,
+ inset: (
+ left: 5pt,
+ right: 5pt,
+ top: 3pt,
+ bottom: 3pt
+ ),
+ )
+ )
+ ctx.groups = ctx.groups.map(g => {
+ if g.group.min-i == grp.min-i { g.start-lvl += 1 }
+ if g.group.max-i == grp.max-i { g.end-lvl += 1 }
+ g
+ })
+ if grp.grp-type == "alt" {
+ grp.insert("elses", ())
+ }
+ ctx.groups.push((
+ start-y: ctx.y,
+ group: grp,
+ start-lvl: 0,
+ end-lvl: 0,
+ min-x: ctx.x-pos.at(grp.min-i) - 10,
+ max-x: ctx.x-pos.at(grp.max-i) + 10
+ ))
+ ctx.y -= m.height / 1pt
+
+ set-ctx(c => {
+ c.y = ctx.y
+ c.groups = ctx.groups
+ return c
+ })
+})
+
+
+#let draw-group(x0, x1, y0, y1, group) = {
+ let name = display-name(group.name)
+ let m = measure(name)
+ let w = m.width / 1pt + 15
+ let h = m.height / 1pt + 6
+ draw.rect(
+ (x0, y0),
+ (x1, y1)
+ )
+ draw.line(
+ (x0, y0),
+ (x0 + w, y0),
+ (x0 + w, y0 - h / 2),
+ (x0 + w - 5, y0 - h),
+ (x0, y0 - h),
+ fill: COL-GRP-NAME,
+ close: true
+ )
+ draw.content(
+ (x0, y0),
+ name,
+ anchor: "north-west",
+ padding: (left: 5pt, right: 10pt, top: 3pt, bottom: 3pt)
+ )
+
+ if group.desc != none {
+ draw.content(
+ (x0 + w, y0),
+ display-desc(group.desc),
+ anchor: "north-west",
+ padding: 3pt
+ )
+ }
+}
+
+#let draw-else(x0, x1, y, elmt) = {
+ draw.line(
+ (x0, y),
+ (x1, y),
+ stroke: (dash: (2pt, 1pt), thickness: .5pt)
+ )
+ draw.content(
+ (x0, y),
+ display-desc(elmt.desc),
+ anchor: "north-west",
+ padding: 3pt
+ )
+}
+
+#let render-end(group) = get-ctx(ctx => {
+ ctx.y -= Y-SPACE
+ let (
+ start-y,
+ group,
+ start-lvl,
+ end-lvl,
+ min-x,
+ max-x
+ ) = ctx.groups.pop()
+ let x0 = min-x - 10
+ let x1 = max-x + 10
+
+ // Fit name and descriptions
+ let name-m = measure(display-name(group.name))
+ let width = name-m.width / 1pt + 15
+ if group.desc != none {
+ let desc-m = measure(display-desc(group.desc))
+ width += desc-m.width / 1pt + 6
+ }
+ if group.grp-type == "alt" {
+ width = calc.max(width, ..group.elses.map(e => {
+ let elmt = e.at(1)
+ let desc-m = measure(display-desc(elmt.desc))
+ return desc-m.width / 1pt + 6
+ }))
+ }
+ x1 = calc.max(x1, x0 + width + 3)
+
+ draw-group(x0, x1, start-y, ctx.y, group)
+
+ if group.grp-type == "alt" {
+ for (else-y, else-elmt) in group.elses {
+ draw-else(x0, x1, else-y, else-elmt)
+ }
+ }
+
+ set-ctx(c => {
+ c.y = ctx.y
+ c.groups = ctx.groups
+ return c
+ })
+
+ expand-parent-group(x0, x1)
+})
+
+#let render-else(else_) = set-ctx(ctx => {
+ ctx.y -= Y-SPACE
+ let m = measure(text([\[#else_.desc\]], weight: "bold", size: .8em))
+ ctx.groups.last().group.elses.push((
+ ctx.y, else_
+ ))
+ ctx.y -= m.height / 1pt
+ return ctx
+})
\ No newline at end of file
diff --git a/src/core/draw/note.typ b/src/core/draw/note.typ
new file mode 100644
index 0000000..33b33aa
--- /dev/null
+++ b/src/core/draw/note.typ
@@ -0,0 +1,165 @@
+#import "/src/cetz.typ": draw
+
+#import "/src/consts.typ": *
+#import "/src/core/utils.typ": get-ctx, set-ctx, expand-parent-group
+
+#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 get-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 render(note, y: auto, forced: false) = {
+ if not note.linked {
+ if not note.aligned {
+ set-ctx(c => {
+ c.y -= Y-SPACE
+ return c
+ })
+ }
+ } else if not forced {
+ return ()
+ }
+
+ get-ctx(ctx => {
+ let y = y
+ if y == auto {
+ y = ctx.y
+ }
+
+
+ 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(ctx.pars-i, ctx.x-pos, note)
+
+ let i = none
+ if note.pos != none and type(note.pos) == str {
+ i = ctx.pars-i.at(note.pos)
+ }
+ let x0 = base-x
+ if note.side == "left" {
+ x0 -= NOTE-GAP
+ x0 -= total-w
+ if ctx.lifelines.at(i).level != 0 {
+ x0 -= LIFELINE-W / 2
+ }
+ } else if note.side == "right" {
+ x0 += NOTE-GAP
+ x0 += ctx.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" {
+ draw.line(
+ (x0, y0),
+ (x1, y0),
+ (x2, y0 - NOTE-CORNER-SIZE),
+ (x2, y1),
+ (x0, y1),
+ stroke: black + .5pt,
+ fill: note.color,
+ close: true
+ )
+ draw.line(
+ (x1, y0),
+ (x1, y0 - NOTE-CORNER-SIZE),
+ (x2, y0 - NOTE-CORNER-SIZE),
+ stroke: black + .5pt
+ )
+ } else if note.shape == "rect" {
+ 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
+ draw.line(
+ (lx, y0),
+ (rx, y0),
+ (x2, my),
+ (rx, y1),
+ (lx, y1),
+ (x0, my),
+ stroke: black + .5pt,
+ fill: note.color,
+ close: true
+ )
+ }
+
+ 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") {
+ set-ctx(c => {
+ c.y -= h
+ return c
+ })
+ }
+
+ expand-parent-group(x0, x2)
+ })
+}
\ No newline at end of file
diff --git a/src/core/draw/participant.typ b/src/core/draw/participant.typ
new file mode 100644
index 0000000..d7c87e3
--- /dev/null
+++ b/src/core/draw/participant.typ
@@ -0,0 +1,406 @@
+#import "/src/cetz.typ": draw
+
+#import "/src/consts.typ": *
+#import "/src/core/utils.typ": get-ctx, get-style, set-ctx
+
+#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 {"base"}
+ )
+}
+
+#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 {"base"}
+ )
+}
+
+#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 {"base"}
+ )
+}
+
+#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 {"base"}
+ )
+}
+
+#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 {"base"}
+ )
+}
+
+#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 {"base"}
+ )
+}
+
+#let render(par, y: 0, bottom: false) = draw.group(cetz-ctx => {
+ let ctx = cetz-ctx.shared-state.chronos
+ let m = measure(box(par.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(par.shape)
+ func(ctx.x-pos.at(par.i), y, par, m, bottom)
+},)
+
+#let render-lifelines() = get-ctx(ctx => {
+ let participants = ctx.participants
+ for p in participants.filter(p => not p.invisible) {
+ let x = ctx.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 ctx.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: p.line-stroke
+ )
+ }
+ 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: p.line-stroke
+ )
+ 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, last-y),
+ (x, ctx.y),
+ stroke: p.line-stroke
+ )
+
+ // 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 {
+ (p.draw)(p, y: ctx.y, bottom: true)
+ }
+ }
+},)
\ No newline at end of file
diff --git a/src/core/draw/separator.typ b/src/core/draw/separator.typ
new file mode 100644
index 0000000..ecc0db2
--- /dev/null
+++ b/src/core/draw/separator.typ
@@ -0,0 +1,47 @@
+#import "/src/cetz.typ": draw
+
+#import "/src/consts.typ": *
+#import "/src/core/utils.typ": get-ctx, set-ctx
+
+#let render(sep) = get-ctx(ctx => {
+ ctx.y -= Y-SPACE
+
+ let x0 = ctx.x-pos.first() - 20
+ let x1 = ctx.x-pos.last() + 20
+ let m = measure(
+ box(
+ sep.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
+
+ ctx.y -= h / 2
+ draw.rect(
+ (x0, ctx.y),
+ (x1, ctx.y - 3),
+ stroke: none,
+ fill: white
+ )
+ draw.line((x0, ctx.y), (x1, ctx.y))
+ ctx.y -= 3
+ draw.line((x0, ctx.y), (x1, ctx.y))
+ draw.content(
+ ((x0 + x1) / 2, ctx.y + 1.5),
+ sep.name,
+ anchor: "center",
+ padding: (5pt, 3pt),
+ frame: "rect",
+ fill: COL-SEP-NAME
+ )
+ ctx.y -= h / 2
+
+ set-ctx(c => {
+ c.y = ctx.y
+ return c
+ })
+})
diff --git a/src/core/draw/sequence.typ b/src/core/draw/sequence.typ
new file mode 100644
index 0000000..b1f0659
--- /dev/null
+++ b/src/core/draw/sequence.typ
@@ -0,0 +1,404 @@
+#import "/src/cetz.typ": draw, vector, coordinate
+
+#import "note.typ"
+#import "/src/consts.typ": *
+#import "/src/core/utils.typ": get-ctx, set-ctx, expand-parent-group
+
+#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),
+ "\\\\": (symbol: "straight", harpoon: true),
+ "/": (symbol: ">", fill: color, harpoon: true, flip: true),
+ "//": (symbol: "straight", harpoon: true, flip: 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 render(seq) = get-ctx(ctx => {
+ ctx.y -= Y-SPACE
+
+ let i1 = ctx.pars-i.at(seq.p1)
+ let i2 = ctx.pars-i.at(seq.p2)
+ let width = calc.abs(ctx.x-pos.at(i1) - ctx.x-pos.at(i2))
+
+ let h = 0
+ let comment = if seq.comment == none {none} else {
+ let w = calc.min(width * 1pt, measure(seq.comment).width)
+ box(
+ width: if i1 == i2 {auto} else {w},
+ seq.comment
+ )
+ }
+ // Reserve space for comment
+ if comment != none {
+ h = calc.max(h, measure(comment).height / 1pt + 6)
+ }
+ h = calc.max(
+ h,
+ ..seq.linked-notes.map(n => {
+ note.get-size(n).height / 2
+ })
+ )
+ ctx.y -= h
+
+ let start-info = (
+ i: i1,
+ x: ctx.x-pos.at(i1),
+ y: ctx.y,
+ ll-lvl: ctx.lifelines.at(i1).level * LIFELINE-W / 2
+ )
+ let end-info = (
+ i: i2,
+ x: ctx.x-pos.at(i2),
+ y: ctx.y,
+ ll-lvl: ctx.lifelines.at(i2).level * LIFELINE-W / 2
+ )
+ let slant = if seq.slant == auto {
+ DEFAULT-SLANT
+ } else if seq.slant != none {
+ seq.slant
+ } else {
+ 0
+ }
+ end-info.y -= slant
+ if seq.p1 == seq.p2 {
+ end-info.y -= 10
+ }
+
+ if seq.disable-src {
+ let src-line = ctx.lifelines.at(i1)
+ src-line.level -= 1
+ src-line.lines.push(("disable", start-info.y))
+ ctx.lifelines.at(i1) = src-line
+ }
+ if seq.destroy-src {
+ let src-line = ctx.lifelines.at(i1)
+ src-line.lines.push(("destroy", start-info.y))
+ ctx.lifelines.at(i1) = src-line
+ }
+ if seq.disable-dst {
+ let dst-line = ctx.lifelines.at(i2)
+ dst-line.level -= 1
+ dst-line.lines.push(("disable", end-info.y))
+ ctx.lifelines.at(i2) = dst-line
+ }
+ if seq.destroy-dst {
+ let dst-line = ctx.lifelines.at(i2)
+ dst-line.lines.push(("destroy", end-info.y))
+ ctx.lifelines.at(i2) = dst-line
+ }
+ if seq.enable-dst {
+ let dst-line = ctx.lifelines.at(i2)
+ dst-line.level += 1
+ ctx.lifelines.at(i2) = dst-line
+ }
+ if seq.create-dst {
+ let par = ctx.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
+ (par.draw)(par, y: end-info.y)
+ }
+
+ end-info.ll-lvl = ctx.lifelines.at(i2).level * LIFELINE-W / 2
+
+ // Compute left/right position at start/end
+ start-info.insert("rx", start-info.x + start-info.ll-lvl)
+ end-info.insert("rx", end-info.x + end-info.ll-lvl)
+ let start-lx = start-info.x
+ let end-lx = end-info.x
+ if seq.outer-lifeline-connect {
+ if start-info.ll-lvl != 0 {start-lx -= LIFELINE-W / 2}
+ if end-info.ll-lvl != 0 {end-lx -= LIFELINE-W / 2}
+ } else {
+ if start-info.ll-lvl != 0 {start-lx = start-info.rx - LIFELINE-W}
+ if end-info.ll-lvl != 0 {end-lx = end-info.rx - LIFELINE-W}
+ }
+ start-info.insert("lx", start-lx)
+ end-info.insert("lx", end-lx)
+
+ // 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(seq.start-tip, seq.color),
+ end: get-arrow-marks(seq.end-tip, seq.color),
+ scale: 1.2
+ ),
+ stroke: (
+ dash: if seq.dashed {(2pt,2pt)} else {"solid"},
+ paint: seq.color,
+ thickness: .5pt
+ )
+ )
+
+ let y0 = start-info.y
+ for n in seq.linked-notes {
+ (n.draw)(n, y: start-info.y, forced: true)
+ }
+
+ let flip-mark = end-info.i <= start-info.i
+ if seq.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 seq.p1 == seq.p2 {
+ if seq.flip {
+ x1 = start-info.lx
+ } else {
+ x2 = end-info.rx
+ }
+
+ let x-mid = if seq.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(seq.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(seq.comment-align)
+ }
+
+ expand-parent-group(
+ calc.min(x1, x2, x-mid),
+ calc.max(x1, x2, x-mid)
+ )
+
+ } else {
+ pts = (
+ (x1, start-info.y),
+ (x2, end-info.y)
+ )
+
+ if comment != none {
+ let start-pt = pts.first()
+ let end-pt = pts.last()
+ if seq.start-tip != "" {
+ start-pt = (pts.first(), COMMENT-PAD, pts.last())
+ }
+ if seq.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(seq.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(seq.comment-align)
+ }
+
+ let (p1, p2) = pts
+ if x2 < x1 {
+ (p1, p2) = (p2, p1)
+ }
+ comment-angle = vector.angle2(p1, p2)
+
+ expand-parent-group(
+ calc.min(x1, x2),
+ calc.max(x1, x2)
+ )
+ }
+
+ // Start circle tip
+ if is-circle-tip(seq.start-tip) {
+ draw.circle(
+ pts.first(),
+ radius: CIRCLE-TIP-RADIUS,
+ stroke: none,
+ fill: seq.color,
+ name: "_circle-start-tip"
+ )
+ pts.at(0) = "_circle-start-tip"
+
+ // Start cross tip
+ } else if is-cross-tip(seq.start-tip) {
+ let size = CROSS-TIP-SIZE
+ let cross-pt = (
+ pts.first(),
+ size * 2,
+ pts.at(1)
+ )
+ draw.line(
+ (rel: (-size, -size), to: cross-pt),
+ (rel: (size, size), to: cross-pt),
+ stroke: seq.color + 1.5pt
+ )
+ draw.line(
+ (rel: (-size, size), to: cross-pt),
+ (rel: (size, -size), to: cross-pt),
+ stroke: seq.color + 1.5pt
+ )
+ pts.at(0) = cross-pt
+ }
+
+ // End circle tip
+ if is-circle-tip(seq.end-tip) {
+ draw.circle(
+ pts.last(),
+ radius: 3,
+ stroke: none,
+ fill: seq.color,
+ name: "_circle-end-tip"
+ )
+ pts.at(pts.len() - 1) = "_circle-end-tip"
+
+ // End cross tip
+ } else if is-cross-tip(seq.end-tip) {
+ let size = CROSS-TIP-SIZE
+ let cross-pt = (
+ pts.last(),
+ size * 2,
+ pts.at(pts.len() - 2)
+ )
+ draw.line(
+ (rel: (-size, -size), to: cross-pt),
+ (rel: (size, size), to: cross-pt),
+ stroke: seq.color + 1.5pt
+ )
+ draw.line(
+ (rel: (-size, size), to: cross-pt),
+ (rel: (size, -size), to: cross-pt),
+ stroke: seq.color + 1.5pt
+ )
+ pts.at(pts.len() - 1) = cross-pt
+ }
+
+ draw.line(..pts, ..style)
+
+ if comment != none {
+ draw.content(
+ comment-pt,
+ comment,
+ anchor: comment-anchor,
+ angle: comment-angle,
+ padding: 3pt,
+ name: "comment"
+ )
+
+ // TODO: Improve this
+ draw.get-ctx(c => {
+ let (_, left, right) = coordinate.resolve(
+ c,
+ "comment.west",
+ "comment.east"
+ )
+ expand-parent-group(
+ left.at(0),
+ right.at(0)
+ )
+ })
+
+ }
+
+ if seq.create-dst {
+ let dst-line = ctx.lifelines.at(i2)
+ dst-line.lines.push(("create", end-info.y))
+ ctx.lifelines.at(i2) = dst-line
+ }
+ if seq.enable-dst {
+ let dst-line = ctx.lifelines.at(i2)
+ dst-line.lines.push(("enable", end-info.y, seq.lifeline-style))
+ ctx.lifelines.at(i2) = dst-line
+ }
+
+ if seq.linked-notes.len() != 0 {
+ end-info.y = calc.min(
+ end-info.y,
+ y0 - calc.max(..seq.linked-notes.map(n => {
+ let m = note.get-size(n)
+ return m.height / 2
+ }))
+ )
+ }
+
+ set-ctx(c => {
+ c.y = end-info.y
+ c.lifelines = ctx.lifelines
+ c.last-drawn = (
+ type: "seq",
+ start-info: start-info,
+ end-info: end-info
+ )
+ return c
+ })
+})
diff --git a/src/core/draw/sync.typ b/src/core/draw/sync.typ
new file mode 100644
index 0000000..050fe90
--- /dev/null
+++ b/src/core/draw/sync.typ
@@ -0,0 +1,66 @@
+#import "/src/core/utils.typ": get-ctx, is-elmt, set-ctx
+#import "/src/cetz.typ": draw
+
+#let render(sync) = get-ctx(ctx => {
+ set-ctx(c => {
+ c.sync = (
+ ctx: ctx,
+ bottoms: (),
+ starts: (),
+ start-y: ctx.y,
+ align-y: ctx.y
+ )
+ c.in-sync = true
+ return c
+ })
+})
+
+#let in-sync-render(elmt) = {
+ set-ctx(c => {
+ c.y = c.sync.start-y
+ return c
+ })
+ draw.hide({
+ (elmt.draw)(elmt)
+ })
+ set-ctx(c => {
+ c.sync.starts.push(c.last-drawn.start-info.y)
+ c.sync.bottoms.push(c.y)
+ return c
+ })
+}
+
+#let render-end(sync) = get-ctx(ctx => {
+ for e in sync.elmts {
+ assert(is-elmt(e), message: "Sync element can only contain chronos elements, found " + repr(e))
+ assert(
+ e.type == "seq",
+ message: "Sync element can only contain sequences, found '" + e.type + "'"
+ )
+ }
+
+ set-ctx(c => {
+ let new-sync = c.sync
+ if new-sync.starts.len() != 0 {
+ new-sync.align-y = calc.min(..new-sync.starts)
+ }
+ new-sync.remove("ctx")
+ return c.sync.ctx + (sync: new-sync)
+ })
+
+ for (i, e) in sync.elmts.enumerate() {
+ set-ctx(c => {
+ let dy = c.sync.starts.at(i) - c.sync.start-y
+ c.y = c.sync.align-y - dy
+ return c
+ })
+ (e.draw)(e)
+ }
+
+ set-ctx(c => {
+ let heights = c.sync.starts.zip(c.sync.bottoms).map(((s, b)) => b - s)
+ c.y = c.sync.align-y + calc.min(..heights)
+ c.remove("sync")
+ return c
+ })
+})
\ No newline at end of file
diff --git a/src/core/renderer.typ b/src/core/renderer.typ
new file mode 100644
index 0000000..14cf8d5
--- /dev/null
+++ b/src/core/renderer.typ
@@ -0,0 +1,403 @@
+#import "/src/cetz.typ": canvas, draw
+
+#import "draw/note.typ": get-box as get-note-box, get-size as get-note-size
+#import "draw/participant.typ"
+#import "draw/sync.typ": in-sync-render
+#import "utils.typ": *
+#import "/src/consts.typ": *
+
+#let DEBUG-INVISIBLE = false
+
+#let init-lifelines(participants) = {
+ return participants.map(p => {
+ p.insert("lifeline-lvl", 0)
+ p.insert("max-lifelines", 0)
+ p
+ })
+}
+
+#let seq-update-lifelines(participants, pars-i, seq) = {
+ let participants = participants
+ let com = if seq.comment == none {""} else {seq.comment}
+ let i1 = pars-i.at(seq.p1)
+ let i2 = pars-i.at(seq.p2)
+ let cell = (
+ elmt: seq,
+ i1: calc.min(i1, i2),
+ i2: calc.max(i1, i2),
+ cell: box(com, inset: 3pt)
+ )
+
+ if seq.disable-src or seq.destroy-src {
+ let p = participants.at(i1)
+ p.lifeline-lvl -= 1
+ participants.at(i1) = p
+ }
+ if seq.disable-dst {
+ let p = participants.at(i2)
+ p.lifeline-lvl -= 1
+ participants.at(i2) = p
+ }
+ if seq.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
+ }
+
+ return (participants, cell)
+}
+
+#let evt-update-lifelines(participants, pars-i, evt) = {
+ let par-name = evt.participant
+ let i = pars-i.at(par-name)
+ let par = participants.at(i)
+ if evt.event == "disable" or evt.event == "destroy" {
+ par.lifeline-lvl -= 1
+
+ } else if evt.event == "enable" {
+ par.lifeline-lvl += 1
+ par.max-lifelines = calc.max(par.max-lifelines, par.lifeline-lvl)
+ }
+ participants.at(i) = par
+ return participants
+}
+
+#let note-get-cell(pars-i, note) = {
+ let (p1, p2) = (none, none)
+ let cell = none
+ if note.side == "left" {
+ p1 = note.pos2
+ p2 = note.pos
+ cell = get-note-box(note)
+ } else if note.side == "right" {
+ p1 = note.pos
+ p2 = note.pos2
+ cell = get-note-box(note)
+ } else if note.side == "over" and note.aligned-with != none {
+ let box1 = get-note-box(note)
+ let box2 = get-note-box(note.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 = note.pos
+ p2 = note.aligned-with.pos
+ } else {
+ return none
+ }
+
+ let i1 = pars-i.at(p1)
+ let i2 = pars-i.at(p2)
+ cell = (
+ elmt: note,
+ i1: calc.min(i1, i2),
+ i2: calc.max(i1, i2),
+ cell: cell
+ )
+
+ return cell
+}
+
+#let compute-max-lifeline-levels(participants, elements, pars-i) = {
+ let cells = ()
+ for elmt in elements {
+ if elmt.type == "seq" {
+ let cell
+ (participants, cell) = seq-update-lifelines(
+ participants,
+ pars-i,
+ elmt
+ )
+ cells.push(cell)
+ } else if elmt.type == "evt" {
+ participants = evt-update-lifelines(
+ participants,
+ pars-i,
+ elmt
+ )
+
+ } else if elmt.type == "note" {
+ let cell = note-get-cell(pars-i, elmt)
+ if cell != none {
+ cells.push(cell)
+ }
+ }
+ }
+
+ return (participants, elements, cells)
+}
+
+/// Compute minimum widths for participant names and shapes
+#let participants-min-col-widths(participants) = {
+ let widths = ()
+ for i in range(participants.len() - 1) {
+ let p1 = participants.at(i)
+ let p2 = participants.at(i + 1)
+ 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)
+ }
+ return widths
+}
+
+/// Compute minimum width for over notes
+#let notes-min-col-widths(elements, widths, pars-i) = {
+ let widths = widths
+ let notes = elements.filter(e => e.type == "note")
+ for n in notes.filter(e => (e.side == "over" and
+ type(e.pos) == str)) {
+
+ let m = get-note-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
+ )
+ }
+ }
+ return widths
+}
+
+/// Compute minimum width for simple sequences (spanning 1 column)
+#let simple-seq-min-col-widths(cells, widths) = {
+ let widths = widths
+ 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 + COMMENT-PAD
+ )
+ }
+ return widths
+}
+
+/// Compute minimum width for self sequences
+#let self-seq-min-col-widths(cells, widths) = {
+ let widths = widths
+ 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
+ )
+ }
+ }
+ return widths
+}
+
+/// Compute remaining widths for longer sequences (spanning multiple columns)
+#let long-seq-min-col-widths(participants, cells, widths) = {
+ let widths = widths
+ let multicol-cells = cells.filter(c => c.i2 - c.i1 > 1)
+ multicol-cells = multicol-cells.sorted(key: c => {
+ c.i1 * 1000 + c.i2
+ })
+ for cell in multicol-cells {
+ let m = measure(cell.cell)
+
+ let i1 = cell.i1
+ let i2 = cell.i2 - 1
+ let i = i2
+ if cell.i1 == 0 and participants.at(0).name == "[" {
+ i = 0
+ i1 += 1
+ i2 += 1
+ }
+ let width = (
+ m.width / 1pt +
+ COMMENT-PAD -
+ widths.slice(i1, i2).sum()
+ )
+
+ widths.at(i) = calc.max(
+ widths.at(i), width
+ )
+ }
+ return widths
+}
+
+/// Add lifeline widths
+#let col-widths-add-lifelines(participants, widths) = {
+ return widths.enumerate().map(((i, w)) => {
+ let p1 = participants.at(i)
+ let p2 = participants.at(i + 1)
+ w += p1.max-lifelines * LIFELINE-W / 2
+ if p2.max-lifelines != 0 {
+ w += LIFELINE-W / 2
+ }
+ return w
+ })
+}
+
+#let process-col-elements(elements, widths, pars-i) = {
+ let widths = widths
+ let cols = elements.filter(e => e.type == "col")
+ for col in cols {
+ let i1 = pars-i.at(col.p1)
+ let i2 = pars-i.at(col.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)
+
+ let width = widths.at(i)
+
+ if col.width != auto {
+ width = normalize-units(col.width)
+ }
+
+ width = calc.max(
+ width,
+ normalize-units(col.min-width)
+ )
+ if col.max-width != none {
+ width = calc.min(
+ width,
+ normalize-units(col.max-width)
+ )
+ }
+ widths.at(i) = width + normalize-units(col.margin)
+ }
+ return widths
+}
+
+#let compute-columns-width(participants, elements, pars-i) = {
+ elements = elements.filter(is-elmt)
+
+ let cells
+ (participants, elements, cells) = compute-max-lifeline-levels(participants, elements, pars-i)
+
+ let widths = participants-min-col-widths(participants)
+ widths = notes-min-col-widths(elements, widths, pars-i)
+ widths = simple-seq-min-col-widths(cells, widths)
+ widths = self-seq-min-col-widths(cells, widths)
+ widths = long-seq-min-col-widths(participants, cells, widths)
+ widths = col-widths-add-lifelines(participants, widths)
+ widths = process-col-elements(elements, widths, pars-i)
+ return widths
+}
+
+#let setup-ctx(participants, elements) = (ctx => {
+ let state = ctx.at("shared-state", default: (:))
+
+ let chronos-ctx = (
+ participants: init-lifelines(participants),
+ pars-i: get-participants-i(participants),
+ y: 0,
+ groups: (),
+ lifelines: participants.map(_ => (
+ level: 0,
+ lines: ()
+ )),
+ in-sync: false
+ )
+ chronos-ctx.insert(
+ "widths",
+ compute-columns-width(
+ chronos-ctx.participants,
+ elements,
+ chronos-ctx.pars-i
+ )
+ )
+
+ // Compute each column's X position
+ let x-pos = (0,)
+ for width in chronos-ctx.widths {
+ x-pos.push(x-pos.last() + width)
+ }
+ chronos-ctx.insert("x-pos", x-pos)
+ state.insert("chronos", chronos-ctx)
+ ctx.shared-state = state
+ return (
+ ctx: ctx
+ )
+},)
+
+#let render-debug() = get-ctx(ctx => {
+ for p in ctx.participants.filter(p => p.invisible) {
+ let color = if p.name.starts-with("?") {green} else if p.name.ends-with("?") {red} else {blue}
+ let x = ctx.x-pos.at(p.i)
+ draw.line(
+ (x, 0),
+ (x, ctx.y),
+ stroke: (paint: color, dash: "dotted")
+ )
+ draw.content(
+ (x, 0),
+ p.display-name,
+ anchor: "west",
+ angle: 90deg
+ )
+ }
+})
+
+#let render(participants, elements) = context canvas(length: 1pt, {
+ setup-ctx(participants, elements)
+
+ // Draw participants (start)
+ get-ctx(ctx => {
+ for p in ctx.participants {
+ if p.from-start and not p.invisible and p.show-top {
+ (p.draw)(p)
+ }
+ }
+ })
+
+ // Draw elements
+ for elmt in elements {
+ if not is-elmt(elmt) {
+ (elmt,)
+ } else if "draw" in elmt and elmt.type != "par" {
+ get-ctx(ctx => {
+ if ctx.in-sync and elmt.type != "sync-end" {
+ in-sync-render(elmt)
+ } else {
+ (elmt.draw)(elmt)
+ }
+ })
+ }
+ }
+
+ set-ctx(ctx => {
+ ctx.y -= Y-SPACE
+ return ctx
+ })
+
+ draw.on-layer(-1, {
+ if DEBUG-INVISIBLE {
+ render-debug()
+ }
+
+ participant.render-lifelines()
+ })
+})
\ No newline at end of file
diff --git a/src/core/setup.typ b/src/core/setup.typ
new file mode 100644
index 0000000..776b2bb
--- /dev/null
+++ b/src/core/setup.typ
@@ -0,0 +1,358 @@
+#import "draw/group.typ": render-end as grp-render-end
+#import "draw/sync.typ": render-end as sync-render-end
+#import "utils.typ": get-group-span, is-elmt
+#import "/src/participant.typ": _exists as par-exists, _par
+#import "/src/sequence.typ": _seq
+
+#let flatten-group(elmts, i) = {
+ let group = elmts.at(i)
+ elmts.at(i) = group
+ return (
+ elmts.slice(0, i + 1) +
+ group.elmts +
+ ((
+ type: "grp-end",
+ draw: grp-render-end,
+ start-i: i
+ ),) +
+ elmts.slice(i+1)
+ )
+}
+
+#let flatten-sync(elmts, i) = {
+ let sync = elmts.at(i)
+ elmts.at(i) = sync
+ let start = sync
+ start.remove("elmts")
+ return (
+ elmts.slice(0, i) +
+ (start,) +
+ sync.elmts +
+ ((
+ type: "sync-end",
+ draw: sync-render-end,
+ elmts: sync.elmts
+ ),) +
+ elmts.slice(i + 1)
+ )
+}
+
+#let update-group-children(elmts, i) = {
+ let elmts = elmts
+ let group-end = elmts.at(i)
+
+ elmts.at(group-end.start-i).elmts = elmts.slice(group-end.start-i + 1, i)
+ return elmts
+}
+
+#let convert-return(elmts, i, activation-history) = {
+ if activation-history.len() == 0 {
+ panic("Cannot return if no lifeline is activated")
+ }
+ let elmts = elmts
+ let activation-history = activation-history
+ let ret = elmts.at(i)
+ let seq = activation-history.pop()
+ elmts.at(i) = _seq(
+ seq.p2, seq.p1,
+ comment: ret.comment,
+ disable-src: true,
+ dashed: true
+ ).first()
+ return (elmts, activation-history)
+}
+
+#let unwrap-containers(elmts) = {
+ let elmts = elmts
+ let i = 0
+ let activation-history = ()
+
+ // Flatten groups + convert returns
+ while i < elmts.len() {
+ let elmt = elmts.at(i)
+ if not is-elmt(elmt) {
+ i += 1
+ continue
+ }
+
+ if elmt.type == "grp" {
+ elmts = flatten-group(elmts, i)
+
+ } else if elmt.type == "sync" {
+ elmts = flatten-sync(elmts, i)
+
+ } 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" {
+ (elmts, activation-history) = convert-return(elmts, i, activation-history)
+ }
+ i += 1
+ }
+
+ return (elmts, activation-history)
+}
+
+
+#let prepare-seq-participants(ctx, seq) = {
+ let ctx = ctx
+ if not par-exists(ctx.participants, seq.p1) {
+ ctx.participants.push(_par(seq.p1).first())
+ }
+ if not par-exists(ctx.participants, seq.p2) {
+ ctx.participants.push(_par(
+ seq.p2,
+ from-start: not seq.create-dst
+ ).first())
+
+ } else if seq.create-dst {
+ let i = ctx.participants.position(p => p.name == seq.p2)
+ ctx.participants.at(i).from-start = false
+ }
+
+ let p1 = seq.p1
+ let p2 = seq.p2
+ if seq.p1 == "?" {
+ p1 = "?" + seq.p2
+ }
+ if seq.p2 == "?" {
+ p2 = seq.p1 + "?"
+ }
+ ctx.linked.push(p1)
+ ctx.linked.push(p2)
+ ctx.last-seq = (
+ seq: seq,
+ i: ctx.i,
+ p1: p1,
+ p2: p2
+ )
+ return ctx
+}
+
+#let prepare-note-participants(ctx, note) = {
+ let ctx = ctx
+ let note = note
+ note.insert(
+ "linked",
+ note.pos == none and note.side != "across"
+ )
+ let names = ctx.participants.map(p => p.name)
+ if note.pos == none and note.side != "across" {
+ let i1 = names.position(n => n == ctx.last-seq.p1)
+ let i2 = names.position(n => n == ctx.last-seq.p2)
+ let pars = (
+ (i1, ctx.last-seq.p1),
+ (i2, ctx.last-seq.p2)
+ ).sorted(key: p => p.first())
+
+ if note.side == "left" {
+ note.pos = pars.first().last()
+ } else if note.side == "right" {
+ note.pos = pars.last().last()
+ }
+
+ let seq = ctx.elmts.at(ctx.last-seq.i)
+ seq.linked-notes.push(note)
+ ctx.elmts.at(ctx.last-seq.i) = seq
+ }
+ if note.aligned {
+ let n = ctx.last-note.note
+ n.aligned-with = note
+ ctx.elmts.at(ctx.last-note.i) = n
+ }
+
+ if note.side in ("left", "right") {
+ let i = names.position(n => n == note.pos)
+ let pos2 = note.pos
+ if note.side == "left" {
+ if i <= 0 or note.allow-overlap {
+ ctx.linked.push("[")
+ pos2 = "["
+ } else {
+ pos2 = names.at(i - 1)
+ }
+ } else if note.side == "right" {
+ if i >= names.len() - 1 or note.allow-overlap {
+ ctx.linked.push("]")
+ pos2 = "]"
+ } else {
+ pos2 = names.at(i + 1)
+ }
+ }
+ note.insert("pos2", pos2)
+ }
+
+ let pars = none
+ if type(note.pos) == str {
+ pars = (note.pos,)
+ } else if type(note.pos) == array {
+ pars = note.pos
+ }
+ if pars != none {
+ for par in pars {
+ if not par-exists(ctx.participants, par) {
+ participants.push(_par(par).first())
+ }
+ }
+ }
+
+ ctx.elmts.at(ctx.i) = note
+
+ ctx.last-note = (
+ note: note,
+ i: ctx.i
+ )
+
+ return ctx
+}
+
+#let prepare-evt-participants(ctx, evt) = {
+ let par = evt.participant
+ if not par-exists(ctx.participants, par) {
+ let p = _par(
+ par,
+ from-start: evt.event != "create"
+ ).first()
+ ctx.participants.push(p)
+
+ } else if evt.event == "create" {
+ let i = ctx.participants.position(p => p.name == par)
+ ctx.participants.at(i).from-start = false
+ }
+ return ctx
+}
+
+#let normalize-special-participants(elmt) = {
+ if elmt.p1 == "?" {
+ elmt.p1 = "?" + elmt.p2
+ } else if elmt.p2 == "?" {
+ elmt.p2 = elmt.p1 + "?"
+ }
+ return elmt
+}
+
+#let prepare-participants(elmts) = {
+ let ctx = (
+ linked: (),
+ last-seq: none,
+ last-note: none,
+ participants: (),
+ elmts: elmts,
+ i: 0
+ )
+
+ for (i, elmt) in ctx.elmts.enumerate() {
+ ctx.i = i
+ if not is-elmt(elmt) {
+ continue
+ }
+
+ if elmt.type == "par" {
+ ctx.participants.push(elmt)
+
+ } else if elmt.type == "seq" {
+ ctx = prepare-seq-participants(ctx, elmt)
+
+ } else if elmt.type == "note" {
+ ctx = prepare-note-participants(ctx, elmt)
+
+ } else if elmt.type == "evt" {
+ ctx = prepare-evt-participants(ctx, elmt)
+ }
+ }
+ ctx.linked = ctx.linked.dedup()
+
+ let pars = ctx.participants
+ let participants = ()
+
+ if "[" in ctx.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 ctx.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 ctx.linked {
+ participants.push(after)
+ }
+ }
+ if "]" in ctx.linked {
+ participants.push(_par(
+ "]",
+ invisible: true
+ ).first())
+ }
+
+ return (ctx.elmts, participants)
+}
+
+#let finalize-setup(elmts, participants) = {
+ for (i, p) in participants.enumerate() {
+ p.insert("i", i)
+ participants.at(i) = p
+ }
+
+ let containers = ()
+
+ for (i, elmt) in elmts.enumerate() {
+ if not is-elmt(elmt) {
+ continue
+ }
+ if elmt.type == "seq" {
+ elmts.at(i) = normalize-special-participants(elmt)
+ } else if elmt.type == "grp-end" {
+ // Put back elements in group because they might have changed
+ elmts = update-group-children(elmts, i)
+ } else if elmt.type in ("grp", "alt") {
+ containers.push(i)
+ }
+ }
+
+ // Compute groups spans (horizontal)
+ for i in containers {
+ let elmt = elmts.at(i)
+ 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)
+ }
+
+ return (elmts, participants)
+}
+
+#let setup(elements) = {
+ let (elmts, activation-history) = unwrap-containers(elements)
+
+ let participants
+ (elmts, participants) = prepare-participants(elmts)
+
+ return finalize-setup(elmts, participants)
+}
\ No newline at end of file
diff --git a/src/utils.typ b/src/core/utils.typ
similarity index 70%
rename from src/utils.typ
rename to src/core/utils.typ
index e8d750d..0dd9ee9 100644
--- a/src/utils.typ
+++ b/src/core/utils.typ
@@ -1,3 +1,15 @@
+#import "/src/cetz.typ": draw
+
+#let is-elmt(elmt) = {
+ if type(elmt) != dictionary {
+ return false
+ }
+ if "type" not in elmt {
+ return false
+ }
+ return true
+}
+
#let normalize-units(value) = {
if type(value) == int or type(value) == float {
return value
@@ -30,7 +42,7 @@
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" {
+ } else if elmt.type == "sync-end" {
let (i0, i1) = get-group-span(participants, elmt)
min-i = calc.min(min-i, i0)
max-i = calc.max(max-i, i1)
@@ -79,4 +91,27 @@
height: new-h,
scale(x: r, y: r, reflow: true, canvas)
)
+})
+
+#let set-ctx(func) = draw.set-ctx(c => {
+ let ctx = c.shared-state.chronos
+ let new-ctx = func(ctx)
+ assert(new-ctx != none, message: "set-ctx must return a context!")
+ c.shared-state.chronos = new-ctx
+ return c
+})
+
+#let get-ctx(func) = draw.get-ctx(c => {
+ let ctx = c.shared-state.chronos
+ func(ctx)
+})
+
+#let expand-parent-group(x0, x1) = set-ctx(ctx => {
+ if ctx.groups.len() != 0 {
+ let group = ctx.groups.last()
+ group.min-x = calc.min(group.min-x, x0)
+ group.max-x = calc.max(group.max-x, x1)
+ ctx.groups.last() = group
+ }
+ return ctx
})
\ No newline at end of file
diff --git a/src/diagram.typ b/src/diagram.typ
index 0e78117..a16809e 100644
--- a/src/diagram.typ
+++ b/src/diagram.typ
@@ -1,249 +1,14 @@
-#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 _gap(size: 20) = {
- return ((
- 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,
- width: width,
- margin: margin,
- min-width: min-width,
- max-width: max-width
- ),)
-}
+#import "core/draw/event.typ": render as evt-render
+#import "core/renderer.typ": render
+#import "core/setup.typ": setup
+#import "core/utils.typ": fit-canvas, set-ctx
#let diagram(elements, width: auto) = {
if elements == none {
return
}
- let participants = ()
- 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" {
- 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) +
- elmt.elmts +
- ((
- type: "grp-end",
- start-i: i
- ),) +
- elmts.slice(i+1)
- )
- } else if elmt.type == "grp-end" {
- // Put back elements in group because they might have changed
- elmts.at(elmt.start-i).elmts = elmts.slice(elmt.start-i + 1, i)
-
- } 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 participant._exists(participants, elmt.p1) {
- participants.push(_par(elmt.p1).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 + "?"
- }
- }
- }
+ let (elmts, participants) = setup(elements)
let canvas = render(participants, elmts)
fit-canvas(canvas, width: width)
diff --git a/src/group.typ b/src/group.typ
index db0933b..dc75065 100644
--- a/src/group.typ
+++ b/src/group.typ
@@ -1,9 +1,9 @@
-#import "/src/cetz.typ": draw
-#import "consts.typ": *
+#import "core/draw/group.typ"
#let _grp(name, desc: none, type: "default", elmts) = {
return ((
type: "grp",
+ draw: group.render-start,
name: name,
desc: desc,
grp-type: type,
@@ -20,6 +20,7 @@
let else-elmts = args.at(i + 1, default: ())
all-elmts.push((
type: "else",
+ draw: group.render-else,
desc: else-desc
))
all-elmts += else-elmts
@@ -38,62 +39,5 @@
}
_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
-}
\ No newline at end of file
+#let _opt(desc, elmts) = _grp("opt", desc: desc, type: "opt", elmts)
+#let _break(desc, elmts) = _grp("break", desc: desc, type: "break", elmts)
diff --git a/src/lib.typ b/src/lib.typ
index 2575674..09d36a8 100644
--- a/src/lib.typ
+++ b/src/lib.typ
@@ -1,9 +1,8 @@
-#let version = version(0, 2, 1)
-#import "diagram.typ": diagram, from-plantuml, _gap, _evt, _col
+#let version = version(0, 3, 0)
+#import "diagram.typ": diagram, from-plantuml
#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
\ No newline at end of file
+#import "misc.typ": _sep, _delay, _sync, _gap, _evt, _col
+#import "note.typ": _note
\ No newline at end of file
diff --git a/src/misc.typ b/src/misc.typ
new file mode 100644
index 0000000..48e6986
--- /dev/null
+++ b/src/misc.typ
@@ -0,0 +1,65 @@
+#import "core/draw/delay.typ"
+#import "core/draw/event.typ": render as evt-render
+#import "core/draw/separator.typ"
+#import "core/draw/sync.typ"
+#import "core/utils.typ": set-ctx
+
+#let _sep(name) = {
+ return ((
+ type: "sep",
+ draw: separator.render,
+ name: name
+ ),)
+}
+
+#let _delay(name: none, size: 30) = {
+ return ((
+ type: "delay",
+ draw: delay.render,
+ name: name,
+ size: size
+ ),)
+}
+
+#let _sync(elmts) = {
+ return ((
+ type: "sync",
+ draw: sync.render,
+ elmts: elmts
+ ),)
+}
+
+#let gap-render(gap) = set-ctx(ctx => {
+ ctx.y -= gap.size
+ return ctx
+})
+
+#let _gap(size: 20) = {
+ return ((
+ type: "gap",
+ draw: gap-render,
+ size: size
+ ),)
+}
+
+#let _evt(participant, event, lifeline-style: auto) = {
+ return ((
+ type: "evt",
+ draw: evt-render,
+ participant: participant,
+ event: event,
+ lifeline-style: lifeline-style
+ ),)
+}
+
+#let _col(p1, p2, width: auto, margin: 0, min-width: 0, max-width: none) = {
+ return ((
+ type: "col",
+ p1: p1,
+ p2: p2,
+ width: width,
+ margin: margin,
+ min-width: min-width,
+ max-width: max-width
+ ),)
+}
\ No newline at end of file
diff --git a/src/note.typ b/src/note.typ
index 14d3c98..01e6b31 100644
--- a/src/note.typ
+++ b/src/note.typ
@@ -1,5 +1,5 @@
-#import "/src/cetz.typ": draw
#import "consts.typ": *
+#import "core/draw/note.typ"
#let SIDES = (
"left",
@@ -14,7 +14,15 @@
"hex"
)
-#let _note(side, content, pos: none, color: COL-NOTE, shape: "default", aligned: false) = {
+#let _note(
+ side,
+ content,
+ pos: none,
+ color: COL-NOTE,
+ shape: "default",
+ aligned: false,
+ allow-overlap: true
+) = {
if side == "over" {
if pos == none {
panic("Pos cannot be none with side 'over'")
@@ -30,156 +38,14 @@
}
return ((
type: "note",
+ draw: note.render,
side: side,
content: content,
pos: pos,
color: color,
shape: shape,
aligned: aligned,
- aligned-with: none
+ aligned-with: none,
+ allow-overlap: allow-overlap
),)
}
-
-#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
-}
\ No newline at end of file
diff --git a/src/participant.typ b/src/participant.typ
index a9e5572..1a4b4b2 100644
--- a/src/participant.typ
+++ b/src/participant.typ
@@ -1,7 +1,6 @@
-#import "/src/cetz.typ": draw
-#import "consts.typ": *
+#import "core/draw/participant.typ"
-#let PAR-SPECIALS = "?[]"
+#let PAR-SPECIALS = ("?", "[", "]")
#let SHAPES = (
"participant",
"actor",
@@ -22,6 +21,11 @@
invisible: false,
shape: "participant",
color: DEFAULT-COLOR,
+ line-stroke: (
+ dash: "dashed",
+ paint: gray.darken(40%),
+ thickness: .5pt
+ ),
custom-image: none,
show-bottom: true,
show-top: true,
@@ -31,12 +35,14 @@
}
return ((
type: "par",
+ draw: participant.render,
name: name,
display-name: if display-name == auto {name} else {display-name},
from-start: from-start,
invisible: invisible,
shape: shape,
color: color,
+ line-stroke: line-stroke,
custom-image: custom-image,
show-bottom: show-bottom,
show-top: show-top
@@ -44,7 +50,7 @@
}
#let _exists(participants, name) = {
- if name == "?" or name == "[" or name == "]" {
+ if name in PAR-SPECIALS {
return true
}
@@ -55,307 +61,3 @@
}
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)
-}
\ No newline at end of file
diff --git a/src/renderer.typ b/src/renderer.typ
deleted file mode 100644
index 4f70d02..0000000
--- a/src/renderer.typ
+++ /dev/null
@@ -1,522 +0,0 @@
-#import "/src/cetz.typ": 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 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}
- let i1 = pars-i.at(elmt.p1)
- let i2 = pars-i.at(elmt.p2)
- cells.push(
- (
- elmt: elmt,
- i1: calc.min(i1, i2),
- i2: calc.max(i1, i2),
- 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 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 + 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
- })
- for cell in multicol-cells {
- let m = measure(cell.cell)
- widths.at(cell.i2 - 1) = calc.max(
- widths.at(cell.i2 - 1),
- 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 (start)
- for p in participants {
- if p.from-start and not p.invisible and p.show-top {
- shapes += draw-par(p)
- }
- }
-
- let y = 0
- let groups = ()
- let lifelines = participants.map(_ => (
- level: 0,
- lines: ()
- ))
-
- // Draw elemnts
- for elmt in elements {
- // Sequences
- if elmt.type == "seq" {
- 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)
-
- 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 = 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
- }
- }
-
- 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, last-y),
- (x, y),
- 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
-})
\ No newline at end of file
diff --git a/src/separator.typ b/src/separator.typ
deleted file mode 100644
index e0d1b71..0000000
--- a/src/separator.typ
+++ /dev/null
@@ -1,63 +0,0 @@
-#import "/src/cetz.typ": 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
-}
\ No newline at end of file
diff --git a/src/sequence.typ b/src/sequence.typ
index eb315de..c916996 100644
--- a/src/sequence.typ
+++ b/src/sequence.typ
@@ -1,51 +1,4 @@
-#import "/src/cetz.typ": 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")
+#import "core/draw/sequence.typ"
#let _seq(
p1,
@@ -64,10 +17,12 @@
disable-src: false,
destroy-src: false,
lifeline-style: auto,
- slant: none
+ slant: none,
+ outer-lifeline-connect: false
) = {
return ((
type: "seq",
+ draw: sequence.render,
p1: p1,
p2: p2,
comment: comment,
@@ -84,7 +39,9 @@
disable-src: disable-src,
destroy-src: destroy-src,
lifeline-style: lifeline-style,
- slant: slant
+ slant: slant,
+ outer-lifeline-connect: outer-lifeline-connect,
+ linked-notes: ()
),)
}
@@ -94,294 +51,3 @@
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
-}
\ No newline at end of file
diff --git a/src/sync.typ b/src/sync.typ
deleted file mode 100644
index 41f0891..0000000
--- a/src/sync.typ
+++ /dev/null
@@ -1,27 +0,0 @@
-#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
-}
\ No newline at end of file
diff --git a/tests/empty/.gitignore b/tests/empty/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/empty/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/empty/ref/1.png b/tests/empty/ref/1.png
new file mode 100644
index 0000000..7971661
Binary files /dev/null and b/tests/empty/ref/1.png differ
diff --git a/tests/empty/test.typ b/tests/empty/test.typ
new file mode 100644
index 0000000..80e1d7a
--- /dev/null
+++ b/tests/empty/test.typ
@@ -0,0 +1,6 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({})
+
+#diagram(())
diff --git a/tests/group/.gitignore b/tests/group/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/group/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/group/ref/1.png b/tests/group/ref/1.png
new file mode 100644
index 0000000..8ee09a4
Binary files /dev/null and b/tests/group/ref/1.png differ
diff --git a/tests/group/ref/2.png b/tests/group/ref/2.png
new file mode 100644
index 0000000..070f238
Binary files /dev/null and b/tests/group/ref/2.png differ
diff --git a/tests/group/ref/3.png b/tests/group/ref/3.png
new file mode 100644
index 0000000..9703fa2
Binary files /dev/null and b/tests/group/ref/3.png differ
diff --git a/tests/group/test.typ b/tests/group/test.typ
new file mode 100644
index 0000000..213df95
--- /dev/null
+++ b/tests/group/test.typ
@@ -0,0 +1,51 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({
+ _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")
+ }
+ )
+})
+
+#pagebreak()
+
+#diagram({
+ _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: ">>")
+})
+
+#pagebreak()
+
+#diagram({
+ _par("A")
+ _par("B")
+ _col("A", "B", width: 3cm)
+ _seq("A", "B", enable-dst: true)
+ _alt([desc], {
+ _ret()
+ })
+})
\ No newline at end of file
diff --git a/tests/lifeline/create-destroy/.gitignore b/tests/lifeline/create-destroy/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/lifeline/create-destroy/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/lifeline/create-destroy/ref/1.png b/tests/lifeline/create-destroy/ref/1.png
new file mode 100644
index 0000000..1802802
Binary files /dev/null and b/tests/lifeline/create-destroy/ref/1.png differ
diff --git a/tests/lifeline/create-destroy/ref/2.png b/tests/lifeline/create-destroy/ref/2.png
new file mode 100644
index 0000000..af4775b
Binary files /dev/null and b/tests/lifeline/create-destroy/ref/2.png differ
diff --git a/tests/lifeline/create-destroy/test.typ b/tests/lifeline/create-destroy/test.typ
new file mode 100644
index 0000000..f1b209a
--- /dev/null
+++ b/tests/lifeline/create-destroy/test.typ
@@ -0,0 +1,32 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ // Left to right
+ _seq("a", "b", create-dst: true)
+ _seq("a", "b", enable-dst: true)
+ _seq("a", "b", enable-dst: true)
+ _gap()
+ _seq("a", "b", destroy-dst: true)
+ _gap()
+ _seq("a", "b", destroy-dst: true)
+})
+
+#pagebreak()
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ // Right to left
+ _seq("b", "a", create-dst: true)
+ _seq("b", "a", enable-dst: true)
+ _seq("b", "a", enable-dst: true)
+ _gap()
+ _seq("b", "a", destroy-dst: true)
+ _gap()
+ _seq("b", "a", destroy-dst: true)
+})
diff --git a/tests/lifeline/enable-disable/.gitignore b/tests/lifeline/enable-disable/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/lifeline/enable-disable/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/lifeline/enable-disable/ref/1.png b/tests/lifeline/enable-disable/ref/1.png
new file mode 100644
index 0000000..d5999c5
Binary files /dev/null and b/tests/lifeline/enable-disable/ref/1.png differ
diff --git a/tests/lifeline/enable-disable/ref/2.png b/tests/lifeline/enable-disable/ref/2.png
new file mode 100644
index 0000000..3e6678d
Binary files /dev/null and b/tests/lifeline/enable-disable/ref/2.png differ
diff --git a/tests/lifeline/enable-disable/ref/3.png b/tests/lifeline/enable-disable/ref/3.png
new file mode 100644
index 0000000..125304c
Binary files /dev/null and b/tests/lifeline/enable-disable/ref/3.png differ
diff --git a/tests/lifeline/enable-disable/ref/4.png b/tests/lifeline/enable-disable/ref/4.png
new file mode 100644
index 0000000..de1b5bb
Binary files /dev/null and b/tests/lifeline/enable-disable/ref/4.png differ
diff --git a/tests/lifeline/enable-disable/ref/5.png b/tests/lifeline/enable-disable/ref/5.png
new file mode 100644
index 0000000..1643084
Binary files /dev/null and b/tests/lifeline/enable-disable/ref/5.png differ
diff --git a/tests/lifeline/enable-disable/ref/6.png b/tests/lifeline/enable-disable/ref/6.png
new file mode 100644
index 0000000..4304b01
Binary files /dev/null and b/tests/lifeline/enable-disable/ref/6.png differ
diff --git a/tests/lifeline/enable-disable/test.typ b/tests/lifeline/enable-disable/test.typ
new file mode 100644
index 0000000..21c7833
--- /dev/null
+++ b/tests/lifeline/enable-disable/test.typ
@@ -0,0 +1,110 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ // Left to right
+ _seq("a", "b")
+ _seq("a", "b", enable-dst: true)
+ _seq("a", "b")
+ _seq("a", "b", enable-dst: true)
+ _seq("a", "b")
+ _seq("a", "b", disable-dst: true)
+ _seq("a", "b")
+ _seq("a", "b", disable-dst: true)
+ _seq("a", "b")
+})
+
+#pagebreak()
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ // Right to left
+ _seq("b", "a")
+ _seq("b", "a", enable-dst: true)
+ _seq("b", "a")
+ _seq("b", "a", enable-dst: true)
+ _seq("b", "a")
+ _seq("b", "a", disable-dst: true)
+ _seq("b", "a")
+ _seq("b", "a", disable-dst: true)
+ _seq("b", "a")
+})
+
+#pagebreak()
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ // Self right
+ _seq("a", "a")
+ _seq("a", "a", enable-dst: true)
+ _seq("a", "a")
+ _seq("a", "a", enable-dst: true)
+ _seq("a", "a")
+ _seq("a", "a", disable-dst: true)
+ _seq("a", "a")
+ _seq("a", "a", disable-dst: true)
+ _seq("a", "a")
+})
+
+#pagebreak()
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ let _seq = _seq.with(flip: true)
+
+ // Self left
+ _seq("b", "b")
+ _seq("b", "b", enable-dst: true)
+ _seq("b", "b")
+ _seq("b", "b", enable-dst: true)
+ _seq("b", "b")
+ _seq("b", "b", disable-dst: true)
+ _seq("b", "b")
+ _seq("b", "b", disable-dst: true)
+ _seq("b", "b")
+})
+
+#pagebreak()
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ // Disable src (rtl)
+ _seq("a", "b")
+ _seq("a", "b", enable-dst: true)
+ _seq("a", "b")
+ _seq("a", "b", enable-dst: true)
+ _seq("a", "b")
+ _seq("b", "a", disable-src: true)
+ _seq("a", "b")
+ _seq("b", "a", disable-src: true)
+ _seq("a", "b")
+})
+
+#pagebreak()
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ // Disable src (ltr)
+ _seq("b", "a")
+ _seq("b", "a", enable-dst: true)
+ _seq("b", "a")
+ _seq("b", "a", enable-dst: true)
+ _seq("b", "a")
+ _seq("a", "b", disable-src: true)
+ _seq("b", "a")
+ _seq("a", "b", disable-src: true)
+ _seq("b", "a")
+})
diff --git a/tests/lifeline/event/.gitignore b/tests/lifeline/event/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/lifeline/event/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/lifeline/event/ref/1.png b/tests/lifeline/event/ref/1.png
new file mode 100644
index 0000000..648fe4a
Binary files /dev/null and b/tests/lifeline/event/ref/1.png differ
diff --git a/tests/lifeline/event/ref/2.png b/tests/lifeline/event/ref/2.png
new file mode 100644
index 0000000..795e60a
Binary files /dev/null and b/tests/lifeline/event/ref/2.png differ
diff --git a/tests/lifeline/event/ref/3.png b/tests/lifeline/event/ref/3.png
new file mode 100644
index 0000000..58327f1
Binary files /dev/null and b/tests/lifeline/event/ref/3.png differ
diff --git a/tests/lifeline/event/test.typ b/tests/lifeline/event/test.typ
new file mode 100644
index 0000000..1167ea9
--- /dev/null
+++ b/tests/lifeline/event/test.typ
@@ -0,0 +1,75 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ _evt("a", "enable")
+ _gap()
+ _seq("a", "b")
+ _evt("a", "disable")
+ _gap()
+ _evt("a", "enable")
+ _evt("b", "enable")
+ _seq("a", "b")
+ _evt("a", "enable")
+ _evt("b", "enable")
+ _gap()
+ _seq("a", "b")
+ _evt("a", "disable")
+ _evt("b", "disable")
+ _gap()
+ _evt("a", "disable")
+ _evt("b", "disable")
+})
+
+#pagebreak()
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+ _par("c", display-name: "Charlie")
+
+ _seq("a", "b")
+ _gap()
+ _seq("a", "b", enable-dst: true)
+ _evt("c", "create")
+ _seq("c", "a")
+ _evt("b", "destroy")
+})
+
+#pagebreak()
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ _seq("a", "b")
+ _evt("b", "enable")
+ _seq("a", "b")
+ _evt("b", "enable", lifeline-style: (fill: red))
+ _gap()
+ _seq("b", "a")
+ _evt("b", "disable")
+ _seq("a", "b")
+ _evt("b", "enable", lifeline-style: (
+ stroke: (
+ paint: green,
+ dash: "dashed",
+ thickness: 2pt
+ )
+ ))
+ _gap()
+ _seq("b", "a")
+ _evt("b", "disable")
+ _seq("b", "a")
+ _evt("b", "disable")
+ _seq("a", "b")
+ _evt("b", "enable", lifeline-style: (
+ radius: 4pt
+ ))
+ _gap()
+ _seq("b", "a")
+ _evt("b", "disable")
+})
diff --git a/tests/lifeline/style/.gitignore b/tests/lifeline/style/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/lifeline/style/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/lifeline/style/ref/1.png b/tests/lifeline/style/ref/1.png
new file mode 100644
index 0000000..055fb4d
Binary files /dev/null and b/tests/lifeline/style/ref/1.png differ
diff --git a/tests/lifeline/style/test.typ b/tests/lifeline/style/test.typ
new file mode 100644
index 0000000..e7622bc
--- /dev/null
+++ b/tests/lifeline/style/test.typ
@@ -0,0 +1,27 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({
+ _par("a", display-name: "Alice")
+ _par("b", display-name: "Bob")
+
+ _seq("a", "b", enable-dst: true)
+ _seq("a", "b", enable-dst: true, lifeline-style: (fill: red))
+ _gap()
+ _seq("b", "a", disable-src: true)
+ _seq("a", "b", enable-dst: true, lifeline-style: (
+ stroke: (
+ paint: green,
+ dash: "dashed",
+ thickness: 2pt
+ )
+ ))
+ _gap()
+ _seq("b", "a", disable-src: true)
+ _seq("b", "a", disable-src: true)
+ _seq("a", "b", enable-dst: true, lifeline-style: (
+ radius: 4pt
+ ))
+ _gap()
+ _seq("b", "a", disable-src: true)
+})
diff --git a/tests/note/color/.gitignore b/tests/note/color/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/note/color/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/note/color/ref/1.png b/tests/note/color/ref/1.png
new file mode 100644
index 0000000..68f4b4c
Binary files /dev/null and b/tests/note/color/ref/1.png differ
diff --git a/tests/note/color/test.typ b/tests/note/color/test.typ
new file mode 100644
index 0000000..bf051c5
--- /dev/null
+++ b/tests/note/color/test.typ
@@ -0,0 +1,13 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#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"))
+})
\ No newline at end of file
diff --git a/tests/note/position/.gitignore b/tests/note/position/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/note/position/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/note/position/ref/1.png b/tests/note/position/ref/1.png
new file mode 100644
index 0000000..04bb1c8
Binary files /dev/null and b/tests/note/position/ref/1.png differ
diff --git a/tests/note/position/ref/2.png b/tests/note/position/ref/2.png
new file mode 100644
index 0000000..c0c6d78
Binary files /dev/null and b/tests/note/position/ref/2.png differ
diff --git a/tests/note/position/ref/3.png b/tests/note/position/ref/3.png
new file mode 100644
index 0000000..c4091b2
Binary files /dev/null and b/tests/note/position/ref/3.png differ
diff --git a/tests/note/position/test.typ b/tests/note/position/test.typ
new file mode 100644
index 0000000..4fef1cd
--- /dev/null
+++ b/tests/note/position/test.typ
@@ -0,0 +1,49 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#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()
+
+#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])
+})
+
+#pagebreak()
+
+#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("across", [This note float above all participants])
+
+ _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"))
+})
\ No newline at end of file
diff --git a/tests/note/shape/.gitignore b/tests/note/shape/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/note/shape/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/note/shape/ref/1.png b/tests/note/shape/ref/1.png
new file mode 100644
index 0000000..e7321d4
Binary files /dev/null and b/tests/note/shape/ref/1.png differ
diff --git a/tests/note/shape/test.typ b/tests/note/shape/test.typ
new file mode 100644
index 0000000..c14131d
--- /dev/null
+++ b/tests/note/shape/test.typ
@@ -0,0 +1,14 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#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")
+})
\ No newline at end of file
diff --git a/tests/note/styled-body/.gitignore b/tests/note/styled-body/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/note/styled-body/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/note/styled-body/ref/1.png b/tests/note/styled-body/ref/1.png
new file mode 100644
index 0000000..d4a3a8b
Binary files /dev/null and b/tests/note/styled-body/ref/1.png differ
diff --git a/tests/note/styled-body/test.typ b/tests/note/styled-body/test.typ
new file mode 100644
index 0000000..419a36c
--- /dev/null
+++ b/tests/note/styled-body/test.typ
@@ -0,0 +1,33 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#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("/gallery/gitea.png", width: 1cm, height: 1cm, fit: "contain"))
+ ], pos: ("a", "b"))
+})
\ No newline at end of file
diff --git a/tests/participant/shapes/.gitignore b/tests/participant/shapes/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/participant/shapes/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/participant/shapes/ref/1.png b/tests/participant/shapes/ref/1.png
new file mode 100644
index 0000000..955b123
Binary files /dev/null and b/tests/participant/shapes/ref/1.png differ
diff --git a/tests/participant/shapes/test.typ b/tests/participant/shapes/test.typ
new file mode 100644
index 0000000..c130cdf
--- /dev/null
+++ b/tests/participant/shapes/test.typ
@@ -0,0 +1,21 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+
+#let TYPST = image("/gallery/typst.png", width: 1.5cm, height: 1.5cm, fit: "contain")
+#let FERRIS = image("/gallery/ferris.png", width: 1.5cm, height: 1.5cm, fit: "contain")
+#let ME = image("/gallery/me.jpg", width: 1.5cm, height: 1.5cm, fit: "contain")
+
+#diagram({
+ _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)
+})
\ No newline at end of file
diff --git a/tests/sequence/comment-align/.gitignore b/tests/sequence/comment-align/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/sequence/comment-align/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/sequence/comment-align/ref/1.png b/tests/sequence/comment-align/ref/1.png
new file mode 100644
index 0000000..1cdb569
Binary files /dev/null and b/tests/sequence/comment-align/ref/1.png differ
diff --git a/tests/sequence/comment-align/ref/2.png b/tests/sequence/comment-align/ref/2.png
new file mode 100644
index 0000000..31185af
Binary files /dev/null and b/tests/sequence/comment-align/ref/2.png differ
diff --git a/tests/sequence/comment-align/test.typ b/tests/sequence/comment-align/test.typ
new file mode 100644
index 0000000..f69d949
--- /dev/null
+++ b/tests/sequence/comment-align/test.typ
@@ -0,0 +1,57 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({
+ _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)
+})
+
+#pagebreak()
+
+#diagram({
+ _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)
+})
\ No newline at end of file
diff --git a/tests/sequence/outer-lifeline/.gitignore b/tests/sequence/outer-lifeline/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/sequence/outer-lifeline/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/sequence/outer-lifeline/ref/1.png b/tests/sequence/outer-lifeline/ref/1.png
new file mode 100644
index 0000000..a5f7d1f
Binary files /dev/null and b/tests/sequence/outer-lifeline/ref/1.png differ
diff --git a/tests/sequence/outer-lifeline/ref/2.png b/tests/sequence/outer-lifeline/ref/2.png
new file mode 100644
index 0000000..d2a82c4
Binary files /dev/null and b/tests/sequence/outer-lifeline/ref/2.png differ
diff --git a/tests/sequence/outer-lifeline/test.typ b/tests/sequence/outer-lifeline/test.typ
new file mode 100644
index 0000000..4bde480
--- /dev/null
+++ b/tests/sequence/outer-lifeline/test.typ
@@ -0,0 +1,29 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#let make-diagram(_seq) = {
+ diagram({
+ _par("a")
+ _par("b")
+ _par("c")
+ _seq("a", "b")
+ _seq("c", "b")
+ _seq("a", "b", enable-dst: true)
+ _seq("a", "b")
+ _seq("c", "b")
+ _seq("a", "b", enable-dst: true)
+ _seq("a", "b")
+ _seq("c", "b")
+ _seq("a", "b", enable-dst: true)
+ _seq("a", "b")
+ _seq("c", "b")
+
+ _evt("b", "disable")
+ _evt("b", "disable")
+ _evt("b", "disable")
+ })
+}
+
+#make-diagram(_seq)
+#pagebreak()
+#make-diagram(_seq.with(outer-lifeline-connect: true))
diff --git a/tests/sequence/special-par/.gitignore b/tests/sequence/special-par/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/sequence/special-par/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/sequence/special-par/ref/1.png b/tests/sequence/special-par/ref/1.png
new file mode 100644
index 0000000..38d5420
Binary files /dev/null and b/tests/sequence/special-par/ref/1.png differ
diff --git a/tests/sequence/special-par/test.typ b/tests/sequence/special-par/test.typ
new file mode 100644
index 0000000..2710cb3
--- /dev/null
+++ b/tests/sequence/special-par/test.typ
@@ -0,0 +1,12 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({
+ _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])
+})
diff --git a/tests/sequence/sync/.gitignore b/tests/sequence/sync/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/sequence/sync/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/sequence/sync/ref/1.png b/tests/sequence/sync/ref/1.png
new file mode 100644
index 0000000..a7d4067
Binary files /dev/null and b/tests/sequence/sync/ref/1.png differ
diff --git a/tests/sequence/sync/ref/2.png b/tests/sequence/sync/ref/2.png
new file mode 100644
index 0000000..be21dc9
Binary files /dev/null and b/tests/sequence/sync/ref/2.png differ
diff --git a/tests/sequence/sync/test.typ b/tests/sequence/sync/test.typ
new file mode 100644
index 0000000..fc69d7e
--- /dev/null
+++ b/tests/sequence/sync/test.typ
@@ -0,0 +1,47 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({
+ _par("a")
+ _par("b")
+ _par("c")
+ _sync({
+ _seq("b", "a")
+ _seq("b", "c")
+ })
+ _gap()
+ _sync({
+ _seq("b", "a", comment: [Comment])
+ _seq("b", "c")
+ })
+ _gap()
+ _sync({
+ _seq("b", "a")
+ _seq("b", "c", comment: [Comment])
+ })
+ _gap()
+ _sync({
+ _seq("b", "a", comment: [Two\ lines])
+ _seq("b", "c", comment: [Comment])
+ })
+
+ _sync({
+ _seq("b", "a")
+ _seq("b", "c", slant: 10)
+ })
+
+ _sync({
+ _seq("b", "a")
+ _seq("b", "b")
+ })
+})
+
+#pagebreak()
+
+#diagram({
+ _sync({
+ _seq("a", "b", comment: [Abcdefgh])
+ _seq("b", "c", comment: [Foo\ bar], slant: 10)
+ _seq("c", "c", slant: 20)
+ })
+})
\ No newline at end of file
diff --git a/tests/sequence/tips/.gitignore b/tests/sequence/tips/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/sequence/tips/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/sequence/tips/ref/1.png b/tests/sequence/tips/ref/1.png
new file mode 100644
index 0000000..00cff5d
Binary files /dev/null and b/tests/sequence/tips/ref/1.png differ
diff --git a/tests/sequence/tips/ref/2.png b/tests/sequence/tips/ref/2.png
new file mode 100644
index 0000000..3725245
Binary files /dev/null and b/tests/sequence/tips/ref/2.png differ
diff --git a/tests/sequence/tips/ref/3.png b/tests/sequence/tips/ref/3.png
new file mode 100644
index 0000000..6c12338
Binary files /dev/null and b/tests/sequence/tips/ref/3.png differ
diff --git a/tests/sequence/tips/test.typ b/tests/sequence/tips/test.typ
new file mode 100644
index 0000000..e3d60bd
--- /dev/null
+++ b/tests/sequence/tips/test.typ
@@ -0,0 +1,84 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#diagram({
+ _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`)
+})
+
+#pagebreak()
+
+#diagram({
+ _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`)
+})
+
+#pagebreak()
+
+#diagram({
+ _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`)
+})
diff --git a/tests/special-group/.gitignore b/tests/special-group/.gitignore
new file mode 100644
index 0000000..40223be
--- /dev/null
+++ b/tests/special-group/.gitignore
@@ -0,0 +1,4 @@
+# generated by tytanic, do not edit
+
+diff/**
+out/**
diff --git a/tests/special-group/ref/1.png b/tests/special-group/ref/1.png
new file mode 100644
index 0000000..a84209f
Binary files /dev/null and b/tests/special-group/ref/1.png differ
diff --git a/tests/special-group/ref/2.png b/tests/special-group/ref/2.png
new file mode 100644
index 0000000..ceffc47
Binary files /dev/null and b/tests/special-group/ref/2.png differ
diff --git a/tests/special-group/ref/3.png b/tests/special-group/ref/3.png
new file mode 100644
index 0000000..ac39b4d
Binary files /dev/null and b/tests/special-group/ref/3.png differ
diff --git a/tests/special-group/ref/4.png b/tests/special-group/ref/4.png
new file mode 100644
index 0000000..6e45733
Binary files /dev/null and b/tests/special-group/ref/4.png differ
diff --git a/tests/special-group/ref/5.png b/tests/special-group/ref/5.png
new file mode 100644
index 0000000..a63f121
Binary files /dev/null and b/tests/special-group/ref/5.png differ
diff --git a/tests/special-group/test.typ b/tests/special-group/test.typ
new file mode 100644
index 0000000..890c33e
--- /dev/null
+++ b/tests/special-group/test.typ
@@ -0,0 +1,71 @@
+#set page(width: auto, height: auto)
+#import "/src/lib.typ": *
+
+#let preamble = {
+ _par("a", display-name: [Alice])
+ _par("b", display-name: [Bob])
+ _col("a", "b", width: 2cm)
+}
+
+#diagram({
+ preamble
+ _grp("Group 1", {
+ _seq("a", "b")
+ })
+ _grp("Group 2", desc: [Description], {
+ _seq("a", "b")
+ })
+})
+
+#pagebreak()
+
+#diagram({
+ preamble
+ _alt(
+ "case 1", {
+ _seq("a", "b")
+ },
+ "case 2", {
+ _seq("a", "b")
+ },
+ "case 3", {
+ _seq("a", "b")
+ }
+ )
+})
+
+#pagebreak()
+
+#diagram({
+ preamble
+ _loop("loop 1", {
+ _seq("a", "b")
+ })
+ _loop("loop 2", min: 1, {
+ _seq("a", "b")
+ })
+ _loop("loop 3", max: 10, {
+ _seq("a", "b")
+ })
+ _loop("loop 3", min: 1, max: 10, {
+ _seq("a", "b")
+ })
+})
+
+#pagebreak()
+
+#diagram({
+ preamble
+ _opt("Optional", {
+ _seq("a", "b")
+ })
+})
+
+#pagebreak()
+
+#diagram({
+ preamble
+ _break("Break", {
+ _seq("a", "b")
+ })
+})
\ No newline at end of file
diff --git a/typst.toml b/typst.toml
index 54d8602..215e741 100644
--- a/typst.toml
+++ b/typst.toml
@@ -1,7 +1,7 @@
[package]
name = "chronos"
-version = "0.2.1"
-compiler = "0.13.1"
+version = "0.3.0"
+compiler = "0.14.2"
repository = "https://git.kb28.ch/HEL/chronos"
entrypoint = "src/lib.typ"
authors = [
|