From fc5e93dcc1cb9ddf2d372a44ea5deb425a27e75d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 13 Jul 2026 09:32:22 +0300 Subject: [PATCH] feat(properties): editable Annotative toggle for MText and MLeader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Annotative row becomes an editable toggle for the two entity types that carry a native per-object annotative flag — MTEXT (is_annotative) and MULTILEADER (enable_annotation_scale). Turning it on flags the object annotative (it then scales with the current annotation scale in model space); turning it off clears the flag AND removes the per-object annotation context (the AcDbContextDataManager -> ACDB_ANNOTATIONSCALES leaf subtree) plus the legacy annotative XDATA markers, via scene::annotative::set_entity_annotative / clear_annotation_context, so the object stops resolving annotative instead of staying on through a stale context. The shared SCALE objects are left intact. Style-derived types (text/dimension/leader/table/insert) keep a read-only Annotative row — their state comes from the assigned style, and making a single object annotative there needs a per-object context object (a dedicated acadrust ObjectContextData writer) which is out of scope here. Verified an existing annotative DWG round-trips its 134 per-object contexts through a save/reload unchanged, so the far more common open-and-save path is unaffected. Co-Authored-By: Claude Opus 4.8 --- src/app/properties.rs | 26 ++++++++++++++------ src/app/update/mod.rs | 39 +++++++++++++++++++++++------ src/scene/annotative.rs | 54 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 14 deletions(-) diff --git a/src/app/properties.rs b/src/app/properties.rs index c0599855a..fa849cabc 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -680,14 +680,26 @@ impl OpenCADStudio { ), ); } - // The read-only text rows get an explicit Yes/No; MLeader - // keeps its editable toggle. + // MTEXT carries a per-object annotative flag, so its + // row is an editable toggle (like MLeader's); the + // style-derived types stay read-only Yes/No. if anno_field == "annotative" { - set_row( - &mut sections, - "annotative", - if is_anno { "Yes" } else { "No" }.to_string(), - ); + if let acadrust::EntityType::MText(t) = entity { + set_row_value( + &mut sections, + "annotative", + crate::scene::model::object::PropValue::BoolToggle { + field: "is_annotative", + value: t.is_annotative, + }, + ); + } else { + set_row( + &mut sections, + "annotative", + if is_anno { "Yes" } else { "No" }.to_string(), + ); + } } if is_anno { // The applied annotation scale follows the current diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index fc5e68102..0a102b83a 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2296,14 +2296,39 @@ impl OpenCADStudio { if !handles.is_empty() { self.push_undo_snapshot(i, "CHPROP"); for &handle in &handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - match field { - "invisible" => { - crate::scene::view::dispatch::toggle_invisible(entity) + match field { + // Per-object annotative flag (MTEXT / MULTILEADER): a + // doc-aware toggle so turning it off also removes the + // per-object annotation context, not just the flag. + "is_annotative" | "enable_annotation_scale" => { + let cur = match self.tabs[i].scene.document.get_entity(handle) { + Some(acadrust::EntityType::MText(t)) => t.is_annotative, + Some(acadrust::EntityType::MultiLeader(m)) => { + m.enable_annotation_scale + } + _ => continue, + }; + crate::scene::annotative::set_entity_annotative( + &mut self.tabs[i].scene.document, + handle, + !cur, + ); + } + "invisible" => { + if let Some(entity) = + self.tabs[i].scene.document.get_entity_mut(handle) + { + crate::scene::view::dispatch::toggle_invisible(entity); + } + } + _ => { + if let Some(entity) = + self.tabs[i].scene.document.get_entity_mut(handle) + { + crate::scene::view::dispatch::apply_geom_prop( + entity, field, "toggle", + ); } - _ => crate::scene::view::dispatch::apply_geom_prop( - entity, field, "toggle", - ), } } } diff --git a/src/scene/annotative.rs b/src/scene/annotative.rs index c1712693f..353777751 100644 --- a/src/scene/annotative.rs +++ b/src/scene/annotative.rs @@ -18,6 +18,60 @@ pub fn as_dict(doc: &CadDocument, handle: Handle) -> Option<&Dictionary> { } } +/// Set the per-object annotative flag on the entity types that carry one +/// (MTEXT, MULTILEADER). Turning it off also strips the per-object annotation +/// context and legacy markers via [`clear_annotation_context`] so the object +/// stops resolving annotative; turning it on leaves the base geometry as the +/// single (implicit, current-scale) representation. Other entity types get +/// their annotative state from a style and are not toggled here. +pub fn set_entity_annotative(doc: &mut CadDocument, handle: Handle, want: bool) { + if let Some(e) = doc.get_entity_mut(handle) { + match e { + EntityType::MText(t) => t.is_annotative = want, + EntityType::MultiLeader(m) => m.enable_annotation_scale = want, + _ => {} + } + } + if !want { + clear_annotation_context(doc, handle); + } +} + +/// Remove an entity's per-object annotation context — the extension-dictionary +/// `AcDbContextDataManager` → `ACDB_ANNOTATIONSCALES` → per-scale leaf subtree — +/// and the legacy annotative XDATA markers, so [`is_annotative`] no longer fires +/// on it. The shared `SCALE` objects in `ACAD_SCALELIST` are document-level and +/// left intact. +pub fn clear_annotation_context(doc: &mut CadDocument, handle: Handle) { + if let Some(xdict_h) = doc.get_entity(handle).and_then(|e| e.common().xdictionary_handle) { + // Collect the manager subtree (manager dict, its scales dict, the leaves) + // before mutating, then drop them. + let mut remove = Vec::new(); + if let Some(mgr_h) = as_dict(doc, xdict_h).and_then(|d| d.get("AcDbContextDataManager")) { + remove.push(mgr_h); + if let Some(scales_h) = + as_dict(doc, mgr_h).and_then(|d| d.get("ACDB_ANNOTATIONSCALES")) + { + remove.push(scales_h); + if let Some(scales) = as_dict(doc, scales_h) { + for (_, leaf) in &scales.entries { + remove.push(*leaf); + } + } + } + } + if let Some(ObjectType::Dictionary(xd)) = doc.objects.get_mut(&xdict_h) { + xd.entries.retain(|(k, _)| k != "AcDbContextDataManager"); + } + for h in remove { + doc.objects.remove(&h); + } + } + // Strip the legacy annotative XDATA markers the detection also honours. + crate::scene::view::dispatch::set_entity_xdata(doc, handle, "AcAnnoPO", None); + crate::scene::view::dispatch::set_entity_xdata(doc, handle, "AcAnnotativeData", None); +} + /// Does a style name resolve to `name` (or to "Standard" when `name` is blank)? fn name_matches(style_name: &str, name: &str) -> bool { style_name.eq_ignore_ascii_case(name)