- 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.)
254 lines
9.4 KiB
Rust
254 lines
9.4 KiB
Rust
//! 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()
|
||
);
|
||
}
|