# TTF Font Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Self-implemented TTF parser + rasterizer with on-demand glyph cache atlas for variable-width Unicode text rendering in the editor. **Architecture:** 4 modules: TtfParser (binary parsing) → Rasterizer (outline→bitmap) → GlyphCache (atlas management) → TtfFont (unified interface). All pure CPU logic, no GPU dependency for core. GPU integration only in editor_demo. **Tech Stack:** Rust, std only (no external crates). wgpu for atlas texture upload in editor integration. **Spec:** `docs/superpowers/specs/2026-03-26-ttf-font-design.md` **Test font:** Tests need a real TTF file. Use a system font or download a small open-source font. For tests, embed a minimal subset or use `C:/Windows/Fonts/arial.ttf` which is available on Windows. --- ### Task 1: TtfParser — table directory + head/hhea/maxp **Files:** - Create: `crates/voltex_editor/src/ttf_parser.rs` - Modify: `crates/voltex_editor/src/lib.rs` - [ ] **Step 1: Write tests and implement table directory parsing** Create `ttf_parser.rs` with byte-reading helpers and table directory parsing: ```rust // Byte reading helpers (big-endian, TTF standard) fn read_u16(data: &[u8], offset: usize) -> u16 fn read_i16(data: &[u8], offset: usize) -> i16 fn read_u32(data: &[u8], offset: usize) -> u32 // Table directory entry struct TableRecord { tag: [u8; 4], offset: u32, length: u32, } pub struct TtfParser { data: Vec, tables: HashMap<[u8; 4], TableRecord>, pub units_per_em: u16, pub num_glyphs: u16, pub ascender: i16, pub descender: i16, pub line_gap: i16, pub num_h_metrics: u16, pub loca_format: i16, } ``` Parse: offset table → table directory → head (units_per_em, indexToLocFormat) → hhea (ascender, descender, lineGap, numberOfHMetrics) → maxp (numGlyphs). Tests: - `test_parse_loads_tables`: Load a TTF file, verify tables exist (head, cmap, glyf, etc.) - `test_head_values`: units_per_em > 0, loca_format is 0 or 1 - `test_hhea_values`: ascender > 0, descender < 0 (typically) - `test_maxp_values`: num_glyphs > 0 - [ ] **Step 2: Run tests** Run: `cargo test -p voltex_editor --lib ttf_parser -- --nocapture` - [ ] **Step 3: Add module to lib.rs** ```rust pub mod ttf_parser; ``` - [ ] **Step 4: Commit** ```bash git commit -m "feat(editor): add TTF parser with table directory and header parsing" ``` --- ### Task 2: TtfParser — cmap (Unicode → glyph index) **Files:** - Modify: `crates/voltex_editor/src/ttf_parser.rs` - [ ] **Step 1: Implement cmap Format 4 parsing** Add `glyph_index(codepoint: u32) -> u16` method: - Find cmap table → find Format 4 subtable (platformID=3, encodingID=1 or platformID=0) - Parse segCount, endCode[], startCode[], idDelta[], idRangeOffset[] - Binary search for the segment containing the codepoint - Calculate glyph index Tests: - `test_cmap_ascii`: 'A' (0x41) → non-zero glyph index - `test_cmap_space`: ' ' (0x20) → valid glyph index - `test_cmap_unmapped`: very high codepoint → 0 (missing glyph) - `test_cmap_multiple_chars`: several chars return distinct indices - [ ] **Step 2: Run tests, commit** ```bash git commit -m "feat(editor): add cmap Format 4 parsing for Unicode glyph mapping" ``` --- ### Task 3: TtfParser — glyf (glyph outlines) + hmtx (metrics) **Files:** - Modify: `crates/voltex_editor/src/ttf_parser.rs` - [ ] **Step 1: Implement loca + glyf parsing** Add: ```rust pub struct OutlinePoint { pub x: f32, pub y: f32, pub on_curve: bool, } pub struct GlyphOutline { pub contours: Vec>, pub x_min: i16, pub y_min: i16, pub x_max: i16, pub y_max: i16, } pub struct GlyphMetrics { pub advance_width: u16, pub left_side_bearing: i16, } ``` Methods: - `glyph_offset(glyph_id: u16) -> usize` — loca table lookup - `glyph_outline(glyph_id: u16) -> Option` — glyf table parse - Simple glyph: endPtsOfContours, flags, x/y deltas - Handle on_curve/off_curve: insert implicit on-curve points between consecutive off-curve - Compound glyph: skip for now (return None) - `glyph_metrics(glyph_id: u16) -> GlyphMetrics` — hmtx table Tests: - `test_glyph_outline_has_contours`: 'A' outline has >= 1 contour - `test_glyph_outline_points`: contour points count > 0 - `test_glyph_metrics_advance`: 'A' advance > 0 - `test_space_glyph_no_contours`: ' ' has 0 contours (or None) - [ ] **Step 2: Run tests, commit** ```bash git commit -m "feat(editor): add glyf outline and hmtx metrics parsing" ``` --- ### Task 4: Rasterizer **Files:** - Create: `crates/voltex_editor/src/rasterizer.rs` - Modify: `crates/voltex_editor/src/lib.rs` - [ ] **Step 1: Implement bezier flattening + scanline rasterizer** ```rust pub struct RasterResult { pub width: u32, pub height: u32, pub bitmap: Vec, // R8 alpha pub offset_x: f32, pub offset_y: f32, } /// Flatten quadratic bezier to line segments. fn flatten_quadratic(p0: (f32,f32), p1: (f32,f32), p2: (f32,f32), segments: &mut Vec<(f32,f32)>); /// Rasterize outline at given scale. pub fn rasterize(outline: &GlyphOutline, scale: f32, units_per_em: u16) -> RasterResult; ``` Rasterize steps: 1. Scale outline points: `px = (x - x_min) * scale`, `py = (y_max - y) * scale` (Y flip) 2. Walk contours: on_curve→on_curve = line, on_curve→off_curve→on_curve = quadratic bezier 3. Flatten all curves into edges (line segments) 4. For each scanline y: find all edge intersections, sort by x 5. Fill using non-zero winding rule 6. Output alpha bitmap Tests: - `test_rasterize_produces_bitmap`: rasterize 'A' at 32px → width>0, height>0, bitmap non-empty - `test_rasterize_has_filled_pixels`: bitmap has at least some non-zero pixels - `test_rasterize_space`: space glyph → 0-width or empty bitmap - `test_flatten_quadratic`: known bezier → correct number of segments - [ ] **Step 2: Add module to lib.rs, run tests, commit** ```bash git commit -m "feat(editor): add glyph rasterizer with bezier flattening and scanline fill" ``` --- ### Task 5: GlyphCache (on-demand atlas) **Files:** - Create: `crates/voltex_editor/src/glyph_cache.rs` - Modify: `crates/voltex_editor/src/lib.rs` - [ ] **Step 1: Implement GlyphCache** ```rust use std::collections::HashMap; pub const ATLAS_SIZE: u32 = 1024; pub struct GlyphInfo { pub uv: [f32; 4], // u0, v0, u1, v1 pub width: f32, pub height: f32, pub advance: f32, pub bearing_x: f32, pub bearing_y: f32, } pub struct GlyphCache { pub atlas_data: Vec, pub atlas_width: u32, pub atlas_height: u32, glyphs: HashMap, cursor_x: u32, cursor_y: u32, row_height: u32, pub dirty: bool, } ``` Methods: - `new(width, height)` → zeroed atlas_data - `get(ch) -> Option<&GlyphInfo>` - `insert(ch, bitmap, width, height, advance, bearing_x, bearing_y) -> &GlyphInfo` - Copy bitmap into atlas at (cursor_x, cursor_y) - Advance cursor, handle row overflow - Set dirty = true - `clear_dirty()` Tests: - `test_insert_and_get`: insert a 10x10 glyph → get returns correct UV - `test_cache_hit`: insert then get same char → same UV - `test_row_wrap`: insert many wide glyphs → cursor_y advances to next row - `test_uv_coordinates`: UV values are in 0.0-1.0 range and match position - [ ] **Step 2: Add module, run tests, commit** ```bash git commit -m "feat(editor): add GlyphCache on-demand atlas manager" ``` --- ### Task 6: TtfFont (unified interface) **Files:** - Create: `crates/voltex_editor/src/ttf_font.rs` - Modify: `crates/voltex_editor/src/lib.rs` - [ ] **Step 1: Implement TtfFont** ```rust pub struct TtfFont { parser: TtfParser, cache: GlyphCache, pub font_size: f32, pub line_height: f32, pub ascender: f32, pub descender: f32, scale: f32, } impl TtfFont { pub fn new(data: &[u8], font_size: f32) -> Result; pub fn glyph(&mut self, ch: char) -> &GlyphInfo; // cache lookup or rasterize+insert pub fn text_width(&mut self, text: &str) -> f32; pub fn atlas_data(&self) -> &[u8]; pub fn atlas_size(&self) -> (u32, u32); pub fn is_dirty(&self) -> bool; pub fn clear_dirty(&mut self); } ``` The `glyph` method: 1. Check cache.get(ch) 2. If miss: parser.glyph_index → parser.glyph_outline → rasterize → cache.insert 3. Return &GlyphInfo Tests: - `test_ttf_font_new`: loads TTF file, font_size = 24 - `test_glyph_caches`: call glyph('A') twice → second is cache hit (dirty resets) - `test_text_width`: "Hello" width > 0, "Hello World" width > "Hello" width - `test_korean_glyph`: '가' glyph loads without panic (if font supports it) - [ ] **Step 2: Add module, run tests, commit** ```bash git commit -m "feat(editor): add TtfFont unified interface with glyph caching" ``` --- ### Task 7: UI integration + editor_demo **Files:** - Modify: `crates/voltex_editor/src/ui_context.rs` - Modify: `crates/voltex_editor/src/renderer.rs` - Modify: `examples/editor_demo/src/main.rs` - [ ] **Step 1: Add ttf_font to UiContext + draw_text helper** In `ui_context.rs`, add: ```rust pub ttf_font: Option, ``` Add helper method: ```rust pub fn draw_text(&mut self, text: &str, x: f32, y: f32, color: [u8; 4]) { if let Some(ref mut ttf) = self.ttf_font { let mut cx = x; for ch in text.chars() { let glyph = ttf.glyph(ch); let gx = cx + glyph.bearing_x; let gy = y + self.font.glyph_height as f32 - glyph.bearing_y; // baseline align let (u0, v0, u1, v1) = (glyph.uv[0], glyph.uv[1], glyph.uv[2], glyph.uv[3]); if glyph.width > 0.0 && glyph.height > 0.0 { self.draw_list.add_rect_uv(gx, gy, glyph.width, glyph.height, u0, v0, u1, v1, color); } cx += glyph.advance; } } else { // Fallback to bitmap font let gw = self.font.glyph_width as f32; let gh = self.font.glyph_height as f32; let mut cx = x; for ch in text.chars() { let (u0, v0, u1, v1) = self.font.glyph_uv(ch); self.draw_list.add_rect_uv(cx, y, gw, gh, u0, v0, u1, v1, color); cx += gw; } } } pub fn ttf_text_width(&mut self, text: &str) -> f32 { if let Some(ref mut ttf) = self.ttf_font { ttf.text_width(text) } else { text.len() as f32 * self.font.glyph_width as f32 } } ``` - [ ] **Step 2: Update UiRenderer for TTF atlas** In `renderer.rs`, add support for a second texture (TTF atlas): - When TtfFont is active, create a TTF atlas texture (R8Unorm, 1024x1024) - Each frame: check dirty → upload atlas data - Create a second bind group for TTF texture - DrawCommands need to know which texture to use Simplest approach: add a `ttf_atlas_texture` and `ttf_bind_group` to UiRenderer. When rendering, if TTF is active, use the TTF bind group for all draws (since TTF atlas includes all rendered glyphs). Actually, simpler: just replace the font atlas texture with the TTF atlas when TTF is active. The shader is the same (R8 alpha sampling). - [ ] **Step 3: Update editor_demo** In `resumed`: ```rust // Load TTF font let ttf_data = std::fs::read("C:/Windows/Fonts/arial.ttf") .or_else(|_| std::fs::read("C:/Windows/Fonts/malgun.ttf")) .ok(); if let Some(data) = ttf_data { if let Ok(font) = TtfFont::new(&data, 16.0) { state.ui.ttf_font = Some(font); } } ``` - [ ] **Step 4: Build and test** Run: `cargo build -p editor_demo` Run: `cargo test -p voltex_editor` - [ ] **Step 5: Commit** ```bash git commit -m "feat(editor): integrate TTF font rendering into UI system and editor_demo" ``` --- ### Task 8: Update docs **Files:** - Modify: `docs/STATUS.md` - Modify: `docs/DEFERRED.md` - [ ] **Step 1: Update STATUS.md** Add: `- voltex_editor: TtfParser, Rasterizer, GlyphCache, TtfFont (TTF rendering, Unicode)` Update test count. - [ ] **Step 2: Update DEFERRED.md** ``` - ~~**TTF 폰트**~~ ✅ 자체 TTF 파서 + 래스터라이저 + 온디맨드 캐시 완료. 한글/유니코드 지원. ``` - [ ] **Step 3: Commit** ```bash git commit -m "docs: update STATUS.md and DEFERRED.md with TTF font support" ```