Compare commits
12 Commits
3b25d68f1e
...
v0.1.0
Author | SHA1 | Date | |
---|---|---|---|
6f563750fb
|
|||
5822d705f2
|
|||
226efa46b7
|
|||
3d9e045b56
|
|||
eb05c41810
|
|||
b9bbe6f93d
|
|||
b5c34c154f
|
|||
030fd3edba
|
|||
cbba14ed04
|
|||
77a65a3199
|
|||
ff6a7e52bc
|
|||
ebd4d8f1ca
|
147
README.md
147
README.md
@ -1,3 +1,150 @@
|
||||
# chronos
|
||||
|
||||
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
|
8
TODO.md
8
TODO.md
@ -10,9 +10,11 @@
|
||||
- [x] Lifelines
|
||||
- [x] Different types of participants
|
||||
- [x] Notes
|
||||
- [ ] Synchronized arrows
|
||||
- [ ] Slanted arrows
|
||||
- [ ] Different types of arrow tips (WIP)
|
||||
- [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
|
||||
|
11
gallery.bash
11
gallery.bash
@ -14,3 +14,14 @@ do
|
||||
typst c --root ./ "$f" "$f2"
|
||||
i=$((i+1))
|
||||
done
|
||||
|
||||
set -- ./gallery/readme/*.typ
|
||||
cnt="$#"
|
||||
i=1
|
||||
for f
|
||||
do
|
||||
f2="${f/typ/png}"
|
||||
echo "($i/$cnt) $f -> $f2"
|
||||
typst c --root ./ "$f" "$f2"
|
||||
i=$((i+1))
|
||||
done
|
||||
|
Binary file not shown.
Binary file not shown.
@ -49,3 +49,121 @@
|
||||
_seq("Alice", "?", comment: [->?\ *short* from actor1])
|
||||
_seq("Alice", "Bob", comment: [->\ from actor1 to actor2])
|
||||
})
|
||||
|
||||
#chronos.diagram({
|
||||
import chronos: *
|
||||
_par("alice", display-name: "Alice")
|
||||
_par("bob", display-name: "Bob")
|
||||
_par("craig", display-name: "Craig")
|
||||
|
||||
_seq("bob", "alice")
|
||||
_seq("bob", "craig")
|
||||
_gap()
|
||||
|
||||
_sync({
|
||||
_seq("bob", "alice", comment: "Synched", comment-align: "start")
|
||||
_seq("bob", "craig", comment: "Synched", comment-align: "start")
|
||||
})
|
||||
_gap()
|
||||
|
||||
_seq("alice", "bob")
|
||||
_seq("craig", "bob")
|
||||
_gap()
|
||||
|
||||
_sync({
|
||||
_seq("alice", "bob")
|
||||
_seq("craig", "bob")
|
||||
})
|
||||
_gap()
|
||||
|
||||
_sync({
|
||||
_seq("alice", "bob", enable-dst: true)
|
||||
_seq("craig", "bob")
|
||||
})
|
||||
_gap()
|
||||
|
||||
_evt("bob", "disable")
|
||||
})
|
||||
|
||||
#chronos.diagram({
|
||||
import chronos: *
|
||||
_par("alice", display-name: "Alice")
|
||||
_par("bob", display-name: "Bob")
|
||||
_par("craig", display-name: "Craig")
|
||||
|
||||
_seq("alice", "bob")
|
||||
_seq("bob", "craig", slant: auto)
|
||||
_seq("alice", "craig", slant: 20)
|
||||
|
||||
_sync({
|
||||
_seq("alice", "bob", slant: 10)
|
||||
_seq("craig", "bob", slant: 20)
|
||||
})
|
||||
|
||||
_sync({
|
||||
_seq("alice", "bob", slant: auto)
|
||||
_seq("bob", "alice", slant: auto)
|
||||
})
|
||||
|
||||
_gap()
|
||||
_evt("bob", "disable")
|
||||
})
|
||||
|
||||
#grid(columns: 2, column-gutter: 2em,
|
||||
chronos.diagram({
|
||||
import chronos: *
|
||||
|
||||
_par("alice", display-name: "Alice")
|
||||
_par("bob", display-name: "Bob")
|
||||
_seq("alice", "bob", comment: "This is a very long comment")
|
||||
|
||||
// Left to right
|
||||
_seq("alice", "bob", comment: "Start aligned", comment-align: "start")
|
||||
_seq("alice", "bob", comment: "End aligned", comment-align: "end")
|
||||
_seq("alice", "bob", comment: "Left aligned", comment-align: "left")
|
||||
_seq("alice", "bob", comment: "Right aligned", comment-align: "right")
|
||||
_seq("alice", "bob", comment: "Centered", comment-align: "center")
|
||||
_gap()
|
||||
|
||||
// Right to left
|
||||
_seq("bob", "alice", comment: "Start aligned", comment-align: "start")
|
||||
_seq("bob", "alice", comment: "End aligned", comment-align: "end")
|
||||
_seq("bob", "alice", comment: "Left aligned", comment-align: "left")
|
||||
_seq("bob", "alice", comment: "Right aligned", comment-align: "right")
|
||||
_seq("bob", "alice", comment: "Centered", comment-align: "center")
|
||||
_gap()
|
||||
|
||||
// Slant left to right
|
||||
_seq("alice", "bob", comment: "Start aligned", comment-align: "start", slant: 10)
|
||||
_seq("alice", "bob", comment: "End aligned", comment-align: "end", slant: 10)
|
||||
_seq("alice", "bob", comment: "Left aligned", comment-align: "left", slant: 10)
|
||||
_seq("alice", "bob", comment: "Right aligned", comment-align: "right", slant: 10)
|
||||
_seq("alice", "bob", comment: "Centered", comment-align: "center", slant: 10)
|
||||
_gap()
|
||||
|
||||
// Slant right to left
|
||||
_seq("bob", "alice", comment: "Start aligned", comment-align: "start", slant: 10)
|
||||
_seq("bob", "alice", comment: "End aligned", comment-align: "end", slant: 10)
|
||||
_seq("bob", "alice", comment: "Left aligned", comment-align: "left", slant: 10)
|
||||
_seq("bob", "alice", comment: "Right aligned", comment-align: "right", slant: 10)
|
||||
_seq("bob", "alice", comment: "Centered", comment-align: "center", slant: 10)
|
||||
}),
|
||||
|
||||
chronos.diagram({
|
||||
import chronos: *
|
||||
|
||||
_par("alice", display-name: "Alice")
|
||||
|
||||
_seq("alice", "alice", comment: "Start aligned", comment-align: "start")
|
||||
_seq("alice", "alice", comment: "End aligned", comment-align: "end")
|
||||
_seq("alice", "alice", comment: "Left aligned", comment-align: "left")
|
||||
_seq("alice", "alice", comment: "Right aligned", comment-align: "right")
|
||||
_seq("alice", "alice", comment: "Centered", comment-align: "center")
|
||||
|
||||
_seq("alice", "alice", comment: "Start aligned", comment-align: "start", flip: true)
|
||||
_seq("alice", "alice", comment: "End aligned", comment-align: "end", flip: true)
|
||||
_seq("alice", "alice", comment: "Left aligned", comment-align: "left", flip: true)
|
||||
_seq("alice", "alice", comment: "Right aligned", comment-align: "right", flip: true)
|
||||
_seq("alice", "alice", comment: "Centered", comment-align: "center", flip: true)
|
||||
})
|
||||
)
|
Binary file not shown.
@ -65,11 +65,11 @@ chronos.diagram({
|
||||
_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", 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", 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`)
|
||||
@ -78,6 +78,34 @@ chronos.diagram({
|
||||
_seq("a", "b", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`)
|
||||
}),
|
||||
|
||||
chronos.diagram({
|
||||
import chronos: *
|
||||
|
||||
_par("a", display-name: "Alice")
|
||||
_par("b", display-name: "Bob")
|
||||
|
||||
_seq("b", "a", end-tip: ">", comment: `->`)
|
||||
_seq("b", "a", end-tip: ">>", comment: `->>`)
|
||||
_seq("b", "a", end-tip: "\\", comment: `-\`)
|
||||
_seq("b", "a", end-tip: "\\\\", comment: `-\\`)
|
||||
_seq("b", "a", end-tip: "/", comment: `-/`)
|
||||
_seq("b", "a", end-tip: "//", comment: `-//`)
|
||||
_seq("b", "a", end-tip: "x", comment: `->x`)
|
||||
_seq("b", "a", start-tip: "x", comment: `x->`)
|
||||
_seq("b", "a", start-tip: "o", comment: `o->`)
|
||||
_seq("b", "a", end-tip: ("o", ">"), comment: `->o`)
|
||||
_seq("b", "a", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`)
|
||||
_seq("b", "a", start-tip: ">", end-tip: ">", comment: `<->`)
|
||||
_seq("b", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`)
|
||||
_seq("b", "a", start-tip: "x", end-tip: "x", comment: `x<->x`)
|
||||
_seq("b", "a", end-tip: ("o", ">>"), comment: `->>o`)
|
||||
_seq("b", "a", end-tip: ("o", "\\"), comment: `-\o`)
|
||||
_seq("b", "a", end-tip: ("o", "\\\\"), comment: `-\\o`)
|
||||
_seq("b", "a", end-tip: ("o", "/"), comment: `-/o`)
|
||||
_seq("b", "a", end-tip: ("o", "//"), comment: `-//o`)
|
||||
_seq("b", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`)
|
||||
}),
|
||||
|
||||
chronos.diagram({
|
||||
import chronos: *
|
||||
|
||||
@ -93,11 +121,11 @@ chronos.diagram({
|
||||
_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", 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", 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`)
|
||||
@ -106,3 +134,14 @@ chronos.diagram({
|
||||
_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.
BIN
gallery/readme/boilerplate.pdf
Normal file
BIN
gallery/readme/boilerplate.pdf
Normal file
Binary file not shown.
BIN
gallery/readme/boilerplate.png
Normal file
BIN
gallery/readme/boilerplate.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
13
gallery/readme/boilerplate.typ
Normal file
13
gallery/readme/boilerplate.typ
Normal 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")
|
||||
})
|
BIN
gallery/readme/lifelines.pdf
Normal file
BIN
gallery/readme/lifelines.pdf
Normal file
Binary file not shown.
BIN
gallery/readme/lifelines.png
Normal file
BIN
gallery/readme/lifelines.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
24
gallery/readme/lifelines.typ
Normal file
24
gallery/readme/lifelines.typ
Normal 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)
|
||||
})
|
BIN
gallery/readme/simple_sequence.pdf
Normal file
BIN
gallery/readme/simple_sequence.pdf
Normal file
Binary file not shown.
BIN
gallery/readme/simple_sequence.png
Normal file
BIN
gallery/readme/simple_sequence.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
17
gallery/readme/simple_sequence.typ
Normal file
17
gallery/readme/simple_sequence.typ
Normal 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")
|
||||
})
|
@ -3,6 +3,9 @@
|
||||
#let COMMENT-PAD = 8
|
||||
#let LIFELINE-W = 10
|
||||
#let CREATE-OFFSET = 15
|
||||
#let DEFAULT-SLANT = 10
|
||||
#let CROSS-TIP-SIZE = 4
|
||||
#let CIRCLE-TIP-RADIUS = 3
|
||||
|
||||
#let SYM-GAP = 5
|
||||
#let PAR-PAD = (5pt, 3pt)
|
||||
|
@ -1,4 +1,4 @@
|
||||
#import "utils.typ": get-group-span
|
||||
#import "utils.typ": get-group-span, fit-canvas
|
||||
#import "renderer.typ": render
|
||||
#import "participant.typ" as participant: _par, PAR-SPECIALS
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
),)
|
||||
}
|
||||
|
||||
#let diagram(elements) = {
|
||||
#let diagram(elements, width: auto) = {
|
||||
if elements == none {
|
||||
return
|
||||
}
|
||||
@ -188,7 +188,8 @@
|
||||
}
|
||||
|
||||
set text(font: "Source Sans 3")
|
||||
render(participants, elmts)
|
||||
let canvas = render(participants, elmts)
|
||||
fit-canvas(canvas, width: width)
|
||||
}
|
||||
|
||||
#let from-plantuml(code) = {
|
||||
|
@ -5,3 +5,4 @@
|
||||
#import "participant.typ": _par
|
||||
#import "separator.typ": _sep
|
||||
#import "note.typ": _note
|
||||
#import "sync.typ": _sync
|
@ -109,7 +109,7 @@
|
||||
}
|
||||
} else if note.side == "right" {
|
||||
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" {
|
||||
x0 -= total-w / 2
|
||||
}
|
||||
|
@ -21,7 +21,9 @@
|
||||
invisible: false,
|
||||
shape: "participant",
|
||||
color: rgb("#E2E2F0"),
|
||||
custom-image: none
|
||||
custom-image: none,
|
||||
show-bottom: true,
|
||||
show-top: true,
|
||||
) = {
|
||||
return ((
|
||||
type: "par",
|
||||
@ -31,7 +33,9 @@
|
||||
invisible: invisible,
|
||||
shape: shape,
|
||||
color: color,
|
||||
custom-image: custom-image
|
||||
custom-image: custom-image,
|
||||
show-bottom: show-bottom,
|
||||
show-top: show-top
|
||||
),)
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
#import participant: PAR-SPECIALS
|
||||
#import "sequence.typ"
|
||||
#import "separator.typ"
|
||||
#import "sync.typ"
|
||||
#import "consts.typ": *
|
||||
#import "note.typ" as note: get-note-box
|
||||
|
||||
@ -19,6 +20,16 @@
|
||||
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" {
|
||||
@ -203,10 +214,11 @@
|
||||
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 {
|
||||
if p.from-start and not p.invisible and p.show-top {
|
||||
shapes += draw-par(p)
|
||||
}
|
||||
}
|
||||
@ -295,6 +307,12 @@
|
||||
(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
|
||||
}
|
||||
}
|
||||
|
||||
@ -399,7 +417,9 @@
|
||||
}
|
||||
|
||||
// Draw participants (end)
|
||||
draw-par(p, y: y, bottom: true)
|
||||
if p.show-bottom {
|
||||
draw-par(p, y: y, bottom: true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
221
src/sequence.typ
221
src/sequence.typ
@ -1,4 +1,4 @@
|
||||
#import "@preview/cetz:0.2.2": draw
|
||||
#import "@preview/cetz:0.2.2": draw, vector
|
||||
#import "consts.typ": *
|
||||
#import "participant.typ"
|
||||
#import "note.typ"
|
||||
@ -16,14 +16,39 @@
|
||||
"/": (symbol: ">", fill: color, harpoon: true),
|
||||
"//": (symbol: "straight", harpoon: true),
|
||||
"x": none,
|
||||
"o": (symbol: "o"),
|
||||
"o": none,
|
||||
).at(sym)
|
||||
}
|
||||
|
||||
#let reverse-arrow-mark(mark) = {
|
||||
if type(mark) == array {
|
||||
return mark.map(m => reverse-arrow-mark(m))
|
||||
}
|
||||
let mark2 = mark
|
||||
if type(mark) == dictionary and mark.at("harpoon", default: false) {
|
||||
let flipped = mark.at("flip", default: false)
|
||||
mark2.insert("flip", not flipped)
|
||||
}
|
||||
return mark2
|
||||
}
|
||||
|
||||
#let is-tip-of-type(type_, tip) = {
|
||||
if type(tip) == str and tip == type_ {
|
||||
return true
|
||||
}
|
||||
if type(tip) == array and tip.contains(type_) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
#let is-circle-tip = is-tip-of-type.with("o")
|
||||
#let is-cross-tip = is-tip-of-type.with("x")
|
||||
|
||||
#let _seq(
|
||||
p1,
|
||||
p2,
|
||||
comment: none,
|
||||
comment-align: "left",
|
||||
dashed: false,
|
||||
start-tip: "",
|
||||
end-tip: ">",
|
||||
@ -35,13 +60,15 @@
|
||||
destroy-dst: false,
|
||||
disable-src: false,
|
||||
destroy-src: false,
|
||||
lifeline-style: auto
|
||||
lifeline-style: auto,
|
||||
slant: none
|
||||
) = {
|
||||
return ((
|
||||
type: "seq",
|
||||
p1: p1,
|
||||
p2: p2,
|
||||
comment: comment,
|
||||
comment-align: comment-align,
|
||||
dashed: dashed,
|
||||
start-tip: start-tip,
|
||||
end-tip: end-tip,
|
||||
@ -54,6 +81,7 @@
|
||||
disable-src: disable-src,
|
||||
destroy-src: destroy-src,
|
||||
lifeline-style: lifeline-style,
|
||||
slant: slant
|
||||
),)
|
||||
}
|
||||
|
||||
@ -87,27 +115,38 @@
|
||||
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", y))
|
||||
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", y))
|
||||
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", y))
|
||||
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", y))
|
||||
dst-line.lines.push(("destroy", end-info.y))
|
||||
lifelines.at(i2) = dst-line
|
||||
}
|
||||
if elmt.enable-dst {
|
||||
@ -120,7 +159,7 @@
|
||||
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: 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
|
||||
@ -156,12 +195,25 @@
|
||||
)
|
||||
)
|
||||
|
||||
let y0 = y
|
||||
let y0 = start-info.y
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
@ -175,61 +227,144 @@
|
||||
calc.max(x1, x2) + 20
|
||||
}
|
||||
|
||||
if elmt.comment != none {
|
||||
shapes += draw.content(
|
||||
(x1, y),
|
||||
elmt.comment,
|
||||
anchor: if elmt.flip {"south-east"} else {"south-west"},
|
||||
padding: 3pt
|
||||
)
|
||||
}
|
||||
|
||||
shapes += draw.line(
|
||||
(x1, y),
|
||||
(x-mid, y),
|
||||
(x-mid, y - 10),
|
||||
(x2, y - 10),
|
||||
..style
|
||||
pts = (
|
||||
(x1, start-info.y),
|
||||
(x-mid, start-info.y),
|
||||
(x-mid, end-info.y),
|
||||
(x2, end-info.y)
|
||||
)
|
||||
y -= 10
|
||||
|
||||
if elmt.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 elmt.comment != none {
|
||||
let x = calc.min(x1, x2)
|
||||
if x2 < x1 {
|
||||
x += COMMENT-PAD
|
||||
let start-pt = pts.first()
|
||||
let end-pt = pts.last()
|
||||
if elmt.start-tip != "" {
|
||||
start-pt = (pts.first(), COMMENT-PAD, pts.last())
|
||||
}
|
||||
shapes += draw.content(
|
||||
(x, y),
|
||||
elmt.comment,
|
||||
anchor: "south-west",
|
||||
padding: 3pt
|
||||
)
|
||||
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(
|
||||
(x1, y),
|
||||
(x2, y),
|
||||
..style
|
||||
(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 {
|
||||
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
|
||||
}
|
||||
if elmt.create-dst {
|
||||
y -= CREATE-OFFSET
|
||||
end-info.y -= CREATE-OFFSET
|
||||
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
|
||||
}
|
||||
|
||||
if "linked-note" in elmt {
|
||||
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
|
||||
}
|
27
src/sync.typ
Normal file
27
src/sync.typ
Normal 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
|
||||
}
|
@ -21,6 +21,10 @@
|
||||
let (i0, i1) = get-group-span(participants, elmt)
|
||||
min-i = calc.min(min-i, i0)
|
||||
max-i = calc.max(max-i, i1)
|
||||
} else if elmt.type == "sync" {
|
||||
let (i0, i1) = get-group-span(participants, elmt)
|
||||
min-i = calc.min(min-i, i0)
|
||||
max-i = calc.max(max-i, i1)
|
||||
}
|
||||
}
|
||||
return (min-i, max-i)
|
||||
@ -41,3 +45,26 @@
|
||||
|
||||
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)
|
||||
)
|
||||
})
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chronos"
|
||||
version = "0.0.1"
|
||||
version = "0.1.0"
|
||||
compiler = "0.11.0"
|
||||
repository = "https://git.kb28.ch/HEL/chronos"
|
||||
entrypoint = "src/lib.typ"
|
||||
@ -11,4 +11,4 @@ categories = ["visualization"]
|
||||
license = "Apache-2.0"
|
||||
description = "A package to draw sequence diagrams with CeTZ"
|
||||
keywords = ["sequence", "diagram", "plantuml"]
|
||||
exclude = [ "/gallery/*" ]
|
||||
exclude = [ "gallery", "gallery.bash" ]
|
||||
|
Reference in New Issue
Block a user