forked from HEL/chronos
		
	Compare commits
	
		
			21 Commits
		
	
	
		
			v0.1.0
			...
			3beaad03d4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3beaad03d4 | |||
| 5cb8f50f2a | |||
| 19c60c5ecf | |||
| df3a2ddf68 | |||
| 42e8e26aa7 | |||
| b33531bef5 | |||
| ab1386e721 | |||
| 1f155063a8 | |||
| d986284e4e | |||
| a037727edf | |||
| b0950f5e68 | |||
| eb09a23fc1 | |||
| c4f09a0a3e | |||
| 9275169e8c | |||
| a198708743 | |||
| b66634d44f | |||
| 3bc103b9d7 | |||
| feff030510 | |||
| 56cc1b11c0 | |||
| 647d50e125 | |||
| 522cd1537a | 
| @@ -15,7 +15,7 @@ This package lets you render sequence diagrams directly in Typst. The following | ||||
| <td> | ||||
|  | ||||
| ```typst | ||||
| #import "@preview/chronos:0.1.0" | ||||
| #import "@preview/chronos:0.2.0" | ||||
| #chronos.diagram({ | ||||
|   import chronos: * | ||||
|   _par("Alice") | ||||
|   | ||||
							
								
								
									
										3
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								TODO.md
									
									
									
									
									
								
							| @@ -22,6 +22,7 @@ | ||||
| - [ ] Add args verification to catch user errors + pretty error messages | ||||
| - [ ] PlantUML parser | ||||
| - [ ] (Message numbering) | ||||
| - [ ] Different types of groups (alt/loop/etc.) | ||||
| - [ ] Mainframes | ||||
| - [x] Different types of groups (alt/loop/etc.) | ||||
| - [ ] Delays | ||||
| - [ ] Auto-fit in parent | ||||
							
								
								
									
										39
									
								
								docs/example.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								docs/example.typ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| #import "../src/lib.typ" as chronos | ||||
|  | ||||
| #let example-preamble = "import \"../src/lib.typ\": *;" | ||||
| #let example-scope = ( | ||||
|   chronos: chronos | ||||
| ) | ||||
|  | ||||
| #let example(src, show-src: true, vertical: false, fill: true, wrap: true) = { | ||||
|   src = src.text | ||||
|   let full-src = example-preamble + src | ||||
|   let body = eval(full-src, scope: example-scope) | ||||
|   let img = if wrap { chronos.diagram(body) } else { body } | ||||
|  | ||||
|   block(width: 100%, | ||||
|     align(center, | ||||
|       box( | ||||
|         stroke: black + 1pt, | ||||
|         radius: .5em, | ||||
|         fill: if fill {color.white.darken(5%)} else {none}, | ||||
|         if show-src { | ||||
|           let src-block = align(left, raw(src, lang: "typc")) | ||||
|           table( | ||||
|             columns: if vertical {1} else {2}, | ||||
|             inset: 1em, | ||||
|             align: horizon + center, | ||||
|             stroke: none, | ||||
|             img, | ||||
|             if vertical {table.hline()} else {table.vline()}, src-block | ||||
|           ) | ||||
|         } else { | ||||
|           table( | ||||
|             inset: 1em, | ||||
|             img | ||||
|           ) | ||||
|         } | ||||
|       ) | ||||
|     ) | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										155
									
								
								docs/examples.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								docs/examples.typ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| #import "example.typ": example | ||||
|  | ||||
| #let seq-comm-align = example(``` | ||||
| _par("p1", display-name: "Start participant") | ||||
| _par("p2", display-name: "End participant") | ||||
| let alignments = ( | ||||
|   "start", "end", | ||||
|   "left", "right", | ||||
|   "center" | ||||
| ) | ||||
| for a in alignments { | ||||
|   _seq( | ||||
|     "p2", "p1", | ||||
|     comment: raw(a), | ||||
|     comment-align: a | ||||
|   ) | ||||
| } | ||||
| ```) | ||||
|  | ||||
| #let seq-tips = example(``` | ||||
| let _seq = _seq.with(comment-align: "center") | ||||
| _par("a", display-name: "Alice") | ||||
| _par("b", display-name: "Bob") | ||||
|  | ||||
| _seq("a", "b", comment: "Various tips", end-tip: "") | ||||
| _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`) | ||||
| ```) | ||||
|  | ||||
| #let grp = example(``` | ||||
| _par("a", display-name: "Alice") | ||||
| _par("b", display-name: "Bob") | ||||
|  | ||||
| _grp("Group 1", desc: "Description", { | ||||
|   _seq("a", "b", comment: "Authentication") | ||||
|   _grp("loop", desc: "1000 times", { | ||||
|     _seq("a", "b", comment: "DoS Attack") | ||||
|   }) | ||||
|   _seq("a", "b", end-tip: "x") | ||||
| }) | ||||
| ```) | ||||
|  | ||||
| #let alt = example(``` | ||||
| _par("a", display-name: "Alice") | ||||
| _par("b", display-name: "Bob") | ||||
|  | ||||
| _alt( | ||||
|   "first encounter", { | ||||
|     _seq("a", "b", comment: "Who are you ?") | ||||
|     _seq("b", "a", comment: "I'm Bob") | ||||
|   }, | ||||
|  | ||||
|   "know eachother", { | ||||
|     _seq("a", "b", comment: "Hello Bob") | ||||
|     _seq("b", "a", comment: "Hello Alice") | ||||
|   }, | ||||
|  | ||||
|   "best friends", { | ||||
|     _seq("a", "b", comment: "Hi !") | ||||
|     _seq("b", "a", comment: "Hi !") | ||||
|   } | ||||
| ) | ||||
| ```) | ||||
|  | ||||
| #let loop = example(``` | ||||
| _par("a", display-name: "Alice") | ||||
| _par("b", display-name: "Bob") | ||||
|  | ||||
| _loop("default loop", { | ||||
|   _seq("a", "b", comment: "Are you here ?") | ||||
| }) | ||||
| _gap() | ||||
| _loop("min loop", min: 1, { | ||||
|   _seq("a", "b", comment: "Are you here ?") | ||||
| }) | ||||
| _gap() | ||||
| _loop("min-max loop", min: 1, max: 5, { | ||||
|   _seq("a", "b", comment: "Are you still here ?") | ||||
| }) | ||||
| ```) | ||||
|  | ||||
| #let sync = example(``` | ||||
| _par("alice", display-name: "Alice") | ||||
| _par("bob", display-name: "Bob") | ||||
| _par("craig", display-name: "Craig") | ||||
|  | ||||
| _seq("bob", "alice")  // Unsynchronized | ||||
| _seq("bob", "craig")  //  " | ||||
| _sync({ | ||||
|   _seq("bob", "alice")  // Synchronized | ||||
|   _seq("bob", "craig")  //  " | ||||
| }) | ||||
| _seq("alice", "bob")  // Unsynchronized | ||||
| _seq("craig", "bob")  //  " | ||||
| _sync({ | ||||
|   _seq("alice", "bob")  // Synchronized | ||||
|   _seq("craig", "bob")  //  " | ||||
| }) | ||||
| ```) | ||||
|  | ||||
| #let gaps-seps = example(``` | ||||
| _par("alice", display-name: "Alice") | ||||
| _par("bob", display-name: "Bob") | ||||
|  | ||||
| _seq("alice", "bob", comment: "Hello") | ||||
| _gap(size: 10) | ||||
| _seq("bob", "alice", comment: "Hi") | ||||
| _sep("Another day") | ||||
| _seq("alice", "bob", comment: "Hello again") | ||||
| ```) | ||||
|  | ||||
| #let notes-shapes = example(``` | ||||
| _par("alice", display-name: "Alice") | ||||
| _par("bob", display-name: "Bob") | ||||
| _note("over", `default`, pos: "alice") | ||||
| _note("over", `rect`, pos: "bob", shape: "rect") | ||||
| _note("over", `hex`, pos: ("alice", "bob"), shape: "hex") | ||||
| ```) | ||||
|  | ||||
| #let notes-sides = example(``` | ||||
| _par("alice", display-name: "Alice") | ||||
| _par("bob", display-name: "Bob") | ||||
| _par("charlie", display-name: "Charlie") | ||||
| _note("left", [`left` of Alice], pos: "alice") | ||||
| _note("right", [`right` of Charlie], pos: "charlie") | ||||
| _note("over", [`over` Alice and Bob], pos: ("alice", "bob")) | ||||
| _note("across", [`across` all participants]) | ||||
| _seq("alice", "bob") | ||||
| _note("left", [linked with sequence]) | ||||
| _note("over", [A note], pos: "alice") | ||||
| _note("over", [Aligned note], pos: "charlie", aligned: true) | ||||
| ```, vertical: true) | ||||
							
								
								
									
										8
									
								
								docs/gaps_seps.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								docs/gaps_seps.typ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| /// Creates a gap before the next element | ||||
| /// - size (int): Size of the gap | ||||
| #let _gap(size: 20) = {} | ||||
|  | ||||
| /// Creates a separator before the next element | ||||
| /// #examples.gaps-seps | ||||
| /// - name (content): Name to display in the middle of the separator | ||||
| #let _sep(name) = {} | ||||
							
								
								
									
										58
									
								
								docs/groups.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								docs/groups.typ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| /// Creates a group of sequences | ||||
| /// #examples.grp | ||||
| /// - name (content): The group's name | ||||
| /// - desc (none, content): Optional description | ||||
| /// - type (str): The groups's type (should only be set through other functions like @@_alt() or @@_loop() ) | ||||
| /// - elmts (array): Elements inside the group (can be sequences, other groups, notes, etc.) | ||||
| #let _grp( | ||||
|   name, | ||||
|   desc: none, | ||||
|   type: "default", | ||||
|   elmts | ||||
| ) = {} | ||||
|  | ||||
| /// Creates an alt-else group of sequences | ||||
| ///  | ||||
| /// It contains at least one section but can have as many as needed | ||||
| /// #examples.alt | ||||
| /// - desc (content): The alt's label | ||||
| /// - elmts (array): Elements inside the alt's first section | ||||
| /// - ..args (content, array): Complementary "else" sections.\ You can add as many else sections as you need by passing a content (else section label) followed by an array of elements (see example) | ||||
| #let _alt( | ||||
|   desc, | ||||
|   elmts, | ||||
|   ..args | ||||
| ) | ||||
|  | ||||
| /// Creates a looped group of sequences | ||||
| /// #examples.loop | ||||
| /// - desc (content): Loop description | ||||
| /// - min (none, number): Optional lower bound of the loop | ||||
| /// - max (auto, number): Upper bound of the loop. If left as `auto` and `min` is set, it will be infinity (`'*'`) | ||||
| /// - elmts (array): Elements inside the group | ||||
| #let _loop( | ||||
|   desc, | ||||
|   min: none, | ||||
|   max: auto, | ||||
|   elmts | ||||
| ) = {} | ||||
|  | ||||
| /// Synchronizes multiple sequences\ | ||||
| /// All elements inside a synchronized group will start at the same time | ||||
| /// #examples.sync | ||||
| /// - elmts (array): Synchronized elements (generally sequences or notes) | ||||
| #let _sync( | ||||
|   elmts | ||||
| ) | ||||
|  | ||||
| /// Creates an optional group\ | ||||
| /// This is a simple wrapper around @@_grp() | ||||
| /// - desc (content): Group description | ||||
| /// - elmts (array): Elements inside the group | ||||
| #let _opt(desc, elmts) = {} | ||||
|  | ||||
| /// Creates a break group\ | ||||
| /// This is a simple wrapper around @@_grp() | ||||
| /// - desc (content): Group description | ||||
| /// - elmts (array): Elements inside the group | ||||
| #let _break(desc, elmts) = {} | ||||
							
								
								
									
										23
									
								
								docs/notes.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								docs/notes.typ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| /// Creates a note | ||||
| /// - side (str): The side on which to place the note (see @@SIDES for accepted values) | ||||
| /// - content (content): The note's content | ||||
| /// - pos (none, str, array): Optional participant(s) on which to draw next to / over. If `side` is "left" or "right", sets next to which participant the note is placed. If `side` is "over", sets over which participant(s) it is placed | ||||
| /// - color (color): The note's color | ||||
| /// - 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 | ||||
| #let _note( | ||||
|   side, | ||||
|   content, | ||||
|   pos: none, | ||||
|   color: rgb("#FEFFDD"), | ||||
|   shape: "default", | ||||
|   aligned: false | ||||
| ) = {} | ||||
|  | ||||
| /// Accepted values for `shape` argument of @@_note() | ||||
| /// #examples.notes-shapes | ||||
| #let SHAPES = ("default", "rect", "hex") | ||||
|  | ||||
| /// Accepted values for `side` argument of @@_note() | ||||
| /// #examples.notes-sides | ||||
| #let SIDES = ("left", "right", "over", "across") | ||||
							
								
								
									
										69
									
								
								docs/participants.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								docs/participants.typ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| /// Possible participant shapes | ||||
| /// #box(width: 100%, align(center)[ | ||||
| ///   #chronos.diagram({ | ||||
| ///     import chronos: * | ||||
| ///     let _par = _par.with(show-bottom: false) | ||||
| ///     _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: "custom", shape: "custom", custom-image: TYPST) | ||||
| ///     _gap() | ||||
| ///   }) | ||||
| /// ]) | ||||
| #let SHAPES = ( | ||||
|   "participant", | ||||
|   "actor", | ||||
|   "boundary", | ||||
|   "control", | ||||
|   "entity", | ||||
|   "database", | ||||
|   "collections", | ||||
|   "queue", | ||||
|   "custom" | ||||
| ) | ||||
|  | ||||
| /// Creates a new participant | ||||
| /// - name (str): Unique participant name used as reference in other functions | ||||
| /// - display-name (auto, content): Name to display in the diagram. If set to `auto`, `name` is used | ||||
| /// - from-start (bool): If set to true, the participant is created at the top of the diagram. Otherwise, it is created at the first reference | ||||
| /// - invisible (bool): If set to true, the participant will not be shown | ||||
| /// - shape (str): The shape of the participant. Possible values in @@SHAPES | ||||
| /// - color (color): The participant's color | ||||
| /// - custom-image (none, image): If shape is 'custom', sets the custom image to display | ||||
| /// - show-bottom (bool): Whether to display the bottom shape | ||||
| /// - show-top (bool): Whether to display the top shape | ||||
| /// -> array | ||||
| #let _par( | ||||
|   name, | ||||
|   display-name: auto, | ||||
|   from-start: true, | ||||
|   invisible: false, | ||||
|   shape: "participant", | ||||
|   color: rgb("#E2E2F0"), | ||||
|   custom-image: none, | ||||
|   show-bottom: true, | ||||
|   show-top: true, | ||||
| ) = {} | ||||
|  | ||||
| /// Sets some options for columns between participants | ||||
| /// | ||||
| /// Parameters `p1` and `p2` MUST be consecutive participants (also counting found/lost messages), but they do not need to be in the left to right order | ||||
| /// - p1 (str): The first neighbouring participant | ||||
| /// - p2 (str): The second neighbouring participant | ||||
| /// - width (auto, int, float, length): Optional fixed width of the column\ If the column's content (e.g. sequence comments) is larger, it will overflow | ||||
| /// - margin (int, float, length): Additional margin to add to the column\ This margin is not included in `width` and `min-width`, but rather added separately | ||||
| /// - min-width (int, float, length): Minimum width of the column\ If set to a larger value than `width`, the latter will be overriden | ||||
| /// - max-width (int, float, length, none): Maximum width of the column\ If set to a lower value than `width`, the latter will be overriden\ If set to `none`, no restriction is applied | ||||
| #let _col( | ||||
|   p1, | ||||
|   p2, | ||||
|   width: auto, | ||||
|   margin: 0, | ||||
|   min-width: 0, | ||||
|   max-width: none | ||||
| ) = {} | ||||
							
								
								
									
										60
									
								
								docs/sequences.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								docs/sequences.typ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| /// Manually adds an event to the given participant | ||||
| /// - participant (str): The participant concerned by the event | ||||
| /// - event (str): The event type (see @@EVENTS for ccepted values) | ||||
| #let _evt(participant, event) = {} | ||||
|  | ||||
| /// Creates a sequence / message between two participants | ||||
| /// - p1 (str): Start participant | ||||
| /// - p2 (str): End participant | ||||
| /// - comment (none, content): Optional comment to display along the arrow | ||||
| /// - comment-align (str): Where to align the comment with respect to the arrow (see @@comment-align for accepted values) | ||||
| /// - dashed (bool): Whether the arrow's stroke is dashed or not | ||||
| /// - start-tip (str): Start arrow tip (see @@tips for accepted values) | ||||
| /// - end-tip (str): End arrow tip (see @@tips for accepted values) | ||||
| /// - color (color): Arrow's color | ||||
| /// - flip (bool): If true, the arrow is flipped (goes from end to start). This is particularly useful for self calls, to change the side on which the arrow appears | ||||
| /// - enable-dst (bool): If true, enables the destination lifeline | ||||
| /// - create-dst (bool): If true, creates the destination lifeline and participant | ||||
| /// - disable-dst (bool): If true, disables the destination lifeline | ||||
| /// - destroy-dst (bool): If true, destroys the destination lifeline and participant | ||||
| /// - disable-src (bool): If true, disables the source lifeline | ||||
| /// - destroy-src (bool): If true, destroy the source lifeline and participant | ||||
| /// - lifeline-style (auto, dict): Optional styling options for lifeline rectangles (see CeTZ documentation for more information on all possible values) | ||||
| /// - slant (none, int): Optional slant of the arrow | ||||
| /// -> array | ||||
| #let _seq( | ||||
|   p1, | ||||
|   p2, | ||||
|   comment: none, | ||||
|   comment-align: "left", | ||||
|   dashed: false, | ||||
|   start-tip: "", | ||||
|   end-tip: ">", | ||||
|   color: black, | ||||
|   flip: false, | ||||
|   enable-dst: false, | ||||
|   create-dst: false, | ||||
|   disable-dst: false, | ||||
|   destroy-dst: false, | ||||
|   disable-src: false, | ||||
|   destroy-src: false, | ||||
|   lifeline-style: auto, | ||||
|   slant: none | ||||
| ) = {} | ||||
|  | ||||
| /// Accepted values for `event` argument of @@_evt() | ||||
| ///  | ||||
| /// `EVENTS = ("create", "destroy", "enable", "disable")` | ||||
| #let EVENTS = ("create", "destroy", "enable", "disable") | ||||
|  | ||||
| /// Accepted values for `start-tip` and `end-tip` arguments of @@_seq() | ||||
| /// #examples.seq-tips | ||||
| #let tips = ( | ||||
|   "", ">", ">>", "\\", "\\\\", "/", "//", "x", "o", | ||||
| ) | ||||
|  | ||||
| /// Accepted values for `comment-align` argument of @@_seq() | ||||
| /// #examples.seq-comm-align | ||||
| #let comment-align = ( | ||||
|   "start", "end", "left", "center", "right" | ||||
| ) | ||||
										
											Binary file not shown.
										
									
								
							| @@ -33,15 +33,38 @@ Alice <-- Bob: Another authentication Response | ||||
| #chronos.diagram({ | ||||
|   import chronos: * | ||||
|   _seq("Alice", "Bob", comment: "Authentication Request") | ||||
|   _seq("Bob", "Alice", comment: "Authentication Failure") | ||||
|  | ||||
|   _grp("My own label", desc: "My own label2", { | ||||
|     _seq("Alice", "Log", comment: "Log attack start") | ||||
|     _grp("loop", desc: "1000 times", { | ||||
|       _seq("Alice", "Bob", comment: "DNS Attack") | ||||
|     }) | ||||
|     _seq("Alice", "Bob", comment: "Log attack end") | ||||
|   _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") | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| #chronos.diagram({ | ||||
|   import chronos: * | ||||
|   _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: ">>") | ||||
| }) | ||||
|  | ||||
| #chronos.diagram({ | ||||
| @@ -55,6 +78,15 @@ Alice <-- Bob: Another authentication Response | ||||
|   _seq("Bob", "Alice", comment: "another authentication Response", dashed: true) | ||||
| }) | ||||
|  | ||||
| #chronos.diagram({ | ||||
|   import chronos: * | ||||
|   _seq("Alice", "Bob", comment: "Authentication Request") | ||||
|   _delay() | ||||
|   _seq("Bob", "Alice", comment: "Authentication Response") | ||||
|   _delay(name: "5 minutes later") | ||||
|   _seq("Bob", "Alice", comment: "Good Bye !") | ||||
| }) | ||||
|  | ||||
| #chronos.diagram({ | ||||
|   import chronos: * | ||||
|   _seq("Alice", "Bob", comment: "message 1") | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -145,3 +145,22 @@ chronos.diagram({ | ||||
|  | ||||
|   _gap() | ||||
| }) | ||||
|  | ||||
| #chronos.diagram({ | ||||
|   import chronos: * | ||||
|  | ||||
|   _par("a", display-name: "Alice") | ||||
|   _par("b", display-name: "Bob") | ||||
|   _par("c", display-name: "Caleb") | ||||
|   _par("d", display-name: "Danny") | ||||
|   _par("e", display-name: "Erika") | ||||
|  | ||||
|   _col("a", "b") | ||||
|   _col("b", "c", width: 2cm) | ||||
|   _col("c", "d", margin: .5cm) | ||||
|   _col("d", "e", min-width: 2cm) | ||||
|  | ||||
|   //_seq("b", "c", comment: [Hello World !]) | ||||
|   //_seq("c", "d", comment: [Hello World]) | ||||
|   //_seq("d", "e", comment: [Hello World]) | ||||
| }) | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 88 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										
											BIN
										
									
								
								manual.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								manual.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										214
									
								
								manual.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								manual.typ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,214 @@ | ||||
| #import "@preview/tidy:0.3.0" | ||||
| #import "src/lib.typ" as chronos | ||||
| #import "src/participant.typ" as mod-par | ||||
| #import "docs/examples.typ" | ||||
| #import "docs/example.typ": example | ||||
|  | ||||
| #let TYPST = image("gallery/typst.png", width: 1.5cm, height: 1.5cm, fit: "contain") | ||||
|  | ||||
| #let doc-ref(target, full: false, var: false) = { | ||||
|   let (module, func) = target.split(".") | ||||
|   let label-name = module + func | ||||
|   let display-name = func | ||||
|   if full { | ||||
|     display-name = target | ||||
|   } | ||||
|   if not var { | ||||
|     label-name += "()" | ||||
|     display-name += "()" | ||||
|   } | ||||
|   link(label(label-name))[#display-name] | ||||
| } | ||||
|  | ||||
| #set heading(numbering: (..num) => if num.pos().len() < 4 { | ||||
|   numbering("1.1", ..num) | ||||
| }) | ||||
| #{ | ||||
|   outline(indent: true, depth: 3) | ||||
| } | ||||
| #show link: set text(fill: blue) | ||||
|  | ||||
| #set page(numbering: "1/1", header: align(right)[chronos #sym.dash.em v#chronos.version]) | ||||
| #set page( | ||||
|   header: align(left)[chronos #sym.dash.em v#chronos.version], | ||||
|   footer: context align(center, counter(page).display("1/1", both: true)) | ||||
| ) | ||||
|  | ||||
| = Introduction | ||||
|  | ||||
| This package lets you create nice sequence diagrams using the CeTZ package. | ||||
|  | ||||
| = Usage | ||||
|  | ||||
| Simply import #link("https://typst.app/universe/package/chronos/")[chronos] and call the `diagram` function: | ||||
| #pad(left: 1em)[```typ | ||||
| #import "@preview/chronos:0.1.0" | ||||
| #chronos.diagram({ | ||||
|   import chronos: * | ||||
|   ... | ||||
| }) | ||||
| ```] | ||||
|  | ||||
| = Examples | ||||
|  | ||||
| You can find the following examples and more in the #link("https://git.kb28.ch/HEL/circuiteria/src/branch/main/gallery")[gallery] directory | ||||
|  | ||||
| == Some groups and sequences | ||||
|  | ||||
| #example(``` | ||||
| chronos.diagram({ | ||||
|   import chronos: * | ||||
|   _seq("Alice", "Bob", comment: "Authentication Request") | ||||
|   _seq("Bob", "Alice", comment: "Authentication Failure") | ||||
|  | ||||
|   _grp("My own label", desc: "My own label2", { | ||||
|     _seq("Alice", "Log", comment: "Log attack start") | ||||
|     _grp("loop", desc: "1000 times", { | ||||
|       _seq("Alice", "Bob", comment: "DNS Attack") | ||||
|     }) | ||||
|     _seq("Alice", "Bob", comment: "Log attack end") | ||||
|   }) | ||||
| }) | ||||
| ```, wrap: false, vertical: true) | ||||
|  | ||||
| #pagebreak(weak: true) | ||||
|  | ||||
| == Lifelines | ||||
|  | ||||
| #example(``` | ||||
| chronos.diagram({ | ||||
|   import chronos: * | ||||
|   _seq("alice", "bob", comment: "hello", enable-dst: true) | ||||
|   _seq("bob", "bob", comment: "self call", enable-dst: true) | ||||
|   _seq( | ||||
|     "bill", "bob", | ||||
|     comment: "hello from thread 2", | ||||
|     enable-dst: true, | ||||
|     lifeline-style: (fill: rgb("#005500")) | ||||
|   ) | ||||
|   _seq("bob", "george", comment: "create", create-dst: true) | ||||
|   _seq( | ||||
|     "bob", "bill", | ||||
|     comment: "done in thread 2", | ||||
|     disable-src: true, | ||||
|     dashed: true | ||||
|   ) | ||||
|   _seq("bob", "bob", comment: "rc", disable-src: true, dashed: true) | ||||
|   _seq("bob", "george", comment: "delete", destroy-dst: true) | ||||
|   _seq("bob", "alice", comment: "success", disable-src: true, dashed: true) | ||||
| }) | ||||
| ```, wrap: false, vertical: true) | ||||
|  | ||||
| #pagebreak(weak: true) | ||||
|  | ||||
| == Found and lost messages | ||||
|  | ||||
| #example(``` | ||||
| chronos.diagram({ | ||||
|   import chronos: * | ||||
|   _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]) | ||||
| }) | ||||
| ```, wrap: false, vertical: true) | ||||
|  | ||||
| #pagebreak(weak: true) | ||||
|  | ||||
| == Custom images | ||||
|  | ||||
| #example(``` | ||||
| let load-img(path) = image(path, width: 1.5cm, height: 1.5cm, fit:"contain") | ||||
| let TYPST = load-img("../gallery/typst.png") | ||||
| let FERRIS = load-img("../gallery/ferris.png") | ||||
| let ME = load-img("../gallery/me.jpg") | ||||
|  | ||||
| chronos.diagram({ | ||||
|   import chronos: * | ||||
|   _par("me", display-name: "Me", shape: "custom", custom-image: ME) | ||||
|   _par("typst", display-name: "Typst", shape: "custom", custom-image: TYPST) | ||||
|   _par("rust", display-name: "Rust", shape: "custom", custom-image: FERRIS) | ||||
|  | ||||
|   _seq("me", "typst", comment: "opens document", enable-dst: true) | ||||
|   _seq("me", "typst", comment: "types document") | ||||
|   _seq("typst", "rust", comment: "compiles content", enable-dst: true) | ||||
|   _seq("rust", "typst", comment: "renders document", disable-src: true) | ||||
|   _seq("typst", "me", comment: "displays document", disable-src: true) | ||||
| }) | ||||
| ```, wrap: false, vertical: true) | ||||
|  | ||||
| #pagebreak(weak: true) | ||||
|  | ||||
| = Reference | ||||
|  | ||||
| #let par-docs = tidy.parse-module( | ||||
|   read("docs/participants.typ"), | ||||
|   name: "Participants", | ||||
|   require-all-parameters: true, | ||||
|   scope: ( | ||||
|     chronos: chronos, | ||||
|     mod-par: mod-par, | ||||
|     TYPST: TYPST, | ||||
|     doc-ref: doc-ref | ||||
|   ) | ||||
| ) | ||||
| #tidy.show-module(par-docs, show-outline: false, sort-functions: none) | ||||
|  | ||||
| #pagebreak(weak: true) | ||||
|  | ||||
| #let seq-docs = tidy.parse-module( | ||||
|   read("docs/sequences.typ"), | ||||
|   name: "Sequences", | ||||
|   require-all-parameters: true, | ||||
|   scope: ( | ||||
|     chronos: chronos, | ||||
|     doc-ref: doc-ref, | ||||
|     examples: examples | ||||
|   ) | ||||
| ) | ||||
| #tidy.show-module(seq-docs, show-outline: false, sort-functions: none) | ||||
|  | ||||
| #pagebreak(weak: true) | ||||
|  | ||||
| #let grp-docs = tidy.parse-module( | ||||
|   read("docs/groups.typ"), | ||||
|   name: "Groups", | ||||
|   require-all-parameters: true, | ||||
|   scope: ( | ||||
|     chronos: chronos, | ||||
|     doc-ref: doc-ref, | ||||
|     examples: examples | ||||
|   ) | ||||
| ) | ||||
| #tidy.show-module(grp-docs, show-outline: false, sort-functions: none) | ||||
|  | ||||
| #pagebreak(weak: true) | ||||
|  | ||||
| #let gap-sep-docs = tidy.parse-module( | ||||
|   read("docs/gaps_seps.typ"), | ||||
|   name: "Gaps and separators", | ||||
|   require-all-parameters: true, | ||||
|   scope: ( | ||||
|     chronos: chronos, | ||||
|     doc-ref: doc-ref, | ||||
|     examples: examples | ||||
|   ) | ||||
| ) | ||||
| #tidy.show-module(gap-sep-docs, show-outline: false) | ||||
|  | ||||
| #pagebreak(weak: true) | ||||
|  | ||||
| #let notes-docs = tidy.parse-module( | ||||
|   read("docs/notes.typ"), | ||||
|   name: "Notes", | ||||
|   require-all-parameters: true, | ||||
|   scope: ( | ||||
|     chronos: chronos, | ||||
|     doc-ref: doc-ref, | ||||
|     examples: examples | ||||
|   ) | ||||
| ) | ||||
| #tidy.show-module(notes-docs, show-outline: false) | ||||
| @@ -1,6 +1,7 @@ | ||||
| #import "utils.typ": get-group-span, fit-canvas | ||||
| #import "renderer.typ": render | ||||
| #import "participant.typ" as participant: _par, PAR-SPECIALS | ||||
| #import "sequence.typ": _seq | ||||
|  | ||||
| #let _gap(size: 20) = { | ||||
|   return (( | ||||
| @@ -18,6 +19,18 @@ | ||||
|   ),) | ||||
| } | ||||
|  | ||||
| #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) = { | ||||
|   if elements == none { | ||||
|     return | ||||
| @@ -27,7 +40,9 @@ | ||||
|   let elmts = elements | ||||
|   let i = 0 | ||||
|  | ||||
|   // Flatten groups | ||||
|   let activation-history = () | ||||
|  | ||||
|   // Flatten groups + convert returns | ||||
|   while i < elmts.len() { | ||||
|     let elmt = elmts.at(i) | ||||
|     if elmt.type == "grp" { | ||||
| @@ -51,6 +66,30 @@ | ||||
|         ),) + | ||||
|         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 | ||||
|   } | ||||
| @@ -119,6 +158,21 @@ | ||||
|       } 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 | ||||
| @@ -174,7 +228,7 @@ | ||||
|  | ||||
|   // Compute groups spans (horizontal) | ||||
|   for (i, elmt) in elmts.enumerate() { | ||||
|     if elmt.type == "grp" { | ||||
|     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) | ||||
| @@ -187,7 +241,6 @@ | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   set text(font: "Source Sans 3") | ||||
|   let canvas = render(participants, elmts) | ||||
|   fit-canvas(canvas, width: width) | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #import "@preview/cetz:0.2.2": draw | ||||
| #import "@preview/cetz:0.3.3": draw | ||||
| #import "consts.typ": * | ||||
|  | ||||
| #let _grp(name, desc: none, type: "default", elmts) = { | ||||
| @@ -11,6 +11,36 @@ | ||||
|   ),) | ||||
| } | ||||
|  | ||||
| #let _alt(desc, elmts, ..args) = { | ||||
|   let all-elmts = () | ||||
|   all-elmts += elmts | ||||
|   let args = args.pos() | ||||
|   for i in range(0, args.len(), step: 2) { | ||||
|     let else-desc = args.at(i) | ||||
|     let else-elmts = args.at(i + 1, default: ()) | ||||
|     all-elmts.push(( | ||||
|       type: "else", | ||||
|       desc: else-desc | ||||
|     )) | ||||
|     all-elmts += else-elmts | ||||
|   } | ||||
|  | ||||
|   return _grp("alt", desc: desc, type: "alt", all-elmts) | ||||
| } | ||||
|  | ||||
| #let _loop(desc, min: none, max: auto, elmts) = { | ||||
|   let name = "loop" | ||||
|   if min != none { | ||||
|     if max == auto { | ||||
|       max = "*" | ||||
|     } | ||||
|     name += "(" + str(min) + "," + str(max) + ")" | ||||
|   } | ||||
|   _grp(name, desc: desc, type: "loop", elmts) | ||||
| } | ||||
| #let _opt(desc, elmts) = grp("opt", desc: desc, type: "opt", elmts) | ||||
| #let _break(desc, elmts) = grp("break", desc: desc, type: "break", elmts) | ||||
|  | ||||
| #let render(x0, x1, y0, y1, group) = { | ||||
|   let shapes = () | ||||
|   let name = text(group.name, weight: "bold") | ||||
| @@ -52,3 +82,18 @@ | ||||
|  | ||||
|   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,8 +1,9 @@ | ||||
| #import "diagram.typ": diagram, from-plantuml, _gap, _evt | ||||
| #let version = version(0, 2, 0) | ||||
| #import "diagram.typ": diagram, from-plantuml, _gap, _evt, _col | ||||
|  | ||||
| #import "sequence.typ": _seq | ||||
| #import "group.typ": _grp | ||||
| #import "sequence.typ": _seq, _ret | ||||
| #import "group.typ": _grp, _loop, _alt, _opt, _break | ||||
| #import "participant.typ": _par | ||||
| #import "separator.typ": _sep | ||||
| #import "separator.typ": _sep, _delay | ||||
| #import "note.typ": _note | ||||
| #import "sync.typ": _sync | ||||
| @@ -1,4 +1,4 @@ | ||||
| #import "@preview/cetz:0.2.2": draw | ||||
| #import "@preview/cetz:0.3.3": draw | ||||
| #import "consts.typ": * | ||||
|  | ||||
| #let SIDES = ( | ||||
| @@ -25,6 +25,9 @@ | ||||
|       panic("Aligned notes can only be over a participant (got side '" + side + "')") | ||||
|     } | ||||
|   } | ||||
|   if color == auto { | ||||
|     color = COL-NOTE | ||||
|   } | ||||
|   return (( | ||||
|     type: "note", | ||||
|     side: side, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #import "@preview/cetz:0.2.2": draw | ||||
| #import "@preview/cetz:0.3.3": draw | ||||
| #import "consts.typ": * | ||||
|  | ||||
| #let PAR-SPECIALS = "?[]" | ||||
| @@ -13,6 +13,7 @@ | ||||
|   "queue", | ||||
|   "custom" | ||||
| ) | ||||
| #let DEFAULT-COLOR = rgb("#E2E2F0") | ||||
|  | ||||
| #let _par( | ||||
|   name, | ||||
| @@ -20,11 +21,14 @@ | ||||
|   from-start: true, | ||||
|   invisible: false, | ||||
|   shape: "participant", | ||||
|   color: rgb("#E2E2F0"), | ||||
|   color: DEFAULT-COLOR, | ||||
|   custom-image: none, | ||||
|   show-bottom: true, | ||||
|   show-top: true, | ||||
| ) = { | ||||
|   if color == auto { | ||||
|     color = DEFAULT-COLOR | ||||
|   } | ||||
|   return (( | ||||
|     type: "par", | ||||
|     name: name, | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| #import "@preview/cetz:0.2.2": canvas, draw | ||||
| #import "utils.typ": get-participants-i, get-style | ||||
| #import "@preview/cetz:0.3.3": canvas, draw | ||||
| #import "utils.typ": get-participants-i, get-style, normalize-units | ||||
| #import "group.typ" | ||||
| #import "participant.typ" | ||||
| #import participant: PAR-SPECIALS | ||||
| @@ -194,6 +194,40 @@ | ||||
|     } | ||||
|     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 | ||||
| } | ||||
|  | ||||
| @@ -211,6 +245,7 @@ | ||||
|  | ||||
|   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) | ||||
| @@ -252,6 +287,9 @@ | ||||
|         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 | ||||
|      | ||||
| @@ -263,6 +301,21 @@ | ||||
|       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 | ||||
| @@ -273,6 +326,26 @@ | ||||
|     } 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 | ||||
| @@ -385,6 +458,28 @@ | ||||
|           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) | ||||
|         } | ||||
|       } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #import "@preview/cetz:0.2.2": draw | ||||
| #import "@preview/cetz:0.3.3": draw | ||||
| #import "consts.typ": * | ||||
|  | ||||
| #let _sep(name) = { | ||||
| @@ -8,6 +8,14 @@ | ||||
|   ),) | ||||
| } | ||||
|  | ||||
| #let _delay(name: none, size: 30) = { | ||||
|   return (( | ||||
|     type: "delay", | ||||
|     name: name, | ||||
|     size: size | ||||
|   ),) | ||||
| } | ||||
|  | ||||
| #let render(x-pos, elmt, y) = { | ||||
|   let shapes = () | ||||
|   y -= Y-SPACE | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| #import "@preview/cetz:0.2.2": draw, vector | ||||
| #import "@preview/cetz:0.3.3": draw, vector | ||||
| #import "consts.typ": * | ||||
| #import "participant.typ" | ||||
| #import "note.typ" | ||||
|  | ||||
| #let get-arrow-marks(sym, color) = { | ||||
|   if sym == none { | ||||
|     return none | ||||
|   } | ||||
|   if type(sym) == array { | ||||
|     return sym.map(s => get-arrow-marks(s, color)) | ||||
|   } | ||||
| @@ -85,24 +88,39 @@ | ||||
|   ),) | ||||
| } | ||||
|  | ||||
| #let _ret(comment: none) = { | ||||
|   return (( | ||||
|     type: "ret", | ||||
|     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 elmt.comment != none { | ||||
|     h = calc.max(h, measure(box(elmt.comment)).height / 1pt + 6) | ||||
|   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 i1 = pars-i.at(elmt.p1) | ||||
|   let i2 = pars-i.at(elmt.p2) | ||||
|  | ||||
|   let start-info = ( | ||||
|     i: i1, | ||||
|     x: x-pos.at(i1), | ||||
| @@ -159,7 +177,7 @@ | ||||
|     let m = measure(box(par.display-name)) | ||||
|     let f = if i1 > i2 {-1} else {1} | ||||
|     end-info.x -= (m.width + PAR-PAD.last() * 2) / 2pt * f | ||||
|     shapes += participant.render(x-pos, par, y: end-info.y - CREATE-OFFSET) | ||||
|     shapes += participant.render(x-pos, par, y: end-info.y) | ||||
|   } | ||||
|  | ||||
|   end-info.ll-lvl = lifelines.at(i2).level * LIFELINE-W / 2 | ||||
| @@ -234,7 +252,7 @@ | ||||
|       (x2, end-info.y) | ||||
|     ) | ||||
|  | ||||
|     if elmt.comment != none { | ||||
|     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"}, | ||||
| @@ -258,7 +276,7 @@ | ||||
|       (x2, end-info.y) | ||||
|     ) | ||||
|  | ||||
|     if elmt.comment != none { | ||||
|     if comment != none { | ||||
|       let start-pt = pts.first() | ||||
|       let end-pt = pts.last() | ||||
|       if elmt.start-tip != "" { | ||||
| @@ -294,7 +312,7 @@ | ||||
|  | ||||
|   // Start circle tip | ||||
|   if is-circle-tip(elmt.start-tip) { | ||||
|     shapes += draw.circle(pts.first(), radius: CIRCLE-TIP-RADIUS, stroke: elmt.color, fill: none, name: "_circle-start-tip") | ||||
|     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 | ||||
| @@ -316,7 +334,7 @@ | ||||
|  | ||||
|   // End circle tip | ||||
|   if is-circle-tip(elmt.end-tip) { | ||||
|     shapes += draw.circle(pts.last(), radius: 3, stroke: elmt.color, fill: none, name: "_circle-end-tip") | ||||
|     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 | ||||
| @@ -338,27 +356,26 @@ | ||||
|  | ||||
|   shapes += draw.line(..pts, ..style) | ||||
|  | ||||
|   if elmt.comment != none { | ||||
|   if comment != none { | ||||
|     shapes += draw.content( | ||||
|       comment-pt, | ||||
|       elmt.comment, | ||||
|       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 elmt.create-dst { | ||||
|     end-info.y -= CREATE-OFFSET | ||||
|     let dst-line = lifelines.at(i2) | ||||
|     dst-line.lines.push(("create", end-info.y)) | ||||
|     lifelines.at(i2) = dst-line | ||||
|   } | ||||
|  | ||||
|   if "linked-note" in elmt { | ||||
|     let m = note.get-size(elmt.linked-note) | ||||
|   | ||||
| @@ -1,3 +1,12 @@ | ||||
| #let normalize-units(value) = { | ||||
|   if type(value) == int or type(value) == float { | ||||
|     return value | ||||
|   } | ||||
|   if type(value) == length { | ||||
|     return value / 1pt | ||||
|   } | ||||
|   panic("Unsupported type '" + str(type(value)) + "'") | ||||
| } | ||||
| #let get-participants-i(participants) = { | ||||
|   let pars-i = (:) | ||||
|   for (i, p) in participants.enumerate() { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| [package] | ||||
| name = "chronos" | ||||
| version = "0.1.0" | ||||
| compiler = "0.11.0" | ||||
| version = "0.2.1" | ||||
| compiler = "0.13.0" | ||||
| repository = "https://git.kb28.ch/HEL/chronos" | ||||
| entrypoint = "src/lib.typ" | ||||
| authors = [ | ||||
| @@ -11,4 +11,4 @@ categories = ["visualization"] | ||||
| license = "Apache-2.0" | ||||
| description = "A package to draw sequence diagrams with CeTZ" | ||||
| keywords = ["sequence", "diagram", "plantuml"] | ||||
| exclude = [ "gallery", "gallery.bash" ] | ||||
| exclude = [ "gallery", "gallery.bash", "docs" ] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user