diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..ce678b7 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,31 @@ +name: CI +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + tests: + runs-on: ubuntu-latest + container: catthehacker/ubuntu:rust-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install tytanic + run: cargo binstall tytanic@0.3.3 + + - name: Run test suite + run: tt run + + - name: Archive artifacts + uses: https://gitea.com/actions/gitea-upload-artifact@v4 + if: always() + with: + name: artifacts + path: | + tests/**/diff/*.png + tests/**/out/*.png + tests/**/ref/*.png + retention-days: 5 \ No newline at end of file diff --git a/README.md b/README.md index 08ce598..db751c8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This package lets you render sequence diagrams directly in Typst. The following ```typst -#import "@preview/chronos:0.2.1" +#import "@preview/chronos:0.3.0" #chronos.diagram({ import chronos: * _par("Alice") diff --git a/docs/notes.typ b/docs/notes.typ index 3b783eb..3c4b72e 100644 --- a/docs/notes.typ +++ b/docs/notes.typ @@ -5,13 +5,15 @@ /// - 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 + aligned: false, + allow-overlap: true ) = {} /// Accepted values for `shape` argument of @@_note() diff --git a/docs/participants.typ b/docs/participants.typ index 30db854..6610c0d 100644 --- a/docs/participants.typ +++ b/docs/participants.typ @@ -34,6 +34,7 @@ /// - 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 @@ -45,6 +46,11 @@ 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, diff --git a/docs/sequences.typ b/docs/sequences.typ index cc5fc1d..d1f9b37 100644 --- a/docs/sequences.typ +++ b/docs/sequences.typ @@ -1,7 +1,8 @@ /// Manually adds an event to the given participant /// - participant (str): The participant concerned by the event /// - event (str): The event type (see @@EVENTS for ccepted values) -#let _evt(participant, event) = {} +/// - lifeline-style (auto, dict): See @@_seq() +#let _evt(participant, event, lifeline-style: auto) = {} /// Creates a sequence / message between two participants /// - p1 (str): Start participant @@ -21,6 +22,7 @@ /// - 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, @@ -39,7 +41,8 @@ disable-src: false, destroy-src: false, lifeline-style: auto, - slant: none + slant: none, + outer-lifeline-connect: false ) = {} /// Creates a return sequence diff --git a/gallery/example1.pdf b/gallery/example1.pdf index 17fe065..7d10576 100644 Binary files a/gallery/example1.pdf and b/gallery/example1.pdf differ diff --git a/gallery/example2.pdf b/gallery/example2.pdf index c13a497..d1927e5 100644 Binary files a/gallery/example2.pdf and b/gallery/example2.pdf differ diff --git a/gallery/example2.typ b/gallery/example2.typ index 9a49d5d..1c41e87 100644 --- a/gallery/example2.typ +++ b/gallery/example2.typ @@ -4,7 +4,7 @@ #chronos.diagram({ import chronos: * _seq("User", "A", comment: "DoWork", enable-dst: true) - _seq("A", "B", comment: [#sym.quote.angle.l createRequest #sym.quote.angle.r], 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) @@ -15,7 +15,7 @@ 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.angle.l createRequest #sym.quote.angle.r], enable-dst: true) + _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) }) diff --git a/gallery/example3.pdf b/gallery/example3.pdf index b6ee5b1..db0d82b 100644 Binary files a/gallery/example3.pdf and b/gallery/example3.pdf differ diff --git a/gallery/notes.pdf b/gallery/notes.pdf index df8c796..c47b3ac 100644 Binary files a/gallery/notes.pdf and b/gallery/notes.pdf differ diff --git a/gallery/readme/lifelines.png b/gallery/readme/lifelines.png index 2e33758..cc72c36 100644 Binary files a/gallery/readme/lifelines.png and b/gallery/readme/lifelines.png differ diff --git a/gallery/readme/simple_sequence.png b/gallery/readme/simple_sequence.png index e55bc0d..eedde29 100644 Binary files a/gallery/readme/simple_sequence.png and b/gallery/readme/simple_sequence.png differ diff --git a/justfile b/justfile index de19b92..5ad4926 100644 --- a/justfile +++ b/justfile @@ -4,9 +4,22 @@ 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 \ No newline at end of file + 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/manual.pdf b/manual.pdf index b3a1bf0..8729fb0 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/manual.typ b/manual.typ index 65dc6e5..2876163 100644 --- a/manual.typ +++ b/manual.typ @@ -71,7 +71,7 @@ $import = Examples -You can find the following examples and more in the #link("https://git.kb28.ch/HEL/circuiteria/src/branch/main/gallery")[gallery] directory +You can find the following examples and more in the #link("https://git.kb28.ch/HEL/chronos/src/branch/main/gallery")[gallery] directory == Some groups and sequences diff --git a/src/cetz.typ b/src/cetz.typ new file mode 100644 index 0000000..670e565 --- /dev/null +++ b/src/cetz.typ @@ -0,0 +1 @@ +#import "@preview/cetz:0.4.2": * \ No newline at end of file diff --git a/src/core/draw/delay.typ b/src/core/draw/delay.typ new file mode 100644 index 0000000..3b0a000 --- /dev/null +++ b/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/src/core/draw/event.typ b/src/core/draw/event.typ new file mode 100644 index 0000000..bc39297 --- /dev/null +++ b/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/src/core/draw/group.typ b/src/core/draw/group.typ new file mode 100644 index 0000000..a182594 --- /dev/null +++ b/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/src/core/draw/note.typ b/src/core/draw/note.typ new file mode 100644 index 0000000..33b33aa --- /dev/null +++ b/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/src/core/draw/participant.typ b/src/core/draw/participant.typ new file mode 100644 index 0000000..d7c87e3 --- /dev/null +++ b/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/src/core/draw/separator.typ b/src/core/draw/separator.typ new file mode 100644 index 0000000..ecc0db2 --- /dev/null +++ b/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/src/core/draw/sequence.typ b/src/core/draw/sequence.typ new file mode 100644 index 0000000..b1f0659 --- /dev/null +++ b/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/src/core/draw/sync.typ b/src/core/draw/sync.typ new file mode 100644 index 0000000..050fe90 --- /dev/null +++ b/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/src/core/renderer.typ b/src/core/renderer.typ new file mode 100644 index 0000000..14cf8d5 --- /dev/null +++ b/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/src/core/setup.typ b/src/core/setup.typ new file mode 100644 index 0000000..776b2bb --- /dev/null +++ b/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/src/utils.typ b/src/core/utils.typ similarity index 70% rename from src/utils.typ rename to src/core/utils.typ index e8d750d..0dd9ee9 100644 --- a/src/utils.typ +++ b/src/core/utils.typ @@ -1,3 +1,15 @@ +#import "/src/cetz.typ": draw + +#let is-elmt(elmt) = { + if type(elmt) != dictionary { + return false + } + if "type" not in elmt { + return false + } + return true +} + #let normalize-units(value) = { if type(value) == int or type(value) == float { return value @@ -30,7 +42,7 @@ 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" { + } 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) @@ -79,4 +91,27 @@ 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/src/diagram.typ b/src/diagram.typ index 0e78117..a16809e 100644 --- a/src/diagram.typ +++ b/src/diagram.typ @@ -1,249 +1,14 @@ -#import "utils.typ": get-group-span, fit-canvas -#import "renderer.typ": render -#import "participant.typ" as participant: _par, PAR-SPECIALS -#import "sequence.typ": _seq - -#let _gap(size: 20) = { - return (( - type: "gap", - size: size - ),) -} - -#let _evt(participant, event) = { - return (( - type: "evt", - participant: participant, - event: event, - lifeline-style: auto - ),) -} - -#let _col(p1, p2, width: auto, margin: 0, min-width: 0, max-width: none) = { - return (( - type: "col", - p1: p1, - p2: p2, - width: width, - margin: margin, - min-width: min-width, - max-width: max-width - ),) -} +#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 participants = () - let elmts = elements - let i = 0 - - let activation-history = () - - // Flatten groups + convert returns - while i < elmts.len() { - let elmt = elmts.at(i) - if elmt.type == "grp" { - elmt.elmts = elmt.elmts.map(e => { - if e.type == "seq" { - if e.p1 == "?" { - e.p1 = "?" + e.p2 - } else if e.p2 == "?" { - e.p2 = e.p1 + "?" - } - } - e - }) - elmts.at(i) = elmt - elmts = ( - elmts.slice(0, i + 1) + - elmt.elmts + - (( - type: "grp-end", - start-i: i - ),) + - elmts.slice(i+1) - ) - } else if elmt.type == "grp-end" { - // Put back elements in group because they might have changed - elmts.at(elmt.start-i).elmts = elmts.slice(elmt.start-i + 1, 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" { - if activation-history.len() == 0 { - panic("Cannot return if no lifeline is activated") - } - let seq = activation-history.pop() - elmts.at(i) = _seq( - seq.p2, seq.p1, - comment: elmt.comment, - disable-src: true, - dashed: true - ).first() - } - i += 1 - } - - // List participants - let linked = () - let last-seq = none - let last-note = none - for (i, elmt) in elmts.enumerate() { - if elmt.type == "par" { - participants.push(elmt) - } else if elmt.type == "seq" { - if not participant._exists(participants, elmt.p1) { - participants.push(_par(elmt.p1).first()) - } - if not participant._exists(participants, elmt.p2) { - let par = _par(elmt.p2, from-start: not elmt.create-dst).first() - participants.push(par) - - } else if elmt.create-dst { - let i = participants.position(p => p.name == elmt.p2) - participants.at(i).from-start = false - } - - let p1 = elmt.p1 - let p2 = elmt.p2 - if elmt.p1 == "?" { - p1 = "?" + elmt.p2 - } - if elmt.p2 == "?" { - p2 = elmt.p1 + "?" - } - linked.push(p1) - linked.push(p2) - last-seq = ( - elmt: elmt, - i: i, - p1: p1, - p2: p2 - ) - } else if elmt.type == "note" { - elmt.insert("linked", elmt.pos == none and elmt.side != "across") - if elmt.pos == none and elmt.side != "across" { - let names = participants.map(p => p.name) - let i1 = names.position(n => n == last-seq.p1) - let i2 = names.position(n => n == last-seq.p2) - let pars = ((i1, last-seq.p1), (i2, last-seq.p2)).sorted(key: p => p.first()) - if elmt.side == "left" { - elmt.pos = pars.first().last() - } else if elmt.side == "right" { - elmt.pos = pars.last().last() - } - - let seq = last-seq.elmt - seq.insert("linked-note", elmt) - elmts.at(last-seq.i) = seq - } - if elmt.aligned { - let n = last-note.elmt - n.aligned-with = elmt - elmts.at(last-note.i) = n - } - elmts.at(i) = elmt - if elmt.side == "left" { - linked.push("[") - } else if elmt.side == "right" { - linked.push("]") - } - - let pars = none - if type(elmt.pos) == str { - pars = (elmt.pos,) - } else if type(elmt.pos) == array { - pars = elmt.pos - } - if pars != none { - for par in pars { - if not participant._exists(participants, par) { - participants.push(_par(par).first()) - } - } - } - - last-note = ( - elmt: elmt, - i: i - ) - } else if elmt.type == "evt" { - let par = elmt.participant - if not participant._exists(participants, par) { - let p = _par(par, from-start: elmt.event != "create").first() - participants.push(p) - - } else if elmt.event == "create" { - let i = participants.position(p => p.name == par) - participants.at(i).from-start = false - } - } - } - linked = linked.dedup() - - let pars = participants - participants = () - - if "[" in linked { - participants.push(_par("[", invisible: true).first()) - } - - for (i, p) in pars.enumerate() { - let before = _par("?" + p.name, invisible: true).first() - let after = _par(p.name + "?", invisible: true).first() - - if before.name in linked { - if participants.len() == 0 or not participants.last().name.ends-with("?") { - participants.push(before) - } else { - participants.insert(-1, before) - } - } - - participants.push(p) - - if after.name in linked { - participants.push(after) - } - } - if "]" in linked { - participants.push(_par("]", invisible: true).first()) - } - - // Add index to participant - for (i, p) in participants.enumerate() { - p.insert("i", i) - participants.at(i) = p - } - - // Compute groups spans (horizontal) - for (i, elmt) in elmts.enumerate() { - if elmt.type == "grp" or elmt.type == "alt" { - let (min-i, max-i) = get-group-span(participants, elmt) - elmts.at(i).insert("min-i", min-i) - elmts.at(i).insert("max-i", max-i) - } else if elmt.type == "seq" { - if elmt.p1 == "?" { - elmts.at(i).p1 = "?" + elmt.p2 - } else if elmt.p2 == "?" { - elmts.at(i).p2 = elmt.p1 + "?" - } - } - } + let (elmts, participants) = setup(elements) let canvas = render(participants, elmts) fit-canvas(canvas, width: width) diff --git a/src/group.typ b/src/group.typ index db0933b..dc75065 100644 --- a/src/group.typ +++ b/src/group.typ @@ -1,9 +1,9 @@ -#import "/src/cetz.typ": draw -#import "consts.typ": * +#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, @@ -20,6 +20,7 @@ let else-elmts = args.at(i + 1, default: ()) all-elmts.push(( type: "else", + draw: group.render-else, desc: else-desc )) all-elmts += else-elmts @@ -38,62 +39,5 @@ } _grp(name, desc: desc, type: "loop", elmts) } -#let _opt(desc, elmts) = grp("opt", desc: desc, type: "opt", elmts) -#let _break(desc, elmts) = grp("break", desc: desc, type: "break", elmts) - -#let render(x0, x1, y0, y1, group) = { - let shapes = () - let name = text(group.name, weight: "bold") - let m = measure(box(name)) - let w = m.width / 1pt + 15 - let h = m.height / 1pt + 6 - shapes += draw.rect( - (x0, y0), - (x1, y1) - ) - shapes += draw.merge-path( - fill: COL-GRP-NAME, - close: true, - { - draw.line( - (x0, y0), - (x0 + w, y0), - (x0 + w, y0 - h / 2), - (x0 + w - 5, y0 - h), - (x0, y0 - h) - ) - } - ) - shapes += draw.content( - (x0, y0), - name, - anchor: "north-west", - padding: (left: 5pt, right: 10pt, top: 3pt, bottom: 3pt) - ) - - if group.desc != none { - shapes += draw.content( - (x0 + w, y0), - text([\[#group.desc\]], weight: "bold", size: .8em), - anchor: "north-west", - padding: 3pt - ) - } - - return shapes -} - -#let render-else(x0, x1, y, elmt) = { - let shapes = draw.line( - (x0, y), - (x1, y), - stroke: (dash: (2pt, 1pt), thickness: .5pt) - ) - shapes += draw.content( - (x0, y), - text([\[#elmt.desc\]], weight: "bold", size: .8em), - anchor: "north-west", - padding: 3pt - ) - return shapes -} \ No newline at end of file +#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/src/lib.typ b/src/lib.typ index 2575674..09d36a8 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,9 +1,8 @@ -#let version = version(0, 2, 1) -#import "diagram.typ": diagram, from-plantuml, _gap, _evt, _col +#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 "separator.typ": _sep, _delay -#import "note.typ": _note -#import "sync.typ": _sync \ No newline at end of file +#import "misc.typ": _sep, _delay, _sync, _gap, _evt, _col +#import "note.typ": _note \ No newline at end of file diff --git a/src/misc.typ b/src/misc.typ new file mode 100644 index 0000000..48e6986 --- /dev/null +++ b/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/src/note.typ b/src/note.typ index 14d3c98..01e6b31 100644 --- a/src/note.typ +++ b/src/note.typ @@ -1,5 +1,5 @@ -#import "/src/cetz.typ": draw #import "consts.typ": * +#import "core/draw/note.typ" #let SIDES = ( "left", @@ -14,7 +14,15 @@ "hex" ) -#let _note(side, content, pos: none, color: COL-NOTE, shape: "default", aligned: false) = { +#let _note( + side, + content, + pos: none, + color: COL-NOTE, + shape: "default", + aligned: false, + allow-overlap: true +) = { if side == "over" { if pos == none { panic("Pos cannot be none with side 'over'") @@ -30,156 +38,14 @@ } return (( type: "note", + draw: note.render, side: side, content: content, pos: pos, color: color, shape: shape, aligned: aligned, - aligned-with: none + aligned-with: none, + allow-overlap: allow-overlap ),) } - -#let get-note-box(note) = { - let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD} - let inset = ( - left: PAD.last() * 1pt, - right: PAD.last() * 1pt, - top: PAD.first() * 1pt, - bottom: PAD.first() * 1pt, - ) - if note.shape == "default" { - inset.right += NOTE-CORNER-SIZE * 1pt - } - if note.side == "left" { - inset.right += NOTE-GAP * 1pt - } else if note.side == "right" { - inset.left += NOTE-GAP * 1pt - } - return box(note.content, inset: inset) -} - -#let get-size(note) = { - let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD} - let m = measure(box(note.content)) - let w = m.width / 1pt + PAD.last() * 2 - let h = m.height / 1pt + PAD.first() * 2 - if note.shape == "default" { - w += NOTE-CORNER-SIZE - } - return ( - width: w, - height: h - ) -} - -#let _get-base-x(pars-i, x-pos, note) = { - if note.side == "across" { - return (x-pos.first() + x-pos.last()) / 2 - } - if note.side == "over" { - if type(note.pos) == array { - let xs = note.pos.map(par => x-pos.at(pars-i.at(par))) - return (calc.min(..xs) + calc.max(..xs)) / 2 - } - } - return x-pos.at(pars-i.at(note.pos)) -} - -#let render(pars-i, x-pos, note, y, lifelines) = { - let shapes = () - let PAD = if note.shape == "hex" {NOTE-HEX-PAD} else {NOTE-PAD} - let m = measure(box(note.content)) - let w = m.width / 1pt + PAD.last() * 2 - let h = m.height / 1pt + PAD.first() * 2 - let total-w = w - if note.shape == "default" { - total-w += NOTE-CORNER-SIZE - } - - let base-x = _get-base-x(pars-i, x-pos, note) - - let i = none - if note.pos != none and type(note.pos) == str { - i = pars-i.at(note.pos) - } - let x0 = base-x - if note.side == "left" { - x0 -= NOTE-GAP - x0 -= total-w - if lifelines.at(i).level != 0 { - x0 -= LIFELINE-W / 2 - } - } else if note.side == "right" { - x0 += NOTE-GAP - x0 += lifelines.at(i).level * LIFELINE-W / 2 - } else if note.side == "over" or note.side == "across" { - x0 -= total-w / 2 - } - - let x1 = x0 + w - let x2 = x0 + total-w - let y0 = y - - if note.linked { - y0 += h / 2 - } - let y1 = y0 - h - - if note.shape == "default" { - shapes += draw.merge-path( - stroke: black + .5pt, - fill: note.color, - close: true, - { - draw.line( - (x0, y0), - (x1, y0), - (x2, y0 - NOTE-CORNER-SIZE), - (x2, y1), - (x0, y1) - ) - } - ) - shapes += draw.line((x1, y0), (x1, y0 - NOTE-CORNER-SIZE), (x2, y0 - NOTE-CORNER-SIZE), stroke: black + .5pt) - } else if note.shape == "rect" { - shapes += draw.rect( - (x0, y0), - (x2, y1), - stroke: black + .5pt, - fill: note.color - ) - } else if note.shape == "hex" { - let lx = x0 + PAD.last() - let rx = x2 - PAD.last() - let my = (y0 + y1) / 2 - shapes += draw.merge-path( - stroke: black + .5pt, - fill: note.color, - close: true, - { - draw.line( - (lx, y0), - (rx, y0), - (x2, my), - (rx, y1), - (lx, y1), - (x0, my), - ) - } - ) - } - - shapes += draw.content( - ((x0 + x1)/2, (y0 + y1)/2), - note.content, - anchor: "center" - ) - - if note.aligned-with == none and (note.pos != none or note.side == "across") { - y -= h - } - - let r = (y, shapes) - return r -} \ No newline at end of file diff --git a/src/participant.typ b/src/participant.typ index a9e5572..1a4b4b2 100644 --- a/src/participant.typ +++ b/src/participant.typ @@ -1,7 +1,6 @@ -#import "/src/cetz.typ": draw -#import "consts.typ": * +#import "core/draw/participant.typ" -#let PAR-SPECIALS = "?[]" +#let PAR-SPECIALS = ("?", "[", "]") #let SHAPES = ( "participant", "actor", @@ -22,6 +21,11 @@ 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, @@ -31,12 +35,14 @@ } 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 @@ -44,7 +50,7 @@ } #let _exists(participants, name) = { - if name == "?" or name == "[" or name == "]" { + if name in PAR-SPECIALS { return true } @@ -55,307 +61,3 @@ } return false } - -#let get-size(par) = { - if par.invisible { - return (width: 0pt, height: 0pt) - } - let m = measure(box(par.display-name)) - let w = m.width - let h = m.height - let (shape-w, shape-h) = ( - participant: (w + PAR-PAD.last() * 2, h + PAR-PAD.first() * 2), - actor: (ACTOR-WIDTH * 1pt, ACTOR-WIDTH * 2pt + SYM-GAP * 1pt + h), - boundary: (BOUNDARY-HEIGHT * 2pt, BOUNDARY-HEIGHT * 1pt + SYM-GAP * 1pt + h), - control: (CONTROL-HEIGHT * 1pt, CONTROL-HEIGHT * 1pt + SYM-GAP * 1pt + h), - entity: (ENTITY-HEIGHT * 1pt, ENTITY-HEIGHT * 1pt + 2pt + SYM-GAP * 1pt + h), - database: (DATABASE-WIDTH * 1pt, DATABASE-WIDTH * 4pt / 3 + SYM-GAP * 1pt + h), - collections: ( - w + COLLECTIONS-PAD.last() * 2 + calc.abs(COLLECTIONS-DX) * 1pt, - h + COLLECTIONS-PAD.first() * 2 + calc.abs(COLLECTIONS-DY) * 1pt, - ), - queue: ( - w + QUEUE-PAD.last() * 2 + 3 * (h + QUEUE-PAD.first() * 2) / 4, - h + QUEUE-PAD.first() * 2 - ), - custom: ( - measure(par.custom-image).width, - measure(par.custom-image).height + SYM-GAP * 1pt + h - ) - ).at(par.shape) - - return ( - width: calc.max(w, shape-w), - height: calc.max(h, shape-h) - ) -} - -#let _render-participant(x, y, p, m, bottom) = { - let w = m.width / 1pt - let h = m.height / 1pt - let x0 = x - w / 2 - PAR-PAD.last() / 1pt - let x1 = x + w / 2 + PAR-PAD.last() / 1pt - let y0 = y + h + PAR-PAD.first() / 1pt * 2 - if bottom { - y0 = y - } - let y1 = y0 - h - PAR-PAD.first() / 1pt * 2 - - draw.rect( - (x0, y0), - (x1, y1), - radius: 2pt, - fill: p.color, - stroke: black + .5pt - ) - draw.content( - ((x0 + x1) / 2, (y0 + y1) / 2), - p.display-name, - anchor: "center" - ) -} - -#let _render-actor(x, y, p, m, bottom) = { - let w2 = ACTOR-WIDTH / 2 - let head-r = ACTOR-WIDTH / 4 - let height = ACTOR-WIDTH * 2 - let arms-y = height * 0.375 - - let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + height + SYM-GAP} - draw.circle( - (x, y0 - head-r), - radius: head-r, - fill: p.color, - stroke: black + .5pt - ) - draw.line((x, y0 - head-r * 2), (x, y0 - height + w2), stroke: black + .5pt) - draw.line((x - w2, y0 - arms-y), (x + w2, y0 - arms-y), stroke: black + .5pt) - draw.line((x - w2, y0 - height), (x, y0 - height + w2), (x + w2, y0 - height), stroke: black + .5pt) - draw.content( - (x, y), - p.display-name, - anchor: if bottom {"north"} else {"south"} - ) -} - -#let _render-boundary(x, y, p, m, bottom) = { - let circle-r = BOUNDARY-HEIGHT / 2 - let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + BOUNDARY-HEIGHT + SYM-GAP} - let x0 = x - BOUNDARY-HEIGHT - let y1 = y0 - circle-r - let y2 = y0 - BOUNDARY-HEIGHT - - draw.circle( - (x + circle-r, y1), - radius: circle-r, - fill: p.color, - stroke: black + .5pt - ) - draw.line( - (x0, y0), (x0, y2), - stroke: black + .5pt - ) - draw.line( - (x0, y1), (x, y1), - stroke: black + .5pt - ) - draw.content( - (x, y), - p.display-name, - anchor: if bottom {"north"} else {"south"} - ) -} - -#let _render-control(x, y, p, m, bottom) = { - let r = CONTROL-HEIGHT / 2 - let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + CONTROL-HEIGHT + SYM-GAP} - - draw.circle( - (x, y0 - r), - radius: r, - fill: p.color, - stroke: black + .5pt - ) - draw.mark((x, y0), (x - r / 2, y0), symbol: "stealth", fill: black) - draw.content( - (x, y), - p.display-name, - anchor: if bottom {"north"} else {"south"} - ) -} - -#let _render-entity(x, y, p, m, bottom) = { - let r = ENTITY-HEIGHT / 2 - let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + ENTITY-HEIGHT + SYM-GAP} - let y1 = y0 - ENTITY-HEIGHT - 1.5 - - draw.circle( - (x, y0 - r), - radius: r, - fill: p.color, - stroke: black + .5pt - ) - draw.line( - (x - r, y1), - (x + r, y1), - stroke: black + .5pt - ) - draw.content( - (x, y), - p.display-name, - anchor: if bottom {"north"} else {"south"} - ) -} - -#let _render-database(x, y, p, m, bottom) = { - let height = DATABASE-WIDTH * 4 / 3 - let rx = DATABASE-WIDTH / 2 - let ry = rx / 2 - let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + height + SYM-GAP} - let y1 = y0 - height - - draw.merge-path( - close: true, - fill: p.color, - stroke: black + .5pt, - { - draw.bezier((x - rx, y0 - ry), (x, y0), (x - rx, y0 - ry/2), (x - rx/2, y0)) - draw.bezier((), (x + rx, y0 - ry), (x + rx/2, y0), (x + rx, y0 - ry/2)) - draw.line((), (x + rx, y1 + ry)) - draw.bezier((), (x, y1), (x + rx, y1 + ry/2), (x + rx/2, y1)) - draw.bezier((), (x - rx, y1 + ry), (x - rx/2, y1), (x - rx, y1 + ry/2)) - } - ) - draw.merge-path( - stroke: black + .5pt, - { - draw.bezier((x - rx, y0 - ry), (x, y0 - ry*2), (x - rx, y0 - 3*ry/2), (x - rx/2, y0 - ry*2)) - draw.bezier((), (x + rx, y0 - ry), (x + rx/2, y0 - ry*2), (x + rx, y0 - 3*ry/2)) - } - ) - draw.content( - (x, y), - p.display-name, - anchor: if bottom {"north"} else {"south"} - ) -} - -#let _render-collections(x, y, p, m, bottom) = { - let w = m.width / 1pt - let h = m.height / 1pt - let dx = COLLECTIONS-DX - let dy = COLLECTIONS-DY - let total-w = w + PAR-PAD.last() * 2 / 1pt + calc.abs(dx) - let total-h = h + PAR-PAD.first() * 2 / 1pt + calc.abs(dy) - - let x0 = x - total-w / 2 - let x1 = x0 + calc.abs(dx) - let x3 = x0 + total-w - let x2 = x3 - calc.abs(dx) - - let y0 = if bottom {y} else {y + total-h} - let y1 = y0 - calc.abs(dy) - let y3 = y0 - total-h - let y2 = y3 + calc.abs(dy) - - let r1 = (x1, y0, x3, y2) - let r2 = (x0, y1, x2, y3) - - if dx < 0 { - r1.at(0) = x0 - r1.at(2) = x2 - r2.at(0) = x1 - r2.at(2) = x3 - } - - if dy < 0 { - r1.at(1) = y1 - r1.at(3) = y3 - r2.at(1) = y0 - r2.at(3) = y2 - } - draw.rect( - (r1.at(0), r1.at(1)), - (r1.at(2), r1.at(3)), - fill: p.color, - stroke: black + .5pt - ) - draw.rect( - (r2.at(0), r2.at(1)), - (r2.at(2), r2.at(3)), - fill: p.color, - stroke: black + .5pt - ) - - draw.content( - ((r2.at(0) + r2.at(2)) / 2, (r2.at(1) + r2.at(3)) / 2), - p.display-name, - anchor: "center" - ) -} - -#let _render-queue(x, y, p, m, bottom) = { - let w = (m.width + QUEUE-PAD.last() * 2) / 1pt - let h = (m.height + QUEUE-PAD.first() * 2) / 1pt - let total-h = h - let ry = total-h / 2 - let rx = ry / 2 - let total-w = w + 3 + 3 * rx - - let x0 = x - total-w / 2 - let y0 = if bottom {y} else {y + total-h} - let y1 = y0 - total-h - let x-left = x0 + rx - let x-right = x-left + w + rx - draw.merge-path( - close: true, - fill: p.color, - stroke: black + .5pt, - { - draw.bezier((x-right, y0), (x-right + rx, y0 - ry), (x-right + rx/2, y0), (x-right + rx, y0 - ry/2)) - draw.bezier((), (x-right, y1), (x-right + rx, y1 + ry/2), (x-right + rx/2, y1)) - draw.line((), (x-left, y1)) - draw.bezier((), (x-left - rx, y0 - ry), (x-left - rx/2, y1), (x-left - rx, y1 + ry/2)) - draw.bezier((), (x-left, y0), (x-left - rx, y0 - ry/2), (x-left - rx/2, y0)) - } - ) - draw.merge-path( - stroke: black + .5pt, - { - draw.bezier((x-right, y0), (x-right - rx, y0 - ry), (x-right - rx/2, y0), (x-right - rx, y0 - ry/2)) - draw.bezier((), (x-right, y1), (x-right - rx, y1 + ry/2), (x-right - rx/2, y1)) - } - ) - draw.content( - ((x-left + x-right - rx) / 2, y0 - ry), - p.display-name, - anchor: "center" - ) -} - -#let _render-custom(x, y, p, m, bottom) = { - let image-m = measure(p.custom-image) - let y0 = if bottom {y - m.height / 1pt - SYM-GAP} else {y + m.height / 1pt + image-m.height / 1pt + SYM-GAP} - draw.content((x - image-m.width / 2pt, y0), p.custom-image, anchor: "north-west") - draw.content( - (x, y), - p.display-name, - anchor: if bottom {"north"} else {"south"} - ) -} - -#let render(x-pos, p, y: 0, bottom: false) = { - let m = measure(box(p.display-name)) - let func = ( - participant: _render-participant, - actor: _render-actor, - boundary: _render-boundary, - control: _render-control, - entity: _render-entity, - database: _render-database, - collections: _render-collections, - queue: _render-queue, - custom: _render-custom, - ).at(p.shape) - func(x-pos.at(p.i), y, p, m, bottom) -} \ No newline at end of file diff --git a/src/renderer.typ b/src/renderer.typ deleted file mode 100644 index 4f70d02..0000000 --- a/src/renderer.typ +++ /dev/null @@ -1,522 +0,0 @@ -#import "/src/cetz.typ": canvas, draw -#import "utils.typ": get-participants-i, get-style, normalize-units -#import "group.typ" -#import "participant.typ" -#import participant: PAR-SPECIALS -#import "sequence.typ" -#import "separator.typ" -#import "sync.typ" -#import "consts.typ": * -#import "note.typ" as note: get-note-box - -#let DEBUG-INVISIBLE = false - -#let get-columns-width(participants, elements) = { - participants = participants.map(p => { - p.insert("lifeline-lvl", 0) - p.insert("max-lifelines", 0) - p - }) - let pars-i = get-participants-i(participants) - let cells = () - - // Unwrap syncs - let i = 0 - while i < elements.len() { - let elmt = elements.at(i) - if elmt.type == "sync" { - elements = elements.slice(0, i + 1) + elmt.elmts + elements.slice(i + 1) - } - i += 1 - } - - // Compute max lifeline levels - for elmt in elements { - if elmt.type == "seq" { - let com = if elmt.comment == none {""} else {elmt.comment} - let i1 = pars-i.at(elmt.p1) - let i2 = pars-i.at(elmt.p2) - cells.push( - ( - elmt: elmt, - i1: calc.min(i1, i2), - i2: calc.max(i1, i2), - cell: box(com, inset: 3pt) - ) - ) - - if elmt.disable-src or elmt.destroy-src { - let p = participants.at(i1) - p.lifeline-lvl -= 1 - participants.at(i1) = p - } - if elmt.disable-dst { - let p = participants.at(i2) - p.lifeline-lvl -= 1 - participants.at(i2) = p - } - if elmt.enable-dst { - let p = participants.at(i2) - p.lifeline-lvl += 1 - p.max-lifelines = calc.max(p.max-lifelines, p.lifeline-lvl) - participants.at(i2) = p - } - } else if elmt.type == "evt" { - let par-name = elmt.participant - let i = pars-i.at(par-name) - let par = participants.at(i) - if elmt.event == "disable" or elmt.event == "destroy" { - par.lifeline-lvl -= 1 - - } else if elmt.event == "enable" { - par.lifeline-lvl += 1 - par.max-lifelines = calc.max(par.max-lifelines, par.lifeline-lvl) - } - participants.at(i) = par - - } else if elmt.type == "note" { - let (p1, p2) = (none, none) - let cell = none - if elmt.side == "left" { - p1 = "[" - p2 = elmt.pos - cell = get-note-box(elmt) - } else if elmt.side == "right" { - p1 = elmt.pos - p2 = "]" - cell = get-note-box(elmt) - } else if elmt.side == "over" { - if elmt.aligned-with != none { - let box1 = get-note-box(elmt) - let box2 = get-note-box(elmt.aligned-with) - let m1 = measure(box1) - let m2 = measure(box2) - cell = box(width: (m1.width + m2.width) / 2, height: calc.max(m1.height, m2.height)) - p1 = elmt.pos - p2 = elmt.aligned-with.pos - } - } - - if p1 != none and p2 != none and cell != none { - let i1 = pars-i.at(p1) - let i2 = pars-i.at(p2) - cells.push( - ( - elmt: elmt, - i1: calc.min(i1, i2), - i2: calc.max(i1, i2), - cell: cell - ) - ) - } - } - } - - // Compute column widths - // Compute minimum widths for participant names and shapes - let widths = () - for i in range(participants.len() - 1) { - let p1 = participants.at(i) - let p2 = participants.at(i + 1) - let m1 = participant.get-size(p1) - let m2 = participant.get-size(p2) - let w1 = m1.width - let w2 = m2.width - widths.push(w1 / 2pt + w2 / 2pt + PAR-SPACE) - } - - // Compute minimum width for over notes - for n in elements.filter(e => (e.type == "note" and - e.side == "over" and - type(e.pos) == str)) { - - let m = note.get-size(n) - let i = pars-i.at(n.pos) - - if i < widths.len() { - widths.at(i) = calc.max( - widths.at(i), - m.width / 2 + NOTE-GAP - ) - } - if i > 0 { - widths.at(i - 1) = calc.max( - widths.at(i - 1), - m.width / 2 + NOTE-GAP - ) - } - } - - // Compute minimum width for simple sequences (spanning 1 column) - for cell in cells.filter(c => c.i2 - c.i1 == 1) { - let m = measure(cell.cell) - widths.at(cell.i1) = calc.max( - widths.at(cell.i1), - m.width / 1pt + COMMENT-PAD - ) - } - - // Compute minimum width for self sequences - for cell in cells.filter(c => c.elmt.type == "seq" and c.i1 == c.i2) { - let m = measure(cell.cell) - let i = cell.i1 - if cell.elmt.flip { - i -= 1 - } - if 0 <= i and i < widths.len() { - widths.at(i) = calc.max( - widths.at(i), - m.width / 1pt + COMMENT-PAD - ) - } - } - - // Compute remaining widths for longer sequences (spanning multiple columns) - let multicol-cells = cells.filter(c => c.i2 - c.i1 > 1) - multicol-cells = multicol-cells.sorted(key: c => { - c.i1 * 1000 + c.i2 - }) - for cell in multicol-cells { - let m = measure(cell.cell) - widths.at(cell.i2 - 1) = calc.max( - widths.at(cell.i2 - 1), - m.width / 1pt + COMMENT-PAD - widths.slice(cell.i1, cell.i2 - 1).sum() - ) - } - - // Add lifeline widths - for (i, w) in widths.enumerate() { - let p1 = participants.at(i) - let p2 = participants.at(i + 1) - let w = w + p1.max-lifelines * LIFELINE-W / 2 - if p2.max-lifelines != 0 { - w += LIFELINE-W / 2 - } - widths.at(i) = w - } - - for elmt in elements { - if elmt.type == "col" { - let i1 = pars-i.at(elmt.p1) - let i2 = pars-i.at(elmt.p2) - if calc.abs(i1 - i2) != 1 { - let i-min = calc.min(i1, i2) - let i-max = calc.max(i1, i2) - let others = pars-i.pairs() - .sorted(key: p => p.last()) - .slice(i-min + 1, i-max) - .map(p => "'" + p.first() + "'") - .join(", ") - panic( - "Column participants must be consecutive (participants (" + - others + - ") are in between)" - ) - } - let i = calc.min(i1, i2) - - if elmt.width != auto { - widths.at(i) = normalize-units(elmt.width) - } - - let width = widths.at(i) - width = calc.max(width, normalize-units(elmt.min-width)) - if elmt.max-width != none { - width = calc.min(width, normalize-units(elmt.max-width)) - } - widths.at(i) = width + normalize-units(elmt.margin) - } - } - - return widths -} - -#let render(participants, elements) = context canvas(length: 1pt, { - let shapes = () - let pars-i = get-participants-i(participants) - - let widths = get-columns-width(participants, elements) - - // Compute each column's X position - let x-pos = (0,) - for width in widths { - x-pos.push(x-pos.last() + width) - } - - let draw-seq = sequence.render.with(pars-i, x-pos, participants) - let draw-group = group.render.with() - let draw-else = group.render-else.with() - let draw-sep = separator.render.with(x-pos) - let draw-par = participant.render.with(x-pos) - let draw-note = note.render.with(pars-i, x-pos) - let draw-sync = sync.render.with(pars-i, x-pos, participants) - - // Draw participants (start) - for p in participants { - if p.from-start and not p.invisible and p.show-top { - shapes += draw-par(p) - } - } - - let y = 0 - let groups = () - let lifelines = participants.map(_ => ( - level: 0, - lines: () - )) - - // Draw elemnts - for elmt in elements { - // Sequences - if elmt.type == "seq" { - let shps - (y, lifelines, shps) = draw-seq(elmt, y, lifelines) - shapes += shps - - // Groups (start) -> reserve space for labels + store position - } else if elmt.type == "grp" { - y -= Y-SPACE - let m = measure( - box( - elmt.name, - inset: (left: 5pt, right: 5pt, top: 3pt, bottom: 3pt), - ) - ) - groups = groups.map(g => { - if g.at(1).min-i == elmt.min-i { g.at(2) += 1 } - if g.at(1).max-i == elmt.max-i { g.at(3) += 1 } - g - }) - if elmt.grp-type == "alt" { - elmt.insert("elses", ()) - } - groups.push((y, elmt, 0, 0)) - y -= m.height / 1pt - - // Groups (end) -> actual drawing - } else if elmt.type == "grp-end" { - y -= Y-SPACE - let (start-y, group, start-lvl, end-lvl) = groups.pop() - let x0 = x-pos.at(group.min-i) - start-lvl * 10 - 20 - let x1 = x-pos.at(group.max-i) + end-lvl * 10 + 20 - shapes += draw-group(x0, x1, start-y, y, group) - - if group.grp-type == "alt" { - for (else-y, else-elmt) in group.elses { - shapes += draw-else(x0, x1, else-y, else-elmt) - } - } - - // Alt's elses -> reserve space for label + store position - } else if elmt.type == "else" { - y -= Y-SPACE - let m = measure(text([\[#elmt.desc\]], weight: "bold", size: .8em)) - groups.last().at(1).elses.push(( - y, elmt - )) - y -= m.height / 1pt - - // Separator - } else if elmt.type == "sep" { - let shps - (y, shps) = draw-sep(elmt, y) - shapes += shps - - // Gap - } else if elmt.type == "gap" { - y -= elmt.size - - // Delay - } else if elmt.type == "delay" { - let y0 = y - let y1 = y - elmt.size - for (i, line) in lifelines.enumerate() { - line.lines.push(("delay-start", y0)) - line.lines.push(("delay-end", y1)) - lifelines.at(i) = line - } - if elmt.name != none { - let x0 = x-pos.first() - let x1 = x-pos.last() - shapes += draw.content( - ((x0 + x1) / 2, (y0 + y1) / 2), - anchor: "center", - elmt.name - ) - } - y = y1 - - // Event - } else if elmt.type == "evt" { - let par-name = elmt.participant - let i = pars-i.at(par-name) - let par = participants.at(i) - let line = lifelines.at(i) - if elmt.event == "disable" { - line.level -= 1 - line.lines.push(("disable", y)) - - } else if elmt.event == "destroy" { - line.lines.push(("destroy", y)) - - } else if elmt.event == "enable" { - line.level += 1 - line.lines.push(("enable", y, elmt.lifeline-style)) - - } else if elmt.event == "create" { - y -= CREATE-OFFSET - shapes += participant.render(x-pos, par, y: y) - line.lines.push(("create", y)) - } - lifelines.at(i) = line - - // Note - } else if elmt.type == "note" { - if not elmt.linked { - if not elmt.aligned { - y -= Y-SPACE - } - let shps - (y, shps) = draw-note(elmt, y, lifelines) - shapes += shps - } - - // Synched sequences - } else if elmt.type == "sync" { - let shps - (y, lifelines, shps) = draw-sync(elmt, y, lifelines) - shapes += shps - } - } - - y -= Y-SPACE - - // Draw vertical lines + lifelines + end participants - shapes += draw.on-layer(-1, { - if DEBUG-INVISIBLE { - for p in participants.filter(p => p.invisible) { - let color = if p.name.starts-with("?") {green} else if p.name.ends-with("?") {red} else {blue} - let x = x-pos.at(p.i) - draw.line( - (x, 0), - (x, y), - stroke: (paint: color, dash: "dotted") - ) - draw.content( - (x, 0), - p.display-name, - anchor: "west", - angle: 90deg - ) - } - } - - for p in participants.filter(p => not p.invisible) { - let x = x-pos.at(p.i) - - // Draw vertical line - let last-y = 0 - - let rects = () - let destructions = () - let lines = () - - // Compute lifeline rectangles + destruction positions - for line in lifelines.at(p.i).lines { - let event = line.first() - if event == "create" { - last-y = line.at(1) - - } else if event == "enable" { - if lines.len() == 0 { - draw.line( - (x, last-y), - (x, line.at(1)), - stroke: ( - dash: "dashed", - paint: gray.darken(40%), - thickness: .5pt - ) - ) - } - lines.push(line) - - } else if event == "disable" or event == "destroy" { - let lvl = 0 - if lines.len() != 0 { - let l = lines.pop() - lvl = lines.len() - rects.push(( - x + lvl * LIFELINE-W / 2, - l.at(1), - line.at(1), - l.at(2) - )) - last-y = line.at(1) - } - - if event == "destroy" { - destructions.push((x + lvl * LIFELINE-W / 2, line.at(1))) - } - } else if event == "delay-start" { - draw.line( - (x, last-y), - (x, line.at(1)), - stroke: ( - dash: "dashed", - paint: gray.darken(40%), - thickness: .5pt - ) - ) - last-y = line.at(1) - } else if event == "delay-end" { - draw.line( - (x, last-y), - (x, line.at(1)), - stroke: ( - dash: "loosely-dotted", - paint: gray.darken(40%), - thickness: .8pt - ) - ) - last-y = line.at(1) - } - } - - draw.line( - (x, last-y), - (x, y), - stroke: ( - dash: "dashed", - paint: gray.darken(40%), - thickness: .5pt - ) - ) - - // Draw lifeline rectangles (reverse for bottom to top) - for rect in rects.rev() { - let (cx, y0, y1, style) = rect - let style = get-style("lifeline", style) - draw.rect( - (cx - LIFELINE-W / 2, y0), - (cx + LIFELINE-W / 2, y1), - ..style - ) - } - - // Draw lifeline destructions - for dest in destructions { - let (cx, cy) = dest - draw.line((cx - 8, cy - 8), (cx + 8, cy + 8), stroke: COL-DESTRUCTION + 2pt) - draw.line((cx - 8, cy + 8), (cx + 8, cy - 8), stroke: COL-DESTRUCTION + 2pt) - } - - // Draw participants (end) - if p.show-bottom { - draw-par(p, y: y, bottom: true) - } - } - }) - - shapes -}) \ No newline at end of file diff --git a/src/separator.typ b/src/separator.typ deleted file mode 100644 index e0d1b71..0000000 --- a/src/separator.typ +++ /dev/null @@ -1,63 +0,0 @@ -#import "/src/cetz.typ": draw -#import "consts.typ": * - -#let _sep(name) = { - return (( - type: "sep", - name: name - ),) -} - -#let _delay(name: none, size: 30) = { - return (( - type: "delay", - name: name, - size: size - ),) -} - -#let render(x-pos, elmt, y) = { - let shapes = () - y -= Y-SPACE - - let x0 = x-pos.first() - 20 - let x1 = x-pos.last() + 20 - let m = measure( - box( - elmt.name, - inset: (left: 3pt, right: 3pt, top: 5pt, bottom: 5pt) - ) - ) - let w = m.width / 1pt - let h = m.height / 1pt - let cx = (x0 + x1) / 2 - let xl = cx - w / 2 - let xr = cx + w / 2 - - y -= h / 2 - shapes += draw.rect( - (x0, y), - (x1, y - 3), - stroke: none, - fill: white - ) - shapes += draw.line((x0, y), (x1, y)) - //shapes += draw.line((x0, y), (xl, y)) - //shapes += draw.line((xr, y), (x1, y)) - y -= 3 - shapes += draw.line((x0, y), (x1, y)) - //shapes += draw.line((x0, y), (xl, y)) - //shapes += draw.line((xr, y), (x1, y)) - shapes += draw.content( - ((x0 + x1) / 2, y + 1.5), - elmt.name, - anchor: "center", - padding: (5pt, 3pt), - frame: "rect", - fill: COL-SEP-NAME - ) - y -= h / 2 - - let r = (y, shapes) - return r -} \ No newline at end of file diff --git a/src/sequence.typ b/src/sequence.typ index eb315de..c916996 100644 --- a/src/sequence.typ +++ b/src/sequence.typ @@ -1,51 +1,4 @@ -#import "/src/cetz.typ": draw, vector -#import "consts.typ": * -#import "participant.typ" -#import "note.typ" - -#let get-arrow-marks(sym, color) = { - if sym == none { - return none - } - if type(sym) == array { - return sym.map(s => get-arrow-marks(s, color)) - } - ( - "": none, - ">": (symbol: ">", fill: color), - ">>": (symbol: "straight"), - "\\": (symbol: ">", fill: color, harpoon: true, flip: true), - "\\\\": (symbol: "straight", harpoon: true, flip: true), - "/": (symbol: ">", fill: color, harpoon: true), - "//": (symbol: "straight", harpoon: true), - "x": none, - "o": none, - ).at(sym) -} - -#let reverse-arrow-mark(mark) = { - if type(mark) == array { - return mark.map(m => reverse-arrow-mark(m)) - } - let mark2 = mark - if type(mark) == dictionary and mark.at("harpoon", default: false) { - let flipped = mark.at("flip", default: false) - mark2.insert("flip", not flipped) - } - return mark2 -} - -#let is-tip-of-type(type_, tip) = { - if type(tip) == str and tip == type_ { - return true - } - if type(tip) == array and tip.contains(type_) { - return true - } - return false -} -#let is-circle-tip = is-tip-of-type.with("o") -#let is-cross-tip = is-tip-of-type.with("x") +#import "core/draw/sequence.typ" #let _seq( p1, @@ -64,10 +17,12 @@ disable-src: false, destroy-src: false, lifeline-style: auto, - slant: none + slant: none, + outer-lifeline-connect: false ) = { return (( type: "seq", + draw: sequence.render, p1: p1, p2: p2, comment: comment, @@ -84,7 +39,9 @@ disable-src: disable-src, destroy-src: destroy-src, lifeline-style: lifeline-style, - slant: slant + slant: slant, + outer-lifeline-connect: outer-lifeline-connect, + linked-notes: () ),) } @@ -94,294 +51,3 @@ comment: comment ),) } - -#let render(pars-i, x-pos, participants, elmt, y, lifelines) = { - let shapes = () - - y -= Y-SPACE - - let i1 = pars-i.at(elmt.p1) - let i2 = pars-i.at(elmt.p2) - let width = calc.abs(x-pos.at(i1) - x-pos.at(i2)) - - let h = 0 - let comment = if elmt.comment == none {none} else { - let w = calc.min(width * 1pt, measure(elmt.comment).width) - box( - width: if i1 == i2 {auto} else {w}, - elmt.comment - ) - } - // Reserve space for comment - if comment != none { - h = calc.max(h, measure(comment).height / 1pt + 6) - } - if "linked-note" in elmt { - h = calc.max(h, note.get-size(elmt.linked-note).height / 2) - } - y -= h - - let start-info = ( - i: i1, - x: x-pos.at(i1), - y: y, - ll-lvl: lifelines.at(i1).level * LIFELINE-W / 2 - ) - let end-info = ( - i: i2, - x: x-pos.at(i2), - y: y, - ll-lvl: lifelines.at(i2).level * LIFELINE-W / 2 - ) - let slant = if elmt.slant == auto { - DEFAULT-SLANT - } else if elmt.slant != none { - elmt.slant - } else { - 0 - } - end-info.y -= slant - if elmt.p1 == elmt.p2 { - end-info.y -= 10 - } - - if elmt.disable-src { - let src-line = lifelines.at(i1) - src-line.level -= 1 - src-line.lines.push(("disable", start-info.y)) - lifelines.at(i1) = src-line - } - if elmt.destroy-src { - let src-line = lifelines.at(i1) - src-line.lines.push(("destroy", start-info.y)) - lifelines.at(i1) = src-line - } - if elmt.disable-dst { - let dst-line = lifelines.at(i2) - dst-line.level -= 1 - dst-line.lines.push(("disable", end-info.y)) - lifelines.at(i2) = dst-line - } - if elmt.destroy-dst { - let dst-line = lifelines.at(i2) - dst-line.lines.push(("destroy", end-info.y)) - lifelines.at(i2) = dst-line - } - if elmt.enable-dst { - let dst-line = lifelines.at(i2) - dst-line.level += 1 - lifelines.at(i2) = dst-line - } - if elmt.create-dst { - let par = participants.at(i2) - let m = measure(box(par.display-name)) - let f = if i1 > i2 {-1} else {1} - end-info.x -= (m.width + PAR-PAD.last() * 2) / 2pt * f - shapes += participant.render(x-pos, par, y: end-info.y) - } - - end-info.ll-lvl = lifelines.at(i2).level * LIFELINE-W / 2 - - // Compute left/right position at start/end - start-info.insert("lx", start-info.x) - if start-info.ll-lvl != 0 { start-info.lx -= LIFELINE-W / 2 } - end-info.insert("lx", end-info.x) - if end-info.ll-lvl != 0 { end-info.lx -= LIFELINE-W / 2 } - - start-info.insert("rx", start-info.x + start-info.ll-lvl) - end-info.insert("rx", end-info.x + end-info.ll-lvl) - - // Choose correct points to link - let x1 = start-info.rx - let x2 = end-info.lx - - if (start-info.i > end-info.i) { - x1 = start-info.lx - x2 = end-info.rx - } - - let style = ( - mark: ( - start: get-arrow-marks(elmt.start-tip, elmt.color), - end: get-arrow-marks(elmt.end-tip, elmt.color), - scale: 1.2 - ), - stroke: ( - dash: if elmt.dashed {(2pt,2pt)} else {"solid"}, - paint: elmt.color, - thickness: .5pt - ) - ) - - let y0 = start-info.y - if "linked-note" in elmt { - let shps = note.render(pars-i, x-pos, elmt.linked-note, start-info.y, lifelines).last() - shapes += shps - } - - let flip-mark = end-info.i <= start-info.i - if elmt.flip { - flip-mark = not flip-mark - } - if flip-mark { - style.mark.end = reverse-arrow-mark(style.mark.end) - } - - let pts - let comment-pt - let comment-anchor - let comment-angle = 0deg - - if elmt.p1 == elmt.p2 { - if elmt.flip { - x1 = start-info.lx - } else { - x2 = end-info.rx - } - - let x-mid = if elmt.flip { - calc.min(x1, x2) - 20 - } else { - calc.max(x1, x2) + 20 - } - - pts = ( - (x1, start-info.y), - (x-mid, start-info.y), - (x-mid, end-info.y), - (x2, end-info.y) - ) - - if comment != none { - comment-anchor = ( - start: if x-mid < x1 {"south-east"} else {"south-west"}, - end: if x-mid < x1 {"south-west"} else {"south-east"}, - left: "south-west", - right: "south-east", - center: "south", - ).at(elmt.comment-align) - - comment-pt = ( - start: pts.first(), - end: pts.at(1), - left: if x-mid < x1 {pts.at(1)} else {pts.first()}, - right: if x-mid < x1 {pts.first()} else {pts.at(1)}, - center: (pts.first(), 50%, pts.at(1)) - ).at(elmt.comment-align) - } - - } else { - pts = ( - (x1, start-info.y), - (x2, end-info.y) - ) - - if comment != none { - let start-pt = pts.first() - let end-pt = pts.last() - if elmt.start-tip != "" { - start-pt = (pts.first(), COMMENT-PAD, pts.last()) - } - if elmt.end-tip != "" { - end-pt = (pts.last(), COMMENT-PAD, pts.first()) - } - - comment-pt = ( - start: start-pt, - end: end-pt, - left: if x2 < x1 {end-pt} else {start-pt}, - right: if x2 < x1 {start-pt} else {end-pt}, - center: (start-pt, 50%, end-pt) - ).at(elmt.comment-align) - - comment-anchor = ( - start: if x2 < x1 {"south-east"} else {"south-west"}, - end: if x2 < x1 {"south-west"} else {"south-east"}, - left: "south-west", - right: "south-east", - center: "south", - ).at(elmt.comment-align) - } - - let (p1, p2) = pts - if x2 < x1 { - (p1, p2) = (p2, p1) - } - comment-angle = vector.angle2(p1, p2) - } - - // Start circle tip - if is-circle-tip(elmt.start-tip) { - shapes += draw.circle(pts.first(), radius: CIRCLE-TIP-RADIUS, stroke: none, fill: elmt.color, name: "_circle-start-tip") - pts.at(0) = "_circle-start-tip" - - // Start cross tip - } else if is-cross-tip(elmt.start-tip) { - let size = CROSS-TIP-SIZE - let cross-pt = (pts.first(), size * 2, pts.at(1)) - shapes += draw.line( - (rel: (-size, -size), to: cross-pt), - (rel: (size, size), to: cross-pt), - stroke: elmt.color + 1.5pt - ) - shapes += draw.line( - (rel: (-size, size), to: cross-pt), - (rel: (size, -size), to: cross-pt), - stroke: elmt.color + 1.5pt - ) - pts.at(0) = cross-pt - } - - // End circle tip - if is-circle-tip(elmt.end-tip) { - shapes += draw.circle(pts.last(), radius: 3, stroke: none, fill: elmt.color, name: "_circle-end-tip") - pts.at(pts.len() - 1) = "_circle-end-tip" - - // End cross tip - } else if is-cross-tip(elmt.end-tip) { - let size = CROSS-TIP-SIZE - let cross-pt = (pts.last(), size * 2, pts.at(pts.len() - 2)) - shapes += draw.line( - (rel: (-size, -size), to: cross-pt), - (rel: (size, size), to: cross-pt), - stroke: elmt.color + 1.5pt - ) - shapes += draw.line( - (rel: (-size, size), to: cross-pt), - (rel: (size, -size), to: cross-pt), - stroke: elmt.color + 1.5pt - ) - pts.at(pts.len() - 1) = cross-pt - } - - shapes += draw.line(..pts, ..style) - - if comment != none { - shapes += draw.content( - comment-pt, - comment, - anchor: comment-anchor, - angle: comment-angle, - padding: 3pt - ) - } - - if elmt.create-dst { - let dst-line = lifelines.at(i2) - dst-line.lines.push(("create", end-info.y)) - lifelines.at(i2) = dst-line - } - if elmt.enable-dst { - let dst-line = lifelines.at(i2) - dst-line.lines.push(("enable", end-info.y, elmt.lifeline-style)) - lifelines.at(i2) = dst-line - } - - if "linked-note" in elmt { - let m = note.get-size(elmt.linked-note) - end-info.y = calc.min(end-info.y, y0 - m.height / 2) - } - - let r = (end-info.y, lifelines, shapes) - return r -} \ No newline at end of file diff --git a/src/sync.typ b/src/sync.typ deleted file mode 100644 index 41f0891..0000000 --- a/src/sync.typ +++ /dev/null @@ -1,27 +0,0 @@ -#import "sequence.typ" - -#let _sync(elmts) = { - return (( - type: "sync", - elmts: elmts - ),) -} - -#let render(pars-i, x-pos, participants, elmt, y, lifelines) = { - let draw-seq = sequence.render.with(pars-i, x-pos, participants) - - let shapes = () - - let end-y = y - - for e in elmt.elmts { - let yi - let shps - (yi, lifelines, shps) = draw-seq(e, y, lifelines) - shapes += shps - end-y = calc.min(end-y, yi) - } - - let r = (end-y, lifelines, shapes) - return r -} \ No newline at end of file diff --git a/tests/empty/.gitignore b/tests/empty/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/empty/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/empty/ref/1.png b/tests/empty/ref/1.png new file mode 100644 index 0000000..7971661 Binary files /dev/null and b/tests/empty/ref/1.png differ diff --git a/tests/empty/test.typ b/tests/empty/test.typ new file mode 100644 index 0000000..80e1d7a --- /dev/null +++ b/tests/empty/test.typ @@ -0,0 +1,6 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({}) + +#diagram(()) diff --git a/tests/group/.gitignore b/tests/group/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/group/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/group/ref/1.png b/tests/group/ref/1.png new file mode 100644 index 0000000..8ee09a4 Binary files /dev/null and b/tests/group/ref/1.png differ diff --git a/tests/group/ref/2.png b/tests/group/ref/2.png new file mode 100644 index 0000000..070f238 Binary files /dev/null and b/tests/group/ref/2.png differ diff --git a/tests/group/ref/3.png b/tests/group/ref/3.png new file mode 100644 index 0000000..9703fa2 Binary files /dev/null and b/tests/group/ref/3.png differ diff --git a/tests/group/test.typ b/tests/group/test.typ new file mode 100644 index 0000000..213df95 --- /dev/null +++ b/tests/group/test.typ @@ -0,0 +1,51 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _seq("Alice", "Bob", comment: "Authentication Request") + + _alt( + "successful case", { + _seq("Bob", "Alice", comment: "Authentication Accepted") + }, + "some kind of failure", { + _seq("Bob", "Alice", comment: "Authentication Failure") + + _grp("My own label", desc: "My own label2", { + _seq("Alice", "Log", comment: "Log attack start") + _loop("1000 times", { + _seq("Alice", "Bob", comment: "DNS Attack") + }) + _seq("Alice", "Log", comment: "Log attack end") + }) + }, + "Another type of failure", { + _seq("Bob", "Alice", comment: "Please repeat") + } + ) +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: box(width: 1.5em, height: .5em), show-bottom: false) + _par("b", display-name: box(width: 1.5em, height: .5em), show-bottom: false) + _col("a", "b", width: 2cm) + _loop("a<1", min: 1, { + _seq("a", "b", end-tip: ">>") + _seq("b", "a", end-tip: ">>") + }) + _seq("a", "b", end-tip: ">>") +}) + +#pagebreak() + +#diagram({ + _par("A") + _par("B") + _col("A", "B", width: 3cm) + _seq("A", "B", enable-dst: true) + _alt([desc], { + _ret() + }) +}) \ No newline at end of file diff --git a/tests/lifeline/create-destroy/.gitignore b/tests/lifeline/create-destroy/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/lifeline/create-destroy/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/lifeline/create-destroy/ref/1.png b/tests/lifeline/create-destroy/ref/1.png new file mode 100644 index 0000000..1802802 Binary files /dev/null and b/tests/lifeline/create-destroy/ref/1.png differ diff --git a/tests/lifeline/create-destroy/ref/2.png b/tests/lifeline/create-destroy/ref/2.png new file mode 100644 index 0000000..af4775b Binary files /dev/null and b/tests/lifeline/create-destroy/ref/2.png differ diff --git a/tests/lifeline/create-destroy/test.typ b/tests/lifeline/create-destroy/test.typ new file mode 100644 index 0000000..f1b209a --- /dev/null +++ b/tests/lifeline/create-destroy/test.typ @@ -0,0 +1,32 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + // Left to right + _seq("a", "b", create-dst: true) + _seq("a", "b", enable-dst: true) + _seq("a", "b", enable-dst: true) + _gap() + _seq("a", "b", destroy-dst: true) + _gap() + _seq("a", "b", destroy-dst: true) +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + // Right to left + _seq("b", "a", create-dst: true) + _seq("b", "a", enable-dst: true) + _seq("b", "a", enable-dst: true) + _gap() + _seq("b", "a", destroy-dst: true) + _gap() + _seq("b", "a", destroy-dst: true) +}) diff --git a/tests/lifeline/enable-disable/.gitignore b/tests/lifeline/enable-disable/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/lifeline/enable-disable/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/lifeline/enable-disable/ref/1.png b/tests/lifeline/enable-disable/ref/1.png new file mode 100644 index 0000000..d5999c5 Binary files /dev/null and b/tests/lifeline/enable-disable/ref/1.png differ diff --git a/tests/lifeline/enable-disable/ref/2.png b/tests/lifeline/enable-disable/ref/2.png new file mode 100644 index 0000000..3e6678d Binary files /dev/null and b/tests/lifeline/enable-disable/ref/2.png differ diff --git a/tests/lifeline/enable-disable/ref/3.png b/tests/lifeline/enable-disable/ref/3.png new file mode 100644 index 0000000..125304c Binary files /dev/null and b/tests/lifeline/enable-disable/ref/3.png differ diff --git a/tests/lifeline/enable-disable/ref/4.png b/tests/lifeline/enable-disable/ref/4.png new file mode 100644 index 0000000..de1b5bb Binary files /dev/null and b/tests/lifeline/enable-disable/ref/4.png differ diff --git a/tests/lifeline/enable-disable/ref/5.png b/tests/lifeline/enable-disable/ref/5.png new file mode 100644 index 0000000..1643084 Binary files /dev/null and b/tests/lifeline/enable-disable/ref/5.png differ diff --git a/tests/lifeline/enable-disable/ref/6.png b/tests/lifeline/enable-disable/ref/6.png new file mode 100644 index 0000000..4304b01 Binary files /dev/null and b/tests/lifeline/enable-disable/ref/6.png differ diff --git a/tests/lifeline/enable-disable/test.typ b/tests/lifeline/enable-disable/test.typ new file mode 100644 index 0000000..21c7833 --- /dev/null +++ b/tests/lifeline/enable-disable/test.typ @@ -0,0 +1,110 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + // Left to right + _seq("a", "b") + _seq("a", "b", enable-dst: true) + _seq("a", "b") + _seq("a", "b", enable-dst: true) + _seq("a", "b") + _seq("a", "b", disable-dst: true) + _seq("a", "b") + _seq("a", "b", disable-dst: true) + _seq("a", "b") +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + // Right to left + _seq("b", "a") + _seq("b", "a", enable-dst: true) + _seq("b", "a") + _seq("b", "a", enable-dst: true) + _seq("b", "a") + _seq("b", "a", disable-dst: true) + _seq("b", "a") + _seq("b", "a", disable-dst: true) + _seq("b", "a") +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + // Self right + _seq("a", "a") + _seq("a", "a", enable-dst: true) + _seq("a", "a") + _seq("a", "a", enable-dst: true) + _seq("a", "a") + _seq("a", "a", disable-dst: true) + _seq("a", "a") + _seq("a", "a", disable-dst: true) + _seq("a", "a") +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + let _seq = _seq.with(flip: true) + + // Self left + _seq("b", "b") + _seq("b", "b", enable-dst: true) + _seq("b", "b") + _seq("b", "b", enable-dst: true) + _seq("b", "b") + _seq("b", "b", disable-dst: true) + _seq("b", "b") + _seq("b", "b", disable-dst: true) + _seq("b", "b") +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + // Disable src (rtl) + _seq("a", "b") + _seq("a", "b", enable-dst: true) + _seq("a", "b") + _seq("a", "b", enable-dst: true) + _seq("a", "b") + _seq("b", "a", disable-src: true) + _seq("a", "b") + _seq("b", "a", disable-src: true) + _seq("a", "b") +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + // Disable src (ltr) + _seq("b", "a") + _seq("b", "a", enable-dst: true) + _seq("b", "a") + _seq("b", "a", enable-dst: true) + _seq("b", "a") + _seq("a", "b", disable-src: true) + _seq("b", "a") + _seq("a", "b", disable-src: true) + _seq("b", "a") +}) diff --git a/tests/lifeline/event/.gitignore b/tests/lifeline/event/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/lifeline/event/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/lifeline/event/ref/1.png b/tests/lifeline/event/ref/1.png new file mode 100644 index 0000000..648fe4a Binary files /dev/null and b/tests/lifeline/event/ref/1.png differ diff --git a/tests/lifeline/event/ref/2.png b/tests/lifeline/event/ref/2.png new file mode 100644 index 0000000..795e60a Binary files /dev/null and b/tests/lifeline/event/ref/2.png differ diff --git a/tests/lifeline/event/ref/3.png b/tests/lifeline/event/ref/3.png new file mode 100644 index 0000000..58327f1 Binary files /dev/null and b/tests/lifeline/event/ref/3.png differ diff --git a/tests/lifeline/event/test.typ b/tests/lifeline/event/test.typ new file mode 100644 index 0000000..1167ea9 --- /dev/null +++ b/tests/lifeline/event/test.typ @@ -0,0 +1,75 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _evt("a", "enable") + _gap() + _seq("a", "b") + _evt("a", "disable") + _gap() + _evt("a", "enable") + _evt("b", "enable") + _seq("a", "b") + _evt("a", "enable") + _evt("b", "enable") + _gap() + _seq("a", "b") + _evt("a", "disable") + _evt("b", "disable") + _gap() + _evt("a", "disable") + _evt("b", "disable") +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + _par("c", display-name: "Charlie") + + _seq("a", "b") + _gap() + _seq("a", "b", enable-dst: true) + _evt("c", "create") + _seq("c", "a") + _evt("b", "destroy") +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _seq("a", "b") + _evt("b", "enable") + _seq("a", "b") + _evt("b", "enable", lifeline-style: (fill: red)) + _gap() + _seq("b", "a") + _evt("b", "disable") + _seq("a", "b") + _evt("b", "enable", lifeline-style: ( + stroke: ( + paint: green, + dash: "dashed", + thickness: 2pt + ) + )) + _gap() + _seq("b", "a") + _evt("b", "disable") + _seq("b", "a") + _evt("b", "disable") + _seq("a", "b") + _evt("b", "enable", lifeline-style: ( + radius: 4pt + )) + _gap() + _seq("b", "a") + _evt("b", "disable") +}) diff --git a/tests/lifeline/style/.gitignore b/tests/lifeline/style/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/lifeline/style/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/lifeline/style/ref/1.png b/tests/lifeline/style/ref/1.png new file mode 100644 index 0000000..055fb4d Binary files /dev/null and b/tests/lifeline/style/ref/1.png differ diff --git a/tests/lifeline/style/test.typ b/tests/lifeline/style/test.typ new file mode 100644 index 0000000..e7622bc --- /dev/null +++ b/tests/lifeline/style/test.typ @@ -0,0 +1,27 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _seq("a", "b", enable-dst: true) + _seq("a", "b", enable-dst: true, lifeline-style: (fill: red)) + _gap() + _seq("b", "a", disable-src: true) + _seq("a", "b", enable-dst: true, lifeline-style: ( + stroke: ( + paint: green, + dash: "dashed", + thickness: 2pt + ) + )) + _gap() + _seq("b", "a", disable-src: true) + _seq("b", "a", disable-src: true) + _seq("a", "b", enable-dst: true, lifeline-style: ( + radius: 4pt + )) + _gap() + _seq("b", "a", disable-src: true) +}) diff --git a/tests/note/color/.gitignore b/tests/note/color/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/note/color/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/note/color/ref/1.png b/tests/note/color/ref/1.png new file mode 100644 index 0000000..68f4b4c Binary files /dev/null and b/tests/note/color/ref/1.png differ diff --git a/tests/note/color/test.typ b/tests/note/color/test.typ new file mode 100644 index 0000000..bf051c5 --- /dev/null +++ b/tests/note/color/test.typ @@ -0,0 +1,13 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _note("left", [This is displayed\ left of Alice.], pos: "a", color: rgb("#00FFFF")) + _note("right", [This is displayed right of Alice.], pos: "a") + _note("over", [This is displayed over Alice.], pos: "a") + _note("over", [This is displayed\ over Bob and Alice.], pos: ("a", "b"), color: rgb("#FFAAAA")) + _note("over", [This is yet another\ example of\ a long note.], pos: ("a", "b")) +}) \ No newline at end of file diff --git a/tests/note/position/.gitignore b/tests/note/position/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/note/position/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/note/position/ref/1.png b/tests/note/position/ref/1.png new file mode 100644 index 0000000..04bb1c8 Binary files /dev/null and b/tests/note/position/ref/1.png differ diff --git a/tests/note/position/ref/2.png b/tests/note/position/ref/2.png new file mode 100644 index 0000000..c0c6d78 Binary files /dev/null and b/tests/note/position/ref/2.png differ diff --git a/tests/note/position/ref/3.png b/tests/note/position/ref/3.png new file mode 100644 index 0000000..c4091b2 Binary files /dev/null and b/tests/note/position/ref/3.png differ diff --git a/tests/note/position/test.typ b/tests/note/position/test.typ new file mode 100644 index 0000000..4fef1cd --- /dev/null +++ b/tests/note/position/test.typ @@ -0,0 +1,49 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _seq("a", "b", comment: [hello]) + _note("left", [this is a first note]) + + _seq("b", "a", comment: [ok]) + _note("right", [this is another note]) + + _seq("b", "b", comment: [I am thinking]) + _note("left", [a note\ can also be defined\ on several lines]) +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _note("over", [initial state of Alice], pos: "a") + _note("over", [initial state of Bob], pos: "b") + _seq("b", "a", comment: [hello]) +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + _par("c", display-name: "Charlie") + _par("d", display-name: "Donald") + _par("e", display-name: "Eddie") + + _note("across", [This note float above all participants]) + + _note("over", [initial state of Alice], pos: "a") + _note("over", [initial state of Bob the builder], pos: "b", aligned: true) + + _note("over", [Note 1], pos: "a") + _note("over", [Note 2], pos: "b", aligned: true) + _note("over", [Note 3], pos: "c", aligned: true) + + _seq("a", "d") + _note("over", [this is an extremely long note], pos: ("d", "e")) +}) \ No newline at end of file diff --git a/tests/note/shape/.gitignore b/tests/note/shape/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/note/shape/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/note/shape/ref/1.png b/tests/note/shape/ref/1.png new file mode 100644 index 0000000..e7321d4 Binary files /dev/null and b/tests/note/shape/ref/1.png differ diff --git a/tests/note/shape/test.typ b/tests/note/shape/test.typ new file mode 100644 index 0000000..c14131d --- /dev/null +++ b/tests/note/shape/test.typ @@ -0,0 +1,14 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("caller") + _par("server") + + _seq("caller", "server", comment: [conReq]) + _note("over", [idle], pos: "caller", shape: "hex") + _seq("server", "caller", comment: [conConf]) + _note("over", ["r" as rectangle\ "h" as hexagon], pos: "server", shape: "rect") + _note("over", [this is\ on several\ lines], pos: "server", shape: "rect") + _note("over", [this is\ on several\ lines], pos: "caller", shape: "hex") +}) \ No newline at end of file diff --git a/tests/note/styled-body/.gitignore b/tests/note/styled-body/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/note/styled-body/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/note/styled-body/ref/1.png b/tests/note/styled-body/ref/1.png new file mode 100644 index 0000000..d4a3a8b Binary files /dev/null and b/tests/note/styled-body/ref/1.png differ diff --git a/tests/note/styled-body/test.typ b/tests/note/styled-body/test.typ new file mode 100644 index 0000000..419a36c --- /dev/null +++ b/tests/note/styled-body/test.typ @@ -0,0 +1,33 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("a", display-name: [Alice]) + _par("b", display-name: [The *Famous* Bob]) + + _seq("a", "b", comment: [hello #strike([there])]) + + _gap() + _seq("b", "a", comment: [ok]) + _note("left", [ + This is *bold*\ + This is _italics_\ + This is `monospaced`\ + This is #strike([stroked])\ + This is #underline([underlined])\ + This is #underline([waved])\ + ]) + + _seq("a", "b", comment: [A _well formatted_ message]) + _note("right", [ + This is #box(text([displayed], size: 18pt), fill: rgb("#5F9EA0"))\ + #underline([left of]) Alice. + ], pos: "a") + _note("left", [ + #underline([This], stroke: red) is #text([displayed], fill: rgb("#118888"))\ + *#text([left of], fill: rgb("#800080")) #strike([Alice], stroke: red) Bob.* + ], pos: "b") + _note("over", [ + #underline([This is hosted], stroke: rgb("#FF33FF")) by #box(baseline: 50%, image("/gallery/gitea.png", width: 1cm, height: 1cm, fit: "contain")) + ], pos: ("a", "b")) +}) \ No newline at end of file diff --git a/tests/participant/shapes/.gitignore b/tests/participant/shapes/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/participant/shapes/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/participant/shapes/ref/1.png b/tests/participant/shapes/ref/1.png new file mode 100644 index 0000000..955b123 Binary files /dev/null and b/tests/participant/shapes/ref/1.png differ diff --git a/tests/participant/shapes/test.typ b/tests/participant/shapes/test.typ new file mode 100644 index 0000000..c130cdf --- /dev/null +++ b/tests/participant/shapes/test.typ @@ -0,0 +1,21 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + + +#let TYPST = image("/gallery/typst.png", width: 1.5cm, height: 1.5cm, fit: "contain") +#let FERRIS = image("/gallery/ferris.png", width: 1.5cm, height: 1.5cm, fit: "contain") +#let ME = image("/gallery/me.jpg", width: 1.5cm, height: 1.5cm, fit: "contain") + +#diagram({ + _par("Foo", display-name: "Participant", shape: "participant") + _par("Foo1", display-name: "Actor", shape: "actor") + _par("Foo2", display-name: "Boundary", shape: "boundary") + _par("Foo3", display-name: "Control", shape: "control") + _par("Foo4", display-name: "Entity", shape: "entity") + _par("Foo5", display-name: "Database", shape: "database") + _par("Foo6", display-name: "Collections", shape: "collections") + _par("Foo7", display-name: "Queue", shape: "queue") + _par("Foo8", display-name: "Typst", shape: "custom", custom-image: TYPST) + _par("Foo9", display-name: "Ferris", shape: "custom", custom-image: FERRIS) + _par("Foo10", display-name: "Baryhobal", shape: "custom", custom-image: ME) +}) \ No newline at end of file diff --git a/tests/sequence/comment-align/.gitignore b/tests/sequence/comment-align/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/sequence/comment-align/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/sequence/comment-align/ref/1.png b/tests/sequence/comment-align/ref/1.png new file mode 100644 index 0000000..1cdb569 Binary files /dev/null and b/tests/sequence/comment-align/ref/1.png differ diff --git a/tests/sequence/comment-align/ref/2.png b/tests/sequence/comment-align/ref/2.png new file mode 100644 index 0000000..31185af Binary files /dev/null and b/tests/sequence/comment-align/ref/2.png differ diff --git a/tests/sequence/comment-align/test.typ b/tests/sequence/comment-align/test.typ new file mode 100644 index 0000000..f69d949 --- /dev/null +++ b/tests/sequence/comment-align/test.typ @@ -0,0 +1,57 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("alice", display-name: "Alice") + _par("bob", display-name: "Bob") + _seq("alice", "bob", comment: "This is a very long comment") + + // Left to right + _seq("alice", "bob", comment: "Start aligned", comment-align: "start") + _seq("alice", "bob", comment: "End aligned", comment-align: "end") + _seq("alice", "bob", comment: "Left aligned", comment-align: "left") + _seq("alice", "bob", comment: "Right aligned", comment-align: "right") + _seq("alice", "bob", comment: "Centered", comment-align: "center") + _gap() + + // Right to left + _seq("bob", "alice", comment: "Start aligned", comment-align: "start") + _seq("bob", "alice", comment: "End aligned", comment-align: "end") + _seq("bob", "alice", comment: "Left aligned", comment-align: "left") + _seq("bob", "alice", comment: "Right aligned", comment-align: "right") + _seq("bob", "alice", comment: "Centered", comment-align: "center") + _gap() + + // Slant left to right + _seq("alice", "bob", comment: "Start aligned", comment-align: "start", slant: 10) + _seq("alice", "bob", comment: "End aligned", comment-align: "end", slant: 10) + _seq("alice", "bob", comment: "Left aligned", comment-align: "left", slant: 10) + _seq("alice", "bob", comment: "Right aligned", comment-align: "right", slant: 10) + _seq("alice", "bob", comment: "Centered", comment-align: "center", slant: 10) + _gap() + + // Slant right to left + _seq("bob", "alice", comment: "Start aligned", comment-align: "start", slant: 10) + _seq("bob", "alice", comment: "End aligned", comment-align: "end", slant: 10) + _seq("bob", "alice", comment: "Left aligned", comment-align: "left", slant: 10) + _seq("bob", "alice", comment: "Right aligned", comment-align: "right", slant: 10) + _seq("bob", "alice", comment: "Centered", comment-align: "center", slant: 10) +}) + +#pagebreak() + +#diagram({ + _par("alice", display-name: "Alice") + + _seq("alice", "alice", comment: "Start aligned", comment-align: "start") + _seq("alice", "alice", comment: "End aligned", comment-align: "end") + _seq("alice", "alice", comment: "Left aligned", comment-align: "left") + _seq("alice", "alice", comment: "Right aligned", comment-align: "right") + _seq("alice", "alice", comment: "Centered", comment-align: "center") + + _seq("alice", "alice", comment: "Start aligned", comment-align: "start", flip: true) + _seq("alice", "alice", comment: "End aligned", comment-align: "end", flip: true) + _seq("alice", "alice", comment: "Left aligned", comment-align: "left", flip: true) + _seq("alice", "alice", comment: "Right aligned", comment-align: "right", flip: true) + _seq("alice", "alice", comment: "Centered", comment-align: "center", flip: true) +}) \ No newline at end of file diff --git a/tests/sequence/outer-lifeline/.gitignore b/tests/sequence/outer-lifeline/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/sequence/outer-lifeline/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/sequence/outer-lifeline/ref/1.png b/tests/sequence/outer-lifeline/ref/1.png new file mode 100644 index 0000000..a5f7d1f Binary files /dev/null and b/tests/sequence/outer-lifeline/ref/1.png differ diff --git a/tests/sequence/outer-lifeline/ref/2.png b/tests/sequence/outer-lifeline/ref/2.png new file mode 100644 index 0000000..d2a82c4 Binary files /dev/null and b/tests/sequence/outer-lifeline/ref/2.png differ diff --git a/tests/sequence/outer-lifeline/test.typ b/tests/sequence/outer-lifeline/test.typ new file mode 100644 index 0000000..4bde480 --- /dev/null +++ b/tests/sequence/outer-lifeline/test.typ @@ -0,0 +1,29 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#let make-diagram(_seq) = { + diagram({ + _par("a") + _par("b") + _par("c") + _seq("a", "b") + _seq("c", "b") + _seq("a", "b", enable-dst: true) + _seq("a", "b") + _seq("c", "b") + _seq("a", "b", enable-dst: true) + _seq("a", "b") + _seq("c", "b") + _seq("a", "b", enable-dst: true) + _seq("a", "b") + _seq("c", "b") + + _evt("b", "disable") + _evt("b", "disable") + _evt("b", "disable") + }) +} + +#make-diagram(_seq) +#pagebreak() +#make-diagram(_seq.with(outer-lifeline-connect: true)) diff --git a/tests/sequence/special-par/.gitignore b/tests/sequence/special-par/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/sequence/special-par/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/sequence/special-par/ref/1.png b/tests/sequence/special-par/ref/1.png new file mode 100644 index 0000000..38d5420 Binary files /dev/null and b/tests/sequence/special-par/ref/1.png differ diff --git a/tests/sequence/special-par/test.typ b/tests/sequence/special-par/test.typ new file mode 100644 index 0000000..2710cb3 --- /dev/null +++ b/tests/sequence/special-par/test.typ @@ -0,0 +1,12 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _seq("?", "Alice", comment: [?->\ *short* to actor1]) + _seq("[", "Alice", comment: [\[->\ *from start* to actor1]) + _seq("[", "Bob", comment: [\[->\ *from start* to actor2]) + _seq("?", "Bob", comment: [?->\ *short* to actor2]) + _seq("Alice", "]", comment: [->\]\ from actor1 *to end*]) + _seq("Alice", "?", comment: [->?\ *short* from actor1]) + _seq("Alice", "Bob", comment: [->\ from actor1 to actor2]) +}) diff --git a/tests/sequence/sync/.gitignore b/tests/sequence/sync/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/sequence/sync/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/sequence/sync/ref/1.png b/tests/sequence/sync/ref/1.png new file mode 100644 index 0000000..a7d4067 Binary files /dev/null and b/tests/sequence/sync/ref/1.png differ diff --git a/tests/sequence/sync/ref/2.png b/tests/sequence/sync/ref/2.png new file mode 100644 index 0000000..be21dc9 Binary files /dev/null and b/tests/sequence/sync/ref/2.png differ diff --git a/tests/sequence/sync/test.typ b/tests/sequence/sync/test.typ new file mode 100644 index 0000000..fc69d7e --- /dev/null +++ b/tests/sequence/sync/test.typ @@ -0,0 +1,47 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("a") + _par("b") + _par("c") + _sync({ + _seq("b", "a") + _seq("b", "c") + }) + _gap() + _sync({ + _seq("b", "a", comment: [Comment]) + _seq("b", "c") + }) + _gap() + _sync({ + _seq("b", "a") + _seq("b", "c", comment: [Comment]) + }) + _gap() + _sync({ + _seq("b", "a", comment: [Two\ lines]) + _seq("b", "c", comment: [Comment]) + }) + + _sync({ + _seq("b", "a") + _seq("b", "c", slant: 10) + }) + + _sync({ + _seq("b", "a") + _seq("b", "b") + }) +}) + +#pagebreak() + +#diagram({ + _sync({ + _seq("a", "b", comment: [Abcdefgh]) + _seq("b", "c", comment: [Foo\ bar], slant: 10) + _seq("c", "c", slant: 20) + }) +}) \ No newline at end of file diff --git a/tests/sequence/tips/.gitignore b/tests/sequence/tips/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/sequence/tips/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/sequence/tips/ref/1.png b/tests/sequence/tips/ref/1.png new file mode 100644 index 0000000..00cff5d Binary files /dev/null and b/tests/sequence/tips/ref/1.png differ diff --git a/tests/sequence/tips/ref/2.png b/tests/sequence/tips/ref/2.png new file mode 100644 index 0000000..3725245 Binary files /dev/null and b/tests/sequence/tips/ref/2.png differ diff --git a/tests/sequence/tips/ref/3.png b/tests/sequence/tips/ref/3.png new file mode 100644 index 0000000..6c12338 Binary files /dev/null and b/tests/sequence/tips/ref/3.png differ diff --git a/tests/sequence/tips/test.typ b/tests/sequence/tips/test.typ new file mode 100644 index 0000000..e3d60bd --- /dev/null +++ b/tests/sequence/tips/test.typ @@ -0,0 +1,84 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _seq("a", "b", end-tip: ">", comment: `->`) + _seq("a", "b", end-tip: ">>", comment: `->>`) + _seq("a", "b", end-tip: "\\", comment: `-\`) + _seq("a", "b", end-tip: "\\\\", comment: `-\\`) + _seq("a", "b", end-tip: "/", comment: `-/`) + _seq("a", "b", end-tip: "//", comment: `-//`) + _seq("a", "b", end-tip: "x", comment: `->x`) + _seq("a", "b", start-tip: "x", comment: `x->`) + _seq("a", "b", start-tip: "o", comment: `o->`) + _seq("a", "b", end-tip: ("o", ">"), comment: `->o`) + _seq("a", "b", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`) + _seq("a", "b", start-tip: ">", end-tip: ">", comment: `<->`) + _seq("a", "b", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`) + _seq("a", "b", start-tip: "x", end-tip: "x", comment: `x<->x`) + _seq("a", "b", end-tip: ("o", ">>"), comment: `->>o`) + _seq("a", "b", end-tip: ("o", "\\"), comment: `-\o`) + _seq("a", "b", end-tip: ("o", "\\\\"), comment: `-\\o`) + _seq("a", "b", end-tip: ("o", "/"), comment: `-/o`) + _seq("a", "b", end-tip: ("o", "//"), comment: `-//o`) + _seq("a", "b", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`) +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _seq("b", "a", end-tip: ">", comment: `->`) + _seq("b", "a", end-tip: ">>", comment: `->>`) + _seq("b", "a", end-tip: "\\", comment: `-\`) + _seq("b", "a", end-tip: "\\\\", comment: `-\\`) + _seq("b", "a", end-tip: "/", comment: `-/`) + _seq("b", "a", end-tip: "//", comment: `-//`) + _seq("b", "a", end-tip: "x", comment: `->x`) + _seq("b", "a", start-tip: "x", comment: `x->`) + _seq("b", "a", start-tip: "o", comment: `o->`) + _seq("b", "a", end-tip: ("o", ">"), comment: `->o`) + _seq("b", "a", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`) + _seq("b", "a", start-tip: ">", end-tip: ">", comment: `<->`) + _seq("b", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`) + _seq("b", "a", start-tip: "x", end-tip: "x", comment: `x<->x`) + _seq("b", "a", end-tip: ("o", ">>"), comment: `->>o`) + _seq("b", "a", end-tip: ("o", "\\"), comment: `-\o`) + _seq("b", "a", end-tip: ("o", "\\\\"), comment: `-\\o`) + _seq("b", "a", end-tip: ("o", "/"), comment: `-/o`) + _seq("b", "a", end-tip: ("o", "//"), comment: `-//o`) + _seq("b", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`) +}) + +#pagebreak() + +#diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _seq("a", "a", end-tip: ">", comment: `->`) + _seq("a", "a", end-tip: ">>", comment: `->>`) + _seq("a", "a", end-tip: "\\", comment: `-\`) + _seq("a", "a", end-tip: "\\\\", comment: `-\\`) + _seq("a", "a", end-tip: "/", comment: `-/`) + _seq("a", "a", end-tip: "//", comment: `-//`) + _seq("a", "a", end-tip: "x", comment: `->x`) + _seq("a", "a", start-tip: "x", comment: `x->`) + _seq("a", "a", start-tip: "o", comment: `o->`) + _seq("a", "a", end-tip: ("o", ">"), comment: `->o`) + _seq("a", "a", start-tip: "o", end-tip: ("o", ">"), comment: `o->o`) + _seq("a", "a", start-tip: ">", end-tip: ">", comment: `<->`) + _seq("a", "a", start-tip: ("o", ">"), end-tip: ("o", ">"), comment: `o<->o`) + _seq("a", "a", start-tip: "x", end-tip: "x", comment: `x<->x`) + _seq("a", "a", end-tip: ("o", ">>"), comment: `->>o`) + _seq("a", "a", end-tip: ("o", "\\"), comment: `-\o`) + _seq("a", "a", end-tip: ("o", "\\\\"), comment: `-\\o`) + _seq("a", "a", end-tip: ("o", "/"), comment: `-/o`) + _seq("a", "a", end-tip: ("o", "//"), comment: `-//o`) + _seq("a", "a", start-tip: "x", end-tip: ("o", ">"), comment: `x->o`) +}) diff --git a/tests/special-group/.gitignore b/tests/special-group/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/special-group/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/special-group/ref/1.png b/tests/special-group/ref/1.png new file mode 100644 index 0000000..a84209f Binary files /dev/null and b/tests/special-group/ref/1.png differ diff --git a/tests/special-group/ref/2.png b/tests/special-group/ref/2.png new file mode 100644 index 0000000..ceffc47 Binary files /dev/null and b/tests/special-group/ref/2.png differ diff --git a/tests/special-group/ref/3.png b/tests/special-group/ref/3.png new file mode 100644 index 0000000..ac39b4d Binary files /dev/null and b/tests/special-group/ref/3.png differ diff --git a/tests/special-group/ref/4.png b/tests/special-group/ref/4.png new file mode 100644 index 0000000..6e45733 Binary files /dev/null and b/tests/special-group/ref/4.png differ diff --git a/tests/special-group/ref/5.png b/tests/special-group/ref/5.png new file mode 100644 index 0000000..a63f121 Binary files /dev/null and b/tests/special-group/ref/5.png differ diff --git a/tests/special-group/test.typ b/tests/special-group/test.typ new file mode 100644 index 0000000..890c33e --- /dev/null +++ b/tests/special-group/test.typ @@ -0,0 +1,71 @@ +#set page(width: auto, height: auto) +#import "/src/lib.typ": * + +#let preamble = { + _par("a", display-name: [Alice]) + _par("b", display-name: [Bob]) + _col("a", "b", width: 2cm) +} + +#diagram({ + preamble + _grp("Group 1", { + _seq("a", "b") + }) + _grp("Group 2", desc: [Description], { + _seq("a", "b") + }) +}) + +#pagebreak() + +#diagram({ + preamble + _alt( + "case 1", { + _seq("a", "b") + }, + "case 2", { + _seq("a", "b") + }, + "case 3", { + _seq("a", "b") + } + ) +}) + +#pagebreak() + +#diagram({ + preamble + _loop("loop 1", { + _seq("a", "b") + }) + _loop("loop 2", min: 1, { + _seq("a", "b") + }) + _loop("loop 3", max: 10, { + _seq("a", "b") + }) + _loop("loop 3", min: 1, max: 10, { + _seq("a", "b") + }) +}) + +#pagebreak() + +#diagram({ + preamble + _opt("Optional", { + _seq("a", "b") + }) +}) + +#pagebreak() + +#diagram({ + preamble + _break("Break", { + _seq("a", "b") + }) +}) \ No newline at end of file diff --git a/typst.toml b/typst.toml index 54d8602..215e741 100644 --- a/typst.toml +++ b/typst.toml @@ -1,7 +1,7 @@ [package] name = "chronos" -version = "0.2.1" -compiler = "0.13.1" +version = "0.3.0" +compiler = "0.14.2" repository = "https://git.kb28.ch/HEL/chronos" entrypoint = "src/lib.typ" authors = [