forked from HEL/chronos
		
	
		
			
				
	
	
		
			275 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Typst
		
	
	
	
	
	
			
		
		
	
	
			275 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Typst
		
	
	
	
	
	
| #import "@preview/cetz:0.2.2": canvas, draw
 | |
| #import "utils.typ": get-participants-i, get-style
 | |
| #import "group.typ"
 | |
| #import "participant.typ"
 | |
| #import participant: PAR-SPECIALS
 | |
| #import "sequence.typ"
 | |
| #import "separator.typ"
 | |
| #import "consts.typ": *
 | |
| 
 | |
| #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 = ()
 | |
| 
 | |
|   // 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
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // 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 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 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
 | |
|   }
 | |
|   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-sep = separator.render.with(x-pos)
 | |
|   let draw-par = participant.render.with(x-pos)
 | |
|   
 | |
|   // Draw participants (start)
 | |
|   for p in participants {
 | |
|     if p.from-start and not p.invisible {
 | |
|       shapes += draw-par(p)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   let y = -Y-SPACE
 | |
|   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" {
 | |
|       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
 | |
|       })
 | |
|       groups.push((y, elmt, 0, 0))
 | |
|       y -= m.height / 1pt + Y-SPACE
 | |
|     
 | |
|     // Groups (end) -> actual drawing
 | |
|     } else if elmt.type == "grp-end" {
 | |
|       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)
 | |
| 
 | |
|       y -= Y-SPACE
 | |
| 
 | |
|     // 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
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // 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: (dash: "dashed", paint: gray.darken(40%))
 | |
|             )
 | |
|           }
 | |
|           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)))
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       draw.line(
 | |
|         (x, last-y),
 | |
|         (x, y),
 | |
|         stroke: (dash: "dashed", paint: gray.darken(40%))
 | |
|       )
 | |
| 
 | |
|       // 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)
 | |
|       draw-par(p, y: y, bottom: true)
 | |
|     }
 | |
|   })
 | |
| 
 | |
|   shapes
 | |
| }) |