use acadrust::CadDocument; use crate::scene::cxf; pub struct ResolvedTextStyle { pub font_name: String, pub width_factor: f32, pub oblique_angle: f32, } pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedTextStyle { let style = document.text_styles.iter().find(|entry| { entry.name.eq_ignore_ascii_case(style_name) || (style_name.trim().is_empty() && entry.name.eq_ignore_ascii_case("Standard")) }); let font_name = if let Some(style) = style { if !style.font_file.trim().is_empty() { let file = style.font_file.trim(); let basename = file.rsplit(['/', '\\']).next().unwrap_or(file); let stem = basename.split('.').next().unwrap_or(basename).trim(); if !stem.is_empty() { stem.to_string() } else if !style.true_type_font.trim().is_empty() { style.true_type_font.trim().to_string() } else if !style.name.trim().is_empty() { style.name.trim().to_string() } else { "Standard".to_string() } } else if !style.true_type_font.trim().is_empty() { style.true_type_font.trim().to_string() } else if !style.name.trim().is_empty() { style.name.trim().to_string() } else { "Standard".to_string() } } else if style_name.trim().is_empty() { "Standard".to_string() } else { style_name.trim().to_string() }; ResolvedTextStyle { font_name, width_factor: style.map(|s| s.width_factor as f32).unwrap_or(1.0), oblique_angle: style.map(|s| s.oblique_angle as f32).unwrap_or(0.0), } } pub fn text_local_bounds( font_name: &str, text: &str, height: f32, width_factor: f32, oblique_angle: f32, ) -> Option<([f32; 2], [f32; 2])> { if text.is_empty() || height <= 0.0 { return None; } let font = cxf::get_font(font_name); let scale = height / 9.0; let wf = width_factor.clamp(0.01, 100.0); let ob = oblique_angle.tan(); let mut cursor_x = 0.0_f32; let mut min_x = f32::INFINITY; let mut max_x = f32::NEG_INFINITY; let mut min_y = f32::INFINITY; let mut max_y = f32::NEG_INFINITY; for ch in text.chars() { if ch == ' ' { cursor_x += font.word_spacing; continue; } match font.glyph(ch) { Some(glyph) => { for stroke in &glyph.strokes { for &[gx, gy] in stroke { let sx = (cursor_x + gx) * scale * wf + gy * scale * ob; let sy = gy * scale; min_x = min_x.min(sx); max_x = max_x.max(sx); min_y = min_y.min(sy); max_y = max_y.max(sy); } } cursor_x += glyph.advance + font.letter_spacing; } None => { cursor_x += 6.0 + font.letter_spacing; } } } if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() { Some(([min_x, min_y], [max_x, max_y])) } else { None } } pub fn strip_mtext_codes(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut chars = s.chars().peekable(); while let Some(ch) = chars.next() { match ch { '\\' => match chars.peek().copied() { Some('P') | Some('n') | Some('N') => { chars.next(); out.push('\n'); } Some('~') => { chars.next(); out.push(' '); } Some(c) if "pHWQTACcLOKlfFUu".contains(c) => { chars.next(); for c in chars.by_ref() { if c == ';' { break; } } } Some('\\') => { chars.next(); out.push('\\'); } Some('{') | Some('}') => { out.push(chars.next().unwrap()); } _ => {} }, '{' | '}' => {} '\r' => {} other => out.push(other), } } out } pub fn split_mtext_lines(s: &str) -> Vec { s.split('\n') .map(|l| l.trim().to_string()) .filter(|l| !l.is_empty()) .collect() } pub fn word_wrap(text: &str, max_w: f32, scale: f32, font: &'static cxf::CxfFile) -> Vec { if max_w <= 0.0 || text.is_empty() { return vec![text.to_string()]; } let glyph_w = |c: char| -> f32 { if c == ' ' { return font.word_spacing * scale; } font.glyph(c) .map(|g| (g.advance + font.letter_spacing) * scale) .unwrap_or(scale * 6.0) }; let mut lines = Vec::new(); let mut current = String::new(); let mut current_w = 0.0_f32; for word in text.split(' ') { let word_w: f32 = word.chars().map(glyph_w).sum(); let space_w = if current.is_empty() { 0.0 } else { glyph_w(' ') }; if !current.is_empty() && current_w + space_w + word_w > max_w { lines.push(std::mem::take(&mut current)); current_w = 0.0; } if !current.is_empty() { current.push(' '); current_w += space_w; } current.push_str(word); current_w += word_w; } if !current.is_empty() { lines.push(current); } lines }