//! Glyph atlas generator (host, std). //! //! Rasterizes the SAME frozen Bravura SMuFL subset the web uses (see `tools/bravura/subset.py`'s //! GLYPHS list, 39 names) with the pure-Rust `fontdue` rasterizer, packs the coverage into a //! row-major 4-bit-alpha atlas, and emits the COMMITTED, generated, `no_std`-compatible //! `rust/pm-ui/src/notation/glyphs.rs` (a `Glyph` enum + a `GlyphMeta` metrics table + the atlas //! bytes). Run after the subset changes: //! //! cargo run --manifest-path rust/glyphgen/Cargo.toml //! //! Device staff geometry: 1 SMuFL em = 4 staff spaces. We pick STAFF_SPACE = 10 px → EM = 40 px, //! which fits the ST7796 320×480 staff (5 lines × 10 px = 40 px tall) and matches the web's S≈11. use std::fmt::Write as _; use std::path::PathBuf; const STAFF_SPACE: f32 = 10.0; const EM: f32 = 4.0 * STAFF_SPACE; // 40 px — SMuFL: 1 em = 4 staff spaces /// (enum variant identifier, SMuFL glyph name) — order defines the `Glyph` enum + table index. /// Keep in lockstep with `src/notation.js` GLYPH map and `tools/bravura/subset.py` GLYPHS. const GLYPHS: &[(&str, char)] = &[ // clef ("Clef", '\u{E069}'), // unpitchedPercussionClef1 // noteheads ("NoteheadBlack", '\u{E0A4}'), ("NoteheadX", '\u{E0A9}'), // noteheadXBlack ("NoteheadCircleX", '\u{E0B3}'), // noteheadCircleX ("ParenL", '\u{E0F5}'), // noteheadParenthesisLeft ("ParenR", '\u{E0F6}'), // noteheadParenthesisRight ("NoteheadHalf", '\u{E0A3}'), ("NoteheadWhole", '\u{E0A2}'), // flags ("Flag8Up", '\u{E240}'), ("Flag8Down", '\u{E241}'), ("Flag16Up", '\u{E242}'), ("Flag16Down", '\u{E243}'), // rests ("RestWhole", '\u{E4E3}'), ("RestHalf", '\u{E4E4}'), ("RestQuarter", '\u{E4E5}'), ("Rest8", '\u{E4E6}'), ("Rest16", '\u{E4E7}'), // articulations + dot ("AccentAbove", '\u{E4A0}'), // articAccentAbove ("AccentBelow", '\u{E4A1}'), // articAccentBelow ("Dot", '\u{E1E7}'), // augmentationDot // time signature digits ("Sig0", '\u{E080}'), ("Sig1", '\u{E081}'), ("Sig2", '\u{E082}'), ("Sig3", '\u{E083}'), ("Sig4", '\u{E084}'), ("Sig5", '\u{E085}'), ("Sig6", '\u{E086}'), ("Sig7", '\u{E087}'), ("Sig8", '\u{E088}'), ("Sig9", '\u{E089}'), ("SigPlus", '\u{E08C}'), ("SigCommon", '\u{E08A}'), ("SigCut", '\u{E08B}'), // ornaments ("GraceAcc", '\u{E560}'), // graceNoteAcciaccaturaStemUp ("GraceSlash", '\u{E564}'), // graceNoteSlashStemUp ("Trem1", '\u{E220}'), ("Trem2", '\u{E221}'), ("Trem3", '\u{E222}'), ("BuzzRoll", '\u{E22A}'), ]; struct Packed { ident: &'static str, atlas_x: u16, atlas_y: u16, w: u16, h: u16, bearing_x: i16, // left edge of bitmap relative to the glyph origin (xmin) bearing_y: i16, // top edge of bitmap above the baseline (ymin + height) advance: i16, // horizontal advance (rounded) } fn main() { let here = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let root = here.parent().unwrap().parent().unwrap(); // repo root // prefer the vendored copy under rust/assets, fall back to tools/ let font_path = { let a = here.parent().unwrap().join("assets/bravura/Bravura.otf"); if a.exists() { a } else { root.join("tools/bravura/Bravura.otf") } }; let font_bytes = std::fs::read(&font_path).expect("read Bravura.otf"); let font = fontdue::Font::from_bytes(font_bytes, fontdue::FontSettings::default()).expect("parse font"); // rasterize each glyph struct Raster { ident: &'static str, metrics: fontdue::Metrics, bitmap: Vec, } let mut rasters: Vec = Vec::new(); for &(ident, ch) in GLYPHS { let (metrics, bitmap) = font.rasterize(ch, EM); rasters.push(Raster { ident, metrics, bitmap }); } // ---- pack into a simple shelf/grid atlas, ATLAS_W wide, 1px padding between cells ---- const ATLAS_W: u16 = 256; const PAD: u16 = 1; let mut packed: Vec = Vec::new(); let mut atlas_h: u16 = 0; let mut cx: u16 = PAD; let mut cy: u16 = PAD; let mut shelf_h: u16 = 0; // pre-compute placements let mut placements: Vec<(u16, u16)> = Vec::with_capacity(rasters.len()); for r in &rasters { let w = r.metrics.width as u16; let h = r.metrics.height as u16; if cx + w + PAD > ATLAS_W { cx = PAD; cy += shelf_h + PAD; shelf_h = 0; } placements.push((cx, cy)); cx += w + PAD; if h > shelf_h { shelf_h = h; } let bottom = cy + h + PAD; if bottom > atlas_h { atlas_h = bottom; } } // 8-bit coverage atlas, then we 4-bit pack it row by row at emit time. let mut cov = vec![0u8; ATLAS_W as usize * atlas_h as usize]; for (r, &(px, py)) in rasters.iter().zip(placements.iter()) { let w = r.metrics.width; let h = r.metrics.height; for row in 0..h { for col in 0..w { let a = r.bitmap[row * w + col]; let ax = px as usize + col; let ay = py as usize + row; cov[ay * ATLAS_W as usize + ax] = a; } } packed.push(Packed { ident: r.ident, atlas_x: px, atlas_y: py, w: w as u16, h: h as u16, bearing_x: r.metrics.xmin as i16, bearing_y: (r.metrics.ymin + h as i32) as i16, advance: r.metrics.advance_width.round() as i16, }); } // 4-bit pack: two pixels per byte, row-major, ATLAS_W pixels per row → ATLAS_W/2 bytes per row // (ATLAS_W is even). High nibble = even column, low nibble = odd column. Coverage 0..255 → 0..15. let row_bytes = ATLAS_W as usize / 2; let mut atlas = vec![0u8; row_bytes * atlas_h as usize]; for y in 0..atlas_h as usize { for x in 0..ATLAS_W as usize { let a4 = (cov[y * ATLAS_W as usize + x] as u16 * 15 / 255) as u8; let bi = y * row_bytes + x / 2; if x & 1 == 0 { atlas[bi] |= a4 << 4; } else { atlas[bi] |= a4; } } } // ---- emit glyphs.rs ---- let mut out = String::new(); out.push_str("//! GENERATED by `rust/glyphgen` — do not edit by hand. Re-run:\n"); out.push_str("//! cargo run --manifest-path rust/glyphgen/Cargo.toml\n"); out.push_str("//!\n"); out.push_str("//! A 4-bit-alpha atlas of the frozen Bravura SMuFL subset (39 glyphs, the SAME set the web\n"); out.push_str("//! froze in tools/bravura/subset.py) rasterized at em = 4 x staff_space. `no_std` const data.\n"); out.push_str("#![allow(dead_code)]\n\n"); let _ = writeln!(out, "/// Staff space the atlas was rasterized for (px). 1 SMuFL em = 4 staff spaces."); let _ = writeln!(out, "pub const STAFF_SPACE: i32 = {};", STAFF_SPACE as i32); let _ = writeln!(out, "pub const EM: i32 = {};", EM as i32); let _ = writeln!(out, "/// Atlas dimensions in pixels (4-bit alpha, row-packed: 2 px per byte)."); let _ = writeln!(out, "pub const ATLAS_W: usize = {};", ATLAS_W); let _ = writeln!(out, "pub const ATLAS_H: usize = {};", atlas_h); out.push('\n'); // GlyphMeta struct out.push_str("/// Per-glyph atlas placement + placement metrics (all in px).\n"); out.push_str("#[derive(Clone, Copy)]\n"); out.push_str("pub struct GlyphMeta {\n"); out.push_str(" pub atlas_x: u16,\n pub atlas_y: u16,\n pub w: u16,\n pub h: u16,\n"); out.push_str(" /// left edge of the bitmap relative to the glyph origin (pen x)\n pub bearing_x: i16,\n"); out.push_str(" /// top edge of the bitmap ABOVE the baseline (positive = up)\n pub bearing_y: i16,\n"); out.push_str(" /// horizontal advance\n pub advance: i16,\n"); out.push_str("}\n\n"); // Glyph enum out.push_str("/// One frozen SMuFL glyph. `as usize` indexes `GLYPHS`.\n"); out.push_str("#[derive(Clone, Copy, PartialEq, Eq)]\n"); out.push_str("pub enum Glyph {\n"); for p in &packed { let _ = writeln!(out, " {},", p.ident); } out.push_str("}\n\n"); // metrics table let _ = writeln!(out, "pub const GLYPHS: [GlyphMeta; {}] = [", packed.len()); for p in &packed { let _ = writeln!( out, " GlyphMeta {{ atlas_x: {}, atlas_y: {}, w: {}, h: {}, bearing_x: {}, bearing_y: {}, advance: {} }}, // {}", p.atlas_x, p.atlas_y, p.w, p.h, p.bearing_x, p.bearing_y, p.advance, p.ident ); } out.push_str("];\n\n"); // atlas bytes let _ = writeln!(out, "/// Row-packed 4-bit alpha. byte = (even_col << 4) | odd_col; coverage 0..15."); let _ = writeln!(out, "pub const ATLAS: &[u8] = &["); for (i, b) in atlas.iter().enumerate() { if i % 16 == 0 { out.push_str(" "); } let _ = write!(out, "0x{:02x},", b); if i % 16 == 15 { out.push('\n'); } else { out.push(' '); } } if atlas.len() % 16 != 0 { out.push('\n'); } out.push_str("];\n"); let dest = here.parent().unwrap().join("pm-ui/src/notation/glyphs.rs"); std::fs::write(&dest, out).expect("write glyphs.rs"); println!( "wrote {} ({} glyphs, atlas {}x{} = {} bytes 4-bit)", dest.display(), packed.len(), ATLAS_W, atlas_h, atlas.len() ); }