forked from 1b-agent/opencadstudio-upstream
Add validated Insert transform scripting coverage
This commit is contained in:
1 parent
11d5614991
commit
db61379bfe
5 files changed
+126
-12
No files matched your search
@@ -24,6 +24,7 @@ struct EntityCoveragePolicy {
|
||||
internal_kinds: Vec<String>,
|
||||
opaque_kinds: Vec<String>,
|
||||
editable: BTreeMap<String, Vec<String>>,
|
||||
readable: BTreeMap<String, Vec<String>>,
|
||||
aliases: BTreeMap<String, BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
@@ -510,7 +511,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
||||
let variants = ®istry.types[&TypeId::new("EntityType")].variants;
|
||||
let variant_names: HashSet<&str> = variants.iter().map(|v| v.name.as_str()).collect();
|
||||
for name in policy.internal_kinds.iter().chain(&policy.opaque_kinds)
|
||||
.chain(policy.editable.keys()) {
|
||||
.chain(policy.editable.keys()).chain(policy.readable.keys()) {
|
||||
assert!(variant_names.contains(name.as_str()), "coverage policy names unknown kind: {name}");
|
||||
}
|
||||
for name in &policy.internal_kinds {
|
||||
@@ -532,8 +533,9 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
||||
else if policy.opaque_kinds.contains(kind) { EntityScope::Opaque }
|
||||
else { EntityScope::Canvas };
|
||||
let editable = policy.editable.get(kind).cloned().unwrap_or_default();
|
||||
let mut unresolved: HashSet<String> = editable.iter().cloned().collect();
|
||||
assert_eq!(unresolved.len(), editable.len(), "duplicate editable property in {kind}");
|
||||
let readable = policy.readable.get(kind).cloned().unwrap_or_default();
|
||||
let mut unresolved: HashSet<String> = editable.iter().chain(&readable).cloned().collect();
|
||||
assert_eq!(unresolved.len(), editable.len() + readable.len(), "duplicate mapped property in {kind}");
|
||||
let aliases = policy.aliases.get(kind);
|
||||
let mut properties = vec![PropertyCoverage {
|
||||
name: "kind".into(), source_path: "<variant>".into(), type_id: "String".into(),
|
||||
@@ -563,13 +565,13 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
||||
let shape = ®istry.types[&shape_type];
|
||||
for field in &shape.fields {
|
||||
if field.name == "common" { continue; }
|
||||
let exposed = editable.iter().find(|name| {
|
||||
let exposed = editable.iter().chain(&readable).find(|name| {
|
||||
aliases.and_then(|map| map.get(*name)).map_or(name.as_str(), String::as_str) == field.name
|
||||
});
|
||||
let (name, access) = match exposed {
|
||||
Some(name) => {
|
||||
unresolved.remove(name);
|
||||
((*name).clone(), ModelAccess::ReadWrite)
|
||||
((*name).clone(), if editable.contains(name) { ModelAccess::ReadWrite } else { ModelAccess::ReadOnly })
|
||||
}
|
||||
None => (field.name.clone(), ModelAccess::Unmapped),
|
||||
};
|
||||
@@ -579,7 +581,9 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
|
||||
("Circle" | "Arc", "center" | "radius") |
|
||||
("Ray" | "XLine", "base_point" | "direction") |
|
||||
("Solid", "first_corner" | "second_corner" | "third_corner" | "fourth_corner" | "normal" | "thickness") |
|
||||
("Face3D", "first_corner" | "second_corner" | "third_corner" | "fourth_corner" | "invisible_edges")) {
|
||||
("Face3D", "first_corner" | "second_corner" | "third_corner" | "fourth_corner" | "invisible_edges") |
|
||||
("Insert", "insert_point" | "x_scale" | "y_scale" | "z_scale" | "rotation" | "normal" |
|
||||
"column_count" | "row_count" | "column_spacing" | "row_spacing")) {
|
||||
"transaction_geometry"
|
||||
} else { "type_conversion_only" };
|
||||
properties.push(PropertyCoverage {
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
"Ray": ["layer", "base_point", "direction"],
|
||||
"XLine": ["layer", "base_point", "direction"],
|
||||
"Solid": ["layer", "first_corner", "second_corner", "third_corner", "fourth_corner", "normal", "thickness", "is_trace"],
|
||||
"Face3D": ["layer", "first_corner", "second_corner", "third_corner", "fourth_corner", "invisible_edges"]
|
||||
"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"]
|
||||
},
|
||||
"readable": {
|
||||
"Insert": ["block_name"]
|
||||
},
|
||||
"aliases": {
|
||||
"Polyline": {"closed": "flags"},
|
||||
|
||||
@@ -41,6 +41,12 @@ fn solid_normal(name: &str, value: &crate::host::acadrust::types::Vector3) -> Re
|
||||
else { Err(format!("{name} must be nonzero")) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
fn insert_scale(name: &str, value: f64) -> Result<(), String> {
|
||||
if value.is_finite() && value.abs() >= 1e-12 { Ok(()) }
|
||||
else { Err(format!("{name} must be finite and nonzero")) }
|
||||
}
|
||||
|
||||
/// 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")]
|
||||
@@ -167,6 +173,34 @@ pub fn validate_entity_mutation(
|
||||
return Err("Face3D.invisible_edges has unknown bits".into());
|
||||
}
|
||||
}
|
||||
(EntityType::Insert(old), EntityType::Insert(new)) => {
|
||||
if old.block_name != new.block_name || old.attributes != new.attributes ||
|
||||
old.view_rep_handle != new.view_rep_handle || old.seqend_handle != new.seqend_handle {
|
||||
return Err("Insert block identity and attached records cannot change in a geometry transaction".into());
|
||||
}
|
||||
changed3("Insert.insert_point", &old.insert_point, &new.insert_point)?;
|
||||
if !same3(&old.normal, &new.normal) { solid_normal("Insert.normal", &new.normal)?; }
|
||||
for (name, before, after) in [
|
||||
("x_scale", old.x_scale(), new.x_scale()),
|
||||
("y_scale", old.y_scale(), new.y_scale()),
|
||||
("z_scale", old.z_scale(), new.z_scale()),
|
||||
] {
|
||||
if before.to_bits() != after.to_bits() { insert_scale(&format!("Insert.{name}"), after)?; }
|
||||
}
|
||||
for (name, before, after) in [
|
||||
("rotation", old.rotation, new.rotation),
|
||||
("column_spacing", old.column_spacing, new.column_spacing),
|
||||
("row_spacing", old.row_spacing, new.row_spacing),
|
||||
] {
|
||||
if before.to_bits() != after.to_bits() && !after.is_finite() {
|
||||
return Err(format!("Insert.{name} must be finite"));
|
||||
}
|
||||
}
|
||||
if (old.column_count != new.column_count && new.column_count == 0) ||
|
||||
(old.row_count != new.row_count && new.row_count == 0) {
|
||||
return Err("Insert array counts must be greater than zero".into());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
@@ -222,7 +256,7 @@ 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") {
|
||||
"Ray" | "XLine" | "Solid" | "Face3D" | "Insert") {
|
||||
assert!(entry.properties.iter().all(|p| p.model_access != ModelAccess::ReadWrite
|
||||
|| (entry.scope == EntityScope::Canvas && p.name == "layer")), "{}", entry.kind);
|
||||
}
|
||||
@@ -249,6 +283,11 @@ mod tests {
|
||||
"Face3D.first_corner".to_owned(), "Face3D.second_corner".to_owned(),
|
||||
"Face3D.third_corner".to_owned(), "Face3D.fourth_corner".to_owned(),
|
||||
"Face3D.invisible_edges".to_owned(),
|
||||
"Insert.insert_point".to_owned(), "Insert.x_scale".to_owned(),
|
||||
"Insert.y_scale".to_owned(), "Insert.z_scale".to_owned(),
|
||||
"Insert.rotation".to_owned(), "Insert.normal".to_owned(),
|
||||
"Insert.column_count".to_owned(), "Insert.row_count".to_owned(),
|
||||
"Insert.column_spacing".to_owned(), "Insert.row_spacing".to_owned(),
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -265,6 +304,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_catalog_separates_transform_writes_from_block_identity() {
|
||||
let catalog: EntityCoverageCatalog = serde_json::from_str(get_embedded_entity_coverage_json()).unwrap();
|
||||
let insert = catalog.entity_kinds.iter().find(|entry| entry.kind == "Insert").unwrap();
|
||||
let block_name = insert.properties.iter().find(|property| property.name == "block_name").unwrap();
|
||||
assert_eq!(block_name.model_access, ModelAccess::ReadOnly);
|
||||
assert_eq!(block_name.validation, "none");
|
||||
assert!(insert.properties.iter().find(|property| property.name == "attributes").unwrap().model_access == ModelAccess::Unmapped);
|
||||
assert!(insert.properties.iter().find(|property| property.name == "insert_point").unwrap().model_access == ModelAccess::ReadWrite);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn unsupported_canvas_kind_has_typed_snapshot_fields() {
|
||||
@@ -326,6 +376,30 @@ mod tests {
|
||||
.unwrap_err().contains("Face3D.invisible_edges"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn insert_transaction_checks_transforms_and_protects_references() {
|
||||
use crate::host::acadrust::{self, types::Vector3};
|
||||
let before = acadrust::entities::Insert::new("DOOR", Vector3::new(1.0, 2.0, 0.0));
|
||||
let mut moved = before.clone();
|
||||
moved.insert_point = Vector3::new(5.0, 6.0, 0.0);
|
||||
moved.set_x_scale(2.0);
|
||||
assert!(validate_entity_mutation(&acadrust::EntityType::Insert(before.clone()),
|
||||
&acadrust::EntityType::Insert(moved)).is_ok());
|
||||
let mut bad = before.clone();
|
||||
bad.set_y_scale(f64::NAN);
|
||||
assert!(validate_entity_mutation(&acadrust::EntityType::Insert(before.clone()),
|
||||
&acadrust::EntityType::Insert(bad)).unwrap_err().contains("Insert.y_scale"));
|
||||
let mut bad = before.clone();
|
||||
bad.column_count = 0;
|
||||
assert!(validate_entity_mutation(&acadrust::EntityType::Insert(before.clone()),
|
||||
&acadrust::EntityType::Insert(bad)).unwrap_err().contains("array counts"));
|
||||
let mut bad = before.clone();
|
||||
bad.block_name = "OTHER".into();
|
||||
assert!(validate_entity_mutation(&acadrust::EntityType::Insert(before),
|
||||
&acadrust::EntityType::Insert(bad)).unwrap_err().contains("block identity"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn canvas_layer_patch_preserves_unmapped_hatch_fields() {
|
||||
|
||||
Reference in new issue
Block a user