Complete Hatch host model coverage

This commit is contained in:
Felix Riestra committed 2026-09-19 18:44:31 +02:00
1 parent e0d35ac84c
commit 98b56ef673
4 files changed
+478 -2

No files matched your search

+16
View File
@@ -740,6 +740,22 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
| "line_count"
| "lock_position"
)
| (
"Hatch",
"elevation"
| "normal"
| "is_solid"
| "pattern"
| "pattern_angle"
| "pattern_scale"
| "pattern_type"
| "is_double"
| "style"
| "is_associative"
| "pixel_size"
| "paths"
| "seed_points"
)
) {
"transaction_geometry"
} else {
@@ -22,7 +22,8 @@
"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"],
"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"],
"AttributeEntity": ["layer", "tag", "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"]
"AttributeEntity": ["layer", "tag", "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"],
"Hatch": ["layer", "elevation", "normal", "is_solid", "pattern", "pattern_angle", "pattern_scale", "pattern_type", "is_double", "style", "is_associative", "pixel_size", "paths", "seed_points"]
},
"readable": {
"Insert": ["block_name"],
@@ -61,6 +61,176 @@ fn insert_scale(name: &str, value: f64) -> Result<(), String> {
}
}
#[cfg(feature = "host")]
fn validate_hatch_paths(paths: &[crate::host::acadrust::entities::BoundaryPath]) -> Result<(), String> {
use crate::host::acadrust::entities::BoundaryEdge;
use crate::host::acadrust::types::Vector2;
let close = |a: Vector2, b: Vector2| (a.x - b.x).hypot(a.y - b.y) <= 1e-8;
let ellipse_point = |center: Vector2, major: Vector2, ratio: f64, angle: f64| {
Vector2::new(
center.x + major.x * angle.cos() - major.y * ratio * angle.sin(),
center.y + major.y * angle.cos() + major.x * ratio * angle.sin(),
)
};
if paths.is_empty() {
return Err("Hatch requires at least one boundary path".into());
}
for (pi, path) in paths.iter().enumerate() {
if path.edges.is_empty() {
return Err(format!("Hatch boundary path {pi} has no edges"));
}
if path.flags.is_not_closed() {
return Err(format!("Hatch boundary path {pi} is marked open"));
}
if path.flags.bits() & !0x1ff != 0 {
return Err(format!("Hatch boundary path {pi} has unknown flag bits"));
}
let mut endpoints = Vec::with_capacity(path.edges.len());
for (ei, edge) in path.edges.iter().enumerate() {
let pair = match edge {
BoundaryEdge::Line(line) => {
if !line.start.x.is_finite() || !line.start.y.is_finite() || !line.end.x.is_finite() || !line.end.y.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} line has non-finite coordinates"));
}
if (line.start.x - line.end.x).hypot(line.start.y - line.end.y) < 1e-12 {
return Err(format!("Hatch boundary path {pi} edge {ei} line has zero length"));
}
(line.start, line.end)
}
BoundaryEdge::CircularArc(arc) => {
if !arc.center.x.is_finite() || !arc.center.y.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} arc center has non-finite coordinates"));
}
if !arc.radius.is_finite() || arc.radius <= 0.0 {
return Err(format!("Hatch boundary path {pi} edge {ei} arc radius must be finite and positive"));
}
if !arc.start_angle.is_finite() || !arc.end_angle.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} arc angles must be finite"));
}
let point = |angle: f64| Vector2::new(
arc.center.x + arc.radius * angle.cos(),
arc.center.y + arc.radius * angle.sin(),
);
let (start, end) = (point(arc.start_angle), point(arc.end_angle));
if arc.counter_clockwise { (start, end) } else { (end, start) }
}
BoundaryEdge::EllipticArc(arc) => {
if !arc.center.x.is_finite() || !arc.center.y.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} ellipse center has non-finite coordinates"));
}
if !arc.major_axis_endpoint.x.is_finite() || !arc.major_axis_endpoint.y.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} ellipse major axis has non-finite coordinates"));
}
if arc.major_axis_endpoint.x.hypot(arc.major_axis_endpoint.y) < 1e-12 {
return Err(format!("Hatch boundary path {pi} edge {ei} ellipse major axis has zero length"));
}
if !arc.minor_axis_ratio.is_finite() || !(0.0..=1.0).contains(&arc.minor_axis_ratio) || arc.minor_axis_ratio == 0.0 {
return Err(format!("Hatch boundary path {pi} edge {ei} ellipse ratio must be within (0, 1]"));
}
if !arc.start_angle.is_finite() || !arc.end_angle.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} ellipse angles must be finite"));
}
let start = ellipse_point(arc.center, arc.major_axis_endpoint, arc.minor_axis_ratio, arc.start_angle);
let end = ellipse_point(arc.center, arc.major_axis_endpoint, arc.minor_axis_ratio, arc.end_angle);
if arc.counter_clockwise { (start, end) } else { (end, start) }
}
BoundaryEdge::Polyline(poly) => {
if poly.vertices.len() < 3 {
return Err(format!("Hatch boundary path {pi} edge {ei} polyline must have at least 3 vertices"));
}
for (vi, v) in poly.vertices.iter().enumerate() {
if !v.x.is_finite() || !v.y.is_finite() || !v.z.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} vertex {vi} has non-finite coordinates"));
}
}
if !poly.is_closed {
return Err(format!("Hatch boundary path {pi} edge {ei} polyline is open"));
}
let first = poly.vertices.first().unwrap();
(Vector2::new(first.x, first.y), Vector2::new(first.x, first.y))
}
BoundaryEdge::Spline(spline) => {
if spline.degree < 1 {
return Err(format!("Hatch boundary path {pi} edge {ei} spline degree must be at least 1"));
}
if spline.control_points.len() < spline.degree as usize + 1 {
return Err(format!("Hatch boundary path {pi} edge {ei} spline has too few control points for its degree"));
}
if spline.knots.iter().any(|knot| !knot.is_finite())
|| spline.knots.windows(2).any(|pair| pair[0] > pair[1])
{
return Err(format!("Hatch boundary path {pi} edge {ei} spline knots must be finite and nondecreasing"));
}
for (ci, cp) in spline.control_points.iter().enumerate() {
if !cp.x.is_finite() || !cp.y.is_finite() || !cp.z.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} control point {ci} has non-finite coordinates"));
}
}
for (fi, fp) in spline.fit_points.iter().enumerate() {
if !fp.x.is_finite() || !fp.y.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} fit point {fi} has non-finite coordinates"));
}
}
for (name, tangent) in [("start", spline.start_tangent), ("end", spline.end_tangent)] {
if !tangent.x.is_finite() || !tangent.y.is_finite() {
return Err(format!("Hatch boundary path {pi} edge {ei} spline {name} tangent is non-finite"));
}
}
let points = if spline.fit_points.len() >= 2 {
spline.fit_points.clone()
} else {
spline.control_points.iter().map(|p| Vector2::new(p.x, p.y)).collect()
};
if points.len() < 2 {
return Err(format!("Hatch boundary path {pi} edge {ei} spline has too few points"));
}
(*points.first().unwrap(), *points.last().unwrap())
}
};
endpoints.push(pair);
}
for ei in 0..endpoints.len() {
let next = (ei + 1) % endpoints.len();
if !close(endpoints[ei].1, endpoints[next].0) {
return Err(format!("Hatch boundary path {pi} is not closed between edges {ei} and {next}"));
}
}
}
Ok(())
}
#[cfg(feature = "host")]
fn validate_hatch_pattern(value: &crate::host::acadrust::entities::Hatch) -> Result<(), String> {
if value.pattern.name.trim().is_empty() {
return Err("Hatch.pattern.name is empty".into());
}
if value.is_solid {
if !value.pattern.name.eq_ignore_ascii_case("SOLID") || !value.pattern.lines.is_empty() {
return Err("solid Hatch requires the SOLID pattern without pattern lines".into());
}
return Ok(());
}
if !value.pattern_scale.is_finite() || value.pattern_scale <= 0.0 {
return Err("Hatch.pattern_scale must be finite and greater than zero".into());
}
if value.pattern.lines.is_empty() {
return Err("patterned Hatch requires at least one pattern line".into());
}
for (index, line) in value.pattern.lines.iter().enumerate() {
if !line.angle.is_finite()
|| !line.base_point.x.is_finite() || !line.base_point.y.is_finite()
|| !line.offset.x.is_finite() || !line.offset.y.is_finite()
|| line.dash_lengths.iter().any(|dash| !dash.is_finite())
{
return Err(format!("Hatch.pattern.lines[{index}] contains non-finite values"));
}
if line.offset.x.hypot(line.offset.y) < 1e-12 {
return Err(format!("Hatch.pattern.lines[{index}].offset must be nonzero"));
}
}
Ok(())
}
/// Validate newly created geometry for the kinds whose mapped fields have
/// host checks. Called by the Python add path before an entity enters the document.
#[cfg(feature = "host")]
@@ -232,6 +402,25 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<()
return Err("AttributeEntity.line_count must be greater than zero".into());
}
}
EntityType::Hatch(value) => {
solid_normal("Hatch.normal", &value.normal)?;
if !value.elevation.is_finite() {
return Err("Hatch.elevation must be finite".into());
}
validate_hatch_pattern(value)?;
if !value.pattern_angle.is_finite() {
return Err("Hatch.pattern_angle must be finite".into());
}
if !value.pixel_size.is_finite() || value.pixel_size < 0.0 {
return Err("Hatch.pixel_size must be finite and non-negative".into());
}
for (si, sp) in value.seed_points.iter().enumerate() {
if !sp.x.is_finite() || !sp.y.is_finite() {
return Err(format!("Hatch.seed_points[{si}] has non-finite coordinates"));
}
}
validate_hatch_paths(&value.paths)?;
}
_ => {}
}
Ok(())
@@ -610,6 +799,50 @@ pub fn validate_entity_mutation(
return Err("AttributeEntity.attdef_handle is read-only".into());
}
}
(EntityType::Hatch(old), EntityType::Hatch(new)) => {
if !same3(&old.normal, &new.normal) {
solid_normal("Hatch.normal", &new.normal)?;
}
if old.elevation.to_bits() != new.elevation.to_bits() && !new.elevation.is_finite() {
return Err("Hatch.elevation must be finite".into());
}
if old.pattern != new.pattern
|| old.is_solid != new.is_solid
|| old.pattern_scale.to_bits() != new.pattern_scale.to_bits()
{
validate_hatch_pattern(new)?;
}
if old.pattern_angle.to_bits() != new.pattern_angle.to_bits()
&& !new.pattern_angle.is_finite()
{
return Err("Hatch.pattern_angle must be finite".into());
}
if old.pixel_size.to_bits() != new.pixel_size.to_bits()
&& (!new.pixel_size.is_finite() || new.pixel_size < 0.0)
{
return Err("Hatch.pixel_size must be finite and non-negative".into());
}
if old.paths != new.paths {
validate_hatch_paths(&new.paths)?;
}
if old.seed_points != new.seed_points {
for (si, sp) in new.seed_points.iter().enumerate() {
if !sp.x.is_finite() || !sp.y.is_finite() {
return Err(format!("Hatch.seed_points[{si}] has non-finite coordinates"));
}
}
}
if old.gradient_color != new.gradient_color {
return Err("Hatch.gradient_color is unmapped and read-only".into());
}
if old.is_mpolygon != new.is_mpolygon
|| old.mpolygon_hatch_color != new.mpolygon_hatch_color
|| old.mpolygon_x_direction != new.mpolygon_x_direction
|| old.mpolygon_boundary_handle_count != new.mpolygon_boundary_handle_count
{
return Err("Hatch MPOLYGON fields are unmapped and read-only".into());
}
}
_ => {}
}
Ok(())
@@ -635,6 +868,22 @@ pub fn validate_canvas_entity_references(
return Err(format!("entity owner {owner:?} does not exist"));
}
}
if let EntityType::Hatch(value) = entity {
let handles = value.paths.iter().flat_map(|path| &path.boundary_handles);
let mut count = 0usize;
for handle in handles {
count += 1;
if handle.is_null() || document.get_entity(*handle).is_none() {
return Err(format!("Hatch boundary handle {handle:?} does not exist"));
}
}
if value.is_associative && count == 0 {
return Err("associative Hatch requires boundary handles".into());
}
if !value.is_associative && count != 0 {
return Err("non-associative Hatch cannot carry boundary handles".into());
}
}
if let EntityType::Tolerance(value) = entity {
let found = value
.dimension_style_handle
@@ -952,6 +1201,7 @@ mod tests {
| "Shape"
| "AttributeDefinition"
| "AttributeEntity"
| "Hatch"
)
{
assert!(
@@ -1073,6 +1323,19 @@ mod tests {
"AttributeEntity.is_multiline".to_owned(),
"AttributeEntity.line_count".to_owned(),
"AttributeEntity.lock_position".to_owned(),
"Hatch.elevation".to_owned(),
"Hatch.normal".to_owned(),
"Hatch.is_solid".to_owned(),
"Hatch.pattern".to_owned(),
"Hatch.pattern_angle".to_owned(),
"Hatch.pattern_scale".to_owned(),
"Hatch.pattern_type".to_owned(),
"Hatch.is_double".to_owned(),
"Hatch.style".to_owned(),
"Hatch.is_associative".to_owned(),
"Hatch.pixel_size".to_owned(),
"Hatch.paths".to_owned(),
"Hatch.seed_points".to_owned(),
])
);
}
@@ -1415,4 +1678,57 @@ mod tests {
.contains("no whitespace")
);
}
#[test]
fn hatch_geometry_and_boundaries_are_validated() {
use acadrust::entities::hatch::{BoundaryEdge, BoundaryPath, LineEdge};
let mut hatch = acadrust::entities::Hatch::new();
let mut path = BoundaryPath::new();
for (start, end) in [
((0.0, 0.0), (10.0, 0.0)),
((10.0, 0.0), (10.0, 10.0)),
((10.0, 10.0), (0.0, 10.0)),
((0.0, 10.0), (0.0, 0.0)),
] {
path.edges.push(BoundaryEdge::Line(LineEdge {
start: acadrust::types::Vector2::new(start.0, start.1),
end: acadrust::types::Vector2::new(end.0, end.1),
}));
}
hatch.paths.push(path);
let entity = acadrust::EntityType::Hatch(hatch.clone());
validate_new_canvas_entity(&entity).unwrap();
let mut invalid = hatch.clone();
invalid.paths.clear();
assert!(validate_new_canvas_entity(&acadrust::EntityType::Hatch(invalid))
.unwrap_err()
.contains("at least one boundary path"));
let mut invalid = hatch.clone();
invalid.is_solid = false;
invalid.pattern_scale = 0.0;
assert!(validate_new_canvas_entity(&acadrust::EntityType::Hatch(invalid))
.unwrap_err()
.contains("greater than zero"));
let mut document = acadrust::CadDocument::new();
let boundary = document.add_entity(acadrust::EntityType::Line(
acadrust::entities::Line::from_points(
acadrust::types::Vector3::ZERO,
acadrust::types::Vector3::UNIT_X,
),
)).unwrap();
let mut associative = hatch.clone();
associative.is_associative = true;
associative.paths[0].boundary_handles.push(boundary);
validate_canvas_entity_references(
&document,
&acadrust::EntityType::Hatch(associative.clone()),
).unwrap();
associative.paths[0].boundary_handles[0] = acadrust::Handle::new(u64::MAX);
assert!(validate_canvas_entity_references(
&document,
&acadrust::EntityType::Hatch(associative),
).unwrap_err().contains("does not exist"));
}
}
+144 -1
View File
@@ -2009,6 +2009,149 @@ mod tests {
);
}
#[test]
fn staged_python_hatch_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 process = ocs_plugin_api::process::PluginProcess::spawn(
std::path::Path::new(&plugin_path),
&mut host,
crate::plugin::v4_support::notification_handler(),
)
.unwrap();
let dispatch = |host: &mut HostSession<'_>, command: &str| {
assert!(process.dispatch(host, command, &mut |_| {}).expect("Python dispatch"));
};
let script = std::env::temp_dir().join(format!("ocs_hatch_create_{}.py", std::process::id()));
std::fs::write(&script, concat!(
"def p(x, y): return {'x': x, 'y': y}\n",
"def boundary(size):\n",
" pairs = [(0.0,0.0,size,0.0),(size,0.0,size,size),(size,size,0.0,size),(0.0,size,0.0,0.0)]\n",
" edges = [{'kind':'Line','value':{'start':p(a,b),'end':p(c,d)}} for a,b,c,d in pairs]\n",
" return {'flags':1,'edges':edges,'boundary_handles':[]}\n",
"doc = ocs.active_document\n",
"hatch = doc.create_entity('Hatch', paths=[boundary(10.0)])\n",
)).unwrap();
dispatch(&mut host, &format!("PY_RUN {}", script.display()));
let _ = std::fs::remove_file(&script);
let hatch_handle = host.document().entities().find_map(|entity| match entity {
EntityType::Hatch(hatch) => Some(hatch.common.handle),
_ => None,
}).unwrap_or_else(|| panic!("Python did not create Hatch: {:?}",
host.app.command_line.history.last().map(|entry| &entry.text)));
let created = host.document().get_entity(hatch_handle).unwrap().clone();
assert!(host.app.tabs[0].scene.hatches.get(&hatch_handle)
.is_some_and(|model| model.boundary.len() >= 4 && model.name == "SOLID"));
dispatch(&mut host, &format!(
"PY_EVAL ocs.active_document.entities[{}].pattern['name']", hatch_handle.value()));
assert!(host.app.command_line.history.last().unwrap().text.contains("SOLID"));
let script = std::env::temp_dir().join(format!("ocs_hatch_edit_{}.py", std::process::id()));
std::fs::write(&script, format!(concat!(
"def p(x, y): return {{'x': x, 'y': y}}\n",
"def boundary(size):\n",
" pairs = [(0.0,0.0,size,0.0),(size,0.0,size,size),(size,size,0.0,size),(0.0,size,0.0,0.0)]\n",
" edges = [{{'kind':'Line','value':{{'start':p(a,b),'end':p(c,d)}}}} for a,b,c,d in pairs]\n",
" return {{'flags':1,'edges':edges,'boundary_handles':[]}}\n",
"doc = ocs.active_document\n",
"hatch = doc.entities[{}]\n",
"with doc.transaction('Edit hatch'):\n",
" hatch.paths = [boundary(12.0)]\n",
" hatch.is_solid = False\n",
" hatch.pattern = {{'name':'TEST','description':'test pattern','lines':[",
"{{'angle':0.0,'base_point':p(0.0,0.0),'offset':p(0.0,2.0),'dash_lengths':[1.0,-1.0]}}]}}\n",
" hatch.pattern_scale = 2.0\n",
" hatch.pattern_angle = 0.25\n",
"doc.selection = [hatch]\n"
), hatch_handle.value())).unwrap();
dispatch(&mut host, &format!("PY_RUN {}", script.display()));
let _ = std::fs::remove_file(&script);
let expected = host.document().get_entity(hatch_handle).unwrap().clone();
let EntityType::Hatch(edited) = &expected else { unreachable!() };
assert!(!edited.is_solid);
assert_eq!(edited.pattern.name, "TEST");
assert_eq!(edited.pattern.lines.len(), 1);
assert_eq!(edited.paths[0].edges.len(), 4);
assert!((edited.pattern_scale - 2.0).abs() < 1e-12);
assert_eq!(edited.gradient_color, match &created { EntityType::Hatch(value) => value.gradient_color.clone(), _ => unreachable!() });
assert_eq!(host.selection(), vec![hatch_handle]);
assert!(host.app.tabs[0].scene.hatches.get(&hatch_handle).is_some_and(|model|
model.boundary.len() >= 4 && model.name == "TEST"
&& matches!(&model.pattern,
crate::scene::model::hatch_model::HatchPattern::Pattern(lines)
if !lines.is_empty())));
for (patch, message) in [
("'pattern_scale':0.0", "greater than zero"),
("'is_associative':True", "requires boundary handles"),
("'gradient_color':{}", "outside the editable schema"),
("'paths':[{'flags':1,'edges':[{'kind':'Line','value':{'start':{'x':0.0,'y':0.0},'end':{'x':1.0,'y':0.0}}}],'boundary_handles':[]}]", "not closed"),
] {
dispatch(&mut host, &format!(
"PY_EVAL ocs.update_many('Reject hatch', [{{'handle':{}, {patch}}}])",
hatch_handle.value()));
assert_eq!(host.document().get_entity(hatch_handle), Some(&expected));
assert!(host.app.command_line.history.last().unwrap().text.contains(message));
}
let assert_saved = |document: &CadDocument| {
assert!(matches!(document.get_entity(hatch_handle), Some(EntityType::Hatch(hatch))
if !hatch.is_solid && hatch.pattern.name == "TEST"
&& hatch.pattern.lines.len() == 1 && hatch.paths[0].edges.len() == 4
&& (hatch.pattern_scale - 2.0).abs() < 1e-12));
};
let reopened_dwg = crate::io::load_bytes("hatch.dwg",
acadrust::DwgWriter::write_to_vec(host.document()).unwrap()).unwrap();
let reopened_dxf = crate::io::load_bytes("hatch.dxf",
acadrust::DxfWriter::new(host.document()).write_to_vec().unwrap()).unwrap();
assert_saved(&reopened_dwg);
assert_saved(&reopened_dxf);
for (name, mut document) in [("hatch-reedit.dwg", reopened_dwg),
("hatch-reedit.dxf", reopened_dxf)] {
let before = document.get_entity(hatch_handle).unwrap().clone();
let mut after = before.clone();
let EntityType::Hatch(hatch) = &mut after else { unreachable!() };
hatch.elevation = 3.0;
ocs_plugin_api::entity_coverage::validate_entity_mutation(&before, &after).unwrap();
*document.get_entity_mut(hatch_handle).unwrap() = after;
let bytes = if name.ends_with("dwg") {
acadrust::DwgWriter::write_to_vec(&document).unwrap()
} else {
acadrust::DxfWriter::new(&document).write_to_vec().unwrap()
};
assert!(matches!(crate::io::load_bytes(name, bytes).unwrap().get_entity(hatch_handle),
Some(EntityType::Hatch(hatch)) if (hatch.elevation - 3.0).abs() < 1e-12));
}
dispatch(&mut host, &format!(
"PY_EVAL ocs.active_document.delete_entity({})", hatch_handle.value()));
assert!(host.document().get_entity(hatch_handle).is_none());
for (name, bytes) in [
("hatch-deleted.dwg", acadrust::DwgWriter::write_to_vec(host.document()).unwrap()),
("hatch-deleted.dxf", acadrust::DxfWriter::new(host.document()).write_to_vec().unwrap()),
] {
assert!(crate::io::load_bytes(name, bytes).unwrap().get_entity(hatch_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(hatch_handle), Some(&expected));
app.undo_steps(1);
assert_eq!(app.tabs[0].scene.document.get_entity(hatch_handle), Some(&created));
app.undo_steps(1);
assert!(app.tabs[0].scene.document.get_entity(hatch_handle).is_none());
app.redo_steps(3);
assert!(app.tabs[0].scene.document.get_entity(hatch_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 {
@@ -2380,7 +2523,7 @@ mod tests {
let handle;
{
let mut host = HostSession::new(&mut app, 0);
handle = host.add_entity(EntityType::Hatch(acadrust::entities::Hatch::default()));
handle = host.add_entity(EntityType::MLine(acadrust::entities::MLine::default()));
let original = host.document().get_entity(handle).unwrap();
let changed =
ocs_plugin_api::entity_coverage::patch_canvas_layer(original, "HATCHES").unwrap();