From 53505ee9b77298256c12bf5d3eccb3229190a02a Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 26 Mar 2026 14:03:11 +0900 Subject: [PATCH] docs: add TTF font design spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-03-26-ttf-font-design.md | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-26-ttf-font-design.md diff --git a/docs/superpowers/specs/2026-03-26-ttf-font-design.md b/docs/superpowers/specs/2026-03-26-ttf-font-design.md new file mode 100644 index 0000000..b45e47f --- /dev/null +++ b/docs/superpowers/specs/2026-03-26-ttf-font-design.md @@ -0,0 +1,270 @@ +# TTF Font Design + +## Overview + +자체 TTF 파서 + 래스터라이저를 구현하고, 온디맨드 글리프 캐시 아틀라스로 가변폭 유니코드 텍스트를 렌더링한다. 기존 FontAtlas를 유지하면서 TtfFont를 병행 추가. + +## Scope + +- TTF 바이너리 파싱 (head, hhea, maxp, cmap, loca, glyf, hmtx) +- Quadratic bezier 아웃라인 → 비트맵 래스터라이징 +- 온디맨드 글리프 캐시 (1024x1024 R8Unorm 아틀라스) +- 가변폭 텍스트 렌더링 (ASCII + 한글 + 유니코드) +- UiContext에 옵션 통합 + +## TtfParser + +TTF 바이너리에서 필요한 테이블을 파싱한다. + +```rust +pub struct TtfParser { + data: Vec, + // 테이블 오프셋 캐시 + head_offset: usize, + hhea_offset: usize, + maxp_offset: usize, + cmap_offset: usize, + loca_offset: usize, + glyf_offset: usize, + hmtx_offset: usize, + // 헤더 정보 + pub units_per_em: u16, + pub num_glyphs: u16, + pub ascender: i16, + pub descender: i16, + pub line_gap: i16, + num_h_metrics: u16, + loca_format: i16, // 0=short, 1=long +} +``` + +### 파싱 순서 +1. Offset Table: sfVersion, numTables → 테이블 디렉토리 순회 +2. 각 테이블 태그(4바이트 ASCII)와 offset/length 기록 +3. head: units_per_em, indexToLocFormat +4. hhea: ascender, descender, lineGap, numberOfHMetrics +5. maxp: numGlyphs +6. cmap: Format 4 (BMP) 또는 Format 12 (full Unicode) 서브테이블 + +### 주요 메서드 +- `parse(data: &[u8]) -> Result` +- `glyph_index(codepoint: u32) -> u16` — cmap 조회 +- `glyph_outline(glyph_id: u16) -> GlyphOutline` — glyf 테이블에서 아웃라인 추출 +- `glyph_metrics(glyph_id: u16) -> GlyphMetrics` — hmtx에서 advance/bearing + +### GlyphOutline +```rust +pub struct GlyphOutline { + pub contours: Vec>, + pub x_min: i16, + pub y_min: i16, + pub x_max: i16, + pub y_max: i16, +} + +pub struct OutlinePoint { + pub x: f32, + pub y: f32, + pub on_curve: bool, +} + +pub struct GlyphMetrics { + pub advance_width: u16, + pub left_side_bearing: i16, +} +``` + +### glyf 테이블 파싱 +- Simple glyph: numberOfContours >= 0 + - endPtsOfContours, flags, x/y 좌표 (delta 인코딩) + - on_curve 플래그로 직선/곡선 구분 + - off-curve 점 사이에 암시적 on-curve 점 삽입 +- Compound glyph: numberOfContours < 0 + - 여러 simple glyph를 변환(translate/scale)하여 합성 + - 재귀적 처리 + +### cmap Format 4 +BMP(U+0000~U+FFFF) 매핑: +- segCount, endCode[], startCode[], idDelta[], idRangeOffset[] 배열 +- 이진 탐색으로 codepoint가 속하는 세그먼트 찾기 +- idRangeOffset == 0: glyph = (codepoint + idDelta) % 65536 +- idRangeOffset != 0: glyphIdArray에서 조회 + +## Rasterizer + +글리프 아웃라인을 알파 비트맵으로 변환한다. + +### 입력 +- GlyphOutline (contours of points) +- font_size (pixel height) +- units_per_em + +### 스케일링 +``` +scale = font_size / units_per_em +pixel_x = (point.x - x_min) * scale +pixel_y = (y_max - point.y) * scale // Y 뒤집기 (TTF는 Y-up) +``` + +### Bezier Flattening +Quadratic bezier (P0, P1, P2)를 선분으로 분할: +- 재귀 분할: 중간점과 직선 거리가 threshold(0.5px) 이하이면 직선으로 근사 +- 결과: Vec<(f32, f32)> 선분 목록 + +### 스캔라인 래스터라이징 +1. 모든 contour를 선분 목록으로 변환 +2. 각 스캔라인 (y = 0..height): + - 모든 선분과 교차점 계산 + - 교차점을 x 기준 정렬 + - winding rule (non-zero): 교차할 때 winding number 증감 + - winding != 0인 구간을 채움 + +### 안티앨리어싱 +- 서브픽셀 없이 단순 coverage: 픽셀 경계에서의 부분 커버리지 계산 +- 또는 4x 수직 슈퍼샘플링 (y를 4등분, 평균) + +### 출력 +```rust +pub fn rasterize(outline: &GlyphOutline, scale: f32) -> RasterResult + +pub struct RasterResult { + pub width: u32, + pub height: u32, + pub bitmap: Vec, // R8 알파맵 + pub offset_x: f32, // bearing + pub offset_y: f32, // baseline offset +} +``` + +## GlyphCache + +온디맨드 아틀라스를 관리한다. + +```rust +pub struct GlyphCache { + atlas_data: Vec, // CPU측 R8 버퍼 (텍스처 업로드용) + atlas_width: u32, // 1024 + atlas_height: u32, // 1024 + glyphs: HashMap, + cursor_x: u32, + cursor_y: u32, + row_height: u32, + dirty: bool, // GPU 업로드 필요 여부 +} + +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, +} +``` + +### 글리프 삽입 로직 +1. `glyphs.get(ch)` 로 캐시 확인 +2. 없으면: parser.glyph_index → parser.glyph_outline → rasterize +3. 아틀라스에 비트맵 복사 (cursor_x, cursor_y 위치) +4. cursor_x 전진. 줄 넘김 시 cursor_y += row_height, cursor_x = 0 +5. GlyphInfo 생성 (UV 좌표 계산) +6. dirty = true + +### GPU 업로드 +- `upload_if_dirty(queue, texture)` — dirty면 queue.write_texture로 전체 아틀라스 업로드 +- 또는 변경된 영역만 부분 업로드 (최적화, 나중에) + +## 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, +} +``` + +### 메서드 +- `new(data: &[u8], font_size: f32) -> Result` +- `glyph(&mut self, ch: char) -> &GlyphInfo` +- `text_width(&mut self, text: &str) -> f32` +- `atlas_data(&self) -> &[u8]` — CPU 아틀라스 데이터 +- `atlas_size(&self) -> (u32, u32)` +- `is_dirty(&self) -> bool` +- `clear_dirty(&mut self)` + +## UI 통합 + +### UiContext 변경 +```rust +pub struct UiContext { + // 기존 필드... + pub font: FontAtlas, // 기존 비트맵 폰트 유지 + pub ttf_font: Option, // TTF 폰트 (옵션) +} +``` + +### 텍스트 렌더링 변경 +기존 위젯(text, button, slider 등)에서 텍스트를 그릴 때: +- `ttf_font`이 Some이면 → TTF 글리프 사용 (가변폭) +- None이면 → 기존 비트맵 폰트 폴백 + +이를 위한 헬퍼: +```rust +impl UiContext { + /// 텍스트 너비 계산 (TTF or 비트맵) + pub fn text_width(&mut self, text: &str) -> f32; + + /// 텍스트 렌더링 (TTF or 비트맵) + pub fn draw_text(&mut self, text: &str, x: f32, y: f32, color: [u8; 4]); +} +``` + +기존 위젯 코드에서 인라인 글리프 렌더링을 `draw_text()` 호출로 교체하면 TTF/비트맵 전환이 투명해짐. + +### UiRenderer 변경 +- TTF 아틀라스 텍스처 생성 + 바인드 그룹 +- 매 프레임 dirty 체크 → 업로드 +- 기존 ui_shader.wgsl 재사용 가능 (R8 알파 텍스처 동일 패턴) +- DrawCommand에 텍스처 종류 플래그 추가 (bitmap vs ttf) 또는 별도 draw call + +## File Structure + +- `crates/voltex_editor/src/ttf_parser.rs` — TTF 바이너리 파싱 +- `crates/voltex_editor/src/rasterizer.rs` — 아웃라인 → 비트맵 +- `crates/voltex_editor/src/glyph_cache.rs` — 온디맨드 아틀라스 +- `crates/voltex_editor/src/ttf_font.rs` — 통합 인터페이스 +- `crates/voltex_editor/src/lib.rs` — 모듈 추가 + +## Testing + +### TtfParser (순수 바이트 파싱, GPU 불필요) +- 테이블 디렉토리 파싱 +- cmap 매핑: 'A'(0x41) → 올바른 glyph index +- 글리프 아웃라인: 알려진 글리프의 contour 수, 점 수 검증 +- hmtx: advance width 검증 +- 에러 케이스: 잘못된 데이터 + +### Rasterizer (순수 수학, GPU 불필요) +- 직선 contour (사각형) → 비트맵 검증 +- Bezier flatten: 곡선이 선분으로 올바르게 분할되는지 +- 빈 글리프(space 등) → 0x0 비트맵 +- 스케일링 정확성 + +### GlyphCache (GPU 불필요 — CPU측만 테스트) +- 글리프 삽입 → UV 좌표 검증 +- 중복 삽입 → 캐시 히트 +- 아틀라스 가득 참 → 줄 넘김 동작 + +### TtfFont (통합) +- text_width: "Hello" 너비 > 0 +- 한글 글리프 래스터라이즈 성공 + +### 테스트 TTF 파일 +NotoSansMono-Regular.ttf 등 오픈소스 폰트를 `assets/fonts/` 에 포함하거나, 테스트에서 시스템 폰트 경로 사용.