Merge remote-tracking branch 'origin/main' into codex/import-formats

# Conflicts:
#	Cargo.lock
This commit is contained in:
ramox81 committed 2026-10-16 16:58:41 +03:00
commit d8959a4635
43 files changed
+189 -760

No files matched your search

+6 -26
View File
@@ -762,7 +762,7 @@ impl OpenCADStudio {
Err(e) => return err(format!("open: {e}")),
};
match crate::io::load_bytes_finalized(&path_buf, bytes) {
Ok((doc, dropped)) => {
Ok(doc) => {
let i = self.active_tab;
self.tabs[i].scene.clear();
self.tabs[i].scene.document = doc;
@@ -784,13 +784,7 @@ impl OpenCADStudio {
// The opened document settled (API v9): the post-op
// boundary announces `DocumentOpened` once for this load.
self.tabs[i].note_document_loaded();
let mut summary = self.entity_summary();
if dropped > 0 {
if let Some(obj) = summary.as_object_mut() {
obj.insert("purged".to_string(), json!(dropped));
}
}
summary
self.entity_summary()
}
Err(e) => err(format!("open: {e}")),
}
@@ -1903,8 +1897,7 @@ mod tests {
fingerprint: None,
});
let bytes = std::fs::read(&path).expect("read the fixture back");
let (document, _dropped) =
crate::io::load_bytes_finalized(&path, bytes).expect("reparse the fixture");
let document = crate::io::load_bytes_finalized(&path, bytes).expect("reparse the fixture");
let caches = crate::scene::build_derived_caches_with_progress(&document, &|_| {}, None);
let _ = plugin_dispatch_log();
let _ = app.update(crate::app::Message::FileOpened(
@@ -3839,7 +3832,7 @@ mod tests {
}
#[test]
fn open_finalizes_and_purges_like_the_ui_open_path() {
fn open_finalizes_like_the_ui_open_path() {
let mut app = OpenCADStudio::new_for_test();
let stale = codec::Handle::from(9999);
app.tabs[app.active_tab].scene.solid_models.insert(
@@ -3857,26 +3850,13 @@ mod tests {
good.center = codec::types::Vector3::new(5.0, 5.0, 0.0);
good.radius = 2.0;
doc.add_entity(codec::EntityType::Circle(good)).unwrap();
let mut corrupt = codec::entities::Circle::new();
corrupt.center = codec::types::Vector3::new(1.0, 1.0, 0.0);
// An absurd radius is rejected; a zero radius is valid.
corrupt.radius = 1.0e11;
doc.add_entity(codec::EntityType::Circle(corrupt)).unwrap();
let bytes = crate::io::save_to_bytes(&doc, "dxf", doc.version)
.expect("save a document containing a corrupt entity");
let bytes = crate::io::save_to_bytes(&doc, "dxf", doc.version).expect("save the fixture");
std::fs::write(&path, bytes).unwrap();
let p = path.to_string_lossy().replace('\\', "\\\\");
let result = app.automation_op(&format!(r#"{{"op":"open","path":"{p}"}}"#));
assert_eq!(result["ok"], true, "{}", result["error"]);
assert_eq!(
result["total"], 1,
"the corrupt circle must not survive the open"
);
assert_eq!(
result["purged"], 1,
"the purge count must be reported, matching the UI open path's diagnostics"
);
assert_eq!(result["total"], 1);
let i = app.active_tab;
assert!(!app.tabs[i].scene.solid_models.contains_key(&stale));
+2 -2
View File
@@ -1128,7 +1128,7 @@ impl OpenCADStudio {
.filter(|br| reload_keys.contains(&br.handle.value()))
.map(|br| br.handle)
.collect();
let (infos, _dropped) = crate::io::xref::resolve_xrefs_for_keys(
let infos = crate::io::xref::resolve_xrefs_for_keys(
&mut self.tabs[i].scene.document,
base_dir,
&handles,
@@ -1674,7 +1674,7 @@ impl OpenCADStudio {
self.tabs[i].xref_unloaded.remove(key);
self.tabs[i].xref_stat_cache.remove(key);
}
let (infos, _dropped) = crate::io::xref::resolve_xrefs(
let infos = crate::io::xref::resolve_xrefs(
&mut self.tabs[i].scene.document,
&base_dir,
);
+49 -40
View File
@@ -3635,53 +3635,62 @@ impl OpenCADStudio {
.push_error(crate::tf!("COMPARE: {error}").as_ref()),
}
}
// AUDIT runs the codec's audit; `AUDIT Yes` also applies its
// repair, undoable as one step.
"AUDIT" => {
use std::collections::BTreeSet;
let mut undefined_layers: BTreeSet<String> = BTreeSet::new();
let mut undefined_blocks: BTreeSet<String> = BTreeSet::new();
let mut total = 0usize;
{
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"AUDIT",
"AUDIT Fix any errors detected? [Yes / No] <No>:",
vec![("Yes", "YES", None), ("No", "NO", None)],
)
.with_default("NO");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("AUDIT ") => {
let fix = cmd[6..].trim().to_uppercase().starts_with('Y');
let report = self.tabs[i].scene.document.audit();
let total = {
let doc = &self.tabs[i].scene.document;
for e in doc.entities() {
total += 1;
let layer = &e.common().layer;
if !layer.is_empty() && doc.layers.get(layer).is_none() {
undefined_layers.insert(layer.clone());
}
if let codec::EntityType::Insert(ins) = e {
if doc.block_records.get(&ins.block_name).is_none() {
undefined_blocks.insert(ins.block_name.clone());
}
}
}
}
doc.entities().count() + doc.objects.len()
};
self.command_line
.push_output(crate::tf!("AUDIT: scanned {total} object(s).").as_ref());
if undefined_layers.is_empty() && undefined_blocks.is_empty() {
if report.is_clean() {
self.command_line
.push_output(crate::t!("AUDIT: no issues found.").as_ref());
} else {
if !undefined_layers.is_empty() {
self.command_line.push_error(
crate::tf!(
"AUDIT: reference(s) to undefined layer(s): {}",
undefined_layers.into_iter().collect::<Vec<_>>().join(", ")
)
.as_ref(),
);
}
if !undefined_blocks.is_empty() {
self.command_line.push_error(
crate::tf!(
"AUDIT: reference(s) to undefined block(s): {}",
undefined_blocks.into_iter().collect::<Vec<_>>().join(", ")
)
.as_ref(),
);
}
self.command_line.push_info(
crate::t!("AUDIT: report only — no automatic repair performed.").as_ref(),
return Some(Task::none());
}
self.command_line.push_error(
crate::tf!(
"AUDIT: {} error(s), {} warning(s) found.",
report.error_count(),
report.warning_count()
)
.as_ref(),
);
// ponytail: first 50 issues only; the counts above cover the rest.
for issue in report.issues.iter().take(50) {
self.command_line
.push_info(&format!(" {:?}: {}", issue.code, issue.message));
}
if fix {
self.push_undo_snapshot(i, "AUDIT");
let repair = self.tabs[i].scene.document.repair();
self.command_line.push_output(
crate::tf!(
"AUDIT: {} repair(s) applied, {} issue(s) left unresolved.",
repair.actions.len(),
repair.unresolved.len()
)
.as_ref(),
);
if !repair.actions.is_empty() {
self.tabs[i].dirty = true;
self.tabs[i].scene.rebuild_derived_caches();
self.refresh_layer_panel();
}
}
}
+1 -2
View File
@@ -1009,10 +1009,9 @@ impl OpenCADStudio {
"new" => {
if let Some(template) = req["template"].as_str() {
match self.apply_template(template) {
Ok(purged) => {
Ok(()) => {
self.set_control_result(json!({
"template": template,
"purged": purged,
"total": self.tabs[i].scene.document.entities().count(),
}));
Task::none()
+4 -4
View File
@@ -434,7 +434,7 @@ impl OpenCADStudio {
/// `new` with `"template"` — the web build reads no files from a path, so
/// a template is refused there rather than silently ignored.
#[cfg(target_arch = "wasm32")]
pub(in crate::app) fn apply_template(&mut self, _template: &str) -> Result<usize, Value> {
pub(in crate::app) fn apply_template(&mut self, _template: &str) -> Result<(), Value> {
Err(failure(
"invalid_request",
"starting from a template needs the desktop build",
@@ -445,7 +445,7 @@ impl OpenCADStudio {
/// file. Tables and styles come from the template; the document keeps no
/// source path.
#[cfg(not(target_arch = "wasm32"))]
pub(in crate::app) fn apply_template(&mut self, template: &str) -> Result<usize, Value> {
pub(in crate::app) fn apply_template(&mut self, template: &str) -> Result<(), Value> {
let path = std::path::PathBuf::from(template);
let bytes = self
.read_drawing(&path)
@@ -461,7 +461,7 @@ impl OpenCADStudio {
} else {
path.clone()
};
let (mut document, purged) = crate::io::load_bytes_finalized(&load_path, bytes)
let mut document = crate::io::load_bytes_finalized(&load_path, bytes)
.map_err(|e| failure("template_failed", e))?;
document.source_path = Some(path.to_string_lossy().into_owned());
let i = self.active_tab;
@@ -482,7 +482,7 @@ impl OpenCADStudio {
// A whole document just became resident in the tab (API v9): the
// boundary announces `DocumentOpened` once.
self.tabs[i].note_document_loaded();
Ok(purged)
Ok(())
}
/// `file_identity` — stable per-document GUID (the drawing's SPMDOCUNIQUE value,
+2 -2
View File
@@ -886,7 +886,7 @@ impl OpenCADStudio {
.filter(|br| dwg_picked.iter().any(|(key, _)| *key == br.handle.value()))
.map(|br| br.handle)
.collect();
let (infos, _dropped) = crate::io::xref::resolve_xrefs_for_keys(
let infos = crate::io::xref::resolve_xrefs_for_keys(
&mut self.tabs[i].scene.document,
&base_dir,
&handles,
@@ -1093,7 +1093,7 @@ impl OpenCADStudio {
self.tabs[i].xref_unloaded.remove(key);
self.tabs[i].xref_stat_cache.remove(key);
}
let (infos, _dropped) =
let infos =
crate::io::xref::resolve_xrefs(&mut self.tabs[i].scene.document, &base_dir);
let fresh = crate::io::xref::collect_entries_with_prev(
&self.tabs[i].scene.document,
+2 -25
View File
@@ -2051,34 +2051,13 @@ impl OpenCADStudio {
.xrefs
.iter()
.any(|item| item.status == crate::io::xref::XrefStatus::Failed);
let document_repaired = parser_errors_recovered
|| reference_recovered
|| caches.corrupt_dropped > 0
|| caches.xref_dropped > 0;
let document_repaired = parser_errors_recovered || reference_recovered;
let recovery_needed = document_repaired || reference_failed;
let total_ms = open_started
.map(|started| started.elapsed().as_millis() as u32)
.unwrap_or(0);
self.command_line
.push_output(crate::tf!("Opened \"{name}\" — {entity_count} entities").as_ref());
if caches.corrupt_dropped > 0 {
self.command_line.push_error(
crate::tf!(
"Warning: {} corrupt entities dropped (parser junk — bad normals / counts)",
caches.corrupt_dropped
)
.as_ref(),
);
}
if caches.xref_dropped > 0 {
self.command_line.push_error(
crate::tf!(
"Warning: {} corrupt xref entities dropped",
caches.xref_dropped
)
.as_ref(),
);
}
for info in &caches.xrefs {
match info.status {
crate::io::xref::XrefStatus::Loaded => {
@@ -2178,9 +2157,7 @@ impl OpenCADStudio {
size_bytes,
caches.source_sha256.clone(),
caches.read_stats.clone(),
entity_count.saturating_add(caches.corrupt_dropped),
caches.corrupt_dropped,
caches.xref_dropped,
entity_count,
&caches.xrefs,
&doc.notifications,
document_repaired,