forked from 1b-agent/opencadstudio-upstream
Use the codec audit and repair in place of OCS's corruption guard
Opening a drawing no longer drops entities with its own sanity checks or rewrites knotless splines; recovery runs the codec's repair and records its actions and unresolved issues in the drawing's notifications. Xref merges keep every entity. The dropped-entity counts, their command-line warnings, recovery report rows and the automation "purged" field go with the guard. AUDIT now reports the codec's audit (counts plus the first 50 issues) and asks whether to fix; Yes applies the codec repair as one undo step. Cargo: codec 325fa98, kernel dfca391, graph 5d4e713. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c9404510f
commit
04b2c07a38
43 files changed
+192
-763
No files matched your search
+6
-26
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user