Complete MultiLeader host model coverage

Extends the adapter generator (Color, LineWeight, mixed unit/payload enums,
fixed float arrays, bitflags-2 newtypes, keep_types override) and maps
MultiLeader with a merge-style context patch, host validation and
reference checks. staged_python_multileader_lifecycle_over_real_ipc passes
the full gate including DWG and DXF reopen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-20 14:27:43 +02:00
1 parent 22f9c20fe8
commit b3f5388208
14 files changed
+523 -31

No files matched your search

@@ -465,6 +465,89 @@ fn validate_dimension(
Ok(())
}
/// Field checks shared by MultiLeader creation and mutation.
#[cfg(feature = "host")]
fn validate_multileader(
old: Option<&crate::host::acadrust::entities::MultiLeader>,
new: &crate::host::acadrust::entities::MultiLeader,
) -> Result<(), String> {
use crate::host::acadrust::entities::LeaderContentType;
if old == Some(new) {
return Ok(());
}
for (name, value, allow_zero) in [
("dogleg_length", new.dogleg_length, true),
("arrowhead_size", new.arrowhead_size, true),
("text_height", new.text_height, false),
("scale_factor", new.scale_factor, false),
] {
if !value.is_finite() || value < 0.0 || (!allow_zero && value == 0.0) {
return Err(format!(
"MultiLeader.{name} must be finite and {}",
if allow_zero { "non-negative" } else { "greater than zero" }
));
}
}
if !new.block_rotation.is_finite() {
return Err("MultiLeader.block_rotation must be finite".into());
}
finite_vector("MultiLeader.block_scale", &new.block_scale)?;
let context = &new.context;
if !context.scale_factor.is_finite() || context.scale_factor <= 0.0 {
return Err("MultiLeader.context.scale_factor must be finite and greater than zero".into());
}
for (name, value) in [
("text_height", context.text_height),
("text_width", context.text_width),
("text_rotation", context.text_rotation),
("landing_gap", context.landing_gap),
("arrowhead_size", context.arrowhead_size),
] {
if !value.is_finite() {
return Err(format!("MultiLeader.context.{name} must be finite"));
}
}
for (name, point) in [
("content_base_point", &context.content_base_point),
("text_location", &context.text_location),
("block_content_location", &context.block_content_location),
("base_point", &context.base_point),
] {
finite_vector(&format!("MultiLeader.context.{name}"), point)?;
}
if context.transform_matrix.iter().any(|value| !value.is_finite()) {
return Err("MultiLeader.context.transform_matrix must be finite".into());
}
for (ri, root) in context.leader_roots.iter().enumerate() {
finite_vector(&format!("MultiLeader.context.leader_roots[{ri}].connection_point"), &root.connection_point)?;
finite_vector(&format!("MultiLeader.context.leader_roots[{ri}].direction"), &root.direction)?;
if !root.landing_distance.is_finite() {
return Err(format!("MultiLeader.context.leader_roots[{ri}].landing_distance must be finite"));
}
for (li, line) in root.lines.iter().enumerate() {
if line.points.is_empty() {
return Err(format!("MultiLeader.context.leader_roots[{ri}].lines[{li}] has no points"));
}
for (pi, point) in line.points.iter().enumerate() {
finite_vector(&format!("MultiLeader.context.leader_roots[{ri}].lines[{li}].points[{pi}]"), point)?;
}
}
}
match new.content_type {
LeaderContentType::MText if !context.has_text_contents => {
return Err("MText MultiLeader requires context.has_text_contents".into());
}
LeaderContentType::Block if !context.has_block_contents => {
return Err("Block MultiLeader requires context.has_block_contents".into());
}
LeaderContentType::None if context.has_text_contents || context.has_block_contents => {
return Err("MultiLeader without content cannot carry text or block contents".into());
}
_ => {}
}
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")]
@@ -559,6 +642,7 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<()
EntityType::Leader(value) => validate_leader(None, value)?,
EntityType::MLine(value) => validate_mline(None, value)?,
EntityType::Dimension(value) => validate_dimension(None, value)?,
EntityType::MultiLeader(value) => validate_multileader(None, value)?,
EntityType::AttributeDefinition(value) => {
finite_vector(
"AttributeDefinition.insertion_point",
@@ -914,6 +998,7 @@ pub fn validate_entity_mutation(
}
}
(EntityType::Dimension(old), EntityType::Dimension(new)) => validate_dimension(Some(old), new)?,
(EntityType::MultiLeader(old), EntityType::MultiLeader(new)) => validate_multileader(Some(old), new)?,
(EntityType::MLine(old), EntityType::MLine(new)) => validate_mline(Some(old), new)?,
(EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?,
(EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(new)) => {
@@ -1177,6 +1262,26 @@ pub fn validate_canvas_entity_references(
return Err(format!("Shape text style {:?} has no SHX file", style.name));
}
}
if let EntityType::MultiLeader(value) = entity {
use crate::host::acadrust::objects::ObjectType;
let handle_exists = |handle: Option<crate::host::acadrust::Handle>, what: &str, ok: &dyn Fn(crate::host::acadrust::Handle) -> bool| {
match handle.filter(|handle| !handle.is_null()) {
Some(handle) if !ok(handle) => Err(format!("MultiLeader {what} handle {handle:?} does not exist")),
_ => Ok(()),
}
};
let is_block = |h| document.block_records.iter().any(|record| record.handle == h);
let is_text_style = |h| document.text_styles.iter().any(|style| style.handle == h);
let is_linetype = |h| document.line_types.iter().any(|ltype| ltype.handle == h);
let is_mleader_style = |h| matches!(document.objects.get(&h), Some(ObjectType::MultiLeaderStyle(_)));
handle_exists(value.style_handle, "style", &is_mleader_style)?;
handle_exists(value.text_style_handle, "text style", &is_text_style)?;
handle_exists(value.context.text_style_handle, "context text style", &is_text_style)?;
handle_exists(value.line_type_handle, "line type", &is_linetype)?;
handle_exists(value.arrowhead_handle, "arrowhead block", &is_block)?;
handle_exists(value.block_content_handle, "block content", &is_block)?;
handle_exists(value.context.block_content_handle, "context block content", &is_block)?;
}
if let EntityType::Dimension(value) = entity {
let name = value.base().style_name.trim();
if !document.dim_styles.iter().any(|style| style.name.eq_ignore_ascii_case(name)) {
@@ -1540,6 +1645,7 @@ mod tests {
| "Leader"
| "MLine"
| "Dimension"
| "MultiLeader"
)
{
assert!(
@@ -1729,6 +1835,38 @@ mod tests {
"Dimension.is_ordinate_type_x".to_owned(),
"Dimension.is_partial".to_owned(),
"Dimension.has_leader".to_owned(),
"MultiLeader.content_type".to_owned(),
"MultiLeader.path_type".to_owned(),
"MultiLeader.line_color".to_owned(),
"MultiLeader.line_weight".to_owned(),
"MultiLeader.enable_landing".to_owned(),
"MultiLeader.enable_dogleg".to_owned(),
"MultiLeader.dogleg_length".to_owned(),
"MultiLeader.arrowhead_size".to_owned(),
"MultiLeader.text_color".to_owned(),
"MultiLeader.text_frame".to_owned(),
"MultiLeader.text_left_attachment".to_owned(),
"MultiLeader.text_right_attachment".to_owned(),
"MultiLeader.text_top_attachment".to_owned(),
"MultiLeader.text_bottom_attachment".to_owned(),
"MultiLeader.text_attachment_direction".to_owned(),
"MultiLeader.text_attachment_point".to_owned(),
"MultiLeader.text_alignment".to_owned(),
"MultiLeader.text_angle_type".to_owned(),
"MultiLeader.text_direction_negative".to_owned(),
"MultiLeader.scale_factor".to_owned(),
"MultiLeader.enable_annotation_scale".to_owned(),
"MultiLeader.extend_leader_to_text".to_owned(),
"MultiLeader.block_content_color".to_owned(),
"MultiLeader.block_connection_type".to_owned(),
"MultiLeader.block_rotation".to_owned(),
"MultiLeader.block_scale".to_owned(),
"MultiLeader.context".to_owned(),
"MultiLeader.style_handle".to_owned(),
"MultiLeader.text_style_handle".to_owned(),
"MultiLeader.arrowhead_handle".to_owned(),
"MultiLeader.line_type_handle".to_owned(),
"MultiLeader.block_content_handle".to_owned(),
])
);
}