-
-
Save cosmobobak/2fb3d298c18abcff3488a53e94c6dd31 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S cargo +nightly -Zscript -q | |
| ---cargo | |
| [package] | |
| name = "type-help-timeline" | |
| version = "1.0.0" | |
| edition = "2024" | |
| [dependencies] | |
| anyhow = "1.0" | |
| arrayvec = "0.7.6" | |
| --- | |
| use anyhow::Context; | |
| use arrayvec::ArrayVec; | |
| use std::{ | |
| collections::{BTreeMap, BTreeSet}, | |
| io, | |
| }; | |
| const WIDTH: usize = 12; | |
| #[derive(Debug, PartialOrd, Ord, PartialEq, Eq)] | |
| struct Scene { | |
| time: u8, | |
| area: [u8; 2], | |
| team: ArrayVec<u8, 16>, | |
| } | |
| fn main() -> anyhow::Result<()> { | |
| let stdin = io::read_to_string(io::stdin())?; | |
| let mut scenes = Vec::with_capacity(64); | |
| for line in stdin.lines() { | |
| let mut parts = line.split('-'); | |
| let time = parts | |
| .next() | |
| .with_context(|| format!("No time code in {line:?}"))?; | |
| let time = time | |
| .parse() | |
| .with_context(|| format!("Could not parse {time:?} as time code"))?; | |
| let area = parts | |
| .next() | |
| .with_context(|| format!("No area code in {line:?}"))?; | |
| let area = area | |
| .as_bytes() | |
| .as_array() | |
| .copied() | |
| .with_context(|| format!("Area code must be two bytes, got {area:?}"))?; | |
| let team = parts | |
| .map(|part| part.parse()) | |
| .collect::<Result<ArrayVec<_, _>, _>>() | |
| .with_context(|| format!("Got unparsable person codes in {line:?}"))?; | |
| scenes.push(Scene { time, area, team }); | |
| } | |
| // We will construct a *grid*. We index by time-slice, then area. | |
| let last_participant = scenes | |
| .iter() | |
| .flat_map(|s| s.team.clone()) | |
| .max() | |
| .context("No scenes provided")?; | |
| let area_indices = scenes | |
| .iter() | |
| .map(|s| s.area) | |
| .collect::<BTreeSet<_>>() | |
| .into_iter() | |
| .zip(0..) | |
| .collect::<BTreeMap<_, _>>(); | |
| let max_participants = area_indices | |
| .iter() | |
| .map(|(k, _)| { | |
| ( | |
| *k, | |
| scenes | |
| .iter() | |
| .filter(|s| s.area == *k) | |
| .map(|s| s.team.len()) | |
| .max() | |
| .unwrap_or(1), | |
| ) | |
| }) | |
| .collect::<BTreeMap<_, _>>(); | |
| let base = |area: [u8; 2]| { | |
| // the base index for an area is the sum of max participants | |
| // in prior areas. | |
| let this_idx = area_indices.get(&area).unwrap(); | |
| let mut sum = 0; | |
| for (area, idx) in area_indices.iter() { | |
| if idx >= this_idx { | |
| continue; | |
| } | |
| sum += max_participants.get(area).unwrap() + 1; | |
| } | |
| sum | |
| }; | |
| let emit_nodes = |scene: &Scene| { | |
| // the y-coördinate is simply the timepoint. | |
| let y = scene.time * 2; | |
| // the x-coördinates are trickier – we emit one | |
| // for each participant, and base-index them from | |
| // the area index. | |
| let base = base(scene.area); | |
| let n = *max_participants.get(&scene.area).unwrap(); | |
| let locs = (base..) | |
| .take(n) | |
| .map(|x| format!("({x}em, {y}em)")) | |
| .collect::<Vec<_>>(); | |
| let width = n * WIDTH; | |
| // note: “shape: rect” is necessary due to a bug in the library. | |
| println!( | |
| " node(shape: rect, width: {width}pt, height: 1em, snap: -1, inset: 0pt, stroke: teal, fill: teal.lighten(90%), enclose: ({locs})),", | |
| locs = locs.join(", ") | |
| ); | |
| // and also emit person markers: | |
| // let locs = (base..) | |
| // .take(scene.team.len()) | |
| // .map(|x| format!("({x}, {y})")); | |
| // for loc in locs { | |
| for (i, p) in scene.team.iter().copied().enumerate() { | |
| let x = base + i; | |
| let loc = format!("({x}em, {y}em)"); | |
| println!(" node({loc}, \"{p}\"),"); | |
| } | |
| }; | |
| // emit document header | |
| let header = r#" | |
| #import "@preview/fletcher:0.5.8" as fletcher: diagram, node, edge | |
| // #set page(width: 1600pt, height: 900pt) | |
| // #set page(flipped: true) | |
| #set page(width: auto, height: auto) | |
| = What’s going on in Type Help? | |
| #line() | |
| #diagram( | |
| // debug: true, // show a coordinate grid | |
| spacing: (6pt, 0.5em), | |
| // node-stroke: 0.6pt, | |
| "#; | |
| println!("{header}"); | |
| // emit zero-time location headers: | |
| for (location, _) in area_indices.iter() { | |
| let base = base(*location); | |
| let n = *max_participants.get(location).unwrap(); | |
| let locs = (base..) | |
| .take(n) | |
| .map(|x| format!("({x}em, 0em)")) | |
| .collect::<Vec<_>>(); | |
| let name = std::str::from_utf8(location).context("bad UTF-8 for area name")?; | |
| let width = n * WIDTH; | |
| // note: “shape: rect” is necessary due to a bug in the library. | |
| println!( | |
| " node(shape: rect, width: {width}pt, height: 2em, snap: -1, inset: 0pt, stroke: blue, fill: blue.lighten(90%), enclose: ({locs}), [{name}]),", | |
| locs = locs.join(", ") | |
| ); | |
| } | |
| for scene in &scenes[..] { | |
| emit_nodes(scene); | |
| } | |
| let next_appearance = |person: u8, from_time: u8| { | |
| scenes | |
| .iter() | |
| .find(move |s| s.time > from_time && s.team.contains(&person)) | |
| }; | |
| // now generate arrows – for each timeslice, generate forward-arrows | |
| // from the participants to their next appearance. | |
| let t0 = scenes.iter().map(|s| s.time).min().unwrap(); | |
| let tx = scenes.iter().map(|s| s.time).max().unwrap(); | |
| for t in t0..=tx { | |
| // select those scenes at this time-point | |
| let at_t = scenes.iter().filter(|s| s.time == t); | |
| for scene in at_t { | |
| // get the base-index for this scene | |
| let src_base = base(scene.area); | |
| for (src_offset, p) in scene.team.iter().copied().enumerate() { | |
| // find this person’s next scene | |
| // if it exists, draw an arrow | |
| if let Some(dst) = next_appearance(p, t) { | |
| let dst_base = base(dst.area); | |
| let dst_offset = dst.team.iter().position(|v| *v == p).unwrap(); | |
| let src_x = src_base + src_offset; | |
| let src_y = t * 2; | |
| let dst_x = dst_base + dst_offset; | |
| let dst_y = dst.time * 2; | |
| // if this is the last appearance, mark it well: | |
| let mark = if next_appearance(p, dst.time).is_none() { | |
| "-x" | |
| } else { | |
| "->" | |
| }; | |
| println!( | |
| " edge(stroke: gradient.linear(space: oklch, red, blue).sample({:.0}%),\ | |
| marks: \"{mark}\", ({src_x}em, {src_y}em), ({dst_x}em, {dst_y}em)),", | |
| p as f64 / last_participant as f64 * 100.0, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| println!(")"); | |
| Ok(()) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment