//! H7CAD linetype catalog — loaded from `assets/linetypes/H7CAD.lin`. //! //! Call [`populate_document`] to add all standard linetypes to a new document. //! Linetypes that already exist in the document are skipped. //! //! [`complex_lt`] returns the complex segment catalog for CPU-side rendering. use std::collections::HashMap; use std::sync::OnceLock; use acadrust::tables::linetype::{LineType, LineTypeElement}; use acadrust::{CadDocument, TableEntry}; // ── Complex linetype types ──────────────────────────────────────────────── /// One element in a complex linetype pattern. #[derive(Clone, Debug)] pub enum LtSegment { /// Draw a dash of this length (world units). Dash(f32), /// Skip a gap of this length (world units). Space(f32), /// Draw a dot (zero-length element, rendered as a tiny mark). Dot, /// Draw a CXF shape at the current pen position. Shape { /// Shape name — looked up in `scene::cxf`. name: String, /// X offset along the linetype direction (can be negative). x: f32, /// Y offset perpendicular to the linetype direction. y: f32, /// Scale factor applied to the CXF coordinates. scale: f32, /// Optional rotation in degrees (0 = along the line). rot_deg: f32, }, } /// A complex linetype — ordered elements for one pattern repeat. #[derive(Clone, Debug)] pub struct ComplexLt { pub segments: Vec, } static COMPLEX_CATALOG: OnceLock> = OnceLock::new(); /// Look up a complex linetype by name (case-insensitive). /// Returns `None` for simple (dash-only) linetypes or unknown names. pub fn complex_lt(name: &str) -> Option<&'static ComplexLt> { COMPLEX_CATALOG .get_or_init(|| parse_complex(LIN_SOURCE)) .get(&name.to_ascii_uppercase()) } const LIN_SOURCE: &str = include_str!("../assets/linetypes/H7CAD.lin"); // ── Pattern art extraction ──────────────────────────────────────────────── /// Extract the ASCII-art portion of a LIN description. /// /// LIN descriptions look like `"Dashed __ __ __ __"` or `"ISO dot . . . ."`. /// This function returns the trailing pattern string (e.g. `"__ __ __ __"`). pub fn extract_pattern(desc: &str) -> String { // Patterns that use dashes start with `__`. if let Some(pos) = desc.find("__") { let start = desc[..pos].rfind(' ').map(|p| p + 1).unwrap_or(0); return desc[start..].trim().to_string(); } // Dot-only patterns: look for `. .` sequence. if let Some(pos) = desc.find(". .") { let start = desc[..pos].rfind(' ').map(|p| p + 1).unwrap_or(0); return desc[start..].trim().to_string(); } // Fallback: return the whole description. desc.trim().to_string() } // ── Public API ──────────────────────────────────────────────────────────── /// Add all standard H7CAD linetypes to `doc`, skipping existing ones. pub fn populate_document(doc: &mut CadDocument) { for mut lt in parse(LIN_SOURCE) { if !doc.line_types.contains(<.name) { lt.set_handle(doc.allocate_handle()); doc.line_types.add(lt).ok(); } } } /// Names of all linetypes that will be in the document after [`populate_document`]. #[allow(dead_code)] pub fn name_list() -> Vec { let mut names = vec![ "ByLayer".to_string(), "ByBlock".to_string(), "Continuous".to_string(), ]; for lt in parse(LIN_SOURCE) { names.push(lt.name); } names } // ── Parser ──────────────────────────────────────────────────────────────── /// Parse a `.lin` file and return all simple linetypes found. fn parse(src: &str) -> Vec { let mut result = Vec::new(); let mut current: Option<(String, String)> = None; // (name, description) for raw in src.lines() { let line = raw.trim(); // Skip comments and blank lines. if line.is_empty() || line.starts_with(";;") { continue; } if let Some(rest) = line.strip_prefix('*') { // New pattern header: *NAME,Description if let Some((name, desc)) = rest.split_once(',') { current = Some((name.trim().to_string(), desc.trim().to_string())); } } else if line.to_ascii_uppercase().starts_with('A') { // Element line: A,v1,v2,... let Some((name, desc)) = current.take() else { continue; }; let after_a = match line[1..].trim_start().strip_prefix(',') { Some(s) => s, None => continue, }; let elements = parse_elements(after_a); let pattern_length: f64 = elements.iter().map(|e| e.length.abs()).sum(); let mut lt = LineType::new(&name); lt.description = desc; lt.pattern_length = pattern_length; for e in elements { lt.add_element(e); } result.push(lt); } } result } /// Parse the comma-separated element values after `A,`. /// Complex elements `[...]` are skipped; only plain numbers are used. fn parse_elements(s: &str) -> Vec { let mut elements = Vec::new(); let mut chars = s.chars().peekable(); let mut token = String::new(); while let Some(&c) = chars.peek() { if c == '[' { // Skip entire complex element including nested brackets. let mut depth = 0usize; for ch in chars.by_ref() { if ch == '[' { depth += 1; } else if ch == ']' { depth -= 1; if depth == 0 { break; } } } // Consume trailing comma if present. if chars.peek() == Some(&',') { chars.next(); } continue; } if c == ',' { chars.next(); push_element(&token, &mut elements); token.clear(); } else { token.push(c); chars.next(); } } push_element(&token, &mut elements); elements } fn push_element(token: &str, out: &mut Vec) { let t = token.trim(); if t.is_empty() { return; } if let Ok(v) = t.parse::() { let elem = if v > 0.0 { LineTypeElement::dash(v) } else if v < 0.0 { LineTypeElement::space(v.abs()) } else { LineTypeElement::dot() }; out.push(elem); } } // ── Complex linetype parser ─────────────────────────────────────────────── /// Parse the LIN source and return a catalog of complex linetypes. /// A linetype is "complex" when its A-line contains at least one `[SHAPE,...]` /// element. Simple (dash-only) linetypes are excluded. fn parse_complex(src: &str) -> HashMap { let mut catalog: HashMap = HashMap::new(); let mut current: Option<(String, String)> = None; // (name, description) for raw in src.lines() { let line = raw.trim(); if line.is_empty() || line.starts_with(";;") { continue; } if let Some(rest) = line.strip_prefix('*') { if let Some((name, desc)) = rest.split_once(',') { current = Some((name.trim().to_string(), desc.trim().to_string())); } } else if line.to_ascii_uppercase().starts_with('A') { let Some((name, _desc)) = current.take() else { continue; }; let after_a = match line[1..].trim_start().strip_prefix(',') { Some(s) => s, None => continue, }; // Only collect linetypes that contain a shape element. if !after_a.contains('[') { continue; } let segments = parse_complex_elements(after_a); if segments.is_empty() { continue; } catalog.insert(name.to_ascii_uppercase(), ComplexLt { segments }); } } catalog } /// Parse complex A-line elements into `LtSegment`s. /// Shape elements: `[SHAPENAME,fontfile,x=v,y=v,s=v,r=v]` /// Text elements: `["TEXT",font,...]` — skipped (returns no segment). fn parse_complex_elements(s: &str) -> Vec { let mut segs = Vec::new(); let mut chars = s.chars().peekable(); let mut token = String::new(); while let Some(&c) = chars.peek() { if c == '[' { // Flush any pending numeric token. push_lt_segment(&token, &mut segs); token.clear(); // Read the entire bracketed element. let mut depth = 0usize; let mut inner = String::new(); for ch in chars.by_ref() { if ch == '[' { depth += 1; if depth > 1 { inner.push(ch); } } else if ch == ']' { depth -= 1; if depth == 0 { break; } inner.push(ch); } else { inner.push(ch); } } // Consume trailing comma. if chars.peek() == Some(&',') { chars.next(); } if let Some(seg) = parse_shape_element(&inner) { segs.push(seg); } } else if c == ',' { chars.next(); push_lt_segment(&token, &mut segs); token.clear(); } else { token.push(c); chars.next(); } } push_lt_segment(&token, &mut segs); segs } /// Try to parse `SHAPENAME,font,x=v,s=v,r=v` into an `LtSegment::Shape`. /// Text elements (start with `"`) are skipped → `None`. fn parse_shape_element(inner: &str) -> Option { let inner = inner.trim(); // Skip text elements. if inner.starts_with('"') { return None; } let mut parts = inner.split(','); let name = parts.next()?.trim().to_string(); if name.is_empty() { return None; } let mut x = 0.0f32; let mut y = 0.0f32; let mut scale = 1.0f32; let mut rot_deg = 0.0f32; for part in parts { let p = part.trim(); if let Some(v) = p.strip_prefix("x=").or_else(|| p.strip_prefix("X=")) { x = v.parse().unwrap_or(0.0); } else if let Some(v) = p.strip_prefix("y=").or_else(|| p.strip_prefix("Y=")) { y = v.parse().unwrap_or(0.0); } else if let Some(v) = p.strip_prefix("s=").or_else(|| p.strip_prefix("S=")) { scale = v.parse().unwrap_or(1.0); } else if let Some(v) = p .strip_prefix("r=") .or_else(|| p.strip_prefix("R=")) .or_else(|| p.strip_prefix("u=")) .or_else(|| p.strip_prefix("U=")) { rot_deg = v.parse().unwrap_or(0.0); } } Some(LtSegment::Shape { name, x, y, scale, rot_deg, }) } fn push_lt_segment(token: &str, out: &mut Vec) { let t = token.trim(); if t.is_empty() { return; } if let Ok(v) = t.parse::() { let seg = if v > 0.0 { LtSegment::Dash(v) } else if v < 0.0 { LtSegment::Space(v.abs()) } else { LtSegment::Dot }; out.push(seg); } }