14 Commits

29 changed files with 697 additions and 65 deletions

147
README.md
View File

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

27
TODO.md Normal file
View File

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

View File

@ -14,3 +14,14 @@ do
typst c --root ./ "$f" "$f2" typst c --root ./ "$f" "$f2"
i=$((i+1)) i=$((i+1))
done done
set -- ./gallery/readme/*.typ
cnt="$#"
i=1
for f
do
f2="${f/typ/png}"
echo "($i/$cnt) $f -> $f2"
typst c --root ./ "$f" "$f2"
i=$((i+1))
done

Binary file not shown.

Binary file not shown.

View File

@ -49,3 +49,121 @@
_seq("Alice", "?", comment: [->?\ *short* from actor1]) _seq("Alice", "?", comment: [->?\ *short* from actor1])
_seq("Alice", "Bob", comment: [->\ from actor1 to actor2]) _seq("Alice", "Bob", comment: [->\ from actor1 to actor2])
}) })
#chronos.diagram({
import chronos: *
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_par("craig", display-name: "Craig")
_seq("bob", "alice")
_seq("bob", "craig")
_gap()
_sync({
_seq("bob", "alice", comment: "Synched", comment-align: "start")
_seq("bob", "craig", comment: "Synched", comment-align: "start")
})
_gap()
_seq("alice", "bob")
_seq("craig", "bob")
_gap()
_sync({
_seq("alice", "bob")
_seq("craig", "bob")
})
_gap()
_sync({
_seq("alice", "bob", enable-dst: true)
_seq("craig", "bob")
})
_gap()
_evt("bob", "disable")
})
#chronos.diagram({
import chronos: *
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_par("craig", display-name: "Craig")
_seq("alice", "bob")
_seq("bob", "craig", slant: auto)
_seq("alice", "craig", slant: 20)
_sync({
_seq("alice", "bob", slant: 10)
_seq("craig", "bob", slant: 20)
})
_sync({
_seq("alice", "bob", slant: auto)
_seq("bob", "alice", slant: auto)
})
_gap()
_evt("bob", "disable")
})
#grid(columns: 2, column-gutter: 2em,
chronos.diagram({
import chronos: *
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_seq("alice", "bob", comment: "This is a very long comment")
// Left to right
_seq("alice", "bob", comment: "Start aligned", comment-align: "start")
_seq("alice", "bob", comment: "End aligned", comment-align: "end")
_seq("alice", "bob", comment: "Left aligned", comment-align: "left")
_seq("alice", "bob", comment: "Right aligned", comment-align: "right")
_seq("alice", "bob", comment: "Centered", comment-align: "center")
_gap()
// Right to left
_seq("bob", "alice", comment: "Start aligned", comment-align: "start")
_seq("bob", "alice", comment: "End aligned", comment-align: "end")
_seq("bob", "alice", comment: "Left aligned", comment-align: "left")
_seq("bob", "alice", comment: "Right aligned", comment-align: "right")
_seq("bob", "alice", comment: "Centered", comment-align: "center")
_gap()
// Slant left to right
_seq("alice", "bob", comment: "Start aligned", comment-align: "start", slant: 10)
_seq("alice", "bob", comment: "End aligned", comment-align: "end", slant: 10)
_seq("alice", "bob", comment: "Left aligned", comment-align: "left", slant: 10)
_seq("alice", "bob", comment: "Right aligned", comment-align: "right", slant: 10)
_seq("alice", "bob", comment: "Centered", comment-align: "center", slant: 10)
_gap()
// Slant right to left
_seq("bob", "alice", comment: "Start aligned", comment-align: "start", slant: 10)
_seq("bob", "alice", comment: "End aligned", comment-align: "end", slant: 10)
_seq("bob", "alice", comment: "Left aligned", comment-align: "left", slant: 10)
_seq("bob", "alice", comment: "Right aligned", comment-align: "right", slant: 10)
_seq("bob", "alice", comment: "Centered", comment-align: "center", slant: 10)
}),
chronos.diagram({
import chronos: *
_par("alice", display-name: "Alice")
_seq("alice", "alice", comment: "Start aligned", comment-align: "start")
_seq("alice", "alice", comment: "End aligned", comment-align: "end")
_seq("alice", "alice", comment: "Left aligned", comment-align: "left")
_seq("alice", "alice", comment: "Right aligned", comment-align: "right")
_seq("alice", "alice", comment: "Centered", comment-align: "center")
_seq("alice", "alice", comment: "Start aligned", comment-align: "start", flip: true)
_seq("alice", "alice", comment: "End aligned", comment-align: "end", flip: true)
_seq("alice", "alice", comment: "Left aligned", comment-align: "left", flip: true)
_seq("alice", "alice", comment: "Right aligned", comment-align: "right", flip: true)
_seq("alice", "alice", comment: "Centered", comment-align: "center", flip: true)
})
)

Binary file not shown.

View File

@ -65,11 +65,11 @@ chronos.diagram({
_seq("a", "b", end-tip: "x", comment: `->x`) _seq("a", "b", end-tip: "x", comment: `->x`)
_seq("a", "b", start-tip: "x", comment: `x->`) _seq("a", "b", start-tip: "x", comment: `x->`)
_seq("a", "b", start-tip: "o", comment: `o->`) _seq("a", "b", start-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: "o", end-tip: "o", comment: `o->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: ">", end-tip: ">", comment: `<->`)
_seq("a", "b", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`) _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", 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", end-tip: ("o", "\\\\"), comment: `-\\o`)
@ -78,6 +78,34 @@ chronos.diagram({
_seq("a", "b", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`) _seq("a", "b", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`)
}), }),
chronos.diagram({
import chronos: *
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_seq("b", "a", end-tip: ">", comment: `->`)
_seq("b", "a", end-tip: ">>", comment: `->>`)
_seq("b", "a", end-tip: "\\", comment: `-\`)
_seq("b", "a", end-tip: "\\\\", comment: `-\\`)
_seq("b", "a", end-tip: "/", comment: `-/`)
_seq("b", "a", end-tip: "//", comment: `-//`)
_seq("b", "a", end-tip: "x", comment: `->x`)
_seq("b", "a", start-tip: "x", comment: `x->`)
_seq("b", "a", start-tip: "o", comment: `o->`)
_seq("b", "a", end-tip: ("o", ">"), comment: `->o`)
_seq("b", "a", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`)
_seq("b", "a", start-tip: ">", end-tip: ">", comment: `<->`)
_seq("b", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`)
_seq("b", "a", start-tip: "x", end-tip: "x", comment: `x<->x`)
_seq("b", "a", end-tip: ("o", ">>"), comment: `->>o`)
_seq("b", "a", end-tip: ("o", "\\"), comment: `-\o`)
_seq("b", "a", end-tip: ("o", "\\\\"), comment: `-\\o`)
_seq("b", "a", end-tip: ("o", "/"), comment: `-/o`)
_seq("b", "a", end-tip: ("o", "//"), comment: `-//o`)
_seq("b", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`)
}),
chronos.diagram({ chronos.diagram({
import chronos: * import chronos: *
@ -93,11 +121,11 @@ chronos.diagram({
_seq("a", "a", end-tip: "x", comment: `->x`) _seq("a", "a", end-tip: "x", comment: `->x`)
_seq("a", "a", start-tip: "x", comment: `x->`) _seq("a", "a", start-tip: "x", comment: `x->`)
_seq("a", "a", start-tip: "o", comment: `o->`) _seq("a", "a", start-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: "o", end-tip: "o", comment: `o->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: ">", end-tip: ">", comment: `<->`)
_seq("a", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`) _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", 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", end-tip: ("o", "\\\\"), comment: `-\\o`)
@ -106,3 +134,14 @@ chronos.diagram({
_seq("a", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`) _seq("a", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`)
}) })
) )
#chronos.diagram({
import chronos: *
_par("a", display-name: "Alice")
_par("b", display-name: "Bob", show-bottom: false)
_par("c", display-name: "Caleb", show-top: false)
_par("d", display-name: "Danny", show-bottom: false, show-top: false)
_gap()
})

Binary file not shown.

View File

@ -136,3 +136,10 @@
_seq("[", "a", comment: [Test]) _seq("[", "a", comment: [Test])
_note("left", [This is also a note]) _note("left", [This is also a note])
})*/ })*/
#pagebreak()
#chronos.diagram({
_seq("Bob", "Alice", comment: [Hello])
_evt("Other", "create")
})

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

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

View File

@ -3,6 +3,9 @@
#let COMMENT-PAD = 8 #let COMMENT-PAD = 8
#let LIFELINE-W = 10 #let LIFELINE-W = 10
#let CREATE-OFFSET = 15 #let CREATE-OFFSET = 15
#let DEFAULT-SLANT = 10
#let CROSS-TIP-SIZE = 4
#let CIRCLE-TIP-RADIUS = 3
#let SYM-GAP = 5 #let SYM-GAP = 5
#let PAR-PAD = (5pt, 3pt) #let PAR-PAD = (5pt, 3pt)

View File

@ -1,4 +1,4 @@
#import "utils.typ": get-group-span #import "utils.typ": get-group-span, fit-canvas
#import "renderer.typ": render #import "renderer.typ": render
#import "participant.typ" as participant: _par, PAR-SPECIALS #import "participant.typ" as participant: _par, PAR-SPECIALS
@ -18,7 +18,7 @@
),) ),)
} }
#let diagram(elements) = { #let diagram(elements, width: auto) = {
if elements == none { if elements == none {
return return
} }
@ -123,6 +123,16 @@
elmt: elmt, elmt: elmt,
i: i 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() linked = linked.dedup()
@ -178,7 +188,8 @@
} }
set text(font: "Source Sans 3") set text(font: "Source Sans 3")
render(participants, elmts) let canvas = render(participants, elmts)
fit-canvas(canvas, width: width)
} }
#let from-plantuml(code) = { #let from-plantuml(code) = {

View File

@ -5,3 +5,4 @@
#import "participant.typ": _par #import "participant.typ": _par
#import "separator.typ": _sep #import "separator.typ": _sep
#import "note.typ": _note #import "note.typ": _note
#import "sync.typ": _sync

View File

@ -109,7 +109,7 @@
} }
} else if note.side == "right" { } else if note.side == "right" {
x0 += NOTE-GAP x0 += NOTE-GAP
x0 -= lifelines.at(i).level * LIFELINE-W / 2 x0 += lifelines.at(i).level * LIFELINE-W / 2
} else if note.side == "over" or note.side == "across" { } else if note.side == "over" or note.side == "across" {
x0 -= total-w / 2 x0 -= total-w / 2
} }

View File

@ -21,7 +21,9 @@
invisible: false, invisible: false,
shape: "participant", shape: "participant",
color: rgb("#E2E2F0"), color: rgb("#E2E2F0"),
custom-image: none custom-image: none,
show-bottom: true,
show-top: true,
) = { ) = {
return (( return ((
type: "par", type: "par",
@ -31,7 +33,9 @@
invisible: invisible, invisible: invisible,
shape: shape, shape: shape,
color: color, color: color,
custom-image: custom-image custom-image: custom-image,
show-bottom: show-bottom,
show-top: show-top
),) ),)
} }

View File

@ -5,6 +5,7 @@
#import participant: PAR-SPECIALS #import participant: PAR-SPECIALS
#import "sequence.typ" #import "sequence.typ"
#import "separator.typ" #import "separator.typ"
#import "sync.typ"
#import "consts.typ": * #import "consts.typ": *
#import "note.typ" as note: get-note-box #import "note.typ" as note: get-note-box
@ -19,6 +20,16 @@
let pars-i = get-participants-i(participants) let pars-i = get-participants-i(participants)
let cells = () 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 // Compute max lifeline levels
for elmt in elements { for elmt in elements {
if elmt.type == "seq" { if elmt.type == "seq" {
@ -203,10 +214,11 @@
let draw-sep = separator.render.with(x-pos) let draw-sep = separator.render.with(x-pos)
let draw-par = participant.render.with(x-pos) let draw-par = participant.render.with(x-pos)
let draw-note = note.render.with(pars-i, 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) // Draw participants (start)
for p in participants { for p in participants {
if p.from-start and not p.invisible { if p.from-start and not p.invisible and p.show-top {
shapes += draw-par(p) shapes += draw-par(p)
} }
} }
@ -279,6 +291,7 @@
line.lines.push(("enable", y, elmt.lifeline-style)) line.lines.push(("enable", y, elmt.lifeline-style))
} else if elmt.event == "create" { } else if elmt.event == "create" {
y -= CREATE-OFFSET
shapes += participant.render(x-pos, par, y: y) shapes += participant.render(x-pos, par, y: y)
line.lines.push(("create", y)) line.lines.push(("create", y))
} }
@ -294,6 +307,12 @@
(y, shps) = draw-note(elmt, y, lifelines) (y, shps) = draw-note(elmt, y, lifelines)
shapes += shps shapes += shps
} }
// Synched sequences
} else if elmt.type == "sync" {
let shps
(y, lifelines, shps) = draw-sync(elmt, y, lifelines)
shapes += shps
} }
} }
@ -398,8 +417,10 @@
} }
// Draw participants (end) // Draw participants (end)
if p.show-bottom {
draw-par(p, y: y, bottom: true) draw-par(p, y: y, bottom: true)
} }
}
}) })
shapes shapes

View File

@ -1,4 +1,4 @@
#import "@preview/cetz:0.2.2": draw #import "@preview/cetz:0.2.2": draw, vector
#import "consts.typ": * #import "consts.typ": *
#import "participant.typ" #import "participant.typ"
#import "note.typ" #import "note.typ"
@ -16,14 +16,39 @@
"/": (symbol: ">", fill: color, harpoon: true), "/": (symbol: ">", fill: color, harpoon: true),
"//": (symbol: "straight", harpoon: true), "//": (symbol: "straight", harpoon: true),
"x": none, "x": none,
"o": (symbol: "o"), "o": none,
).at(sym) ).at(sym)
} }
#let reverse-arrow-mark(mark) = {
if type(mark) == array {
return mark.map(m => reverse-arrow-mark(m))
}
let mark2 = mark
if type(mark) == dictionary and mark.at("harpoon", default: false) {
let flipped = mark.at("flip", default: false)
mark2.insert("flip", not flipped)
}
return mark2
}
#let is-tip-of-type(type_, tip) = {
if type(tip) == str and tip == type_ {
return true
}
if type(tip) == array and tip.contains(type_) {
return true
}
return false
}
#let is-circle-tip = is-tip-of-type.with("o")
#let is-cross-tip = is-tip-of-type.with("x")
#let _seq( #let _seq(
p1, p1,
p2, p2,
comment: none, comment: none,
comment-align: "left",
dashed: false, dashed: false,
start-tip: "", start-tip: "",
end-tip: ">", end-tip: ">",
@ -35,13 +60,15 @@
destroy-dst: false, destroy-dst: false,
disable-src: false, disable-src: false,
destroy-src: false, destroy-src: false,
lifeline-style: auto lifeline-style: auto,
slant: none
) = { ) = {
return (( return ((
type: "seq", type: "seq",
p1: p1, p1: p1,
p2: p2, p2: p2,
comment: comment, comment: comment,
comment-align: comment-align,
dashed: dashed, dashed: dashed,
start-tip: start-tip, start-tip: start-tip,
end-tip: end-tip, end-tip: end-tip,
@ -54,6 +81,7 @@
disable-src: disable-src, disable-src: disable-src,
destroy-src: destroy-src, destroy-src: destroy-src,
lifeline-style: lifeline-style, lifeline-style: lifeline-style,
slant: slant
),) ),)
} }
@ -87,27 +115,38 @@
y: y, y: y,
ll-lvl: lifelines.at(i2).level * LIFELINE-W / 2 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 { if elmt.disable-src {
let src-line = lifelines.at(i1) let src-line = lifelines.at(i1)
src-line.level -= 1 src-line.level -= 1
src-line.lines.push(("disable", y)) src-line.lines.push(("disable", start-info.y))
lifelines.at(i1) = src-line lifelines.at(i1) = src-line
} }
if elmt.destroy-src { if elmt.destroy-src {
let src-line = lifelines.at(i1) let src-line = lifelines.at(i1)
src-line.lines.push(("destroy", y)) src-line.lines.push(("destroy", start-info.y))
lifelines.at(i1) = src-line lifelines.at(i1) = src-line
} }
if elmt.disable-dst { if elmt.disable-dst {
let dst-line = lifelines.at(i2) let dst-line = lifelines.at(i2)
dst-line.level -= 1 dst-line.level -= 1
dst-line.lines.push(("disable", y)) dst-line.lines.push(("disable", end-info.y))
lifelines.at(i2) = dst-line lifelines.at(i2) = dst-line
} }
if elmt.destroy-dst { if elmt.destroy-dst {
let dst-line = lifelines.at(i2) let dst-line = lifelines.at(i2)
dst-line.lines.push(("destroy", y)) dst-line.lines.push(("destroy", end-info.y))
lifelines.at(i2) = dst-line lifelines.at(i2) = dst-line
} }
if elmt.enable-dst { if elmt.enable-dst {
@ -120,7 +159,7 @@
let m = measure(box(par.display-name)) let m = measure(box(par.display-name))
let f = if i1 > i2 {-1} else {1} let f = if i1 > i2 {-1} else {1}
end-info.x -= (m.width + PAR-PAD.last() * 2) / 2pt * f end-info.x -= (m.width + PAR-PAD.last() * 2) / 2pt * f
shapes += participant.render(x-pos, par, y: y - CREATE-OFFSET) shapes += participant.render(x-pos, par, y: end-info.y - CREATE-OFFSET)
} }
end-info.ll-lvl = lifelines.at(i2).level * LIFELINE-W / 2 end-info.ll-lvl = lifelines.at(i2).level * LIFELINE-W / 2
@ -156,12 +195,25 @@
) )
) )
let y0 = y let y0 = start-info.y
if "linked-note" in elmt { if "linked-note" in elmt {
let shps = note.render(pars-i, x-pos, elmt.linked-note, y, lifelines).last() let shps = note.render(pars-i, x-pos, elmt.linked-note, start-info.y, lifelines).last()
shapes += shps 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.p1 == elmt.p2 {
if elmt.flip { if elmt.flip {
x1 = start-info.lx x1 = start-info.lx
@ -175,61 +227,144 @@
calc.max(x1, x2) + 20 calc.max(x1, x2) + 20
} }
if elmt.comment != none { pts = (
shapes += draw.content( (x1, start-info.y),
(x1, y), (x-mid, start-info.y),
elmt.comment, (x-mid, end-info.y),
anchor: if elmt.flip {"south-east"} else {"south-west"}, (x2, end-info.y)
padding: 3pt
) )
}
shapes += draw.line( if elmt.comment != none {
(x1, y), comment-anchor = (
(x-mid, y), start: if x-mid < x1 {"south-east"} else {"south-west"},
(x-mid, y - 10), end: if x-mid < x1 {"south-west"} else {"south-east"},
(x2, y - 10), left: "south-west",
..style right: "south-east",
) center: "south",
y -= 10 ).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 { } else {
pts = (
(x1, start-info.y),
(x2, end-info.y)
)
if elmt.comment != none { if elmt.comment != none {
let x = calc.min(x1, x2) let start-pt = pts.first()
if x2 < x1 { let end-pt = pts.last()
x += COMMENT-PAD 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: elmt.color, fill: none, 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: elmt.color, fill: none, 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 elmt.comment != none {
shapes += draw.content( shapes += draw.content(
(x, y), comment-pt,
elmt.comment, elmt.comment,
anchor: "south-west", anchor: comment-anchor,
angle: comment-angle,
padding: 3pt padding: 3pt
) )
} }
shapes += draw.line(
(x1, y),
(x2, y),
..style
)
}
if elmt.enable-dst { if elmt.enable-dst {
let dst-line = lifelines.at(i2) let dst-line = lifelines.at(i2)
dst-line.lines.push(("enable", y, elmt.lifeline-style)) dst-line.lines.push(("enable", end-info.y, elmt.lifeline-style))
lifelines.at(i2) = dst-line lifelines.at(i2) = dst-line
} }
if elmt.create-dst { if elmt.create-dst {
y -= CREATE-OFFSET end-info.y -= CREATE-OFFSET
let dst-line = lifelines.at(i2) let dst-line = lifelines.at(i2)
dst-line.lines.push(("create", y)) dst-line.lines.push(("create", end-info.y))
lifelines.at(i2) = dst-line lifelines.at(i2) = dst-line
} }
if "linked-note" in elmt { if "linked-note" in elmt {
let m = note.get-size(elmt.linked-note) let m = note.get-size(elmt.linked-note)
y = calc.min(y, y0 - m.height / 2) end-info.y = calc.min(end-info.y, y0 - m.height / 2)
} }
let r = (y, lifelines, shapes) let r = (end-info.y, lifelines, shapes)
return r return r
} }

27
src/sync.typ Normal file
View File

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

View File

@ -21,6 +21,10 @@
let (i0, i1) = get-group-span(participants, elmt) let (i0, i1) = get-group-span(participants, elmt)
min-i = calc.min(min-i, i0) min-i = calc.min(min-i, i0)
max-i = calc.max(max-i, i1) max-i = calc.max(max-i, i1)
} else if elmt.type == "sync" {
let (i0, i1) = get-group-span(participants, elmt)
min-i = calc.min(min-i, i0)
max-i = calc.max(max-i, i1)
} }
} }
return (min-i, max-i) return (min-i, max-i)
@ -41,3 +45,26 @@
panic("Invalid type for parameter mods, expected auto or dictionary, got " + str(type(mods))) panic("Invalid type for parameter mods, expected auto or dictionary, got " + str(type(mods)))
} }
#let fit-canvas(canvas, width: auto) = layout(size => {
let m = measure(canvas)
let w = m.width
let h = m.height
let r = if w == 0pt {0} else {
if width == auto {1}
else if type(width) == length {
width / w
} else {
size.width * width / w
}
}
let new-w = w * r
let new-h = h * r
r *= 100%
box(
width: new-w,
height: new-h,
scale(x: r, y: r, reflow: true, canvas)
)
})

View File

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