use crate::{font::{FontProvider, Glyph}, grid::VoxelGrid, models::VoxelType}; enum GlyphToken { Char(Glyph), Space(u32), } pub enum OutlineMode { FourConnected, EightConnected, } pub struct GenerationOptions { pub extrusion_depth: u32, pub generate_outline: bool, pub outline_mode: OutlineMode, pub generate_shadow: bool, pub shadow_offset: (i32, i32), /// Overrides the font's default letter spacing (None = use font default). pub letter_spacing: Option, /// Width in blocks inserted for a space character. pub word_spacing: u32, } impl Default for GenerationOptions { fn default() -> Self { Self { extrusion_depth: 1, generate_outline: true, outline_mode: OutlineMode::EightConnected, generate_shadow: false, shadow_offset: (1, -1), letter_spacing: None, word_spacing: 4, } } } pub struct TextBuilder<'a> { font: &'a dyn FontProvider, options: GenerationOptions, } impl<'a> TextBuilder<'a> { pub fn new(font: &'a dyn FontProvider) -> Self { Self { font, options: GenerationOptions::default(), } } pub fn with_depth(mut self, depth: u32) -> Self { self.options.extrusion_depth = depth.max(1); self } pub fn with_outline(mut self, generate: bool) -> Self { self.options.generate_outline = generate; self } pub fn with_outline_mode(mut self, mode: OutlineMode) -> Self { self.options.outline_mode = mode; self } pub fn with_shadow(mut self, generate: bool) -> Self { self.options.generate_shadow = generate; self } pub fn with_shadow_offset(mut self, dx: i32, dy: i32) -> Self { self.options.shadow_offset = (dx, dy); self } pub fn with_letter_spacing(mut self, spacing: u32) -> Self { self.options.letter_spacing = Some(spacing); self } pub fn with_word_spacing(mut self, spacing: u32) -> Self { self.options.word_spacing = spacing; self } pub fn generate(&self, text: &str) -> VoxelGrid { let (tokens, text_width, max_height) = self.collect_glyphs(text); let (base_x, base_y, grid_w, grid_h) = self.compute_layout(text_width, max_height); let mut grid = VoxelGrid::new(grid_w, grid_h, self.options.extrusion_depth); self.place_glyphs(&mut grid, &tokens, base_x, base_y); if self.options.generate_shadow { self.apply_shadow(&mut grid); } if self.options.generate_outline { self.apply_outline(&mut grid); } grid } fn effective_letter_spacing(&self) -> u32 { self.options.letter_spacing.unwrap_or_else(|| self.font.letter_spacing()) } fn collect_glyphs(&self, text: &str) -> (Vec, u32, u32) { let letter_spacing = self.effective_letter_spacing(); let mut total_width = 0; let mut max_height = 0; let mut tokens = Vec::new(); for c in text.chars() { if c == ' ' { total_width += self.options.word_spacing; tokens.push(GlyphToken::Space(self.options.word_spacing)); } else if let Some(glyph) = self.font.get_glyph(c) { total_width += glyph.width + letter_spacing; max_height = max_height.max(glyph.height); tokens.push(GlyphToken::Char(glyph)); } } (tokens, total_width, max_height) } fn compute_layout(&self, text_width: u32, max_height: u32) -> (u32, u32, u32, u32) { let outline_pad = if self.options.generate_outline { 1u32 } else { 0 }; let (sdx, sdy) = self.options.shadow_offset; let shadow_x_extra = if self.options.generate_shadow { sdx.max(0) as u32 } else { 0 }; let shadow_y_extra = if self.options.generate_shadow { (-sdy).max(0) as u32 } else { 0 }; let grid_w = text_width + outline_pad * 2 + shadow_x_extra; let grid_h = max_height + outline_pad * 2 + shadow_y_extra; let base_x = outline_pad; let base_y = outline_pad + shadow_y_extra; (base_x, base_y, grid_w, grid_h) } fn place_glyphs(&self, grid: &mut VoxelGrid, tokens: &[GlyphToken], base_x: u32, base_y: u32) { let letter_spacing = self.effective_letter_spacing(); let mut current_x = base_x; for token in tokens { match token { GlyphToken::Space(w) => current_x += w, GlyphToken::Char(glyph) => { for gy in 0..glyph.height { for gx in 0..glyph.width { if glyph.data[(gy * glyph.width + gx) as usize] { let wx = current_x + gx; let wy = base_y + (glyph.height - 1 - gy); // flip vertically for z in 0..self.options.extrusion_depth { if let Err(e) = grid.set(wx, wy, z, VoxelType::Body) { tracing::warn!("failed to set voxel: {}", e); } } } } } current_x += glyph.width + letter_spacing; } } } } fn apply_shadow(&self, grid: &mut VoxelGrid) { let (dx, dy) = self.options.shadow_offset; let (w, h, d) = (grid.width, grid.height, grid.depth); let body_positions: Vec<(u32, u32, u32)> = (0..w) .flat_map(|x| (0..h).flat_map(move |y| (0..d).map(move |z| (x, y, z)))) .filter(|&(x, y, z)| grid.get(x, y, z) == Some(VoxelType::Body)) .collect(); for (x, y, z) in body_positions { let sx = x as i32 + dx; let sy = y as i32 + dy; if sx >= 0 && sy >= 0 { let (sx, sy) = (sx as u32, sy as u32); if grid.get(sx, sy, z).is_none() { if let Err(e) = grid.set(sx, sy, z, VoxelType::Shadow) { tracing::warn!("failed to set shadow voxel: {}", e); } } } } } fn apply_outline(&self, grid: &mut VoxelGrid) { let (w, h, d) = (grid.width, grid.height, grid.depth); let body_xy: Vec<(u32, u32)> = (0..w) .flat_map(|x| (0..h).map(move |y| (x, y))) .filter(|&(x, y)| (0..d).any(|z| grid.get(x, y, z) == Some(VoxelType::Body))) .collect(); for (x, y) in body_xy { for (ox, oy) in Self::neighbor_offsets(&self.options.outline_mode) { let nx = x as i32 + ox; let ny = y as i32 + oy; if nx >= 0 && ny >= 0 { let (nx, ny) = (nx as u32, ny as u32); for z in 0..d { if grid.get(nx, ny, z) != Some(VoxelType::Body) { if let Err(e) = grid.set(nx, ny, z, VoxelType::Outline) { tracing::warn!("failed to set outline voxel: {}", e); } } } } } } } fn neighbor_offsets(mode: &OutlineMode) -> &'static [(i32, i32)] { match mode { OutlineMode::FourConnected => &[(-1, 0), (1, 0), (0, -1), (0, 1)], OutlineMode::EightConnected => &[ (-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1), ], } } }