10 Commits

26 changed files with 534 additions and 53 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

View File

@ -10,9 +10,11 @@
- [x] Lifelines - [x] Lifelines
- [x] Different types of participants - [x] Different types of participants
- [x] Notes - [x] Notes
- [ ] Synchronized arrows - [x] Synchronized arrows
- [ ] Slanted arrows - [x] Slanted arrows
- [ ] Different types of arrow tips (WIP) - [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 column spacing with notes over multiple columns
- [ ] Fix notes with arrows from start / to end / small arrows - [ ] Fix notes with arrows from start / to end / small arrows
- [ ] Fix group size with self arrows + notes - [ ] Fix group size with self arrows + notes

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

@ -61,8 +61,8 @@
_gap() _gap()
_sync({ _sync({
_seq("bob", "alice") _seq("bob", "alice", comment: "Synched", comment-align: "start")
_seq("bob", "craig") _seq("bob", "craig", comment: "Synched", comment-align: "start")
}) })
_gap() _gap()
@ -108,3 +108,62 @@
_gap() _gap()
_evt("bob", "disable") _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.

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

@ -4,6 +4,8 @@
#let LIFELINE-W = 10 #let LIFELINE-W = 10
#let CREATE-OFFSET = 15 #let CREATE-OFFSET = 15
#let DEFAULT-SLANT = 10 #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
} }
@ -188,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

@ -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

@ -20,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" {
@ -208,7 +218,7 @@
// 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)
} }
} }
@ -407,7 +417,9 @@
} }
// Draw participants (end) // Draw participants (end)
draw-par(p, y: y, bottom: true) if p.show-bottom {
draw-par(p, y: y, bottom: true)
}
} }
}) })

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: ">",
@ -43,6 +68,7 @@
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,
@ -175,6 +201,19 @@
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
@ -188,43 +227,127 @@
calc.max(x1, x2) + 20 calc.max(x1, x2) + 20
} }
if elmt.comment != none { pts = (
shapes += draw.content(
(x1, start-info.y),
elmt.comment,
anchor: if elmt.flip {"south-east"} else {"south-west"},
padding: 3pt
)
}
shapes += draw.line(
(x1, start-info.y), (x1, start-info.y),
(x-mid, start-info.y), (x-mid, start-info.y),
(x-mid, end-info.y), (x-mid, end-info.y),
(x2, end-info.y), (x2, end-info.y)
..style
) )
} else {
if elmt.comment != none { if elmt.comment != none {
let x = calc.min(x1, x2) comment-anchor = (
if x2 < x1 { start: if x-mid < x1 {"south-east"} else {"south-west"},
x += COMMENT-PAD end: if x-mid < x1 {"south-west"} else {"south-east"},
} left: "south-west",
shapes += draw.content( right: "south-east",
(x, start-info.y), center: "south",
elmt.comment, ).at(elmt.comment-align)
anchor: "south-west",
padding: 3pt 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)
} }
shapes += draw.line( } else {
pts = (
(x1, start-info.y), (x1, start-info.y),
(x2, end-info.y), (x2, end-info.y)
..style )
if elmt.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: 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(
comment-pt,
elmt.comment,
anchor: comment-anchor,
angle: comment-angle,
padding: 3pt
) )
} }
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", end-info.y, elmt.lifeline-style)) dst-line.lines.push(("enable", end-info.y, elmt.lifeline-style))

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" ]