metronome/rust/glyphgen/src/main.rs
Me Here 5dcef691c1 Add untracked notation deliverables (build/compile depend on these)
- src/notation.js — web notation engine inlined into pm_e-2.html (@BUILD:include)
- rust/pm-ui/src/notation/ — the notation module pm-ui/lib.rs imports
- rust/glyphgen/ + rust/assets/bravura/ (Bravura.otf + OFL.txt) — host atlas generator + font src
- rust/Cargo.toml (workspace) + rust/.gitignore
- assets/bravura.woff2.b64 (web font subset, @BUILD:bravura@) + info-pm_e-2.html

Without these a clean checkout couldn't build pm_e-2.html or compile pm-ui. (Left hardware/eda
make_svg* + kicad/_svgtest.json untracked — unrelated scratch.)
2026-06-02 13:46:45 -05:00

254 lines
9.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<u8>,
}
let mut rasters: Vec<Raster> = 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<Packed> = 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()
);
}