feat(annotative): synthesize per-object contexts + editable UI

Make objects genuinely annotative — carrying real per-scale
representations that interoperate with other CAD apps — rather than just
a native flag. `create_annotation_context` synthesizes the extension-dict
chain (AcDbContextDataManager -> ACDB_ANNOTATIONSCALES -> per-scale leaf)
from the entity's placement; the editable Annotative toggle now covers
Text and block references as well as MText/MLeader; and a new
"Annotation Object Scale" dialog (OBJECTSCALE) adds/removes an object's
per-scale memberships. Requires the acadrust ObjectContextData encoder
(Cargo.lock bump).

Also fixes a render bug where objects carrying an *empty*
ACDB_ANNOTATIONSCALES (a single-representation marker with no per-scale
reps) were treated as annotative and (mis)scaled by the annotation factor
in scaled paper viewports, ballooning the text: an object is now
annotative-by-context only when its scale collection is non-empty, so
such objects render at their base geometry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-13 22:38:29 +03:00
1 parent 31d3023167
commit c0844ad55c
14 files changed
+922 -30

No files matched your search

+7 -1
View File
@@ -780,6 +780,12 @@ impl OpenCADStudio {
}
}
// OBJECTSCALE — open the Annotation Object Scale dialog for the
// selected object (add / remove its per-object scale representations).
"OBJECTSCALE" => {
return Some(Task::done(Message::AnnoObjectScaleOpen));
}
// DATALINK <path.csv> — import a CSV file into a table placed at the
// origin (one-time import; a live re-reading link is future work).
cmd if cmd == "DATALINK" || cmd.starts_with("DATALINK ") => {
@@ -873,7 +879,7 @@ impl OpenCADStudio {
}
}
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" | "UNDERLAYLAYERS" | "OBJECTSCALE"
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" | "UNDERLAYLAYERS"
| "UOSNAP" => {
self.command_line
.push_info(&format!("{cmd}: not yet implemented."));
+1
View File
@@ -311,6 +311,7 @@ inventory::submit!(crate::command::CommandRegistration {
"ANNOSCALE",
"CANNOSCALE",
"SCALELISTEDIT",
"OBJECTSCALE",
// Import CSV into a table + LandXML survey points.
"DATALINK",
"LANDXMLIMPORT",
+11
View File
@@ -666,6 +666,9 @@ pub(super) struct OpenCADStudio {
/// The scale row being renamed inline (double-click), + its edit buffer.
scale_rename: Option<String>,
scale_rename_buf: String,
/// The entity the Annotation Object Scale dialog is editing (its per-object
/// annotation-scale membership).
anno_object_scale_target: Option<acadrust::types::Handle>,
/// Open transaction for the scale manager — restored if the window closes
/// without Apply, mirroring the style managers' staging.
scale_stage: Option<crate::app::style_ops::ScaleStage>,
@@ -1118,6 +1121,9 @@ pub enum ModalKind {
LayerDeleteWarning,
Aliases,
ScaleManager,
/// Add / remove the annotation scales a single selected object has a
/// per-object representation for.
AnnoObjectScale,
}
/// Identifies a DimStyle field that can be edited in the dialog.
@@ -1469,6 +1475,10 @@ pub enum Message {
CloseScalePopup,
/// Open the annotation-scale manager (from the scale popup's Manage row).
ScaleManagerOpen,
/// Open the Annotation Object Scale dialog for the current single selection.
AnnoObjectScaleOpen,
/// Toggle whether the dialog's object has a representation for this scale.
AnnoObjectScaleToggle(String),
/// Select a scale row in the manager (loads it into the editor).
ScaleManagerSelect(String),
/// Add a new scale to the list (staged) and select it for editing.
@@ -2269,6 +2279,7 @@ impl OpenCADStudio {
scale_manager_paper_buf: String::new(),
scale_manager_drawing_buf: String::new(),
scale_rename: None,
anno_object_scale_target: None,
scale_rename_buf: String::new(),
scale_stage: None,
layout_manager_rename_buf: String::new(),
+20 -9
View File
@@ -669,6 +669,7 @@ impl OpenCADStudio {
let anno: Option<(&str, Option<&str>)> = match entity {
acadrust::EntityType::Text(_)
| acadrust::EntityType::MText(_)
| acadrust::EntityType::Insert(_)
| acadrust::EntityType::Leader(_) => Some(("annotative", None)),
acadrust::EntityType::MultiLeader(_) => {
Some(("enable_annotation_scale", None))
@@ -696,25 +697,35 @@ impl OpenCADStudio {
),
);
}
// 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.
// Objects that carry a per-object annotation context
// (MTEXT via its native flag, single-line TEXT via the
// context alone) get an editable toggle: turning it on
// synthesizes a real per-scale representation. The
// remaining types are style-driven and stay read-only.
if anno_field == "annotative" {
if let acadrust::EntityType::MText(t) = entity {
set_row_value(
match entity {
acadrust::EntityType::MText(t) => set_row_value(
&mut sections,
"annotative",
crate::scene::model::object::PropValue::BoolToggle {
field: "is_annotative",
value: t.is_annotative,
},
);
} else {
set_row(
),
acadrust::EntityType::Text(_)
| acadrust::EntityType::Insert(_) => set_row_value(
&mut sections,
"annotative",
crate::scene::model::object::PropValue::BoolToggle {
field: "annotative_ctx",
value: is_anno,
},
),
_ => set_row(
&mut sections,
"annotative",
if is_anno { "Yes" } else { "No" }.to_string(),
);
),
}
}
if is_anno {
+80 -6
View File
@@ -1681,6 +1681,57 @@ impl OpenCADStudio {
self.active_modal = Some(crate::app::ModalKind::ScaleManager);
Task::none()
}
Message::AnnoObjectScaleOpen => {
// The dialog edits a single object's per-scale memberships.
let i = self.active_tab;
let handles = self.property_target_handles(i);
if handles.len() == 1 {
// Only object types that carry a per-object context.
let ok = matches!(
self.tabs[i].scene.document.get_entity(handles[0]),
Some(
acadrust::EntityType::Text(_)
| acadrust::EntityType::MText(_)
| acadrust::EntityType::Insert(_)
)
);
if ok {
self.anno_object_scale_target = Some(handles[0]);
self.active_modal = Some(crate::app::ModalKind::AnnoObjectScale);
} else {
self.command_line.push_info(
"OBJECTSCALE applies to a single Text, MText or block reference.",
);
}
} else {
self.command_line
.push_info("Select one object first, then run OBJECTSCALE.");
}
Task::none()
}
Message::AnnoObjectScaleToggle(name) => {
let i = self.active_tab;
if let Some(entity) = self.anno_object_scale_target {
if let Some(sh) = self.tabs[i].scene.scale_handle_ensuring(&name) {
self.push_undo_snapshot(i, "OBJECTSCALE");
let doc = &mut self.tabs[i].scene.document;
let is_member = crate::scene::annotative::object_scale_memberships(doc, entity)
.iter()
.any(|(_, h)| *h == sh);
if is_member {
crate::scene::annotative::remove_annotation_context_for_scale(
doc, entity, sh,
);
} else {
crate::scene::annotative::create_annotation_context(doc, entity, sh);
}
self.tabs[i].dirty = true;
self.invalidate_property_targets(i, &[entity]);
self.refresh_properties();
}
}
Task::none()
}
Message::ScaleManagerSelect(name) => {
// Stage the current editor edits before switching so they aren't
// lost, then load the newly-selected scale.
@@ -2472,22 +2523,45 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "CHPROP");
for &handle in &handles {
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) {
// Per-object annotative toggle: MTEXT/MULTILEADER carry
// a native flag; single-line TEXT is annotative purely
// by the presence of a per-object context. A doc-aware
// toggle so turning it on synthesizes a real per-scale
// representation and turning it off removes it (not just
// the flag).
"is_annotative" | "enable_annotation_scale" | "annotative_ctx" => {
let doc = &self.tabs[i].scene.document;
let cur = match doc.get_entity(handle) {
Some(acadrust::EntityType::MText(t)) => t.is_annotative,
Some(acadrust::EntityType::MultiLeader(m)) => {
m.enable_annotation_scale
}
_ => continue,
// TEXT (and any other context-only type): its
// annotative state is whether a context exists.
Some(e) => crate::scene::annotative::is_annotative(doc, e),
None => continue,
};
crate::scene::annotative::set_entity_annotative(
&mut self.tabs[i].scene.document,
handle,
!cur,
);
// Turning it on also gives the object a real
// per-scale representation at the current
// annotation scale (not just the native flag),
// so it interoperates as a genuine annotative
// object. Off is handled inside set_entity_*.
if !cur {
if let Some(sh) =
self.tabs[i].scene.current_annotation_scale_handle()
{
crate::scene::annotative::create_annotation_context(
&mut self.tabs[i].scene.document,
handle,
sh,
);
}
}
}
"invisible" => {
if let Some(entity) =
+1
View File
@@ -1574,6 +1574,7 @@ impl OpenCADStudio {
LayerDeleteWarning => (440, 200),
Aliases => (480, 520),
ScaleManager => (520, 360),
AnnoObjectScale => (360, 420),
};
Some((w as f32 + EXTRA_W, h as f32 + EXTRA_H))
}
+46
View File
@@ -95,6 +95,52 @@ impl OpenCADStudio {
360,
)
}
super::super::ModalKind::AnnoObjectScale => {
let tab = &self.tabs[self.active_tab];
let entity = self.anno_object_scale_target;
// Which scales the object currently has a representation for.
let members: Vec<acadrust::types::Handle> = entity
.map(|h| {
crate::scene::annotative::object_scale_memberships(
&tab.scene.document,
h,
)
.into_iter()
.map(|(_, sh)| sh)
.collect()
})
.unwrap_or_default();
let label = entity
.and_then(|h| tab.scene.document.get_entity(h))
.map(|e| match e {
acadrust::EntityType::Text(_) => "TEXT",
acadrust::EntityType::MText(_) => "MTEXT",
acadrust::EntityType::Insert(_) => "BLOCK",
acadrust::EntityType::MultiLeader(_) => "MULTILEADER",
_ => "OBJECT",
})
.unwrap_or("—");
let scales: Vec<(String, String, bool)> = tab
.scene
.scale_list()
.into_iter()
.map(|(name, _, _)| {
let sh = tab.scene.scale_object_handle(&name);
let ratio = tab
.scene
.scale_paper_drawing(&name)
.map(|(p, d)| format!("{p}:{d}"))
.unwrap_or_default();
let is_member = sh.map(|h| members.contains(&h)).unwrap_or(false);
(name, ratio, is_member)
})
.collect();
sized(
crate::ui::style::anno_object_scale::view_window(&label, &scales),
360,
420,
)
}
super::super::ModalKind::Plotstyle => sized(
crate::ui::style::plotstyle::view_window(
self.active_plot_style.as_ref(),