forked from 1b-agent/opencadstudio-upstream
Add AttributeDefinition host model support
This commit is contained in:
1 parent
54322e00e3
commit
5b854bd121
5 files changed
+326
-12
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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; };
|
||||
|
||||
+4
-9
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user