refactored pre-rendering
This commit is contained in:
		
							
								
								
									
										604
									
								
								src/core/renderer.typ
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										604
									
								
								src/core/renderer.typ
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,604 @@ | ||||
| #import "/src/cetz.typ" as cetz: canvas, draw | ||||
| #import "utils.typ": get-participants-i, get-style, normalize-units, is-elmt | ||||
| #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 | ||||
| #import "../styles.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(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(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 = 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 | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
|   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 get-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 render(participants, elements) = context canvas(length: 1pt, { | ||||
|   let participants = participants | ||||
|   let elements = elements | ||||
|  | ||||
|   let shapes = () | ||||
|   participants = init-lifelines(participants) | ||||
|   let pars-i = get-participants-i(participants) | ||||
|  | ||||
|   let widths = get-columns-width(participants, elements, pars-i) | ||||
|  | ||||
|   // 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 { | ||||
|     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 | ||||
|  | ||||
|     // 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 | ||||
| }) | ||||
		Reference in New Issue
	
	Block a user