12 KiB
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:
// 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<u8>,
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
pub mod ttf_parser;
- Step 4: Commit
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
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:
pub struct OutlinePoint {
pub x: f32,
pub y: f32,
pub on_curve: bool,
}
pub struct GlyphOutline {
pub contours: Vec<Vec<OutlinePoint>>,
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 lookupglyph_outline(glyph_id: u16) -> Option<GlyphOutline>— 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
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
pub struct RasterResult {
pub width: u32,
pub height: u32,
pub bitmap: Vec<u8>, // 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:
- Scale outline points:
px = (x - x_min) * scale,py = (y_max - y) * scale(Y flip) - Walk contours: on_curve→on_curve = line, on_curve→off_curve→on_curve = quadratic bezier
- Flatten all curves into edges (line segments)
- For each scanline y: find all edge intersections, sort by x
- Fill using non-zero winding rule
- 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
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
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<u8>,
pub atlas_width: u32,
pub atlas_height: u32,
glyphs: HashMap<char, GlyphInfo>,
cursor_x: u32,
cursor_y: u32,
row_height: u32,
pub dirty: bool,
}
Methods:
new(width, height)→ zeroed atlas_dataget(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
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
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<Self, String>;
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:
- Check cache.get(ch)
- If miss: parser.glyph_index → parser.glyph_outline → rasterize → cache.insert
- 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
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:
pub ttf_font: Option<crate::ttf_font::TtfFont>,
Add helper method:
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:
// 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
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
git commit -m "docs: update STATUS.md and DEFERRED.md with TTF font support"