diff --git a/src/io/linetypes.rs b/src/io/linetypes.rs index 7066e5cf7..4d286baf6 100644 --- a/src/io/linetypes.rs +++ b/src/io/linetypes.rs @@ -157,18 +157,11 @@ pub fn document_lt_segments(document: &CadDocument, name: &str) -> Option "TRACK1", - 131 => "ZIG", - 132 => "BOX", - 133 => "CIRC1", - 134 => "BAT", - _ => "", - }; + // 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; } @@ -268,20 +261,163 @@ pub fn populate_document(doc: &mut CadDocument) { populate_document_from_source(doc, LIN_SOURCE); } -/// Add all simple linetypes parsed from a caller-supplied `.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) { - if !doc.line_types.contains(<.name) { - lt.set_handle(doc.allocate_handle()); - doc.line_types.add(lt).ok(); - added += 1; + let segments = complex.get(<.name.to_ascii_uppercase()); + if let Some(existing) = doc.line_types.get(<.name) { + let stripped = segments.is_some() + && !existing.is_complex() + && existing.elements.len() == lt.elements.len() + && existing + .elements + .iter() + .zip(<.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, <.elements)) { + if let Some(existing) = doc.line_types.get_mut(<.name) { + existing.elements = elements; + } + } + } + continue; } + if let Some(elements) = segments.and_then(|s| complex_elements(doc, s, <.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) +} + +/// 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> { + use codec::tables::linetype::LineTypeComplexData; + let mut out: Vec = Vec::new(); + let mut lengths = plain.iter(); + for segment in <.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 ──────────────────────────────────────────────────────────────── diff --git a/src/io/mod.rs b/src/io/mod.rs index e40a7a40f..a6c4e96b3 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -355,6 +355,10 @@ async fn open_path_with_phase_attempt( recoverable: true, }); } + // Before the geometry is prepared below: completing the + // drawing's catalog linetypes (and repairing stripped ones) + // after it would leave the first view drawn without them. + crate::io::linetypes::populate_document(&mut doc); progress2.set(crate::app::OPEN_PHASE_XREF, 6000, 0, 1); let t_xref = Instant::now(); let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {