use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use tracing::warn;
pub(super) const MAX_RECURSE_DEPTH: usize = 32;
pub(super) fn collect_asc_files_recursive(
dir: &Path,
recurse: bool,
depth: usize,
max_depth: usize,
visited: &mut HashSet<PathBuf>,
out: &mut Vec<PathBuf>,
gem_gcs_skipped: &mut usize,
) {
if depth > max_depth {
warn!(
"Import: not descending into '{}' -- exceeded max recursion depth ({max_depth})",
dir.display()
);
return;
}
let canon = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
if !visited.insert(canon) {
warn!(
"Import: skipping '{}' -- already visited (symlink loop?)",
dir.display()
);
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_file() && p.extension().is_some_and(|e| e.eq_ignore_ascii_case("asc")) {
out.push(p);
} else if p.is_file() && is_gem_or_gcs(&p) {
*gem_gcs_skipped += 1;
} else if recurse && p.is_dir() {
collect_asc_files_recursive(
&p,
recurse,
depth + 1,
max_depth,
visited,
out,
gem_gcs_skipped,
);
}
}
}
#[must_use]
fn is_gem_or_gcs(path: &Path) -> bool {
path.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("gem") || e.eq_ignore_ascii_case("gcs"))
}
pub(super) const GEM_GCS_EXPLANATION: &str = ".gem is GemCAD's own binary format -- this app reads only \
partial data from it (no facet geometry) elsewhere, never through Import; .gcs is read-only \
wherever it is read. Import a .asc export of the same design instead.";
pub(super) fn collect_import_candidates(
path: &Path,
recurse: bool,
) -> Result<(Vec<PathBuf>, usize), String> {
let mut candidates: Vec<PathBuf> = Vec::new();
let mut gem_gcs_skipped = 0usize;
if path.is_dir() {
match std::fs::read_dir(path) {
Ok(entries) => {
let mut visited: HashSet<PathBuf> = HashSet::new();
for entry in entries.flatten() {
let p = entry.path();
if p.is_file() && p.extension().is_some_and(|e| e.eq_ignore_ascii_case("asc")) {
candidates.push(p);
} else if p.is_file() && is_gem_or_gcs(&p) {
gem_gcs_skipped += 1;
} else if recurse && p.is_dir() {
collect_asc_files_recursive(
&p,
recurse,
1,
MAX_RECURSE_DEPTH,
&mut visited,
&mut candidates,
&mut gem_gcs_skipped,
);
}
}
}
Err(e) => return Err(format!("Could not read folder '{}': {e}", path.display())),
}
} else if path.is_file() {
if indicatrix_cut_core::native::asc_path_for_native(path).is_some() {
return Err(format!(
"'{}' is an Indicatrix native design file, not a .asc -- open it with \"Open \
Native\" in the Edit tab instead of Import.",
path.display()
));
}
if is_gem_or_gcs(path) {
return Err(format!(
"'{}' is not a .asc file. {GEM_GCS_EXPLANATION}",
path.display()
));
}
candidates.push(path.to_path_buf());
} else {
return Err(format!("'{}' is not a file or folder.", path.display()));
}
if candidates.is_empty() {
return Err(if gem_gcs_skipped > 0 {
format!(
"No .asc files found at '{}' ({gem_gcs_skipped} .gem/.gcs file(s) found but not \
imported -- {GEM_GCS_EXPLANATION})",
path.display()
)
} else {
format!("No .asc files found at '{}'.", path.display())
});
}
Ok((candidates, gem_gcs_skipped))
}
pub(super) fn find_native_sidecar(asc_path: &Path) -> Option<(String, Vec<u8>)> {
let mut current = asc_path.to_path_buf();
current.set_extension(indicatrix_cut_core::native::NATIVE_EXTENSION_SUFFIX);
let mut legacy = asc_path.to_path_buf();
legacy.set_extension(indicatrix_cut_core::native::LEGACY_NATIVE_EXTENSION_SUFFIX);
[current, legacy].into_iter().find_map(|candidate| {
let bytes = std::fs::read(&candidate).ok()?;
let name = candidate.file_name()?.to_string_lossy().into_owned();
Some((name, bytes))
})
}