Files
OneCAD-Draft/src/io/linetypes.rs
T
Hakan SevenandClaude Opus 5.5 893dcee6a7 feat(resources): load the user's own hatch patterns and linetypes
*.pat files in <config>/patterns and *.lin files in <config>/linetypes
are read after the bundled catalogs, next to the fonts folder SHX
files already resolve from.

Fixes #1725

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:04:25 +03:00

811 lines
29 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! OpenCADStudio linetype catalog — loaded from `assets/linetypes/OpenCADStudio.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 rustc_hash::FxHashMap as HashMap;
use std::sync::OnceLock;
use codec::tables::linetype::{LineType, LineTypeComplexContent, LineTypeElement};
use codec::{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 shape at the current pen position.
Shape {
/// Shape name (`.lin` catalog reference; empty for document shapes).
name: String,
/// Resolved .SHX shape-file path ("" when none resolved — the LFF
/// substitute set is the fallback for catalog names).
shx_file: String,
/// Shape number inside the file (0 = look up by name).
number: u16,
/// 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,
},
/// Draw a text string at the current pen position.
Text {
/// The string to render.
text: String,
/// Text style / font name.
style: String,
/// X offset along the linetype direction.
x: f32,
/// Y offset perpendicular to the linetype direction.
y: f32,
/// Height scale factor.
scale: f32,
/// 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<LtSegment>,
}
static COMPLEX_CATALOG: OnceLock<HashMap<String, ComplexLt>> = 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())
}
/// Build the full segment list — dashes, spaces, dots **and** embedded
/// text / shape — for a linetype defined in the loaded **document**, whether it
/// is simple or complex.
///
/// Returns `None` when the linetype is missing or continuous (no pattern). Used
/// by the MLINE renderer: every element is CPU-dashed by the same walker so the
/// parallel lines (and any embedded text) stay in phase with each other. Ordinary
/// entities keep the cheaper shader dash for simple linetypes; see
/// [`document_complex_lt`] / [`resolve_complex_lt`].
pub fn document_lt_segments(document: &CadDocument, name: &str) -> Option<ComplexLt> {
let lt = document
.line_types
.iter()
.find(|l| l.name.eq_ignore_ascii_case(name))?;
let mut segments: Vec<LtSegment> = Vec::with_capacity(lt.elements.len() + 1);
let mut has_length = false;
for e in &lt.elements {
// Emit the dash / space for this element FIRST, then any embedded
// text / shape — the pen advances across the element's length before the
// glyph is placed, so the text lands at the element's *end* (inside the
// gap), matching the `.lin` token order (`dash, -space, ["TEXT"], …`) and
// AutoCAD's placement. Putting the text first drops it onto the preceding
// dash, so the dash strikes through the glyphs.
let len = e.length as f32;
if len > 1e-9 {
segments.push(LtSegment::Dash(len));
has_length = true;
} else if len < -1e-9 {
segments.push(LtSegment::Space(-len));
has_length = true;
} else if e.complex.is_none() {
// A zero-length *non-complex* element is a dot; a zero-length complex
// element only contributes its text/shape (pushed below).
segments.push(LtSegment::Dot);
has_length = true;
}
if let Some(c) = &e.complex {
match &c.content {
LineTypeComplexContent::Text { text } => {
let style_name = document
.text_styles
.iter()
.find(|s| s.handle == c.style_handle)
.map(|s| s.name.as_str())
.unwrap_or("");
let font =
crate::entities::text_support::resolve_text_style(style_name, document)
.font_name;
let scale = c.scale as f32;
segments.push(LtSegment::Text {
text: text.clone(),
style: font,
x: c.offset[0] as f32,
y: c.offset[1] as f32,
scale: if scale.abs() < 1e-6 { 1.0 } else { scale },
// DWG stores the element rotation in RADIANS; the
// segment field carries degrees (the `.lin` unit).
rot_deg: (c.rotation as f32).to_degrees(),
});
}
LineTypeComplexContent::Shape { shape_number } => {
// Resolve the style's shape file with the shared path
// fallbacks (as stored / next to the drawing) so the real
// SHX glyph draws; unresolved files simply skip.
let font = document
.text_styles
.iter()
.find(|s| s.handle == c.style_handle)
.map(|s| s.font_file.trim().to_string())
.unwrap_or_default();
if *shape_number <= 0 {
continue;
}
let base = document
.source_path
.as_deref()
.map(std::path::Path::new)
.and_then(|p| p.parent());
let resolved = (!font.is_empty())
.then(|| crate::io::resolve_image_file(&font, base))
.flatten();
// The standard ltypeshp.shx numbers (and the bundled
// catalog's own) map to the LFF substitute shapes, so the
// glyph still draws when the shape file isn't on disk
// (the usual case — traded drawings rarely ship their .shx).
let sub_name = bundled_shape_name(&font, *shape_number).unwrap_or("");
if resolved.is_none() && sub_name.is_empty() {
continue;
}
let scale = c.scale as f32;
segments.push(LtSegment::Shape {
name: sub_name.to_string(),
shx_file: resolved.unwrap_or_default(),
number: *shape_number as u16,
x: c.offset[0] as f32,
y: c.offset[1] as f32,
scale: if scale.abs() < 1e-6 { 1.0 } else { scale },
rot_deg: (c.rotation as f32).to_degrees(),
});
}
}
}
}
// No dash/space/dot means "continuous" — let the caller draw a solid line.
if !has_length {
return None;
}
Some(ComplexLt { segments })
}
/// Complex (embedded text/shape) segments for a **document** linetype, or `None`
/// when it is simple (dash-only) — simple dashes are handled by the ordinary
/// `resolve_pattern` shader path, so ordinary entities don't pay the CPU-dash
/// cost. This is the gate used by [`resolve_complex_lt`].
pub fn document_complex_lt(document: &CadDocument, name: &str) -> Option<ComplexLt> {
let lt = document
.line_types
.iter()
.find(|l| l.name.eq_ignore_ascii_case(name))?;
// Treat the linetype as complex only when an element carries *renderable*
// embedded content — a non-empty text string, or a shape backed by a shape
// file. Some DWGs store dash elements with a placeholder `complex` (shape
// #0, null style handle, zero scale) that draws nothing; routing those
// through the CPU complex-linetype path needlessly skips the normal dash
// shader — and with it the "A"-type endpoint alignment. Fall through to the
// ordinary `resolve_pattern` path for them.
let has_real_complex = lt.elements.iter().any(|e| {
e.complex.as_ref().is_some_and(|cx| match &cx.content {
LineTypeComplexContent::Text { text } => !text.trim().is_empty(),
LineTypeComplexContent::Shape { .. } => !cx.style_handle.is_null(),
})
});
if !has_real_complex {
return None;
}
document_lt_segments(document, name)
}
/// Resolve a complex linetype honouring "**document first, bundled catalog
/// second**": if the drawing defines the linetype, its own definition wins
/// (embedded text/shape, or `None` when the drawing's version is simple); only a
/// linetype absent from the drawing falls back to the bundled `.lin` catalog.
pub fn resolve_complex_lt(document: &CadDocument, name: &str) -> Option<ComplexLt> {
let in_document = document
.line_types
.iter()
.any(|l| l.name.eq_ignore_ascii_case(name));
if in_document {
document_complex_lt(document, name)
} else {
complex_lt(name).cloned()
}
}
const LIN_SOURCE: &str = include_str!("../../assets/linetypes/OpenCADStudio.lin");
/// The bundled linetypes followed by the user's own `.lin` files.
fn lin_source() -> &'static str {
static SOURCE: OnceLock<String> = OnceLock::new();
SOURCE.get_or_init(|| {
format!("{LIN_SOURCE}\n{}", crate::io::patterns::user_definitions("linetypes", "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 OpenCADStudio linetypes to `doc`, skipping existing ones.
pub fn populate_document(doc: &mut CadDocument) {
populate_document_from_source(doc, lin_source());
}
/// Add the linetypes of a caller-supplied `.lin` source, embedded shapes and
/// text included, so the drawing draws them and a saved file carries them.
/// Returns the number of definitions newly registered in the drawing.
///
/// A linetype the drawing already has keeps its own definition, with one
/// repair: a copy stripped to its dashes (as this loader used to add them,
/// and so saved) gets its shapes and text back when its dashes match the
/// source's exactly.
pub fn populate_document_from_source(doc: &mut CadDocument, source: &str) -> usize {
let complex = parse_complex(source);
let mut added = 0;
for mut lt in parse(source) {
let segments = complex.get(&lt.name.to_ascii_uppercase());
if let Some(existing) = doc.line_types.get(&lt.name) {
let stripped =
segments.is_some()
&& !existing.is_complex()
&& existing.elements.len() == lt.elements.len()
&& existing.elements.iter().zip(&lt.elements).all(|(a, b)| {
(a.length - b.length).abs() <= 1e-9 * b.length.abs().max(1.0)
});
if stripped {
if let Some(elements) =
segments.and_then(|s| complex_elements(doc, s, &lt.elements))
{
if let Some(existing) = doc.line_types.get_mut(&lt.name) {
existing.elements = elements;
}
}
}
continue;
}
if let Some(elements) = segments.and_then(|s| complex_elements(doc, s, &lt.elements)) {
lt.elements = elements;
}
lt.set_handle(doc.allocate_handle());
doc.line_types.add(lt).ok();
added += 1;
}
added
}
/// Shape numbers for the shape files the bundled catalog references,
/// keyed by file stem and shape name. The `ltypeshp` 130–134 are the
/// standard ones; the rest exist only in the bundled LFF substitutes, so
/// their numbers are this application's own.
const BUNDLED_SHAPES: &[(&str, &str, i16)] = &[
("ltypeshp", "TRACK1", 130),
("ltypeshp", "ZIG", 131),
("ltypeshp", "BOX", 132),
("ltypeshp", "CIRC1", 133),
("ltypeshp", "BAT", 134),
("ltypeshp", "PT1", 135),
("ltypeshp", "PT2", 136),
("ltypeshp", "PLANT1", 137),
("ltypeshp", "PLANT2", 138),
("ltypeshp", "PLANT3", 139),
("ltypeshp", "PLANT4", 140),
("ltypeshp", "PLANT5", 141),
("ltypeshp", "PLANT6", 142),
("qcadshp", "RIGHT_ARROW", 130),
];
/// A catalog value as the decimal it was written as: 2.54 stays 2.54, not
/// the 2.5399999618… an `f32` widens to.
fn decimal(value: f32) -> f64 {
value.to_string().parse().unwrap_or(value as f64)
}
fn shape_file_stem(file: &str) -> String {
let name = file.rsplit(['/', '\\']).next().unwrap_or(file);
name.split('.').next().unwrap_or(name).to_ascii_lowercase()
}
/// The bundled shape a stored shape number names in `file` (an empty file
/// reads as `ltypeshp`).
fn bundled_shape_name(file: &str, number: i16) -> Option<&'static str> {
let stem = shape_file_stem(file);
let stem = if stem.is_empty() {
"ltypeshp".to_string()
} else {
stem
};
BUNDLED_SHAPES
.iter()
.find(|(file, _, n)| *file == stem && *n == number)
.map(|(_, name, _)| *name)
}
/// Whether `file` is a shape file the bundled LFF substitutes stand in for,
/// so its absence on disk never needs reporting.
pub fn is_bundled_shape_file(file: &str) -> bool {
let stem = shape_file_stem(file);
BUNDLED_SHAPES.iter().any(|(f, _, _)| *f == stem)
}
/// The drawing's shape-file STYLE for `file`, added when missing (an empty
/// name, as shape files are stored).
fn shape_file_style(doc: &mut CadDocument, file: &str) -> codec::Handle {
if let Some(style) = doc
.text_styles
.iter()
.find(|s| s.is_shape_file && s.font_file.eq_ignore_ascii_case(file))
{
return style.handle;
}
let mut style = codec::tables::TextStyle::new("");
style.handle = doc.allocate_handle();
style.is_shape_file = true;
style.font_file = file.to_string();
let handle = style.handle;
doc.text_styles.add_allow_duplicate(style);
handle
}
/// Document elements for catalog segments: each shape or text rides on the
/// element before it, as in the `.lin` token order. `None` when a piece
/// cannot be stored (an unknown shape or text style), so the caller keeps
/// the plain dashes rather than a half-built pattern.
///
/// The lengths come from `plain` — the same pattern parsed at full
/// precision — rather than the catalog's `f32` segments.
fn complex_elements(
doc: &mut CadDocument,
lt: &ComplexLt,
plain: &[LineTypeElement],
) -> Option<Vec<LineTypeElement>> {
use codec::tables::linetype::LineTypeComplexData;
let mut out: Vec<LineTypeElement> = Vec::new();
let mut lengths = plain.iter();
for segment in &lt.segments {
let complex = match segment {
LtSegment::Dash(_) | LtSegment::Space(_) | LtSegment::Dot => {
out.push(lengths.next()?.clone());
continue;
}
LtSegment::Shape {
name,
shx_file,
x,
y,
scale,
rot_deg,
..
} => {
let stem = shape_file_stem(shx_file);
let number = BUNDLED_SHAPES
.iter()
.find(|(file, shape, _)| *file == stem && shape.eq_ignore_ascii_case(name))?
.2;
LineTypeComplexData {
content: LineTypeComplexContent::Shape {
shape_number: number,
},
style_handle: shape_file_style(doc, shx_file),
scale: decimal(*scale),
rotation: decimal(*rot_deg).to_radians(),
absolute_rotation: false,
offset: [decimal(*x), decimal(*y)],
}
}
LtSegment::Text {
text,
style,
x,
y,
scale,
rot_deg,
} => LineTypeComplexData {
content: LineTypeComplexContent::Text { text: text.clone() },
style_handle: doc
.text_styles
.iter()
.find(|s| !s.is_shape_file && s.name.eq_ignore_ascii_case(style))?
.handle,
scale: decimal(*scale),
rotation: decimal(*rot_deg).to_radians(),
absolute_rotation: false,
offset: [decimal(*x), decimal(*y)],
},
};
out.last_mut()?.complex = Some(complex);
}
(lengths.next().is_none()).then_some(out)
}
// ── Parser ────────────────────────────────────────────────────────────────
/// Parse a `.lin` file and return all simple linetypes found.
fn parse(src: &str) -> Vec<LineType> {
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]. The description is
// optional, and the comma goes with it.
let (name, desc) = rest.split_once(',').unwrap_or((rest, ""));
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<LineTypeElement> {
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<LineTypeElement>) {
let t = token.trim();
if t.is_empty() {
return;
}
if let Ok(v) = t.parse::<f64>() {
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<String, ComplexLt> {
let mut catalog: HashMap<String, ComplexLt> = HashMap::default();
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<LtSegment> {
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
}
/// Parse a bracketed linetype element.
/// Handles both shape (`SHAPENAME,...`) and text (`"string",...`) elements.
fn parse_shape_element(inner: &str) -> Option<LtSegment> {
let inner = inner.trim();
if inner.starts_with('"') {
return parse_text_element(inner);
}
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;
let mut shx_file = String::new();
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);
} else if p.to_ascii_lowercase().ends_with(".shx") {
// `[NAME,ltypeshp.shx,…]` — the shape file reference.
shx_file = p.to_string();
}
}
Some(LtSegment::Shape {
name,
shx_file,
number: 0,
x,
y,
scale,
rot_deg,
})
}
/// Parse `"TEXT",STYLE,x=v,y=v,s=v,r=v` into an `LtSegment::Text`.
fn parse_text_element(inner: &str) -> Option<LtSegment> {
// Find the closing quote.
let rest = inner.strip_prefix('"')?;
let end = rest.find('"')?;
let text = rest[..end].to_string();
let after = rest[end + 1..].trim().trim_start_matches(',');
let mut parts = after.split(',');
let style = parts
.next()
.map(|s| s.trim().to_string())
.unwrap_or_default();
let style = if style.is_empty() {
"Standard".to_string()
} else {
style
};
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::Text {
text,
style,
x,
y,
scale,
rot_deg,
})
}
fn push_lt_segment(token: &str, out: &mut Vec<LtSegment>) {
let t = token.trim();
if t.is_empty() {
return;
}
if let Ok(v) = t.parse::<f32>() {
let seg = if v > 0.0 {
LtSegment::Dash(v)
} else if v < 0.0 {
LtSegment::Space(v.abs())
} else {
LtSegment::Dot
};
out.push(seg);
}
}
#[cfg(test)]
mod header_tests {
use super::{parse, populate_document_from_source};
use codec::CadDocument;
/// A `.lin` header's description is optional, and without one the comma
/// goes too: `*PLAIN` followed by its `A,` line is a whole definition.
#[test]
fn a_linetype_without_a_description_is_loaded() {
let mut doc = CadDocument::new();
let added = populate_document_from_source(&mut doc, "*PLAIN\nA,.5,-.25\n");
assert_eq!(added, 1, "the definition was dropped");
assert!(doc.line_types.contains("PLAIN"));
let types = parse("*PLAIN\nA,.5,-.25\n");
assert_eq!(types[0].description, "");
let lengths: Vec<f64> = types[0].elements.iter().map(|e| e.length).collect();
assert_eq!(lengths, vec![0.5, -0.25]);
}
/// A header with a description still splits at the first comma.
#[test]
fn a_description_after_the_comma_is_kept() {
let types = parse("*DASHED,Dashed __ __\nA,.5,-.25\n");
assert_eq!(types[0].name, "DASHED");
assert_eq!(types[0].description, "Dashed __ __");
}
}