Compare commits
	
		
			46 Commits
		
	
	
		
			v0.2.0
			...
			f812ac93c2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f812ac93c2 | |||
| caad9ed823 | |||
| 0b401df67d | |||
| 8ee14167de | |||
| 1f24ba8efe | |||
| 03138269f9 | |||
| 90d97a9ae5 | |||
| 7106cc3e22 | |||
| d51a08830d | |||
| 08f7eea0e4 | |||
| ef2541f267 | |||
| aed215ff17 | |||
| babce5195e | |||
| b1ad3b98d2 | |||
| 3a1c274e83 | |||
| e27bb9a9eb | |||
| 4f916359a9 | |||
| 276e9dae1b | |||
| 0c0aa0a420 | |||
| 534caf24a7 | |||
| 2e29a79f4c | |||
| 4ae06a5f03 | |||
| 91ff0efb06 | |||
| bdeabb39f9 | |||
| 7ad39decb6 | |||
| b8b775c3ce | |||
| 2a35b86f6e | |||
| 2a46612bfb | |||
| 964d708280 | |||
| e1a5b0b431 | |||
| cdfdd96966 | |||
| 5e70e5179f | |||
| 000813ff3b | |||
| f94c6f25a2 | |||
| d1a44d0b0f | |||
| 1b8c40d70d | |||
| b9c19ac730 | |||
| c46b345771 | |||
| 060e37e38e | |||
| 788c8999b1 | |||
| 3eadf63db3 | |||
| e57e617bb9 | |||
| dfb980e366 | |||
| 3beaad03d4 | |||
| 5cb8f50f2a | |||
| e8d737ff00 | 
							
								
								
									
										31
									
								
								.gitea/workflows/ci.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | |||||||
|  | name: CI | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: [ main, dev ] | ||||||
|  |   pull_request: | ||||||
|  |     branches: [ main, dev ] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   tests: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     container: catthehacker/ubuntu:rust-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  |  | ||||||
|  |       - name: Install tytanic | ||||||
|  |         run: cargo binstall tytanic@0.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 | ||||||
| @@ -15,7 +15,7 @@ This package lets you render sequence diagrams directly in Typst. The following | |||||||
| <td> | <td> | ||||||
|  |  | ||||||
| ```typst | ```typst | ||||||
| #import "@preview/chronos:0.2.0" | #import "@preview/chronos:0.2.2" | ||||||
| #chronos.diagram({ | #chronos.diagram({ | ||||||
|   import chronos: * |   import chronos: * | ||||||
|   _par("Alice") |   _par("Alice") | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
|   chronos: chronos |   chronos: chronos | ||||||
| ) | ) | ||||||
|  |  | ||||||
| #let example(src, show-src: true, vertical: false, fill: true, wrap: true) = { | #let example(src, show-src: true, vertical: false, fill: false, wrap: true) = { | ||||||
|   src = src.text |   src = src.text | ||||||
|   let full-src = example-preamble + src |   let full-src = example-preamble + src | ||||||
|   let body = eval(full-src, scope: example-scope) |   let body = eval(full-src, scope: example-scope) | ||||||
| @@ -16,15 +16,15 @@ | |||||||
|       box( |       box( | ||||||
|         stroke: black + 1pt, |         stroke: black + 1pt, | ||||||
|         radius: .5em, |         radius: .5em, | ||||||
|         fill: if fill {color.white.darken(5%)} else {none}, |         fill: if fill {color.white.darken(2%)} else {none}, | ||||||
|         if show-src { |         if show-src { | ||||||
|           let src-block = align(left, raw(src, lang: "typc")) |           let src-block = raw(src, block: true, lang: "typc") | ||||||
|           table( |           table( | ||||||
|             columns: if vertical {1} else {2}, |             columns: if vertical {1} else {2}, | ||||||
|             inset: 1em, |             inset: 5pt, | ||||||
|             align: horizon + center, |             align: horizon + center, | ||||||
|             stroke: none, |             stroke: none, | ||||||
|             img, |             table.cell(inset: 1em, img), | ||||||
|             if vertical {table.hline()} else {table.vline()}, src-block |             if vertical {table.hline()} else {table.vline()}, src-block | ||||||
|           ) |           ) | ||||||
|         } else { |         } else { | ||||||
|   | |||||||
| @@ -1,8 +1,23 @@ | |||||||
| #import "example.typ": example | #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(``` | #let seq-comm-align = example(``` | ||||||
| _par("p1", display-name: "Start participant") | _par("p1", | ||||||
| _par("p2", display-name: "End participant") |      display-name: "Start participant") | ||||||
|  | _par("p2", | ||||||
|  |      display-name: "End participant") | ||||||
| let alignments = ( | let alignments = ( | ||||||
|   "start", "end", |   "start", "end", | ||||||
|   "left", "right", |   "left", "right", | ||||||
| @@ -121,15 +136,58 @@ _sync({ | |||||||
| }) | }) | ||||||
| ```) | ```) | ||||||
|  |  | ||||||
| #let gaps-seps = example(``` | #let gaps = example(``` | ||||||
| _par("alice", display-name: "Alice") | _par("a", display-name: "Alice") | ||||||
| _par("bob", display-name: "Bob") | _par("b", display-name: "Bob") | ||||||
|  |  | ||||||
| _seq("alice", "bob", comment: "Hello") | _seq("a", "b", comment: [message 1]) | ||||||
| _gap(size: 10) | _seq("b", "a", comment: [ok], dashed: true) | ||||||
| _seq("bob", "alice", comment: "Hi") | _gap() | ||||||
| _sep("Another day") | _seq("a", "b", comment: [message 2]) | ||||||
| _seq("alice", "bob", comment: "Hello again") | _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 | ||||||
|  | ) | ||||||
| ```) | ```) | ||||||
|  |  | ||||||
| #let notes-shapes = example(``` | #let notes-shapes = example(``` | ||||||
|   | |||||||
| @@ -1,8 +1,15 @@ | |||||||
| /// Creates a gap before the next element |  | ||||||
| /// - size (int): Size of the gap |  | ||||||
| #let _gap(size: 20) = {} |  | ||||||
|  |  | ||||||
| /// Creates a separator before the next element | /// Creates a separator before the next element | ||||||
| /// #examples.gaps-seps | /// #examples.seps | ||||||
| /// - name (content): Name to display in the middle of the separator | /// - name (content): Name to display in the middle of the separator | ||||||
| #let _sep(name) = {} | #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) = {} | ||||||
| @@ -5,13 +5,15 @@ | |||||||
| /// - color (color): The note's color | /// - color (color): The note's color | ||||||
| /// - shape (str): The note's shape (see @@SHAPES for accepted values) | /// - shape (str): The note's shape (see @@SHAPES for accepted values) | ||||||
| /// - aligned (bool): True if the note is aligned with another note, in which case `side` must be `"over"`, false otherwise | /// - aligned (bool): True if the note is aligned with another note, in which case `side` must be `"over"`, false otherwise | ||||||
|  | /// - allow-overlap (bool): If set to `false`, the note will try to reserve space in the column to avoid overlapping with neighboring participants. If set to `true`, the not will overlap other participants | ||||||
| #let _note( | #let _note( | ||||||
|   side, |   side, | ||||||
|   content, |   content, | ||||||
|   pos: none, |   pos: none, | ||||||
|   color: rgb("#FEFFDD"), |   color: rgb("#FEFFDD"), | ||||||
|   shape: "default", |   shape: "default", | ||||||
|   aligned: false |   aligned: false, | ||||||
|  |   allow-overlap: true | ||||||
| ) = {} | ) = {} | ||||||
|  |  | ||||||
| /// Accepted values for `shape` argument of @@_note() | /// Accepted values for `shape` argument of @@_note() | ||||||
|   | |||||||
| @@ -34,6 +34,7 @@ | |||||||
| /// - invisible (bool): If set to true, the participant will not be shown | /// - invisible (bool): If set to true, the participant will not be shown | ||||||
| /// - shape (str): The shape of the participant. Possible values in @@SHAPES | /// - shape (str): The shape of the participant. Possible values in @@SHAPES | ||||||
| /// - color (color): The participant's color | /// - 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 | /// - custom-image (none, image): If shape is 'custom', sets the custom image to display | ||||||
| /// - show-bottom (bool): Whether to display the bottom shape | /// - show-bottom (bool): Whether to display the bottom shape | ||||||
| /// - show-top (bool): Whether to display the top shape | /// - show-top (bool): Whether to display the top shape | ||||||
| @@ -45,6 +46,11 @@ | |||||||
|   invisible: false, |   invisible: false, | ||||||
|   shape: "participant", |   shape: "participant", | ||||||
|   color: rgb("#E2E2F0"), |   color: rgb("#E2E2F0"), | ||||||
|  |   line-stroke: ( | ||||||
|  |     dash: "dashed", | ||||||
|  |     paint: gray.darken(40%), | ||||||
|  |     thickness: .5pt | ||||||
|  |   ), | ||||||
|   custom-image: none, |   custom-image: none, | ||||||
|   show-bottom: true, |   show-bottom: true, | ||||||
|   show-top: true, |   show-top: true, | ||||||
|   | |||||||
| @@ -42,6 +42,17 @@ | |||||||
|   slant: none |   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() | /// Accepted values for `event` argument of @@_evt() | ||||||
| ///  | ///  | ||||||
| /// `EVENTS = ("create", "destroy", "enable", "disable")` | /// `EVENTS = ("create", "destroy", "enable", "disable")` | ||||||
| @@ -52,9 +63,3 @@ | |||||||
| #let tips = ( | #let tips = ( | ||||||
|   "", ">", ">>", "\\", "\\\\", "/", "//", "x", "o", |   "", ">", ">>", "\\", "\\\\", "/", "//", "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
									
									
									
									
									
								
							
							
						
						| @@ -1,27 +0,0 @@ | |||||||
| #!/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 |  | ||||||
| Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB | 
| Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 88 KiB | 
| Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										18
									
								
								justfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | |||||||
|  | # 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}} | ||||||
							
								
								
									
										
											BIN
										
									
								
								manual.pdf
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										78
									
								
								manual.typ
									
									
									
									
									
								
							
							
						
						| @@ -1,4 +1,6 @@ | |||||||
| #import "@preview/tidy:0.3.0" | #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 "src/lib.typ" as chronos | #import "src/lib.typ" as chronos | ||||||
| #import "src/participant.typ" as mod-par | #import "src/participant.typ" as mod-par | ||||||
| #import "docs/examples.typ" | #import "docs/examples.typ" | ||||||
| @@ -6,25 +8,41 @@ | |||||||
|  |  | ||||||
| #let TYPST = image("gallery/typst.png", width: 1.5cm, height: 1.5cm, fit: "contain") | #let TYPST = image("gallery/typst.png", width: 1.5cm, height: 1.5cm, fit: "contain") | ||||||
|  |  | ||||||
| #let doc-ref(target, full: false, var: false) = { | #show: codly-init | ||||||
|   let (module, func) = target.split(".") | #codly( | ||||||
|   let label-name = module + func |   languages: codly-languages | ||||||
|   let display-name = func | ) | ||||||
|   if full { |  | ||||||
|     display-name = target | #set text(font: "Source Sans 3") | ||||||
|   } |  | ||||||
|   if not var { |  | ||||||
|     label-name += "()" |  | ||||||
|     display-name += "()" |  | ||||||
|   } |  | ||||||
|   link(label(label-name))[#display-name] |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #set heading(numbering: (..num) => if num.pos().len() < 4 { | #set heading(numbering: (..num) => if num.pos().len() < 4 { | ||||||
|   numbering("1.1", ..num) |   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: true, depth: 3) |   outline(indent: auto, depth: 3) | ||||||
| } | } | ||||||
| #show link: set text(fill: blue) | #show link: set text(fill: blue) | ||||||
|  |  | ||||||
| @@ -40,18 +58,20 @@ This package lets you create nice sequence diagrams using the CeTZ package. | |||||||
|  |  | ||||||
| = Usage | = 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: | Simply import #link("https://typst.app/universe/package/chronos/")[chronos] and call the `diagram` function: | ||||||
| #pad(left: 1em)[```typ | #raw(block:true, lang: "typ", ```typ | ||||||
| #import "@preview/chronos:0.1.0" | $import | ||||||
| #chronos.diagram({ | #chronos.diagram({ | ||||||
|   import chronos: * |   import chronos: * | ||||||
|   ... |   ... | ||||||
| }) | }) | ||||||
| ```] | ```.text.replace("$import", import-stmt)) | ||||||
|  |  | ||||||
| = Examples | = Examples | ||||||
|  |  | ||||||
| You can find the following examples and more in the #link("https://git.kb28.ch/HEL/circuiteria/src/branch/main/gallery")[gallery] directory | You can find the following examples and more in the #link("https://git.kb28.ch/HEL/chronos/src/branch/main/gallery")[gallery] directory | ||||||
|  |  | ||||||
| == Some groups and sequences | == Some groups and sequences | ||||||
|  |  | ||||||
| @@ -121,7 +141,11 @@ chronos.diagram({ | |||||||
| == Custom images | == Custom images | ||||||
|  |  | ||||||
| #example(``` | #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 TYPST = load-img("../gallery/typst.png") | ||||||
| let FERRIS = load-img("../gallery/ferris.png") | let FERRIS = load-img("../gallery/ferris.png") | ||||||
| let ME = load-img("../gallery/me.jpg") | let ME = load-img("../gallery/me.jpg") | ||||||
| @@ -148,11 +172,11 @@ chronos.diagram({ | |||||||
|   read("docs/participants.typ"), |   read("docs/participants.typ"), | ||||||
|   name: "Participants", |   name: "Participants", | ||||||
|   require-all-parameters: true, |   require-all-parameters: true, | ||||||
|  |   old-syntax: true, | ||||||
|   scope: ( |   scope: ( | ||||||
|     chronos: chronos, |     chronos: chronos, | ||||||
|     mod-par: mod-par, |     mod-par: mod-par, | ||||||
|     TYPST: TYPST, |     TYPST: TYPST | ||||||
|     doc-ref: doc-ref |  | ||||||
|   ) |   ) | ||||||
| ) | ) | ||||||
| #tidy.show-module(par-docs, show-outline: false, sort-functions: none) | #tidy.show-module(par-docs, show-outline: false, sort-functions: none) | ||||||
| @@ -163,9 +187,9 @@ chronos.diagram({ | |||||||
|   read("docs/sequences.typ"), |   read("docs/sequences.typ"), | ||||||
|   name: "Sequences", |   name: "Sequences", | ||||||
|   require-all-parameters: true, |   require-all-parameters: true, | ||||||
|  |   old-syntax: true, | ||||||
|   scope: ( |   scope: ( | ||||||
|     chronos: chronos, |     chronos: chronos, | ||||||
|     doc-ref: doc-ref, |  | ||||||
|     examples: examples |     examples: examples | ||||||
|   ) |   ) | ||||||
| ) | ) | ||||||
| @@ -177,9 +201,9 @@ chronos.diagram({ | |||||||
|   read("docs/groups.typ"), |   read("docs/groups.typ"), | ||||||
|   name: "Groups", |   name: "Groups", | ||||||
|   require-all-parameters: true, |   require-all-parameters: true, | ||||||
|  |   old-syntax: true, | ||||||
|   scope: ( |   scope: ( | ||||||
|     chronos: chronos, |     chronos: chronos, | ||||||
|     doc-ref: doc-ref, |  | ||||||
|     examples: examples |     examples: examples | ||||||
|   ) |   ) | ||||||
| ) | ) | ||||||
| @@ -191,13 +215,13 @@ chronos.diagram({ | |||||||
|   read("docs/gaps_seps.typ"), |   read("docs/gaps_seps.typ"), | ||||||
|   name: "Gaps and separators", |   name: "Gaps and separators", | ||||||
|   require-all-parameters: true, |   require-all-parameters: true, | ||||||
|  |   old-syntax: true, | ||||||
|   scope: ( |   scope: ( | ||||||
|     chronos: chronos, |     chronos: chronos, | ||||||
|     doc-ref: doc-ref, |  | ||||||
|     examples: examples |     examples: examples | ||||||
|   ) |   ) | ||||||
| ) | ) | ||||||
| #tidy.show-module(gap-sep-docs, show-outline: false) | #tidy.show-module(gap-sep-docs, show-outline: false, sort-functions: none) | ||||||
|  |  | ||||||
| #pagebreak(weak: true) | #pagebreak(weak: true) | ||||||
|  |  | ||||||
| @@ -205,9 +229,9 @@ chronos.diagram({ | |||||||
|   read("docs/notes.typ"), |   read("docs/notes.typ"), | ||||||
|   name: "Notes", |   name: "Notes", | ||||||
|   require-all-parameters: true, |   require-all-parameters: true, | ||||||
|  |   old-syntax: true, | ||||||
|   scope: ( |   scope: ( | ||||||
|     chronos: chronos, |     chronos: chronos, | ||||||
|     doc-ref: doc-ref, |  | ||||||
|     examples: examples |     examples: examples | ||||||
|   ) |   ) | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								src/cetz.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | #import "@preview/cetz:0.4.0": * | ||||||
							
								
								
									
										28
									
								
								src/core/draw/delay.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | |||||||
|  | #import "/src/cetz.typ": draw | ||||||
|  |  | ||||||
|  | #import "/src/core/utils.typ": get-ctx, set-ctx | ||||||
|  |  | ||||||
|  | #let render(delay) = get-ctx(ctx => { | ||||||
|  |   let y0 = ctx.y | ||||||
|  |   let y1 = ctx.y - delay.size | ||||||
|  |   for (i, line) in ctx.lifelines.enumerate() { | ||||||
|  |     line.lines.push(("delay-start", y0)) | ||||||
|  |     line.lines.push(("delay-end", y1)) | ||||||
|  |     ctx.lifelines.at(i) = line | ||||||
|  |   } | ||||||
|  |   if delay.name != none { | ||||||
|  |     let x0 = ctx.x-pos.first() | ||||||
|  |     let x1 = ctx.x-pos.last() | ||||||
|  |     draw.content( | ||||||
|  |       ((x0 + x1) / 2, (y0 + y1) / 2), | ||||||
|  |       anchor: "center", | ||||||
|  |       delay.name | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |   ctx.y = y1 | ||||||
|  |   set-ctx(c => { | ||||||
|  |     c.y = ctx.y | ||||||
|  |     c.lifelines = ctx.lifelines | ||||||
|  |     return c | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										31
									
								
								src/core/draw/event.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | |||||||
|  | #import "/src/consts.typ": * | ||||||
|  | #import "/src/core/utils.typ": get-ctx, set-ctx | ||||||
|  |  | ||||||
|  | #let render(evt) = get-ctx(ctx => { | ||||||
|  |   let par-name = evt.participant | ||||||
|  |   let i = ctx.pars-i.at(par-name) | ||||||
|  |   let par = ctx.participants.at(i) | ||||||
|  |   let line = ctx.lifelines.at(i) | ||||||
|  |   let entry = (evt.event, ctx.y) | ||||||
|  |  | ||||||
|  |   if evt.event == "disable" { | ||||||
|  |     line.level -= 1 | ||||||
|  |   } else if evt.event == "enable" { | ||||||
|  |     line.level += 1 | ||||||
|  |     entry.push(evt.lifeline-style) | ||||||
|  |   } else if evt.event == "create" { | ||||||
|  |     ctx.y -= CREATE-OFFSET | ||||||
|  |     entry.at(1) = ctx.y | ||||||
|  |     (par.draw)(par, y: ctx.y) | ||||||
|  |   } else if evt.event == "destroy" { | ||||||
|  |   } else { | ||||||
|  |     panic("Unknown event '" + evt.event + "'") | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   line.lines.push(entry) | ||||||
|  |   set-ctx(c => { | ||||||
|  |     c.lifelines.at(i) = line | ||||||
|  |     c.y = ctx.y | ||||||
|  |     return c | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										133
									
								
								src/core/draw/group.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,133 @@ | |||||||
|  | #import "/src/cetz.typ": draw | ||||||
|  |  | ||||||
|  | #import "/src/consts.typ": * | ||||||
|  | #import "/src/core/utils.typ": get-ctx, set-ctx, expand-parent-group | ||||||
|  |  | ||||||
|  | #let render-start(grp) = get-ctx(ctx => { | ||||||
|  |   let grp = grp | ||||||
|  |   ctx.y -= Y-SPACE | ||||||
|  |   let m = measure( | ||||||
|  |     box( | ||||||
|  |       grp.name, | ||||||
|  |       inset: ( | ||||||
|  |         left: 5pt, | ||||||
|  |         right: 5pt, | ||||||
|  |         top: 3pt, | ||||||
|  |         bottom: 3pt | ||||||
|  |       ), | ||||||
|  |     ) | ||||||
|  |   ) | ||||||
|  |   ctx.groups = ctx.groups.map(g => { | ||||||
|  |     if g.group.min-i == grp.min-i { g.start-lvl += 1 } | ||||||
|  |     if g.group.max-i == grp.max-i { g.end-lvl += 1 } | ||||||
|  |     g | ||||||
|  |   }) | ||||||
|  |   if grp.grp-type == "alt" { | ||||||
|  |     grp.insert("elses", ()) | ||||||
|  |   } | ||||||
|  |   ctx.groups.push(( | ||||||
|  |     start-y: ctx.y, | ||||||
|  |     group: grp, | ||||||
|  |     start-lvl: 0, | ||||||
|  |     end-lvl: 0, | ||||||
|  |     min-x: ctx.x-pos.at(grp.min-i) - 10, | ||||||
|  |     max-x: ctx.x-pos.at(grp.max-i) + 10 | ||||||
|  |   )) | ||||||
|  |   ctx.y -= m.height / 1pt | ||||||
|  |  | ||||||
|  |   set-ctx(c => { | ||||||
|  |     c.y = ctx.y | ||||||
|  |     c.groups = ctx.groups | ||||||
|  |     return c | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #let draw-group(x0, x1, y0, y1, group) = { | ||||||
|  |   let name = 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, | ||||||
|  |     min-x, | ||||||
|  |     max-x | ||||||
|  |   ) = ctx.groups.pop() | ||||||
|  |   let x0 = min-x - 10 | ||||||
|  |   let x1 = max-x + 10 | ||||||
|  |    | ||||||
|  |   draw-group(x0, x1, start-y, ctx.y, group) | ||||||
|  |  | ||||||
|  |   if group.grp-type == "alt" { | ||||||
|  |     for (else-y, else-elmt) in group.elses { | ||||||
|  |       draw-else(x0, x1, else-y, else-elmt) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set-ctx(c => { | ||||||
|  |     c.y = ctx.y | ||||||
|  |     c.groups = ctx.groups | ||||||
|  |     return c | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   expand-parent-group(x0, x1) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | #let render-else(else_) = set-ctx(ctx => { | ||||||
|  |   ctx.y -= Y-SPACE | ||||||
|  |   let m = measure(text([\[#else_.desc\]], weight: "bold", size: .8em)) | ||||||
|  |   ctx.groups.last().group.elses.push(( | ||||||
|  |     ctx.y, else_ | ||||||
|  |   )) | ||||||
|  |   ctx.y -= m.height / 1pt | ||||||
|  |   return ctx | ||||||
|  | }) | ||||||
							
								
								
									
										165
									
								
								src/core/draw/note.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,165 @@ | |||||||
|  | #import "/src/cetz.typ": draw | ||||||
|  |  | ||||||
|  | #import "/src/consts.typ": * | ||||||
|  | #import "/src/core/utils.typ": get-ctx, set-ctx, expand-parent-group | ||||||
|  |  | ||||||
|  | #let get-size(note) = { | ||||||
|  |   let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD} | ||||||
|  |   let m = measure(box(note.content)) | ||||||
|  |   let w = m.width / 1pt + PAD.last() * 2 | ||||||
|  |   let h = m.height / 1pt + PAD.first() * 2 | ||||||
|  |   if note.shape == "default" { | ||||||
|  |     w += NOTE-CORNER-SIZE | ||||||
|  |   } | ||||||
|  |   return ( | ||||||
|  |     width: w, | ||||||
|  |     height: h | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let get-base-x(pars-i, x-pos, note) = { | ||||||
|  |   if note.side == "across" { | ||||||
|  |     return (x-pos.first() + x-pos.last()) / 2 | ||||||
|  |   } | ||||||
|  |   if note.side == "over" { | ||||||
|  |     if type(note.pos) == array { | ||||||
|  |       let xs = note.pos.map(par => x-pos.at(pars-i.at(par))) | ||||||
|  |       return (calc.min(..xs) + calc.max(..xs)) / 2 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return x-pos.at(pars-i.at(note.pos)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let get-box(note) = { | ||||||
|  |   let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD} | ||||||
|  |   let inset = ( | ||||||
|  |     left: PAD.last() * 1pt, | ||||||
|  |     right: PAD.last() * 1pt, | ||||||
|  |     top: PAD.first() * 1pt, | ||||||
|  |     bottom: PAD.first() * 1pt, | ||||||
|  |   ) | ||||||
|  |   if note.shape == "default" { | ||||||
|  |     inset.right += NOTE-CORNER-SIZE * 1pt | ||||||
|  |   } | ||||||
|  |   if note.side == "left" { | ||||||
|  |     inset.right += NOTE-GAP * 1pt | ||||||
|  |   } else if note.side == "right" { | ||||||
|  |     inset.left += NOTE-GAP * 1pt | ||||||
|  |   } | ||||||
|  |   return box(note.content, inset: inset) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let render(note, y: auto, forced: false) = { | ||||||
|  |   if not note.linked { | ||||||
|  |     if not note.aligned { | ||||||
|  |       set-ctx(c => { | ||||||
|  |         c.y -= Y-SPACE | ||||||
|  |         return c | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } else if not forced { | ||||||
|  |     return () | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get-ctx(ctx => { | ||||||
|  |     let y = y | ||||||
|  |     if y == auto { | ||||||
|  |       y = ctx.y | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |  | ||||||
|  |     let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD} | ||||||
|  |     let m = measure(box(note.content)) | ||||||
|  |     let w = m.width / 1pt + PAD.last() * 2 | ||||||
|  |     let h = m.height / 1pt + PAD.first() * 2 | ||||||
|  |     let total-w = w | ||||||
|  |     if note.shape == "default" { | ||||||
|  |       total-w += NOTE-CORNER-SIZE | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let base-x = get-base-x(ctx.pars-i, ctx.x-pos, note) | ||||||
|  |  | ||||||
|  |     let i = none | ||||||
|  |     if note.pos != none and type(note.pos) == str { | ||||||
|  |       i = ctx.pars-i.at(note.pos) | ||||||
|  |     } | ||||||
|  |     let x0 = base-x | ||||||
|  |     if note.side == "left" { | ||||||
|  |       x0 -= NOTE-GAP | ||||||
|  |       x0 -= total-w | ||||||
|  |       if ctx.lifelines.at(i).level != 0 { | ||||||
|  |         x0 -= LIFELINE-W / 2 | ||||||
|  |       } | ||||||
|  |     } else if note.side == "right" { | ||||||
|  |       x0 += NOTE-GAP | ||||||
|  |       x0 += ctx.lifelines.at(i).level * LIFELINE-W / 2 | ||||||
|  |     } else if note.side == "over" or note.side == "across" { | ||||||
|  |       x0 -= total-w / 2 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let x1 = x0 + w | ||||||
|  |     let x2 = x0 + total-w | ||||||
|  |     let y0 = y | ||||||
|  |  | ||||||
|  |     if note.linked { | ||||||
|  |       y0 += h / 2 | ||||||
|  |     } | ||||||
|  |     let y1 = y0 - h | ||||||
|  |  | ||||||
|  |     if note.shape == "default" { | ||||||
|  |       draw.line( | ||||||
|  |         (x0, y0), | ||||||
|  |         (x1, y0), | ||||||
|  |         (x2, y0 - NOTE-CORNER-SIZE), | ||||||
|  |         (x2, y1), | ||||||
|  |         (x0, y1), | ||||||
|  |         stroke: black + .5pt, | ||||||
|  |         fill: note.color, | ||||||
|  |         close: true | ||||||
|  |       ) | ||||||
|  |       draw.line( | ||||||
|  |         (x1, y0), | ||||||
|  |         (x1, y0 - NOTE-CORNER-SIZE), | ||||||
|  |         (x2, y0 - NOTE-CORNER-SIZE), | ||||||
|  |         stroke: black + .5pt | ||||||
|  |       ) | ||||||
|  |     } else if note.shape == "rect" { | ||||||
|  |       draw.rect( | ||||||
|  |         (x0, y0), | ||||||
|  |         (x2, y1), | ||||||
|  |         stroke: black + .5pt, | ||||||
|  |         fill: note.color | ||||||
|  |       ) | ||||||
|  |     } else if note.shape == "hex" { | ||||||
|  |       let lx = x0 + PAD.last() | ||||||
|  |       let rx = x2 - PAD.last() | ||||||
|  |       let my = (y0 + y1) / 2 | ||||||
|  |       draw.line( | ||||||
|  |         (lx, y0), | ||||||
|  |         (rx, y0), | ||||||
|  |         (x2, my), | ||||||
|  |         (rx, y1), | ||||||
|  |         (lx, y1), | ||||||
|  |         (x0, my), | ||||||
|  |         stroke: black + .5pt, | ||||||
|  |         fill: note.color, | ||||||
|  |         close: true | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     draw.content( | ||||||
|  |       ((x0 + x1)/2, (y0 + y1)/2), | ||||||
|  |       note.content, | ||||||
|  |       anchor: "center" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     if note.aligned-with == none and (note.pos != none or note.side == "across") { | ||||||
|  |       set-ctx(c => { | ||||||
|  |         c.y -= h | ||||||
|  |         return c | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     expand-parent-group(x0, x2) | ||||||
|  |   }) | ||||||
|  | } | ||||||
							
								
								
									
										406
									
								
								src/core/draw/participant.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,406 @@ | |||||||
|  | #import "/src/cetz.typ": draw | ||||||
|  |  | ||||||
|  | #import "/src/consts.typ": * | ||||||
|  | #import "/src/core/utils.typ": get-ctx, get-style, set-ctx | ||||||
|  |  | ||||||
|  | #let get-size(par) = { | ||||||
|  |   if par.invisible { | ||||||
|  |     return (width: 0pt, height: 0pt) | ||||||
|  |   } | ||||||
|  |   let m = measure(box(par.display-name)) | ||||||
|  |   let w = m.width | ||||||
|  |   let h = m.height | ||||||
|  |   let (shape-w, shape-h) = ( | ||||||
|  |     participant: (w + PAR-PAD.last() * 2, h + PAR-PAD.first() * 2), | ||||||
|  |     actor: (ACTOR-WIDTH * 1pt, ACTOR-WIDTH * 2pt + SYM-GAP * 1pt + h), | ||||||
|  |     boundary: (BOUNDARY-HEIGHT * 2pt, BOUNDARY-HEIGHT * 1pt + SYM-GAP * 1pt + h), | ||||||
|  |     control: (CONTROL-HEIGHT * 1pt, CONTROL-HEIGHT * 1pt + SYM-GAP * 1pt + h), | ||||||
|  |     entity: (ENTITY-HEIGHT * 1pt, ENTITY-HEIGHT * 1pt + 2pt + SYM-GAP * 1pt + h), | ||||||
|  |     database: (DATABASE-WIDTH * 1pt, DATABASE-WIDTH * 4pt / 3 + SYM-GAP * 1pt + h), | ||||||
|  |     collections: ( | ||||||
|  |       w + COLLECTIONS-PAD.last() * 2 + calc.abs(COLLECTIONS-DX) * 1pt, | ||||||
|  |       h + COLLECTIONS-PAD.first() * 2 + calc.abs(COLLECTIONS-DY) * 1pt, | ||||||
|  |     ), | ||||||
|  |     queue: ( | ||||||
|  |       w + QUEUE-PAD.last() * 2 + 3 * (h + QUEUE-PAD.first() * 2) / 4, | ||||||
|  |       h + QUEUE-PAD.first() * 2 | ||||||
|  |     ), | ||||||
|  |     custom: ( | ||||||
|  |       measure(par.custom-image).width, | ||||||
|  |       measure(par.custom-image).height + SYM-GAP * 1pt + h | ||||||
|  |     ) | ||||||
|  |   ).at(par.shape) | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     width: calc.max(w, shape-w), | ||||||
|  |     height: calc.max(h, shape-h) | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let _render-participant(x, y, p, m, bottom) = { | ||||||
|  |   let w = m.width / 1pt | ||||||
|  |   let h = m.height / 1pt | ||||||
|  |   let x0 = x - w / 2 - PAR-PAD.last() / 1pt | ||||||
|  |   let x1 = x + w / 2 + PAR-PAD.last() / 1pt | ||||||
|  |   let y0 = y + h + PAR-PAD.first() / 1pt * 2 | ||||||
|  |   if bottom { | ||||||
|  |     y0 = y | ||||||
|  |   } | ||||||
|  |   let y1 = y0 - h - PAR-PAD.first() / 1pt * 2 | ||||||
|  |  | ||||||
|  |   draw.rect( | ||||||
|  |     (x0, y0), | ||||||
|  |     (x1, y1), | ||||||
|  |     radius: 2pt, | ||||||
|  |     fill: p.color, | ||||||
|  |     stroke: black + .5pt | ||||||
|  |   ) | ||||||
|  |   draw.content( | ||||||
|  |     ((x0 + x1) / 2, (y0 + y1) / 2), | ||||||
|  |     p.display-name, | ||||||
|  |     anchor: "center" | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let _render-actor(x, y, p, m, bottom) = { | ||||||
|  |   let w2 = ACTOR-WIDTH / 2 | ||||||
|  |   let head-r = ACTOR-WIDTH / 4 | ||||||
|  |   let height = ACTOR-WIDTH * 2 | ||||||
|  |   let arms-y = height * 0.375 | ||||||
|  |  | ||||||
|  |   let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + height + SYM-GAP} | ||||||
|  |   draw.circle( | ||||||
|  |     (x, y0 - head-r), | ||||||
|  |     radius: head-r, | ||||||
|  |     fill: p.color, | ||||||
|  |     stroke: black + .5pt | ||||||
|  |   ) | ||||||
|  |   draw.line((x, y0 - head-r * 2), (x, y0 - height + w2), stroke: black + .5pt) | ||||||
|  |   draw.line((x - w2, y0 - arms-y), (x + w2, y0 - arms-y), stroke: black + .5pt) | ||||||
|  |   draw.line((x - w2, y0 - height), (x, y0 - height + w2), (x + w2, y0 - height), stroke: black + .5pt) | ||||||
|  |   draw.content( | ||||||
|  |     (x, y), | ||||||
|  |     p.display-name, | ||||||
|  |     anchor: if bottom {"north"} else {"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) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | },) | ||||||
							
								
								
									
										47
									
								
								src/core/draw/separator.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | |||||||
|  | #import "/src/cetz.typ": draw | ||||||
|  |  | ||||||
|  | #import "/src/consts.typ": * | ||||||
|  | #import "/src/core/utils.typ": get-ctx, set-ctx | ||||||
|  |  | ||||||
|  | #let render(sep) = get-ctx(ctx => { | ||||||
|  |   ctx.y -= Y-SPACE | ||||||
|  |  | ||||||
|  |   let x0 = ctx.x-pos.first() - 20 | ||||||
|  |   let x1 = ctx.x-pos.last() + 20 | ||||||
|  |   let m = measure( | ||||||
|  |     box( | ||||||
|  |       sep.name, | ||||||
|  |       inset: (left: 3pt, right: 3pt, top: 5pt, bottom: 5pt) | ||||||
|  |     ) | ||||||
|  |   ) | ||||||
|  |   let w = m.width / 1pt | ||||||
|  |   let h = m.height / 1pt | ||||||
|  |   let cx = (x0 + x1) / 2 | ||||||
|  |   let xl = cx - w / 2 | ||||||
|  |   let xr = cx + w / 2 | ||||||
|  |  | ||||||
|  |   ctx.y -= h / 2 | ||||||
|  |   draw.rect( | ||||||
|  |     (x0, ctx.y), | ||||||
|  |     (x1, ctx.y - 3), | ||||||
|  |     stroke: none, | ||||||
|  |     fill: white | ||||||
|  |   ) | ||||||
|  |   draw.line((x0, ctx.y), (x1, ctx.y)) | ||||||
|  |   ctx.y -= 3 | ||||||
|  |   draw.line((x0, ctx.y), (x1, ctx.y)) | ||||||
|  |   draw.content( | ||||||
|  |     ((x0 + x1) / 2, ctx.y + 1.5), | ||||||
|  |     sep.name, | ||||||
|  |     anchor: "center", | ||||||
|  |     padding: (5pt, 3pt), | ||||||
|  |     frame: "rect", | ||||||
|  |     fill: COL-SEP-NAME | ||||||
|  |   ) | ||||||
|  |   ctx.y -= h / 2 | ||||||
|  |  | ||||||
|  |   set-ctx(c => { | ||||||
|  |     c.y = ctx.y | ||||||
|  |     return c | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										368
									
								
								src/core/draw/sequence.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,368 @@ | |||||||
|  | #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) | ||||||
|  |   } | ||||||
|  |   h = calc.max( | ||||||
|  |     h, | ||||||
|  |     ..seq.linked-notes.map(n => { | ||||||
|  |       note.get-size(n).height / 2 | ||||||
|  |     }) | ||||||
|  |   ) | ||||||
|  |   ctx.y -= h | ||||||
|  |  | ||||||
|  |   let start-info = ( | ||||||
|  |     i: i1, | ||||||
|  |     x: ctx.x-pos.at(i1), | ||||||
|  |     y: ctx.y, | ||||||
|  |     ll-lvl: ctx.lifelines.at(i1).level * LIFELINE-W / 2 | ||||||
|  |   ) | ||||||
|  |   let end-info = ( | ||||||
|  |     i: i2, | ||||||
|  |     x: ctx.x-pos.at(i2), | ||||||
|  |     y: ctx.y, | ||||||
|  |     ll-lvl: ctx.lifelines.at(i2).level * LIFELINE-W / 2 | ||||||
|  |   ) | ||||||
|  |   let slant = if seq.slant == auto { | ||||||
|  |     DEFAULT-SLANT | ||||||
|  |   } else if seq.slant != none { | ||||||
|  |     seq.slant | ||||||
|  |   } else { | ||||||
|  |     0 | ||||||
|  |   } | ||||||
|  |   end-info.y -= slant | ||||||
|  |   if seq.p1 == seq.p2 { | ||||||
|  |     end-info.y -= 10 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if seq.disable-src { | ||||||
|  |     let src-line = ctx.lifelines.at(i1) | ||||||
|  |     src-line.level -= 1 | ||||||
|  |     src-line.lines.push(("disable", start-info.y)) | ||||||
|  |     ctx.lifelines.at(i1) = src-line | ||||||
|  |   } | ||||||
|  |   if seq.destroy-src { | ||||||
|  |     let src-line = ctx.lifelines.at(i1) | ||||||
|  |     src-line.lines.push(("destroy", start-info.y)) | ||||||
|  |     ctx.lifelines.at(i1) = src-line | ||||||
|  |   } | ||||||
|  |   if seq.disable-dst { | ||||||
|  |     let dst-line = ctx.lifelines.at(i2) | ||||||
|  |     dst-line.level -= 1 | ||||||
|  |     dst-line.lines.push(("disable", end-info.y)) | ||||||
|  |     ctx.lifelines.at(i2) = dst-line | ||||||
|  |   } | ||||||
|  |   if seq.destroy-dst { | ||||||
|  |     let dst-line = ctx.lifelines.at(i2) | ||||||
|  |     dst-line.lines.push(("destroy", end-info.y)) | ||||||
|  |     ctx.lifelines.at(i2) = dst-line | ||||||
|  |   } | ||||||
|  |   if seq.enable-dst { | ||||||
|  |     let dst-line = ctx.lifelines.at(i2) | ||||||
|  |     dst-line.level += 1 | ||||||
|  |     ctx.lifelines.at(i2) = dst-line | ||||||
|  |   } | ||||||
|  |   if seq.create-dst { | ||||||
|  |     let par = ctx.participants.at(i2) | ||||||
|  |     let m = measure(box(par.display-name)) | ||||||
|  |     let f = if i1 > i2 {-1} else {1} | ||||||
|  |     end-info.x -= (m.width + PAR-PAD.last() * 2) / 2pt * f | ||||||
|  |     (par.draw)(par, y: end-info.y) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   end-info.ll-lvl = ctx.lifelines.at(i2).level * LIFELINE-W / 2 | ||||||
|  |  | ||||||
|  |   // Compute left/right position at start/end | ||||||
|  |   start-info.insert("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 | ||||||
|  |   for n in seq.linked-notes { | ||||||
|  |     (n.draw)(n, y: start-info.y, forced: true) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let flip-mark = end-info.i <= start-info.i | ||||||
|  |   if seq.flip { | ||||||
|  |     flip-mark = not flip-mark | ||||||
|  |   } | ||||||
|  |   if flip-mark { | ||||||
|  |     style.mark.end = reverse-arrow-mark(style.mark.end) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let pts | ||||||
|  |   let comment-pt | ||||||
|  |   let comment-anchor | ||||||
|  |   let comment-angle = 0deg | ||||||
|  |  | ||||||
|  |   if seq.p1 == seq.p2 { | ||||||
|  |     if seq.flip { | ||||||
|  |       x1 = start-info.lx | ||||||
|  |     } else { | ||||||
|  |       x2 = end-info.rx | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let x-mid = if seq.flip { | ||||||
|  |       calc.min(x1, x2) - 20 | ||||||
|  |     } else { | ||||||
|  |       calc.max(x1, x2) + 20 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pts = ( | ||||||
|  |       (x1, start-info.y), | ||||||
|  |       (x-mid, start-info.y), | ||||||
|  |       (x-mid, end-info.y), | ||||||
|  |       (x2, end-info.y) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     if comment != none { | ||||||
|  |       comment-anchor = ( | ||||||
|  |         start: if x-mid < x1 {"south-east"} else {"south-west"}, | ||||||
|  |         end: if x-mid < x1 {"south-west"} else {"south-east"}, | ||||||
|  |         left: "south-west", | ||||||
|  |         right: "south-east", | ||||||
|  |         center: "south", | ||||||
|  |       ).at(seq.comment-align) | ||||||
|  |  | ||||||
|  |       comment-pt = ( | ||||||
|  |         start: pts.first(), | ||||||
|  |         end: pts.at(1), | ||||||
|  |         left: if x-mid < x1 {pts.at(1)} else {pts.first()}, | ||||||
|  |         right: if x-mid < x1 {pts.first()} else {pts.at(1)}, | ||||||
|  |         center: (pts.first(), 50%, pts.at(1)) | ||||||
|  |       ).at(seq.comment-align) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |   } 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 seq.linked-notes.len() != 0 { | ||||||
|  |     end-info.y = calc.min( | ||||||
|  |       end-info.y, | ||||||
|  |       y0 - calc.max(..seq.linked-notes.map(n => { | ||||||
|  |         let m = note.get-size(n) | ||||||
|  |         return m.height / 2 | ||||||
|  |       })) | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set-ctx(c => { | ||||||
|  |     c.y = end-info.y | ||||||
|  |     c.lifelines = ctx.lifelines | ||||||
|  |     return c | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										32
									
								
								src/core/draw/sync.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | |||||||
|  | #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 | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										412
									
								
								src/core/renderer.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,412 @@ | |||||||
|  | #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 = note.pos2 | ||||||
|  |     p2 = note.pos | ||||||
|  |     cell = get-note-box(note) | ||||||
|  |   } else if note.side == "right" { | ||||||
|  |     p1 = note.pos | ||||||
|  |     p2 = note.pos2 | ||||||
|  |     cell = get-note-box(note) | ||||||
|  |   } else if note.side == "over" and note.aligned-with != none { | ||||||
|  |     let box1 = get-note-box(note) | ||||||
|  |     let box2 = get-note-box(note.aligned-with) | ||||||
|  |     let m1 = measure(box1) | ||||||
|  |     let m2 = measure(box2) | ||||||
|  |     cell = box( | ||||||
|  |       width: (m1.width + m2.width) / 2, | ||||||
|  |       height: calc.max(m1.height, m2.height) | ||||||
|  |     ) | ||||||
|  |     p1 = note.pos | ||||||
|  |     p2 = note.aligned-with.pos | ||||||
|  |   } else { | ||||||
|  |     return none | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let i1 = pars-i.at(p1) | ||||||
|  |   let i2 = pars-i.at(p2) | ||||||
|  |   cell = ( | ||||||
|  |     elmt: note, | ||||||
|  |     i1: calc.min(i1, i2), | ||||||
|  |     i2: calc.max(i1, i2), | ||||||
|  |     cell: cell | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   return cell | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let compute-max-lifeline-levels(participants, elements, pars-i) = { | ||||||
|  |   let cells = () | ||||||
|  |   for elmt in elements { | ||||||
|  |     if elmt.type == "seq" { | ||||||
|  |       let cell | ||||||
|  |       (participants, cell) = seq-update-lifelines( | ||||||
|  |         participants, | ||||||
|  |         pars-i, | ||||||
|  |         elmt | ||||||
|  |       ) | ||||||
|  |       cells.push(cell) | ||||||
|  |     } else if elmt.type == "evt" { | ||||||
|  |       participants = evt-update-lifelines( | ||||||
|  |         participants, | ||||||
|  |         pars-i, | ||||||
|  |         elmt | ||||||
|  |       ) | ||||||
|  |      | ||||||
|  |     } else if elmt.type == "note" { | ||||||
|  |       let cell = note-get-cell(pars-i, elmt) | ||||||
|  |       if cell != none { | ||||||
|  |         cells.push(cell) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return (participants, elements, cells) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Compute minimum widths for participant names and shapes | ||||||
|  | #let participants-min-col-widths(participants) = { | ||||||
|  |   let widths = () | ||||||
|  |   for i in range(participants.len() - 1) { | ||||||
|  |     let p1 = participants.at(i) | ||||||
|  |     let p2 = participants.at(i + 1) | ||||||
|  |     let m1 = participant.get-size(p1) | ||||||
|  |     let m2 = participant.get-size(p2) | ||||||
|  |     let w1 = m1.width | ||||||
|  |     let w2 = m2.width | ||||||
|  |     widths.push(w1 / 2pt + w2 / 2pt + PAR-SPACE) | ||||||
|  |   } | ||||||
|  |   return widths | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Compute minimum width for over notes | ||||||
|  | #let notes-min-col-widths(elements, widths, pars-i) = { | ||||||
|  |   let widths = widths | ||||||
|  |   let notes = elements.filter(e => e.type == "note") | ||||||
|  |   for n in notes.filter(e => (e.side == "over" and  | ||||||
|  |                               type(e.pos) == str)) { | ||||||
|  |      | ||||||
|  |     let m = get-note-size(n) | ||||||
|  |     let i = pars-i.at(n.pos) | ||||||
|  |  | ||||||
|  |     if i < widths.len() { | ||||||
|  |       widths.at(i) = calc.max( | ||||||
|  |         widths.at(i), | ||||||
|  |         m.width / 2 + NOTE-GAP | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |     if i > 0 { | ||||||
|  |       widths.at(i - 1) = calc.max( | ||||||
|  |         widths.at(i - 1), | ||||||
|  |         m.width / 2 + NOTE-GAP | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return widths | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Compute minimum width for simple sequences (spanning 1 column) | ||||||
|  | #let simple-seq-min-col-widths(cells, widths) = { | ||||||
|  |   let widths = widths | ||||||
|  |   for cell in cells.filter(c => c.i2 - c.i1 == 1) { | ||||||
|  |     let m = measure(cell.cell) | ||||||
|  |     widths.at(cell.i1) = calc.max( | ||||||
|  |       widths.at(cell.i1), | ||||||
|  |       m.width / 1pt + COMMENT-PAD | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |   return widths | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Compute minimum width for self sequences | ||||||
|  | #let self-seq-min-col-widths(cells, widths) = { | ||||||
|  |   let widths = widths | ||||||
|  |   for cell in cells.filter(c => (c.elmt.type == "seq" and | ||||||
|  |                                  c.i1 == c.i2)) { | ||||||
|  |     let m = measure(cell.cell) | ||||||
|  |     let i = cell.i1 | ||||||
|  |     if cell.elmt.flip { | ||||||
|  |       i -= 1 | ||||||
|  |     } | ||||||
|  |     if 0 <= i and i < widths.len() { | ||||||
|  |       widths.at(i) = calc.max( | ||||||
|  |         widths.at(i), | ||||||
|  |         m.width / 1pt + COMMENT-PAD | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return widths | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Compute remaining widths for longer sequences (spanning multiple columns) | ||||||
|  | #let long-seq-min-col-widths(participants, cells, widths) = { | ||||||
|  |   let widths = widths | ||||||
|  |   let multicol-cells = cells.filter(c => c.i2 - c.i1 > 1) | ||||||
|  |   multicol-cells = multicol-cells.sorted(key: c => { | ||||||
|  |     c.i1 * 1000 + c.i2 | ||||||
|  |   }) | ||||||
|  |   for cell in multicol-cells { | ||||||
|  |     let m = measure(cell.cell) | ||||||
|  |  | ||||||
|  |     let i1 = cell.i1 | ||||||
|  |     let i2 = cell.i2 - 1 | ||||||
|  |     let i = i2 | ||||||
|  |     if cell.i1 == 0 and participants.at(0).name == "[" { | ||||||
|  |       i = 0 | ||||||
|  |       i1 += 1 | ||||||
|  |       i2 += 1 | ||||||
|  |     } | ||||||
|  |     let width = ( | ||||||
|  |       m.width / 1pt + | ||||||
|  |       COMMENT-PAD - | ||||||
|  |       widths.slice(i1, i2).sum() | ||||||
|  |     ) | ||||||
|  |      | ||||||
|  |     widths.at(i) = calc.max( | ||||||
|  |       widths.at(i), width | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |   return widths | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Add lifeline widths | ||||||
|  | #let col-widths-add-lifelines(participants, widths) = { | ||||||
|  |   return widths.enumerate().map(((i, w)) => { | ||||||
|  |     let p1 = participants.at(i) | ||||||
|  |     let p2 = participants.at(i + 1) | ||||||
|  |     w += p1.max-lifelines * LIFELINE-W / 2 | ||||||
|  |     if p2.max-lifelines != 0 { | ||||||
|  |       w += LIFELINE-W / 2 | ||||||
|  |     } | ||||||
|  |     return w | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let process-col-elements(elements, widths, pars-i) = { | ||||||
|  |   let widths = widths | ||||||
|  |   let cols = elements.filter(e => e.type == "col") | ||||||
|  |   for col in cols { | ||||||
|  |     let i1 = pars-i.at(col.p1) | ||||||
|  |     let i2 = pars-i.at(col.p2) | ||||||
|  |     if calc.abs(i1 - i2) != 1 { | ||||||
|  |       let i-min = calc.min(i1, i2) | ||||||
|  |       let i-max = calc.max(i1, i2) | ||||||
|  |       let others = pars-i.pairs() | ||||||
|  |                          .sorted(key: p => p.last()) | ||||||
|  |                          .slice(i-min + 1, i-max) | ||||||
|  |                          .map(p => "'" + p.first() + "'") | ||||||
|  |                          .join(", ") | ||||||
|  |       panic( | ||||||
|  |         "Column participants must be consecutive (participants (" + | ||||||
|  |         others + | ||||||
|  |         ") are in between)" | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |     let i = calc.min(i1, i2) | ||||||
|  |  | ||||||
|  |     let width = widths.at(i) | ||||||
|  |  | ||||||
|  |     if col.width != auto { | ||||||
|  |       width = normalize-units(col.width) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     width = calc.max( | ||||||
|  |       width, | ||||||
|  |       normalize-units(col.min-width) | ||||||
|  |     ) | ||||||
|  |     if col.max-width != none { | ||||||
|  |       width = calc.min( | ||||||
|  |         width, | ||||||
|  |         normalize-units(col.max-width) | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |     widths.at(i) = width + normalize-units(col.margin) | ||||||
|  |   } | ||||||
|  |   return widths | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let compute-columns-width(participants, elements, pars-i) = { | ||||||
|  |   elements = elements.filter(is-elmt) | ||||||
|  |   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(participants, cells, widths) | ||||||
|  |   widths = col-widths-add-lifelines(participants, widths) | ||||||
|  |   widths = process-col-elements(elements, widths, pars-i) | ||||||
|  |   return widths | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let setup-ctx(participants, elements) = (ctx => { | ||||||
|  |   let state = ctx.at("shared-state", default: (:)) | ||||||
|  |  | ||||||
|  |   let chronos-ctx = ( | ||||||
|  |     participants: init-lifelines(participants), | ||||||
|  |     pars-i: get-participants-i(participants), | ||||||
|  |     y: 0, | ||||||
|  |     groups: (), | ||||||
|  |     lifelines: participants.map(_ => ( | ||||||
|  |       level: 0, | ||||||
|  |       lines: () | ||||||
|  |     )) | ||||||
|  |   ) | ||||||
|  |   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() | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										336
									
								
								src/core/setup.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,336 @@ | |||||||
|  | #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" | ||||||
|  |   ) | ||||||
|  |   let names = ctx.participants.map(p => p.name) | ||||||
|  |   if note.pos == none and note.side != "across" { | ||||||
|  |     let i1 = names.position(n => n == ctx.last-seq.p1) | ||||||
|  |     let i2 = names.position(n => n == ctx.last-seq.p2) | ||||||
|  |     let pars = ( | ||||||
|  |       (i1, ctx.last-seq.p1), | ||||||
|  |       (i2, ctx.last-seq.p2) | ||||||
|  |     ).sorted(key: p => p.first()) | ||||||
|  |  | ||||||
|  |     if note.side == "left" { | ||||||
|  |       note.pos = pars.first().last() | ||||||
|  |     } else if note.side == "right" { | ||||||
|  |       note.pos = pars.last().last() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let seq = ctx.elmts.at(ctx.last-seq.i) | ||||||
|  |     seq.linked-notes.push(note) | ||||||
|  |     ctx.elmts.at(ctx.last-seq.i) = seq | ||||||
|  |   } | ||||||
|  |   if note.aligned { | ||||||
|  |     let n = ctx.last-note.note | ||||||
|  |     n.aligned-with = note | ||||||
|  |     ctx.elmts.at(ctx.last-note.i) = n | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if note.side in ("left", "right") { | ||||||
|  |     let i = names.position(n => n == note.pos) | ||||||
|  |     let pos2 = note.pos | ||||||
|  |     if note.side == "left" { | ||||||
|  |       if i <= 0 or note.allow-overlap { | ||||||
|  |         ctx.linked.push("[") | ||||||
|  |         pos2 = "[" | ||||||
|  |       } else { | ||||||
|  |         pos2 = names.at(i - 1) | ||||||
|  |       } | ||||||
|  |     } else if note.side == "right" { | ||||||
|  |       if i >= names.len() - 1 or note.allow-overlap { | ||||||
|  |         ctx.linked.push("]") | ||||||
|  |         pos2 = "]" | ||||||
|  |       } else { | ||||||
|  |         pos2 = names.at(i + 1) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     note.insert("pos2", pos2) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let pars = none | ||||||
|  |   if type(note.pos) == str { | ||||||
|  |     pars = (note.pos,) | ||||||
|  |   } else if type(note.pos) == array { | ||||||
|  |     pars = note.pos | ||||||
|  |   } | ||||||
|  |   if pars != none { | ||||||
|  |     for par in pars { | ||||||
|  |       if not par-exists(ctx.participants, par) { | ||||||
|  |         participants.push(_par(par).first()) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ctx.elmts.at(ctx.i) = note | ||||||
|  |  | ||||||
|  |   ctx.last-note = ( | ||||||
|  |     note: note, | ||||||
|  |     i: ctx.i | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   return ctx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let prepare-evt-participants(ctx, evt) = { | ||||||
|  |   let par = evt.participant | ||||||
|  |   if not par-exists(ctx.participants, par) { | ||||||
|  |     let p = _par( | ||||||
|  |       par, | ||||||
|  |       from-start: evt.event != "create" | ||||||
|  |     ).first() | ||||||
|  |     ctx.participants.push(p) | ||||||
|  |    | ||||||
|  |   } else if evt.event == "create" { | ||||||
|  |     let i = ctx.participants.position(p => p.name == par) | ||||||
|  |     ctx.participants.at(i).from-start = false | ||||||
|  |   } | ||||||
|  |   return ctx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let normalize-special-participants(elmt) = { | ||||||
|  |   if elmt.p1 == "?" { | ||||||
|  |     elmt.p1 = "?" + elmt.p2 | ||||||
|  |   } else if elmt.p2 == "?" { | ||||||
|  |     elmt.p2 = elmt.p1 + "?" | ||||||
|  |   } | ||||||
|  |   return elmt | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let prepare-participants(elmts) = { | ||||||
|  |   let ctx = ( | ||||||
|  |     linked: (), | ||||||
|  |     last-seq: none, | ||||||
|  |     last-note: none, | ||||||
|  |     participants: (), | ||||||
|  |     elmts: elmts, | ||||||
|  |     i: 0 | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   for (i, elmt) in ctx.elmts.enumerate() { | ||||||
|  |     ctx.i = i | ||||||
|  |     if not is-elmt(elmt) { | ||||||
|  |       continue | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if elmt.type == "par" { | ||||||
|  |       ctx.participants.push(elmt) | ||||||
|  |  | ||||||
|  |     } else if elmt.type == "seq" { | ||||||
|  |       ctx = prepare-seq-participants(ctx, elmt) | ||||||
|  |      | ||||||
|  |     } else if elmt.type == "note" { | ||||||
|  |       ctx = prepare-note-participants(ctx, elmt) | ||||||
|  |      | ||||||
|  |     } else if elmt.type == "evt" { | ||||||
|  |       ctx = prepare-evt-participants(ctx, elmt) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   ctx.linked = ctx.linked.dedup() | ||||||
|  |  | ||||||
|  |   let pars = ctx.participants | ||||||
|  |   let participants = () | ||||||
|  |    | ||||||
|  |   if "[" in ctx.linked { | ||||||
|  |     participants.push(_par("[", invisible: true).first()) | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   for (i, p) in pars.enumerate() { | ||||||
|  |     let before = _par( | ||||||
|  |       "?" + p.name, | ||||||
|  |       invisible: true | ||||||
|  |     ).first() | ||||||
|  |     let after = _par( | ||||||
|  |       p.name + "?", | ||||||
|  |       invisible: true | ||||||
|  |     ).first() | ||||||
|  |      | ||||||
|  |     if before.name in ctx.linked { | ||||||
|  |       if participants.len() == 0 or not participants.last().name.ends-with("?") { | ||||||
|  |         participants.push(before) | ||||||
|  |       } else { | ||||||
|  |         participants.insert(-1, before) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     participants.push(p) | ||||||
|  |  | ||||||
|  |     if after.name in ctx.linked { | ||||||
|  |       participants.push(after) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if "]" in ctx.linked { | ||||||
|  |     participants.push(_par( | ||||||
|  |       "]", | ||||||
|  |       invisible: true | ||||||
|  |     ).first()) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return (ctx.elmts, participants) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let finalize-setup(elmts, participants) = { | ||||||
|  |   for (i, p) in participants.enumerate() { | ||||||
|  |     p.insert("i", i) | ||||||
|  |     participants.at(i) = p | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let containers = () | ||||||
|  |  | ||||||
|  |   for (i, elmt) in elmts.enumerate() { | ||||||
|  |     if not is-elmt(elmt) { | ||||||
|  |       continue | ||||||
|  |     } | ||||||
|  |     if elmt.type == "seq" { | ||||||
|  |       elmts.at(i) = normalize-special-participants(elmt) | ||||||
|  |     } else if elmt.type == "grp-end" { | ||||||
|  |       // Put back elements in group because they might have changed | ||||||
|  |       elmts = update-group-children(elmts, i) | ||||||
|  |     } else if elmt.type in ("grp", "alt") { | ||||||
|  |       containers.push(i) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Compute groups spans (horizontal) | ||||||
|  |   for i in containers { | ||||||
|  |     let elmt = elmts.at(i) | ||||||
|  |     let (min-i, max-i) = get-group-span(participants, elmt) | ||||||
|  |     elmts.at(i).insert("min-i", min-i) | ||||||
|  |     elmts.at(i).insert("max-i", max-i) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return (elmts, participants) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let setup(elements) = { | ||||||
|  |   let (elmts, activation-history) = unwrap-containers(elements) | ||||||
|  |    | ||||||
|  |   let participants | ||||||
|  |   (elmts, participants) = prepare-participants(elmts) | ||||||
|  |  | ||||||
|  |   return finalize-setup(elmts, participants) | ||||||
|  | } | ||||||
| @@ -1,3 +1,15 @@ | |||||||
|  | #import "/src/cetz.typ": draw | ||||||
|  | 
 | ||||||
|  | #let is-elmt(elmt) = { | ||||||
|  |   if type(elmt) != dictionary { | ||||||
|  |     return false | ||||||
|  |   } | ||||||
|  |   if "type" not in elmt { | ||||||
|  |     return false | ||||||
|  |   } | ||||||
|  |   return true | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #let normalize-units(value) = { | #let normalize-units(value) = { | ||||||
|   if type(value) == int or type(value) == float { |   if type(value) == int or type(value) == float { | ||||||
|     return value |     return value | ||||||
| @@ -36,6 +48,9 @@ | |||||||
|       max-i = calc.max(max-i, i1) |       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) |   return (min-i, max-i) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -77,3 +92,26 @@ | |||||||
|     scale(x: r, y: r, reflow: true, canvas) |     scale(x: r, y: r, reflow: true, canvas) | ||||||
|   ) |   ) | ||||||
| }) | }) | ||||||
|  | 
 | ||||||
|  | #let set-ctx(func) = draw.set-ctx(c => { | ||||||
|  |   let ctx = c.shared-state.chronos | ||||||
|  |   let new-ctx = func(ctx) | ||||||
|  |   assert(new-ctx != none, message: "set-ctx must return a context!") | ||||||
|  |   c.shared-state.chronos = new-ctx | ||||||
|  |   return c | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | #let get-ctx(func) = draw.get-ctx(c => { | ||||||
|  |   let ctx = c.shared-state.chronos | ||||||
|  |   func(ctx) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | #let expand-parent-group(x0, x1) = set-ctx(ctx => { | ||||||
|  |   if ctx.groups.len() != 0 { | ||||||
|  |     let group = ctx.groups.last() | ||||||
|  |     group.min-x = calc.min(group.min-x, x0) | ||||||
|  |     group.max-x = calc.max(group.max-x, x1) | ||||||
|  |     ctx.groups.last() = group | ||||||
|  |   } | ||||||
|  |   return ctx | ||||||
|  | }) | ||||||
							
								
								
									
										241
									
								
								src/diagram.typ
									
									
									
									
									
								
							
							
						
						| @@ -1,245 +1,14 @@ | |||||||
| #import "utils.typ": get-group-span, fit-canvas | #import "core/draw/event.typ": render as evt-render | ||||||
| #import "renderer.typ": render | #import "core/renderer.typ": render | ||||||
| #import "participant.typ" as participant: _par, PAR-SPECIALS | #import "core/setup.typ": setup | ||||||
| #import "sequence.typ": _seq | #import "core/utils.typ": fit-canvas, set-ctx | ||||||
|  |  | ||||||
| #let _gap(size: 20) = { |  | ||||||
|   return (( |  | ||||||
|     type: "gap", |  | ||||||
|     size: size |  | ||||||
|   ),) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #let _evt(participant, event) = { |  | ||||||
|   return (( |  | ||||||
|     type: "evt", |  | ||||||
|     participant: participant, |  | ||||||
|     event: event, |  | ||||||
|     lifeline-style: auto |  | ||||||
|   ),) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #let _col(p1, p2, width: auto, margin: 0, min-width: 0, max-width: none) = { |  | ||||||
|   return (( |  | ||||||
|     type: "col", |  | ||||||
|     p1: p1, |  | ||||||
|     p2: p2, |  | ||||||
|     width: width, |  | ||||||
|     margin: margin, |  | ||||||
|     min-width: min-width, |  | ||||||
|     max-width: max-width |  | ||||||
|   ),) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #let diagram(elements, width: auto) = { | #let diagram(elements, width: auto) = { | ||||||
|   if elements == none { |   if elements == none { | ||||||
|     return |     return | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   let participants = () |   let (elmts, participants) = setup(elements) | ||||||
|   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 + "?" |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let canvas = render(participants, elmts) |   let canvas = render(participants, elmts) | ||||||
|   fit-canvas(canvas, width: width) |   fit-canvas(canvas, width: width) | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| #import "@preview/cetz:0.3.1": draw | #import "core/draw/group.typ" | ||||||
| #import "consts.typ": * |  | ||||||
|  |  | ||||||
| #let _grp(name, desc: none, type: "default", elmts) = { | #let _grp(name, desc: none, type: "default", elmts) = { | ||||||
|   return (( |   return (( | ||||||
|     type: "grp", |     type: "grp", | ||||||
|  |     draw: group.render-start, | ||||||
|     name: name, |     name: name, | ||||||
|     desc: desc, |     desc: desc, | ||||||
|     grp-type: type, |     grp-type: type, | ||||||
| @@ -20,6 +20,7 @@ | |||||||
|     let else-elmts = args.at(i + 1, default: ()) |     let else-elmts = args.at(i + 1, default: ()) | ||||||
|     all-elmts.push(( |     all-elmts.push(( | ||||||
|       type: "else", |       type: "else", | ||||||
|  |       draw: group.render-else, | ||||||
|       desc: else-desc |       desc: else-desc | ||||||
|     )) |     )) | ||||||
|     all-elmts += else-elmts |     all-elmts += else-elmts | ||||||
| @@ -40,60 +41,3 @@ | |||||||
| } | } | ||||||
| #let _opt(desc, elmts) = grp("opt", desc: desc, type: "opt", elmts) | #let _opt(desc, elmts) = grp("opt", desc: desc, type: "opt", elmts) | ||||||
| #let _break(desc, elmts) = grp("break", desc: desc, type: "break", 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 |  | ||||||
| } |  | ||||||
| @@ -1,9 +1,8 @@ | |||||||
| #let version = version(0, 2, 0) | #let version = version(0, 2, 2) | ||||||
| #import "diagram.typ": diagram, from-plantuml, _gap, _evt, _col | #import "diagram.typ": diagram, from-plantuml | ||||||
|  |  | ||||||
| #import "sequence.typ": _seq, _ret | #import "sequence.typ": _seq, _ret | ||||||
| #import "group.typ": _grp, _loop, _alt, _opt, _break | #import "group.typ": _grp, _loop, _alt, _opt, _break | ||||||
| #import "participant.typ": _par | #import "participant.typ": _par | ||||||
| #import "separator.typ": _sep, _delay | #import "misc.typ": _sep, _delay, _sync, _gap, _evt, _col | ||||||
| #import "note.typ": _note | #import "note.typ": _note | ||||||
| #import "sync.typ": _sync |  | ||||||
							
								
								
									
										65
									
								
								src/misc.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,65 @@ | |||||||
|  | #import "core/draw/delay.typ" | ||||||
|  | #import "core/draw/event.typ": render as evt-render | ||||||
|  | #import "core/draw/separator.typ" | ||||||
|  | #import "core/draw/sync.typ" | ||||||
|  | #import "core/utils.typ": set-ctx | ||||||
|  |  | ||||||
|  | #let _sep(name) = { | ||||||
|  |   return (( | ||||||
|  |     type: "sep", | ||||||
|  |     draw: separator.render, | ||||||
|  |     name: name | ||||||
|  |   ),) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let _delay(name: none, size: 30) = { | ||||||
|  |   return (( | ||||||
|  |     type: "delay", | ||||||
|  |     draw: delay.render, | ||||||
|  |     name: name, | ||||||
|  |     size: size | ||||||
|  |   ),) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let _sync(elmts) = { | ||||||
|  |   return (( | ||||||
|  |     type: "sync", | ||||||
|  |     draw: sync.render, | ||||||
|  |     elmts: elmts | ||||||
|  |   ),) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let gap-render(gap) = set-ctx(ctx => { | ||||||
|  |   ctx.y -= gap.size | ||||||
|  |   return ctx | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | #let _gap(size: 20) = { | ||||||
|  |   return (( | ||||||
|  |     type: "gap", | ||||||
|  |     draw: gap-render, | ||||||
|  |     size: size | ||||||
|  |   ),) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #let _evt(participant, event) = { | ||||||
|  |   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 | ||||||
|  |   ),) | ||||||
|  | } | ||||||
							
								
								
									
										160
									
								
								src/note.typ
									
									
									
									
									
								
							
							
						
						| @@ -1,5 +1,5 @@ | |||||||
| #import "@preview/cetz:0.3.1": draw |  | ||||||
| #import "consts.typ": * | #import "consts.typ": * | ||||||
|  | #import "core/draw/note.typ" | ||||||
|  |  | ||||||
| #let SIDES = ( | #let SIDES = ( | ||||||
|   "left", |   "left", | ||||||
| @@ -14,7 +14,15 @@ | |||||||
|   "hex" |   "hex" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| #let _note(side, content, pos: none, color: COL-NOTE, shape: "default", aligned: false) = { | #let _note( | ||||||
|  |   side, | ||||||
|  |   content, | ||||||
|  |   pos: none, | ||||||
|  |   color: COL-NOTE, | ||||||
|  |   shape: "default", | ||||||
|  |   aligned: false, | ||||||
|  |   allow-overlap: true | ||||||
|  | ) = { | ||||||
|   if side == "over" { |   if side == "over" { | ||||||
|     if pos == none { |     if pos == none { | ||||||
|       panic("Pos cannot be none with side 'over'") |       panic("Pos cannot be none with side 'over'") | ||||||
| @@ -30,156 +38,14 @@ | |||||||
|   } |   } | ||||||
|   return (( |   return (( | ||||||
|     type: "note", |     type: "note", | ||||||
|  |     draw: note.render, | ||||||
|     side: side, |     side: side, | ||||||
|     content: content, |     content: content, | ||||||
|     pos: pos, |     pos: pos, | ||||||
|     color: color, |     color: color, | ||||||
|     shape: shape, |     shape: shape, | ||||||
|     aligned: aligned, |     aligned: aligned, | ||||||
|     aligned-with: none |     aligned-with: none, | ||||||
|  |     allow-overlap: allow-overlap | ||||||
|   ),) |   ),) | ||||||
| } | } | ||||||
|  |  | ||||||
| #let get-note-box(note) = { |  | ||||||
|   let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD} |  | ||||||
|   let inset = ( |  | ||||||
|     left: PAD.last() * 1pt, |  | ||||||
|     right: PAD.last() * 1pt, |  | ||||||
|     top: PAD.first() * 1pt, |  | ||||||
|     bottom: PAD.first() * 1pt, |  | ||||||
|   ) |  | ||||||
|   if note.shape == "default" { |  | ||||||
|     inset.right += NOTE-CORNER-SIZE * 1pt |  | ||||||
|   } |  | ||||||
|   if note.side == "left" { |  | ||||||
|     inset.right += NOTE-GAP * 1pt |  | ||||||
|   } else if note.side == "right" { |  | ||||||
|     inset.left += NOTE-GAP * 1pt |  | ||||||
|   } |  | ||||||
|   return box(note.content, inset: inset) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #let get-size(note) = { |  | ||||||
|   let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD} |  | ||||||
|   let m = measure(box(note.content)) |  | ||||||
|   let w = m.width / 1pt + PAD.last() * 2 |  | ||||||
|   let h = m.height / 1pt + PAD.first() * 2 |  | ||||||
|   if note.shape == "default" { |  | ||||||
|     w += NOTE-CORNER-SIZE |  | ||||||
|   } |  | ||||||
|   return ( |  | ||||||
|     width: w, |  | ||||||
|     height: h |  | ||||||
|   ) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #let _get-base-x(pars-i, x-pos, note) = { |  | ||||||
|   if note.side == "across" { |  | ||||||
|     return (x-pos.first() + x-pos.last()) / 2 |  | ||||||
|   } |  | ||||||
|   if note.side == "over" { |  | ||||||
|     if type(note.pos) == array { |  | ||||||
|       let xs = note.pos.map(par => x-pos.at(pars-i.at(par))) |  | ||||||
|       return (calc.min(..xs) + calc.max(..xs)) / 2 |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return x-pos.at(pars-i.at(note.pos)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #let render(pars-i, x-pos, note, y, lifelines) = { |  | ||||||
|   let shapes = () |  | ||||||
|   let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD} |  | ||||||
|   let m = measure(box(note.content)) |  | ||||||
|   let w = m.width / 1pt + PAD.last() * 2 |  | ||||||
|   let h = m.height / 1pt + PAD.first() * 2 |  | ||||||
|   let total-w = w |  | ||||||
|   if note.shape == "default" { |  | ||||||
|     total-w += NOTE-CORNER-SIZE |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let base-x = _get-base-x(pars-i, x-pos, note) |  | ||||||
|  |  | ||||||
|   let i = none |  | ||||||
|   if note.pos != none and type(note.pos) == str { |  | ||||||
|     i = pars-i.at(note.pos) |  | ||||||
|   } |  | ||||||
|   let x0 = base-x |  | ||||||
|   if note.side == "left" { |  | ||||||
|     x0 -= NOTE-GAP |  | ||||||
|     x0 -= total-w |  | ||||||
|     if lifelines.at(i).level != 0 { |  | ||||||
|       x0 -= LIFELINE-W / 2 |  | ||||||
|     } |  | ||||||
|   } else if note.side == "right" { |  | ||||||
|     x0 += NOTE-GAP |  | ||||||
|     x0 += lifelines.at(i).level * LIFELINE-W / 2 |  | ||||||
|   } else if note.side == "over" or note.side == "across" { |  | ||||||
|     x0 -= total-w / 2 |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let x1 = x0 + w |  | ||||||
|   let x2 = x0 + total-w |  | ||||||
|   let y0 = y |  | ||||||
|  |  | ||||||
|   if note.linked { |  | ||||||
|     y0 += h / 2 |  | ||||||
|   } |  | ||||||
|   let y1 = y0 - h |  | ||||||
|  |  | ||||||
|   if note.shape == "default" { |  | ||||||
|     shapes += draw.merge-path( |  | ||||||
|       stroke: black + .5pt, |  | ||||||
|       fill: note.color, |  | ||||||
|       close: true, |  | ||||||
|       { |  | ||||||
|         draw.line( |  | ||||||
|           (x0, y0), |  | ||||||
|           (x1, y0), |  | ||||||
|           (x2, y0 - NOTE-CORNER-SIZE), |  | ||||||
|           (x2, y1), |  | ||||||
|           (x0, y1) |  | ||||||
|         ) |  | ||||||
|       } |  | ||||||
|     ) |  | ||||||
|     shapes += draw.line((x1, y0), (x1, y0 - NOTE-CORNER-SIZE), (x2, y0 - NOTE-CORNER-SIZE), stroke: black + .5pt) |  | ||||||
|   } else if note.shape == "rect" { |  | ||||||
|     shapes += draw.rect( |  | ||||||
|       (x0, y0), |  | ||||||
|       (x2, y1), |  | ||||||
|       stroke: black + .5pt, |  | ||||||
|       fill: note.color |  | ||||||
|     ) |  | ||||||
|   } else if note.shape == "hex" { |  | ||||||
|     let lx = x0 + PAD.last() |  | ||||||
|     let rx = x2 - PAD.last() |  | ||||||
|     let my = (y0 + y1) / 2 |  | ||||||
|     shapes += draw.merge-path( |  | ||||||
|       stroke: black + .5pt, |  | ||||||
|       fill: note.color, |  | ||||||
|       close: true, |  | ||||||
|       { |  | ||||||
|         draw.line( |  | ||||||
|           (lx, y0), |  | ||||||
|           (rx, y0), |  | ||||||
|           (x2, my), |  | ||||||
|           (rx, y1), |  | ||||||
|           (lx, y1), |  | ||||||
|           (x0, my), |  | ||||||
|         ) |  | ||||||
|       } |  | ||||||
|     ) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   shapes += draw.content( |  | ||||||
|     ((x0 + x1)/2, (y0 + y1)/2), |  | ||||||
|     note.content, |  | ||||||
|     anchor: "center" |  | ||||||
|   ) |  | ||||||
|  |  | ||||||
|   if note.aligned-with == none and (note.pos != none or note.side == "across") { |  | ||||||
|     y -= h |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let r = (y, shapes) |  | ||||||
|   return r |  | ||||||
| } |  | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| #import "@preview/cetz:0.3.1": draw | #import "core/draw/participant.typ" | ||||||
| #import "consts.typ": * |  | ||||||
|  |  | ||||||
| #let PAR-SPECIALS = "?[]" | #let PAR-SPECIALS = ("?", "[", "]") | ||||||
| #let SHAPES = ( | #let SHAPES = ( | ||||||
|   "participant", |   "participant", | ||||||
|   "actor", |   "actor", | ||||||
| @@ -22,6 +21,11 @@ | |||||||
|   invisible: false, |   invisible: false, | ||||||
|   shape: "participant", |   shape: "participant", | ||||||
|   color: DEFAULT-COLOR, |   color: DEFAULT-COLOR, | ||||||
|  |   line-stroke: ( | ||||||
|  |     dash: "dashed", | ||||||
|  |     paint: gray.darken(40%), | ||||||
|  |     thickness: .5pt | ||||||
|  |   ), | ||||||
|   custom-image: none, |   custom-image: none, | ||||||
|   show-bottom: true, |   show-bottom: true, | ||||||
|   show-top: true, |   show-top: true, | ||||||
| @@ -31,12 +35,14 @@ | |||||||
|   } |   } | ||||||
|   return (( |   return (( | ||||||
|     type: "par", |     type: "par", | ||||||
|  |     draw: participant.render, | ||||||
|     name: name, |     name: name, | ||||||
|     display-name: if display-name == auto {name} else {display-name}, |     display-name: if display-name == auto {name} else {display-name}, | ||||||
|     from-start: from-start, |     from-start: from-start, | ||||||
|     invisible: invisible, |     invisible: invisible, | ||||||
|     shape: shape, |     shape: shape, | ||||||
|     color: color, |     color: color, | ||||||
|  |     line-stroke: line-stroke, | ||||||
|     custom-image: custom-image, |     custom-image: custom-image, | ||||||
|     show-bottom: show-bottom, |     show-bottom: show-bottom, | ||||||
|     show-top: show-top |     show-top: show-top | ||||||
| @@ -44,7 +50,7 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| #let _exists(participants, name) = { | #let _exists(participants, name) = { | ||||||
|   if name == "?" or name == "[" or name == "]" { |   if name in PAR-SPECIALS { | ||||||
|     return true |     return true | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -55,307 +61,3 @@ | |||||||
|   } |   } | ||||||
|   return false |   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) |  | ||||||
| } |  | ||||||
							
								
								
									
										522
									
								
								src/renderer.typ
									
									
									
									
									
								
							
							
						
						| @@ -1,522 +0,0 @@ | |||||||
| #import "@preview/cetz:0.3.1": canvas, draw |  | ||||||
| #import "utils.typ": get-participants-i, get-style, normalize-units |  | ||||||
| #import "group.typ" |  | ||||||
| #import "participant.typ" |  | ||||||
| #import participant: PAR-SPECIALS |  | ||||||
| #import "sequence.typ" |  | ||||||
| #import "separator.typ" |  | ||||||
| #import "sync.typ" |  | ||||||
| #import "consts.typ": * |  | ||||||
| #import "note.typ" as note: get-note-box |  | ||||||
|  |  | ||||||
| #let DEBUG-INVISIBLE = false |  | ||||||
|  |  | ||||||
| #let get-columns-width(participants, elements) = { |  | ||||||
|   participants = participants.map(p => { |  | ||||||
|     p.insert("lifeline-lvl", 0) |  | ||||||
|     p.insert("max-lifelines", 0) |  | ||||||
|     p |  | ||||||
|   }) |  | ||||||
|   let pars-i = get-participants-i(participants) |  | ||||||
|   let cells = () |  | ||||||
|  |  | ||||||
|   // Unwrap syncs |  | ||||||
|   let i = 0 |  | ||||||
|   while i < elements.len() { |  | ||||||
|     let elmt = elements.at(i) |  | ||||||
|     if elmt.type == "sync" { |  | ||||||
|       elements = elements.slice(0, i + 1) + elmt.elmts + elements.slice(i + 1) |  | ||||||
|     } |  | ||||||
|     i += 1 |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Compute max lifeline levels |  | ||||||
|   for elmt in elements { |  | ||||||
|     if elmt.type == "seq" { |  | ||||||
|       let com = if elmt.comment == none {""} else {elmt.comment} |  | ||||||
|       let i1 = pars-i.at(elmt.p1) |  | ||||||
|       let i2 = pars-i.at(elmt.p2) |  | ||||||
|       cells.push( |  | ||||||
|         ( |  | ||||||
|           elmt: elmt, |  | ||||||
|           i1: calc.min(i1, i2), |  | ||||||
|           i2: calc.max(i1, i2), |  | ||||||
|           cell: box(com, inset: 3pt) |  | ||||||
|         ) |  | ||||||
|       ) |  | ||||||
|  |  | ||||||
|       if elmt.disable-src or elmt.destroy-src { |  | ||||||
|         let p = participants.at(i1) |  | ||||||
|         p.lifeline-lvl -= 1 |  | ||||||
|         participants.at(i1) = p |  | ||||||
|       } |  | ||||||
|       if elmt.disable-dst { |  | ||||||
|         let p = participants.at(i2) |  | ||||||
|         p.lifeline-lvl -= 1 |  | ||||||
|         participants.at(i2) = p |  | ||||||
|       } |  | ||||||
|       if elmt.enable-dst { |  | ||||||
|         let p = participants.at(i2) |  | ||||||
|         p.lifeline-lvl += 1 |  | ||||||
|         p.max-lifelines = calc.max(p.max-lifelines, p.lifeline-lvl) |  | ||||||
|         participants.at(i2) = p |  | ||||||
|       } |  | ||||||
|     } else if elmt.type == "evt" { |  | ||||||
|       let par-name = elmt.participant |  | ||||||
|       let i = pars-i.at(par-name) |  | ||||||
|       let par = participants.at(i) |  | ||||||
|       if elmt.event == "disable" or elmt.event == "destroy" { |  | ||||||
|         par.lifeline-lvl -= 1 |  | ||||||
|        |  | ||||||
|       } else if elmt.event == "enable" { |  | ||||||
|         par.lifeline-lvl += 1 |  | ||||||
|         par.max-lifelines = calc.max(par.max-lifelines, par.lifeline-lvl) |  | ||||||
|       } |  | ||||||
|       participants.at(i) = par |  | ||||||
|      |  | ||||||
|     } else if elmt.type == "note" { |  | ||||||
|       let (p1, p2) = (none, none) |  | ||||||
|       let cell = none |  | ||||||
|       if elmt.side == "left" { |  | ||||||
|         p1 = "[" |  | ||||||
|         p2 = elmt.pos |  | ||||||
|         cell = get-note-box(elmt) |  | ||||||
|       } else if elmt.side == "right" { |  | ||||||
|         p1 = elmt.pos |  | ||||||
|         p2 = "]" |  | ||||||
|         cell = get-note-box(elmt) |  | ||||||
|       } else if elmt.side == "over" { |  | ||||||
|         if elmt.aligned-with != none { |  | ||||||
|           let box1 = get-note-box(elmt) |  | ||||||
|           let box2 = get-note-box(elmt.aligned-with) |  | ||||||
|           let m1 = measure(box1) |  | ||||||
|           let m2 = measure(box2) |  | ||||||
|           cell = box(width: (m1.width + m2.width) / 2, height: calc.max(m1.height, m2.height)) |  | ||||||
|           p1 = elmt.pos |  | ||||||
|           p2 = elmt.aligned-with.pos |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if p1 != none and p2 != none and cell != none { |  | ||||||
|         let i1 = pars-i.at(p1) |  | ||||||
|         let i2 = pars-i.at(p2) |  | ||||||
|         cells.push( |  | ||||||
|           ( |  | ||||||
|             elmt: elmt, |  | ||||||
|             i1: calc.min(i1, i2), |  | ||||||
|             i2: calc.max(i1, i2), |  | ||||||
|             cell: cell |  | ||||||
|           ) |  | ||||||
|         ) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Compute column widths |  | ||||||
|   // Compute minimum widths for participant names and shapes |  | ||||||
|   let widths = () |  | ||||||
|   for i in range(participants.len() - 1) { |  | ||||||
|     let p1 = participants.at(i) |  | ||||||
|     let p2 = participants.at(i + 1) |  | ||||||
|     let m1 = participant.get-size(p1) |  | ||||||
|     let m2 = participant.get-size(p2) |  | ||||||
|     let w1 = m1.width |  | ||||||
|     let w2 = m2.width |  | ||||||
|     widths.push(w1 / 2pt + w2 / 2pt + PAR-SPACE) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Compute minimum width for over notes |  | ||||||
|   for n in elements.filter(e => (e.type == "note" and |  | ||||||
|                                  e.side == "over" and  |  | ||||||
|                                  type(e.pos) == str)) { |  | ||||||
|      |  | ||||||
|     let m = note.get-size(n) |  | ||||||
|     let i = pars-i.at(n.pos) |  | ||||||
|  |  | ||||||
|     if i < widths.len() { |  | ||||||
|       widths.at(i) = calc.max( |  | ||||||
|         widths.at(i), |  | ||||||
|         m.width / 2 + NOTE-GAP |  | ||||||
|       ) |  | ||||||
|     } |  | ||||||
|     if i > 0 { |  | ||||||
|       widths.at(i - 1) = calc.max( |  | ||||||
|         widths.at(i - 1), |  | ||||||
|         m.width / 2 + NOTE-GAP |  | ||||||
|       ) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Compute minimum width for simple sequences (spanning 1 column) |  | ||||||
|   for cell in cells.filter(c => c.i2 - c.i1 == 1) { |  | ||||||
|     let m = measure(cell.cell) |  | ||||||
|     widths.at(cell.i1) = calc.max( |  | ||||||
|       widths.at(cell.i1), |  | ||||||
|       m.width / 1pt + COMMENT-PAD |  | ||||||
|     ) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Compute minimum width for self sequences |  | ||||||
|   for cell in cells.filter(c => c.elmt.type == "seq" and c.i1 == c.i2) { |  | ||||||
|     let m = measure(cell.cell) |  | ||||||
|     let i = cell.i1 |  | ||||||
|     if cell.elmt.flip { |  | ||||||
|       i -= 1 |  | ||||||
|     } |  | ||||||
|     if 0 <= i and i < widths.len() { |  | ||||||
|       widths.at(i) = calc.max( |  | ||||||
|         widths.at(i), |  | ||||||
|         m.width / 1pt + COMMENT-PAD |  | ||||||
|       ) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Compute remaining widths for longer sequences (spanning multiple columns) |  | ||||||
|   let multicol-cells = cells.filter(c => c.i2 - c.i1 > 1) |  | ||||||
|   multicol-cells = multicol-cells.sorted(key: c => { |  | ||||||
|     c.i1 * 1000 + c.i2 |  | ||||||
|   }) |  | ||||||
|   for cell in multicol-cells { |  | ||||||
|     let m = measure(cell.cell) |  | ||||||
|     widths.at(cell.i2 - 1) = calc.max( |  | ||||||
|       widths.at(cell.i2 - 1), |  | ||||||
|       m.width / 1pt + COMMENT-PAD - widths.slice(cell.i1, cell.i2 - 1).sum() |  | ||||||
|     ) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Add lifeline widths |  | ||||||
|   for (i, w) in widths.enumerate() { |  | ||||||
|     let p1 = participants.at(i) |  | ||||||
|     let p2 = participants.at(i + 1) |  | ||||||
|     let w = w + p1.max-lifelines * LIFELINE-W / 2 |  | ||||||
|     if p2.max-lifelines != 0 { |  | ||||||
|       w += LIFELINE-W / 2 |  | ||||||
|     } |  | ||||||
|     widths.at(i) = w |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   for elmt in elements { |  | ||||||
|     if elmt.type == "col" { |  | ||||||
|       let i1 = pars-i.at(elmt.p1) |  | ||||||
|       let i2 = pars-i.at(elmt.p2) |  | ||||||
|       if calc.abs(i1 - i2) != 1 { |  | ||||||
|         let i-min = calc.min(i1, i2) |  | ||||||
|         let i-max = calc.max(i1, i2) |  | ||||||
|         let others = pars-i.pairs() |  | ||||||
|                            .sorted(key: p => p.last()) |  | ||||||
|                            .slice(i-min + 1, i-max) |  | ||||||
|                            .map(p => "'" + p.first() + "'") |  | ||||||
|                            .join(", ") |  | ||||||
|         panic( |  | ||||||
|           "Column participants must be consecutive (participants (" + |  | ||||||
|           others + |  | ||||||
|           ") are in between)" |  | ||||||
|         ) |  | ||||||
|       } |  | ||||||
|       let i = calc.min(i1, i2) |  | ||||||
|  |  | ||||||
|       if elmt.width != auto { |  | ||||||
|         widths.at(i) = normalize-units(elmt.width) |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let width = widths.at(i) |  | ||||||
|       width = calc.max(width, normalize-units(elmt.min-width)) |  | ||||||
|       if elmt.max-width != none { |  | ||||||
|         width = calc.min(width, normalize-units(elmt.max-width)) |  | ||||||
|       } |  | ||||||
|       widths.at(i) = width + normalize-units(elmt.margin) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return widths |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #let render(participants, elements) = context canvas(length: 1pt, { |  | ||||||
|   let shapes = () |  | ||||||
|   let pars-i = get-participants-i(participants) |  | ||||||
|  |  | ||||||
|   let widths = get-columns-width(participants, elements) |  | ||||||
|  |  | ||||||
|   // Compute each column's X position |  | ||||||
|   let x-pos = (0,) |  | ||||||
|   for width in widths { |  | ||||||
|     x-pos.push(x-pos.last() + width) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let draw-seq = sequence.render.with(pars-i, x-pos, participants) |  | ||||||
|   let draw-group = group.render.with() |  | ||||||
|   let draw-else = group.render-else.with() |  | ||||||
|   let draw-sep = separator.render.with(x-pos) |  | ||||||
|   let draw-par = participant.render.with(x-pos) |  | ||||||
|   let draw-note = note.render.with(pars-i, x-pos) |  | ||||||
|   let draw-sync = sync.render.with(pars-i, x-pos, participants) |  | ||||||
|    |  | ||||||
|   // Draw participants (start) |  | ||||||
|   for p in participants { |  | ||||||
|     if p.from-start and not p.invisible and p.show-top { |  | ||||||
|       shapes += draw-par(p) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let y = 0 |  | ||||||
|   let groups = () |  | ||||||
|   let lifelines = participants.map(_ => ( |  | ||||||
|     level: 0, |  | ||||||
|     lines: () |  | ||||||
|   )) |  | ||||||
|  |  | ||||||
|   // Draw elemnts |  | ||||||
|   for elmt in elements { |  | ||||||
|     // Sequences |  | ||||||
|     if elmt.type == "seq" { |  | ||||||
|       let shps |  | ||||||
|       (y, lifelines, shps) = draw-seq(elmt, y, lifelines) |  | ||||||
|       shapes += shps |  | ||||||
|  |  | ||||||
|     // Groups (start) -> reserve space for labels + store position |  | ||||||
|     } else if elmt.type == "grp" { |  | ||||||
|       y -= Y-SPACE |  | ||||||
|       let m = measure( |  | ||||||
|         box( |  | ||||||
|           elmt.name, |  | ||||||
|           inset: (left: 5pt, right: 5pt, top: 3pt, bottom: 3pt), |  | ||||||
|         ) |  | ||||||
|       ) |  | ||||||
|       groups = groups.map(g => { |  | ||||||
|         if g.at(1).min-i == elmt.min-i { g.at(2) += 1 } |  | ||||||
|         if g.at(1).max-i == elmt.max-i { g.at(3) += 1 } |  | ||||||
|         g |  | ||||||
|       }) |  | ||||||
|       if elmt.grp-type == "alt" { |  | ||||||
|         elmt.insert("elses", ()) |  | ||||||
|       } |  | ||||||
|       groups.push((y, elmt, 0, 0)) |  | ||||||
|       y -= m.height / 1pt |  | ||||||
|      |  | ||||||
|     // Groups (end) -> actual drawing |  | ||||||
|     } else if elmt.type == "grp-end" { |  | ||||||
|       y -= Y-SPACE |  | ||||||
|       let (start-y, group, start-lvl, end-lvl) = groups.pop() |  | ||||||
|       let x0 = x-pos.at(group.min-i) - start-lvl * 10 - 20 |  | ||||||
|       let x1 = x-pos.at(group.max-i) + end-lvl * 10 + 20 |  | ||||||
|       shapes += draw-group(x0, x1, start-y, y, group) |  | ||||||
|  |  | ||||||
|       if group.grp-type == "alt" { |  | ||||||
|         for (else-y, else-elmt) in group.elses { |  | ||||||
|           shapes += draw-else(x0, x1, else-y, else-elmt) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|     // Alt's elses -> reserve space for label + store position |  | ||||||
|     } else if elmt.type == "else" { |  | ||||||
|       y -= Y-SPACE |  | ||||||
|       let m = measure(text([\[#elmt.desc\]], weight: "bold", size: .8em)) |  | ||||||
|       groups.last().at(1).elses.push(( |  | ||||||
|         y, elmt |  | ||||||
|       )) |  | ||||||
|       y -= m.height / 1pt |  | ||||||
|  |  | ||||||
|     // Separator |  | ||||||
|     } else if elmt.type == "sep" { |  | ||||||
|       let shps |  | ||||||
|       (y, shps) = draw-sep(elmt, y) |  | ||||||
|       shapes += shps |  | ||||||
|      |  | ||||||
|     // Gap |  | ||||||
|     } else if elmt.type == "gap" { |  | ||||||
|       y -= elmt.size |  | ||||||
|  |  | ||||||
|     // Delay |  | ||||||
|     } else if elmt.type == "delay" { |  | ||||||
|       let y0 = y |  | ||||||
|       let y1 = y - elmt.size |  | ||||||
|       for (i, line) in lifelines.enumerate() { |  | ||||||
|         line.lines.push(("delay-start", y0)) |  | ||||||
|         line.lines.push(("delay-end", y1)) |  | ||||||
|         lifelines.at(i) = line |  | ||||||
|       } |  | ||||||
|       if elmt.name != none { |  | ||||||
|         let x0 = x-pos.first() |  | ||||||
|         let x1 = x-pos.last() |  | ||||||
|         shapes += draw.content( |  | ||||||
|           ((x0 + x1) / 2, (y0 + y1) / 2), |  | ||||||
|           anchor: "center", |  | ||||||
|           elmt.name |  | ||||||
|         ) |  | ||||||
|       } |  | ||||||
|       y = y1 |  | ||||||
|      |  | ||||||
|     // Event |  | ||||||
|     } else if elmt.type == "evt" { |  | ||||||
|       let par-name = elmt.participant |  | ||||||
|       let i = pars-i.at(par-name) |  | ||||||
|       let par = participants.at(i) |  | ||||||
|       let line = lifelines.at(i) |  | ||||||
|       if elmt.event == "disable" { |  | ||||||
|         line.level -= 1 |  | ||||||
|         line.lines.push(("disable", y)) |  | ||||||
|        |  | ||||||
|       } else if elmt.event == "destroy" { |  | ||||||
|         line.lines.push(("destroy", y)) |  | ||||||
|        |  | ||||||
|       } else if elmt.event == "enable" { |  | ||||||
|         line.level += 1 |  | ||||||
|         line.lines.push(("enable", y, elmt.lifeline-style)) |  | ||||||
|        |  | ||||||
|       } else if elmt.event == "create" { |  | ||||||
|         y -= CREATE-OFFSET |  | ||||||
|         shapes += participant.render(x-pos, par, y: y) |  | ||||||
|         line.lines.push(("create", y)) |  | ||||||
|       } |  | ||||||
|       lifelines.at(i) = line |  | ||||||
|      |  | ||||||
|     // Note |  | ||||||
|     } else if elmt.type == "note" { |  | ||||||
|       if not elmt.linked { |  | ||||||
|         if not elmt.aligned { |  | ||||||
|           y -= Y-SPACE |  | ||||||
|         } |  | ||||||
|         let shps |  | ||||||
|         (y, shps) = draw-note(elmt, y, lifelines) |  | ||||||
|         shapes += shps |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|     // Synched sequences |  | ||||||
|     } else if elmt.type == "sync" { |  | ||||||
|       let shps |  | ||||||
|       (y, lifelines, shps) = draw-sync(elmt, y, lifelines) |  | ||||||
|       shapes += shps |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   y -= Y-SPACE |  | ||||||
|  |  | ||||||
|   // Draw vertical lines + lifelines + end participants |  | ||||||
|   shapes += draw.on-layer(-1, { |  | ||||||
|     if DEBUG-INVISIBLE { |  | ||||||
|       for p in participants.filter(p => p.invisible) { |  | ||||||
|         let color = if p.name.starts-with("?") {green} else if p.name.ends-with("?") {red} else {blue} |  | ||||||
|         let x = x-pos.at(p.i) |  | ||||||
|         draw.line( |  | ||||||
|           (x, 0), |  | ||||||
|           (x, y), |  | ||||||
|           stroke: (paint: color, dash: "dotted") |  | ||||||
|         ) |  | ||||||
|         draw.content( |  | ||||||
|           (x, 0), |  | ||||||
|           p.display-name, |  | ||||||
|           anchor: "west", |  | ||||||
|           angle: 90deg |  | ||||||
|         ) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     for p in participants.filter(p => not p.invisible) { |  | ||||||
|       let x = x-pos.at(p.i) |  | ||||||
|  |  | ||||||
|       // Draw vertical line |  | ||||||
|       let last-y = 0 |  | ||||||
|  |  | ||||||
|       let rects = () |  | ||||||
|       let destructions = () |  | ||||||
|       let lines = () |  | ||||||
|  |  | ||||||
|       // Compute lifeline rectangles + destruction positions |  | ||||||
|       for line in lifelines.at(p.i).lines { |  | ||||||
|         let event = line.first() |  | ||||||
|         if event == "create" { |  | ||||||
|           last-y = line.at(1) |  | ||||||
|  |  | ||||||
|         } else if event == "enable" { |  | ||||||
|           if lines.len() == 0 { |  | ||||||
|             draw.line( |  | ||||||
|               (x, last-y), |  | ||||||
|               (x, line.at(1)), |  | ||||||
|               stroke: ( |  | ||||||
|                 dash: "dashed", |  | ||||||
|                 paint: gray.darken(40%), |  | ||||||
|                 thickness: .5pt |  | ||||||
|               ) |  | ||||||
|             ) |  | ||||||
|           } |  | ||||||
|           lines.push(line) |  | ||||||
|          |  | ||||||
|         } else if event == "disable" or event == "destroy" { |  | ||||||
|           let lvl = 0 |  | ||||||
|           if lines.len() != 0 { |  | ||||||
|             let l = lines.pop() |  | ||||||
|             lvl = lines.len() |  | ||||||
|             rects.push(( |  | ||||||
|               x + lvl * LIFELINE-W / 2, |  | ||||||
|               l.at(1), |  | ||||||
|               line.at(1), |  | ||||||
|               l.at(2) |  | ||||||
|             )) |  | ||||||
|             last-y = line.at(1) |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|           if event == "destroy" { |  | ||||||
|             destructions.push((x + lvl * LIFELINE-W / 2, line.at(1))) |  | ||||||
|           } |  | ||||||
|         } else if event == "delay-start" { |  | ||||||
|           draw.line( |  | ||||||
|             (x, last-y), |  | ||||||
|             (x, line.at(1)), |  | ||||||
|             stroke: ( |  | ||||||
|               dash: "dashed", |  | ||||||
|               paint: gray.darken(40%), |  | ||||||
|               thickness: .5pt |  | ||||||
|             ) |  | ||||||
|           ) |  | ||||||
|           last-y = line.at(1) |  | ||||||
|         } else if event == "delay-end" { |  | ||||||
|           draw.line( |  | ||||||
|             (x, last-y), |  | ||||||
|             (x, line.at(1)), |  | ||||||
|             stroke: ( |  | ||||||
|               dash: "loosely-dotted", |  | ||||||
|               paint: gray.darken(40%), |  | ||||||
|               thickness: .8pt |  | ||||||
|             ) |  | ||||||
|           ) |  | ||||||
|           last-y = line.at(1) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       draw.line( |  | ||||||
|         (x, last-y), |  | ||||||
|         (x, y), |  | ||||||
|         stroke: ( |  | ||||||
|           dash: "dashed", |  | ||||||
|           paint: gray.darken(40%), |  | ||||||
|           thickness: .5pt |  | ||||||
|         ) |  | ||||||
|       ) |  | ||||||
|  |  | ||||||
|       // Draw lifeline rectangles (reverse for bottom to top) |  | ||||||
|       for rect in rects.rev() { |  | ||||||
|         let (cx, y0, y1, style) = rect |  | ||||||
|         let style = get-style("lifeline", style) |  | ||||||
|         draw.rect( |  | ||||||
|           (cx - LIFELINE-W / 2, y0), |  | ||||||
|           (cx + LIFELINE-W / 2, y1), |  | ||||||
|           ..style |  | ||||||
|         ) |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Draw lifeline destructions |  | ||||||
|       for dest in destructions { |  | ||||||
|         let (cx, cy) = dest |  | ||||||
|         draw.line((cx - 8, cy - 8), (cx + 8, cy + 8), stroke: COL-DESTRUCTION + 2pt) |  | ||||||
|         draw.line((cx - 8, cy + 8), (cx + 8, cy - 8), stroke: COL-DESTRUCTION + 2pt) |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Draw participants (end) |  | ||||||
|       if p.show-bottom { |  | ||||||
|         draw-par(p, y: y, bottom: true) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|  |  | ||||||
|   shapes |  | ||||||
| }) |  | ||||||
| @@ -1,63 +0,0 @@ | |||||||
| #import "@preview/cetz:0.3.1": 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 |  | ||||||
| } |  | ||||||
							
								
								
									
										344
									
								
								src/sequence.typ
									
									
									
									
									
								
							
							
						
						| @@ -1,51 +1,4 @@ | |||||||
| #import "@preview/cetz:0.3.1": draw, vector | #import "core/draw/sequence.typ" | ||||||
| #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( | #let _seq( | ||||||
|   p1, |   p1, | ||||||
| @@ -68,6 +21,7 @@ | |||||||
| ) = { | ) = { | ||||||
|   return (( |   return (( | ||||||
|     type: "seq", |     type: "seq", | ||||||
|  |     draw: sequence.render, | ||||||
|     p1: p1, |     p1: p1, | ||||||
|     p2: p2, |     p2: p2, | ||||||
|     comment: comment, |     comment: comment, | ||||||
| @@ -84,7 +38,8 @@ | |||||||
|     disable-src: disable-src, |     disable-src: disable-src, | ||||||
|     destroy-src: destroy-src, |     destroy-src: destroy-src, | ||||||
|     lifeline-style: lifeline-style, |     lifeline-style: lifeline-style, | ||||||
|     slant: slant |     slant: slant, | ||||||
|  |     linked-notes: () | ||||||
|   ),) |   ),) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -94,294 +49,3 @@ | |||||||
|     comment: comment |     comment: comment | ||||||
|   ),) |   ),) | ||||||
| } | } | ||||||
|  |  | ||||||
| #let render(pars-i, x-pos, participants, elmt, y, lifelines) = { |  | ||||||
|   let shapes = () |  | ||||||
|  |  | ||||||
|   y -= Y-SPACE |  | ||||||
|  |  | ||||||
|   let i1 = pars-i.at(elmt.p1) |  | ||||||
|   let i2 = pars-i.at(elmt.p2) |  | ||||||
|   let width = calc.abs(x-pos.at(i1) - x-pos.at(i2)) |  | ||||||
|  |  | ||||||
|   let h = 0 |  | ||||||
|   let comment = if elmt.comment == none {none} else { |  | ||||||
|     let w = calc.min(width * 1pt, measure(elmt.comment).width) |  | ||||||
|     box( |  | ||||||
|       width: if i1 == i2 {auto} else {w}, |  | ||||||
|       elmt.comment |  | ||||||
|     ) |  | ||||||
|   } |  | ||||||
|   // Reserve space for comment |  | ||||||
|   if comment != none { |  | ||||||
|     h = calc.max(h, measure(comment).height / 1pt + 6) |  | ||||||
|   } |  | ||||||
|   if "linked-note" in elmt { |  | ||||||
|     h = calc.max(h, note.get-size(elmt.linked-note).height / 2) |  | ||||||
|   } |  | ||||||
|   y -= h |  | ||||||
|  |  | ||||||
|   let start-info = ( |  | ||||||
|     i: i1, |  | ||||||
|     x: x-pos.at(i1), |  | ||||||
|     y: y, |  | ||||||
|     ll-lvl: lifelines.at(i1).level * LIFELINE-W / 2 |  | ||||||
|   ) |  | ||||||
|   let end-info = ( |  | ||||||
|     i: i2, |  | ||||||
|     x: x-pos.at(i2), |  | ||||||
|     y: y, |  | ||||||
|     ll-lvl: lifelines.at(i2).level * LIFELINE-W / 2 |  | ||||||
|   ) |  | ||||||
|   let slant = if elmt.slant == auto { |  | ||||||
|     DEFAULT-SLANT |  | ||||||
|   } else if elmt.slant != none { |  | ||||||
|     elmt.slant |  | ||||||
|   } else { |  | ||||||
|     0 |  | ||||||
|   } |  | ||||||
|   end-info.y -= slant |  | ||||||
|   if elmt.p1 == elmt.p2 { |  | ||||||
|     end-info.y -= 10 |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if elmt.disable-src { |  | ||||||
|     let src-line = lifelines.at(i1) |  | ||||||
|     src-line.level -= 1 |  | ||||||
|     src-line.lines.push(("disable", start-info.y)) |  | ||||||
|     lifelines.at(i1) = src-line |  | ||||||
|   } |  | ||||||
|   if elmt.destroy-src { |  | ||||||
|     let src-line = lifelines.at(i1) |  | ||||||
|     src-line.lines.push(("destroy", start-info.y)) |  | ||||||
|     lifelines.at(i1) = src-line |  | ||||||
|   } |  | ||||||
|   if elmt.disable-dst { |  | ||||||
|     let dst-line = lifelines.at(i2) |  | ||||||
|     dst-line.level -= 1 |  | ||||||
|     dst-line.lines.push(("disable", end-info.y)) |  | ||||||
|     lifelines.at(i2) = dst-line |  | ||||||
|   } |  | ||||||
|   if elmt.destroy-dst { |  | ||||||
|     let dst-line = lifelines.at(i2) |  | ||||||
|     dst-line.lines.push(("destroy", end-info.y)) |  | ||||||
|     lifelines.at(i2) = dst-line |  | ||||||
|   } |  | ||||||
|   if elmt.enable-dst { |  | ||||||
|     let dst-line = lifelines.at(i2) |  | ||||||
|     dst-line.level += 1 |  | ||||||
|     lifelines.at(i2) = dst-line |  | ||||||
|   } |  | ||||||
|   if elmt.create-dst { |  | ||||||
|     let par = participants.at(i2) |  | ||||||
|     let m = measure(box(par.display-name)) |  | ||||||
|     let f = if i1 > i2 {-1} else {1} |  | ||||||
|     end-info.x -= (m.width + PAR-PAD.last() * 2) / 2pt * f |  | ||||||
|     shapes += participant.render(x-pos, par, y: end-info.y) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   end-info.ll-lvl = lifelines.at(i2).level * LIFELINE-W / 2 |  | ||||||
|  |  | ||||||
|   // Compute left/right position at start/end |  | ||||||
|   start-info.insert("lx", start-info.x) |  | ||||||
|   if start-info.ll-lvl != 0 { start-info.lx -= LIFELINE-W / 2 } |  | ||||||
|   end-info.insert("lx", end-info.x) |  | ||||||
|   if end-info.ll-lvl != 0 { end-info.lx -= LIFELINE-W / 2 } |  | ||||||
|  |  | ||||||
|   start-info.insert("rx", start-info.x + start-info.ll-lvl) |  | ||||||
|   end-info.insert("rx", end-info.x + end-info.ll-lvl) |  | ||||||
|  |  | ||||||
|   // Choose correct points to link |  | ||||||
|   let x1 = start-info.rx |  | ||||||
|   let x2 = end-info.lx |  | ||||||
|  |  | ||||||
|   if (start-info.i > end-info.i) { |  | ||||||
|     x1 = start-info.lx |  | ||||||
|     x2 = end-info.rx |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let style = ( |  | ||||||
|     mark: ( |  | ||||||
|       start: get-arrow-marks(elmt.start-tip, elmt.color), |  | ||||||
|       end: get-arrow-marks(elmt.end-tip, elmt.color), |  | ||||||
|       scale: 1.2 |  | ||||||
|     ), |  | ||||||
|     stroke: ( |  | ||||||
|       dash: if elmt.dashed {(2pt,2pt)} else {"solid"}, |  | ||||||
|       paint: elmt.color, |  | ||||||
|       thickness: .5pt |  | ||||||
|     ) |  | ||||||
|   ) |  | ||||||
|  |  | ||||||
|   let y0 = start-info.y |  | ||||||
|   if "linked-note" in elmt { |  | ||||||
|     let shps = note.render(pars-i, x-pos, elmt.linked-note, start-info.y, lifelines).last() |  | ||||||
|     shapes += shps |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let flip-mark = end-info.i <= start-info.i |  | ||||||
|   if elmt.flip { |  | ||||||
|     flip-mark = not flip-mark |  | ||||||
|   } |  | ||||||
|   if flip-mark { |  | ||||||
|     style.mark.end = reverse-arrow-mark(style.mark.end) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let pts |  | ||||||
|   let comment-pt |  | ||||||
|   let comment-anchor |  | ||||||
|   let comment-angle = 0deg |  | ||||||
|  |  | ||||||
|   if elmt.p1 == elmt.p2 { |  | ||||||
|     if elmt.flip { |  | ||||||
|       x1 = start-info.lx |  | ||||||
|     } else { |  | ||||||
|       x2 = end-info.rx |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let x-mid = if elmt.flip { |  | ||||||
|       calc.min(x1, x2) - 20 |  | ||||||
|     } else { |  | ||||||
|       calc.max(x1, x2) + 20 |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pts = ( |  | ||||||
|       (x1, start-info.y), |  | ||||||
|       (x-mid, start-info.y), |  | ||||||
|       (x-mid, end-info.y), |  | ||||||
|       (x2, end-info.y) |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     if comment != none { |  | ||||||
|       comment-anchor = ( |  | ||||||
|         start: if x-mid < x1 {"south-east"} else {"south-west"}, |  | ||||||
|         end: if x-mid < x1 {"south-west"} else {"south-east"}, |  | ||||||
|         left: "south-west", |  | ||||||
|         right: "south-east", |  | ||||||
|         center: "south", |  | ||||||
|       ).at(elmt.comment-align) |  | ||||||
|  |  | ||||||
|       comment-pt = ( |  | ||||||
|         start: pts.first(), |  | ||||||
|         end: pts.at(1), |  | ||||||
|         left: if x-mid < x1 {pts.at(1)} else {pts.first()}, |  | ||||||
|         right: if x-mid < x1 {pts.first()} else {pts.at(1)}, |  | ||||||
|         center: (pts.first(), 50%, pts.at(1)) |  | ||||||
|       ).at(elmt.comment-align) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|   } else { |  | ||||||
|     pts = ( |  | ||||||
|       (x1, start-info.y), |  | ||||||
|       (x2, end-info.y) |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     if comment != none { |  | ||||||
|       let start-pt = pts.first() |  | ||||||
|       let end-pt = pts.last() |  | ||||||
|       if elmt.start-tip != "" { |  | ||||||
|         start-pt = (pts.first(), COMMENT-PAD, pts.last()) |  | ||||||
|       } |  | ||||||
|       if elmt.end-tip != "" { |  | ||||||
|         end-pt = (pts.last(), COMMENT-PAD, pts.first()) |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       comment-pt = ( |  | ||||||
|         start: start-pt, |  | ||||||
|         end: end-pt, |  | ||||||
|         left: if x2 < x1 {end-pt} else {start-pt}, |  | ||||||
|         right: if x2 < x1 {start-pt} else {end-pt}, |  | ||||||
|         center: (start-pt, 50%, end-pt) |  | ||||||
|       ).at(elmt.comment-align) |  | ||||||
|  |  | ||||||
|       comment-anchor = ( |  | ||||||
|         start: if x2 < x1 {"south-east"} else {"south-west"}, |  | ||||||
|         end: if x2 < x1 {"south-west"} else {"south-east"}, |  | ||||||
|         left: "south-west", |  | ||||||
|         right: "south-east", |  | ||||||
|         center: "south", |  | ||||||
|       ).at(elmt.comment-align) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let (p1, p2) = pts |  | ||||||
|     if x2 < x1 { |  | ||||||
|       (p1, p2) = (p2, p1) |  | ||||||
|     } |  | ||||||
|     comment-angle = vector.angle2(p1, p2) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Start circle tip |  | ||||||
|   if is-circle-tip(elmt.start-tip) { |  | ||||||
|     shapes += draw.circle(pts.first(), radius: CIRCLE-TIP-RADIUS, stroke: none, fill: elmt.color, name: "_circle-start-tip") |  | ||||||
|     pts.at(0) = "_circle-start-tip" |  | ||||||
|    |  | ||||||
|   // Start cross tip |  | ||||||
|   } else if is-cross-tip(elmt.start-tip) { |  | ||||||
|     let size = CROSS-TIP-SIZE |  | ||||||
|     let cross-pt = (pts.first(), size * 2, pts.at(1)) |  | ||||||
|     shapes += draw.line( |  | ||||||
|       (rel: (-size, -size), to: cross-pt), |  | ||||||
|       (rel: (size, size), to: cross-pt), |  | ||||||
|       stroke: elmt.color + 1.5pt |  | ||||||
|     ) |  | ||||||
|     shapes += draw.line( |  | ||||||
|       (rel: (-size, size), to: cross-pt), |  | ||||||
|       (rel: (size, -size), to: cross-pt), |  | ||||||
|       stroke: elmt.color + 1.5pt |  | ||||||
|     ) |  | ||||||
|     pts.at(0) = cross-pt |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // End circle tip |  | ||||||
|   if is-circle-tip(elmt.end-tip) { |  | ||||||
|     shapes += draw.circle(pts.last(), radius: 3, stroke: none, fill: elmt.color, name: "_circle-end-tip") |  | ||||||
|     pts.at(pts.len() - 1) = "_circle-end-tip" |  | ||||||
|    |  | ||||||
|   // End cross tip |  | ||||||
|   } else if is-cross-tip(elmt.end-tip) { |  | ||||||
|     let size = CROSS-TIP-SIZE |  | ||||||
|     let cross-pt = (pts.last(), size * 2, pts.at(pts.len() - 2)) |  | ||||||
|     shapes += draw.line( |  | ||||||
|       (rel: (-size, -size), to: cross-pt), |  | ||||||
|       (rel: (size, size), to: cross-pt), |  | ||||||
|       stroke: elmt.color + 1.5pt |  | ||||||
|     ) |  | ||||||
|     shapes += draw.line( |  | ||||||
|       (rel: (-size, size), to: cross-pt), |  | ||||||
|       (rel: (size, -size), to: cross-pt), |  | ||||||
|       stroke: elmt.color + 1.5pt |  | ||||||
|     ) |  | ||||||
|     pts.at(pts.len() - 1) = cross-pt |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   shapes += draw.line(..pts, ..style) |  | ||||||
|  |  | ||||||
|   if comment != none { |  | ||||||
|     shapes += draw.content( |  | ||||||
|       comment-pt, |  | ||||||
|       comment, |  | ||||||
|       anchor: comment-anchor, |  | ||||||
|       angle: comment-angle, |  | ||||||
|       padding: 3pt |  | ||||||
|     ) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if elmt.create-dst { |  | ||||||
|     let dst-line = lifelines.at(i2) |  | ||||||
|     dst-line.lines.push(("create", end-info.y)) |  | ||||||
|     lifelines.at(i2) = dst-line |  | ||||||
|   } |  | ||||||
|   if elmt.enable-dst { |  | ||||||
|     let dst-line = lifelines.at(i2) |  | ||||||
|     dst-line.lines.push(("enable", end-info.y, elmt.lifeline-style)) |  | ||||||
|     lifelines.at(i2) = dst-line |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if "linked-note" in elmt { |  | ||||||
|     let m = note.get-size(elmt.linked-note) |  | ||||||
|     end-info.y = calc.min(end-info.y, y0 - m.height / 2) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let r = (end-info.y, lifelines, shapes) |  | ||||||
|   return r |  | ||||||
| } |  | ||||||
							
								
								
									
										27
									
								
								src/sync.typ
									
									
									
									
									
								
							
							
						
						| @@ -1,27 +0,0 @@ | |||||||
| #import "sequence.typ" |  | ||||||
|  |  | ||||||
| #let _sync(elmts) = { |  | ||||||
|   return (( |  | ||||||
|     type: "sync", |  | ||||||
|     elmts: elmts |  | ||||||
|   ),) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #let render(pars-i, x-pos, participants, elmt, y, lifelines) = { |  | ||||||
|   let draw-seq = sequence.render.with(pars-i, x-pos, participants) |  | ||||||
|  |  | ||||||
|   let shapes = () |  | ||||||
|  |  | ||||||
|   let end-y = y |  | ||||||
|  |  | ||||||
|   for e in elmt.elmts { |  | ||||||
|     let yi |  | ||||||
|     let shps |  | ||||||
|     (yi, lifelines, shps) = draw-seq(e, y, lifelines) |  | ||||||
|     shapes += shps |  | ||||||
|     end-y = calc.min(end-y, yi) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   let r = (end-y, lifelines, shapes) |  | ||||||
|   return r |  | ||||||
| } |  | ||||||
							
								
								
									
										4
									
								
								tests/empty/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/empty/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 105 B | 
							
								
								
									
										6
									
								
								tests/empty/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  | #diagram({}) | ||||||
|  |  | ||||||
|  | #diagram(()) | ||||||
							
								
								
									
										4
									
								
								tests/group/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/group/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 46 KiB | 
							
								
								
									
										
											BIN
										
									
								
								tests/group/ref/2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								tests/group/ref/3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.7 KiB | 
							
								
								
									
										51
									
								
								tests/group/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _seq("Alice", "Bob", comment: "Authentication Request") | ||||||
|  |  | ||||||
|  |   _alt( | ||||||
|  |     "successful case", { | ||||||
|  |       _seq("Bob", "Alice", comment: "Authentication Accepted") | ||||||
|  |     }, | ||||||
|  |     "some kind of failure", { | ||||||
|  |       _seq("Bob", "Alice", comment: "Authentication Failure") | ||||||
|  |  | ||||||
|  |       _grp("My own label", desc: "My own label2", { | ||||||
|  |         _seq("Alice", "Log", comment: "Log attack start") | ||||||
|  |         _loop("1000 times", { | ||||||
|  |           _seq("Alice", "Bob", comment: "DNS Attack") | ||||||
|  |         }) | ||||||
|  |         _seq("Alice", "Log", comment: "Log attack end") | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     "Another type of failure", { | ||||||
|  |       _seq("Bob", "Alice", comment: "Please repeat") | ||||||
|  |     } | ||||||
|  |   ) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | #pagebreak() | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("a", display-name: box(width: 1.5em, height: .5em), show-bottom: false) | ||||||
|  |   _par("b", display-name: box(width: 1.5em, height: .5em), show-bottom: false) | ||||||
|  |   _col("a", "b", width: 2cm) | ||||||
|  |   _loop("a<1", min: 1, { | ||||||
|  |     _seq("a", "b", end-tip: ">>") | ||||||
|  |     _seq("b", "a", end-tip: ">>") | ||||||
|  |   }) | ||||||
|  |   _seq("a", "b", end-tip: ">>") | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | #pagebreak() | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("A") | ||||||
|  |   _par("B") | ||||||
|  |   _col("A", "B", width: 3cm) | ||||||
|  |   _seq("A", "B", enable-dst: true) | ||||||
|  |   _alt([desc], { | ||||||
|  |     _ret() | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										4
									
								
								tests/note/color/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/note/color/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 33 KiB | 
							
								
								
									
										13
									
								
								tests/note/color/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("a", display-name: "Alice") | ||||||
|  |   _par("b", display-name: "Bob") | ||||||
|  |  | ||||||
|  |   _note("left", [This is displayed\ left of Alice.], pos: "a", color: rgb("#00FFFF")) | ||||||
|  |   _note("right", [This is displayed right of Alice.], pos: "a") | ||||||
|  |   _note("over", [This is displayed over Alice.], pos: "a") | ||||||
|  |   _note("over", [This is displayed\ over Bob and Alice.], pos: ("a", "b"), color: rgb("#FFAAAA")) | ||||||
|  |   _note("over", [This is yet another\ example of\ a long note.], pos: ("a", "b")) | ||||||
|  | }) | ||||||
							
								
								
									
										4
									
								
								tests/note/position/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/note/position/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 23 KiB | 
							
								
								
									
										
											BIN
										
									
								
								tests/note/position/ref/2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 13 KiB | 
							
								
								
									
										
											BIN
										
									
								
								tests/note/position/ref/3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 35 KiB | 
							
								
								
									
										49
									
								
								tests/note/position/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,49 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("a", display-name: "Alice") | ||||||
|  |   _par("b", display-name: "Bob") | ||||||
|  |  | ||||||
|  |   _seq("a", "b", comment: [hello]) | ||||||
|  |   _note("left", [this is a first note]) | ||||||
|  |  | ||||||
|  |   _seq("b", "a", comment: [ok]) | ||||||
|  |   _note("right", [this is another note]) | ||||||
|  |  | ||||||
|  |   _seq("b", "b", comment: [I am thinking]) | ||||||
|  |   _note("left", [a note\ can also be defined\ on several lines]) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | #pagebreak() | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("a", display-name: "Alice") | ||||||
|  |   _par("b", display-name: "Bob") | ||||||
|  |  | ||||||
|  |   _note("over", [initial state of Alice], pos: "a") | ||||||
|  |   _note("over", [initial state of Bob], pos: "b") | ||||||
|  |   _seq("b", "a", comment: [hello]) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | #pagebreak() | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("a", display-name: "Alice") | ||||||
|  |   _par("b", display-name: "Bob") | ||||||
|  |   _par("c", display-name: "Charlie") | ||||||
|  |   _par("d", display-name: "Donald") | ||||||
|  |   _par("e", display-name: "Eddie") | ||||||
|  |  | ||||||
|  |   _note("across", [This note float above all participants]) | ||||||
|  |  | ||||||
|  |   _note("over", [initial state of Alice], pos: "a") | ||||||
|  |   _note("over", [initial state of Bob the builder], pos: "b", aligned: true) | ||||||
|  |  | ||||||
|  |   _note("over", [Note 1], pos: "a") | ||||||
|  |   _note("over", [Note 2], pos: "b", aligned: true) | ||||||
|  |   _note("over", [Note 3], pos: "c", aligned: true) | ||||||
|  |  | ||||||
|  |   _seq("a", "d") | ||||||
|  |   _note("over", [this is an extremely long note], pos: ("d", "e")) | ||||||
|  | }) | ||||||
							
								
								
									
										4
									
								
								tests/note/shape/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/note/shape/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 25 KiB | 
							
								
								
									
										14
									
								
								tests/note/shape/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("caller") | ||||||
|  |   _par("server") | ||||||
|  |  | ||||||
|  |   _seq("caller", "server", comment: [conReq]) | ||||||
|  |   _note("over", [idle], pos: "caller", shape: "hex") | ||||||
|  |   _seq("server", "caller", comment: [conConf]) | ||||||
|  |   _note("over", ["r" as rectangle\ "h" as hexagon], pos: "server", shape: "rect") | ||||||
|  |   _note("over", [this is\ on several\ lines], pos: "server", shape: "rect") | ||||||
|  |   _note("over", [this is\ on several\ lines], pos: "caller", shape: "hex") | ||||||
|  | }) | ||||||
							
								
								
									
										4
									
								
								tests/note/styled-body/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/note/styled-body/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 50 KiB | 
							
								
								
									
										33
									
								
								tests/note/styled-body/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("a", display-name: [Alice]) | ||||||
|  |   _par("b", display-name: [The *Famous* Bob]) | ||||||
|  |  | ||||||
|  |   _seq("a", "b", comment: [hello #strike([there])]) | ||||||
|  |  | ||||||
|  |   _gap() | ||||||
|  |   _seq("b", "a", comment: [ok]) | ||||||
|  |   _note("left", [ | ||||||
|  |     This is *bold*\ | ||||||
|  |     This is _italics_\ | ||||||
|  |     This is `monospaced`\ | ||||||
|  |     This is #strike([stroked])\ | ||||||
|  |     This is #underline([underlined])\ | ||||||
|  |     This is #underline([waved])\ | ||||||
|  |   ]) | ||||||
|  |  | ||||||
|  |   _seq("a", "b", comment: [A _well formatted_ message]) | ||||||
|  |   _note("right", [ | ||||||
|  |     This is #box(text([displayed], size: 18pt), fill: rgb("#5F9EA0"))\ | ||||||
|  |     #underline([left of]) Alice. | ||||||
|  |   ], pos: "a") | ||||||
|  |   _note("left", [ | ||||||
|  |     #underline([This], stroke: red) is #text([displayed], fill: rgb("#118888"))\ | ||||||
|  |     *#text([left of], fill: rgb("#800080")) #strike([Alice], stroke: red) Bob.* | ||||||
|  |   ], pos: "b") | ||||||
|  |   _note("over", [ | ||||||
|  |     #underline([This is hosted], stroke: rgb("#FF33FF")) by #box(baseline: 50%, image("/gallery/gitea.png", width: 1cm, height: 1cm, fit: "contain")) | ||||||
|  |   ], pos: ("a", "b")) | ||||||
|  | }) | ||||||
							
								
								
									
										4
									
								
								tests/participant/shapes/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/participant/shapes/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 102 KiB | 
							
								
								
									
										21
									
								
								tests/participant/shapes/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #let TYPST = image("/gallery/typst.png", width: 1.5cm, height: 1.5cm, fit: "contain") | ||||||
|  | #let FERRIS = image("/gallery/ferris.png", width: 1.5cm, height: 1.5cm, fit: "contain") | ||||||
|  | #let ME = image("/gallery/me.jpg", width: 1.5cm, height: 1.5cm, fit: "contain") | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("Foo", display-name: "Participant", shape: "participant") | ||||||
|  |   _par("Foo1", display-name: "Actor", shape: "actor") | ||||||
|  |   _par("Foo2", display-name: "Boundary", shape: "boundary") | ||||||
|  |   _par("Foo3", display-name: "Control", shape: "control") | ||||||
|  |   _par("Foo4", display-name: "Entity", shape: "entity") | ||||||
|  |   _par("Foo5", display-name: "Database", shape: "database") | ||||||
|  |   _par("Foo6", display-name: "Collections", shape: "collections") | ||||||
|  |   _par("Foo7", display-name: "Queue", shape: "queue") | ||||||
|  |   _par("Foo8", display-name: "Typst", shape: "custom", custom-image: TYPST) | ||||||
|  |   _par("Foo9", display-name: "Ferris", shape: "custom", custom-image: FERRIS) | ||||||
|  |   _par("Foo10", display-name: "Baryhobal", shape: "custom", custom-image: ME) | ||||||
|  | }) | ||||||
							
								
								
									
										4
									
								
								tests/sequence/comment-align/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/sequence/comment-align/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 57 KiB | 
							
								
								
									
										
											BIN
										
									
								
								tests/sequence/comment-align/ref/2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 28 KiB | 
							
								
								
									
										57
									
								
								tests/sequence/comment-align/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,57 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("alice", display-name: "Alice") | ||||||
|  |   _par("bob", display-name: "Bob") | ||||||
|  |   _seq("alice", "bob", comment: "This is a very long comment") | ||||||
|  |  | ||||||
|  |   // Left to right | ||||||
|  |   _seq("alice", "bob", comment: "Start aligned", comment-align: "start") | ||||||
|  |   _seq("alice", "bob", comment: "End aligned", comment-align: "end") | ||||||
|  |   _seq("alice", "bob", comment: "Left aligned", comment-align: "left") | ||||||
|  |   _seq("alice", "bob", comment: "Right aligned", comment-align: "right") | ||||||
|  |   _seq("alice", "bob", comment: "Centered", comment-align: "center") | ||||||
|  |   _gap() | ||||||
|  |  | ||||||
|  |   // Right to left | ||||||
|  |   _seq("bob", "alice", comment: "Start aligned", comment-align: "start") | ||||||
|  |   _seq("bob", "alice", comment: "End aligned", comment-align: "end") | ||||||
|  |   _seq("bob", "alice", comment: "Left aligned", comment-align: "left") | ||||||
|  |   _seq("bob", "alice", comment: "Right aligned", comment-align: "right") | ||||||
|  |   _seq("bob", "alice", comment: "Centered", comment-align: "center") | ||||||
|  |   _gap() | ||||||
|  |  | ||||||
|  |   // Slant left to right | ||||||
|  |   _seq("alice", "bob", comment: "Start aligned", comment-align: "start", slant: 10) | ||||||
|  |   _seq("alice", "bob", comment: "End aligned", comment-align: "end", slant: 10) | ||||||
|  |   _seq("alice", "bob", comment: "Left aligned", comment-align: "left", slant: 10) | ||||||
|  |   _seq("alice", "bob", comment: "Right aligned", comment-align: "right", slant: 10) | ||||||
|  |   _seq("alice", "bob", comment: "Centered", comment-align: "center", slant: 10) | ||||||
|  |   _gap() | ||||||
|  |  | ||||||
|  |   // Slant right to left | ||||||
|  |   _seq("bob", "alice", comment: "Start aligned", comment-align: "start", slant: 10) | ||||||
|  |   _seq("bob", "alice", comment: "End aligned", comment-align: "end", slant: 10) | ||||||
|  |   _seq("bob", "alice", comment: "Left aligned", comment-align: "left", slant: 10) | ||||||
|  |   _seq("bob", "alice", comment: "Right aligned", comment-align: "right", slant: 10) | ||||||
|  |   _seq("bob", "alice", comment: "Centered", comment-align: "center", slant: 10) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | #pagebreak() | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("alice", display-name: "Alice") | ||||||
|  |  | ||||||
|  |   _seq("alice", "alice", comment: "Start aligned", comment-align: "start") | ||||||
|  |   _seq("alice", "alice", comment: "End aligned", comment-align: "end") | ||||||
|  |   _seq("alice", "alice", comment: "Left aligned", comment-align: "left") | ||||||
|  |   _seq("alice", "alice", comment: "Right aligned", comment-align: "right") | ||||||
|  |   _seq("alice", "alice", comment: "Centered", comment-align: "center") | ||||||
|  |  | ||||||
|  |   _seq("alice", "alice", comment: "Start aligned", comment-align: "start", flip: true) | ||||||
|  |   _seq("alice", "alice", comment: "End aligned", comment-align: "end", flip: true) | ||||||
|  |   _seq("alice", "alice", comment: "Left aligned", comment-align: "left", flip: true) | ||||||
|  |   _seq("alice", "alice", comment: "Right aligned", comment-align: "right", flip: true) | ||||||
|  |   _seq("alice", "alice", comment: "Centered", comment-align: "center", flip: true) | ||||||
|  | }) | ||||||
							
								
								
									
										4
									
								
								tests/sequence/special-par/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/sequence/special-par/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 29 KiB | 
							
								
								
									
										12
									
								
								tests/sequence/special-par/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _seq("?", "Alice", comment: [?->\ *short* to actor1]) | ||||||
|  |   _seq("[", "Alice", comment: [\[->\ *from start* to actor1]) | ||||||
|  |   _seq("[", "Bob", comment: [\[->\ *from start* to actor2]) | ||||||
|  |   _seq("?", "Bob", comment: [?->\ *short* to actor2]) | ||||||
|  |   _seq("Alice", "]", comment: [->\]\ from actor1 *to end*]) | ||||||
|  |   _seq("Alice", "?", comment: [->?\ *short* from actor1]) | ||||||
|  |   _seq("Alice", "Bob", comment: [->\ from actor1 to actor2]) | ||||||
|  | }) | ||||||
							
								
								
									
										4
									
								
								tests/sequence/tips/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | # generated by tytanic, do not edit | ||||||
|  |  | ||||||
|  | diff/** | ||||||
|  | out/** | ||||||
							
								
								
									
										
											BIN
										
									
								
								tests/sequence/tips/ref/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								tests/sequence/tips/ref/2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								tests/sequence/tips/ref/3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 26 KiB | 
							
								
								
									
										84
									
								
								tests/sequence/tips/test.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,84 @@ | |||||||
|  | #set page(width: auto, height: auto) | ||||||
|  | #import "/src/lib.typ": * | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("a", display-name: "Alice") | ||||||
|  |   _par("b", display-name: "Bob") | ||||||
|  |  | ||||||
|  |   _seq("a", "b", end-tip: ">", comment: `->`) | ||||||
|  |   _seq("a", "b", end-tip: ">>", comment: `->>`) | ||||||
|  |   _seq("a", "b", end-tip: "\\", comment: `-\`) | ||||||
|  |   _seq("a", "b", end-tip: "\\\\", comment: `-\\`) | ||||||
|  |   _seq("a", "b", end-tip: "/", comment: `-/`) | ||||||
|  |   _seq("a", "b", end-tip: "//", comment: `-//`) | ||||||
|  |   _seq("a", "b", end-tip: "x", comment: `->x`) | ||||||
|  |   _seq("a", "b", start-tip: "x", comment: `x->`) | ||||||
|  |   _seq("a", "b", start-tip: "o", comment: `o->`) | ||||||
|  |   _seq("a", "b", end-tip: ("o", ">"), comment: `->o`) | ||||||
|  |   _seq("a", "b", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`) | ||||||
|  |   _seq("a", "b", start-tip: ">", end-tip: ">", comment: `<->`) | ||||||
|  |   _seq("a", "b", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`) | ||||||
|  |   _seq("a", "b", start-tip: "x", end-tip: "x", comment: `x<->x`) | ||||||
|  |   _seq("a", "b", end-tip: ("o", ">>"), comment: `->>o`) | ||||||
|  |   _seq("a", "b", end-tip: ("o", "\\"), comment: `-\o`) | ||||||
|  |   _seq("a", "b", end-tip: ("o", "\\\\"), comment: `-\\o`) | ||||||
|  |   _seq("a", "b", end-tip: ("o", "/"), comment: `-/o`) | ||||||
|  |   _seq("a", "b", end-tip: ("o", "//"), comment: `-//o`) | ||||||
|  |   _seq("a", "b", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | #pagebreak() | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("a", display-name: "Alice") | ||||||
|  |   _par("b", display-name: "Bob") | ||||||
|  |  | ||||||
|  |   _seq("b", "a", end-tip: ">", comment: `->`) | ||||||
|  |   _seq("b", "a", end-tip: ">>", comment: `->>`) | ||||||
|  |   _seq("b", "a", end-tip: "\\", comment: `-\`) | ||||||
|  |   _seq("b", "a", end-tip: "\\\\", comment: `-\\`) | ||||||
|  |   _seq("b", "a", end-tip: "/", comment: `-/`) | ||||||
|  |   _seq("b", "a", end-tip: "//", comment: `-//`) | ||||||
|  |   _seq("b", "a", end-tip: "x", comment: `->x`) | ||||||
|  |   _seq("b", "a", start-tip: "x", comment: `x->`) | ||||||
|  |   _seq("b", "a", start-tip: "o", comment: `o->`) | ||||||
|  |   _seq("b", "a", end-tip: ("o", ">"), comment: `->o`) | ||||||
|  |   _seq("b", "a", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`) | ||||||
|  |   _seq("b", "a", start-tip: ">", end-tip: ">", comment: `<->`) | ||||||
|  |   _seq("b", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`) | ||||||
|  |   _seq("b", "a", start-tip: "x", end-tip: "x", comment: `x<->x`) | ||||||
|  |   _seq("b", "a", end-tip: ("o", ">>"), comment: `->>o`) | ||||||
|  |   _seq("b", "a", end-tip: ("o", "\\"), comment: `-\o`) | ||||||
|  |   _seq("b", "a", end-tip: ("o", "\\\\"), comment: `-\\o`) | ||||||
|  |   _seq("b", "a", end-tip: ("o", "/"), comment: `-/o`) | ||||||
|  |   _seq("b", "a", end-tip: ("o", "//"), comment: `-//o`) | ||||||
|  |   _seq("b", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | #pagebreak() | ||||||
|  |  | ||||||
|  | #diagram({ | ||||||
|  |   _par("a", display-name: "Alice") | ||||||
|  |   _par("b", display-name: "Bob") | ||||||
|  |  | ||||||
|  |   _seq("a", "a", end-tip: ">", comment: `->`) | ||||||
|  |   _seq("a", "a", end-tip: ">>", comment: `->>`) | ||||||
|  |   _seq("a", "a", end-tip: "\\", comment: `-\`) | ||||||
|  |   _seq("a", "a", end-tip: "\\\\", comment: `-\\`) | ||||||
|  |   _seq("a", "a", end-tip: "/", comment: `-/`) | ||||||
|  |   _seq("a", "a", end-tip: "//", comment: `-//`) | ||||||
|  |   _seq("a", "a", end-tip: "x", comment: `->x`) | ||||||
|  |   _seq("a", "a", start-tip: "x", comment: `x->`) | ||||||
|  |   _seq("a", "a", start-tip: "o", comment: `o->`) | ||||||
|  |   _seq("a", "a", end-tip: ("o", ">"), comment: `->o`) | ||||||
|  |   _seq("a", "a", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`) | ||||||
|  |   _seq("a", "a", start-tip: ">", end-tip: ">", comment: `<->`) | ||||||
|  |   _seq("a", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`) | ||||||
|  |   _seq("a", "a", start-tip: "x", end-tip: "x", comment: `x<->x`) | ||||||
|  |   _seq("a", "a", end-tip: ("o", ">>"), comment: `->>o`) | ||||||
|  |   _seq("a", "a", end-tip: ("o", "\\"), comment: `-\o`) | ||||||
|  |   _seq("a", "a", end-tip: ("o", "\\\\"), comment: `-\\o`) | ||||||
|  |   _seq("a", "a", end-tip: ("o", "/"), comment: `-/o`) | ||||||
|  |   _seq("a", "a", end-tip: ("o", "//"), comment: `-//o`) | ||||||
|  |   _seq("a", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`) | ||||||
|  | }) | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| [package] | [package] | ||||||
| name = "chronos" | name = "chronos" | ||||||
| version = "0.2.0" | version = "0.2.2" | ||||||
| compiler = "0.12.0" | compiler = "0.13.1" | ||||||
| repository = "https://git.kb28.ch/HEL/chronos" | repository = "https://git.kb28.ch/HEL/chronos" | ||||||
| entrypoint = "src/lib.typ" | entrypoint = "src/lib.typ" | ||||||
| authors = [ | authors = [ | ||||||
| @@ -11,4 +11,4 @@ categories = ["visualization"] | |||||||
| license = "Apache-2.0" | license = "Apache-2.0" | ||||||
| description = "A package to draw sequence diagrams with CeTZ" | description = "A package to draw sequence diagrams with CeTZ" | ||||||
| keywords = ["sequence", "diagram", "plantuml"] | keywords = ["sequence", "diagram", "plantuml"] | ||||||
| exclude = [ "gallery", "gallery.bash", "docs" ] | exclude = [ "gallery", "justfile", "docs" ] | ||||||
|   | |||||||