2 Commits

Author SHA1 Message Date
f369fc6e43 added delay to parser 2024-10-08 12:14:20 +02:00
0bfe68b429 added plantuml parser 2024-10-08 11:46:44 +02:00
83 changed files with 3782 additions and 2625 deletions

View File

@ -1,31 +0,0 @@
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.2.2
- 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

View File

@ -15,7 +15,7 @@ This package lets you render sequence diagrams directly in Typst. The following
<td>
```typst
#import "@preview/chronos:0.2.2"
#import "@preview/chronos:0.1.0"
#chronos.diagram({
import chronos: *
_par("Alice")

View File

@ -5,7 +5,7 @@
chronos: chronos
)
#let example(src, show-src: true, vertical: false, fill: false, wrap: true) = {
#let example(src, show-src: true, vertical: false, fill: true, wrap: true) = {
src = src.text
let full-src = example-preamble + src
let body = eval(full-src, scope: example-scope)
@ -16,15 +16,15 @@
box(
stroke: black + 1pt,
radius: .5em,
fill: if fill {color.white.darken(2%)} else {none},
fill: if fill {color.white.darken(5%)} else {none},
if show-src {
let src-block = raw(src, block: true, lang: "typc")
let src-block = align(left, raw(src, lang: "typc"))
table(
columns: if vertical {1} else {2},
inset: 5pt,
inset: 1em,
align: horizon + center,
stroke: none,
table.cell(inset: 1em, img),
img,
if vertical {table.hline()} else {table.vline()}, src-block
)
} else {

View File

@ -1,23 +1,8 @@
#import "example.typ": example
#let seq-return = example(```
_seq(
"Bob", "Alice",
comment: [hello],
enable-dst: true
)
_seq(
"Alice", "Alice",
comment: [some action]
)
_ret(comment: [bye])
```)
#let seq-comm-align = example(```
_par("p1",
display-name: "Start participant")
_par("p2",
display-name: "End participant")
_par("p1", display-name: "Start participant")
_par("p2", display-name: "End participant")
let alignments = (
"start", "end",
"left", "right",
@ -105,15 +90,15 @@ _par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_loop("default loop", {
_seq("a", "b", comment: "Are you here?")
_seq("a", "b", comment: "Are you here ?")
})
_gap()
_loop("min loop", min: 1, {
_seq("a", "b", comment: "Are you here?")
_seq("a", "b", comment: "Are you here ?")
})
_gap()
_loop("min-max loop", min: 1, max: 5, {
_seq("a", "b", comment: "Are you still here?")
_seq("a", "b", comment: "Are you still here ?")
})
```)
@ -136,58 +121,15 @@ _sync({
})
```)
#let gaps = example(```
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
#let gaps-seps = example(```
_par("alice", display-name: "Alice")
_par("bob", display-name: "Bob")
_seq("a", "b", comment: [message 1])
_seq("b", "a", comment: [ok], dashed: true)
_gap()
_seq("a", "b", comment: [message 2])
_seq("b", "a", comment: [ok], dashed: true)
_gap(size: 40)
_seq("a", "b", comment: [message 3])
_seq("b", "a", comment: [ok], dashed: true)
```)
#let seps = example(```
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_sep[Initialization]
_seq("a", "b", comment: [Request 1])
_seq(
"b", "a",
comment: [Response 1],
dashed: true
)
_sep[Repetition]
_seq("a", "b", comment: [Request 2])
_seq(
"b", "a",
comment: [Response 2],
dashed: true
)
```)
#let delays = example(```
_par("a", display-name: "Alice")
_par("b", display-name: "Bob")
_seq("a", "b", comment: [Auth Request])
_delay()
_seq(
"b", "a",
comment: [Auth Response],
dashed: true
)
_delay(name: [5 minutes later])
_seq(
"b", "a",
comment: [Good Bye !],
dashed: true
)
_seq("alice", "bob", comment: "Hello")
_gap(size: 10)
_seq("bob", "alice", comment: "Hi")
_sep("Another day")
_seq("alice", "bob", comment: "Hello again")
```)
#let notes-shapes = example(```

View File

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

View File

@ -34,7 +34,6 @@
/// - 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
@ -46,11 +45,6 @@
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,
@ -64,12 +58,10 @@
/// - width (auto, int, float, length): Optional fixed width of the column\ If the column's content (e.g. sequence comments) is larger, it will overflow
/// - margin (int, float, length): Additional margin to add to the column\ This margin is not included in `width` and `min-width`, but rather added separately
/// - min-width (int, float, length): Minimum width of the column\ If set to a larger value than `width`, the latter will be overriden
/// - max-width (int, float, length, none): Maximum width of the column\ If set to a lower value than `width`, the latter will be overriden\ If set to `none`, no restriction is applied
#let _col(
p1,
p2,
width: auto,
margin: 0,
min-width: 0,
max-width: none
min-width: 0
) = {}

View File

@ -42,17 +42,6 @@
slant: none
) = {}
/// Creates a return sequence
/// #examples.seq-return
/// - comment (none, content): Optional comment to display along the arrow
#let _ret(comment: none) = {}
/// Accepted values for `comment-align` argument of @@_seq()
/// #examples.seq-comm-align
#let comment-align = (
"start", "end", "left", "center", "right"
)
/// Accepted values for `event` argument of @@_evt()
///
/// `EVENTS = ("create", "destroy", "enable", "disable")`
@ -62,4 +51,10 @@
/// #examples.seq-tips
#let tips = (
"", ">", ">>", "\\", "\\\\", "/", "//", "x", "o",
)
/// Accepted values for `comment-align` argument of @@_seq()
/// #examples.seq-comm-align
#let comment-align = (
"start", "end", "left", "center", "right"
)

27
gallery.bash Normal file
View File

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

BIN
gallery/doc_examples.pdf Normal file

Binary file not shown.

689
gallery/doc_examples.typ Normal file
View File

@ -0,0 +1,689 @@
#import "../src/lib.typ": from-plantuml
#set page(width: auto, height: auto)
#let examples = (
(
[Basic Examples],
```
@startuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response
Alice -> Bob: Another authentication Request
Alice <-- Bob: Another authentication Response
@enduml
```
),
(
[Declaring participant],
```
@startuml
participant Participant as Foo
actor Actor as Foo1
boundary Boundary as Foo2
control Control as Foo3
entity Entity as Foo4
database Database as Foo5
collections Collections as Foo6
queue Queue as Foo7
Foo -> Foo1 : To actor
Foo -> Foo2 : To boundary
Foo -> Foo3 : To control
Foo -> Foo4 : To entity
Foo -> Foo5 : To database
Foo -> Foo6 : To collections
Foo -> Foo7: To queue
@enduml
```
),
(
[Declaring participant (2)],
```
@startuml
actor Bob #red
' The only difference between actor
'and participant is the drawing
participant Alice
participant "I have a really\nlong name" as L #99FF99
/' You can also declare:
participant L as "I have a really\nlong name" #99FF99
'/
Alice->Bob: Authentication Request
Bob->Alice: Authentication Response
Bob->L: Log transaction
@enduml
```
),
(
[Use non-letters in participants],
```
@startuml
Alice -> "Bob()" : Hello
"Bob()" -> "This is very\nlong" as Long
' You can also declare:
' "Bob()" -> Long as "This is very\nlong"
Long --> "Bob()" : ok
@enduml
```
),
(
[Message to Self],
```
@startuml
Alice -> Alice: This is a signal to self.\nIt also demonstrates\nmultiline \ntext
@enduml
```
),
(
[Message to Self (2)],
```
@startuml
Alice <- Alice: This is a signal to self.\nIt also demonstrates\nmultiline \ntext
@enduml
```
),
(
[Change arrow style],
```
@startuml
Bob ->x Alice
Bob -> Alice
Bob ->> Alice
Bob -\ Alice
Bob \\- Alice
Bob //-- Alice
Bob ->o Alice
Bob o\\-- Alice
Bob <-> Alice
Bob <->o Alice
@enduml
```
),
(
[Grouping message],
```
@startuml
Alice -> Bob: Authentication Request
alt successful case
Bob -> Alice: Authentication Accepted
else some kind of failure
Bob -> Alice: Authentication Failure
group My own label
Alice -> Log : Log attack start
loop 1000 times
Alice -> Bob: DNS Attack
end
Alice -> Log : Log attack end
end
else Another type of failure
Bob -> Alice: Please repeat
end
@enduml
```
),
(
[Secondary group label],
```
@startuml
Alice -> Bob: Authentication Request
Bob -> Alice: Authentication Failure
group My own label [My own label 2]
Alice -> Log : Log attack start
loop 1000 times
Alice -> Bob: DNS Attack
end
Alice -> Log : Log attack end
end
@enduml
```
),
(
[Notes on messages],
```
@startuml
Alice->Bob : hello
note left: this is a first note
Bob->Alice : ok
note right: this is another note
Bob->Bob : I am thinking
note left
a note
can also be defined
on several lines
end note
@enduml
```
),
(
[Some other notes],
```
@startuml
participant Alice
participant Bob
note left of Alice #aqua
This is displayed
left of Alice.
end note
note right of Alice: This is displayed right of Alice.
note over Alice: This is displayed over Alice.
note over Alice, Bob #FFAAAA: This is displayed\n over Bob and Alice.
note over Bob, Alice
This is yet another
example of
a long note.
end note
@enduml
```
),
(
[Changing notes shape \[hnote, rnote\]],
```
@startuml
caller -> server : conReq
hnote over caller : idle
caller <- server : conConf
rnote over server
"r" as rectangle
"h" as hexagon
endrnote
rnote over server
this is
on several
lines
endrnote
hnote over caller
this is
on several
lines
endhnote
@enduml
```
),
(
[Note over all participants \[across\]],
```
@startuml
Alice->Bob:m1
Bob->Charlie:m2
note over Alice, Charlie: Old method for note over all part. with:\n ""note over //FirstPart, LastPart//"".
note across: New method with:\n""note across""
Bob->Alice
hnote across:Note across all part.
@enduml
```
),
(
[Several notes aligned at the same level \[/\]],
```
@startuml
note over Alice : initial state of Alice
note over Bob : initial state of Bob
Bob -> Alice : hello
@enduml
```
),
(
[Several notes aligned at the same level \[/\] (2)],
```
@startuml
note over Alice : initial state of Alice
/ note over Bob : initial state of Bob
Bob -> Alice : hello
@enduml
```
),
(
[Divider or separator],
```
@startuml
== Initialization ==
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response
== Repetition ==
Alice -> Bob: Another authentication Request
Alice <-- Bob: another authentication Response
@enduml
```
),
(
[Delay],
```
@startuml
Alice -> Bob: Authentication Request
...
Bob --> Alice: Authentication Response
...5 minutes later...
Bob --> Alice: Good Bye !
@enduml
```
),
(
[Space],
```
@startuml
Alice -> Bob: message 1
Bob --> Alice: ok
|||
Alice -> Bob: message 2
Bob --> Alice: ok
||45||
Alice -> Bob: message 3
Bob --> Alice: ok
@enduml
```
),
(
[Lifeline Activation and Destruction],
```
@startuml
participant User
User -> A: DoWork
activate A
A -> B: << createRequest >>
activate B
B -> C: DoWork
activate C
C --> B: WorkDone
destroy C
B --> A: RequestCreated
deactivate B
A -> User: Done
deactivate A
@enduml
```
),
(
[Lifeline Activation and Destruction (2)],
```
@startuml
participant User
User -> A: DoWork
activate A #FFBBBB
A -> A: Internal call
activate A #DarkSalmon
A -> B: << createRequest >>
activate B
B --> A: RequestCreated
deactivate B
deactivate A
A -> User: Done
deactivate A
@enduml
```
),
/*(
[Lifeline Activation and Destruction (3)],
```
@startuml
'autoactivate on
alice -> bob : hello
bob -> bob : self call
bill -> bob /'#005500'/ : hello from thread 2
bob -> george ** : create
return done in thread 2
return rc
bob -> george !! : delete
return success
@enduml
```
),*/
(
[Return],
```
@startuml
Bob -> Alice : hello
activate Alice
Alice -> Alice : some action
return bye
@enduml
```
),
(
[Participant creation],
```
@startuml
Bob -> Alice : hello
create Other
Alice -> Other : new
create /'control'/ String
Alice -> String
note right : You can also put notes!
Alice --> Bob : ok
@enduml
```
),
(
[Shortcut syntax for activation, deactivation, creation],
```
@startuml
alice -> bob ++ : hello
bob -> bob ++ : self call
bob -> bib ++ /' #005500'/ : hello
bob -> george ** : create
return done
return rc
bob -> george !! : delete
return success
@enduml
```
),
(
[Shortcut syntax for activation, deactivation, creation (2)],
```
@startuml
alice -> bob ++ : hello1
bob -> charlie --++ : hello2
charlie --> alice -- : ok
@enduml
```
),
(
[Shortcut syntax for activation, deactivation, creation (3)],
```
@startuml
alice -> bob ++ /'#gold'/: hello
bob -> alice --++ /'#gold'/: you too
alice -> bob --: step1
alice -> bob : step2
@enduml
@enduml
```
),
(
[Incoming and outgoing messages],
```
@startuml
[-> A: DoWork
activate A
A -> A: Internal call
activate A
A ->] : << createRequest >>
A<--] : RequestCreated
deactivate A
[<- A: Done
deactivate A
@enduml
```
),
(
[Incoming and outgoing messages (2)],
```
@startuml
participant Alice
participant Bob #lightblue
Alice -> Bob
Bob -> Carol
...
[-> Bob
[o-> Bob
[o->o Bob
[x-> Bob
...
[<- Bob
[x<- Bob
...
Bob ->]
Bob ->o]
Bob o->o]
Bob ->x]
...
Bob <-]
Bob x<-]
@enduml
```
),
(
[Short arrows for incoming and outgoing messages],
```
@startuml
?-> Alice : ""?->""\n**short** to actor1
[-> Alice : ""[->""\n**from start** to actor1
[-> Bob : ""[->""\n**from start** to actor2
?-> Bob : ""?->""\n**short** to actor2
Alice ->] : ""->]""\nfrom actor1 **to end**
Alice ->? : ""->?""\n**short** from actor1
Alice -> Bob : ""->"" \nfrom actor1 to actor2
@enduml
```
),
(
[Normal arrow],
```
@startuml
participant Alice as a
participant Bob as b
a -> b : ""-> ""
a ->> b : ""->> ""
a -\ b : ""-\ ""
a -\\ b : ""-\\\\""
a -/ b : ""-/ ""
a -// b : ""-// ""
a ->x b : ""->x ""
a x-> b : ""x-> ""
a o-> b : ""o-> ""
a ->o b : ""->o ""
a o->o b : ""o->o ""
a <-> b : ""<-> ""
a o<->o b : ""o<->o""
a x<->x b : ""x<->x""
a ->>o b : ""->>o ""
a -\o b : ""-\o ""
a -\\o b : ""-\\\\o""
a -/o b : ""-/o ""
a -//o b : ""-//o ""
a x->o b : ""x->o ""
@enduml
```
),
(
[Itself arrow],
```
@startuml
participant Alice as a
participant Bob as b
a -> a : ""-> ""
a ->> a : ""->> ""
a -\ a : ""-\ ""
a -\\ a : ""-\\\\""
a -/ a : ""-/ ""
a -// a : ""-// ""
a ->x a : ""->x ""
a x-> a : ""x-> ""
a o-> a : ""o-> ""
a ->o a : ""->o ""
a o->o a : ""o->o ""
a <-> a : ""<-> ""
a o<->o a : ""o<->o""
a x<->x a : ""x<->x""
a ->>o a : ""->>o ""
a -\o a : ""-\o ""
a -\\o a : ""-\\\\o""
a -/o a : ""-/o ""
a -//o a : ""-//o ""
a x->o a : ""x->o ""
@enduml
```
),
(
[Incoming messages (with '|')],
```
@startuml
participant Alice as a
participant Bob as b
[-> b : ""[-> ""
[->> b : ""[->> ""
[-\ b : ""[-\ ""
[-\\ b : ""[-\\\\""
[-/ b : ""[-/ ""
[-// b : ""[-// ""
[->x b : ""[->x ""
[x-> b : ""[x-> ""
[o-> b : ""[o-> ""
[->o b : ""[->o ""
[o->o b : ""[o->o ""
[<-> b : ""[<-> ""
[o<->o b : ""[o<->o""
[x<->x b : ""[x<->x""
[->>o b : ""[->>o ""
[-\o b : ""[-\o ""
[-\\o b : ""[-\\\\o""
[-/o b : ""[-/o ""
[-//o b : ""[-//o ""
[x->o b : ""[x->o ""
@enduml
```
),
(
[Outgoing messages (with '|')],
```
@startuml
participant Alice as a
participant Bob as b
a ->] : ""->] ""
a ->>] : ""->>] ""
a -\] : ""-\] ""
a -\\] : ""-\\\\]""
a -/] : ""-/] ""
a -//] : ""-//] ""
a ->x] : ""->x] ""
a x->] : ""x->] ""
a o->] : ""o->] ""
a ->o] : ""->o] ""
a o->o] : ""o->o] ""
a <->] : ""<->] ""
a o<->o] : ""o<->o]""
a x<->x] : ""x<->x]""
a ->>o] : ""->>o] ""
a -\o] : ""-\o] ""
a -\\o] : ""-\\\\o]""
a -/o] : ""-/o] ""
a -//o] : ""-//o] ""
a x->o] : ""x->o] ""
@enduml
```
),
(
[Short incoming (with '?')],
```
@startuml
participant Alice as a
participant Bob as b
a -> b : //Long long label//
?-> b : ""?-> ""
?->> b : ""?->> ""
?-\ b : ""?-\ ""
?-\\ b : ""?-\\\\""
?-/ b : ""?-/ ""
?-// b : ""?-// ""
?->x b : ""?->x ""
?x-> b : ""?x-> ""
?o-> b : ""?o-> ""
?->o b : ""?->o ""
?o->o b : ""?o->o ""
?<-> b : ""?<-> ""
?o<->o b : ""?o<->o""
?x<->x b : ""?x<->x""
?->>o b : ""?->>o ""
?-\o b : ""?-\o ""
?-\\o b : ""?-\\\\o ""
?-/o b : ""?-/o ""
?-//o b : ""?-//o ""
?x->o b : ""?x->o ""
@enduml
```
),
(
[Short outgoing (with '?')],
```
@startuml
participant Alice as a
participant Bob as b
a -> b : //Long long label//
a ->? : ""->? ""
a ->>? : ""->>? ""
a -\? : ""-\? ""
a -\\? : ""-\\\\?""
a -/? : ""-/? ""
a -//? : ""-//? ""
a ->x? : ""->x? ""
a x->? : ""x->? ""
a o->? : ""o->? ""
a ->o? : ""->o? ""
a o->o? : ""o->o? ""
a <->? : ""<->? ""
a o<->o? : ""o<->o?""
a x<->x? : ""x<->x?""
a ->>o? : ""->>o? ""
a -\o? : ""-\o? ""
a -\\o? : ""-\\\\o?""
a -/o? : ""-/o? ""
a -//o? : ""-//o? ""
a x->o? : ""x->o? ""
@enduml
```
)
)
#{
for (title, uml) in examples {
heading(title)
box(
stroke: gray,
inset: 1em,
stack(
dir: ltr,
spacing: 1em,
raw(uml.text, block: true, lang: "plantuml"),
from-plantuml(uml)
)
)
pagebreak(weak: true)
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
gallery/plantuml_test.pdf Normal file

Binary file not shown.

271
gallery/plantuml_test.typ Normal file
View File

@ -0,0 +1,271 @@
#import "../src/lib.typ": from-plantuml
#set page(width: auto, height: auto)
/*
#from-plantuml(```
@startuml
actor User as usr
participant can_message as can
control kartculator as kc
queue XF as xf
entity Drive as drive
entity Steering as steering
usr -\ xf : set message "move"
xf -> can : new value on joystick
== If X axis change value ==
can -> kc : calculate new position
kc -> can : build message
can -> steering : set new position
== If Y axis change value ==
can -> kc : calculate new torque
kc -> can : build message
can -> xf : set message "torque"
xf -> drive : set new torque
@enduml
```)
#pagebreak(weak: true)
#from-plantuml(```
@startuml
actor CAN_BUS as bus
participant interrupt as ISR
queue XF as xf
participant ecan as ecan
participant canInterface as can
control canMessageController as msg
bus -\\ ISR ++ : can message
ISR -> can : newMsg
can -> ecan : read
ecan --> can : message
can -> xf : POST XF
destroy ISR
group TICK XF
xf o-> can : receiveCan()
can -> msg : processIncoming()
msg -> can : create message
can -> xf : POST XF
end
group TICK XF
xf o-> can : sendCan()
can -> ecan : write
ecan -\\ bus : can message
end
@enduml
```)
#pagebreak(weak: true)
*/
/*
#from-plantuml(```
@startuml
participant "Behavior::StateMachine" as sm
participant Dispatcher as d
participant TimeoutManager as tm
entity "Event::Timeout" as t
queue "TimeoutManager::timeouts_" as timeouts
autoactivate off
|||
|||
== Schedule timeout ==
|||
sm -> sm++ : scheduleTimeout
sm -> d ++: getDispatcher
d --> sm --: dispatcher
sm -> d --++ : scheduleTimeout
d -> tm ++: getTimeoutManager
tm --> d --: timeoutManager
d -> tm --++ : scheduleTimeout
tm -> t ** : new
t --> tm
tm -> timeouts --++: insert
|||
|||
== Decrement timeout (and dispatch) ==
|||
loop every tickInterval
?->> tm ++: tick
tm -> timeouts : getFront
timeouts -> t ++
t --> timeouts
timeouts --> tm : timeout
tm -> t --: decrement
end
|||
note left t
When timeout is 0,
dispatch event
end note
t -> timeouts : pop
deactivate timeouts
t ->? --: pushEvent
|||
|||
== Unschedule timeout ==
|||
sm -> sm++ : unscheduleTimeout
sm -> d ++: getDispatcher
d --> sm --: dispatcher
sm -> d --++ : unscheduleTimeout
d -> tm ++: getTimeoutManager
tm --> d --: timeoutManager
d -> tm --++ : unscheduleTimeout
tm -> timeouts --: erase
timeouts -> t !!
@enduml
```)
*/
#pagebreak(weak: true)
#from-plantuml(```
@startuml
participant Behavior as b
participant Dispatcher as d
entity Event as e
participant EventQueue as eq
queue "EventQueue::queue_" as q
== Create an Event ==
|||
?->> b ++ : GEN
b -> e ** : new
b -> b --++ : pushEvent
e -> b : getBehavior
b --> e ++: setBehavior
e --> b --
b -> d ++ : getDispatcher
d --> b
b -> d -- : pushEvent
d ->? -- : push
|||
|||
== Push Event ==
|||
?->> d ++: pushEvent
d -> eq--++: push
eq -> q ++
q --> eq
eq -> q -- : pushEndQueue
|||
|||
== Dispatch ==
|||
?->> d ++: executeOnce
d -> q : getFront
q -> e ++
e --> q
q --> d : event
d -> q : pop
deactivate q
d -> d --++ : dispatchEvent
d -> b ++ : getBehavior
b --> d
d -> b -- : process
b -> b--: processEvent
destroy e
@enduml
```)
#pagebreak(weak: true)
#from-plantuml(```
@startuml
'https://plantuml.com/sequence-diagram
actor User as usr
participant "Pb L" as pbL
participant "Pb R" as pbR
participant "LED L" as ledL
participant "LED R" as ledR
== Single click ==
group Single click left
usr -\ pbL ++: pressButton
usr -\ pbL : releaseButton
pbL -> ledL --++ : blink
usr -\ pbL ++: pressButton
usr -\ pbL : releaseButton
pbL -> ledL -- : endBlink
deactivate ledL
end
group Single click right
usr -\ pbR ++: pressButton
usr -\ pbR : releaseButton
pbR -> ledR --++ : blink
usr -\ pbR ++: pressButton
usr -\ pbR : releaseButton
pbR -> ledR -- : endBlink
deactivate ledR
end
== Double click ==
group Double click left
usr -\ pbL ++: pressButton
usr -\ pbL : releaseButton
usr -\ pbL : pressButton
pbL -> ledL --++ : blink
note right ledL: blink 3x
ledL ->x ledL -- : finished
end
group Double click right
usr -\ pbR ++: pressButton
usr -\ pbR : releaseButton
usr -\ pbR : pressButton
pbR -> ledR --++ : blink
note right ledR: blink 3x
ledR ->x ledR -- : finished
end
== Long click ==
group Long click left
usr -\ pbL ++: pressButton
pbL -> ledR--: blink
activate ledL
activate ledR
usr -\ pbL ++: pressButton
pbL -> ledR -- : endBlink
deactivate ledL
deactivate ledR
end
group Long click right
usr -\ pbR ++: pressButton
pbR -> ledR--: blink
activate ledL
activate ledR
usr -\ pbL ++: pressButton
pbL -> ledR -- : endBlink
deactivate ledL
deactivate ledR
end
@enduml
```)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,18 +0,0 @@
# Local Variables:
# mode: makefile
# End:
gallery_dir := "./gallery"
set shell := ["bash", "-uc"]
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
test *filter:
tt run {{filter}}
update-test *filter:
tt update {{filter}}

Binary file not shown.

View File

@ -1,6 +1,4 @@
#import "@preview/tidy:0.4.2"
#import "@preview/codly:1.2.0": codly-init, codly
#import "@preview/codly-languages:0.1.8": codly-languages
#import "@preview/tidy:0.3.0"
#import "src/lib.typ" as chronos
#import "src/participant.typ" as mod-par
#import "docs/examples.typ"
@ -8,48 +6,32 @@
#let TYPST = image("gallery/typst.png", width: 1.5cm, height: 1.5cm, fit: "contain")
#show: codly-init
#codly(
languages: codly-languages
)
#set text(font: "Source Sans 3")
#let doc-ref(target, full: false, var: false) = {
let (module, func) = target.split(".")
let label-name = module + func
let display-name = func
if full {
display-name = target
}
if not var {
label-name += "()"
display-name += "()"
}
link(label(label-name))[#display-name]
}
#set heading(numbering: (..num) => if num.pos().len() < 4 {
numbering("1.1", ..num)
})
#align(center)[
#v(2cm)
#text(size: 2em)[*Chronos*]
_v#chronos.version;_
#v(1cm)
#chronos.diagram({
import chronos: *
_par("u", display-name: [User], shape: "actor")
_par("wa", display-name: [Web App])
_par("tu", display-name: [Typst Universe], shape: "database")
_seq("u", "wa", comment: [Compile document], enable-dst: true)
_seq("wa", "tu", comment: [Fetch Chronos])
_seq("tu", "wa", dashed: true, slant: 10)
_seq("wa", "wa", comment: [Render])
_ret(comment: [Nice sequence diagram])
})
]
#pagebreak()
#{
outline(indent: auto, depth: 3)
outline(indent: true, depth: 3)
}
#show link: set text(fill: blue)
#set page(numbering: "1/1", header: align(right)[chronos #sym.dash.em v#chronos.version])
#set page(
header: align(left)[chronos #sym.dash.em v#chronos.version],
footer: context align(center, counter(page).display("1/1", both: true))
header: locate(loc => align(left)[chronos #sym.dash.em v#chronos.version]),
footer: locate(loc => align(center, counter(page).display("1/1", both: true)))
)
= Introduction
@ -58,20 +40,18 @@ This package lets you create nice sequence diagrams using the CeTZ package.
= Usage
#let import-stmt = "#import \"@preview/chronos:" + str(chronos.version) + "\""
Simply import #link("https://typst.app/universe/package/chronos/")[chronos] and call the `diagram` function:
#raw(block:true, lang: "typ", ```typ
$import
#pad(left: 1em)[```typ
#import "@preview/chronos:0.1.0"
#chronos.diagram({
import chronos: *
...
})
```.text.replace("$import", import-stmt))
```]
= Examples
You can find the following examples and more in the #link("https://git.kb28.ch/HEL/chronos/src/branch/main/gallery")[gallery] directory
You can find the following examples and more in the #link("https://git.kb28.ch/HEL/circuiteria/src/branch/main/gallery")[gallery] directory
== Some groups and sequences
@ -141,11 +121,7 @@ chronos.diagram({
== Custom images
#example(```
let load-img(path) = image(
path,
width: 1.5cm, height: 1.5cm,
fit:"contain"
)
let load-img(path) = image(path, width: 1.5cm, height: 1.5cm, fit:"contain")
let TYPST = load-img("../gallery/typst.png")
let FERRIS = load-img("../gallery/ferris.png")
let ME = load-img("../gallery/me.jpg")
@ -172,11 +148,11 @@ chronos.diagram({
read("docs/participants.typ"),
name: "Participants",
require-all-parameters: true,
old-syntax: true,
scope: (
chronos: chronos,
mod-par: mod-par,
TYPST: TYPST
TYPST: TYPST,
doc-ref: doc-ref
)
)
#tidy.show-module(par-docs, show-outline: false, sort-functions: none)
@ -187,9 +163,9 @@ chronos.diagram({
read("docs/sequences.typ"),
name: "Sequences",
require-all-parameters: true,
old-syntax: true,
scope: (
chronos: chronos,
doc-ref: doc-ref,
examples: examples
)
)
@ -201,9 +177,9 @@ chronos.diagram({
read("docs/groups.typ"),
name: "Groups",
require-all-parameters: true,
old-syntax: true,
scope: (
chronos: chronos,
doc-ref: doc-ref,
examples: examples
)
)
@ -215,13 +191,13 @@ chronos.diagram({
read("docs/gaps_seps.typ"),
name: "Gaps and separators",
require-all-parameters: true,
old-syntax: true,
scope: (
chronos: chronos,
doc-ref: doc-ref,
examples: examples
)
)
#tidy.show-module(gap-sep-docs, show-outline: false, sort-functions: none)
#tidy.show-module(gap-sep-docs, show-outline: false)
#pagebreak(weak: true)
@ -229,9 +205,9 @@ chronos.diagram({
read("docs/notes.typ"),
name: "Notes",
require-all-parameters: true,
old-syntax: true,
scope: (
chronos: chronos,
doc-ref: doc-ref,
examples: examples
)
)

View File

@ -1 +0,0 @@
#import "@preview/cetz:0.4.0": *

View File

@ -1,28 +0,0 @@
#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
})
})

View File

@ -1,31 +0,0 @@
#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
})
})

View File

@ -1,117 +0,0 @@
#import "/src/cetz.typ": draw
#import "/src/consts.typ": *
#import "/src/core/utils.typ": get-ctx, set-ctx
#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.at(1).min-i == grp.min-i { g.at(2) += 1 }
if g.at(1).max-i == grp.max-i { g.at(3) += 1 }
g
})
if grp.grp-type == "alt" {
grp.insert("elses", ())
}
ctx.groups.push((ctx.y, grp, 0, 0))
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 = text(group.name, weight: "bold")
let m = measure(box(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),
text([\[#group.desc\]], weight: "bold", size: .8em),
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),
text([\[#elmt.desc\]], weight: "bold", size: .8em),
anchor: "north-west",
padding: 3pt
)
}
#let render-end(group) = get-ctx(ctx => {
ctx.y -= Y-SPACE
let (start-y, group, start-lvl, end-lvl) = ctx.groups.pop()
let x0 = ctx.x-pos.at(group.min-i) - start-lvl * 10 - 20
let x1 = ctx.x-pos.at(group.max-i) + end-lvl * 10 + 20
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
})
})
#let render-else(else_) = set-ctx(ctx => {
ctx.y -= Y-SPACE
let m = measure(text([\[#else_.desc\]], weight: "bold", size: .8em))
ctx.groups.last().at(1).elses.push((
ctx.y, else_
))
ctx.y -= m.height / 1pt
return ctx
})

View File

@ -1,163 +0,0 @@
#import "/src/cetz.typ": draw
#import "/src/consts.typ": *
#import "/src/core/utils.typ": get-ctx, set-ctx
#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
})
}
})
}

View File

@ -1,406 +0,0 @@
#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 {"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(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)
}
}
},)

View File

@ -1,47 +0,0 @@
#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
})
})

View File

@ -1,361 +0,0 @@
#import "/src/cetz.typ": draw, vector
#import "note.typ"
#import "/src/consts.typ": *
#import "/src/core/utils.typ": get-ctx, set-ctx
#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)
}
if "linked-note" in seq {
h = calc.max(h, note.get-size(seq.linked-note).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("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(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
if "linked-note" in seq {
// TODO: adapt note.render
(seq.linked-note.draw)(seq.linked-note, 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)
}
} 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)
}
// 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
)
}
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 "linked-note" in seq {
let m = note.get-size(seq.linked-note)
end-info.y = calc.min(end-info.y, y0 - m.height / 2)
}
set-ctx(c => {
c.y = end-info.y
c.lifelines = ctx.lifelines
return c
})
})

View File

@ -1,32 +0,0 @@
#import "/src/core/utils.typ": get-ctx, is-elmt, set-ctx
#let render(sync) = get-ctx(ctx => {
set-ctx(c => {
c.sync-ys = ()
return c
})
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 => {
c.y = ctx.y
return c
})
(e.draw)(e)
set-ctx(c => {
c.sync-ys.push(c.y)
return c
})
}
set-ctx(c => {
c.y = calc.min(..c.sync-ys)
c.remove("sync-ys")
return c
})
})

View File

@ -1,402 +0,0 @@
#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 "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 unwrap-syncs(elements) = {
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
}
return elements
}
#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 = "["
p2 = note.pos
cell = get-note-box(note)
} else if note.side == "right" {
p1 = note.pos
p2 = "]"
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(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 width = (
m.width / 1pt +
COMMENT-PAD -
widths.slice(cell.i1, cell.i2 - 1).sum()
)
widths.at(cell.i2 - 1) = calc.max(
widths.at(cell.i2 - 1), 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)
elements = unwrap-syncs(elements)
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(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: ()
))
)
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" {
(elmt.draw)(elmt)
}
}
set-ctx(ctx => {
ctx.y -= Y-SPACE
return ctx
})
draw.on-layer(-1, {
if DEBUG-INVISIBLE {
render-debug()
}
participant.render-lifelines()
})
})

View File

@ -1,320 +0,0 @@
#import "draw/group.typ": render-end as grp-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 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 == "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"
)
if note.pos == none and note.side != "across" {
let names = ctx.participants.map(p => p.name)
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.last-seq.seq
seq.insert("linked-note", 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 == "left" {
ctx.linked.push("[")
} else if note.side == "right" {
ctx.linked.push("]")
}
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)
}

View File

@ -1,19 +1,246 @@
#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
#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) = {
return ((
type: "col",
p1: p1,
p2: p2,
width: width,
margin: margin,
min-width: min-width
),)
}
#let diagram(elements, width: auto) = {
if elements == none {
return
}
let (elmts, participants) = setup(elements)
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" {
let grp-elmts = elmt.elmts
elmt.elmts = elmt.elmts.map(e => {
if e.type == "seq" {
if e.p1 == "?" {
e.p1 = "?" + e.p2
} else if e.p2 == "?" {
e.p2 = e.p1 + "?"
}
}
e
})
elmts.at(i) = elmt
elmts = (
elmts.slice(0, i + 1) +
grp-elmts +
((
type: "grp-end"
),) +
elmts.slice(i+1)
)
} else if elmt.type == "seq" {
if elmt.enable-dst {
activation-history.push(elmt)
}
} else if elmt.type == "evt" {
if elmt.event == "enable" {
for elmt2 in elmts.slice(0, i).rev() {
if elmt2.type == "seq" {
activation-history.push(elmt2)
break
}
}
}
} else if elmt.type == "ret" {
if activation-history.len() == 0 {
panic("Cannot return if no lifeline is activated")
}
let seq = activation-history.pop()
elmts.at(i) = _seq(
seq.p2, seq.p1,
comment: elmt.comment,
disable-src: true,
dashed: true
).first()
}
i += 1
}
// List participants
let linked = ()
let last-seq = none
let last-note = none
for (i, elmt) in elmts.enumerate() {
if elmt.type == "par" {
participants.push(elmt)
} else if elmt.type == "seq" {
if not 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 + "?"
}
}
}
set text(font: "Source Sans 3")
let canvas = render(participants, elmts)
fit-canvas(canvas, width: width)
}
#let from-plantuml(code) = {
let code = code.text
}

View File

@ -1,9 +1,9 @@
#import "core/draw/group.typ"
#import "@preview/cetz:0.2.2": draw
#import "consts.typ": *
#let _grp(name, desc: none, type: "default", elmts) = {
return ((
type: "grp",
draw: group.render-start,
name: name,
desc: desc,
grp-type: type,
@ -20,7 +20,6 @@
let else-elmts = args.at(i + 1, default: ())
all-elmts.push((
type: "else",
draw: group.render-else,
desc: else-desc
))
all-elmts += else-elmts
@ -41,3 +40,60 @@
}
#let _opt(desc, elmts) = grp("opt", desc: desc, type: "opt", elmts)
#let _break(desc, elmts) = grp("break", desc: desc, type: "break", elmts)
#let render(x0, x1, y0, y1, group) = {
let shapes = ()
let name = text(group.name, weight: "bold")
let m = measure(box(name))
let w = m.width / 1pt + 15
let h = m.height / 1pt + 6
shapes += draw.rect(
(x0, y0),
(x1, y1)
)
shapes += draw.merge-path(
fill: COL-GRP-NAME,
close: true,
{
draw.line(
(x0, y0),
(x0 + w, y0),
(x0 + w, y0 - h / 2),
(x0 + w - 5, y0 - h),
(x0, y0 - h)
)
}
)
shapes += draw.content(
(x0, y0),
name,
anchor: "north-west",
padding: (left: 5pt, right: 10pt, top: 3pt, bottom: 3pt)
)
if group.desc != none {
shapes += draw.content(
(x0 + w, y0),
text([\[#group.desc\]], weight: "bold", size: .8em),
anchor: "north-west",
padding: 3pt
)
}
return shapes
}
#let render-else(x0, x1, y, elmt) = {
let shapes = draw.line(
(x0, y),
(x1, y),
stroke: (dash: (2pt, 1pt), thickness: .5pt)
)
shapes += draw.content(
(x0, y),
text([\[#elmt.desc\]], weight: "bold", size: .8em),
anchor: "north-west",
padding: 3pt
)
return shapes
}

View File

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

View File

@ -1,64 +0,0 @@
#import "core/draw/delay.typ"
#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) = {
return ((
type: "evt",
draw: evt-render,
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
),)
}

View File

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

1036
src/parser.typ Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
#import "core/draw/participant.typ"
#import "@preview/cetz:0.2.2": draw
#import "consts.typ": *
#let PAR-SPECIALS = ("?", "[", "]")
#let PAR-SPECIALS = "?[]"
#let SHAPES = (
"participant",
"actor",
@ -21,11 +22,6 @@
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,
@ -35,14 +31,12 @@
}
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
@ -50,7 +44,7 @@
}
#let _exists(participants, name) = {
if name in PAR-SPECIALS {
if name == "?" or name == "[" or name == "]" {
return true
}
@ -61,3 +55,307 @@
}
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)
}

519
src/renderer.typ Normal file
View File

@ -0,0 +1,519 @@
#import "@preview/cetz:0.2.2": 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)
}
widths.at(i) = calc.max(
widths.at(i),
normalize-units(elmt.min-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
})

63
src/separator.typ Normal file
View File

@ -0,0 +1,63 @@
#import "@preview/cetz:0.2.2": 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
}

View File

@ -1,4 +1,51 @@
#import "core/draw/sequence.typ"
#import "@preview/cetz:0.2.2": draw, vector
#import "consts.typ": *
#import "participant.typ"
#import "note.typ"
#let get-arrow-marks(sym, color) = {
if sym == none {
return none
}
if type(sym) == array {
return sym.map(s => get-arrow-marks(s, color))
}
(
"": none,
">": (symbol: ">", fill: color),
">>": (symbol: "straight"),
"\\": (symbol: ">", fill: color, harpoon: true, flip: true),
"\\\\": (symbol: "straight", harpoon: true, flip: true),
"/": (symbol: ">", fill: color, harpoon: true),
"//": (symbol: "straight", harpoon: true),
"x": none,
"o": none,
).at(sym)
}
#let reverse-arrow-mark(mark) = {
if type(mark) == array {
return mark.map(m => reverse-arrow-mark(m))
}
let mark2 = mark
if type(mark) == dictionary and mark.at("harpoon", default: false) {
let flipped = mark.at("flip", default: false)
mark2.insert("flip", not flipped)
}
return mark2
}
#let is-tip-of-type(type_, tip) = {
if type(tip) == str and tip == type_ {
return true
}
if type(tip) == array and tip.contains(type_) {
return true
}
return false
}
#let is-circle-tip = is-tip-of-type.with("o")
#let is-cross-tip = is-tip-of-type.with("x")
#let _seq(
p1,
@ -21,7 +68,6 @@
) = {
return ((
type: "seq",
draw: sequence.render,
p1: p1,
p2: p2,
comment: comment,
@ -48,3 +94,287 @@
comment: comment
),)
}
#let render(pars-i, x-pos, participants, elmt, y, lifelines) = {
let shapes = ()
y -= Y-SPACE
let h = 0
// Reserve space for comment
if elmt.comment != none {
h = calc.max(h, measure(box(elmt.comment)).height / 1pt + 6)
}
if "linked-note" in elmt {
h = calc.max(h, note.get-size(elmt.linked-note).height / 2)
}
y -= h
let i1 = pars-i.at(elmt.p1)
let i2 = pars-i.at(elmt.p2)
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 - CREATE-OFFSET)
}
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 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 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 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", end-info.y, elmt.lifeline-style))
lifelines.at(i2) = dst-line
}
if elmt.create-dst {
end-info.y -= CREATE-OFFSET
let dst-line = lifelines.at(i2)
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)
end-info.y = calc.min(end-info.y, y0 - m.height / 2)
}
let r = (end-info.y, lifelines, shapes)
return r
}

27
src/sync.typ Normal file
View File

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

View File

@ -1,15 +1,3 @@
#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
@ -48,9 +36,6 @@
max-i = calc.max(max-i, i1)
}
}
if max-i < min-i {
(min-i, max-i) = (max-i, min-i)
}
return (min-i, max-i)
}
@ -91,17 +76,4 @@
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)
})

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

View File

@ -1,6 +0,0 @@
#set page(width: auto, height: auto)
#import "/src/lib.typ": *
#diagram({})
#diagram(())

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,51 +0,0 @@
#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()
})
})

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@ -1,13 +0,0 @@
#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"))
})

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,49 +0,0 @@
#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"))
})

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,14 +0,0 @@
#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")
})

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,33 +0,0 @@
#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"))
})

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View File

@ -1,21 +0,0 @@
#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)
})

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,57 +0,0 @@
#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)
})

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,12 +0,0 @@
#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])
})

View File

@ -1,4 +0,0 @@
# generated by tytanic, do not edit
diff/**
out/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,84 +0,0 @@
#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`)
})

View File

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