From 9a567d4f44f858bf3bb438402407c4725ace233a Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 16 Oct 2026 01:27:50 +0300 Subject: [PATCH] fix(linetype): an opened drawing keeps exactly its own linetypes (#1826) Opening a drawing added all 50 catalog linetypes to it, so PURGE found and removed them again after every save and reopen. Opening now only repairs the catalog linetypes a drawing already has. The linetype pickers (layers, properties, ribbon, attribute editor, layer states) offer the missing catalog ones after the drawing's own, and the one picked, or named in LINETYPE SET, is loaded on first use. Co-Authored-By: Claude Opus 5.5 --- src/app/commands/styleprops.rs | 5 ++++ src/app/layers.rs | 14 +++++++++++ src/app/properties.rs | 3 +++ src/app/update/file.rs | 2 +- src/app/update/mod.rs | 14 +++++++++++ src/io/linetypes.rs | 43 ++++++++++++++++++++++++++++++++++ src/io/mod.rs | 9 +++---- 7 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index b109907ca..d1147aa5f 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -205,6 +205,11 @@ impl OpenCADStudio { } else if name.eq_ignore_ascii_case("BYBLOCK") { Some(("ByBlock".to_string(), codec::types::Handle::NULL)) } else { + // A catalog linetype loads on first use. + crate::io::linetypes::ensure_catalog_linetype( + &mut self.tabs[i].scene.document, + name, + ); self.tabs[i] .scene .document diff --git a/src/app/layers.rs b/src/app/layers.rs index 76a1b6dd3..3984701d5 100644 --- a/src/app/layers.rs +++ b/src/app/layers.rs @@ -157,6 +157,7 @@ impl OpenCADStudio { let art = crate::io::linetypes::extract_pattern(<.description); ui::properties::LinetypeItem { name, art } }) + .chain(catalog_linetype_items(&self.tabs[i].scene.document)) .collect(); self.tabs[i].layers.sync_linetypes(lt_items.clone()); self.ribbon.set_available_linetypes(lt_items); @@ -247,3 +248,16 @@ impl OpenCADStudio { ); } } + +/// The catalog linetypes the drawing lacks, offered after its own; a pick +/// loads the one chosen (`ensure_catalog_linetype`). +pub(super) fn catalog_linetype_items( + document: &codec::CadDocument, +) -> impl Iterator { + crate::io::linetypes::catalog_missing_from(document) + .into_iter() + .map(|(name, description)| ui::properties::LinetypeItem { + name, + art: crate::io::linetypes::extract_pattern(&description), + }) +} diff --git a/src/app/properties.rs b/src/app/properties.rs index cdb514190..e376a84e9 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1824,6 +1824,9 @@ impl OpenCADStudio { let art = linetypes::extract_pattern(<.description); ui::properties::LinetypeItem { name, art } }) + .chain(super::layers::catalog_linetype_items( + &self.tabs[i].scene.document, + )) .collect(); let text_style_names: Vec = self.tabs[i] .scene diff --git a/src/app/update/file.rs b/src/app/update/file.rs index fecfbb10f..16bd379cb 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -2315,7 +2315,7 @@ impl OpenCADStudio { // not inherit the previous tab's filter, and persisted entity // visibility remains independent (not an isolation session). self.tabs[i].scene.reset_transient_visibility(); - crate::io::linetypes::populate_document(&mut self.tabs[i].scene.document); + crate::io::linetypes::repair_document(&mut self.tabs[i].scene.document); self.tabs[i].properties = PropertiesPanel::empty(); // Seed the current table / multileader style from the file's // header so the ✓ marks the right one (text/dim/mline come from diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 58d5ef0c7..164e8ad17 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -389,6 +389,20 @@ impl OpenCADStudio { // creating or closing a tab), the contextual ribbon tab follows that // document's selection. let before = self.tabs.get(self.active_tab).map(|tab| tab.id); + // Pickers offer the catalog linetypes the drawing lacks; the one + // picked is loaded before it is assigned. + if let Message::LayerLinetypeSet(name) + | Message::LayerStateEditorLayerLinetype(_, name) + | Message::RibbonLinetypeChanged(name) + | Message::PropLinetypeChanged(name) + | Message::AttrEditorLinetype(name) = &msg + { + if let Some(tab) = self.tabs.get_mut(self.active_tab) { + if crate::io::linetypes::ensure_catalog_linetype(&mut tab.scene.document, name) { + tab.scene.bump_geometry(); + } + } + } let task = self.update_message(msg); self.restore_dynamic_ucs_after_command(); self.sync_block_visibility_display(); diff --git a/src/io/linetypes.rs b/src/io/linetypes.rs index 88d5f0049..f029496e5 100644 --- a/src/io/linetypes.rs +++ b/src/io/linetypes.rs @@ -271,6 +271,40 @@ pub fn populate_document(doc: &mut CadDocument) { populate_document_from_source(doc, lin_source()); } +/// Repair the catalog linetypes `doc` already has (see +/// [`populate_document_from_source`]) without adding any: an opened drawing +/// keeps exactly its own linetypes, so a purged one stays purged. +pub fn repair_document(doc: &mut CadDocument) { + populate_matching(doc, lin_source(), |_| false); +} + +/// The catalog linetypes `doc` does not have, as (name, description): what a +/// picker offers beside the drawing's own. Picking one loads it with +/// [`ensure_catalog_linetype`]. +pub fn catalog_missing_from(doc: &CadDocument) -> Vec<(String, String)> { + static CATALOG: OnceLock> = OnceLock::new(); + CATALOG + .get_or_init(|| { + parse(lin_source()) + .into_iter() + .map(|lt| (lt.name, lt.description)) + .collect() + }) + .iter() + .filter(|(name, _)| !doc.line_types.contains(name)) + .cloned() + .collect() +} + +/// Load catalog linetype `name` into `doc` when the drawing lacks it. +/// Returns whether it was added. +pub fn ensure_catalog_linetype(doc: &mut CadDocument, name: &str) -> bool { + !doc.line_types.contains(name) + && populate_matching(doc, lin_source(), |candidate| { + candidate.eq_ignore_ascii_case(name) + }) > 0 +} + /// 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. @@ -280,6 +314,12 @@ pub fn populate_document(doc: &mut CadDocument) { /// 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 { + populate_matching(doc, source, |_| true) +} + +/// [`populate_document_from_source`], adding only the missing linetypes +/// `add` accepts by name; existing ones are repaired either way. +fn populate_matching(doc: &mut CadDocument, source: &str, add: impl Fn(&str) -> bool) -> usize { let complex = parse_complex(source); let mut added = 0; for mut lt in parse(source) { @@ -303,6 +343,9 @@ pub fn populate_document_from_source(doc: &mut CadDocument, source: &str) -> usi } continue; } + if !add(<.name) { + continue; + } if let Some(elements) = segments.and_then(|s| complex_elements(doc, s, <.elements)) { lt.elements = elements; } diff --git a/src/io/mod.rs b/src/io/mod.rs index 4d3deee2f..11392d93f 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -450,10 +450,11 @@ 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); + // Before the geometry is prepared below: repairing the + // drawing's stripped catalog linetypes after it would leave + // the first view drawn without their shapes. None is added: + // the drawing keeps exactly its own linetypes (#1826). + crate::io::linetypes::repair_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() {