diff --git a/gallery/example1.pdf b/gallery/example1.pdf index 672bd3e..b1f22ec 100644 Binary files a/gallery/example1.pdf and b/gallery/example1.pdf differ diff --git a/gallery/example1.typ b/gallery/example1.typ index 94676cb..48be250 100644 --- a/gallery/example1.typ +++ b/gallery/example1.typ @@ -10,7 +10,7 @@ Alice <-- Bob: Another authentication Response #chronos.diagram({ - import "/src/diagram.typ": * + import chronos: * _seq("Alice", "Bob", comment: "Authentication Request") _seq("Bob", "Alice", comment: "Authentication Response", dashed: true) @@ -19,19 +19,19 @@ Alice <-- Bob: Another authentication Response }) #chronos.diagram({ - import "/src/diagram.typ": * + import chronos: * _seq("Bob", "Alice", comment: "bonjour", color: red) _seq("Alice", "Bob", comment: "ok", color: blue) }) #chronos.diagram({ - import "/src/diagram.typ": * + import chronos: * _seq("Alice", "Bob", comment: "This is a test") _seq("Alice", "Callum", comment: "This is another test with a long text") }) #chronos.diagram({ - import "/src/diagram.typ": * + import chronos: * _seq("Alice", "Bob", comment: "Authentication Request") _seq("Bob", "Alice", comment: "Authentication Failure") @@ -45,7 +45,7 @@ Alice <-- Bob: Another authentication Response }) #chronos.diagram({ - import "/src/diagram.typ": * + import chronos: * _sep("Initialization") _seq("Alice", "Bob", comment: "Authentication Request") _seq("Bob", "Alice", comment: "Authentication Response", dashed: true) @@ -56,7 +56,7 @@ Alice <-- Bob: Another authentication Response }) #chronos.diagram({ - import "/src/diagram.typ": * + import chronos: * _seq("Alice", "Bob", comment: "message 1") _seq("Bob", "Alice", comment: "ok", dashed: true) _gap() @@ -68,7 +68,7 @@ Alice <-- Bob: Another authentication Response }) #chronos.diagram({ - import "/src/diagram.typ": * + import chronos: * _seq("Alice", "Alice", comment: "On the\nright") _seq("Alice", "Alice", flip: true, comment: "On the\nleft") }) \ No newline at end of file diff --git a/gallery/example2.pdf b/gallery/example2.pdf index 364a2ee..d9f55c1 100644 Binary files a/gallery/example2.pdf and b/gallery/example2.pdf differ diff --git a/gallery/example2.typ b/gallery/example2.typ index 8f0e5fb..1496b46 100644 --- a/gallery/example2.typ +++ b/gallery/example2.typ @@ -2,7 +2,7 @@ #chronos.diagram({ - import "/src/diagram.typ": * + 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("B", "C", comment: "DoWork", enable-dst: true) @@ -12,7 +12,7 @@ }) #chronos.diagram({ - import "/src/diagram.typ": * + import chronos: * _seq("User", "A", comment: "DoWork", enable-dst: true) _seq("A", "A", comment: "Internal call", enable-dst: true) _seq("A", "B", comment: [#sym.quote.angle.l createRequest #sym.quote.angle.r], enable-dst: true) @@ -21,7 +21,7 @@ }) #chronos.diagram({ - import "/src/diagram.typ": * + import chronos: * _seq("alice", "bob", comment: "hello", enable-dst: true) _seq("bob", "bob", comment: "self call", enable-dst: true) _seq("bill", "bob", comment: "hello from thread 2", enable-dst: true) @@ -30,4 +30,11 @@ _seq("bob", "bob", comment: "rc", disable-src: true, dashed: true) _seq("bob", "george", comment: "delete", destroy-dst: true) _seq("bob", "alice", comment: "success", disable-src: true, dashed: true) +}) + +#chronos.diagram({ + import chronos: * + _seq("alice", "bob", comment: "hello1", enable-dst: true) + _seq("bob", "charlie", comment: "hello2", enable-dst: true, disable-src: true) + _seq("charlie", "alice", comment: "ok", dashed: true, disable-src: true) }) \ No newline at end of file diff --git a/src/consts.typ b/src/consts.typ new file mode 100644 index 0000000..e393728 --- /dev/null +++ b/src/consts.typ @@ -0,0 +1,5 @@ +#let Y-SPACE = 10 +#let PAR-PAD = (5pt, 3pt) +#let PAR-SPACE = 10 +#let COMMENT-PAD = 8 +#let LIFELINE-W = 10 \ No newline at end of file diff --git a/src/diagram.typ b/src/diagram.typ index cd4c168..9ec2f50 100644 --- a/src/diagram.typ +++ b/src/diagram.typ @@ -1,71 +1,6 @@ #import "utils.typ": get-group-span #import "renderer.typ": render - -#let _seq( - p1, - p2, - comment: none, - dashed: false, - tip: "default", - color: black, - flip: false, - enable-dst: false, - disable-dst: false, - destroy-dst: false, - disable-src: false, - destroy-src: false, -) = { - return (( - type: "seq", - p1: p1, - p2: p2, - comment: comment, - dashed: dashed, - tip: tip, - color: color, - flip: flip, - enable-dst: enable-dst, - disable-dst: disable-dst, - destroy-dst: destroy-dst, - disable-src: disable-src, - destroy-src: destroy-src, - ),) -} - -#let _par(name, display-name: auto, start-at: 0) = { - return (( - type: "par", - name: name, - display-name: if display-name == auto {name} else {display-name}, - start-at: start-at - ),) -} - -#let _par-exists(participants, name) = { - for p in participants { - if name == p.name { - return true - } - } - return false -} - -#let _grp(name, desc: none, type: "default", elmts) = { - return (( - type: "grp", - name: name, - desc: desc, - grp-type: type, - elmts: elmts - ),) -} - -#let _sep(name) = { - return (( - type: "sep", - name: name - ),) -} +#import "participant.typ" as participant: _par #let _gap(size: 20) = { return (( @@ -78,6 +13,8 @@ let participants = () let elmts = elements let i = 0 + + // Flatten groups while i < elmts.len() { let elmt = elmts.at(i) if elmt.type == "grp" { @@ -93,19 +30,21 @@ i += 1 } + // List participants for elmt in elmts { if elmt.type == "par" { participants.push(elmt) } else if elmt.type == "seq" { - if not _par-exists(participants, elmt.p1) { + if not participant._exists(participants, elmt.p1) { participants.push(_par(elmt.p1).first()) } - if not _par-exists(participants, elmt.p2) { + if not participant._exists(participants, elmt.p2) { participants.push(_par(elmt.p2).first()) } } } + // Compute groups spans (horizontal) for (i, elmt) in elmts.enumerate() { if elmt.type == "grp" { let (min-i, max-i) = get-group-span(participants, elmt) diff --git a/src/group.typ b/src/group.typ new file mode 100644 index 0000000..4c20f89 --- /dev/null +++ b/src/group.typ @@ -0,0 +1,52 @@ +#import "@preview/cetz:0.2.2": draw + +#let _grp(name, desc: none, type: "default", elmts) = { + return (( + type: "grp", + name: name, + desc: desc, + grp-type: type, + elmts: elmts + ),) +} + +#let render(x0, x1, y0, y1, group) = { + let shapes = () + let m = measure(box(group.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: gray.lighten(20%), + 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), + group.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"), + anchor: "north-west", + padding: 3pt + ) + } + + return shapes +} \ No newline at end of file diff --git a/src/lib.typ b/src/lib.typ index 2f20eb6..eb85b65 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1 +1,6 @@ -#import "diagram.typ": diagram, from-plantuml \ No newline at end of file +#import "diagram.typ": diagram, from-plantuml, _gap + +#import "sequence.typ": _seq +#import "group.typ": _grp +#import "participant.typ": _par +#import "separator.typ": _sep \ No newline at end of file diff --git a/src/participant.typ b/src/participant.typ new file mode 100644 index 0000000..5a1c6f1 --- /dev/null +++ b/src/participant.typ @@ -0,0 +1,17 @@ +#let _par(name, display-name: auto, start-at: 0) = { + return (( + type: "par", + name: name, + display-name: if display-name == auto {name} else {display-name}, + start-at: start-at + ),) +} + +#let _exists(participants, name) = { + for p in participants { + if name == p.name { + return true + } + } + return false +} \ No newline at end of file diff --git a/src/renderer.typ b/src/renderer.typ index 7bf0f10..07e1341 100644 --- a/src/renderer.typ +++ b/src/renderer.typ @@ -1,11 +1,9 @@ #import "@preview/cetz:0.2.2": canvas, draw #import "utils.typ": get-participants-i - -#let Y-SPACE = 10 -#let PAR-PAD = (5pt, 3pt) -#let PAR-SPACE = 10 -#let COMMENT-PAD = 8 -#let LIFELINE-W = 10 +#import "group.typ" +#import "sequence.typ" +#import "separator.typ" +#import "consts.typ": * #let get-columns-width(participants, elements) = { @@ -16,6 +14,8 @@ }) let pars-i = get-participants-i(participants) let cells = () + + // Compute max lifeline levels for elmt in elements { if elmt.type == "seq" { let com = if elmt.comment == none {""} else {elmt.comment} @@ -49,6 +49,8 @@ } } + // Compute column widths + // Compute minimum widths for participant names let widths = () for i in range(participants.len() - 1) { let p1 = participants.at(i) @@ -58,6 +60,7 @@ widths.push(w1 / 2pt + w2 / 2pt + PAR-SPACE) } + // 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( @@ -66,6 +69,7 @@ ) } + // 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 @@ -77,6 +81,8 @@ m.width / 1pt - widths.slice(0, cell.i2 - 1).sum() ) } + + // Add lifeline widths for (i, w) in widths.enumerate() { let p1 = participants.at(i) let p2 = participants.at(i + 1) @@ -89,57 +95,21 @@ return widths } -#let draw-group(x0, x1, y0, y1, group) = { - let m = measure(box(group.name)) - let w = m.width / 1pt + 15 - let h = m.height / 1pt + 6 - draw.rect( - (x0, y0), - (x1, y1) - ) - draw.merge-path( - fill: gray.lighten(20%), - close: true, - { - draw.line( - (x0, y0), - (x0 + w, y0), - (x0 + w, y0 - h / 2), - (x0 + w - 5, y0 - h), - (x0, y0 - h) - ) - } - ) - draw.content( - (x0, y0), - group.name, - anchor: "north-west", - padding: (left: 5pt, right: 10pt, top: 3pt, bottom: 3pt) - ) - - if group.desc != none { - draw.content( - (x0 + w, y0), - text([\[#group.desc\]], weight: "bold"), - anchor: "north-west", - padding: 3pt - ) - } -} - #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) } - // Draw participants + // Draw participants (start) for (i, p) in participants.enumerate() { - draw.content( + shapes += draw.content( (x-pos.at(i), 0), p.display-name, name: p.name, @@ -156,124 +126,23 @@ lines: () )) - // Draw sequences + let draw-seq = sequence.render.with(pars-i, x-pos) + let draw-group = group.render.with() + let draw-sep = separator.render.with(x-pos) + + // Draw elemnts for elmt in elements { + // Sequences if elmt.type == "seq" { - let i1 = pars-i.at(elmt.p1) - let i2 = pars-i.at(elmt.p2) - - if elmt.comment != none { - y -= measure(box(elmt.comment)).height / 1pt + 6 - } - - if elmt.disable-src { - let src-line = lifelines.at(i1) - src-line.level -= 1 - src-line.lines.push(("disable", y, auto)) - lifelines.at(i1) = src-line - } - if elmt.destroy-src { - let src-line = lifelines.at(i1) - src-line.level -= 1 - src-line.lines.push(("destroy", y, auto)) - lifelines.at(i1) = src-line - } - - let ll-lvl1 = lifelines.at(i1).level * LIFELINE-W / 2 - - if elmt.disable-dst { - let dst-line = lifelines.at(i2) - dst-line.level -= 1 - dst-line.lines.push(("disable", y, auto)) - lifelines.at(i2) = dst-line - } - if elmt.destroy-dst { - let dst-line = lifelines.at(i2) - dst-line.level -= 1 - dst-line.lines.push(("destroy", y, auto)) - lifelines.at(i2) = dst-line - } - if elmt.enable-dst { - let dst-line = lifelines.at(i2) - dst-line.level += 1 - lifelines.at(i2) = dst-line - } - - let x1 = x-pos.at(i1) - let x2 = x-pos.at(i2) - - let ll-lvl2 = lifelines.at(i2).level * LIFELINE-W / 2 - - let f = if elmt.flip {-1} else {1} - if i1 <= i2 { - x1 += ll-lvl1 * f - x2 -= ll-lvl2 * f - } else { - x1 -= ll-lvl1 * f - x2 += ll-lvl2 * f - } - - let style = ( - mark: (end: "straight"), - stroke: ( - dash: if elmt.dashed {"dashed"} else {"solid"}, - paint: elmt.color - ) - ) - - if elmt.p1 == elmt.p2 { - let x3 = x1 - ll-lvl1 + ll-lvl2 - - x2 = if elmt.flip {x1 - 20} else {x1 + 20} - - if elmt.comment != none { - draw.content( - (x1, y), - elmt.comment, - anchor: if elmt.flip {"south-east"} else {"south-west"}, - padding: 3pt - ) - } - - draw.line( - (x1, y), - (x2, y), - (x2, y - 10), - (x3, y - 10), - ..style - ) - y -= 10 - - } else { - if elmt.comment != none { - let x = calc.min(x1, x2) - if x2 < x1 { - x += COMMENT-PAD - } - draw.content( - (x, y), - elmt.comment, - anchor: "south-west", - padding: 3pt - ) - } - - draw.line( - (x1, y), - (x2, y), - ..style - ) - } - if elmt.enable-dst { - let dst-line = lifelines.at(i2) - dst-line.lines.push(("enable", y, auto)) - lifelines.at(i2) = dst-line - } - y -= Y-SPACE + 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" { let m = measure( box( + elmt.name, inset: (left: 5pt, right: 5pt, top: 3pt, bottom: 3pt), ) ) @@ -285,53 +154,33 @@ groups.push((y, elmt, 0, 0)) y -= m.height / 1pt + Y-SPACE + // Groups (end) -> actual drawing } else if elmt.type == "grp-end" { 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 - draw-group(x0, x1, start-y, y, group) + shapes += draw-group(x0, x1, start-y, y, group) y -= Y-SPACE + // Separator } else if elmt.type == "sep" { - 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 - draw.line((x0, y), (xl, y)) - draw.line((xr, y), (x1, y)) - y -= 3 - draw.line((x0, y), (xl, y)) - draw.line((xr, y), (x1, y)) - draw.content( - ((x0 + x1) / 2, y + 1.5), - elmt.name, - anchor: "center", - padding: (5pt, 3pt), - frame: "rect" - ) - y -= h / 2 - y -= Y-SPACE + let shps + (y, shps) = draw-sep(elmt, y) + shapes += shps + + // Gap } else if elmt.type == "gap" { y -= elmt.size } } - // Draw vertical lines + end participants - draw.on-layer(-1, { + // Draw vertical lines + lifelines + end participants + shapes += draw.on-layer(-1, { for (i, p) in participants.enumerate() { let x = x-pos.at(i) + + // Draw vertical line draw.line( (x, 0), (x, y), @@ -341,10 +190,13 @@ let rects = () let destructions = () let lines = () + + // Compute lifeline rectangles + destruction positions for line in lifelines.at(i).lines { let event = line.first() if event == "enable" { lines.push(line) + } else if event == "disable" or event == "destroy" { let l = lines.pop() let lvl = lines.len() @@ -355,19 +207,23 @@ } } + // Draw lifeline rectangles (reverse for bottom to top) for rect in rects.rev() { let (cx, y0, y1) = rect draw.rect( (cx - LIFELINE-W / 2, y0), - (cx + LIFELINE-W / 2, y1), + (cx + LIFELINE-W / 2, y1) ) } + + // Draw lifeline destructions for dest in destructions { let (cx, cy) = dest draw.line((cx - 8, cy - 8), (cx + 8, cy + 8), stroke: red + 2pt) draw.line((cx - 8, cy + 8), (cx + 8, cy - 8), stroke: red + 2pt) } + // Draw participants (end) draw.content( (x, y), p.display-name, @@ -378,4 +234,6 @@ ) } }) + + shapes }) \ No newline at end of file diff --git a/src/separator.typ b/src/separator.typ new file mode 100644 index 0000000..eed197a --- /dev/null +++ b/src/separator.typ @@ -0,0 +1,46 @@ +#import "@preview/cetz:0.2.2": draw +#import "consts.typ": * + +#let _sep(name) = { + return (( + type: "sep", + name: name + ),) +} + +#let render(x-pos, elmt, y) = { + let shapes = () + + 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.line((x0, y), (xl, y)) + shapes += draw.line((xr, y), (x1, y)) + y -= 3 + 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" + ) + y -= h / 2 + y -= Y-SPACE + + let r = (y, shapes) + return r +} \ No newline at end of file diff --git a/src/sequence.typ b/src/sequence.typ new file mode 100644 index 0000000..beb2256 --- /dev/null +++ b/src/sequence.typ @@ -0,0 +1,152 @@ +#import "consts.typ": * +#import "@preview/cetz:0.2.2": draw + +#let _seq( + p1, + p2, + comment: none, + dashed: false, + tip: "default", + color: black, + flip: false, + enable-dst: false, + disable-dst: false, + destroy-dst: false, + disable-src: false, + destroy-src: false, +) = { + return (( + type: "seq", + p1: p1, + p2: p2, + comment: comment, + dashed: dashed, + tip: tip, + color: color, + flip: flip, + enable-dst: enable-dst, + disable-dst: disable-dst, + destroy-dst: destroy-dst, + disable-src: disable-src, + destroy-src: destroy-src, + ),) +} + +#let render(pars-i, x-pos, elmt, y, lifelines) = { + let shapes = () + + let i1 = pars-i.at(elmt.p1) + let i2 = pars-i.at(elmt.p2) + + if elmt.comment != none { + y -= measure(box(elmt.comment)).height / 1pt + 6 + } + + if elmt.disable-src { + let src-line = lifelines.at(i1) + src-line.level -= 1 + src-line.lines.push(("disable", y, auto)) + lifelines.at(i1) = src-line + } + if elmt.destroy-src { + let src-line = lifelines.at(i1) + src-line.level -= 1 + src-line.lines.push(("destroy", y, auto)) + lifelines.at(i1) = src-line + } + + let ll-lvl1 = lifelines.at(i1).level * LIFELINE-W / 2 + + if elmt.disable-dst { + let dst-line = lifelines.at(i2) + dst-line.level -= 1 + dst-line.lines.push(("disable", y, auto)) + lifelines.at(i2) = dst-line + } + if elmt.destroy-dst { + let dst-line = lifelines.at(i2) + dst-line.level -= 1 + dst-line.lines.push(("destroy", y, auto)) + lifelines.at(i2) = dst-line + } + if elmt.enable-dst { + let dst-line = lifelines.at(i2) + dst-line.level += 1 + lifelines.at(i2) = dst-line + } + + let x1 = x-pos.at(i1) + let x2 = x-pos.at(i2) + + let ll-lvl2 = lifelines.at(i2).level * LIFELINE-W / 2 + + let f = if elmt.flip {-1} else {1} + if i1 <= i2 { + x1 += ll-lvl1 * f + x2 -= ll-lvl2 * f + } else { + x1 -= ll-lvl1 * f + x2 += ll-lvl2 * f + } + + let style = ( + mark: (end: "straight"), + stroke: ( + dash: if elmt.dashed {"dashed"} else {"solid"}, + paint: elmt.color + ) + ) + + if elmt.p1 == elmt.p2 { + let x3 = x1 - ll-lvl1 + ll-lvl2 + + x2 = if elmt.flip {x1 - 20} else {x1 + 20} + + if elmt.comment != none { + shapes += draw.content( + (x1, y), + elmt.comment, + anchor: if elmt.flip {"south-east"} else {"south-west"}, + padding: 3pt + ) + } + + shapes += draw.line( + (x1, y), + (x2, y), + (x2, y - 10), + (x3, y - 10), + ..style + ) + y -= 10 + + } else { + if elmt.comment != none { + let x = calc.min(x1, x2) + if x2 < x1 { + x += COMMENT-PAD + } + shapes += draw.content( + (x, y), + elmt.comment, + anchor: "south-west", + padding: 3pt + ) + } + + shapes += draw.line( + (x1, y), + (x2, y), + ..style + ) + } + if elmt.enable-dst { + let dst-line = lifelines.at(i2) + dst-line.lines.push(("enable", y, auto)) + lifelines.at(i2) = dst-line + } + y -= Y-SPACE + + let r = (y, lifelines, shapes) + return r +} \ No newline at end of file