forked from 1b-agent/opencadstudio-upstream
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:
1 parent
31d3023167
commit
c0844ad55c
14 files changed
+922
-30
No files matched your search
@@ -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."));
|
||||
|
||||
@@ -311,6 +311,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"ANNOSCALE",
|
||||
"CANNOSCALE",
|
||||
"SCALELISTEDIT",
|
||||
"OBJECTSCALE",
|
||||
// Import CSV into a table + LandXML survey points.
|
||||
"DATALINK",
|
||||
"LANDXMLIMPORT",
|
||||
|
||||
@@ -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
@@ -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
@@ -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) =
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user