Compare commits

..

2 Commits

Author SHA1 Message Date
0cff9d6799
implemented aligned notes 2024-06-21 17:26:50 +02:00
f08f30b9e2
added notes 2024-06-21 15:59:35 +02:00
9 changed files with 467 additions and 11 deletions

BIN
gallery/gitea.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
gallery/notes.pdf Normal file

Binary file not shown.

138
gallery/notes.typ Normal file
View File

@ -0,0 +1,138 @@
#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")
_par("c", display-name: "Charlie")
_par("d", display-name: "Donald")
_par("e", display-name: "Eddie")
_note("over", [initial state of Alice], pos: "a")
_note("over", [initial state of Bob the builder], pos: "b", aligned: true)
_note("over", [Note 1], pos: "a")
_note("over", [Note 2], pos: "b", aligned: true)
_note("over", [Note 3], pos: "c", aligned: true)
_seq("a", "d")
_note("over", [this is an extremely long note], pos: ("d", "e"))
})
#pagebreak()
#chronos.diagram({
_par("a", display-name: [Alice])
_par("b", display-name: [The *Famous* Bob])
_seq("a", "b", comment: [hello #strike([there])])
_gap()
_seq("b", "a", comment: [ok])
_note("left", [
This is *bold*\
This is _italics_\
This is `monospaced`\
This is #strike([stroked])\
This is #underline([underlined])\
This is #underline([waved])\
])
_seq("a", "b", comment: [A _well formatted_ message])
_note("right", [
This is #box(text([displayed], size: 18pt), fill: rgb("#5F9EA0"))\
#underline([left of]) Alice.
], pos: "a")
_note("left", [
#underline([This], stroke: red) is #text([displayed], fill: rgb("#118888"))\
*#text([left of], fill: rgb("#800080")) #strike([Alice], stroke: red) Bob.*
], pos: "b")
_note("over", [
#underline([This is hosted], stroke: rgb("#FF33FF")) by #box(baseline: 50%, image("gitea.png", width: 1cm, height: 1cm, fit: "contain"))
], pos: ("a", "b"))
})
// TODO
/*
#pagebreak()
#chronos.diagram({
_par("a", display-name: [Alice])
_par("b", display-name: [Bob])
_seq("a", "b", comment: [Hello])
_note("left", [This is a note])
_seq("[", "a", comment: [Test])
_note("left", [This is also a note])
})*/

View File

@ -15,6 +15,12 @@
#let COLLECTIONS-DY = 3 #let COLLECTIONS-DY = 3
#let QUEUE-PAD = (5pt, 3pt) #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-DESTRUCTION = rgb("#A80238")
#let COL-GRP-NAME = rgb("#EEEEEE") #let COL-GRP-NAME = rgb("#EEEEEE")
#let COL-SEP-NAME = rgb("#EEEEEE") #let COL-SEP-NAME = rgb("#EEEEEE")
#let COL-NOTE = rgb("#FEFFDD")

View File

@ -57,7 +57,9 @@
// List participants // List participants
let linked = () let linked = ()
for elmt in elmts { let last-seq = none
let last-note = none
for (i, elmt) in elmts.enumerate() {
if elmt.type == "par" { if elmt.type == "par" {
participants.push(elmt) participants.push(elmt)
} else if elmt.type == "seq" { } else if elmt.type == "seq" {
@ -73,16 +75,54 @@
participants.at(i).from-start = false participants.at(i).from-start = false
} }
let p1 = elmt.p1
let p2 = elmt.p2
if elmt.p1 == "?" { if elmt.p1 == "?" {
linked.push("?" + elmt.p2) p1 = "?" + elmt.p2
} else {
linked.push(elmt.p1)
} }
if elmt.p2 == "?" { if elmt.p2 == "?" {
linked.push(elmt.p1 + "?") p2 = elmt.p1 + "?"
} else {
linked.push(elmt.p2)
} }
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("]")
}
last-note = (
elmt: elmt,
i: i
)
} }
} }
linked = linked.dedup() linked = linked.dedup()
@ -137,6 +177,7 @@
} }
} }
set text(font: "Source Sans 3")
render(participants, elmts) render(participants, elmts)
} }

View File

@ -4,3 +4,4 @@
#import "group.typ": _grp #import "group.typ": _grp
#import "participant.typ": _par #import "participant.typ": _par
#import "separator.typ": _sep #import "separator.typ": _sep
#import "note.typ": _note

182
src/note.typ Normal file
View File

@ -0,0 +1,182 @@
#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) = {
if side == "over" {
if pos == none {
panic("Pos cannot be none with side 'over'")
}
}
if aligned {
if side != "over" {
panic("Aligned notes can only be over a participant (got side '" + side + "')")
}
}
return ((
type: "note",
side: side,
content: content,
pos: pos,
color: color,
shape: shape,
aligned: aligned,
aligned-with: none
),)
}
#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
}

View File

@ -6,6 +6,7 @@
#import "sequence.typ" #import "sequence.typ"
#import "separator.typ" #import "separator.typ"
#import "consts.typ": * #import "consts.typ": *
#import "note.typ" as note: get-note-box
#let DEBUG-INVISIBLE = false #let DEBUG-INVISIBLE = false
@ -61,6 +62,42 @@
par.max-lifelines = calc.max(par.max-lifelines, par.lifeline-lvl) par.max-lifelines = calc.max(par.max-lifelines, par.lifeline-lvl)
} }
participants.at(i) = par 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
)
)
}
} }
} }
@ -77,6 +114,28 @@
widths.push(w1 / 2pt + w2 / 2pt + PAR-SPACE) 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) // Compute minimum width for simple sequences (spanning 1 column)
for cell in cells.filter(c => c.i2 - c.i1 == 1) { for cell in cells.filter(c => c.i2 - c.i1 == 1) {
let m = measure(cell.cell) let m = measure(cell.cell)
@ -87,7 +146,7 @@
} }
// Compute minimum width for self sequences // Compute minimum width for self sequences
for cell in cells.filter(c => c.i1 == c.i2) { for cell in cells.filter(c => c.elmt.type == "seq" and c.i1 == c.i2) {
let m = measure(cell.cell) let m = measure(cell.cell)
let i = cell.i1 let i = cell.i1
if cell.elmt.flip { if cell.elmt.flip {
@ -143,6 +202,7 @@
let draw-group = group.render.with() let draw-group = group.render.with()
let draw-sep = separator.render.with(x-pos) let draw-sep = separator.render.with(x-pos)
let draw-par = participant.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) // Draw participants (start)
for p in participants { for p in participants {
@ -223,6 +283,17 @@
line.lines.push(("create", y)) line.lines.push(("create", y))
} }
lifelines.at(i) = line 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
}
} }
} }

View File

@ -1,6 +1,7 @@
#import "@preview/cetz:0.2.2": draw #import "@preview/cetz:0.2.2": draw
#import "consts.typ": * #import "consts.typ": *
#import "participant.typ" #import "participant.typ"
#import "note.typ"
#let get-arrow-marks(sym, color) = { #let get-arrow-marks(sym, color) = {
if type(sym) == array { if type(sym) == array {
@ -61,10 +62,15 @@
y -= Y-SPACE y -= Y-SPACE
let h = 0
// Reserve space for comment // Reserve space for comment
if elmt.comment != none { 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 i1 = pars-i.at(elmt.p1)
let i2 = pars-i.at(elmt.p2) 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.p1 == elmt.p2 {
if elmt.flip { if elmt.flip {
x1 = start-info.lx x1 = start-info.lx
@ -210,6 +222,11 @@
lifelines.at(i2) = dst-line 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) let r = (y, lifelines, shapes)
return r return r
} }