From 893dcee6a717d9cc110aafec675a58a367f3c4af Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 13 Oct 2026 10:04:20 +0300 Subject: [PATCH] feat(resources): load the user's own hatch patterns and linetypes *.pat files in /patterns and *.lin files in /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 --- src/io/linetypes.rs | 12 ++++++++++-- src/io/patterns.rs | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/io/linetypes.rs b/src/io/linetypes.rs index 527f200ff..92dc46749 100644 --- a/src/io/linetypes.rs +++ b/src/io/linetypes.rs @@ -69,7 +69,7 @@ static COMPLEX_CATALOG: OnceLock> = OnceLock::new(); /// 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_or_init(|| parse_complex(lin_source())) .get(&name.to_ascii_uppercase()) } @@ -232,6 +232,14 @@ pub fn resolve_complex_lt(document: &CadDocument, name: &str) -> Option &'static str { + static SOURCE: OnceLock = 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. @@ -257,7 +265,7 @@ pub fn extract_pattern(desc: &str) -> String { /// Add all standard OpenCADStudio linetypes to `doc`, skipping existing ones. pub fn populate_document(doc: &mut CadDocument) { - populate_document_from_source(doc, LIN_SOURCE); + populate_document_from_source(doc, lin_source()); } /// Add the linetypes of a caller-supplied `.lin` source, embedded shapes and diff --git a/src/io/patterns.rs b/src/io/patterns.rs index 47f6f1c38..413b840e9 100644 --- a/src/io/patterns.rs +++ b/src/io/patterns.rs @@ -39,9 +39,41 @@ pub struct PatternDef { static CATALOG: OnceLock> = OnceLock::new(); -/// All parsed patterns, in file order. +/// All parsed patterns, in file order: the bundled catalog, then the user's +/// own `.pat` files. pub fn catalog() -> &'static [PatternDef] { - CATALOG.get_or_init(|| parse(PAT_SRC)) + CATALOG.get_or_init(|| parse(&format!("{PAT_SRC}\n{}", user_definitions("patterns", "pat")))) +} + +/// The text of every `*.` file in `/`, where users drop +/// their own hatch patterns and linetypes. Bundled names come first, so a +/// user file adds definitions rather than replacing the built-in ones. +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn user_definitions(folder: &str, ext: &str) -> String { + let Some(dir) = crate::config::config_dir().map(|dir| dir.join(folder)) else { + return String::new(); + }; + let Ok(entries) = std::fs::read_dir(dir) else { + return String::new(); + }; + let mut paths: Vec<_> = entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().is_some_and(|e| e.eq_ignore_ascii_case(ext))) + .collect(); + paths.sort(); + paths + .iter() + .filter_map(|path| std::fs::read(path).ok()) + .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()) + .collect::>() + .join("\n") +} + +/// The web build has no user folder. +#[cfg(target_arch = "wasm32")] +pub(crate) fn user_definitions(_folder: &str, _ext: &str) -> String { + String::new() } // ── Geometry helper ───────────────────────────────────────────────────────