diff --git a/src/cetz.typ b/src/cetz.typ index 980a619..eb7ec62 100644 --- a/src/cetz.typ +++ b/src/cetz.typ @@ -1 +1 @@ -#import "@preview/cetz:0.3.4": * \ No newline at end of file +#import "@preview/cetz:0.4.0": * \ 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..25aafaf --- /dev/null +++ b/src/core/draw/group.typ @@ -0,0 +1,117 @@ +#import "/src/cetz.typ": draw + +#import "/src/consts.typ": * +#import "/src/core/utils.typ": get-ctx, set-ctx + +#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.at(1).min-i == grp.min-i { g.at(2) += 1 } + if g.at(1).max-i == grp.max-i { g.at(3) += 1 } + g + }) + if grp.grp-type == "alt" { + grp.insert("elses", ()) + } + ctx.groups.push((ctx.y, grp, 0, 0)) + 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 = text(group.name, weight: "bold") + let m = measure(box(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), + text([\[#group.desc\]], weight: "bold", size: .8em), + 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), + text([\[#elmt.desc\]], weight: "bold", size: .8em), + anchor: "north-west", + padding: 3pt + ) +} + +#let render-end(group) = get-ctx(ctx => { + ctx.y -= Y-SPACE + let (start-y, group, start-lvl, end-lvl) = ctx.groups.pop() + let x0 = ctx.x-pos.at(group.min-i) - start-lvl * 10 - 20 + let x1 = ctx.x-pos.at(group.max-i) + end-lvl * 10 + 20 + + 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 + }) +}) + +#let render-else(else_) = set-ctx(ctx => { + ctx.y -= Y-SPACE + let m = measure(text([\[#else_.desc\]], weight: "bold", size: .8em)) + ctx.groups.last().at(1).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..d8b353d --- /dev/null +++ b/src/core/draw/note.typ @@ -0,0 +1,163 @@ +#import "/src/cetz.typ": draw + +#import "/src/consts.typ": * +#import "/src/core/utils.typ": get-ctx, set-ctx + +#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 + }) + } + }) +} \ 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..e93d715 --- /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 {"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(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..0d0ae47 --- /dev/null +++ b/src/core/draw/sequence.typ @@ -0,0 +1,361 @@ +#import "/src/cetz.typ": draw, vector + +#import "note.typ" +#import "/src/consts.typ": * +#import "/src/core/utils.typ": get-ctx, set-ctx + +#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) + } + if "linked-note" in seq { + h = calc.max(h, note.get-size(seq.linked-note).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("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(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 + if "linked-note" in seq { + // TODO: adapt note.render + (seq.linked-note.draw)(seq.linked-note, 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) + } + + } 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) + } + + // 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 + ) + } + + 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 "linked-note" in seq { + let m = note.get-size(seq.linked-note) + end-info.y = calc.min(end-info.y, y0 - m.height / 2) + } + + set-ctx(c => { + c.y = end-info.y + c.lifelines = ctx.lifelines + return c + }) +}) diff --git a/src/core/draw/sync.typ b/src/core/draw/sync.typ new file mode 100644 index 0000000..1a300a7 --- /dev/null +++ b/src/core/draw/sync.typ @@ -0,0 +1,32 @@ +#import "/src/core/utils.typ": get-ctx, is-elmt, set-ctx + +#let render(sync) = get-ctx(ctx => { + set-ctx(c => { + c.sync-ys = () + return c + }) + + 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 => { + c.y = ctx.y + return c + }) + (e.draw)(e) + set-ctx(c => { + c.sync-ys.push(c.y) + return c + }) + } + + set-ctx(c => { + c.y = calc.min(..c.sync-ys) + c.remove("sync-ys") + 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..1b5418c --- /dev/null +++ b/src/core/renderer.typ @@ -0,0 +1,402 @@ +#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 "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 unwrap-syncs(elements) = { + 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 + } + return elements +} + +#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 = "[" + p2 = note.pos + cell = get-note-box(note) + } else if note.side == "right" { + p1 = note.pos + p2 = "]" + 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(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 width = ( + m.width / 1pt + + COMMENT-PAD - + widths.slice(cell.i1, cell.i2 - 1).sum() + ) + widths.at(cell.i2 - 1) = calc.max( + widths.at(cell.i2 - 1), 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) + elements = unwrap-syncs(elements) + + 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(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: () + )) + ) + 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" { + (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..3d8f7fe --- /dev/null +++ b/src/core/setup.typ @@ -0,0 +1,320 @@ +#import "draw/group.typ": render-end as grp-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 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 == "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" + ) + if note.pos == none and note.side != "across" { + let names = ctx.participants.map(p => p.name) + 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.last-seq.seq + seq.insert("linked-note", 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 == "left" { + ctx.linked.push("[") + } else if note.side == "right" { + ctx.linked.push("]") + } + + 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 79% rename from src/utils.typ rename to src/core/utils.typ index e8d750d..7698037 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 @@ -79,4 +91,17 @@ 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) }) \ 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..8e29d6f 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 @@ -40,60 +41,3 @@ } #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 diff --git a/src/lib.typ b/src/lib.typ index ccae827..b972768 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,9 +1,8 @@ #let version = version(0, 2, 2) -#import "diagram.typ": diagram, from-plantuml, _gap, _evt, _col +#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..86eb4a0 --- /dev/null +++ b/src/misc.typ @@ -0,0 +1,64 @@ +#import "core/draw/delay.typ" +#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) = { + return (( + type: "evt", + draw: evt-render, + 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 + ),) +} \ No newline at end of file diff --git a/src/note.typ b/src/note.typ index 14d3c98..948b8d5 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", @@ -30,6 +30,7 @@ } return (( type: "note", + draw: note.render, side: side, content: content, pos: pos, @@ -39,147 +40,3 @@ 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 -} \ No newline at end of file diff --git a/src/participant.typ b/src/participant.typ index aa38c37..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", @@ -36,6 +35,7 @@ } return (( type: "par", + draw: participant.render, name: name, display-name: if display-name == auto {name} else {display-name}, from-start: from-start, @@ -50,7 +50,7 @@ } #let _exists(participants, name) = { - if name == "?" or name == "[" or name == "]" { + if name in PAR-SPECIALS { return true } @@ -61,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 205ca58..0000000 --- a/src/renderer.typ +++ /dev/null @@ -1,510 +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: 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, 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 { - 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..c99df3a 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, @@ -68,6 +21,7 @@ ) = { return (( type: "seq", + draw: sequence.render, p1: p1, p2: p2, comment: comment, @@ -94,294 +48,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/group/ref/1.png b/tests/group/ref/1.png index 4c6b690..8ee09a4 100644 Binary files a/tests/group/ref/1.png and b/tests/group/ref/1.png differ diff --git a/tests/group/ref/2.png b/tests/group/ref/2.png index 6628385..070f238 100644 Binary files a/tests/group/ref/2.png and b/tests/group/ref/2.png differ diff --git a/tests/group/ref/3.png b/tests/group/ref/3.png index 3c8b5bb..87c8628 100644 Binary files a/tests/group/ref/3.png and b/tests/group/ref/3.png differ diff --git a/tests/note/position/ref/1.png b/tests/note/position/ref/1.png index 4ac364d..276c4b4 100644 Binary files a/tests/note/position/ref/1.png 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 index fb6618c..c0c6d78 100644 Binary files a/tests/note/position/ref/2.png 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 index 06d4a60..c4091b2 100644 Binary files a/tests/note/position/ref/3.png and b/tests/note/position/ref/3.png differ diff --git a/tests/note/shape/ref/1.png b/tests/note/shape/ref/1.png index e9bff45..0baabbe 100644 Binary files a/tests/note/shape/ref/1.png and b/tests/note/shape/ref/1.png differ diff --git a/tests/note/styled-body/ref/1.png b/tests/note/styled-body/ref/1.png index 49b7d78..e10a0de 100644 Binary files a/tests/note/styled-body/ref/1.png and b/tests/note/styled-body/ref/1.png differ diff --git a/tests/sequence/comment-align/ref/1.png b/tests/sequence/comment-align/ref/1.png index c3a1f6c..1cdb569 100644 Binary files a/tests/sequence/comment-align/ref/1.png 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 index 9823f8b..31185af 100644 Binary files a/tests/sequence/comment-align/ref/2.png and b/tests/sequence/comment-align/ref/2.png differ diff --git a/tests/sequence/special-par/ref/1.png b/tests/sequence/special-par/ref/1.png index aa44644..1259a7e 100644 Binary files a/tests/sequence/special-par/ref/1.png and b/tests/sequence/special-par/ref/1.png differ diff --git a/tests/sequence/tips/ref/1.png b/tests/sequence/tips/ref/1.png index c3753c5..00cff5d 100644 Binary files a/tests/sequence/tips/ref/1.png 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 index 7bba0e4..3725245 100644 Binary files a/tests/sequence/tips/ref/2.png 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 index 85bc2c2..6c12338 100644 Binary files a/tests/sequence/tips/ref/3.png and b/tests/sequence/tips/ref/3.png differ