diff --git a/gallery/gitea.png b/gallery/gitea.png new file mode 100644 index 0000000..7d2874c Binary files /dev/null and b/gallery/gitea.png differ diff --git a/gallery/notes.pdf b/gallery/notes.pdf new file mode 100644 index 0000000..7e65f04 Binary files /dev/null and b/gallery/notes.pdf differ diff --git a/gallery/notes.typ b/gallery/notes.typ new file mode 100644 index 0000000..f2f20fa --- /dev/null +++ b/gallery/notes.typ @@ -0,0 +1,114 @@ +#import "/src/lib.typ" as chronos: * + +#set page(width: auto, height: auto) +#chronos.diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _seq("a", "b", comment: [hello]) + _note("left", [this is a first note]) + + _seq("b", "a", comment: [ok]) + _note("right", [this is another note]) + + _seq("b", "b", comment: [I am thinking]) + _note("left", [a note\ can also be defined\ on several lines]) +}) + +#pagebreak() + +#chronos.diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _note("left", [This is displayed\ left of Alice.], pos: "a", color: rgb("#00FFFF")) + _note("right", [This is displayed right of Alice.], pos: "a") + _note("over", [This is displayed over Alice.], pos: "a") + _note("over", [This is displayed\ over Bob and Alice.], pos: ("a", "b"), color: rgb("#FFAAAA")) + _note("over", [This is yet another\ example of\ a long note.], pos: ("a", "b")) +}) + +#pagebreak() + +#chronos.diagram({ + _par("caller") + _par("server") + + _seq("caller", "server", comment: [conReq]) + _note("over", [idle], pos: "caller", shape: "hex") + _seq("server", "caller", comment: [conConf]) + _note("over", ["r" as rectangle\ "h" as hexagon], pos: "server", shape: "rect") + _note("over", [this is\ on several\ lines], pos: "server", shape: "rect") + _note("over", [this is\ on several\ lines], pos: "caller", shape: "hex") +}) + +#pagebreak() + +#chronos.diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + _par("c", display-name: "Charlie") + + _seq("a", "b", comment: [m1]) + _seq("b", "c", comment: [m2]) + + _note("over", [Old method for note over all part. with:\ `note over FirstPart, LastPart`.], pos: ("a", "c")) + _note("across", [New method with:\ `note across`.]) + + _seq("b", "a") + + _note("across", [Note across all part.], shape: "hex") +}) + +#pagebreak() + +#chronos.diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _note("over", [initial state of Alice], pos: "a") + _note("over", [initial state of Bob], pos: "b") + _seq("b", "a", comment: [hello]) +}) + +#chronos.diagram({ + _par("a", display-name: "Alice") + _par("b", display-name: "Bob") + + _note("over", [initial state of Alice], pos: "a") + _note("over", [initial state of Bob], pos: "b", aligned: true) + _seq("b", "a", comment: [hello]) +}) + +#pagebreak() + +#chronos.diagram({ + _par("a", display-name: [Alice]) + _par("b", display-name: [The *Famous* Bob]) + + _seq("a", "b", comment: [hello #strike([there])]) + + _gap() + _seq("b", "a", comment: [ok]) + _note("left", [ + This is *bold*\ + This is _italics_\ + This is `monospaced`\ + This is #strike([stroked])\ + This is #underline([underlined])\ + This is #underline([waved])\ + ]) + + _seq("a", "b", comment: [A _well formatted_ message]) + _note("right", [ + This is #box(text([displayed], size: 18pt), fill: rgb("#5F9EA0"))\ + #underline([left of]) Alice. + ], pos: "a") + _note("left", [ + #underline([This], stroke: red) is #text([displayed], fill: rgb("#118888"))\ + *#text([left of], fill: rgb("#800080")) #strike([Alice], stroke: red) Bob.* + ], pos: "b") + _note("over", [ + #underline([This is hosted], stroke: rgb("#FF33FF")) by #box(baseline: 50%, image("gitea.png", width: 1cm, height: 1cm, fit: "contain")) + ], pos: ("a", "b")) +}) \ No newline at end of file diff --git a/src/consts.typ b/src/consts.typ index bac5d0c..8db4541 100644 --- a/src/consts.typ +++ b/src/consts.typ @@ -15,6 +15,12 @@ #let COLLECTIONS-DY = 3 #let QUEUE-PAD = (5pt, 3pt) +#let NOTE-PAD = (6, 3) +#let NOTE-CORNER-SIZE = 6 +#let NOTE-GAP = 3 +#let NOTE-HEX-PAD = (6, 8) + #let COL-DESTRUCTION = rgb("#A80238") #let COL-GRP-NAME = rgb("#EEEEEE") -#let COL-SEP-NAME = rgb("#EEEEEE") \ No newline at end of file +#let COL-SEP-NAME = rgb("#EEEEEE") +#let COL-NOTE = rgb("#FEFFDD") \ No newline at end of file diff --git a/src/diagram.typ b/src/diagram.typ index c2c2656..48685dc 100644 --- a/src/diagram.typ +++ b/src/diagram.typ @@ -57,7 +57,8 @@ // List participants let linked = () - for elmt in elmts { + let last-seq = none + for (i, elmt) in elmts.enumerate() { if elmt.type == "par" { participants.push(elmt) } else if elmt.type == "seq" { @@ -73,15 +74,44 @@ participants.at(i).from-start = false } + let p1 = elmt.p1 + let p2 = elmt.p2 if elmt.p1 == "?" { - linked.push("?" + elmt.p2) - } else { - linked.push(elmt.p1) + p1 = "?" + elmt.p2 } if elmt.p2 == "?" { - linked.push(elmt.p1 + "?") - } else { - linked.push(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 + } + elmts.at(i) = elmt + if elmt.side == "left" { + linked.push("[") + } else if elmt.side == "right" { + linked.push("]") } } } @@ -137,6 +167,7 @@ } } + set text(font: "Source Sans 3") render(participants, elmts) } diff --git a/src/lib.typ b/src/lib.typ index 8574fbd..f44c8e9 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -3,4 +3,5 @@ #import "sequence.typ": _seq #import "group.typ": _grp #import "participant.typ": _par -#import "separator.typ": _sep \ No newline at end of file +#import "separator.typ": _sep +#import "note.typ": _note \ No newline at end of file diff --git a/src/note.typ b/src/note.typ new file mode 100644 index 0000000..39281da --- /dev/null +++ b/src/note.typ @@ -0,0 +1,171 @@ +#import "@preview/cetz:0.2.2": draw +#import "consts.typ": * + +#let SIDES = ( + "left", + "right", + "over", + "across" +) + +#let SHAPES = ( + "default", + "rect", + "hex" +) + +#let _note(side, content, pos: none, color: COL-NOTE, shape: "default", aligned: false) = { + return (( + type: "note", + side: side, + content: content, + pos: pos, + color: color, + shape: shape, + aligned: aligned + ),) +} + +#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 xs.sum() / xs.len() + } + } + 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.pos != none or note.side == "across" { + y -= h + } + + let r = (y, shapes) + return r +} \ No newline at end of file diff --git a/src/renderer.typ b/src/renderer.typ index bd78e8b..540a4ce 100644 --- a/src/renderer.typ +++ b/src/renderer.typ @@ -6,6 +6,7 @@ #import "sequence.typ" #import "separator.typ" #import "consts.typ": * +#import "note.typ" as note: get-note-box #let DEBUG-INVISIBLE = false @@ -61,6 +62,29 @@ 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) + if elmt.side == "left" { + p1 = "[" + p2 = elmt.pos + } else if elmt.side == "right" { + p1 = elmt.pos + p2 = "]" + } + + if p1 != none and p2 != none { + let i1 = pars-i.at(p1) + let i2 = pars-i.at(p2) + cells.push( + ( + elmt: elmt, + i1: i1, + i2: i2, + cell: get-note-box(elmt) + ) + ) + } } } @@ -143,6 +167,7 @@ let draw-group = group.render.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) // Draw participants (start) for p in participants { @@ -223,6 +248,15 @@ line.lines.push(("create", y)) } lifelines.at(i) = line + + // Note + } else if elmt.type == "note" { + if not elmt.linked { + y -= Y-SPACE + let shps + (y, shps) = draw-note(elmt, y, lifelines) + shapes += shps + } } } diff --git a/src/sequence.typ b/src/sequence.typ index 23dd557..cc81393 100644 --- a/src/sequence.typ +++ b/src/sequence.typ @@ -1,6 +1,7 @@ #import "@preview/cetz:0.2.2": draw #import "consts.typ": * #import "participant.typ" +#import "note.typ" #let get-arrow-marks(sym, color) = { if type(sym) == array { @@ -61,10 +62,15 @@ y -= Y-SPACE + let h = 0 // Reserve space for comment if elmt.comment != none { - y -= measure(box(elmt.comment)).height / 1pt + 6 + h = calc.max(h, measure(box(elmt.comment)).height / 1pt + 6) } + if "linked-note" in elmt { + h = calc.max(h, note.get-size(elmt.linked-note).height / 2) + } + y -= h let i1 = pars-i.at(elmt.p1) let i2 = pars-i.at(elmt.p2) @@ -148,6 +154,12 @@ ) ) + let y0 = y + if "linked-note" in elmt { + let shps = note.render(pars-i, x-pos, elmt.linked-note, y, lifelines).last() + shapes += shps + } + if elmt.p1 == elmt.p2 { if elmt.flip { x1 = start-info.lx @@ -210,6 +222,11 @@ lifelines.at(i2) = dst-line } + if "linked-note" in elmt { + let m = note.get-size(elmt.linked-note) + y = calc.min(y, y0 - m.height / 2) + } + let r = (y, lifelines, shapes) return r } \ No newline at end of file