feat(properties): editable Annotative toggle for MText and MLeader

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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-13 09:32:22 +03:00
commit fc5e93dcc1
3 changed files with 105 additions and 14 deletions

View file

@ -680,14 +680,26 @@ impl OpenCADStudio {
), ),
); );
} }
// The read-only text rows get an explicit Yes/No; MLeader // MTEXT carries a per-object annotative flag, so its
// keeps its editable toggle. // row is an editable toggle (like MLeader's); the
// style-derived types stay read-only Yes/No.
if anno_field == "annotative" { if anno_field == "annotative" {
set_row( if let acadrust::EntityType::MText(t) = entity {
&mut sections, set_row_value(
"annotative", &mut sections,
if is_anno { "Yes" } else { "No" }.to_string(), "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 { if is_anno {
// The applied annotation scale follows the current // The applied annotation scale follows the current

View file

@ -2296,14 +2296,39 @@ impl OpenCADStudio {
if !handles.is_empty() { if !handles.is_empty() {
self.push_undo_snapshot(i, "CHPROP"); self.push_undo_snapshot(i, "CHPROP");
for &handle in &handles { for &handle in &handles {
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { match field {
match field { // Per-object annotative flag (MTEXT / MULTILEADER): a
"invisible" => { // doc-aware toggle so turning it off also removes the
crate::scene::view::dispatch::toggle_invisible(entity) // 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",
),
} }
} }
} }

View file

@ -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)? /// Does a style name resolve to `name` (or to "Standard" when `name` is blank)?
fn name_matches(style_name: &str, name: &str) -> bool { fn name_matches(style_name: &str, name: &str) -> bool {
style_name.eq_ignore_ascii_case(name) style_name.eq_ignore_ascii_case(name)