From d62057b869da11e11b89101a36dc1b6adc4c0ca6 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 12 Jul 2026 19:26:11 +0300 Subject: [PATCH] feat(properties): show Annotative state + applied annotation scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve, per selected entity, whether it is annotative (from its text / dimension / multileader style, or its own flag) and — when annotative — the name(s) of the annotation scale(s) applied to it, shown as a read-only "Annotative scale" row (Text / MText / Leader / MLeader). The applied scale is read from the entity's per-scale object-context leaves via the new acadrust CadDocument.context_scales map (context leaf -> AcDbScale handle), walked from the entity's extension dictionary (AcDbContextDataManager -> ACDB_ANNOTATIONSCALES). Bumps acadrust to pick up that parsing. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 2 +- src/app/properties.rs | 155 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2980d904e..4d9423ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/HakanSeven12/acadrust?branch=main#4ac3d68cd7229e8a79b1b9369ce985a50a4b4de0" +source = "git+https://github.com/HakanSeven12/acadrust?branch=main#b9ad5386d6d6a19c80cf77f6ed7f94f681ba35db" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/src/app/properties.rs b/src/app/properties.rs index 82dfe43fa..e17d73fb3 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -481,6 +481,60 @@ impl OpenCADStudio { _ => {} } + // Annotative Yes/No + a conditional "Annotative scale" row. + // Read-only: annotative state comes from the entity's style + // (or its own flag) and the assigned annotation-scale name(s) + // are walked from the entity's extension dictionary — both + // need the document, so they are resolved here. + { + let anno = match entity { + acadrust::EntityType::Text(t) => { + Some((text_style_annotative(doc, &t.style), "annotative")) + } + acadrust::EntityType::MText(t) => Some(( + t.is_annotative || text_style_annotative(doc, &t.style), + "annotative", + )), + acadrust::EntityType::Leader(l) => { + Some((dim_style_annotative(doc, &l.dimension_style), "annotative")) + } + acadrust::EntityType::MultiLeader(ml) => Some(( + ml.enable_annotation_scale + || mleader_style_annotative(doc, ml.style_handle), + "enable_annotation_scale", + )), + _ => None, + }; + if let Some((is_anno, anno_field)) = anno { + // MLeader keeps its editable toggle; the read-only + // text rows get an explicit Yes/No. + if anno_field == "annotative" { + set_row( + &mut sections, + "annotative", + if is_anno { "Yes" } else { "No" }.to_string(), + ); + } + if is_anno { + let names = entity_scale_names(doc, entity.common()); + let display = if names.is_empty() { + doc.header.current_annotation_scale.clone() + } else { + names.join(", ") + }; + insert_row_after( + &mut sections, + anno_field, + crate::entities::common::ro_prop( + "Annotative scale", + "annotative_scale", + display, + ), + ); + } + } + } + if !group_names.is_empty() { let label = group_names.join(", "); if let Some(general) = sections.first_mut() { @@ -1189,6 +1243,107 @@ fn find_dim_style<'a>( }) } +/// Is the named text style annotative? +fn text_style_annotative(doc: &acadrust::CadDocument, name: &str) -> bool { + doc.text_styles + .iter() + .find(|s| { + s.name.eq_ignore_ascii_case(name) + || (name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard")) + }) + .map_or(false, |s| s.annotative) +} + +/// Is the named dimension style annotative? +fn dim_style_annotative(doc: &acadrust::CadDocument, name: &str) -> bool { + find_dim_style(doc, name).map_or(false, |s| s.annotative) +} + +/// Is the multileader style (by handle) annotative? +fn mleader_style_annotative(doc: &acadrust::CadDocument, handle: Option) -> bool { + let Some(h) = handle else { + return false; + }; + doc.objects.iter().any(|(oh, o)| { + matches!(o, acadrust::objects::ObjectType::MultiLeaderStyle(s) if *oh == h && s.is_annotative) + }) +} + +/// Resolve a handle to a `Dictionary` object, if it is one. +fn as_dict( + doc: &acadrust::CadDocument, + handle: acadrust::Handle, +) -> Option<&acadrust::objects::Dictionary> { + match doc.objects.get(&handle) { + Some(acadrust::objects::ObjectType::Dictionary(d)) => Some(d), + _ => None, + } +} + +/// Case-insensitive dictionary entry lookup by key. +fn dict_get(dict: &acadrust::objects::Dictionary, key: &str) -> Option { + dict.entries + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case(key)) + .map(|(_, h)| *h) +} + +/// The AcDbScale object's ratio name (e.g. "1:50"), by handle. +fn scale_name(doc: &acadrust::CadDocument, handle: acadrust::Handle) -> Option { + match doc.objects.get(&handle) { + Some(acadrust::objects::ObjectType::Scale(s)) => Some(s.name.clone()), + _ => None, + } +} + +/// The annotation-scale name(s) assigned to an entity, walked from its extension +/// dictionary → AcDbContextDataManager → ACDB_ANNOTATIONSCALES. +/// +/// The collection's entry keys are internal anonymous names (e.g. "*A1"); the +/// real scale is each leaf's group-340 SCALE handle, which acadrust surfaces via +/// `doc.context_scales` (context-leaf handle → AcDbScale handle). We resolve each +/// leaf handle to its scale, then the scale to its ratio name. Empty when the +/// entity has no per-scale context (it then uses the drawing's current scale). +fn entity_scale_names( + doc: &acadrust::CadDocument, + common: &acadrust::entities::EntityCommon, +) -> Vec { + let Some(xd) = common.xdictionary_handle else { + return Vec::new(); + }; + let Some(coll) = as_dict(doc, xd) + .and_then(|d| dict_get(d, "AcDbContextDataManager")) + .and_then(|h| as_dict(doc, h)) + .and_then(|m| dict_get(m, "ACDB_ANNOTATIONSCALES")) + .and_then(|h| as_dict(doc, h)) + else { + return Vec::new(); + }; + let mut names: Vec = Vec::new(); + for (_, leaf) in &coll.entries { + if let Some(name) = doc.context_scales.get(leaf).and_then(|sh| scale_name(doc, *sh)) { + if !names.contains(&name) { + names.push(name); + } + } + } + names +} + +/// Insert `row` immediately after the first property whose field matches. +fn insert_row_after( + sections: &mut [crate::scene::model::object::PropSection], + field: &str, + row: crate::scene::model::object::Property, +) { + for section in sections.iter_mut() { + if let Some(idx) = section.props.iter().position(|p| p.field == field) { + section.props.insert(idx + 1, row); + return; + } + } +} + /// Vertical text placement (DIMTAD) label. fn dimtad_label(dimtad: i16) -> &'static str { match dimtad {