diff --git a/crates/ocs_plugin_api/build.rs b/crates/ocs_plugin_api/build.rs index 388c31471..83eff3320 100644 --- a/crates/ocs_plugin_api/build.rs +++ b/crates/ocs_plugin_api/build.rs @@ -545,6 +545,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) { for field in &common.fields { let (name, access) = match field.name.as_str() { "handle" => ("handle".to_owned(), ModelAccess::ReadOnly), + "owner_handle" => ("owner_handle".to_owned(), ModelAccess::ReadOnly), "layer" if scope == EntityScope::Canvas => { unresolved.remove("layer"); ("layer".to_owned(), ModelAccess::ReadWrite) @@ -588,7 +589,13 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) { "dimension_style_name" | "text_height" | "dimension_gap") | ("Shape", "insertion_point" | "size" | "shape_name" | "shape_number" | "rotation" | "relative_x_scale" | "oblique_angle" | "normal" | - "thickness" | "style_name")) { + "thickness" | "style_name") | + ("AttributeDefinition", "tag" | "prompt" | "default_value" | + "insertion_point" | "alignment_point" | "height" | "rotation" | + "width_factor" | "oblique_angle" | "text_style" | + "text_generation_flags" | "horizontal_alignment" | + "vertical_alignment" | "flags" | "field_length" | "normal" | + "mtext_flag" | "is_multiline" | "line_count" | "lock_position")) { "transaction_geometry" } else { "type_conversion_only" }; properties.push(PropertyCoverage { diff --git a/crates/ocs_plugin_api/entity_coverage_policy.json b/crates/ocs_plugin_api/entity_coverage_policy.json index f0d4fb537..e4f372ee4 100644 --- a/crates/ocs_plugin_api/entity_coverage_policy.json +++ b/crates/ocs_plugin_api/entity_coverage_policy.json @@ -20,7 +20,8 @@ "Face3D": ["layer", "first_corner", "second_corner", "third_corner", "fourth_corner", "invisible_edges"], "Insert": ["layer", "insert_point", "x_scale", "y_scale", "z_scale", "rotation", "normal", "column_count", "row_count", "column_spacing", "row_spacing"], "Tolerance": ["layer", "insertion_point", "direction", "normal", "text", "dimension_style_name", "text_height", "dimension_gap"], - "Shape": ["layer", "insertion_point", "size", "shape_name", "shape_number", "rotation", "relative_x_scale", "oblique_angle", "normal", "thickness", "style_name"] + "Shape": ["layer", "insertion_point", "size", "shape_name", "shape_number", "rotation", "relative_x_scale", "oblique_angle", "normal", "thickness", "style_name"], + "AttributeDefinition": ["layer", "tag", "prompt", "default_value", "insertion_point", "alignment_point", "height", "rotation", "width_factor", "oblique_angle", "text_style", "text_generation_flags", "horizontal_alignment", "vertical_alignment", "flags", "field_length", "normal", "mtext_flag", "is_multiline", "line_count", "lock_position"] }, "readable": { "Insert": ["block_name"], diff --git a/crates/ocs_plugin_api/src/entity_coverage.rs b/crates/ocs_plugin_api/src/entity_coverage.rs index fc591e7dd..17a52851b 100644 --- a/crates/ocs_plugin_api/src/entity_coverage.rs +++ b/crates/ocs_plugin_api/src/entity_coverage.rs @@ -106,6 +106,25 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<() if !value.thickness.is_finite() { return Err("Shape.thickness must be finite".into()); } if value.style_name.trim().is_empty() { return Err("Shape.style_name is empty".into()); } } + EntityType::AttributeDefinition(value) => { + finite_vector("AttributeDefinition.insertion_point", &value.insertion_point)?; + finite_vector("AttributeDefinition.alignment_point", &value.alignment_point)?; + solid_normal("AttributeDefinition.normal", &value.normal)?; + if value.tag.trim().is_empty() || value.tag.chars().any(char::is_whitespace) { + return Err("AttributeDefinition.tag must be nonempty and contain no whitespace".into()); + } + if !value.height.is_finite() || value.height <= 0.0 { return Err("AttributeDefinition.height must be finite and greater than zero".into()); } + if !value.rotation.is_finite() { return Err("AttributeDefinition.rotation must be finite".into()); } + if !value.width_factor.is_finite() || value.width_factor.abs() < 1e-12 { + return Err("AttributeDefinition.width_factor must be finite and nonzero".into()); + } + if !value.oblique_angle.is_finite() || value.oblique_angle.abs() >= std::f64::consts::FRAC_PI_2 { + return Err("AttributeDefinition.oblique_angle must be finite and between -PI/2 and PI/2".into()); + } + if value.text_style.trim().is_empty() { return Err("AttributeDefinition.text_style is empty".into()); } + if value.field_length < 0 { return Err("AttributeDefinition.field_length must be nonnegative".into()); } + if value.line_count < 1 { return Err("AttributeDefinition.line_count must be greater than zero".into()); } + } _ => {} } Ok(()) @@ -276,6 +295,45 @@ pub fn validate_entity_mutation( return Err("Shape.style_handle is read-only".into()); } } + (EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(new)) => { + changed3("AttributeDefinition.insertion_point", &old.insertion_point, &new.insertion_point)?; + changed3("AttributeDefinition.alignment_point", &old.alignment_point, &new.alignment_point)?; + if !same3(&old.normal, &new.normal) { solid_normal("AttributeDefinition.normal", &new.normal)?; } + if old.tag != new.tag && (new.tag.trim().is_empty() || new.tag.chars().any(char::is_whitespace)) { + return Err("AttributeDefinition.tag must be nonempty and contain no whitespace".into()); + } + if old.height.to_bits() != new.height.to_bits() && (!new.height.is_finite() || new.height <= 0.0) { + return Err("AttributeDefinition.height must be finite and greater than zero".into()); + } + for (name, before, after) in [ + ("rotation", old.rotation, new.rotation), + ("oblique_angle", old.oblique_angle, new.oblique_angle), + ] { + if before.to_bits() != after.to_bits() && !after.is_finite() { + return Err(format!("AttributeDefinition.{name} must be finite")); + } + } + if old.width_factor.to_bits() != new.width_factor.to_bits() + && (!new.width_factor.is_finite() || new.width_factor.abs() < 1e-12) { + return Err("AttributeDefinition.width_factor must be finite and nonzero".into()); + } + if old.oblique_angle.to_bits() != new.oblique_angle.to_bits() + && new.oblique_angle.abs() >= std::f64::consts::FRAC_PI_2 { + return Err("AttributeDefinition.oblique_angle must be between -PI/2 and PI/2".into()); + } + if old.text_style != new.text_style && new.text_style.trim().is_empty() { + return Err("AttributeDefinition.text_style is empty".into()); + } + if old.field_length != new.field_length && new.field_length < 0 { + return Err("AttributeDefinition.field_length must be nonnegative".into()); + } + if old.line_count != new.line_count && new.line_count < 1 { + return Err("AttributeDefinition.line_count must be greater than zero".into()); + } + if old.embedded_mtext != new.embedded_mtext { + return Err("AttributeDefinition.embedded_mtext is unmapped and read-only".into()); + } + } _ => {} } Ok(()) @@ -289,6 +347,15 @@ pub fn validate_canvas_entity_references( entity: &crate::host::EntityType, ) -> Result<(), String> { use crate::host::EntityType; + let owner = entity.common().owner_handle; + if !owner.is_null() { + let block_owner = document.block_records.iter().any(|record| record.handle == owner); + let insert_owner = matches!(entity, EntityType::AttributeEntity(_)) + && matches!(document.get_entity(owner), Some(EntityType::Insert(_))); + if !block_owner && !insert_owner { + return Err(format!("entity owner {owner:?} does not exist")); + } + } if let EntityType::Tolerance(value) = entity { let found = value.dimension_style_handle.filter(|handle| !handle.is_null()).map_or_else( || document.dim_styles.iter().any(|style| style.name.eq_ignore_ascii_case(value.dimension_style_name.trim())), @@ -304,6 +371,19 @@ pub fn validate_canvas_entity_references( if !style.is_shape_file { return Err(format!("Shape text style {:?} is not a shape-file style", style.name)); } if style.font_file.trim().is_empty() { return Err(format!("Shape text style {:?} has no SHX file", style.name)); } } + if let EntityType::AttributeDefinition(value) = entity { + let owner = entity.common().owner_handle; + let block = document.block_records.iter().find(|record| record.handle == owner) + .ok_or_else(|| "AttributeDefinition requires an existing block-record owner".to_owned())?; + if block.is_model_space() || block.is_paper_space() { + return Err("AttributeDefinition owner must be a block definition".into()); + } + let style = document.text_styles.get(value.text_style.trim()) + .ok_or_else(|| format!("AttributeDefinition text style {:?} does not exist", value.text_style))?; + if style.is_shape_file { + return Err(format!("AttributeDefinition text style {:?} is a shape-file style", style.name)); + } + } Ok(()) } @@ -364,6 +444,7 @@ mod tests { assert!(variants.iter().any(|v| v.name == entry.kind)); assert!(entry.properties.iter().any(|p| p.name == "handle" && p.model_access == ModelAccess::ReadOnly)); assert!(entry.properties.iter().any(|p| p.name == "kind" && p.model_access == ModelAccess::ReadOnly)); + assert!(entry.properties.iter().any(|p| p.name == "owner_handle" && p.model_access == ModelAccess::ReadOnly)); let mut source_paths = std::collections::HashSet::new(); for property in &entry.properties { assert!(source_paths.insert(&property.source_path), "duplicate source path in {}: {}", entry.kind, property.source_path); @@ -382,7 +463,8 @@ mod tests { if entry.scope != EntityScope::Canvas || !matches!(entry.kind.as_str(), "Point" | "Line" | "Circle" | "Arc" | "Ellipse" | "Polyline" | "Polyline2D" | "Polyline3D" | "LwPolyline" | "Spline" | "Text" | "MText" | - "Ray" | "XLine" | "Solid" | "Face3D" | "Insert" | "Tolerance" | "Shape") { + "Ray" | "XLine" | "Solid" | "Face3D" | "Insert" | "Tolerance" | "Shape" | + "AttributeDefinition") { assert!(entry.properties.iter().all(|p| p.model_access != ModelAccess::ReadWrite || (entry.scope == EntityScope::Canvas && p.name == "layer")), "{}", entry.kind); } @@ -423,6 +505,21 @@ mod tests { "Shape.rotation".to_owned(), "Shape.relative_x_scale".to_owned(), "Shape.oblique_angle".to_owned(), "Shape.normal".to_owned(), "Shape.thickness".to_owned(), "Shape.style_name".to_owned(), + "AttributeDefinition.tag".to_owned(), "AttributeDefinition.prompt".to_owned(), + "AttributeDefinition.default_value".to_owned(), + "AttributeDefinition.insertion_point".to_owned(), + "AttributeDefinition.alignment_point".to_owned(), + "AttributeDefinition.height".to_owned(), "AttributeDefinition.rotation".to_owned(), + "AttributeDefinition.width_factor".to_owned(), + "AttributeDefinition.oblique_angle".to_owned(), + "AttributeDefinition.text_style".to_owned(), + "AttributeDefinition.text_generation_flags".to_owned(), + "AttributeDefinition.horizontal_alignment".to_owned(), + "AttributeDefinition.vertical_alignment".to_owned(), + "AttributeDefinition.flags".to_owned(), "AttributeDefinition.field_length".to_owned(), + "AttributeDefinition.normal".to_owned(), "AttributeDefinition.mtext_flag".to_owned(), + "AttributeDefinition.is_multiline".to_owned(), "AttributeDefinition.line_count".to_owned(), + "AttributeDefinition.lock_position".to_owned(), ])); } @@ -593,4 +690,50 @@ mod tests { assert_eq!(patched, expected); assert!(patch_canvas_layer(&hatch, " ").is_err()); } + + #[cfg(feature = "host")] + #[test] + fn attribute_definition_requires_a_block_owner_and_text_style() { + use crate::host::acadrust::{self, entities::{Block, BlockEnd}, types::{Handle, Vector3}}; + let mut document = acadrust::CadDocument::new(); + let next = document.next_handle(); + let record_handle = Handle::new(next); + let block_handle = Handle::new(next + 1); + let end_handle = Handle::new(next + 2); + let mut record = acadrust::tables::BlockRecord::new("TAGBLOCK"); + record.handle = record_handle; + record.block_entity_handle = block_handle; + record.block_end_handle = end_handle; + document.block_records.add(record).unwrap(); + let mut block = Block::new("TAGBLOCK", Vector3::ZERO); + block.common.handle = block_handle; + block.common.owner_handle = record_handle; + document.add_entity(acadrust::EntityType::Block(block)).unwrap(); + let mut end = BlockEnd::new(); + end.common.handle = end_handle; + end.common.owner_handle = record_handle; + document.add_entity(acadrust::EntityType::BlockEnd(end)).unwrap(); + + let mut definition = acadrust::entities::AttributeDefinition::new( + "PART_NO".into(), "Part number".into(), "PN-001".into()); + definition.common.owner_handle = record_handle; + definition.insertion_point = Vector3::new(1.0, 2.0, 0.0); + let entity = acadrust::EntityType::AttributeDefinition(definition.clone()); + validate_new_canvas_entity(&entity).unwrap(); + validate_canvas_entity_references(&document, &entity).unwrap(); + + definition.common.owner_handle = Handle::NULL; + assert!(validate_canvas_entity_references( + &document, &acadrust::EntityType::AttributeDefinition(definition.clone())) + .unwrap_err().contains("block-record owner")); + definition.common.owner_handle = record_handle; + definition.text_style = "Missing".into(); + assert!(validate_canvas_entity_references( + &document, &acadrust::EntityType::AttributeDefinition(definition.clone())) + .unwrap_err().contains("does not exist")); + definition.text_style = "Standard".into(); + definition.tag = "BAD TAG".into(); + assert!(validate_new_canvas_entity(&acadrust::EntityType::AttributeDefinition(definition)) + .unwrap_err().contains("no whitespace")); + } } diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index 3b481586c..17984346f 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -1019,6 +1019,174 @@ mod tests { assert!(app.tabs[0].scene.document.get_entity(handle).is_none()); } + #[test] + fn staged_python_attribute_definition_lifecycle_over_real_ipc() { + let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else { return; }; + let runner_path = std::env::var_os("OCS_PLUGIN_RUNNER_EXE") + .expect("set OCS_PLUGIN_RUNNER_EXE to the built OpenCADStudio executable"); + assert!(std::path::Path::new(&runner_path).is_file()); + + let mut app = OpenCADStudio::new_for_test(); + app.tabs[0].is_start = false; + let mut host = HostSession::new(&mut app, 0); + let next = host.document().next_handle(); + let block_record_handle = Handle::new(next); + let block_handle = Handle::new(next + 1); + let block_end_handle = Handle::new(next + 2); + let mut block_record = acadrust::tables::BlockRecord::new("TAGBLOCK"); + block_record.handle = block_record_handle; + block_record.block_entity_handle = block_handle; + block_record.block_end_handle = block_end_handle; + host.document_mut().block_records.add(block_record).unwrap(); + let mut block = acadrust::entities::Block::new( + "TAGBLOCK", acadrust::types::Vector3::ZERO); + block.common.handle = block_handle; + block.common.owner_handle = block_record_handle; + host.document_mut().add_entity(EntityType::Block(block)).unwrap(); + let mut block_end = acadrust::entities::BlockEnd::new(); + block_end.common.handle = block_end_handle; + block_end.common.owner_handle = block_record_handle; + host.document_mut().add_entity(EntityType::BlockEnd(block_end)).unwrap(); + let insert = acadrust::entities::Insert::new( + "TAGBLOCK", acadrust::types::Vector3::new(10.0, 0.0, 0.0)); + let insert_handle = host.document_mut().add_entity(EntityType::Insert(insert)).unwrap(); + + let process = ocs_plugin_api::process::PluginProcess::spawn( + std::path::Path::new(&plugin_path), &mut host, + crate::plugin::v4_support::notification_handler(), + ).expect("spawn staged Python plugin"); + let dispatch = |host: &mut HostSession<'_>, command: &str| { + assert!(process.dispatch(host, command, &mut |_| {}).expect("Python dispatch")); + }; + dispatch(&mut host, &format!(concat!( + "PY_EVAL ocs.active_document.create_entity('AttributeDefinition',", + "owner_handle={},tag='PART_NO',prompt='Part number',default_value='PN-001',", + "insertion_point={{'x':1.0,'y':2.0,'z':0.0}},height=2.5).handle" + ), block_record_handle.value())); + let handle = host.document().entities() + .find(|entity| matches!(entity, EntityType::AttributeDefinition(_))) + .expect("Python created an AttributeDefinition").common().handle; + let Some(EntityType::AttributeDefinition(created)) = host.document().get_entity(handle) else { + panic!("expected AttributeDefinition"); + }; + assert_eq!(created.common.owner_handle, block_record_handle); + assert_eq!(created.tag, "PART_NO"); + assert_eq!(created.text_style, "Standard"); + assert!(host.document().block_records.get("TAGBLOCK").unwrap() + .entity_handles.contains(&handle)); + let created = created.clone(); + dispatch(&mut host, &format!( + "PY_EVAL (ocs.active_document.entities[{}].tag, ocs.active_document.entities[{}].owner_handle)", + handle.value(), handle.value())); + let readback = &host.app.command_line.history.last().unwrap().text; + assert!(readback.contains("PART_NO") && readback.contains(&block_record_handle.value().to_string())); + + let script_path = std::env::temp_dir().join(format!( + "ocs_attribute_definition_document_model_{}.py", std::process::id())); + std::fs::write(&script_path, format!(concat!( + "doc = ocs.active_document\n", + "definition = doc.entities[{}]\n", + "with doc.transaction('Edit attribute definition'):\n", + " definition.insertion_point = (4.0, 5.0, 0.0)\n", + " definition.default_value = 'PN-002'\n", + " definition.rotation = 0.25\n", + " definition.width_factor = 1.25\n", + "doc.selection = [definition]\n" + ), handle.value())).unwrap(); + dispatch(&mut host, &format!("PY_RUN {}", script_path.display())); + let _ = std::fs::remove_file(&script_path); + let expected = host.document().get_entity(handle).unwrap().clone(); + let EntityType::AttributeDefinition(edited) = &expected else { unreachable!() }; + assert_eq!(edited.insertion_point, acadrust::types::Vector3::new(4.0, 5.0, 0.0)); + assert_eq!(edited.default_value, "PN-002"); + assert_eq!(edited.rotation, 0.25); + assert_eq!(edited.width_factor, 1.25); + assert_eq!(edited.common, created.common); + assert_eq!(edited.tag, created.tag); + assert_eq!(edited.prompt, created.prompt); + assert_eq!(edited.text_style, created.text_style); + assert_eq!(edited.flags, created.flags); + assert_eq!(edited.embedded_mtext, created.embedded_mtext); + assert_eq!(host.selection(), vec![handle]); + assert!(matches!(host.document().get_entity(insert_handle), Some(EntityType::Insert(value)) + if value.block_name == "TAGBLOCK")); + + let render = edited.to_render(host.document()).expect("attribute definition render"); + let crate::scene::convert::acad_to_render::RenderObject::Text(strokes) = render.object else { + panic!("expected attribute definition text"); + }; + assert!(!strokes.is_empty()); + assert!(strokes.iter().any(|stroke| stroke.run.as_ref() + .is_some_and(|run| run.text.contains("PN-002")))); + + for (label, patch, message) in [ + ("Reject tag", "'tag':'BAD TAG'", "whitespace"), + ("Reject height", "'height':0.0", "greater than zero"), + ("Reject style", "'text_style':'Missing'", "does not exist"), + ("Reject owner", "'owner_handle':999999", "read-only"), + ] { + dispatch(&mut host, &format!( + "PY_EVAL ocs.update_many({label:?}, [{{'handle':{}, {patch}}}])", handle.value())); + assert_eq!(host.document().get_entity(handle), Some(&expected)); + assert!(host.app.command_line.history.last().unwrap().text.contains(message)); + } + + let assert_persisted = |label: &str, document: &CadDocument, full_text_state: bool| { + let actual = document.get_entity(handle); + assert!(matches!(actual, Some(EntityType::AttributeDefinition(value)) + if value.common.owner_handle == block_record_handle + && value.tag == "PART_NO" && value.default_value == "PN-002" + && value.insertion_point == acadrust::types::Vector3::new(4.0, 5.0, 0.0) + && (value.rotation - 0.25).abs() < 1e-12), "{label}: {actual:?}"); + if full_text_state { + assert!(matches!(actual, Some(EntityType::AttributeDefinition(value)) + if value.width_factor == 1.25 && value.text_style == "Standard" + && value.prompt == "Part number"), "{label}: {actual:?}"); + } + assert!(document.block_records.get("TAGBLOCK").unwrap().entity_handles.contains(&handle)); + assert!(matches!(document.get_entity(insert_handle), Some(EntityType::Insert(value)) + if value.block_name == "TAGBLOCK")); + }; + let dwg_bytes = acadrust::DwgWriter::write_to_vec(host.document()).unwrap(); + let dwg_doc = crate::io::load_bytes("attribute-definition.dwg", dwg_bytes).unwrap(); + assert_persisted("DWG", &dwg_doc, true); + let dxf_bytes = acadrust::DxfWriter::new(host.document()).write_to_vec().unwrap(); + let dxf_doc = crate::io::load_bytes("attribute-definition.dxf", dxf_bytes).unwrap(); + // The pinned cadcodec DXF ATTDEF reader retains identity, placement, + // height, value, prompt and rotation, but currently drops several + // optional AcDbText fields such as width factor. Keep that external + // codec gap explicit in the coverage ledger rather than claiming a + // complete DXF property round trip here. + assert_persisted("DXF", &dxf_doc, false); + + dispatch(&mut host, &format!( + "PY_EVAL ocs.active_document.delete_entity({})", handle.value())); + assert!(host.document().get_entity(handle).is_none()); + // CadDocument intentionally leaves the block membership slot in place + // so delta undo can restore the shared entity without rewriting the + // record. Writers must still omit the absent entity. + let deleted_dwg = acadrust::DwgWriter::write_to_vec(host.document()).unwrap(); + assert!(crate::io::load_bytes("attribute-definition-deleted.dwg", deleted_dwg) + .unwrap().get_entity(handle).is_none()); + let deleted_dxf = acadrust::DxfWriter::new(host.document()).write_to_vec().unwrap(); + assert!(crate::io::load_bytes("attribute-definition-deleted.dxf", deleted_dxf) + .unwrap().get_entity(handle).is_none()); + drop(process); + drop(host); + app.finish_pending_history(0); + assert_eq!(app.tabs[0].history.undo_stack.len(), 3); + app.undo_steps(1); + assert_eq!(app.tabs[0].scene.document.get_entity(handle), Some(&expected)); + app.undo_steps(1); + assert!(matches!(app.tabs[0].scene.document.get_entity(handle), + Some(EntityType::AttributeDefinition(value)) + if value.default_value == "PN-001" && value.insertion_point.x == 1.0)); + app.undo_steps(1); + assert!(app.tabs[0].scene.document.get_entity(handle).is_none()); + app.redo_steps(3); + assert!(app.tabs[0].scene.document.get_entity(handle).is_none()); + } + #[test] fn staged_python_point_pick_and_cancel_over_real_ipc() { let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else { return; }; diff --git a/src/io/mod.rs b/src/io/mod.rs index 04e8de258..8fab9dda2 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -2174,21 +2174,16 @@ fn fix_viewport_status_flags(doc: &mut CadDocument) { } } -/// The acadrust DXF reader stores several rotation fields directly from DXF -/// group code 50 in degrees, while DWG and our own creation code store radians. -/// Apply to_radians() on load so tessellation can call cos/sin uniformly. +/// The acadrust DXF reader still stores Shape rotation directly from group code +/// 50 in degrees, while DWG and our own creation code store radians. Attribute +/// entities and definitions are converted by the reader itself and must not be +/// converted a second time here. fn fix_dxf_dimension_rotations(doc: &mut CadDocument) { for entity in doc.entities_mut() { match entity { // Dimension angles (rotation / text / oblique) are converted // degrees->radians inside the acadrust DXF reader now, so a // dimension arm here would double-convert. - EntityType::AttributeDefinition(a) => { - a.rotation = a.rotation.to_radians(); - } - EntityType::AttributeEntity(a) => { - a.rotation = a.rotation.to_radians(); - } EntityType::Shape(s) => { s.rotation = s.rotation.to_radians(); }