diff --git a/packages/preview/chronos/0.3.0/LICENSE b/packages/preview/chronos/0.3.0/LICENSE new file mode 100644 index 0000000000..74e2338b68 --- /dev/null +++ b/packages/preview/chronos/0.3.0/LICENSE @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2024 HEL + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/preview/chronos/0.3.0/README.md b/packages/preview/chronos/0.3.0/README.md new file mode 100644 index 0000000000..54ecd6cc0b --- /dev/null +++ b/packages/preview/chronos/0.3.0/README.md @@ -0,0 +1,154 @@ +# chronos + +A Typst package to draw sequence diagrams with CeTZ + +--- + +This package lets you render sequence diagrams directly in Typst. The following boilerplate code creates an empty sequence diagram with two participants: + + + + + + + + + + +
TypstResult
+ +```typst +#import "@preview/chronos:0.3.0" +#chronos.diagram({ + import chronos: * + _par("Alice") + _par("Bob") +}) +``` + +
+ +> *Disclaimer*\ +> The package cannot parse PlantUML syntax for the moment, and thus requires the use of element functions, as shown in the examples. +> A PlantUML parser is in the TODO list, just not the top priority + +## Basic sequences + +You can make basic sequences using the `_seq` function: + + + + + + + + + + +
TypstResult
+ +```typst +#chronos.diagram({ + import chronos: * + _par("Alice") + _par("Bob") + + _seq("Alice", "Bob", comment: "Hello") + _seq("Bob", "Bob", comment: "Think") + _seq("Bob", "Alice", comment: "Hi") +}) +``` + +
+ +You can make lifelines using the following parameters of the `_seq` function: + - `enable-dst`: enables the destination lifeline + - `create-dst`: creates the destination lifeline and participant + - `disable-dst`: disables the destination lifeline + - `destroy-dst`: destroys the destination lifeline and participant + - `disable-src`: disables the source lifeline + - `destroy-src`: destroy the source lifeline and participant + + + + + + + + + + +
TypstResult
+ +```typst +#chronos.diagram({ + import chronos: * + _par("A", display-name: "Alice") + _par("B", display-name: "Bob") + _par("C", display-name: "Charlie") + _par("D", display-name: "Derek") + + _seq("A", "B", comment: "hello", enable-dst: true) + _seq("B", "B", comment: "self call", enable-dst: true) + _seq("C", "B", comment: "hello from thread 2", enable-dst: true, lifeline-style: (fill: rgb("#005500"))) + _seq("B", "D", comment: "create", create-dst: true) + _seq("B", "C", comment: "done in thread 2", disable-src: true, dashed: true) + _seq("B", "B", comment: "rc", disable-src: true, dashed: true) + _seq("B", "D", comment: "delete", destroy-dst: true) + _seq("B", "A", comment: "success", disable-src: true, dashed: true) +}) +``` + +
+ +## Showcase + +Several features have already been implemented in Chronos. Don't hesitate to checkout the examples in the [gallery](./gallery) folder to see what you can do. + +#### Quick example reference: + + + + + + + + + + + + + + + + + + + + + +
ExampleFeatures
+ +`example1`
([PDF](./gallery/example1.pdf)|[Typst](./gallery/example1.typ)) + +
Simple cases, color sequences, groups, separators, gaps, self-sequences
+ +`example2`
([PDF](./gallery/example2.pdf)|[Typst](./gallery/example2.typ)) + +
Lifelines, found/lost messages, synchronized sequences, slanted sequences
+ +`example3`
([PDF](./gallery/example3.pdf)|[Typst](./gallery/example3.typ)) + +
Participant shapes, sequence tips, hidden partipicant ends
+ +`notes`
([PDF](./gallery/notes.pdf)|[Typst](./gallery/notes.typ)) + +
Notes (duh), deferred participant creation
+ +## Manual + +The complete documentation describing all functions and their arguments can be found in the [manual](./manual.pdf). + +> [!NOTE] +> +> Many examples were taken/adapted from the PlantUML [documentation](https://plantuml.com/sequence-diagram) on sequence diagrams diff --git a/packages/preview/chronos/0.3.0/docs/example.typ b/packages/preview/chronos/0.3.0/docs/example.typ new file mode 100644 index 0000000000..5f980a3dd8 --- /dev/null +++ b/packages/preview/chronos/0.3.0/docs/example.typ @@ -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: false, 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(2%)} else {none}, + if show-src { + let src-block = raw(src, block: true, lang: "typc") + table( + columns: if vertical {1} else {2}, + inset: 5pt, + align: horizon + center, + stroke: none, + table.cell(inset: 1em, img), + if vertical {table.hline()} else {table.vline()}, src-block + ) + } else { + table( + inset: 1em, + img + ) + } + ) + ) + ) +} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/docs/examples.typ b/packages/preview/chronos/0.3.0/docs/examples.typ new file mode 100644 index 0000000000..f50b16c17c --- /dev/null +++ b/packages/preview/chronos/0.3.0/docs/examples.typ @@ -0,0 +1,213 @@ +#import "example.typ": example + +#let seq-return = example(``` +_seq( + "Bob", "Alice", + comment: [hello], + enable-dst: true +) +_seq( + "Alice", "Alice", + comment: [some action] +) +_ret(comment: [bye]) +```) + +#let seq-comm-align = example(``` +_par("p1", + display-name: "Start participant") +_par("p2", + display-name: "End participant") +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 = example(``` +_par("a", display-name: "Alice") +_par("b", display-name: "Bob") + +_seq("a", "b", comment: [message 1]) +_seq("b", "a", comment: [ok], dashed: true) +_gap() +_seq("a", "b", comment: [message 2]) +_seq("b", "a", comment: [ok], dashed: true) +_gap(size: 40) +_seq("a", "b", comment: [message 3]) +_seq("b", "a", comment: [ok], dashed: true) +```) + +#let seps = example(``` +_par("a", display-name: "Alice") +_par("b", display-name: "Bob") + +_sep[Initialization] +_seq("a", "b", comment: [Request 1]) +_seq( + "b", "a", + comment: [Response 1], + dashed: true +) + +_sep[Repetition] +_seq("a", "b", comment: [Request 2]) +_seq( + "b", "a", + comment: [Response 2], + dashed: true +) +```) + +#let delays = example(``` +_par("a", display-name: "Alice") +_par("b", display-name: "Bob") + +_seq("a", "b", comment: [Auth Request]) +_delay() +_seq( + "b", "a", + comment: [Auth Response], + dashed: true +) +_delay(name: [5 minutes later]) +_seq( + "b", "a", + comment: [Good Bye !], + dashed: true +) +```) + +#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) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/docs/gaps_seps.typ b/packages/preview/chronos/0.3.0/docs/gaps_seps.typ new file mode 100644 index 0000000000..678a6f3d7a --- /dev/null +++ b/packages/preview/chronos/0.3.0/docs/gaps_seps.typ @@ -0,0 +1,15 @@ +/// Creates a separator before the next element +/// #examples.seps +/// - name (content): Name to display in the middle of the separator +#let _sep(name) = {} + +/// Creates a delay before the next element +/// #examples.delays +/// - name (content, none): Name to display in the middle of the delay area +/// - size (int): Size of the delay +#let _delay(name: none, size: 30) = {} + +/// Creates a gap before the next element +/// #examples.gaps +/// - size (int): Size of the gap +#let _gap(size: 20) = {} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/docs/groups.typ b/packages/preview/chronos/0.3.0/docs/groups.typ new file mode 100644 index 0000000000..ece4ae777b --- /dev/null +++ b/packages/preview/chronos/0.3.0/docs/groups.typ @@ -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) = {} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/docs/notes.typ b/packages/preview/chronos/0.3.0/docs/notes.typ new file mode 100644 index 0000000000..3c4b72e674 --- /dev/null +++ b/packages/preview/chronos/0.3.0/docs/notes.typ @@ -0,0 +1,25 @@ +/// 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 +/// - 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( + side, + content, + pos: none, + color: rgb("#FEFFDD"), + shape: "default", + aligned: false, + allow-overlap: true +) = {} + +/// 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") \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/docs/participants.typ b/packages/preview/chronos/0.3.0/docs/participants.typ new file mode 100644 index 0000000000..6610c0dd51 --- /dev/null +++ b/packages/preview/chronos/0.3.0/docs/participants.typ @@ -0,0 +1,75 @@ +/// 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 +/// - line-stroke (stroke): The participant's line style (defaults to a light gray dashed line) +/// - custom-image (none, image): If shape is 'custom', sets the custom image to display +/// - show-bottom (bool): Whether to display the bottom shape +/// - show-top (bool): Whether to display the top shape +/// -> array +#let _par( + name, + display-name: auto, + from-start: true, + invisible: false, + shape: "participant", + color: rgb("#E2E2F0"), + line-stroke: ( + dash: "dashed", + paint: gray.darken(40%), + thickness: .5pt + ), + custom-image: none, + show-bottom: true, + show-top: true, +) = {} + +/// 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 +) = {} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/docs/sequences.typ b/packages/preview/chronos/0.3.0/docs/sequences.typ new file mode 100644 index 0000000000..d1f9b372b5 --- /dev/null +++ b/packages/preview/chronos/0.3.0/docs/sequences.typ @@ -0,0 +1,68 @@ +/// 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) +/// - lifeline-style (auto, dict): See @@_seq() +#let _evt(participant, event, lifeline-style: auto) = {} + +/// 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 +/// - outer-lifeline-connect (bool): If true, enables legacy anchoring, making sequences connect to the leftmost lifeline when arriving from the left side. If false, all connections are made with the latest/rightmost lifeline +/// -> 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, + outer-lifeline-connect: false +) = {} + +/// Creates a return sequence +/// #examples.seq-return +/// - comment (none, content): Optional comment to display along the arrow +#let _ret(comment: none) = {} + +/// Accepted values for `comment-align` argument of @@_seq() +/// #examples.seq-comm-align +#let comment-align = ( + "start", "end", "left", "center", "right" +) + +/// Accepted values for `event` argument of @@_evt() +/// +/// `EVENTS = ("create", "destroy", "enable", "disable")` +#let EVENTS = ("create", "destroy", "enable", "disable") + +/// Accepted values for `start-tip` and `end-tip` arguments of @@_seq() +/// #examples.seq-tips +#let tips = ( + "", ">", ">>", "\\", "\\\\", "/", "//", "x", "o", +) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/gallery/example1.pdf b/packages/preview/chronos/0.3.0/gallery/example1.pdf new file mode 100644 index 0000000000..1da2a60f91 Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/example1.pdf differ diff --git a/packages/preview/chronos/0.3.0/gallery/example1.typ b/packages/preview/chronos/0.3.0/gallery/example1.typ new file mode 100644 index 0000000000..6ca8d391ec --- /dev/null +++ b/packages/preview/chronos/0.3.0/gallery/example1.typ @@ -0,0 +1,106 @@ +#import "@preview/chronos:0.3.0" as chronos + +#chronos.from-plantuml(```plantuml +Alice -> Bob: Authentication Request +Bob --> Alice: Authentication Response + +Alice -> Bob: Another authentication Request +Alice <-- Bob: Another authentication Response +```) + + +#chronos.diagram({ + import chronos: * + _seq("Alice", "Bob", comment: "Authentication Request") + _seq("Bob", "Alice", comment: "Authentication Response", dashed: true) + + _seq("Alice", "Bob", comment: "Another authentication Request") + _seq("Bob", "Alice", comment: "Another authentication Response", dashed: true) +}) + +#chronos.diagram({ + import chronos: * + _seq("Bob", "Alice", comment: "bonjour", color: red) + _seq("Alice", "Bob", comment: "ok", color: blue) +}) + +#chronos.diagram({ + import chronos: * + _seq("Alice", "Bob", comment: "This is a test") + _seq("Alice", "Callum", comment: "This is another test with a long text") +}) + +#chronos.diagram({ + import chronos: * + _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") + } + ) +}) + +#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({ + import chronos: * + _sep("Initialization") + _seq("Alice", "Bob", comment: "Authentication Request") + _seq("Bob", "Alice", comment: "Authentication Response", dashed: true) + + _sep("Repetition") + _seq("Alice", "Bob", comment: "Another authentication Request") + _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") + _seq("Bob", "Alice", comment: "ok", dashed: true) + _gap() + _seq("Alice", "Bob", comment: "message 2") + _seq("Bob", "Alice", comment: "ok", dashed: true) + _gap(size: 45) + _seq("Alice", "Bob", comment: "message 3") + _seq("Bob", "Alice", comment: "ok", dashed: true) +}) + +#chronos.diagram({ + import chronos: * + _seq("Alice", "Alice", comment: "On the\nright") + _seq("Alice", "Alice", flip: true, comment: "On the\nleft") +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/gallery/example2.pdf b/packages/preview/chronos/0.3.0/gallery/example2.pdf new file mode 100644 index 0000000000..750ea71fd2 Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/example2.pdf differ diff --git a/packages/preview/chronos/0.3.0/gallery/example2.typ b/packages/preview/chronos/0.3.0/gallery/example2.typ new file mode 100644 index 0000000000..c5d77f714a --- /dev/null +++ b/packages/preview/chronos/0.3.0/gallery/example2.typ @@ -0,0 +1,169 @@ +#import "@preview/chronos:0.3.0" as chronos + + +#chronos.diagram({ + import chronos: * + _seq("User", "A", comment: "DoWork", enable-dst: true) + _seq("A", "B", comment: [#sym.quote.chevron.l createRequest #sym.quote.chevron.r], enable-dst: true) + _seq("B", "C", comment: "DoWork", enable-dst: true) + _seq("C", "B", comment: "WorkDone", destroy-src: true, disable-src: true, dashed: true) + _seq("B", "A", comment: "RequestCreated", disable-src: true, dashed: true) + _seq("A", "User", comment: "Done", disable-src: true) +}) + +#chronos.diagram({ + import chronos: * + _seq("User", "A", comment: "DoWork", enable-dst: true, lifeline-style: (fill: rgb("#FFBBBB"))) + _seq("A", "A", comment: "Internal call", enable-dst: true, lifeline-style: (fill: rgb("#E9967A"))) + _seq("A", "B", comment: [#sym.quote.chevron.l createRequest #sym.quote.chevron.r], enable-dst: true) + _seq("B", "A", comment: "RequestCreated", disable-src: true, disable-dst: true, dashed: true) + _seq("A", "User", comment: "Done", disable-src: true) +}) + +#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) +}) + +#chronos.diagram({ + import chronos: * + _seq("alice", "bob", comment: "hello1", enable-dst: true) + _seq("bob", "charlie", comment: "hello2", enable-dst: true, disable-src: true) + _seq("charlie", "alice", comment: "ok", dashed: true, disable-src: true) +}) + +#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]) +}) + +#chronos.diagram({ + import chronos: * + _par("alice", display-name: "Alice") + _par("bob", display-name: "Bob") + _par("craig", display-name: "Craig") + + _seq("bob", "alice") + _seq("bob", "craig") + _gap() + + _sync({ + _seq("bob", "alice", comment: "Synched", comment-align: "start") + _seq("bob", "craig", comment: "Synched", comment-align: "start") + }) + _gap() + + _seq("alice", "bob") + _seq("craig", "bob") + _gap() + + _sync({ + _seq("alice", "bob") + _seq("craig", "bob") + }) + _gap() + + _sync({ + _seq("alice", "bob", enable-dst: true) + _seq("craig", "bob") + }) + _gap() + + _evt("bob", "disable") +}) + +#chronos.diagram({ + import chronos: * + _par("alice", display-name: "Alice") + _par("bob", display-name: "Bob") + _par("craig", display-name: "Craig") + + _seq("alice", "bob") + _seq("bob", "craig", slant: auto) + _seq("alice", "craig", slant: 20) + + _sync({ + _seq("alice", "bob", slant: 10) + _seq("craig", "bob", slant: 20) + }) + + _sync({ + _seq("alice", "bob", slant: auto) + _seq("bob", "alice", slant: auto) + }) + + _gap() + _evt("bob", "disable") +}) + +#grid(columns: 2, column-gutter: 2em, + chronos.diagram({ + import chronos: * + + _par("alice", display-name: "Alice") + _par("bob", display-name: "Bob") + _seq("alice", "bob", comment: "This is a very long comment") + + // Left to right + _seq("alice", "bob", comment: "Start aligned", comment-align: "start") + _seq("alice", "bob", comment: "End aligned", comment-align: "end") + _seq("alice", "bob", comment: "Left aligned", comment-align: "left") + _seq("alice", "bob", comment: "Right aligned", comment-align: "right") + _seq("alice", "bob", comment: "Centered", comment-align: "center") + _gap() + + // Right to left + _seq("bob", "alice", comment: "Start aligned", comment-align: "start") + _seq("bob", "alice", comment: "End aligned", comment-align: "end") + _seq("bob", "alice", comment: "Left aligned", comment-align: "left") + _seq("bob", "alice", comment: "Right aligned", comment-align: "right") + _seq("bob", "alice", comment: "Centered", comment-align: "center") + _gap() + + // Slant left to right + _seq("alice", "bob", comment: "Start aligned", comment-align: "start", slant: 10) + _seq("alice", "bob", comment: "End aligned", comment-align: "end", slant: 10) + _seq("alice", "bob", comment: "Left aligned", comment-align: "left", slant: 10) + _seq("alice", "bob", comment: "Right aligned", comment-align: "right", slant: 10) + _seq("alice", "bob", comment: "Centered", comment-align: "center", slant: 10) + _gap() + + // Slant right to left + _seq("bob", "alice", comment: "Start aligned", comment-align: "start", slant: 10) + _seq("bob", "alice", comment: "End aligned", comment-align: "end", slant: 10) + _seq("bob", "alice", comment: "Left aligned", comment-align: "left", slant: 10) + _seq("bob", "alice", comment: "Right aligned", comment-align: "right", slant: 10) + _seq("bob", "alice", comment: "Centered", comment-align: "center", slant: 10) + }), + + chronos.diagram({ + import chronos: * + + _par("alice", display-name: "Alice") + + _seq("alice", "alice", comment: "Start aligned", comment-align: "start") + _seq("alice", "alice", comment: "End aligned", comment-align: "end") + _seq("alice", "alice", comment: "Left aligned", comment-align: "left") + _seq("alice", "alice", comment: "Right aligned", comment-align: "right") + _seq("alice", "alice", comment: "Centered", comment-align: "center") + + _seq("alice", "alice", comment: "Start aligned", comment-align: "start", flip: true) + _seq("alice", "alice", comment: "End aligned", comment-align: "end", flip: true) + _seq("alice", "alice", comment: "Left aligned", comment-align: "left", flip: true) + _seq("alice", "alice", comment: "Right aligned", comment-align: "right", flip: true) + _seq("alice", "alice", comment: "Centered", comment-align: "center", flip: true) + }) +) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/gallery/example3.pdf b/packages/preview/chronos/0.3.0/gallery/example3.pdf new file mode 100644 index 0000000000..b4b30f3e86 Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/example3.pdf differ diff --git a/packages/preview/chronos/0.3.0/gallery/example3.typ b/packages/preview/chronos/0.3.0/gallery/example3.typ new file mode 100644 index 0000000000..1435e56914 --- /dev/null +++ b/packages/preview/chronos/0.3.0/gallery/example3.typ @@ -0,0 +1,166 @@ +#import "@preview/chronos:0.3.0" as chronos + +#set page(width: auto, height: auto) + +#let TYPST = image("typst.png", width: 1.5cm, height: 1.5cm, fit: "contain") +#let FERRIS = image("ferris.png", width: 1.5cm, height: 1.5cm, fit: "contain") +#let ME = image("me.jpg", width: 1.5cm, height: 1.5cm, fit: "contain") + +#chronos.diagram({ + import chronos: * + _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) + + _seq("Foo", "Foo1", comment: "To actor") + _seq("Foo", "Foo2", comment: "To boundary") + _seq("Foo", "Foo3", comment: "To control") + _seq("Foo", "Foo4", comment: "To entity") + _seq("Foo", "Foo5", comment: "To database") + _seq("Foo", "Foo6", comment: "To collections") + _seq("Foo", "Foo7", comment: "To queue") + _seq("Foo", "Foo8", comment: "To Typst") + _seq("Foo", "Foo9", comment: "To ferris") + _seq("Foo", "Foo10", comment: "To Baryhobal") +}) + +#pagebreak() +#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") + _evt("typst", "disable") +}) + +#pagebreak() + +#stack(dir: ltr, spacing: 1em, +chronos.diagram({ + import chronos: * + + _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`) +}), + +chronos.diagram({ + import chronos: * + + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _seq("b", "a", end-tip: ">", comment: `->`) + _seq("b", "a", end-tip: ">>", comment: `->>`) + _seq("b", "a", end-tip: "\\", comment: `-\`) + _seq("b", "a", end-tip: "\\\\", comment: `-\\`) + _seq("b", "a", end-tip: "/", comment: `-/`) + _seq("b", "a", end-tip: "//", comment: `-//`) + _seq("b", "a", end-tip: "x", comment: `->x`) + _seq("b", "a", start-tip: "x", comment: `x->`) + _seq("b", "a", start-tip: "o", comment: `o->`) + _seq("b", "a", end-tip: ("o", ">"), comment: `->o`) + _seq("b", "a", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`) + _seq("b", "a", start-tip: ">", end-tip: ">", comment: `<->`) + _seq("b", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`) + _seq("b", "a", start-tip: "x", end-tip: "x", comment: `x<->x`) + _seq("b", "a", end-tip: ("o", ">>"), comment: `->>o`) + _seq("b", "a", end-tip: ("o", "\\"), comment: `-\o`) + _seq("b", "a", end-tip: ("o", "\\\\"), comment: `-\\o`) + _seq("b", "a", end-tip: ("o", "/"), comment: `-/o`) + _seq("b", "a", end-tip: ("o", "//"), comment: `-//o`) + _seq("b", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`) +}), + +chronos.diagram({ + import chronos: * + + _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`) +}) +) + +#chronos.diagram({ + import chronos: * + + _par("a", display-name: "Alice") + _par("b", display-name: "Bob", show-bottom: false) + _par("c", display-name: "Caleb", show-top: false) + _par("d", display-name: "Danny", show-bottom: false, show-top: false) + + _gap() +}) + +#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]) +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/gallery/ferris.png b/packages/preview/chronos/0.3.0/gallery/ferris.png new file mode 100644 index 0000000000..e7b957890f Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/ferris.png differ diff --git a/packages/preview/chronos/0.3.0/gallery/gitea.png b/packages/preview/chronos/0.3.0/gallery/gitea.png new file mode 100644 index 0000000000..7d2874cf2f Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/gitea.png differ diff --git a/packages/preview/chronos/0.3.0/gallery/me.jpg b/packages/preview/chronos/0.3.0/gallery/me.jpg new file mode 100644 index 0000000000..ccf8d07d9f Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/me.jpg differ diff --git a/packages/preview/chronos/0.3.0/gallery/notes.pdf b/packages/preview/chronos/0.3.0/gallery/notes.pdf new file mode 100644 index 0000000000..5f90076852 Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/notes.pdf differ diff --git a/packages/preview/chronos/0.3.0/gallery/notes.typ b/packages/preview/chronos/0.3.0/gallery/notes.typ new file mode 100644 index 0000000000..bebb7a61a9 --- /dev/null +++ b/packages/preview/chronos/0.3.0/gallery/notes.typ @@ -0,0 +1,145 @@ +#import "@preview/chronos:0.3.0" as chronos: * + +#set page(width: auto, height: auto) +#chronos.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() + +#chronos.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")) +}) + +#pagebreak() + +#chronos.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") +}) + +#pagebreak() + +#chronos.diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + _par("c", display-name: "Charlie") + + _seq("a", "b", comment: [m1]) + _seq("b", "c", comment: [m2]) + + _note("over", [Old method for note over all part. with:\ `note over FirstPart, LastPart`.], pos: ("a", "c")) + _note("across", [New method with:\ `note across`.]) + + _seq("b", "a") + + _note("across", [Note across all part.], shape: "hex") +}) + +#pagebreak() + +#chronos.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]) +}) + +#chronos.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("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")) +}) + +#pagebreak() + +#chronos.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("gitea.png", width: 1cm, height: 1cm, fit: "contain")) + ], pos: ("a", "b")) +}) + +// TODO +/* +#pagebreak() + +#chronos.diagram({ + _par("a", display-name: [Alice]) + _par("b", display-name: [Bob]) + + _seq("a", "b", comment: [Hello]) + _note("left", [This is a note]) + + _seq("[", "a", comment: [Test]) + _note("left", [This is also a note]) +})*/ + +#pagebreak() + +#chronos.diagram({ + _seq("Bob", "Alice", comment: [Hello]) + _evt("Other", "create") +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/gallery/readme/boilerplate.png b/packages/preview/chronos/0.3.0/gallery/readme/boilerplate.png new file mode 100644 index 0000000000..4fcfa88f88 Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/readme/boilerplate.png differ diff --git a/packages/preview/chronos/0.3.0/gallery/readme/boilerplate.typ b/packages/preview/chronos/0.3.0/gallery/readme/boilerplate.typ new file mode 100644 index 0000000000..34891e5830 --- /dev/null +++ b/packages/preview/chronos/0.3.0/gallery/readme/boilerplate.typ @@ -0,0 +1,13 @@ +#import "@preview/chronos:0.3.0" as chronos + +#set page( + width: auto, + height: auto, + margin: 0.5cm +) + +#chronos.diagram({ + import chronos: * + _par("Alice") + _par("Bob") +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/gallery/readme/lifelines.png b/packages/preview/chronos/0.3.0/gallery/readme/lifelines.png new file mode 100644 index 0000000000..cc72c36019 Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/readme/lifelines.png differ diff --git a/packages/preview/chronos/0.3.0/gallery/readme/lifelines.typ b/packages/preview/chronos/0.3.0/gallery/readme/lifelines.typ new file mode 100644 index 0000000000..6480273416 --- /dev/null +++ b/packages/preview/chronos/0.3.0/gallery/readme/lifelines.typ @@ -0,0 +1,24 @@ +#import "@preview/chronos:0.3.0" as chronos + +#set page( + width: auto, + height: auto, + margin: 0.5cm +) + +#chronos.diagram({ + import chronos: * + _par("A", display-name: "Alice") + _par("B", display-name: "Bob") + _par("C", display-name: "Charlie") + _par("D", display-name: "Derek") + + _seq("A", "B", comment: "hello", enable-dst: true) + _seq("B", "B", comment: "self call", enable-dst: true) + _seq("C", "B", comment: "hello from thread 2", enable-dst: true, lifeline-style: (fill: rgb("#005500"))) + _seq("B", "D", comment: "create", create-dst: true) + _seq("B", "C", comment: "done in thread 2", disable-src: true, dashed: true) + _seq("B", "B", comment: "rc", disable-src: true, dashed: true) + _seq("B", "D", comment: "delete", destroy-dst: true) + _seq("B", "A", comment: "success", disable-src: true, dashed: true) +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/gallery/readme/simple_sequence.png b/packages/preview/chronos/0.3.0/gallery/readme/simple_sequence.png new file mode 100644 index 0000000000..eedde29565 Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/readme/simple_sequence.png differ diff --git a/packages/preview/chronos/0.3.0/gallery/readme/simple_sequence.typ b/packages/preview/chronos/0.3.0/gallery/readme/simple_sequence.typ new file mode 100644 index 0000000000..e0a6f65008 --- /dev/null +++ b/packages/preview/chronos/0.3.0/gallery/readme/simple_sequence.typ @@ -0,0 +1,17 @@ +#import "@preview/chronos:0.3.0" as chronos + +#set page( + width: auto, + height: auto, + margin: 0.5cm +) + +#chronos.diagram({ + import chronos: * + _par("Alice") + _par("Bob") + + _seq("Alice", "Bob", comment: "Hello") + _seq("Bob", "Bob", comment: "Think") + _seq("Bob", "Alice", comment: "Hi") +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/gallery/typst.png b/packages/preview/chronos/0.3.0/gallery/typst.png new file mode 100644 index 0000000000..40613e7541 Binary files /dev/null and b/packages/preview/chronos/0.3.0/gallery/typst.png differ diff --git a/packages/preview/chronos/0.3.0/justfile b/packages/preview/chronos/0.3.0/justfile new file mode 100644 index 0000000000..5ad4926689 --- /dev/null +++ b/packages/preview/chronos/0.3.0/justfile @@ -0,0 +1,25 @@ +# Local Variables: +# mode: makefile +# End: +gallery_dir := "./gallery" +set shell := ["bash", "-uc"] + +@version: + echo $'\e[1mTypst:\e[0m' + typst --version + echo + echo $'\e[1mTytanic:\e[0m' + tt util about + +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}} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/manual.pdf b/packages/preview/chronos/0.3.0/manual.pdf new file mode 100644 index 0000000000..8729fb0851 Binary files /dev/null and b/packages/preview/chronos/0.3.0/manual.pdf differ diff --git a/packages/preview/chronos/0.3.0/manual.typ b/packages/preview/chronos/0.3.0/manual.typ new file mode 100644 index 0000000000..2876163da4 --- /dev/null +++ b/packages/preview/chronos/0.3.0/manual.typ @@ -0,0 +1,238 @@ +#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/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") + +#show: codly-init +#codly( + languages: codly-languages +) + +#set text(font: "Source Sans 3") + +#set heading(numbering: (..num) => if num.pos().len() < 4 { + numbering("1.1", ..num) +}) + +#align(center)[ + #v(2cm) + #text(size: 2em)[*Chronos*] + + _v#chronos.version;_ + #v(1cm) + #chronos.diagram({ + import chronos: * + _par("u", display-name: [User], shape: "actor") + _par("wa", display-name: [Web App]) + _par("tu", display-name: [Typst Universe], shape: "database") + + _seq("u", "wa", comment: [Compile document], enable-dst: true) + _seq("wa", "tu", comment: [Fetch Chronos]) + _seq("tu", "wa", dashed: true, slant: 10) + _seq("wa", "wa", comment: [Render]) + _ret(comment: [Nice sequence diagram]) + }) +] + +#pagebreak() + +#{ + outline(indent: auto, depth: 3) +} +#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 + +#let import-stmt = "#import \"@preview/chronos:" + str(chronos.version) + "\"" + +Simply import #link("https://typst.app/universe/package/chronos/")[chronos] and call the `diagram` function: +#raw(block:true, lang: "typ", ```typ +$import +#chronos.diagram({ + import chronos: * + ... +}) +```.text.replace("$import", import-stmt)) + += Examples + +You can find the following examples and more in the #link("https://git.kb28.ch/HEL/chronos/src/branch/main/gallery")[gallery] directory + +== 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, + old-syntax: true, + scope: ( + chronos: chronos, + mod-par: mod-par, + TYPST: TYPST + ) +) +#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, + old-syntax: true, + scope: ( + chronos: chronos, + 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, + old-syntax: true, + scope: ( + chronos: chronos, + 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, + old-syntax: true, + scope: ( + chronos: chronos, + examples: examples + ) +) +#tidy.show-module(gap-sep-docs, show-outline: false, sort-functions: none) + +#pagebreak(weak: true) + +#let notes-docs = tidy.parse-module( + read("docs/notes.typ"), + name: "Notes", + require-all-parameters: true, + old-syntax: true, + scope: ( + chronos: chronos, + examples: examples + ) +) +#tidy.show-module(notes-docs, show-outline: false) diff --git a/packages/preview/chronos/0.3.0/src/cetz.typ b/packages/preview/chronos/0.3.0/src/cetz.typ new file mode 100644 index 0000000000..670e5655d1 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/cetz.typ @@ -0,0 +1 @@ +#import "@preview/cetz:0.4.2": * \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/consts.typ b/packages/preview/chronos/0.3.0/src/consts.typ new file mode 100644 index 0000000000..ba99801e73 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/consts.typ @@ -0,0 +1,30 @@ +#let Y-SPACE = 10 +#let PAR-SPACE = 10 +#let COMMENT-PAD = 8 +#let LIFELINE-W = 10 +#let CREATE-OFFSET = 15 +#let DEFAULT-SLANT = 10 +#let CROSS-TIP-SIZE = 4 +#let CIRCLE-TIP-RADIUS = 3 + +#let SYM-GAP = 5 +#let PAR-PAD = (5pt, 3pt) +#let ACTOR-WIDTH = 20 +#let BOUNDARY-HEIGHT = 20 +#let CONTROL-HEIGHT = 20 +#let ENTITY-HEIGHT = 20 +#let DATABASE-WIDTH = 24 +#let COLLECTIONS-PAD = (5pt, 3pt) +#let COLLECTIONS-DX = 3 +#let COLLECTIONS-DY = 3 +#let QUEUE-PAD = (5pt, 3pt) + +#let NOTE-PAD = (6, 3) +#let NOTE-CORNER-SIZE = 6 +#let NOTE-GAP = 3 +#let NOTE-HEX-PAD = (6, 8) + +#let COL-DESTRUCTION = rgb("#A80238") +#let COL-GRP-NAME = rgb("#EEEEEE") +#let COL-SEP-NAME = rgb("#EEEEEE") +#let COL-NOTE = rgb("#FEFFDD") \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/core/draw/delay.typ b/packages/preview/chronos/0.3.0/src/core/draw/delay.typ new file mode 100644 index 0000000000..3b0a000d1b --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/draw/delay.typ @@ -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 + }) +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/core/draw/event.typ b/packages/preview/chronos/0.3.0/src/core/draw/event.typ new file mode 100644 index 0000000000..bc39297612 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/draw/event.typ @@ -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 + }) +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/core/draw/group.typ b/packages/preview/chronos/0.3.0/src/core/draw/group.typ new file mode 100644 index 0000000000..a1825946f9 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/draw/group.typ @@ -0,0 +1,152 @@ +#import "/src/cetz.typ": draw + +#import "/src/consts.typ": * +#import "/src/core/utils.typ": get-ctx, set-ctx, expand-parent-group + +#let display-name(name) = text(name, weight: "bold") +#let display-desc(desc) = text([\[#desc\]], weight: "bold", size: .8em) + +#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 = display-name(group.name) + let m = measure(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), + display-desc(group.desc), + 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), + display-desc(elmt.desc), + 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 + + // Fit name and descriptions + let name-m = measure(display-name(group.name)) + let width = name-m.width / 1pt + 15 + if group.desc != none { + let desc-m = measure(display-desc(group.desc)) + width += desc-m.width / 1pt + 6 + } + if group.grp-type == "alt" { + width = calc.max(width, ..group.elses.map(e => { + let elmt = e.at(1) + let desc-m = measure(display-desc(elmt.desc)) + return desc-m.width / 1pt + 6 + })) + } + x1 = calc.max(x1, x0 + width + 3) + + 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 +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/core/draw/note.typ b/packages/preview/chronos/0.3.0/src/core/draw/note.typ new file mode 100644 index 0000000000..33b33aab6b --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/draw/note.typ @@ -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) + }) +} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/core/draw/participant.typ b/packages/preview/chronos/0.3.0/src/core/draw/participant.typ new file mode 100644 index 0000000000..d7c87e3c9d --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/draw/participant.typ @@ -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 {"base"} + ) +} + +#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 {"base"} + ) +} + +#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 {"base"} + ) +} + +#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 {"base"} + ) +} + +#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 {"base"} + ) +} + +#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 {"base"} + ) +} + +#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) + } + } +},) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/core/draw/separator.typ b/packages/preview/chronos/0.3.0/src/core/draw/separator.typ new file mode 100644 index 0000000000..ecc0db2ba5 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/draw/separator.typ @@ -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 + }) +}) diff --git a/packages/preview/chronos/0.3.0/src/core/draw/sequence.typ b/packages/preview/chronos/0.3.0/src/core/draw/sequence.typ new file mode 100644 index 0000000000..b1f0659ee1 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/draw/sequence.typ @@ -0,0 +1,404 @@ +#import "/src/cetz.typ": draw, vector, coordinate + +#import "note.typ" +#import "/src/consts.typ": * +#import "/src/core/utils.typ": get-ctx, set-ctx, expand-parent-group + +#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("rx", start-info.x + start-info.ll-lvl) + end-info.insert("rx", end-info.x + end-info.ll-lvl) + let start-lx = start-info.x + let end-lx = end-info.x + if seq.outer-lifeline-connect { + if start-info.ll-lvl != 0 {start-lx -= LIFELINE-W / 2} + if end-info.ll-lvl != 0 {end-lx -= LIFELINE-W / 2} + } else { + if start-info.ll-lvl != 0 {start-lx = start-info.rx - LIFELINE-W} + if end-info.ll-lvl != 0 {end-lx = end-info.rx - LIFELINE-W} + } + start-info.insert("lx", start-lx) + end-info.insert("lx", end-lx) + + // 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) + } + + expand-parent-group( + calc.min(x1, x2, x-mid), + calc.max(x1, x2, x-mid) + ) + + } 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) + + expand-parent-group( + calc.min(x1, x2), + calc.max(x1, x2) + ) + } + + // 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, + name: "comment" + ) + + // TODO: Improve this + draw.get-ctx(c => { + let (_, left, right) = coordinate.resolve( + c, + "comment.west", + "comment.east" + ) + expand-parent-group( + left.at(0), + right.at(0) + ) + }) + + } + + 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 + c.last-drawn = ( + type: "seq", + start-info: start-info, + end-info: end-info + ) + return c + }) +}) diff --git a/packages/preview/chronos/0.3.0/src/core/draw/sync.typ b/packages/preview/chronos/0.3.0/src/core/draw/sync.typ new file mode 100644 index 0000000000..050fe90b42 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/draw/sync.typ @@ -0,0 +1,66 @@ +#import "/src/core/utils.typ": get-ctx, is-elmt, set-ctx +#import "/src/cetz.typ": draw + +#let render(sync) = get-ctx(ctx => { + set-ctx(c => { + c.sync = ( + ctx: ctx, + bottoms: (), + starts: (), + start-y: ctx.y, + align-y: ctx.y + ) + c.in-sync = true + return c + }) +}) + +#let in-sync-render(elmt) = { + set-ctx(c => { + c.y = c.sync.start-y + return c + }) + draw.hide({ + (elmt.draw)(elmt) + }) + set-ctx(c => { + c.sync.starts.push(c.last-drawn.start-info.y) + c.sync.bottoms.push(c.y) + return c + }) +} + +#let render-end(sync) = get-ctx(ctx => { + 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 => { + let new-sync = c.sync + if new-sync.starts.len() != 0 { + new-sync.align-y = calc.min(..new-sync.starts) + } + new-sync.remove("ctx") + return c.sync.ctx + (sync: new-sync) + }) + + for (i, e) in sync.elmts.enumerate() { + set-ctx(c => { + let dy = c.sync.starts.at(i) - c.sync.start-y + c.y = c.sync.align-y - dy + return c + }) + (e.draw)(e) + } + + set-ctx(c => { + let heights = c.sync.starts.zip(c.sync.bottoms).map(((s, b)) => b - s) + c.y = c.sync.align-y + calc.min(..heights) + c.remove("sync") + return c + }) +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/core/renderer.typ b/packages/preview/chronos/0.3.0/src/core/renderer.typ new file mode 100644 index 0000000000..14cf8d5eec --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/renderer.typ @@ -0,0 +1,403 @@ +#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 "draw/sync.typ": in-sync-render +#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 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) + + 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: () + )), + in-sync: false + ) + 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" { + get-ctx(ctx => { + if ctx.in-sync and elmt.type != "sync-end" { + in-sync-render(elmt) + } else { + (elmt.draw)(elmt) + } + }) + } + } + + set-ctx(ctx => { + ctx.y -= Y-SPACE + return ctx + }) + + draw.on-layer(-1, { + if DEBUG-INVISIBLE { + render-debug() + } + + participant.render-lifelines() + }) +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/core/setup.typ b/packages/preview/chronos/0.3.0/src/core/setup.typ new file mode 100644 index 0000000000..776b2bb31f --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/setup.typ @@ -0,0 +1,358 @@ +#import "draw/group.typ": render-end as grp-render-end +#import "draw/sync.typ": render-end as sync-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 flatten-sync(elmts, i) = { + let sync = elmts.at(i) + elmts.at(i) = sync + let start = sync + start.remove("elmts") + return ( + elmts.slice(0, i) + + (start,) + + sync.elmts + + (( + type: "sync-end", + draw: sync-render-end, + elmts: sync.elmts + ),) + + 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 == "sync" { + elmts = flatten-sync(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) +} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/core/utils.typ b/packages/preview/chronos/0.3.0/src/core/utils.typ new file mode 100644 index 0000000000..0dd9ee9433 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/core/utils.typ @@ -0,0 +1,117 @@ +#import "/src/cetz.typ": draw + +#let is-elmt(elmt) = { + if type(elmt) != dictionary { + return false + } + if "type" not in elmt { + return false + } + return true +} + +#let normalize-units(value) = { + if type(value) == int or type(value) == float { + return value + } + 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() { + pars-i.insert(p.name, i) + } + return pars-i +} + +#let get-group-span(participants, group) = { + let min-i = participants.len() - 1 + let max-i = 0 + let pars-i = get-participants-i(participants) + + for elmt in group.elmts { + if elmt.type == "seq" { + let i1 = pars-i.at(elmt.p1) + let i2 = pars-i.at(elmt.p2) + min-i = calc.min(min-i, i1, i2) + max-i = calc.max(max-i, i1, i2) + } else if elmt.type == "grp" { + let (i0, i1) = get-group-span(participants, elmt) + min-i = calc.min(min-i, i0) + max-i = calc.max(max-i, i1) + } else if elmt.type == "sync-end" { + let (i0, i1) = get-group-span(participants, elmt) + min-i = calc.min(min-i, i0) + 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) +} + +#let get-style(base-name, mods) = { + let style = if base-name == "lifeline" {( + fill: white, + stroke: black + 1pt + )} + + if mods == auto { + return style + } + if type(mods) == dictionary { + return style + mods + } + + panic("Invalid type for parameter mods, expected auto or dictionary, got " + str(type(mods))) +} + +#let fit-canvas(canvas, width: auto) = layout(size => { + let m = measure(canvas) + let w = m.width + let h = m.height + let r = if w == 0pt {0} else { + if width == auto {1} + else if type(width) == length { + width / w + } else { + size.width * width / w + } + } + let new-w = w * r + let new-h = h * r + r *= 100% + + box( + width: new-w, + height: new-h, + scale(x: r, y: r, reflow: true, canvas) + ) +}) + +#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 +}) \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/diagram.typ b/packages/preview/chronos/0.3.0/src/diagram.typ new file mode 100644 index 0000000000..a16809e8ac --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/diagram.typ @@ -0,0 +1,19 @@ +#import "core/draw/event.typ": render as evt-render +#import "core/renderer.typ": render +#import "core/setup.typ": setup +#import "core/utils.typ": fit-canvas, set-ctx + +#let diagram(elements, width: auto) = { + if elements == none { + return + } + + let (elmts, participants) = setup(elements) + + let canvas = render(participants, elmts) + fit-canvas(canvas, width: width) +} + +#let from-plantuml(code) = { + let code = code.text +} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/group.typ b/packages/preview/chronos/0.3.0/src/group.typ new file mode 100644 index 0000000000..dc75065e10 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/group.typ @@ -0,0 +1,43 @@ +#import "core/draw/group.typ" + +#let _grp(name, desc: none, type: "default", elmts) = { + return (( + type: "grp", + draw: group.render-start, + name: name, + desc: desc, + grp-type: type, + elmts: elmts + ),) +} + +#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", + draw: group.render-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) diff --git a/packages/preview/chronos/0.3.0/src/lib.typ b/packages/preview/chronos/0.3.0/src/lib.typ new file mode 100644 index 0000000000..09d36a8792 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/lib.typ @@ -0,0 +1,8 @@ +#let version = version(0, 3, 0) +#import "diagram.typ": diagram, from-plantuml + +#import "sequence.typ": _seq, _ret +#import "group.typ": _grp, _loop, _alt, _opt, _break +#import "participant.typ": _par +#import "misc.typ": _sep, _delay, _sync, _gap, _evt, _col +#import "note.typ": _note \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/misc.typ b/packages/preview/chronos/0.3.0/src/misc.typ new file mode 100644 index 0000000000..48e69861b2 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/misc.typ @@ -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, lifeline-style: auto) = { + return (( + type: "evt", + draw: evt-render, + participant: participant, + event: event, + lifeline-style: lifeline-style + ),) +} + +#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 + ),) +} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/note.typ b/packages/preview/chronos/0.3.0/src/note.typ new file mode 100644 index 0000000000..01e6b3103f --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/note.typ @@ -0,0 +1,51 @@ +#import "consts.typ": * +#import "core/draw/note.typ" + +#let SIDES = ( + "left", + "right", + "over", + "across" +) + +#let SHAPES = ( + "default", + "rect", + "hex" +) + +#let _note( + side, + content, + pos: none, + color: COL-NOTE, + shape: "default", + aligned: false, + allow-overlap: true +) = { + if side == "over" { + if pos == none { + panic("Pos cannot be none with side 'over'") + } + } + if aligned { + if side != "over" { + panic("Aligned notes can only be over a participant (got side '" + side + "')") + } + } + if color == auto { + color = COL-NOTE + } + return (( + type: "note", + draw: note.render, + side: side, + content: content, + pos: pos, + color: color, + shape: shape, + aligned: aligned, + aligned-with: none, + allow-overlap: allow-overlap + ),) +} diff --git a/packages/preview/chronos/0.3.0/src/parser2.typ b/packages/preview/chronos/0.3.0/src/parser2.typ new file mode 100644 index 0000000000..7237189025 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/parser2.typ @@ -0,0 +1,36 @@ +#let parse- + +#let NOTE-END = ( + "default": "end note", + "rect": "endrnote", + "hex": "endhnote" +) + +#let from-plantuml(code, width: auto) = { + let code = code.text + code = code.replace(regex("(?s)/'.*?'/"), "") + + let elmts = () + let lines = code.split("\n") + let group-stack = () + let note-data = none + + for line in lines { + // [BEGIN] Multiline notes // + if note-data != none { + let l = line.trim() + if l = NOTE-END.at(note-data.type) { + + } else { + note-data.lines.push(line) + } + continue + } + // [END] Multiline notes // + + + + } + + return diagram(elmts, width: width) +} \ No newline at end of file diff --git a/packages/preview/chronos/0.3.0/src/participant.typ b/packages/preview/chronos/0.3.0/src/participant.typ new file mode 100644 index 0000000000..1a4b4b25da --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/participant.typ @@ -0,0 +1,63 @@ +#import "core/draw/participant.typ" + +#let PAR-SPECIALS = ("?", "[", "]") +#let SHAPES = ( + "participant", + "actor", + "boundary", + "control", + "entity", + "database", + "collections", + "queue", + "custom" +) +#let DEFAULT-COLOR = rgb("#E2E2F0") + +#let _par( + name, + display-name: auto, + from-start: true, + invisible: false, + shape: "participant", + color: DEFAULT-COLOR, + line-stroke: ( + dash: "dashed", + paint: gray.darken(40%), + thickness: .5pt + ), + custom-image: none, + show-bottom: true, + show-top: true, +) = { + if color == auto { + color = DEFAULT-COLOR + } + return (( + type: "par", + draw: participant.render, + name: name, + display-name: if display-name == auto {name} else {display-name}, + from-start: from-start, + invisible: invisible, + shape: shape, + color: color, + line-stroke: line-stroke, + custom-image: custom-image, + show-bottom: show-bottom, + show-top: show-top + ),) +} + +#let _exists(participants, name) = { + if name in PAR-SPECIALS { + return true + } + + for p in participants { + if name == p.name { + return true + } + } + return false +} diff --git a/packages/preview/chronos/0.3.0/src/sequence.typ b/packages/preview/chronos/0.3.0/src/sequence.typ new file mode 100644 index 0000000000..c916996e38 --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/sequence.typ @@ -0,0 +1,53 @@ +#import "core/draw/sequence.typ" + +#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, + outer-lifeline-connect: false +) = { + return (( + type: "seq", + draw: sequence.render, + p1: p1, + p2: p2, + comment: comment, + comment-align: comment-align, + dashed: dashed, + start-tip: start-tip, + end-tip: end-tip, + color: color, + flip: flip, + enable-dst: enable-dst, + create-dst: create-dst, + disable-dst: disable-dst, + destroy-dst: destroy-dst, + disable-src: disable-src, + destroy-src: destroy-src, + lifeline-style: lifeline-style, + slant: slant, + outer-lifeline-connect: outer-lifeline-connect, + linked-notes: () + ),) +} + +#let _ret(comment: none) = { + return (( + type: "ret", + comment: comment + ),) +} diff --git a/packages/preview/chronos/0.3.0/src/syntax.ebnf b/packages/preview/chronos/0.3.0/src/syntax.ebnf new file mode 100644 index 0000000000..359cc9bced --- /dev/null +++ b/packages/preview/chronos/0.3.0/src/syntax.ebnf @@ -0,0 +1,29 @@ +lowercase-letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | + "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | + "u" | "v" | "w" | "x" | "y" | "z"; +uppercase-letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | + "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "T" | "S" | "T" | + "U" | "V" | "W" | "X" | "Y" | "Z"; +digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; + +(* Arrows *) +ltr-arrow = ["o" | "x"] ("-" | "--") right-tip ["o" | "x"]; +rtl-arrow = ["o" | "x"] left-tip ("-" | "--") ["o" | "x"]; +double-arrow = ["o" | "x"] left-tip ("-" | "--") right-tip ["o" | "x"]; +tip = "\\" | "\" | "/" | "//"; +left-tip = "<<" | "<" | tip; +right-tip = ">>" | ">" | tip; +arrow = ltr-arrow | rtl-arrow | double-arrow; + +id-char = lowercase-letter | uppercase-letter | digit | "_"; +id = id-char {id-char}; +participant-name = id ["as" id]; + +color = "#"; + +whitespace = " " | "\t" ; +space = whitespace {whitespace}; + +shape = "participant" | "actor" | "boundary" | "control" | "entity" | "database" | "collections" | "queue" | "custom"; +par-stmt = shape space participant-name [space color]; +seq-stmt = (participant-name | "?" | "[") [space] arrow [space] (participant-name | "?" | "]"); diff --git a/packages/preview/chronos/0.3.0/typst.toml b/packages/preview/chronos/0.3.0/typst.toml new file mode 100644 index 0000000000..9d3ea55699 --- /dev/null +++ b/packages/preview/chronos/0.3.0/typst.toml @@ -0,0 +1,14 @@ +[package] +name = "chronos" +version = "0.3.0" +compiler = "0.14.2" +repository = "https://git.kb28.ch/HEL/chronos" +entrypoint = "src/lib.typ" +authors = [ + "Louis Heredero " +] +categories = ["visualization"] +license = "Apache-2.0" +description = "A package to draw sequence diagrams with CeTZ" +keywords = ["sequence", "diagram", "plantuml"] +exclude = [ "gallery", "justfile", "docs", "manual.pdf", "manual.typ" ]