Merge upstream main; follow the boxed MText codec type and the updated palette branch

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-16 16:56:46 +03:00
commit 8f20f0aef6
119 files changed
+560 -1020

No files matched your search

+13 -33
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(
@@ -2129,7 +2122,7 @@ mod tests {
dimension.definition_point = Vector3::new(0.0, 5.0, 0.0);
dimension.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
document
.add_entity(EntityType::Dimension(Dimension::Linear(dimension)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(dimension))))
.unwrap();
crate::modules::draw::modify::explode::bake_dimension_blocks(&mut document);
@@ -2152,7 +2145,7 @@ mod tests {
dimension.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
app.tabs[app.active_tab]
.scene
.add_entity(EntityType::Dimension(Dimension::Linear(dimension)));
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(dimension))));
let path = std::env::temp_dir().join(format!(
"ocs_verified_dimension_{}_{}.dwg",
std::process::id(),
@@ -2190,7 +2183,7 @@ mod tests {
let i = app.active_tab;
app.tabs[i]
.scene
.add_entity(EntityType::Dimension(Dimension::Linear(dimension)));
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(dimension))));
let path = std::env::temp_dir().join(format!(
"ocs_verified_dimension_follow_up_{}_{}.dwg",
std::process::id(),
@@ -2240,7 +2233,7 @@ mod tests {
app.tabs[i]
.scene
.document
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(d))))
.unwrap();
let path = std::env::temp_dir().join(format!(
"ocs_verified_dim_{}_{}.dwg",
@@ -2330,7 +2323,7 @@ mod tests {
viewport.width = 100.0;
viewport.height = 50.0;
viewport.turn_on();
scene.add_entity(codec::EntityType::Viewport(viewport));
scene.add_entity(codec::EntityType::Viewport(Box::new(viewport)));
for entity in scene.document.entities_mut() {
if let codec::EntityType::Viewport(viewport) = entity {
viewport.status.grid_on = true;
@@ -2774,7 +2767,7 @@ mod tests {
codec::MText::with_value("\\A1;10.5000", codec::types::Vector3::new(10.0, 20.0, 0.0));
app.tabs[i]
.scene
.add_entity(codec::EntityType::MText(mtext));
.add_entity(codec::EntityType::MText(Box::new(mtext)));
let q = app.automation_op(
r#"{"op":"query","type":"MTEXT","detail":"full","fields":["value","text","height","bounds"]}"#,
@@ -3642,7 +3635,7 @@ mod tests {
let mut dst_mtext = MText::new();
dst_mtext.value = "DSTM".into();
dst_mtext.height = 2.0;
let dst_mtext_h = app.tabs[i].scene.add_entity(EntityType::MText(dst_mtext));
let dst_mtext_h = app.tabs[i].scene.add_entity(EntityType::MText(Box::new(dst_mtext)));
// Drive the interactive command exactly as the viewport does:
// phase 1 source pick, phase 2 destination selection.
@@ -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));
+1 -1
View File
@@ -1405,7 +1405,7 @@ mod paste_entities_kernel_tests {
fn remaps_leader_annotation_link_to_pasted_handles() {
let mut seed = codec::CadDocument::new();
let note = seed
.add_entity(codec::EntityType::MText(codec::entities::MText::new()))
.add_entity(codec::EntityType::MText(Box::new(codec::entities::MText::new())))
.unwrap();
let mut leader = codec::entities::Leader::default();
leader.annotation_handle = note;
+14 -8
View File
@@ -36,10 +36,12 @@ impl OpenCADStudio {
) {
let i = self.active_tab;
let valid = matches!(
self.tabs[i].scene.document.get_entity(dimension),
Some(codec::EntityType::Dimension(
codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_)
))
self.tabs[i]
.scene
.document
.get_entity(dimension)
.and_then(crate::entities::dimension::as_dimension),
Some(codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_))
);
if !valid {
self.command_line.push_error(
@@ -415,8 +417,12 @@ fn apply_dimspace(
requested_spacing: Option<f64>,
) -> bool {
use codec::entities::Dimension;
let (axis, normal, definition, auto_spacing) = match scene.document.get_entity(base_h) {
Some(codec::EntityType::Dimension(dimension @ Dimension::Linear(d))) => {
let base = scene
.document
.get_entity(base_h)
.and_then(crate::entities::dimension::as_dimension);
let (axis, normal, definition, auto_spacing) = match base {
Some(dimension @ Dimension::Linear(d)) => {
let spacing = dimension_auto_spacing(
&scene.document,
dimension,
@@ -433,7 +439,7 @@ fn apply_dimspace(
spacing,
)
}
Some(codec::EntityType::Dimension(dimension @ Dimension::Aligned(d))) => {
Some(dimension @ Dimension::Aligned(d)) => {
let spacing = dimension_auto_spacing(
&scene.document,
dimension,
@@ -471,7 +477,7 @@ fn apply_dimspace(
kernel::space::move_to_dimension_spacing([p.x, p.y, p.z], frame, target);
*p = codec::types::Vector3::new(point[0], point[1], point[2]);
};
match dim {
match &mut **dim {
Dimension::Linear(d) => {
slide(&mut d.definition_point);
d.base.definition_point = d.definition_point;
+6 -10
View File
@@ -39,10 +39,8 @@ impl OpenCADStudio {
let delta_safe = self.delta_add_safe(i, &entity);
let pending = self.begin_undo(i, label, 1, delta_safe);
let is_associative_dimension = matches!(
entity,
codec::EntityType::Dimension(
codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_)
)
crate::entities::dimension::as_dimension(&entity),
Some(codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_))
);
let association_enabled = self.tabs[i].scene.document.header.dimension_associativity == 2;
let committed = if preserve_commit_layer {
@@ -257,10 +255,8 @@ impl OpenCADStudio {
};
let pending = self.begin_undo(i, label, 1, delta_safe);
let is_associative_dimension = matches!(
entity,
codec::EntityType::Dimension(
codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_)
)
crate::entities::dimension::as_dimension(&entity),
Some(codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_))
);
let association_enabled = self.tabs[i].scene.document.header.dimension_associativity == 2;
let committed = if preserve_commit_layer {
@@ -324,8 +320,8 @@ impl OpenCADStudio {
let label = self.history_label_from_active_cmd(i, "DIMENSION");
let association_mode = self.tabs[i].scene.document.header.dimension_associativity;
let single_source_dimension = matches!(
&entity,
codec::EntityType::Dimension(codec::entities::Dimension::Ordinate(_))
crate::entities::dimension::as_dimension(&entity),
Some(codec::entities::Dimension::Ordinate(_))
);
// A dimension placed on the sheet but measuring model geometry
// through a viewport carries the compensation as a negative
+1 -1
View File
@@ -160,7 +160,7 @@ impl OpenCADStudio {
Operation::Rotate(degrees) => {
dimension.base_mut().text_rotation = degrees.to_radians();
}
Operation::Oblique(degrees) => match dimension {
Operation::Oblique(degrees) => match &mut **dimension {
codec::entities::Dimension::Linear(value) => {
value.ext_line_rotation = degrees.to_radians();
}
+1 -1
View File
@@ -463,7 +463,7 @@ impl OpenCADStudio {
false
}
};
match dim {
match &mut **dim {
Dimension::Linear(d) => {
stretched |= mv(&mut d.first_point);
stretched |= mv(&mut d.second_point);
+3 -2
View File
@@ -21,7 +21,7 @@ impl OpenCADStudio {
});
let label = self.history_label_from_active_cmd(i, "MVIEW");
let pending = self.begin_undo(i, label, 1, true);
let handle = self.commit_entity_handle(codec::EntityType::Viewport(viewport));
let handle = self.commit_entity_handle(codec::EntityType::Viewport(Box::new(viewport)));
if let (Some(handle), Some(saved)) = (handle, saved_view) {
if let Some(codec::EntityType::Viewport(viewport)) =
self.tabs[i].scene.document.get_entity_mut(handle)
@@ -244,7 +244,8 @@ impl OpenCADStudio {
viewport.id = 2;
viewport.clip_boundary_handle = clip_handle;
viewport.status.non_rectangular_clipping = !clip_handle.is_null();
let viewport_handle = self.commit_entity_handle(codec::EntityType::Viewport(viewport));
let viewport_handle =
self.commit_entity_handle(codec::EntityType::Viewport(Box::new(viewport)));
if let Some(viewport_handle) = viewport_handle {
if !created_boundary {
let before = self.tabs[i]
+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,
);
+1 -1
View File
@@ -294,7 +294,7 @@ impl OpenCADStudio {
} else {
line.layer
};
scene.add_entity(codec::EntityType::MText(mtext));
scene.add_entity(codec::EntityType::MText(Box::new(mtext)));
erase.extend(line.handles);
}
erase.sort();
+1 -1
View File
@@ -2389,7 +2389,7 @@ impl OpenCADStudio {
Vector3::new(mv.center.x - mv.target.x, mv.center.y - mv.target.y, 0.0);
vp.view_height = mv.height;
vp.custom_scale = scale;
scene.add_entity(codec::EntityType::Viewport(vp));
scene.add_entity(codec::EntityType::Viewport(Box::new(vp)));
// A paper-space named view of the same name frames it.
let mut named = codec::tables::View::new(title.as_str());
named.handle = scene.document.allocate_handle();
+49 -40
View File
@@ -3671,53 +3671,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();
}
}
}
+2 -2
View File
@@ -126,10 +126,10 @@ mod tests {
let i = app.active_tab;
let first = app.tabs[i]
.scene
.add_entity(codec::EntityType::Viewport(codec::entities::Viewport::new()));
.add_entity(codec::EntityType::Viewport(Box::new(codec::entities::Viewport::new())));
let second = app.tabs[i]
.scene
.add_entity(codec::EntityType::Viewport(codec::entities::Viewport::new()));
.add_entity(codec::EntityType::Viewport(Box::new(codec::entities::Viewport::new())));
app.tabs[i].scene.active_viewport = Some(first);
let _ = app.run_command_line("UCSFOLLOW 1");
assert!(app.tabs[i].scene.ucs_follow());
+5 -2
View File
@@ -1000,7 +1000,10 @@ impl OpenCADStudio {
match self.tabs[i]
.scene
.document
.add_entity_to_layout(codec::EntityType::Viewport(vp), &layout_name)
.add_entity_to_layout(
codec::EntityType::Viewport(Box::new(vp)),
&layout_name,
)
{
Ok(handle) => {
self.tabs[i].scene.auto_fit_viewport(handle);
@@ -2207,7 +2210,7 @@ mod tests {
locked_hatch.common.layer = "LOCKED_LAYER".into();
let h_locked_hatch = app.tabs[i]
.scene
.add_entity_clone(EntityType::Hatch(locked_hatch));
.add_entity_clone(EntityType::Hatch(Box::new(locked_hatch)));
let h_line = app.tabs[i]
.scene
+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
@@ -352,7 +352,7 @@ impl OpenCADStudio {
fn is_angular_dimension(entity: &EntityType) -> bool {
matches!(
entity,
EntityType::Dimension(Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_))
crate::entities::dimension::as_dimension(entity),
Some(Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_))
)
}
+6 -4
View File
@@ -841,7 +841,7 @@ impl super::OpenCADStudio {
// catastrophic cancellation. Insertion is a pure translation, so zeroing
// it keeps the layout's shape, wrapping and alignment identical.
mt.insertion_point = Vector3::new(0.0, 0.0, 0.0);
let entity = EntityType::MText(mt.clone());
let entity = EntityType::MText(Box::new(mt.clone()));
let anno = 1.0;
let bg = self.tabs[i].scene.bg_color;
let wires: Vec<WireModel> = tessellate::tessellate(
@@ -1753,7 +1753,8 @@ impl super::OpenCADStudio {
));
mt.insertion_point = codec::types::Vector3::new(position.x, position.y, position.z);
self.push_undo_snapshot(i, "MTEXT");
let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
let handle =
self.commit_entity_handle(plane.place_entity(EntityType::MText(Box::new(mt))));
if annotative {
let scale = self.tabs[i].scene.creation_annotation_scale_handle();
if let (Some(handle), Some(scale)) = (handle, scale) {
@@ -1869,7 +1870,8 @@ impl super::OpenCADStudio {
));
mt.insertion_point = codec::types::Vector3::new(position.x, position.y, position.z);
self.push_undo_snapshot(i, "MTEXT");
let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
let handle =
self.commit_entity_handle(plane.place_entity(EntityType::MText(Box::new(mt))));
self.tabs[i].dirty = true;
if annotative {
let scale = self.tabs[i].scene.creation_annotation_scale_handle();
@@ -1885,7 +1887,7 @@ impl super::OpenCADStudio {
if let (Some(h), Some(ed)) = (handle, self.mtext_editor.as_mut()) {
ed.editing = Some(h);
if let Some(EntityType::MText(mtext)) = self.tabs[i].scene.document.get_entity(h) {
ed.original = Some(mtext.clone());
ed.original = Some((**mtext).clone());
}
}
}
+1 -1
View File
@@ -497,7 +497,7 @@ mod tests {
vp.width = 200.;
vp.height = 100.;
vp.view_height = 50.;
let handle = scene.add_entity(codec::EntityType::Viewport(vp));
let handle = scene.add_entity(codec::EntityType::Viewport(Box::new(vp)));
scene.active_viewport = Some(handle);
let mut camera = scene.navigation_camera();
camera.target = DVec3::new(120., 30., 8.);
+4 -4
View File
@@ -943,7 +943,7 @@ impl<'a> HostSession<'a> {
frame.common.layer = layer;
}
self.push_undo("Embed picture");
let handle = self.add_entity(EntityType::Ole2Frame(frame));
let handle = self.add_entity(EntityType::Ole2Frame(Box::new(frame)));
if handle.is_null() {
return Err("the picture frame could not be added".into());
}
@@ -6708,7 +6708,7 @@ mod tests {
let EntityType::Dimension(dimension) = &mut after else {
unreachable!()
};
if let codec::entities::Dimension::Linear(value) = dimension {
if let codec::entities::Dimension::Linear(value) = &mut **dimension {
value.second_point.x = 30.0;
}
ocs_plugin_api::entity_coverage::validate_entity_mutation(&before, &after).unwrap();
@@ -7414,7 +7414,7 @@ mod tests {
.unwrap();
let mut viewport = codec::entities::Viewport::new();
viewport.common.owner_handle = owner;
let viewport_handle = host.add_entity(EntityType::Viewport(viewport));
let viewport_handle = host.add_entity(EntityType::Viewport(Box::new(viewport)));
let scale_handle = host.document_mut().allocate_handle();
let mut scale = codec::objects::Scale::new("1:1", 1.0, 1.0);
scale.handle = scale_handle;
@@ -10861,7 +10861,7 @@ mod tests {
bare.height = 30.0;
let bare = host
.document_mut()
.add_entity(EntityType::Viewport(bare))
.add_entity(EntityType::Viewport(Box::new(bare)))
.unwrap();
assert_eq!(
host.document().get_entity(bare).map(|e| match e {
+37 -33
View File
@@ -1489,12 +1489,14 @@ impl OpenCADStudio {
if let Some((anno_field, insert_after)) = anno {
let is_anno = crate::scene::annotative::is_annotative(doc, entity)
|| match entity {
codec::EntityType::Dimension(codec::entities::Dimension::Arc(
dimension,
)) => crate::scene::annotative::dim_style_is_annotative(
doc,
&dimension.base.style_name,
),
codec::EntityType::Dimension(dimension)
if matches!(**dimension, codec::entities::Dimension::Arc(_)) =>
{
crate::scene::annotative::dim_style_is_annotative(
doc,
&dimension.base().style_name,
)
}
codec::EntityType::Tolerance(_) => {
crate::scene::annotative::annotation_style_is_annotative(doc, entity)
}
@@ -1530,7 +1532,9 @@ impl OpenCADStudio {
"annotative",
yes_no_choice(t.is_annotative || is_anno),
),
codec::EntityType::Dimension(codec::entities::Dimension::Arc(_)) => {
codec::EntityType::Dimension(dimension)
if matches!(**dimension, codec::entities::Dimension::Arc(_)) =>
{
set_row(
&mut sections,
"annotative",
@@ -1683,10 +1687,14 @@ impl OpenCADStudio {
format!("{} ({})", t!("Surface"), t!("Extrusion"))
}
// The reference names an angular dimension by its kind.
codec::EntityType::Dimension(codec::entities::Dimension::Angular2Ln(_)) => {
codec::EntityType::Dimension(dimension)
if matches!(**dimension, codec::entities::Dimension::Angular2Ln(_)) =>
{
t!("Angular Dimension").into_owned()
}
codec::EntityType::Dimension(codec::entities::Dimension::Angular3Pt(_)) => {
codec::EntityType::Dimension(dimension)
if matches!(**dimension, codec::entities::Dimension::Angular3Pt(_)) =>
{
t!("3 Point Angular Dimension").into_owned()
}
codec::EntityType::Underlay(underlay) => match underlay.underlay_type {
@@ -1719,22 +1727,14 @@ impl OpenCADStudio {
title
} else {
use codec::entities::Dimension;
match entity {
codec::EntityType::Dimension(Dimension::Aligned(_)) => {
t!("Aligned Dimensional Constraint")
}
codec::EntityType::Dimension(Dimension::Angular2Ln(_)) => {
t!("Angular Dimension (Dynamic)")
}
codec::EntityType::Dimension(Dimension::Angular3Pt(_)) => {
match crate::entities::dimension::as_dimension(entity) {
Some(Dimension::Aligned(_)) => t!("Aligned Dimensional Constraint"),
Some(Dimension::Angular2Ln(_)) => t!("Angular Dimension (Dynamic)"),
Some(Dimension::Angular3Pt(_)) => {
t!("3 Point Angular Dimension (Dynamic)")
}
codec::EntityType::Dimension(Dimension::Radius(_)) => {
t!("Radius Dimensional Constraint")
}
codec::EntityType::Dimension(Dimension::Diameter(_)) => {
t!("Diameter Dimensional Constraint")
}
Some(Dimension::Radius(_)) => t!("Radius Dimensional Constraint"),
Some(Dimension::Diameter(_)) => t!("Diameter Dimensional Constraint"),
_ => t!("Linear Dimensional Constraint"),
}
.into_owned()
@@ -2743,7 +2743,7 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
// scale are available.
if let codec::EntityType::Dimension(dim) = contextual.as_ref() {
if matches!(
dim,
&**dim,
codec::entities::Dimension::Linear(_)
| codec::entities::Dimension::Aligned(_)
) && !dim.base().text_user_positioned
@@ -2780,13 +2780,17 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
}
}
}
let dynamic_angle = match contextual.as_ref() {
codec::EntityType::Dimension(
dim @ (codec::entities::Dimension::Angular2Ln(_)
| codec::entities::Dimension::Angular3Pt(_)),
) if self.tabs[i].scene.is_dynamic_dimension(handle) => Some(dim),
_ => None,
};
let contextual_dimension =
crate::entities::dimension::as_dimension(contextual.as_ref());
let dynamic_angle = contextual_dimension
.filter(|dim| {
matches!(
dim,
codec::entities::Dimension::Angular2Ln(_)
| codec::entities::Dimension::Angular3Pt(_)
)
})
.filter(|_| self.tabs[i].scene.is_dynamic_dimension(handle));
if let Some(dim) = dynamic_angle {
// A dynamic angle shows a triangle at each end of its arc
// pointing away from it, and squares at the arc point and
@@ -3060,8 +3064,8 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
) -> Option<Handle> {
let i = self.active_tab;
let tracks_dimension_chain = matches!(
&entity,
codec::EntityType::Dimension(
crate::entities::dimension::as_dimension(&entity),
Some(
codec::entities::Dimension::Linear(_)
| codec::entities::Dimension::Aligned(_)
| codec::entities::Dimension::Angular2Ln(_)
+2 -2
View File
@@ -224,7 +224,7 @@ fn fixture_dimension_reference(app: &mut OpenCADStudio) -> String {
dimension.base.style_name = "BO_ARROWSTYLE".to_string();
app.tabs[i]
.scene
.add_entity(EntityType::Dimension(Dimension::Aligned(dimension)));
.add_entity(EntityType::Dimension(Box::new(Dimension::Aligned(dimension))));
name.to_string()
}
@@ -244,7 +244,7 @@ fn fixture_dimension_name_reference(app: &mut OpenCADStudio) -> String {
dimension.base.style_name = "BO_ARROWNAMESTYLE".to_string();
app.tabs[i]
.scene
.add_entity(EntityType::Dimension(Dimension::Aligned(dimension)));
.add_entity(EntityType::Dimension(Box::new(Dimension::Aligned(dimension))));
name.to_string()
}
+13 -6
View File
@@ -2913,9 +2913,12 @@ impl OpenCADStudio {
&value,
);
if applied && field == "dim_text_inside" {
if let Some(codec::EntityType::Dimension(
codec::entities::Dimension::LargeRadial(dimension),
)) = app.tabs[i].scene.document.get_entity_mut(handle)
let radial = app.tabs[i]
.scene
.document
.get_entity_mut(handle)
.and_then(crate::entities::dimension::as_dimension_mut);
if let Some(codec::entities::Dimension::LargeRadial(dimension)) = radial
{
dimension.base.text_user_positioned = false;
}
@@ -4182,9 +4185,13 @@ impl OpenCADStudio {
&val,
);
if applied && field == "dim_text_inside" {
if let Some(codec::EntityType::Dimension(
codec::entities::Dimension::LargeRadial(dimension),
)) = self.tabs[i].scene.document.get_entity_mut(handle)
if let Some(codec::entities::Dimension::LargeRadial(
dimension,
)) = self.tabs[i]
.scene
.document
.get_entity_mut(handle)
.and_then(crate::entities::dimension::as_dimension_mut)
{
dimension.base.text_user_positioned = false;
}
+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
@@ -2053,34 +2053,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 => {
@@ -2180,9 +2159,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,
+1 -1
View File
@@ -1084,7 +1084,7 @@ mod tests {
.document
.entities()
.filter_map(|entity| match entity {
EntityType::Hatch(hatch) => Some(hatch),
EntityType::Hatch(hatch) => Some(&**hatch),
_ => None,
})
.collect()
+5 -5
View File
@@ -28,7 +28,7 @@ fn fixture() -> (OpenCADStudio, Handle, ViewportFrame) {
viewport.custom_scale = 0.1;
viewport.view_direction = Vector3::UNIT_Z;
viewport.turn_on();
let viewport = scene.add_entity(EntityType::Viewport(viewport));
let viewport = scene.add_entity(EntityType::Viewport(Box::new(viewport)));
let frame = scene.viewport_frame(viewport).unwrap();
scene.document.header.dimension_associativity = 2;
let i = app.active_tab;
@@ -323,7 +323,7 @@ fn viewport_dimension_linear_aligned_and_radial_object_picks() {
assert_eq!(
preview_text(
&app,
&EntityType::Dimension(dimension(&app.tabs[i].scene.document).clone())
&EntityType::Dimension(Box::new(dimension(&app.tabs[i].scene.document).clone()))
),
value,
"{name}"
@@ -361,7 +361,7 @@ fn viewport_dimension_angular_preserves_angle_without_length_factor() {
let _ = app.feed_command(StepInput::Point(place));
let doc = &app.tabs[app.active_tab].scene.document;
assert_eq!(
preview_text(&app, &EntityType::Dimension(dimension(doc).clone())),
preview_text(&app, &EntityType::Dimension(Box::new(dimension(doc).clone()))),
value
);
assert!((dimension(doc).measurement() - 90.0).abs() < 1e-5);
@@ -1461,9 +1461,9 @@ fn viewport_dimension_invalid_paths_leave_no_partial_association() {
Vector3::new(55.0, 60.0, 0.0),
)));
for (source, path) in [(model, vec![Handle::new(u64::MAX)]), (leaf, vec![root])] {
let dim = scene.add_entity(EntityType::Dimension(Dimension::Aligned(
let dim = scene.add_entity(EntityType::Dimension(Box::new(Dimension::Aligned(
DimensionAligned::new(Vector3::new(50.0, 50.0, 0.0), Vector3::new(55.0, 50.0, 0.0)),
)));
))));
let invalid = AcceptedSnap {
paper_point: DVec3::new(50.0, 50.0, 0.0),
model_point: DVec3::ZERO,
+24 -5
View File
@@ -3248,7 +3248,7 @@ pub(crate) fn materialize_large_radial_text_position(
let Some(EntityType::Dimension(dimension)) = document.get_entity(handle) else {
return;
};
if !matches!(dimension, Dimension::LargeRadial(_)) || dimension.base().text_user_positioned {
if !matches!(&**dimension, Dimension::LargeRadial(_)) || dimension.base().text_user_positioned {
return;
}
let source = document.dim_styles.iter().find(|style| {
@@ -3262,8 +3262,8 @@ pub(crate) fn materialize_large_radial_text_position(
.map(|style| resolved_dimension_style(style, dimension, document))
.map(|style| styled_dimension_text_position(dimension, &style, 1.0))
.unwrap_or_else(|| dimension_text_pos_f64(dimension, None, 2.5, 1.0));
if let Some(EntityType::Dimension(Dimension::LargeRadial(radial))) =
document.get_entity_mut(handle)
if let Some(Dimension::LargeRadial(radial)) =
document.get_entity_mut(handle).and_then(as_dimension_mut)
{
radial.base.text_middle_point = position;
}
@@ -6491,7 +6491,7 @@ fn dimension_text_entity(
}
mtext.normal = base.normal;
mtext.common = base.common.clone();
return Some(EntityType::MText(mtext));
return Some(EntityType::MText(Box::new(mtext)));
}
let value = if style.is_some_and(|style| style.dimtxtdirection) {
@@ -7150,7 +7150,7 @@ fn dimension_tolerance_entity(
mtext.style = primary_style;
mtext.attachment_point = codec::entities::AttachmentPoint::MiddleCenter;
mtext.common = primary_common;
return Some(EntityType::MText(mtext));
return Some(EntityType::MText(Box::new(mtext)));
}
let mut t = Text::with_value(tol, pos)
@@ -9559,3 +9559,22 @@ mod layout_parity_tests {
);
}
}
/// The dimension an entity is, if it is one. The variant holds it boxed, so
/// a pattern cannot reach into it; match on this instead.
pub(crate) fn as_dimension(entity: &codec::EntityType) -> Option<&codec::entities::Dimension> {
match entity {
codec::EntityType::Dimension(dimension) => Some(dimension),
_ => None,
}
}
/// [`as_dimension`], mutably.
pub(crate) fn as_dimension_mut(
entity: &mut codec::EntityType,
) -> Option<&mut codec::entities::Dimension> {
match entity {
codec::EntityType::Dimension(dimension) => Some(dimension),
_ => None,
}
}
+3 -3
View File
@@ -620,7 +620,7 @@ fn dynamic_data_text(data: &DynamicBlockData) -> String {
"{}; dependencies {}; parameters {}; distance multiplier {:.6}; angle {:.6}",
value.action.element.name,
value.action.dependencies.len(),
value.action.parameter_ids.len(),
value.action.action_ids.len(),
value.offsets.distance_multiplier,
value.offsets.angle_offset
),
@@ -628,14 +628,14 @@ fn dynamic_data_text(data: &DynamicBlockData) -> String {
"{}; dependencies {}; parameters {}; connections {}",
value.action.element.name,
value.action.dependencies.len(),
value.action.parameter_ids.len(),
value.action.action_ids.len(),
value.connections.len()
),
DynamicBlockData::RotateAction(value) | DynamicBlockData::ScaleAction(value) => format!(
"{}; dependencies {}; parameters {}; base {:.6},{:.6},{:.6}; dependent {}",
value.action.action.element.name,
value.action.action.dependencies.len(),
value.action.action.parameter_ids.len(),
value.action.action.action_ids.len(),
value.action.base_point.x,
value.action.base_point.y,
value.action.base_point.z,
+3 -1
View File
@@ -676,7 +676,9 @@ pub fn adapt_mtext_paragraphs(
ATab::Left(_) => TabKind::Left,
ATab::Center(_) => TabKind::Center,
ATab::Right(_) => TabKind::Right,
ATab::Decimal(..) => TabKind::Decimal,
ATab::Decimal(..) | ATab::DecimalWithCharacter { .. } => {
TabKind::Decimal
}
};
TabStop {
position: ts.position() as f32 * entity_height,
+10
View File
@@ -158,6 +158,16 @@ pub trait TextContent {
fn replace_text(&mut self, search: &str, rep: &str);
}
/// Entity variants holding their entity boxed (MTEXT) dispatch through here.
impl<T: TextContent + ?Sized> TextContent for Box<T> {
fn text_content(&self) -> Option<String> {
(**self).text_content()
}
fn replace_text(&mut self, search: &str, rep: &str) {
(**self).replace_text(search, rep)
}
}
/// Human-readable variant name used by Quick Select / Select Similar
/// filtering. Stable across releases — the strings here are the canonical
/// type identifiers throughout the UI.
+2 -2
View File
@@ -638,7 +638,7 @@ pub(crate) fn build_entity(
if let Some(style) = text_style_field(spec, document)? {
mtext.style = style;
}
EntityType::MText(mtext)
EntityType::MText(Box::new(mtext))
}
"INSERT" => {
let block = spec["block"].as_str().unwrap_or("");
@@ -724,7 +724,7 @@ pub(crate) fn build_entity(
let mut path = BoundaryPath::external();
path.add_edge(BoundaryEdge::Polyline(PolylineEdge::new(vertices, true)));
hatch.paths.push(path);
EntityType::Hatch(hatch)
EntityType::Hatch(Box::new(hatch))
}
other => {
return Err(failure(
+2
View File
@@ -472,6 +472,7 @@ fn complex_elements(
scale: decimal(*scale),
rotation: decimal(*rot_deg).to_radians(),
absolute_rotation: false,
source_flags: 0,
offset: [decimal(*x), decimal(*y)],
}
}
@@ -492,6 +493,7 @@ fn complex_elements(
scale: decimal(*scale),
rotation: decimal(*rot_deg).to_radians(),
absolute_rotation: false,
source_flags: 0,
offset: [decimal(*x), decimal(*y)],
},
};
+34 -408
View File
@@ -130,7 +130,7 @@ fn recovery_fingerprint_needed(caches: &DerivedCaches) -> bool {
crate::io::xref::XrefStatus::Recovered | crate::io::xref::XrefStatus::Failed
)
});
parser_issue || caches.corrupt_dropped > 0 || caches.xref_dropped > 0 || reference_issue
parser_issue || reference_issue
}
#[derive(Debug, Clone)]
@@ -439,17 +439,10 @@ async fn open_path_with_phase_attempt(
let parse_ms = t_parse.elapsed().as_millis() as u32;
progress2.set(crate::app::OPEN_PHASE_PARSING, 5800, 1000, 1000);
let t_purge = Instant::now();
let dropped = purge_corrupt_entities(&mut doc);
let purge_ms = t_purge.elapsed().as_millis() as u32;
if matches!(attempt, OpenAttempt::Strict) && dropped > 0 {
return Err(OpenAttemptFailure {
message: format!(
"normal read found {dropped} structurally invalid drawing records"
),
read_stats: Some(read_stats),
recoverable: true,
});
if matches!(attempt, OpenAttempt::Recovery(..)) {
repair_drawing(&mut doc);
}
let purge_ms = t_purge.elapsed().as_millis() as u32;
// 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:
@@ -457,7 +450,7 @@ async fn open_path_with_phase_attempt(
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() {
let xref_infos = if let Some(base_dir) = path2.parent() {
let xref_progress = {
let progress = Arc::clone(&progress2);
let callback: Arc<dyn Fn(usize, usize) + Send + Sync> =
@@ -479,7 +472,7 @@ async fn open_path_with_phase_attempt(
Some(xref_progress),
)
} else {
(Vec::new(), 0)
Vec::new()
};
let xref_ms = t_xref.elapsed().as_millis() as u32;
progress2.set(crate::app::OPEN_PHASE_CACHING, 7400, 0, 10000);
@@ -506,9 +499,7 @@ async fn open_path_with_phase_attempt(
// Filled in after `prepare_open_geometry` below.
finalize_ms: 0,
};
caches.corrupt_dropped = dropped;
caches.read_stats = Some(read_stats);
caches.xref_dropped = xref_dropped;
caches.xrefs = xref_infos;
if recovery_fingerprint_needed(&caches) {
caches.source_sha256 =
@@ -800,25 +791,17 @@ async fn load_web_bytes(
fix_current_style_names(&mut doc);
crate::entities::field::local_header_dates(&mut doc);
progress.set(crate::app::OPEN_PHASE_CACHING, 7000, 0, 1);
let dropped = purge_corrupt_entities(&mut doc);
if !recovery_mode && dropped > 0 {
return Err(OpenLoadError {
message: format!("normal read found {dropped} structurally invalid drawing records"),
source_sha256: None,
read_stats: Some(outcome.stats),
recovery_available: true,
});
if recovery_mode {
repair_drawing(&mut doc);
}
// XREFs inside the opened drawing resolve from the in-session bytes
// registry (see `resolve_web_xrefs`) before the caches snapshot the
// document, so merged xref geometry is part of the caches — the same
// phase order the native open uses.
let (xref_infos, xref_dropped) = resolve_web_xrefs(&mut doc, progress.clone()).await;
let xref_infos = resolve_web_xrefs(&mut doc, progress.clone()).await;
let mut caches = crate::scene::build_derived_caches(&doc);
caches.corrupt_dropped = dropped;
caches.read_stats = Some(outcome.stats);
caches.xrefs = xref_infos;
caches.xref_dropped = xref_dropped;
if source_sha256.is_none() && recovery_fingerprint_needed(&caches) {
source_sha256 = web_worker::sha256_document(bytes).await.ok();
}
@@ -891,7 +874,7 @@ pub async fn parse_drawing_bytes_web(name: &str, bytes: &[u8]) -> Result<CadDocu
async fn resolve_web_xrefs(
doc: &mut CadDocument,
progress: Arc<OpenProgressState>,
) -> (Vec<crate::io::xref::XrefInfo>, usize) {
) -> Vec<crate::io::xref::XrefInfo> {
let pending: Vec<String> = doc
.block_records
.iter()
@@ -995,7 +978,7 @@ pub fn load_bytes(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
}
#[cfg(not(target_arch = "wasm32"))]
pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<(CadDocument, usize), String> {
pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<CadDocument, String> {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
@@ -1006,8 +989,7 @@ pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<(CadDocument,
doc.source_path = Some(path.to_string_lossy().into_owned());
// Opening is a field evaluation event (FIELDEVAL bit 1).
crate::entities::field::update_fields(&mut doc, 1, None);
let dropped = purge_corrupt_entities(&mut doc);
Ok((doc, dropped))
Ok(doc)
}
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
@@ -1254,7 +1236,6 @@ fn finalize_loaded_outcome(
let doc = &mut outcome.document;
normalize_block_origins(doc);
materialize_missing_block_markers(doc);
normalize_knotless_splines(doc);
if outcome.stats.source_format == Some(codec::SourceFormat::Dxf) {
fix_dxf_dimension_rotations(doc);
fix_dxf_layout_plot_settings(doc);
@@ -2679,174 +2660,24 @@ fn sync_current_styles_on_save(doc: &mut CadDocument) {
set_sketch_settings(doc, sketch_type, increment, tolerance);
}
// ── Corrupt-entity guard ──────────────────────────────────────────────────
//
// opencadcodec's DWG parser occasionally desynchronises on certain files and
// produces entities with garbage fields: non-unit normals (components in
// 1e200+), nonsensical vertex counts (e.g. 100000), or infinite/NaN
// coordinates. Tessellating such entities triggers huge allocations and
// numerical blow-ups in the wire pipeline.
//
// `purge_corrupt_entities` scans the document and removes any entity that
// fails a cheap sanity check, returning the number dropped so the caller can
// surface it to the UI / log.
//
// Keep valid degenerate geometry: dropping it would trigger strict-open recovery.
fn finite_unit_normal(n: &codec::types::Vector3) -> bool {
let (x, y, z) = (n.x, n.y, n.z);
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
return false;
/// Recovery runs the codec's repair and reports what it did, and what it
/// left, through the drawing's notifications.
pub(crate) fn repair_drawing(doc: &mut CadDocument) {
let repair = doc.repair();
for action in &repair.actions {
doc.notifications.notify(
codec::notification::NotificationType::Warning,
format!("Recovery repair: {action:?}"),
);
}
let mag2 = x * x + y * y + z * z;
// Accept anything within ~10% of unit length. Real files sometimes
// store slightly denormalised normals from rounding.
(mag2 - 1.0).abs() < 0.21
}
fn finite_coord(v: f64) -> bool {
v.is_finite() && v.abs() < 1.0e12
}
fn finite_vec3(v: &codec::types::Vector3) -> bool {
finite_coord(v.x) && finite_coord(v.y) && finite_coord(v.z)
}
/// Returns true if the entity looks like parser garbage and should be dropped.
pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
use codec::entities::EntityType as E;
// Reject polylines at or above this vertex count. Even valid drawings
// rarely use this many — and parser desync produces exactly-100_000-vertex
// junk records.
const MAX_VERTS: usize = 100_000;
match e {
E::LwPolyline(p) => {
!finite_unit_normal(&p.normal)
|| p.vertices.len() >= MAX_VERTS
|| !finite_coord(p.elevation)
|| p.elevation.abs() > 1.0e10
|| !finite_coord(p.thickness)
|| p.thickness.abs() > 1.0e10
|| p.vertices
.iter()
.any(|v| !finite_coord(v.location.x) || !finite_coord(v.location.y))
}
E::Polyline2D(p) => {
!finite_unit_normal(&p.normal)
|| p.vertices.len() >= MAX_VERTS
|| !finite_coord(p.elevation)
|| p.elevation.abs() > 1.0e10
|| !finite_coord(p.thickness)
|| p.thickness.abs() > 1.0e10
|| p.vertices.iter().any(|v| !finite_vec3(&v.location))
}
E::Polyline3D(p) => {
p.vertices.len() >= MAX_VERTS || p.vertices.iter().any(|v| !finite_vec3(&v.position))
}
E::Polyline(p) => {
p.vertices.len() >= MAX_VERTS || p.vertices.iter().any(|v| !finite_vec3(&v.location))
}
E::Line(l) => !finite_vec3(&l.start) || !finite_vec3(&l.end),
// Zero-radius curves are valid and have bounded kernel tessellation.
E::Circle(c) => {
!finite_vec3(&c.center) || !finite_coord(c.radius) || c.radius.abs() > 1.0e10
}
E::Arc(a) => {
!finite_vec3(&a.center)
|| !finite_coord(a.radius)
|| !a.start_angle.is_finite()
|| !a.end_angle.is_finite()
|| a.radius.abs() > 1.0e10
|| !finite_unit_normal(&a.normal)
}
E::Ellipse(e) => {
!finite_vec3(&e.center)
|| !finite_vec3(&e.major_axis)
|| !e.start_parameter.is_finite()
|| !e.end_parameter.is_finite()
|| (e.end_parameter - e.start_parameter).abs() < 1.0e-9
|| {
let m2 = e.major_axis.x * e.major_axis.x
+ e.major_axis.y * e.major_axis.y
+ e.major_axis.z * e.major_axis.z;
!m2.is_finite() || m2 < 1.0e-20 || m2 > 1.0e20
}
|| !e.minor_axis_ratio.is_finite()
|| e.minor_axis_ratio.abs() < 1.0e-10
}
E::Spline(s) => {
// Parser desync emits exactly-100_000-control-point splines with a
// garbage knot vector. Building a NURBS from one and
// tessellating it runs `parameter_division` into an unbounded
// allocation — single-threaded, 32 GB+ — long before the drawing
// finishes loading. Reject the desync signature plus any spline
// the kernel can't build: non-finite control points, or a knot vector
// that's non-finite, non-monotonic, or the wrong length
// (the kernel requires `knots.len() == ctrl.len() + degree + 1`).
let n = s.control_points.len();
let degree_bad = s.degree < 1;
let deg = s.degree.max(0) as usize;
let compact_periodic_knots = s.flags.periodic && s.knots.len() == n.saturating_add(1);
let knots_bad = !s.knots.is_empty()
&& (s.knots.iter().any(|k| !k.is_finite())
|| s.knots.windows(2).any(|w| w[1] < w[0])
|| (!compact_periodic_knots && s.knots.len() != n + deg + 1));
// A single control point with no knots and no fit points cannot
// form a curve, and no knot vector exists for it.
let lone_point = n == 1 && s.knots.is_empty() && s.fit_points.is_empty();
n >= MAX_VERTS
|| degree_bad
|| lone_point
|| s.control_points.iter().any(|p| !finite_vec3(p))
|| knots_bad
}
_ => false,
for issue in &repair.unresolved {
doc.notifications.notify(
codec::notification::NotificationType::Warning,
format!("Recovery left unresolved: {}", issue.message),
);
}
}
/// Give control-point splines stored without knots a degree their control
/// points can carry, and the matching clamped knot vector.
///
/// A degree-p curve needs at least p + 1 control points. Knot generation
/// (here, and in the DWG writer for knot-less splines) subtracts in `usize`
/// and underflows when there are fewer; in release builds the wrapped count
/// drives an unbounded allocation. Lowering the degree keeps the geometry the
/// points describe: two points are a straight segment at any degree.
fn normalize_knotless_splines(doc: &mut CadDocument) {
for entity in doc.entities_mut() {
let EntityType::Spline(spline) = entity else {
continue;
};
let n = spline.control_points.len();
if !spline.knots.is_empty() || n < 2 || spline.degree < 1 {
continue;
}
let degree = (spline.degree as usize).min(n - 1);
spline.degree = degree as i32;
spline.knots = codec::entities::Spline::generate_clamped_knots(degree, n);
}
}
pub fn purge_corrupt_entities(doc: &mut CadDocument) -> usize {
use crate::par::prelude::*;
normalize_knotless_splines(doc);
// Detection is pure and read-only; the per-vertex finite/extent checks on
// large polylines dominate, so fan the scan out across cores. Gather
// entity references in one pass, test in parallel, then remove serially
// (`remove_entity` needs `&mut doc`).
let entities: Vec<&EntityType> = doc.entities().collect();
let bad: Vec<codec::Handle> = entities
.par_iter()
.filter(|e| is_entity_corrupt(e))
.map(|e| e.common().handle)
.collect();
let n = bad.len();
for h in bad {
doc.remove_entity(h);
}
n
}
/// opencadcodec's ViewportStatusFlags::from_bits() maps bit 0 → is_on and bit 15 → locked,
/// but the real DXF/DWG spec uses bit 15 (0x8000) → viewport on and bit 14 (0x4000) → locked.
/// Files from AutoCAD and other tools always set bit 15 for active viewports, leaving bit 0
@@ -2969,7 +2800,7 @@ mod dimension_arrow_save_tests {
dimension.definition_point = Vector3::new(0.0, 20.0, 0.0);
dimension.base.definition_point = dimension.definition_point;
dimension.base.text_middle_point = Vector3::new(50.0, 20.0, 0.0);
doc.add_entity(EntityType::Dimension(Dimension::Linear(dimension)))
doc.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(dimension))))
.unwrap();
doc
}
@@ -3161,214 +2992,6 @@ mod layer_roundtrip_tests {
}
}
#[cfg(test)]
mod corrupt_guard_tests {
use super::*;
use codec::entities::{Arc, Circle, EntityType, Spline};
use codec::types::Vector3;
fn knotless_spline(degree: i32, points: usize) -> Spline {
let mut spline = Spline::new();
spline.degree = degree;
spline.control_points = (0..points)
.map(|index| Vector3::new(index as f64, index as f64, 0.0))
.collect();
spline
}
// Degree 3 over two control points with no knot vector: generating knots
// for it underflows, and writing it to DWG used to panic (debug) or
// allocate without bound (release).
#[test]
fn knotless_spline_with_too_few_points_is_lowered_and_saves_as_dwg() {
let mut doc = CadDocument::new();
doc.add_entity(EntityType::Spline(knotless_spline(3, 2)))
.unwrap();
doc.add_entity(EntityType::Spline(knotless_spline(3, 1)))
.unwrap();
assert_eq!(
purge_corrupt_entities(&mut doc),
1,
"the lone point is dropped"
);
let spline = doc
.entities()
.find_map(|entity| match entity {
EntityType::Spline(spline) => Some(spline.clone()),
_ => None,
})
.expect("the two-point spline is kept");
assert_eq!(spline.degree, 1);
assert_eq!(spline.knots, vec![0.0, 0.0, 1.0, 1.0]);
let path =
std::env::temp_dir().join(format!("ocs_knotless_spline_{}.dwg", std::process::id()));
save_as_version(&doc, &path, codec::DxfVersion::AC1032).expect("save");
let _ = std::fs::remove_file(&path);
}
// `load_file` (headless export, xrefs, block import) does not purge, so
// the spline repair has to happen when any load is finalized.
#[test]
fn knotless_spline_is_repaired_by_every_load_path() {
const DXF: &str =
"0\nSECTION\n2\nENTITIES\n0\nSPLINE\n8\n0\n70\n0\n71\n3\n72\n0\n73\n2\n74\n0\n\
10\n0\n20\n0\n30\n0\n10\n1\n20\n1\n30\n0\n0\nENDSEC\n0\nEOF\n";
let dir = std::env::temp_dir();
let source = dir.join(format!("ocs_knotless_load_{}.dxf", std::process::id()));
let target = dir.join(format!("ocs_knotless_load_{}.dwg", std::process::id()));
std::fs::write(&source, DXF).unwrap();
let doc = load_file(&source).expect("load");
let _ = std::fs::remove_file(&source);
let spline = doc
.entities()
.find_map(|entity| match entity {
EntityType::Spline(spline) => Some(spline),
_ => None,
})
.expect("spline");
assert_eq!(spline.degree, 1);
assert_eq!(spline.knots.len(), 4);
save_as_version(&doc, &target, codec::DxfVersion::AC1032).expect("save");
let _ = std::fs::remove_file(&target);
}
// Small but finite arcs are valid records. Kernel tessellation is bounded,
// so opening must retain them instead of treating their size as corruption.
#[test]
fn keeps_small_finite_arc() {
let mut a = Arc::new();
a.center = Vector3::new(2880.84, 891.83, 0.0);
a.radius = 0.0038974142851181423;
a.start_angle = 1.0401656235942365;
a.end_angle = 1.0401671831670538;
a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
// A nearly straight arc still carries valid source geometry.
#[test]
fn keeps_nearly_straight_arc() {
let mut a = Arc::new();
a.center = Vector3::new(551435.3071786845, 4051623.7156955916, 0.0);
a.radius = 35.0;
a.start_angle = 5.823361856481176;
a.end_angle = 5.823362506017916;
a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
// A large-radius small-sweep arc is still a visible curve and must survive:
// radius 1e6 × sweep 1e-4 ≈ 100 units of arc.
#[test]
fn keeps_large_radius_small_sweep_arc() {
let mut a = Arc::new();
a.radius = 1.0e6;
a.start_angle = 0.0;
a.end_angle = 1.0e-4;
a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
#[test]
fn keeps_zero_radius_circle_and_arc() {
let mut c = Circle::new();
c.center = Vector3::new(206.2, 150.7, 0.0);
c.radius = 0.0;
assert!(!is_entity_corrupt(&EntityType::Circle(c)));
let mut a = Arc::new();
a.center = Vector3::new(223.5, 174.5, 0.0);
a.radius = 0.0;
a.start_angle = 0.0;
a.end_angle = 0.0;
a.normal = Vector3::new(0.0, 0.0, 1.0);
assert!(!is_entity_corrupt(&EntityType::Arc(a)));
}
#[test]
fn drops_absurd_radius_circle() {
let mut c = Circle::new();
c.radius = 1.0e11;
assert!(is_entity_corrupt(&EntityType::Circle(c)));
let mut c = Circle::new();
c.radius = f64::NAN;
assert!(is_entity_corrupt(&EntityType::Circle(c)));
}
// Parser desync emits 100_000-control-point splines; building a kernel
// NURBS from one and tessellating it OOMs. The control-point cap rejects it.
#[test]
fn rejects_desync_spline() {
let pts = vec![Vector3::new(0.0, 0.0, 0.0); 100_000];
let s = Spline::from_control_points(3, pts);
assert!(is_entity_corrupt(&EntityType::Spline(s)));
}
// A collapsed spline is still a valid source record. Kernel subdivision
// has a fixed depth bound and can process it safely.
#[test]
fn keeps_degenerate_point_spline() {
let pts = vec![Vector3::new(1e-12, -1e-12, 0.0); 9];
let s = Spline::from_control_points(3, pts);
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
}
#[test]
fn keeps_compact_periodic_spline_knots() {
let pts = (0..12)
.map(|index| Vector3::new(index as f64, (index % 3) as f64, 0.0))
.collect();
let mut s = Spline::from_control_points(2, pts);
s.flags.closed = true;
s.flags.periodic = true;
s.knots = (0..13).map(|value| value as f64).collect();
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
}
// A normal cubic spline (4 control points, valid clamped knots) survives.
#[test]
fn keeps_valid_spline() {
let pts = vec![
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 1.0, 0.0),
Vector3::new(2.0, -1.0, 0.0),
Vector3::new(3.0, 0.0, 0.0),
];
let s = Spline::from_control_points(3, pts);
assert!(!is_entity_corrupt(&EntityType::Spline(s)));
}
// A corrupt or adversarial MINSERT row/column pair (u16, so its unchecked
// product can reach into the billions) must be rejected before it can
// drive the render graph's per-instance allocation and expansion.
#[test]
fn preserves_large_minsert_data_while_rendering_is_bounded() {
let mut i = codec::entities::Insert::new("BLOCK", Vector3::ZERO);
i.row_count = u16::MAX;
i.column_count = u16::MAX;
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
}
// An ordinary array insert, well under the budget, is valid source data.
#[test]
fn keeps_a_reasonable_minsert() {
let mut i = codec::entities::Insert::new("BLOCK", Vector3::ZERO);
i.row_count = 10;
i.column_count = 10;
i.row_spacing = 5.0;
i.column_spacing = 5.0;
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
}
// A plain (non-array) INSERT is never treated as a MINSERT-count problem.
#[test]
fn keeps_a_plain_insert() {
let i = codec::entities::Insert::new("BLOCK", Vector3::ZERO);
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
}
}
#[cfg(test)]
mod pre_r2000_layer_plot_tests {
use super::*;
@@ -3545,11 +3168,12 @@ mod prepare_save_tests {
dim.base.definition_point = Vector3::ZERO;
dim.base.common.handle = doc.allocate_handle();
let handle = dim.base.common.handle;
doc.add_entity(EntityType::Dimension(Dimension::Linear(dim)))
doc.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(dim))))
.unwrap();
let prepared = prepare_document_for_save(doc.clone(), doc.version);
let EntityType::Dimension(Dimension::Linear(linear)) = prepared.get_entity(handle).unwrap()
let Some(Dimension::Linear(linear)) =
crate::entities::dimension::as_dimension(prepared.get_entity(handle).unwrap())
else {
panic!("dimension missing from prepared clone");
};
@@ -3565,7 +3189,9 @@ mod prepare_save_tests {
assert_eq!(linear.base.definition_point, linear.definition_point);
// The live document is untouched: no block name, stale group-10.
let EntityType::Dimension(Dimension::Linear(live)) = doc.get_entity(handle).unwrap() else {
let Some(Dimension::Linear(live)) =
crate::entities::dimension::as_dimension(doc.get_entity(handle).unwrap())
else {
panic!("dimension missing from live document");
};
assert!(live.base.block_name.is_empty());
@@ -3582,7 +3208,7 @@ mod prepare_save_tests {
let mut dim =
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
dim.base.common.handle = doc.allocate_handle();
doc.add_entity(EntityType::Dimension(Dimension::Linear(dim)))
doc.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(dim))))
.unwrap();
for ext in ["dwg", "dxf"] {
let inline = save_to_bytes(&doc, ext, codec::DxfVersion::AC1032).expect("inline");
+1 -1
View File
@@ -168,7 +168,7 @@ pub fn add_embedded_image(
) -> Result<codec::Handle, String> {
let (upper_left, lower_right) = corners_from_placement(origin, width, image.aspect());
let ole = build_embedded_ole(image, upper_left, lower_right);
doc.add_entity(codec::EntityType::Ole2Frame(ole))
doc.add_entity(codec::EntityType::Ole2Frame(Box::new(ole)))
.map_err(|e| e.to_string())
}
-19
View File
@@ -24,8 +24,6 @@ pub struct RecoveryReport {
pub source_sha256: Option<String>,
pub read_stats: Option<codec::ReadStats>,
pub entities_scanned: usize,
pub entities_removed: usize,
pub referenced_entities_removed: usize,
pub parser_errors_recovered: usize,
pub diagnostics: Vec<(String, String)>,
pub references_checked: usize,
@@ -54,8 +52,6 @@ impl RecoveryReport {
source_sha256: Option<String>,
read_stats: Option<codec::ReadStats>,
entities_scanned: usize,
entities_removed: usize,
referenced_entities_removed: usize,
references: &[XrefInfo],
notifications: &codec::notification::NotificationCollection,
save_as_required: bool,
@@ -163,8 +159,6 @@ impl RecoveryReport {
source_sha256,
read_stats,
entities_scanned,
entities_removed,
referenced_entities_removed,
parser_errors_recovered,
diagnostics,
references_checked: references.len(),
@@ -211,8 +205,6 @@ impl RecoveryReport {
.unwrap_or(0),
read_stats,
entities_scanned: 0,
entities_removed: 0,
referenced_entities_removed: 0,
diagnostics: Vec::new(),
references_checked: 0,
references_loaded: 0,
@@ -233,17 +225,11 @@ impl RecoveryReport {
}
}
pub fn removed_total(&self) -> usize {
self.entities_removed
.saturating_add(self.referenced_entities_removed)
}
pub fn issues_found(&self) -> usize {
self.parser_errors_recovered
.saturating_add(self.references_recovered)
.saturating_add(self.references_failed)
.saturating_add(self.references_missing)
.saturating_add(self.removed_total())
.saturating_add(usize::from(self.error.is_some()))
}
@@ -315,11 +301,6 @@ impl RecoveryReport {
));
}
lines.push(format!("Entities scanned: {}", self.entities_scanned));
lines.push(format!("Entities removed: {}", self.entities_removed));
lines.push(format!(
"Referenced entities removed: {}",
self.referenced_entities_removed
));
lines.push(format!(
"Parser errors recovered: {}",
self.parser_errors_recovered
+17 -45
View File
@@ -277,12 +277,8 @@ fn read_outcome_from_bytes(
/// `base_dir`, and populate each xref block with entities from the
/// referenced file.
///
/// Returns a list of [`XrefInfo`] describing each xref block found, plus the
/// number of corrupt xref entities dropped during merge. Purging happens
/// inline as each xref's entities are merged — xref content is parser output
/// just like the host doc, so it gets the same corrupt-entity guard. Folding
/// it in here avoids a second full-document `entities()` walk after resolve.
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>, usize) {
/// Returns a list of [`XrefInfo`] describing each xref block found.
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> Vec<XrefInfo> {
resolve_xrefs_with_filter(doc, base_dir, None, None)
}
@@ -295,7 +291,7 @@ pub fn resolve_xrefs_for_keys(
doc: &mut CadDocument,
base_dir: &Path,
keys: &HashSet<Handle>,
) -> (Vec<XrefInfo>, usize) {
) -> Vec<XrefInfo> {
resolve_xrefs_with_filter(doc, base_dir, None, Some(keys))
}
@@ -308,7 +304,7 @@ pub fn resolve_xrefs_with_progress(
doc: &mut CadDocument,
base_dir: &Path,
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
) -> (Vec<XrefInfo>, usize) {
) -> Vec<XrefInfo> {
resolve_xrefs_with_filter(doc, base_dir, progress, None)
}
@@ -317,7 +313,7 @@ fn resolve_xrefs_with_filter(
base_dir: &Path,
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
only: Option<&HashSet<Handle>>,
) -> (Vec<XrefInfo>, usize) {
) -> Vec<XrefInfo> {
// Auto-resolve every xref — frustum + LOD culling keep GPU cost bounded.
let xref_entries: Vec<(String, String, Handle)> = doc
.block_records
@@ -432,7 +428,6 @@ fn resolve_xrefs_with_filter(
// Phase 2 — merge each parsed xref into the host document, in the original
// block order (par_iter preserves it), so handle allocation is deterministic.
let mut result = Vec::with_capacity(parsed.len());
let mut dropped = 0usize;
#[allow(unused_mut, unused_variables)]
for (
merge_index,
@@ -468,19 +463,7 @@ fn resolve_xrefs_with_filter(
.iter()
.map(|diagnostic| diagnostic.message.clone()),
);
let invalid = super::purge_corrupt_entities(&mut outcome.document);
if invalid > 0 {
diagnostics.push(format!(
"normal read found {invalid} structurally invalid reference records"
));
#[cfg(not(target_arch = "wasm32"))]
if source_sha256.is_none() {
source_sha256 = resolved.as_ref().and_then(|path| {
super::stable_sha256_file(path, initial_fingerprint.as_ref())
});
}
}
if recovered || invalid > 0 {
if recovered {
(XrefStatus::Failed, diagnostics, Some(outcome.stats))
} else {
// Reload is replacement, never append. Keep the prior merge
@@ -502,7 +485,7 @@ fn resolve_xrefs_with_filter(
let _ = unload_reference(doc, br_handle.value());
remove_pipe_symbols(doc, &block_name);
ensure_block_entities(doc, &block_name);
dropped += merge_xref_into_block(doc, &block_name, br_handle, outcome.document);
merge_xref_into_block(doc, &block_name, br_handle, outcome.document);
if visretain {
restore_dependent_layers(doc, &block_name, kept_layers);
}
@@ -533,7 +516,7 @@ fn resolve_xrefs_with_filter(
}
}
(result, dropped)
result
}
/// Underlay files named by a relative (or moved) path: read them from where
@@ -1218,13 +1201,12 @@ fn copy_xref_sortents(
/// — which doesn't know about the xref's layers — and silently falls
/// back to WHITE / 1 px line weight. AutoCAD's BIND command uses the
/// same naming scheme.
/// Returns the number of corrupt entities skipped during the merge.
fn merge_xref_into_block(
doc: &mut CadDocument,
xref_block_name: &str,
br_handle: Handle,
xref_doc: CadDocument,
) -> usize {
) {
let prefix = xref_block_name;
// Carry the xref's INSUNITS onto the host BlockRecord. Used at INSERT
@@ -1264,7 +1246,7 @@ fn merge_xref_into_block(
// ── Entities (shared helper) ────────────────────────────────────────
// The unremapped-handle count is ignored on the load path (BIND reports
// it per bind).
let (dropped, _, entity_handle_map) = merge_source_entities(doc, &xref_doc, br_handle, &maps);
let (_, entity_handle_map) = merge_source_entities(doc, &xref_doc, br_handle, &maps);
// ── Draw-order overrides (shared helper) ──────────────────────────────
copy_xref_sortents(
@@ -1274,7 +1256,6 @@ fn merge_xref_into_block(
&maps.br_handles,
&entity_handle_map,
);
dropped
}
/// Source entities in display order: each block record's `entity_handles`
@@ -1308,26 +1289,18 @@ fn ordered_source_entities(xref_doc: &CadDocument) -> Vec<(Handle, EntityType)>
/// Merge one source doc's entities into `target`, routing model-space content
/// into `target_br` and nested-block content into its imported counterpart
/// (`maps.br_handles`). Returns corrupt-drop count, unremapped-handle count,
/// and the old→new entity handle map for the sortents copy.
/// (`maps.br_handles`). Returns the unremapped-handle count and the old→new
/// entity handle map for the sortents copy.
fn merge_source_entities(
target: &mut CadDocument,
source: &CadDocument,
target_br: Handle,
maps: &XrefSymbolMaps,
) -> (usize, usize, HashMap<Handle, Handle>) {
) -> (usize, HashMap<Handle, Handle>) {
let source_ms_handle = source.header.model_space_block_handle;
let mut entity_handle_map: HashMap<Handle, Handle> = HashMap::default();
let mut dropped = 0usize;
let mut unremapped = 0usize;
for (old_h, mut entity) in ordered_source_entities(source) {
// Drop parser-garbage entities (bad normals / vertex counts / inf
// coords) before they enter the host doc — they trigger huge
// allocations and unbounded recursion in the wire pipeline.
if super::is_entity_corrupt(&entity) {
dropped += 1;
continue;
}
// Remap every symbol reference so the host's resolver hits the copies
// just imported (load path ignores the unremapped count; BIND reports
// it per bind).
@@ -1351,7 +1324,7 @@ fn merge_source_entities(
entity_handle_map.insert(old_h, new_h);
}
}
(dropped, unremapped, entity_handle_map)
(unremapped, entity_handle_map)
}
fn is_sentinel_linetype(name: &str) -> bool {
@@ -2170,8 +2143,7 @@ fn merge_bound_doc(
taken,
};
let maps = import_xref_symbols(target, source, &mut naming);
let (_, unremapped, entity_handle_map) =
merge_source_entities(target, source, target_br, &maps);
let (unremapped, entity_handle_map) = merge_source_entities(target, source, target_br, &maps);
copy_xref_sortents(
target,
source,
@@ -2970,7 +2942,7 @@ mod tests {
// 3. Targeted reload: back to Loaded.
let mut handles = HashSet::default();
handles.insert(doc.block_records.get("MID").unwrap().handle);
let (infos, _) = resolve_xrefs_for_keys(&mut doc, &dir, &handles);
let infos = resolve_xrefs_for_keys(&mut doc, &dir, &handles);
assert!(infos.iter().any(|i| i.name == "MID"));
let listed = collect_entries_with_prev(&doc, &dir, &empty, &no_prev);
assert!(listed
@@ -3336,7 +3308,7 @@ mod tests {
// Hatch (pattern names have no table — preserved verbatim).
let mut hatch = codec::entities::Hatch::new();
hatch.pattern.name = "ANSI31".to_string();
doc.add_entity(codec::EntityType::Hatch(hatch)).unwrap();
doc.add_entity(codec::EntityType::Hatch(Box::new(hatch))).unwrap();
// Dim-style reference via a tolerance entity.
doc.dim_styles
.add(codec::tables::DimStyle::new("ARCH"))
+3 -5
View File
@@ -696,10 +696,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"ATTSYNC block name to sync:" => Some(("insert", "attsync-block-name-to-sync")),
"ATTSYNC: no block named \"{arg}\"." => Some(("insert", "attsync-no-block-named-arg")),
"ATTSYNC: synchronised {synced} insert(s) of \"{block}\" against {} attribute definition(s)." => Some(("insert", "attsync-synchronised-synced-insert-s-of-block")),
"AUDIT Fix any errors detected? [Yes / No] <No>:" => Some(("styles", "audit-fix-any-errors-detected")),
"AUDIT: no issues found." => Some(("styles", "audit-no-issues-found")),
"AUDIT: reference(s) to undefined block(s): {}" => Some(("styles", "audit-reference-s-to-undefined-block-s")),
"AUDIT: reference(s) to undefined layer(s): {}" => Some(("styles", "audit-reference-s-to-undefined-layer-s")),
"AUDIT: report only — no automatic repair performed." => Some(("styles", "audit-report-only-no-automatic-repair-performe")),
"AUDIT: {} error(s), {} warning(s) found." => Some(("styles", "audit-error-s-warning-s-found")),
"AUDIT: {} repair(s) applied, {} issue(s) left unresolved." => Some(("styles", "audit-repair-s-applied-issue-s-left")),
"AUDIT: scanned {total} object(s)." => Some(("styles", "audit-scanned-total-object-s")),
"About" => Some(("manage", "about")),
"Above" => Some(("common", "above")),
@@ -3806,8 +3806,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"WIPEOUT Rectangular Specify first corner:" => Some(("draw", "wipeout-rectangular-specify-first-corner")),
"WIPEOUT Rectangular Specify opposite corner:" => Some(("draw", "wipeout-rectangular-specify-opposite-corner")),
"Warning" => Some(("errors", "warning")),
"Warning: {} corrupt entities dropped (parser junk — bad normals / counts)" => Some(("file", "warning-corrupt-entities-dropped-parser-junk-b")),
"Warning: {} corrupt xref entities dropped" => Some(("file", "warning-corrupt-xref-entities-dropped")),
"Watertight" => Some(("properties", "watertight")),
"Web" => Some(("common", "web")),
"Web File" => Some(("properties", "web-file")),
+1 -1
View File
@@ -95,7 +95,7 @@ pub(crate) fn aligned_dimension_entity(
crate::entities::dimension::reset_automatic_text_position(&mut dim.base);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text_override);
EntityType::Dimension(Dimension::Aligned(dim))
EntityType::Dimension(Box::new(Dimension::Aligned(dim)))
}
impl CadCommand for AlignedDimensionCommand {
+3 -3
View File
@@ -232,7 +232,7 @@ impl AngularDimensionCommand {
}
fn commit(&self, dimension: Dimension) -> CmdResult {
let entity = self.plane.place_entity(EntityType::Dimension(dimension));
let entity = self.plane.place_entity(EntityType::Dimension(Box::new(dimension)));
CmdResult::CommitDimension {
entity,
association: DimensionAssociationInput::Explicit(self.source_refs.clone()),
@@ -1242,7 +1242,7 @@ pub(crate) fn angular_two_line_entity(
dim.base.insertion_point = dim.dimension_arc;
dim.base.actual_measurement = dim.measurement_degrees();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text);
Some(EntityType::Dimension(Dimension::Angular2Ln(dim)))
Some(EntityType::Dimension(Box::new(Dimension::Angular2Ln(dim))))
}
/// A three-point angular dimension (vertex, two points, arc point), for a
@@ -1261,7 +1261,7 @@ pub(crate) fn angular_three_point_entity(
dim.base.insertion_point = dim.definition_point;
dim.base.actual_measurement = dim.measurement_degrees();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text);
Some(EntityType::Dimension(Dimension::Angular3Pt(dim)))
Some(EntityType::Dimension(Box::new(Dimension::Angular3Pt(dim))))
}
fn v3(point: DVec3) -> Vector3 {
+1 -1
View File
@@ -258,7 +258,7 @@ impl ArcLengthDimensionCommand {
|handle| DimensionAssociationInput::Explicit(selection.association_sources(handle)),
);
CmdResult::CommitDimension {
entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))),
entity: plane.place_entity(EntityType::Dimension(Box::new(Dimension::Arc(dimension)))),
association,
preserve_base_style: false,
continue_command: false,
+2 -2
View File
@@ -83,7 +83,7 @@ impl DiameterDimensionCommand {
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
}
plane.place_entity(EntityType::Dimension(Dimension::Diameter(dim)))
plane.place_entity(EntityType::Dimension(Box::new(Dimension::Diameter(dim))))
}
}
@@ -103,7 +103,7 @@ pub(crate) fn diameter_constraint_entity(
dim.leader_length = chord.distance(location).max(radius * 0.5);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text);
Some(EntityType::Dimension(Dimension::Diameter(dim)))
Some(EntityType::Dimension(Box::new(Dimension::Diameter(dim))))
}
impl CadCommand for DiameterDimensionCommand {
+3 -3
View File
@@ -151,7 +151,7 @@ impl DimBaselineCommand {
let increment = self.effective_increment(&style.style_name);
let plane = plane_from_normal(dimension.base().definition_point, style.normal);
let pick = pick.and_then(|point| plane.project(point.to_array()).map(Vec2::from));
let kind = match dimension {
let kind = match &**dimension {
Dimension::Linear(source) => {
let first = project(&plane, source.first_point);
let second = project(&plane, source.second_point);
@@ -441,7 +441,7 @@ impl CadCommand for DimBaselineCommand {
self.committed += 1;
self.advance(1.0);
CmdResult::CommitDimension {
entity: EntityType::Dimension(dimension),
entity: EntityType::Dimension(Box::new(dimension)),
association: DimensionAssociationInput::Infer(None),
preserve_base_style: self.preserve_base_style,
continue_command: true,
@@ -489,7 +489,7 @@ impl CadCommand for DimBaselineCommand {
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
self.build_dimension(cursor).map(|dimension| {
vec![DimensionPreview {
entity: EntityType::Dimension(dimension),
entity: EntityType::Dimension(Box::new(dimension)),
preserve_base_style: self.preserve_base_style,
}]
})
+3 -3
View File
@@ -117,7 +117,7 @@ impl DimContinueCommand {
let style = SourceStyle::from_base(dimension.base());
let plane = plane_from_normal(dimension.base().definition_point, style.normal);
let pick = pick_world.and_then(|point| plane.project(point.to_array()).map(Vec2::from));
let kind = match dimension {
let kind = match &**dimension {
Dimension::Linear(source) => {
let rotation = source.rotation;
let perpendicular = Vec2::new(-rotation.sin(), rotation.cos());
@@ -383,7 +383,7 @@ impl CadCommand for DimContinueCommand {
}
self.advance(point);
CmdResult::CommitDimension {
entity: EntityType::Dimension(dimension),
entity: EntityType::Dimension(Box::new(dimension)),
association: DimensionAssociationInput::Infer(None),
preserve_base_style: self.preserve_base_style,
continue_command: true,
@@ -433,7 +433,7 @@ impl CadCommand for DimContinueCommand {
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
self.build_dimension(cursor).map(|dimension| {
vec![DimensionPreview {
entity: EntityType::Dimension(dimension),
entity: EntityType::Dimension(Box::new(dimension)),
preserve_base_style: self.preserve_base_style,
}]
})
+7 -3
View File
@@ -145,13 +145,17 @@ impl CadCommand for DimJogLineCommand {
}
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
let Some(EntityType::Dimension(dimension @ (Dimension::Linear(_) | Dimension::Aligned(_)))) =
self.picked_entity.take()
else {
let Some(EntityType::Dimension(dimension)) = self.picked_entity.take() else {
return CmdResult::ReportError(
t!("DIMJOGLINE: select a linear or aligned dimension.").into_owned(),
);
};
let dimension = *dimension;
if !matches!(dimension, Dimension::Linear(_) | Dimension::Aligned(_)) {
return CmdResult::ReportError(
t!("DIMJOGLINE: select a linear or aligned dimension.").into_owned(),
);
}
if handle.is_null() {
return CmdResult::NeedPoint;
}
+8 -5
View File
@@ -104,8 +104,8 @@ impl CadCommand for DimSpaceCommand {
for selected in entities {
if selected.handle != *base
&& matches!(
selected.entity,
EntityType::Dimension(
crate::entities::dimension::as_dimension(&selected.entity),
Some(
codec::entities::Dimension::Linear(_)
| codec::entities::Dimension::Aligned(_)
)
@@ -124,11 +124,14 @@ impl CadCommand for DimSpaceCommand {
match &mut self.step {
Step::PickBase => {
if !matches!(
self.picked_entity.take(),
Some(EntityType::Dimension(
self.picked_entity
.take()
.as_ref()
.and_then(crate::entities::dimension::as_dimension),
Some(
codec::entities::Dimension::Linear(_)
| codec::entities::Dimension::Aligned(_)
))
)
) {
return CmdResult::ReportError(
t!("DIMSPACE: select a linear or aligned dimension.").into_owned(),
+1 -1
View File
@@ -158,7 +158,7 @@ impl CadCommand for FieldPlaceCommand {
mtext.attachment_point = self.attachment;
// No defined width: the field text never wraps.
mtext.rectangle_width = 0.0;
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::MText(mtext)))
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::MText(Box::new(mtext))))
}
fn on_enter(&mut self) -> CmdResult {
+3 -1
View File
@@ -138,7 +138,9 @@ impl JoggedRadiusDimensionCommand {
);
CmdResult::CommitDimension {
entity: plane.place_entity(EntityType::Dimension(Dimension::LargeRadial(dimension))),
entity: plane.place_entity(EntityType::Dimension(Box::new(Dimension::LargeRadial(
dimension,
)))),
association,
preserve_base_style: false,
continue_command: false,
+1 -1
View File
@@ -151,7 +151,7 @@ impl LeaderCommand {
CmdResult::CommitManyAndEditText {
entities: vec![
self.plane.place_entity(EntityType::Leader(leader)),
self.plane.place_entity(EntityType::MText(mtext)),
self.plane.place_entity(EntityType::MText(Box::new(mtext))),
],
edit_index: 1,
open_editor,
+1 -1
View File
@@ -141,7 +141,7 @@ pub(crate) fn linear_dimension_entity(
crate::entities::dimension::reset_automatic_text_position(&mut dim.base);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text_override);
EntityType::Dimension(Dimension::Linear(dim))
EntityType::Dimension(Box::new(Dimension::Linear(dim)))
}
impl CadCommand for LinearDimensionCommand {
+1 -1
View File
@@ -648,7 +648,7 @@ impl CadCommand for MLeaderCommand {
text.insertion_point.z,
));
self.text = text.value.clone();
self.selected_mtext = Some(text);
self.selected_mtext = Some(*text);
self.content_type = LeaderContentType::MText;
self.order = CreationOrder::ContentFirst;
self.step = Step::PickPoints;
+1 -1
View File
@@ -80,7 +80,7 @@ impl OrdinateDimCommand {
}
dim.refresh_measurement();
self.plane
.place_entity(EntityType::Dimension(Dimension::Ordinate(dim)))
.place_entity(EntityType::Dimension(Box::new(Dimension::Ordinate(dim))))
}
}
+5 -5
View File
@@ -341,7 +341,7 @@ impl QdimCommand {
]);
(
self.plane
.place_entity(EntityType::Dimension(Dimension::Linear(dimension))),
.place_entity(EntityType::Dimension(Box::new(Dimension::Linear(dimension)))),
association,
)
}
@@ -366,7 +366,7 @@ impl QdimCommand {
dimension.refresh_measurement();
(
self.plane
.place_entity(EntityType::Dimension(Dimension::Ordinate(dimension))),
.place_entity(EntityType::Dimension(Box::new(Dimension::Ordinate(dimension)))),
DimensionAssociationInput::Explicit(vec![point.sources.first().copied()]),
)
}
@@ -405,7 +405,7 @@ impl QdimCommand {
dimension.base.text_user_positioned = true;
dimension.leader_length = chord.distance(text);
dimension.base.actual_measurement = dimension.measurement();
EntityType::Dimension(Dimension::Radius(dimension))
EntityType::Dimension(Box::new(Dimension::Radius(dimension)))
} else {
let opposite = center - direction * radius;
let mut dimension = DimensionDiameter::new(v3(chord), v3(opposite));
@@ -415,7 +415,7 @@ impl QdimCommand {
dimension.base.text_user_positioned = true;
dimension.leader_length = chord.distance(text);
dimension.base.actual_measurement = dimension.measurement();
EntityType::Dimension(Dimension::Diameter(dimension))
EntityType::Dimension(Box::new(Dimension::Diameter(dimension)))
};
(self.plane.place_entity(entity), association)
})
@@ -680,7 +680,7 @@ fn preview_entity(index: usize, entity: &EntityType) -> Vec<WireModel> {
return Vec::new();
};
let mut lines: Vec<Vec<[f64; 3]>> = Vec::new();
match dimension {
match &**dimension {
Dimension::Linear(value) => {
let first = d3(value.first_point);
let second = d3(value.second_point);
+2 -2
View File
@@ -93,7 +93,7 @@ pub(crate) fn radial_dimension_entity(
if let Some(angle) = text_angle {
dim.base.text_rotation = angle;
}
plane.place_entity(EntityType::Dimension(Dimension::Radius(dim)))
plane.place_entity(EntityType::Dimension(Box::new(Dimension::Radius(dim))))
}
/// A radius dimension for a dimensional constraint: the leader leaves the
@@ -111,7 +111,7 @@ pub(crate) fn radius_constraint_entity(
dim.leader_length = chord.distance(location).max(radius * 0.5);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text);
Some(EntityType::Dimension(Dimension::Radius(dim)))
Some(EntityType::Dimension(Box::new(Dimension::Radius(dim))))
}
/// The unit direction a radial constraint's dimension points in, from the
+4 -3
View File
@@ -222,7 +222,7 @@ fn separate(p: &Piece, doc: &CadDocument) -> Vec<EntityType> {
let at = p.point(p.rect[0] + x * (p.rect[1] - p.rect[0]), p.rect[3]);
let mut m = mtext_from(p.entity, p.content.clone(), at, p.height, p.rotation);
m.attachment_point = attachment;
vec![EntityType::MText(m)]
vec![EntityType::MText(Box::new(m))]
}
EntityType::MText(m) => {
// The drawn words, grouped into lines by their baseline.
@@ -261,7 +261,8 @@ fn separate(p: &Piece, doc: &CadDocument) -> Vec<EntityType> {
.collect::<Vec<_>>()
.join(" ");
let at = p.point(line[0].0 - origin_along, p.rect[3] - k as f64 * step);
EntityType::MText(mtext_from(p.entity, value, at, line[0].1.height, m.rotation))
let height = line[0].1.height;
EntityType::MText(Box::new(mtext_from(p.entity, value, at, height, m.rotation)))
})
.collect()
}
@@ -352,7 +353,7 @@ pub fn plan(
}
Some(Txt2MtxtPlan {
erase: pieces.iter().map(|p| p.handle).collect(),
add: vec![EntityType::MText(m)],
add: vec![EntityType::MText(Box::new(m))],
})
}
+1 -1
View File
@@ -113,7 +113,7 @@ impl CadCommand for PasteSpecCommand {
mtext.style = self.style.clone();
mtext.attachment_point = AttachmentPoint::TopLeft;
mtext.rectangle_width = 0.0;
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::MText(mtext)))
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::MText(Box::new(mtext))))
}
fn on_enter(&mut self) -> CmdResult {
+2 -2
View File
@@ -925,7 +925,7 @@ mod tests {
}));
}
hatch.paths.push(path);
let entity = EntityType::Hatch(hatch);
let entity = EntityType::Hatch(Box::new(hatch));
HatcheditCommand::with_handle(Handle::new(7), "ANSI31".into(), 1.0, 0.0, false)
.with_appearance(Some(&entity), Color::Index(6), Transparency::T_40)
.with_origin([9.0, 8.0])
@@ -1096,7 +1096,7 @@ mod tests {
counter_clockwise: true,
}));
hatch.paths.push(path);
let entity = EntityType::Hatch(hatch);
let entity = EntityType::Hatch(Box::new(hatch));
let mut command =
HatcheditCommand::with_handle(Handle::new(7), "ANSI31".into(), 1.0, 0.0, false)
.with_appearance(Some(&entity), Color::ByLayer, Transparency::ByLayer);
+1 -1
View File
@@ -104,7 +104,7 @@ impl ImageCommand {
let (upper_left, lower_right) =
corners_from_placement(ins, world_width, self.aspect());
let ole = build_embedded_ole(image, upper_left, lower_right);
self.plane.place_entity(EntityType::Ole2Frame(ole))
self.plane.place_entity(EntityType::Ole2Frame(Box::new(ole)))
}
}
}
+13 -13
View File
@@ -1439,7 +1439,7 @@ fn sync_dimension_base_points_and_collect_pending(doc: &mut CadDocument) -> Vec<
let EntityType::Dimension(dimension) = entity else {
return None;
};
let definition_point = match dimension {
let definition_point = match &**dimension {
Dimension::Linear(value) => Some(value.definition_point),
Dimension::Aligned(value) => Some(value.definition_point),
Dimension::Radius(value) => Some(value.definition_point),
@@ -1493,7 +1493,7 @@ pub fn bake_dimension_blocks(doc: &mut CadDocument) {
let dimensions: Vec<(Handle, Dimension)> = pending
.into_iter()
.filter_map(|handle| match doc.get_entity(handle) {
Some(EntityType::Dimension(d)) => Some((handle, d.clone())),
Some(EntityType::Dimension(d)) => Some((handle, (**d).clone())),
_ => None,
})
.collect();
@@ -1654,7 +1654,7 @@ mod tests {
d.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
// block_name is left empty — exactly what OCS-created dimensions carry.
let handle = doc
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(d))))
.unwrap();
bake_dimension_blocks(&mut doc);
@@ -1693,7 +1693,7 @@ mod tests {
// Exactly what OCS-created dimensions carry: insertion == text anchor.
d.base.insertion_point = d.base.text_middle_point;
let handle = doc
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(d))))
.unwrap();
bake_dimension_blocks(&mut doc);
@@ -1720,7 +1720,7 @@ mod tests {
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
d.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
let handle = doc
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(d))))
.unwrap();
bake_dimension_blocks(&mut doc);
@@ -1798,7 +1798,7 @@ mod tests {
d.rotation = 0.0;
d.definition_point = Vector3::new(0.0, 8.0, 0.0);
let handle = doc
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(d))))
.unwrap();
bake_dimension_blocks(&mut doc);
let name = match doc.get_entity(handle) {
@@ -1827,7 +1827,7 @@ mod tests {
let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
let handle = doc
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(d))))
.unwrap();
bake_dimension_blocks(&mut doc);
let name = match doc.get_entity(handle) {
@@ -1875,7 +1875,7 @@ mod tests {
.collect();
assert!(!expected.is_empty(), "the dimension explodes into no lines");
let mut turned = EntityType::Dimension(flat);
let mut turned = EntityType::Dimension(Box::new(flat));
crate::scene::view::dispatch::apply_transform(
&mut turned,
&EntityTransform::Rotate {
@@ -1911,7 +1911,7 @@ mod tests {
);
d.definition_point = Vector3::new(5.0, 5.0, 0.0); // arc location
let handle = doc
.add_entity(EntityType::Dimension(Dimension::Angular3Pt(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Angular3Pt(d))))
.unwrap();
bake_dimension_blocks(&mut doc);
let name = match doc.get_entity(handle) {
@@ -1940,7 +1940,7 @@ mod tests {
let mut d = DimensionDiameter::new(edge, far);
d.base.text_middle_point = Vector3::new(3.0, 9.0, 0.0);
let handle = doc
.add_entity(EntityType::Dimension(Dimension::Diameter(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Diameter(d))))
.unwrap();
bake_dimension_blocks(&mut doc);
let name = match doc.get_entity(handle) {
@@ -1971,7 +1971,7 @@ mod tests {
d.rotation = std::f64::consts::FRAC_PI_2; // 90°, vertical dimension line
d.definition_point = Vector3::new(8.0, 0.0, 0.0);
let handle = doc
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(d))))
.unwrap();
bake_dimension_blocks(&mut doc);
let name = match doc.get_entity(handle) {
@@ -2064,7 +2064,7 @@ mod tests {
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
d.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
doc.add_entity(EntityType::Dimension(Dimension::Linear(d)))
doc.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(d))))
.unwrap();
let path =
@@ -2171,7 +2171,7 @@ mod tests {
d.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
d.base.common.xdictionary_handle = Some(dict_handle);
let dim_handle = doc
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Linear(d))))
.unwrap();
bake_dimension_blocks(&mut doc);
+2 -2
View File
@@ -462,7 +462,7 @@ fn mtext_of_text(geom: &DgnGeom, color: u16) -> Option<EntityType> {
m.rectangle_width = 0.0;
m.drawing_direction = DrawingDirection::ByStyle;
m.style = "Standard".to_string();
Some(EntityType::MText(m))
Some(EntityType::MText(Box::new(m)))
}
/// A text node as one MTEXT, a paragraph a line, attached at the node's
@@ -515,7 +515,7 @@ fn mtext_of_node(
m.rectangle_width = 0.0;
m.drawing_direction = DrawingDirection::ByStyle;
m.style = style;
Some(EntityType::MText(m))
Some(EntityType::MText(Box::new(m)))
}
/// An arc, circle or ellipse in its own plane.
+2 -2
View File
@@ -1482,7 +1482,7 @@ pub fn convert(
let layer = naming.layer(pdf_layer, Kind::Text);
mtext.common.color = color_on(&layer, color_of(text.color));
mtext.common.layer = layer;
result.entities.push(EntityType::MText(mtext));
result.entities.push(EntityType::MText(Box::new(mtext)));
}
}
@@ -1712,7 +1712,7 @@ fn fill_entities(
return Vec::new();
}
hatch.elevation = z;
vec![EntityType::Hatch(hatch)]
vec![EntityType::Hatch(Box::new(hatch))]
}
inventory::submit!(crate::command::CommandRegistration {
+1 -1
View File
@@ -144,7 +144,7 @@ impl MvsetupCommand {
if self.arrangement == Arrangement::Engineering {
viewport.view_direction = directions[(row * 2 + column) as usize].clone();
}
viewports.push(EntityType::Viewport(viewport));
viewports.push(EntityType::Viewport(Box::new(viewport)));
}
}
CmdResult::CommitEntitiesAndExit(viewports)
+1 -1
View File
@@ -322,7 +322,7 @@ pub fn supports_annotation_context(entity: &EntityType) -> bool {
| EntityType::Tolerance(_)
| EntityType::Hatch(_) => true,
EntityType::Dimension(dimension) => {
!matches!(dimension, codec::entities::Dimension::Arc(_))
!matches!(&**dimension, codec::entities::Dimension::Arc(_))
}
_ => false,
}
+2 -2
View File
@@ -788,7 +788,7 @@ mod tests {
dimension.base.block_name = "SVC_TARGET".to_string();
scene
.document
.add_entity(EntityType::Dimension(Dimension::Aligned(dimension)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Aligned(dimension))))
.unwrap();
// A table's picture block and a block-valued cell, by handle.
@@ -894,7 +894,7 @@ mod tests {
dimension.base.block_name = "SVC_RENAMED".to_string();
scene
.document
.add_entity(EntityType::Dimension(Dimension::Aligned(dimension)))
.add_entity(EntityType::Dimension(Box::new(Dimension::Aligned(dimension))))
.unwrap();
let mut style = DimStyle::new("SVC_STYLE");
style.dimblk = target;
+1 -1
View File
@@ -747,7 +747,7 @@ fn tessellate_sub_local(
// Sanity guard: skip sub-entities whose primary dimension is so large
// that adaptive tessellation will explode into hundreds of millions
// of points. These are typically corrupt-radius primitives that slipped
// past purge_corrupt_entities (finite but absurd values).
// past the codec's unusable-geometry audit (finite but absurd values).
if is_unreasonable_extent(sub) {
return vec![];
}
+2 -2
View File
@@ -479,7 +479,7 @@ impl Scene {
self.previous_view = Some(ViewSnapshot::Floating {
layout,
handle,
viewport: Box::new(viewport.clone()),
viewport: viewport.clone(),
});
return;
}
@@ -538,7 +538,7 @@ impl Scene {
});
return false;
}
let current = Box::new(current.clone());
let current = current.clone();
if let Some(EntityType::Viewport(destination)) =
self.document.get_entity_mut(handle)
{
+26 -22
View File
@@ -869,7 +869,7 @@ pub(crate) fn constraint_from_associative_dimension(
Some(ParametricRef::whole(*reference.xrefs.first()?))
};
let measurement = DrivingValue::Literal(dimension.measurement());
match dimension {
match &**dimension {
Dimension::Aligned(_) => Some((
ConstraintKind::Distance,
vec![point(0)?, point(1)?],
@@ -921,7 +921,7 @@ pub(crate) fn radial_extension_points(
let EntityType::Dimension(dimension_entity) = document.get_entity(dimension)? else {
return None;
};
let target_point = match dimension_entity {
let target_point = match &**dimension_entity {
Dimension::Radius(radius) => radius.definition_point,
Dimension::Diameter(diameter) => diameter.angle_vertex,
Dimension::LargeRadial(radial) => radial.chord_point,
@@ -1009,7 +1009,7 @@ impl Scene {
else {
return;
};
let radial_data = match dimension_entity {
let radial_data = match &**dimension_entity {
Dimension::Radius(radius) => Some((
radius.angle_vertex,
radius.measurement(),
@@ -1223,7 +1223,7 @@ impl Scene {
};
// Radial references identify a circle/arc and angle, rather than a vertex.
let radial_dimension = matches!(
dimension_entity,
&**dimension_entity,
Dimension::Radius(_) | Dimension::Diameter(_) | Dimension::LargeRadial(_)
);
if radial_dimension {
@@ -1383,7 +1383,7 @@ impl Scene {
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension) else {
return Vec::new();
};
match entity {
match &**entity {
// A radial dimension's single reference describes the point where
// the leader meets the circle.
Dimension::Radius(radius) => vec![radius.definition_point],
@@ -1458,17 +1458,15 @@ impl Scene {
let measurement_supported = !association.trans_space
|| self.document.get_entity(dimension).is_some_and(|entity| {
matches!(
entity,
EntityType::Dimension(Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_))
crate::entities::dimension::as_dimension(entity),
Some(Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_))
) || persisted_measurement_scale(&self.document, entity).is_some()
});
let measurement_supported = measurement_supported
&& self.document.get_entity(dimension).is_some_and(|entity| {
if !matches!(
entity,
EntityType::Dimension(
Dimension::Radius(_) | Dimension::Diameter(_) | Dimension::LargeRadial(_)
)
crate::entities::dimension::as_dimension(entity),
Some(Dimension::Radius(_) | Dimension::Diameter(_) | Dimension::LargeRadial(_))
) {
return true;
}
@@ -1652,7 +1650,7 @@ impl Scene {
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension) else {
return Vec::new();
};
let radial_data = match entity {
let radial_data = match &**entity {
Dimension::Radius(radius) => Some((
radius.angle_vertex,
radius.measurement(),
@@ -1850,8 +1848,8 @@ impl Scene {
continue;
};
let angular = matches!(
&before,
EntityType::Dimension(Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_))
crate::entities::dimension::as_dimension(&before),
Some(Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_))
);
let measurement_scale = if let Some(frame) = map.frame.filter(|_| !angular) {
let Some(mut scale) = persisted_measurement_scale(&self.document, &before) else {
@@ -1874,7 +1872,7 @@ impl Scene {
else {
continue;
};
match dimension {
match &mut **dimension {
Dimension::Linear(linear) => {
// The sheet annotation the user placed — the dimension
// line's standoff and, when they dragged it, the text — is
@@ -2104,7 +2102,7 @@ impl Scene {
arc.base.actual_measurement = arc.measurement();
}
}
let measurement = match &*dimension {
let measurement = match &**dimension {
Dimension::Linear(linear) => linear_measurement(linear),
_ => dimension.measurement(),
};
@@ -2155,8 +2153,12 @@ impl Scene {
refreshed
}
pub(crate) fn sync_diameter_association_angle(&mut self, dimension: Handle) {
let chord = match self.document.get_entity(dimension) {
Some(EntityType::Dimension(Dimension::Diameter(diameter))) => diameter.angle_vertex,
let chord = match self
.document
.get_entity(dimension)
.and_then(crate::entities::dimension::as_dimension)
{
Some(Dimension::Diameter(diameter)) => diameter.angle_vertex,
_ => return,
};
@@ -2247,9 +2249,9 @@ mod tests {
Vector3::ZERO,
5.0,
)));
let dimension = scene.add_entity(EntityType::Dimension(Dimension::Diameter(
let dimension = scene.add_entity(EntityType::Dimension(Box::new(Dimension::Diameter(
DimensionDiameter::new(Vector3::new(5.0, 0.0, 0.0), Vector3::new(-5.0, 0.0, 0.0)),
)));
))));
scene.attach_dimension_association(dimension, vec![Some(circle)]);
let association = scene
.document
@@ -2270,8 +2272,10 @@ mod tests {
.expect("diameter association");
scene.begin_undo_recording();
let Some(EntityType::Dimension(Dimension::Diameter(diameter))) =
scene.document.get_entity_mut(dimension)
let Some(Dimension::Diameter(diameter)) = scene
.document
.get_entity_mut(dimension)
.and_then(crate::entities::dimension::as_dimension_mut)
else {
panic!("diameter dimension");
};
+1 -1
View File
@@ -3206,7 +3206,7 @@ impl Scene {
if let Some(origin) = model.pattern_origin {
dxf.record_pattern_origin(Vector2::new(origin[0], origin[1]));
}
let mut entity = EntityType::Hatch(dxf);
let mut entity = EntityType::Hatch(Box::new(dxf));
if let Some(layer) = layer {
entity.as_entity_mut().set_layer(layer.to_string());
+3 -3
View File
@@ -64,10 +64,10 @@ impl Scene {
if let Some(EntityType::Viewport(sheet)) = self.document.get_entity(from.viewport).cloned()
{
if let Some(EntityType::Viewport(target)) = self.document.get_entity_mut(to.viewport) {
*target = codec::entities::Viewport {
*target = Box::new(codec::entities::Viewport {
common: target.common.clone(),
..sheet
};
..*sheet
});
}
}
self.unopened_layouts.remove(name);
+5 -13
View File
@@ -1221,12 +1221,6 @@ pub struct DerivedCaches {
/// Prepared during file open so entity selection/deselection never scans
/// the complete object store on the UI thread.
pub object_data: crate::entities::object_data::ObjectDataCache,
/// Number of entities removed by the corrupt-entity guard during load.
/// Reported back to the UI so the user knows when a file had parser-junk
/// entities silently dropped.
pub corrupt_dropped: usize,
/// Corrupt entities dropped while resolving referenced drawings.
pub xref_dropped: usize,
/// XREF resolution results produced by the loader worker. Keeping these in
/// the open bundle prevents parsing and merging references on the UI thread.
pub xrefs: Vec<crate::io::xref::XrefInfo>,
@@ -1523,8 +1517,6 @@ fn build_derived_caches_impl(
meshes,
block_meshes,
object_data,
corrupt_dropped: 0,
xref_dropped: 0,
xrefs: Vec::new(),
prepared_geometry: None,
timings: OpenTimings::default(),
@@ -4956,7 +4948,7 @@ impl Scene {
fit_sheet_viewport(&mut vp, min_lim, max_lim);
if let Ok(handle) = self
.document
.add_entity_to_layout(EntityType::Viewport(vp), layout_name)
.add_entity_to_layout(EntityType::Viewport(Box::new(vp)), layout_name)
{
if let Some(ObjectType::Layout(l)) = self.document.objects.get_mut(&layout_handle) {
l.viewports.retain(|candidate| *candidate != handle);
@@ -5027,7 +5019,7 @@ impl Scene {
viewport.status = codec::entities::ViewportStatusFlags::default_on();
if let Ok(handle) = self
.document
.add_entity_to_layout(EntityType::Viewport(viewport), layout_name)
.add_entity_to_layout(EntityType::Viewport(Box::new(viewport)), layout_name)
{
// The reference zooms the new viewport to the model's extents
// with a margin of under one percent.
@@ -13802,7 +13794,7 @@ mod journal_tests {
let mut annotative = MText::new();
annotative.value = "annotative".to_string();
annotative.is_annotative = true;
let annotative = s.add_entity(EntityType::MText(annotative));
let annotative = s.add_entity(EntityType::MText(Box::new(annotative)));
let before_points = s.geometry_epoch;
s.invalidate_point_dependencies();
@@ -14117,7 +14109,7 @@ mod journal_tests {
scene.document.layers.add_or_replace(Layer::new("Shadows"));
let mut hatch = Hatch::new();
hatch.common.layer = "Shadows".to_string();
let handle = scene.add_entity(EntityType::Hatch(hatch));
let handle = scene.add_entity(EntityType::Hatch(Box::new(hatch)));
let model = scene.model_space_block_handle();
assert_eq!(
scene
@@ -15447,7 +15439,7 @@ mod layout_cache_tests {
vp.height = 100.0;
let handle = scene
.document
.add_entity_to_layout(codec::EntityType::Viewport(vp), "P1")
.add_entity_to_layout(codec::EntityType::Viewport(Box::new(vp)), "P1")
.unwrap();
assert!(scene.set_viewport_scale_named_for(handle, "1:50").is_some());
assert!(scene.active_viewport.is_none());
+2 -2
View File
@@ -285,7 +285,7 @@ mod viewport_zoom_tests {
viewport.width = 100.0;
viewport.height = 100.0;
viewport.view_height = 200.0;
let handle = scene.add_entity(EntityType::Viewport(viewport));
let handle = scene.add_entity(EntityType::Viewport(Box::new(viewport)));
scene.active_viewport = Some(handle);
let view_height = |scene: &Scene| match scene.document.get_entity(handle) {
@@ -337,7 +337,7 @@ mod clip_tests {
viewport.height = 20.0;
viewport.clip_boundary_handle = clip;
viewport.status.non_rectangular_clipping = true;
let viewport = scene.add_entity(EntityType::Viewport(viewport));
let viewport = scene.add_entity(EntityType::Viewport(Box::new(viewport)));
for (x, y, inside) in [
(2.0, 2.0, true),
(8.0, 2.0, true),
+15 -11
View File
@@ -696,7 +696,7 @@ fn dynamic_dimension_follow_points(
target.attachment_point = base.attachment_point;
target.line_spacing_style = base.line_spacing_style;
target.line_spacing_factor = base.line_spacing_factor;
*dimension = rebuilt;
*dimension = *rebuilt;
true
}
@@ -794,7 +794,7 @@ fn dynamic_dimension_follow_radial(
let Some(codec::EntityType::Dimension(mut rebuilt)) = rebuilt else {
return false;
};
let unchanged = match (&*dimension, &rebuilt) {
let unchanged = match (&*dimension, &*rebuilt) {
(Dimension::Radius(old), Dimension::Radius(new)) => {
same(old.angle_vertex, new.angle_vertex)
&& same(old.definition_point, new.definition_point)
@@ -808,7 +808,7 @@ fn dynamic_dimension_follow_radial(
if unchanged {
return false;
}
match &mut rebuilt {
match &mut *rebuilt {
Dimension::Radius(new) => new.leader_length = leader,
Dimension::Diameter(new) => new.leader_length = leader,
_ => {}
@@ -821,7 +821,7 @@ fn dynamic_dimension_follow_radial(
target.attachment_point = base.attachment_point;
target.line_spacing_style = base.line_spacing_style;
target.line_spacing_factor = base.line_spacing_factor;
*dimension = rebuilt;
*dimension = *rebuilt;
true
}
@@ -881,7 +881,7 @@ fn dynamic_dimension_follow_angle(
target.attachment_point = base.attachment_point;
target.line_spacing_style = base.line_spacing_style;
target.line_spacing_factor = base.line_spacing_factor;
*dimension = rebuilt;
*dimension = *rebuilt;
true
}
@@ -2264,10 +2264,14 @@ impl super::Scene {
});
let measured = points.filter(|_| !angular).map(|(first, second)| {
let delta = second - first;
let axis = match self.document.get_entity(*dimension) {
Some(codec::EntityType::Dimension(codec::entities::Dimension::Linear(
linear,
))) => Some((linear.rotation.cos(), linear.rotation.sin())),
let axis = match self
.document
.get_entity(*dimension)
.and_then(crate::entities::dimension::as_dimension)
{
Some(codec::entities::Dimension::Linear(linear)) => {
Some((linear.rotation.cos(), linear.rotation.sin()))
}
_ => None,
};
match (constraint.kind, axis) {
@@ -2605,7 +2609,7 @@ impl super::Scene {
// line, which breaks around the text, so the text is centred on
// it rather than lifted above it.
let radial = matches!(
dimension,
&**dimension,
codec::entities::Dimension::Radius(_) | codec::entities::Dimension::Diameter(_)
);
let centred = !radial || ov::int(xdata, ov::DIMTAD) == Some(0);
@@ -2615,7 +2619,7 @@ impl super::Scene {
// The reference draws dynamic distance text horizontally; an
// angle's text keeps its style's alignment.
let angular = matches!(
dimension,
&**dimension,
codec::entities::Dimension::Angular2Ln(_)
| codec::entities::Dimension::Angular3Pt(_)
);
+2 -2
View File
@@ -16,7 +16,7 @@ impl Scene {
let viewports: Vec<&Viewport> = viewport_handles
.iter()
.filter_map(|handle| match self.document.get_entity(*handle) {
Some(EntityType::Viewport(vp)) => Some(vp),
Some(EntityType::Viewport(vp)) => Some(&**vp),
_ => None,
})
.filter(|vp| {
@@ -446,7 +446,7 @@ impl Scene {
Some(EntityType::Viewport(viewport))
if viewport.common.owner_handle == paper_block && viewport.is_on() =>
{
Some(viewport)
Some(&**viewport)
}
_ => None,
})
+1 -1
View File
@@ -152,7 +152,7 @@ impl ObjectKind {
EntityType::Text(e::Text::with_value("Text", origin).with_height(2.5))
}
ObjectKind::MText => {
EntityType::MText(e::MText::with_value("MText", origin).with_height(2.5))
EntityType::MText(Box::new(e::MText::with_value("MText", origin).with_height(2.5)))
}
}
}
-14
View File
@@ -62,10 +62,6 @@ pub fn view_window<'a>(
crate::tr!("recovery", "issues-found"),
report.issues_found().to_string(),
),
metric(
crate::tr!("recovery", "entities-removed"),
report.removed_total().to_string(),
),
metric(
crate::tr!("recovery", "references-checked"),
report.references_checked.to_string(),
@@ -75,16 +71,6 @@ pub fn view_window<'a>(
.width(Fill);
let mut details = column![].spacing(6);
if report.referenced_entities_removed > 0 {
details = details.push(
text(format!(
"{}: {}",
crate::tr!("recovery", "referenced-entities-removed"),
report.referenced_entities_removed
))
.size(11),
);
}
if report.references_missing > 0 || report.references_failed > 0 {
details = details.push(
text(format!(