From 3cc600d05829a57821b034cd6794300b2d53bbdd Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Mon, 14 Jul 2025 16:30:25 +0200 Subject: [PATCH] refactored sequences, sync and gaps --- src/core/draw/event.typ | 31 +++ src/core/draw/participant.typ | 405 ++++++++++++++++++++++++++++++++++ src/core/draw/sequence.typ | 360 ++++++++++++++++++++++++++++++ src/core/draw/sync.typ | 32 +++ src/core/renderer.typ | 44 +--- src/core/utils.typ | 5 +- src/diagram.typ | 11 +- src/participant.typ | 4 +- src/sequence.typ | 337 +--------------------------- src/sync.typ | 21 +- 10 files changed, 849 insertions(+), 401 deletions(-) create mode 100644 src/core/draw/event.typ create mode 100644 src/core/draw/participant.typ create mode 100644 src/core/draw/sequence.typ create mode 100644 src/core/draw/sync.typ diff --git a/src/core/draw/event.typ b/src/core/draw/event.typ new file mode 100644 index 0000000..5dc8996 --- /dev/null +++ b/src/core/draw/event.typ @@ -0,0 +1,31 @@ +#import "../utils.typ": get-ctx, set-ctx +#import "../../consts.typ": * + +#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) + } 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/participant.typ b/src/core/draw/participant.typ new file mode 100644 index 0000000..5f76a4d --- /dev/null +++ b/src/core/draw/participant.typ @@ -0,0 +1,405 @@ +#import "../../consts.typ": * +#import "../../cetz.typ": draw +#import "../utils.typ": get-ctx, set-ctx, get-style + +#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/sequence.typ b/src/core/draw/sequence.typ new file mode 100644 index 0000000..0df14a3 --- /dev/null +++ b/src/core/draw/sequence.typ @@ -0,0 +1,360 @@ +#import "../utils.typ": get-ctx, set-ctx +#import "../../consts.typ": * +#import "../../cetz.typ": draw, vector + + +#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") + +#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 + note.render(seq.linked-note, y: start-info.y) + } + + 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..6c63522 --- /dev/null +++ b/src/core/draw/sync.typ @@ -0,0 +1,32 @@ +#import "../utils.typ": get-ctx, set-ctx, is-elmt + +#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 index df8cf2c..d26a6ca 100644 --- a/src/core/renderer.typ +++ b/src/core/renderer.typ @@ -379,13 +379,9 @@ // Draw elements for elmt in elements { if not is-elmt(elmt) { - shapes.push(elmt) - - // Sequences - } else if elmt.type == "seq" { - let shps - (y, lifelines, shps) = draw-seq(elmt, y, lifelines) - shapes += shps + (elmt,) + } else if "draw" in elmt and elmt.type != "par" { + (elmt.draw)(elmt) // Groups (start) -> reserve space for labels + store position } else if elmt.type == "grp" { @@ -440,10 +436,6 @@ 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" { @@ -465,30 +457,6 @@ } 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 { @@ -499,12 +467,6 @@ (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 } } diff --git a/src/core/utils.typ b/src/core/utils.typ index 011ecd7..dfc682a 100644 --- a/src/core/utils.typ +++ b/src/core/utils.typ @@ -95,8 +95,9 @@ #let set-ctx(func) = draw.set-ctx(c => { let ctx = c.shared-state.chronos - ctx = func(ctx) - c.shared-state.chronos = ctx + let new-ctx = func(ctx) + assert(new-ctx != none, message: "set-ctx must return a context!") + c.shared-state.chronos = new-ctx return c }) diff --git a/src/diagram.typ b/src/diagram.typ index 6156ad1..41335c4 100644 --- a/src/diagram.typ +++ b/src/diagram.typ @@ -1,13 +1,19 @@ -#import "core/utils.typ": fit-canvas +#import "core/utils.typ": fit-canvas, set-ctx #import "core/renderer.typ": render #import "participant.typ" as participant: _par, PAR-SPECIALS #import "sequence.typ": _seq - +#import "core/draw/event.typ": render as evt-render #import "core/setup.typ": setup +#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 ),) } @@ -15,6 +21,7 @@ #let _evt(participant, event) = { return (( type: "evt", + draw: evt-render, participant: participant, event: event, lifeline-style: auto diff --git a/src/participant.typ b/src/participant.typ index c8095db..1c2ca3e 100644 --- a/src/participant.typ +++ b/src/participant.typ @@ -1,6 +1,6 @@ #import "/src/cetz.typ": draw #import "consts.typ": * -#import "core/draw/participant.typ" +#import "core/draw/participant.typ": render as par-render #let PAR-SPECIALS = "?[]" #let SHAPES = ( @@ -37,7 +37,7 @@ } return (( type: "par", - draw: participant.render, + draw: par-render, name: name, display-name: if display-name == auto {name} else {display-name}, from-start: from-start, diff --git a/src/sequence.typ b/src/sequence.typ index eb315de..a3e367c 100644 --- a/src/sequence.typ +++ b/src/sequence.typ @@ -2,50 +2,7 @@ #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 +25,7 @@ ) = { return (( type: "seq", + draw: sequence.render, p1: p1, p2: p2, comment: comment, @@ -94,294 +52,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 index 41f0891..85106ff 100644 --- a/src/sync.typ +++ b/src/sync.typ @@ -1,27 +1,10 @@ #import "sequence.typ" +#import "core/draw/sync.typ" #let _sync(elmts) = { return (( type: "sync", + draw: sync.render, 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