diff --git a/rust/pm-ui/src/lib.rs b/rust/pm-ui/src/lib.rs index eb2d089..4d72c6d 100644 --- a/rust/pm-ui/src/lib.rs +++ b/rust/pm-ui/src/lib.rs @@ -245,6 +245,7 @@ where } // ---- drum notation ---- +#[derive(Clone, Copy)] enum Head { Oval, Cross, @@ -257,9 +258,11 @@ fn map_voice(name: &str) -> (i32, Head, bool) { } else if name.starts_with("snare") || name.starts_with("clap") || name.starts_with("rim") { (18, Head::Oval, true) } else if name.starts_with("hat") || name.starts_with("openHat") { - (-6, Head::Cross, true) // hi-hat: above the staff - } else if name.starts_with("ride") || name.starts_with("crash") { + (-12, Head::Cross, true) // hi-hat: first ledger line above the staff + } else if name.starts_with("ride") { (0, Head::Cross, true) + } else if name.starts_with("crash") { + (-24, Head::Cross, true) // crash: high above, more ledgers } else if name.starts_with("tom") { (12, Head::Oval, true) } else if name.starts_with("cowbell") || name.starts_with("woodblock") || name.starts_with("claves") || name.starts_with("tambourine") { @@ -308,61 +311,112 @@ where Text::with_alignment(fmt_u32(beats as u32, &mut tb), Point::new(m + clef_w / 2, staff_top + 16), ts, Alignment::Center).draw(d)?; Text::with_alignment("4", Point::new(m + clef_w / 2, staff_top + 40), ts, Alignment::Center).draw(d)?; - let stem_len = 30; + // Time resolution = finest lane (so off-beats land on columns); lanes whose step count divides + // `res` align to it (polymeter that doesn't divide falls back to its own grid implicitly). + let res = s.lanes.iter().filter(|l| !l.muted).map(|l| l.levels.len() as i32).max().unwrap_or(4).max(1); + let up_end = staff_top - 22; // fixed stem-end levels → horizontal beams + let dn_end = staff_top + 4 * line_gap + 22; + let bot = staff_top + 4 * line_gap; - for lane in s.lanes.iter() { - if lane.muted { - continue; - } - let (off, head, up) = map_voice(lane.name); - let hy = staff_top + off; - let steps = lane.levels.len().max(1) as i32; - let sub = (steps / beats).max(1); // steps per beat → note value - let stem_y = if up { hy - stem_len } else { hy + stem_len }; + let mut up_prev: Option<(i32, i32)> = None; // (stem_x, beat) for beaming + let mut dn_prev: Option<(i32, i32)> = None; - // stem x is on the right for up-stems, left for down-stems - let mut prev_beam: Option<(i32, i32)> = None; // (x, beatgroup) of previous beamable note + for c in 0..res { + let cx = x0 + c * bw / res + bw / (2 * res); + // gather notes at this column, per voice + let (mut up_lo, mut up_hi, mut up_any, mut up_sub2) = (i32::MIN, i32::MAX, false, false); + let (mut dn_lo, mut dn_hi, mut dn_any, mut dn_sub2) = (i32::MIN, i32::MAX, false, false); - for (i, &lvl) in lane.levels.iter().enumerate() { + for lane in s.lanes.iter() { + if lane.muted { + continue; + } + let steps = lane.levels.len() as i32; + if steps == 0 || (c * steps) % res != 0 { + continue; // this lane has no note position at this column + } + let lvl = lane.levels[(c * steps / res) as usize]; if lvl == 0 { continue; } - let i = i as i32; - let cx = x0 + i * bw / steps + bw / (2 * steps); - let stem_x = if up { cx + 5 } else { cx - 5 }; - let col = if lvl == 2 { AMBER } else { ink }; // accent tint + let (off, head, up) = map_voice(lane.name); + let hy = staff_top + off; + let col = if lvl == 2 { AMBER } else { ink }; // notehead match head { - Head::Oval => { - embedded_graphics::primitives::Ellipse::new(Point::new(cx - 6, hy - 4), Size::new(12, 8)) - .into_styled(PrimitiveStyle::with_fill(col)) - .draw(d)?; - } + Head::Oval => embedded_graphics::primitives::Ellipse::new(Point::new(cx - 6, hy - 4), Size::new(12, 8)) + .into_styled(PrimitiveStyle::with_fill(col)) + .draw(d)?, Head::Cross => { Line::new(Point::new(cx - 5, hy - 5), Point::new(cx + 5, hy + 5)).into_styled(PrimitiveStyle::with_stroke(col, 2)).draw(d)?; Line::new(Point::new(cx - 5, hy + 5), Point::new(cx + 5, hy - 5)).into_styled(PrimitiveStyle::with_stroke(col, 2)).draw(d)?; } } - // stem - Line::new(Point::new(stem_x, hy), Point::new(stem_x, stem_y)).into_styled(PrimitiveStyle::with_stroke(ink, 2)).draw(d)?; + // ledger lines for notes a full line+ above or below the staff + let mut ly = staff_top - 12; + while ly >= hy { + Line::new(Point::new(cx - 9, ly), Point::new(cx + 9, ly)).into_styled(PrimitiveStyle::with_stroke(ink, 1)).draw(d)?; + ly -= 12; + } + let mut ly = bot + 12; + while ly <= hy { + Line::new(Point::new(cx - 9, ly), Point::new(cx + 9, ly)).into_styled(PrimitiveStyle::with_stroke(ink, 1)).draw(d)?; + ly += 12; + } - // beam to the previous note if both are subdivided and in the same beat - if sub >= 2 { - let beat = i / sub; - if let Some((px, pbeat)) = prev_beam { - if pbeat == beat { - Rectangle::new(Point::new(px.min(stem_x), stem_y - 1), Size::new((px - stem_x).unsigned_abs() + 2, 4)) - .into_styled(PrimitiveStyle::with_fill(ink)) - .draw(d)?; + let lsub = steps / beats.max(1); + if up { + up_any = true; + up_lo = up_lo.max(hy); + up_hi = up_hi.min(hy); + up_sub2 |= lsub >= 2; + } else { + dn_any = true; + dn_lo = dn_lo.max(hy); + dn_hi = dn_hi.min(hy); + dn_sub2 |= lsub >= 2; + } + } + + let beat = c * beats / res; + // shared up-stem (hands): right side, from lowest head up past the highest head + if up_any { + let sx = cx + 6; + let top = up_end.min(up_hi - 12); // always clear the highest notehead + Line::new(Point::new(sx, up_lo), Point::new(sx, top)).into_styled(PrimitiveStyle::with_stroke(ink, 2)).draw(d)?; + let up_end = top; + if up_sub2 { + if let Some((px, pb)) = up_prev { + if pb == beat { + Rectangle::new(Point::new(px.min(sx), up_end), Size::new((px - sx).unsigned_abs(), 4)).into_styled(PrimitiveStyle::with_fill(ink)).draw(d)?; } } - prev_beam = Some((stem_x, beat)); + up_prev = Some((sx, beat)); + } else { + up_prev = None; + } + } + // shared down-stem (feet): left side, from highest head down past the lowest head + if dn_any { + let sx = cx - 6; + let bottom = dn_end.max(dn_lo + 12); + Line::new(Point::new(sx, dn_hi), Point::new(sx, bottom)).into_styled(PrimitiveStyle::with_stroke(ink, 2)).draw(d)?; + let dn_end = bottom; + if dn_sub2 { + if let Some((px, pb)) = dn_prev { + if pb == beat { + Rectangle::new(Point::new(px.min(sx), dn_end - 3), Size::new((px - sx).unsigned_abs(), 4)).into_styled(PrimitiveStyle::with_fill(ink)).draw(d)?; + } + } + dn_prev = Some((sx, beat)); + } else { + dn_prev = None; } } } - Text::new("drum notation", Point::new(12, staff_top + 4 * line_gap + 40), MonoTextStyle::new(&FONT_6X10, MUTE)).draw(d)?; + Text::new("drum notation", Point::new(12, bot + 40), MonoTextStyle::new(&FONT_6X10, MUTE)).draw(d)?; Ok(()) }