feat(annotation): integrate object contexts

This commit is contained in:
Hakan Seven 2026-08-02 12:00:15 +03:00
commit e03e091b3c
37 changed files with 2820 additions and 581 deletions

View file

@ -3155,7 +3155,11 @@ impl OpenCADStudio {
self.tabs[i].scene.add_entity_clone(entity)
})
.collect();
self.merge_clipboard_ext_objects(i, &by_index);
let annotation_delta = match translate {
Some(crate::command::EntityTransform::Translate(delta)) => delta,
_ => glam::DVec3::ZERO,
};
self.merge_clipboard_ext_objects(i, &by_index, annotation_delta);
// Recreate any group whose whole membership was copied, so a pasted
// group stays grouped — cross-drawing too, since the groups were
// snapshotted into the clipboard at copy time. `by_index` is aligned
@ -3187,7 +3191,12 @@ impl OpenCADStudio {
/// references, and re-pointing the pasted entity's `xdictionary_handle` at
/// the new root. `by_index` is the paste's new entity handles, aligned with
/// the clipboard order (NULL where the add failed). No-op without captures.
pub(super) fn merge_clipboard_ext_objects(&mut self, i: usize, by_index: &[Handle]) {
pub(super) fn merge_clipboard_ext_objects(
&mut self,
i: usize,
by_index: &[Handle],
annotation_delta: glam::DVec3,
) {
if self.clipboard_deps.ext_objects.is_empty() {
return;
}
@ -3204,6 +3213,11 @@ impl OpenCADStudio {
if let Some(e) = doc.get_entity_mut(new_entity) {
e.common_mut().xdictionary_handle = Some(new_root);
}
crate::scene::annotative::translate_annotation_contexts(
doc,
new_entity,
annotation_delta,
);
}
}
// The wires were tessellated before the filters existed; refresh only
@ -3270,6 +3284,10 @@ fn recreate_ext_subtree(
if let Some(eh) = entity_handle {
remap.insert(cap.src_entity_handle, eh);
}
for (old, scale) in &cap.annotation_scales {
let target = crate::scene::annotative::ensure_scale_object(doc, scale);
remap.insert(*old, target);
}
for (old, _) in &cap.objects {
remap.insert(*old, doc.allocate_handle());
}
@ -3282,6 +3300,50 @@ fn recreate_ext_subtree(
remap.get(&cap.root).copied()
}
/// Replace references to a clipboard entity inside one recreated extension
/// dictionary graph after its final block-owned handle becomes known.
pub(crate) fn remap_ext_subtree_reference(
doc: &mut acadrust::CadDocument,
root: Handle,
source_entity: Handle,
target_entity: Handle,
) {
use acadrust::objects::ObjectType;
use rustc_hash::FxHashSet;
use std::collections::HashMap;
let remap = HashMap::from([(source_entity, target_entity)]);
let mut seen = FxHashSet::default();
let mut pending = vec![root];
while let Some(handle) = pending.pop() {
if handle.is_null() || !seen.insert(handle) {
continue;
}
let children = match doc.objects.get(&handle) {
Some(ObjectType::Dictionary(dictionary)) => {
let mut children: Vec<_> =
dictionary.entries.iter().map(|(_, child)| *child).collect();
if let Some(extension) = dictionary.xdictionary_handle {
children.push(extension);
}
children
}
Some(ObjectType::DictionaryWithDefault(dictionary)) => {
let mut children: Vec<_> =
dictionary.entries.iter().map(|(_, child)| *child).collect();
children.push(dictionary.default_handle);
children
}
_ => Vec::new(),
};
pending.extend(children);
if let Some(mut object) = doc.objects.remove(&handle) {
remap_object(&mut object, handle, &remap);
doc.objects.insert(handle, object);
}
}
}
/// Rewrite a cloned extension-dictionary object onto fresh handles: set its own
/// handle to `new_handle` and remap its owner and any handle references it holds
/// through `remap` (a handle still in the source space stays unchanged, which is
@ -3326,6 +3388,11 @@ fn remap_object(
ObjectType::XRecord(x) => {
x.handle = new_handle;
x.owner = map(x.owner);
for entry in &mut x.entries {
if let acadrust::objects::XRecordValue::Handle(handle) = &mut entry.value {
*handle = map(*handle);
}
}
}
ObjectType::Group(g) => {
g.handle = new_handle;
@ -3334,6 +3401,59 @@ fn remap_object(
*h = map(*h);
}
}
ObjectType::ObjectContextData(context) => {
context.handle = new_handle;
context.owner_handle = map(context.owner_handle);
for reactor in &mut context.reactors {
*reactor = map(*reactor);
}
if let Some(dictionary) = &mut context.xdictionary_handle {
*dictionary = map(*dictionary);
}
context.scale = map(context.scale);
match &mut context.kind {
acadrust::objects::ObjectContextKind::Dim(dimension) => {
dimension.block = map(dimension.block);
}
acadrust::objects::ObjectContextKind::HatchView(hatch) => {
hatch.view = map(hatch.view);
}
acadrust::objects::ObjectContextKind::MTextAttribute(attribute) => {
if let Some(embedded) = &mut attribute.context {
embedded.owner_handle = map(embedded.owner_handle);
for reactor in &mut embedded.reactors {
*reactor = map(*reactor);
}
if let Some(dictionary) = &mut embedded.xdictionary_handle {
*dictionary = map(*dictionary);
}
embedded.scale = map(embedded.scale);
}
}
acadrust::objects::ObjectContextKind::MLeader(mleader) => {
if let Some(handle) = &mut mleader.text_style_handle {
*handle = map(*handle);
}
if let Some(handle) = &mut mleader.block_content_handle {
*handle = map(*handle);
}
if let Some(handle) = &mut mleader.scale_handle {
*handle = map(*handle);
}
for root in &mut mleader.leader_roots {
for line in &mut root.lines {
if let Some(handle) = &mut line.line_type_handle {
*handle = map(*handle);
}
if let Some(handle) = &mut line.arrowhead_handle {
*handle = map(*handle);
}
}
}
}
_ => {}
}
}
// Other leaf object kinds don't appear in an entity xdictionary; if one
// does, it's inserted with the fresh handle below via the caller's key,
// but its internal owner is left as-is (best effort).

View file

@ -246,7 +246,7 @@ impl OpenCADStudio {
let name = self.unique_block_name("Block");
let base = self.clipboard_base;
let mut entities = self.clipboard.clone();
for (idx, root) in ext_roots {
for (&idx, &root) in &ext_roots {
if let Some(e) = entities.get_mut(idx) {
e.common_mut().xdictionary_handle = Some(root);
}
@ -255,7 +255,32 @@ impl OpenCADStudio {
.scene
.define_block_from_owned_entities(entities, &name, base)
{
Ok(()) => {
Ok(entity_handles) => {
let remaps: Vec<_> = ext_roots
.iter()
.filter_map(|(&idx, &root)| {
Some((
root,
self.clipboard.get(idx)?.common().handle,
*entity_handles.get(idx)?,
))
})
.collect();
let scene = &mut self.tabs[i].scene;
for (root, source, target) in remaps {
super::super::command_driver::remap_ext_subtree_reference(
&mut scene.document,
root,
source,
target,
);
crate::scene::annotative::translate_annotation_contexts(
&mut scene.document,
target,
-base,
);
}
scene.bump_geometry();
// Block defined; now place it interactively so the
// user picks the drop point (insertion uses the
// clipboard lower-left corner as the block's base). The

View file

@ -624,12 +624,9 @@ impl OpenCADStudio {
// still being built. Acknowledge them with an honest status so the
// button responds instead of reporting an unknown command; each is
// replaced by its real handler as the feature lands.
// OBJECTSCALE ADD — the ribbon "Add Scale" quick action: mark the
// selected objects annotative by attaching the AcAnnotativeData XData
// record the tessellator already honours, so they scale with the
// current annotation scale. Bare OBJECTSCALE opens the dialog below.
// OBJECTSCALE ADD — add the active scale representation to every
// selected object that supports per-scale context data.
"OBJECTSCALE ADD" => {
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
let handles: Vec<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
@ -642,15 +639,23 @@ impl OpenCADStudio {
return Some(Task::none());
}
self.push_undo_snapshot(i, "OBJECTSCALE");
let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() else {
self.command_line
.push_error("OBJECTSCALE: the active annotation scale is unavailable.");
return Some(Task::none());
};
let mut n = 0usize;
for h in &handles {
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
let xd = &mut e.common_mut().extended_data;
if xd.get_record("AcAnnotativeData").is_none() {
let mut rec = ExtendedDataRecord::new("AcAnnotativeData");
rec.add_value(XDataValue::String("1".to_string()));
xd.add_record(rec);
}
if crate::scene::annotative::create_annotation_context(
&mut self.tabs[i].scene.document,
*h,
scale,
) {
crate::scene::annotative::set_entity_annotative(
&mut self.tabs[i].scene.document,
*h,
true,
);
n += 1;
}
}
@ -661,7 +666,7 @@ impl OpenCADStudio {
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.command_line.push_output(&format!(
"OBJECTSCALE: marked {n} object(s) annotative (they scale with the annotation scale)."
"OBJECTSCALE: added the active scale to {n} object(s)."
));
return Some(Task::none());
}
@ -810,6 +815,85 @@ impl OpenCADStudio {
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
"ANNOALLVISIBLE" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new(
"ANNOALLVISIBLE",
"ANNOALLVISIBLE new value [0/1]:",
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("ANNOALLVISIBLE ") => {
let value = cmd.split_whitespace().nth(1).unwrap_or("");
match value {
"0" | "OFF" | "FALSE" => {
self.tabs[i].scene.set_annotation_all_visible(false);
self.tabs[i].dirty = true;
}
"1" | "ON" | "TRUE" => {
self.tabs[i].scene.set_annotation_all_visible(true);
self.tabs[i].dirty = true;
}
_ => self
.command_line
.push_error("ANNOALLVISIBLE: enter 0 or 1."),
}
}
"ANNOAUTOSCALE" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new(
"ANNOAUTOSCALE",
"ANNOAUTOSCALE new value [-4..4]:",
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("ANNOAUTOSCALE ") => {
let value = cmd.split_whitespace().nth(1).unwrap_or("");
match value.parse::<i8>() {
Ok(mode @ -4..=4) => self.annotation_auto_scale = mode,
_ => self.command_line.push_error(
"ANNOAUTOSCALE: enter an integer from -4 through 4.",
),
}
}
"ANNOUPDATE" => {
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.iter()
.map(|(handle, _)| *handle)
.collect();
if handles.is_empty() {
self.command_line
.push_error("ANNOUPDATE: select annotation objects first.");
return Some(Task::none());
}
self.push_undo_snapshot(i, "ANNOUPDATE");
let scale = self.tabs[i].scene.creation_annotation_scale_handle();
let mut updated = 0usize;
for handle in &handles {
if crate::scene::annotative::update_entity_from_annotation_style(
&mut self.tabs[i].scene.document,
*handle,
scale,
) {
updated += 1;
}
}
if updated > 0 {
let changes: Vec<_> = handles
.into_iter()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
}
self.command_line
.push_output(&format!("ANNOUPDATE: updated {updated} object(s)."));
return Some(Task::none());
}
cmd if cmd.starts_with("ANNOSCALE ") || cmd.starts_with("CANNOSCALE ") => {
let arg = cmd
.split_whitespace()
@ -828,27 +912,21 @@ impl OpenCADStudio {
.push_output(&format!("Current annotation scale: {name}"));
return Some(Task::none());
}
// anno multiplier = denominator / numerator: 1:50 → 50, 2:1 → 0.5.
let anno = if let Some((a, b)) = arg.split_once(':') {
match (a.trim().parse::<f64>(), b.trim().parse::<f64>()) {
(Ok(a), Ok(b)) if a != 0.0 => Some((b / a) as f32),
_ => None,
}
} else {
arg.parse::<f32>().ok()
};
match anno {
Some(v) if v > 0.0 => {
self.tabs[i].scene.annotation_scale = v;
let hdr = &mut self.tabs[i].scene.document.header;
hdr.current_annotation_scale = arg.clone();
hdr.annotation_scale_value = 1.0 / v as f64;
self.tabs[i].scene.invalidate_annotation_dependencies();
let previous = self.tabs[i].scene.displayed_annotation_scale_handle();
match self.tabs[i].scene.set_annotation_scale_named(&arg) {
Some(handle) => {
if self.annotation_auto_scale > 0 {
self.tabs[i].scene.add_annotation_scale_to_objects(
handle,
previous,
self.annotation_auto_scale as u8,
);
}
self.tabs[i].dirty = true;
self.command_line
.push_output(&format!("Annotation scale: {arg}"));
}
_ => self
None => self
.command_line
.push_error("Usage: ANNOSCALE <ratio> e.g. 1:50, 2:1, or a factor"),
}

View file

@ -336,6 +336,9 @@ inventory::submit!(crate::command::CommandRegistration {
// Annotation scale.
"ANNOSCALE",
"CANNOSCALE",
"ANNOALLVISIBLE",
"ANNOAUTOSCALE",
"ANNOUPDATE",
"SCALELISTEDIT",
"OBJECTSCALE",
// Import CSV into a table + LandXML survey points.

View file

@ -27,6 +27,8 @@ pub struct AppConfig {
pub start: StartConfig,
/// Which status-bar pills the user has hidden.
pub statusbar: StatusBarConfig,
/// Add a newly selected annotation scale to existing annotative objects.
pub annotation_auto_scale: i8,
/// Ribbon collapse density.
pub ribbon: RibbonConfig,
/// Print dialog preferences (only the persisted fields; runtime state is
@ -42,6 +44,7 @@ impl Default for AppConfig {
recent: RecentConfig::default(),
start: StartConfig::default(),
statusbar: StatusBarConfig::default(),
annotation_auto_scale: -4,
ribbon: RibbonConfig::default(),
plot: PlotDialogState::default(),
}

View file

@ -349,6 +349,8 @@ pub(super) struct OpenCADStudio {
cycle_candidates: Option<(iced::Point, Vec<acadrust::Handle>)>,
/// Which status-bar pills the user has chosen to show (persisted).
statusbar_config: crate::ui::statusbar::statusbar_config::StatusBarConfig,
/// Add selected scales to existing annotative objects.
annotation_auto_scale: i8,
/// Last persisted user preferences (DYN/OSNAP/OTRACK/POLAR/…). Compared
/// after each message so a change is written to disk exactly once.
last_saved_config: Option<config::AppConfig>,
@ -1093,6 +1095,7 @@ pub struct ClipExtObjects {
pub src_entity_handle: acadrust::Handle,
pub root: acadrust::Handle,
pub objects: Vec<(acadrust::Handle, acadrust::objects::ObjectType)>,
pub annotation_scales: Vec<(acadrust::Handle, acadrust::objects::Scale)>,
}
impl ClipboardDeps {
@ -1154,11 +1157,29 @@ impl ClipboardDeps {
}
let objects = Self::collect_ext_subtree(doc, root);
if !objects.is_empty() {
let mut annotation_scales = Vec::new();
for (_, object) in &objects {
let acadrust::objects::ObjectType::ObjectContextData(context) = object else {
continue;
};
if annotation_scales
.iter()
.any(|(handle, _)| *handle == context.scale)
{
continue;
}
if let Some(acadrust::objects::ObjectType::Scale(scale)) =
doc.objects.get(&context.scale)
{
annotation_scales.push((context.scale, scale.clone()));
}
}
ext_objects.push(ClipExtObjects {
entity_index,
src_entity_handle: c.handle,
root,
objects,
annotation_scales,
});
}
}
@ -1873,9 +1894,12 @@ pub enum Message {
/// Apply the typed custom polar angle (Enter in the picker's field).
SubmitPolarCustom,
/// Set the model-space annotation scale (CANNOSCALE equivalent).
SetAnnotationScale(f32),
SetAnnotationScale(String),
/// Set the active viewport's custom_scale (paper space).
SetViewportScale(f64),
SetViewportScale(String),
ToggleAnnotationVisibility,
ToggleAnnotationAutoAdd,
SyncViewportAnnotationScale,
/// Toggle the scale picker popup open/closed.
ToggleScalePopup,
/// Close the scale picker popup.
@ -2667,6 +2691,7 @@ impl OpenCADStudio {
selection_filter_popup_open: false,
status_menu_tooltip_hidden: false,
statusbar_config: crate::ui::statusbar::statusbar_config::StatusBarConfig::default(),
annotation_auto_scale: -4,
last_saved_config: None,
otrack_active: None,
clean_screen: false,

View file

@ -657,6 +657,7 @@ impl super::OpenCADStudio {
1.0,
anno,
None,
None,
bg,
// Editor preview draws on a 2D canvas with no SDF shader — force the
// glyph outline strokes so the text is visible (#308).

View file

@ -158,6 +158,7 @@ impl OpenCADStudio {
};
crate::scene::view::dispatch::set_prop_current_vertex(prop_vertex);
let annotation_scale_handle = self.tabs[i].scene.displayed_annotation_scale_handle();
let new_panel = {
let selected = self.tabs[i].scene.selected_entities();
let mut panel = match selected.len() {
@ -368,9 +369,10 @@ impl OpenCADStudio {
}
1 => {
let (handle, source_entity) = selected[0];
let contextual = crate::scene::annotative::entity_for_active_context(
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.tabs[i].scene.document,
source_entity,
annotation_scale_handle,
);
let entity = contextual.as_ref();
let group_names = self.tabs[i].scene.group_names_for_entity(handle);
@ -1688,14 +1690,16 @@ impl OpenCADStudio {
[0.0_f64; 3]
};
let (new_handle, new_grips, new_grip_handles) = {
let annotation_scale_handle = self.tabs[i].scene.displayed_annotation_scale_handle();
let selected = self.tabs[i].scene.selected_entities();
let single_handle = (selected.len() == 1).then(|| selected[0].0);
let mut grips = Vec::new();
let mut handles = Vec::new();
for (handle, entity) in selected {
let contextual = crate::scene::annotative::entity_for_active_context(
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.tabs[i].scene.document,
entity,
annotation_scale_handle,
);
for mut grip in dispatch::grips(contextual.as_ref()) {
// Subtract in f64: at UTM magnitudes an f32 cast before
@ -1738,11 +1742,9 @@ impl OpenCADStudio {
pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) {
let mut context_object_changed = false;
for &handle in handles {
context_object_changed |=
crate::scene::annotative::sync_active_context_from_entity(
&mut self.tabs[i].scene.document,
handle,
);
context_object_changed |= self.tabs[i]
.scene
.sync_displayed_annotation_context(handle);
// Hatch / SOLID fills render from prebuilt cached models; rebuild
// them or pattern edits (scale, background, …) stay invisible
// (#415).
@ -1790,22 +1792,6 @@ impl OpenCADStudio {
entity.as_entity_mut().set_layer(layer.clone());
}
// A new dimension adopts the current dimension style (DIMSTYLE), like
// AutoCAD — the DIM commands leave the default "Standard" on the entity,
// so stamp the header's current style here. ADDSELECTED sets DIMSTYLE to
// the template's first, so a cloned dimension keeps its style (#239).
if let acadrust::EntityType::Dimension(ref mut d) = entity {
let cur = self.tabs[i]
.scene
.document
.header
.current_dimstyle_name
.clone();
if !cur.trim().is_empty() {
d.base_mut().style_name = cur;
}
}
// INSUNITS: when inserting a block whose BlockRecord.units differ
// from the host's header.insertion_units, scale the new INSERT so
// 1 source-unit equals the matching host length. When either side
@ -1889,45 +1875,8 @@ impl OpenCADStudio {
_ => None,
});
if let Some((h, s)) = found {
ml.style_handle = Some(h);
// Inherit the style's settings so a new multileader
// reflects the current MLeaderStyle (the renderer reads
// these entity fields). See #94.
// The entity and style enums are distinct types with
// matching discriminants — round-trip through i16.
ml.content_type = (s.content_type as i16).into();
ml.path_type = (s.path_type as i16).into();
ml.line_color = s.line_color;
ml.line_type_handle = s.line_type_handle;
ml.line_weight = s.line_weight;
ml.enable_landing = s.enable_landing;
ml.enable_dogleg = s.enable_dogleg;
ml.dogleg_length = s.landing_distance;
ml.arrowhead_handle = s.arrowhead_handle;
ml.arrowhead_size = s.arrowhead_size;
ml.text_style_handle = s.text_style_handle;
ml.text_color = s.text_color;
ml.text_frame = s.text_frame;
ml.text_height = s.text_height;
ml.context.text_height = s.text_height;
ml.text_left_attachment = (s.text_left_attachment as i16).into();
ml.text_right_attachment = (s.text_right_attachment as i16).into();
ml.text_top_attachment = (s.text_top_attachment as i16).into();
ml.text_bottom_attachment = (s.text_bottom_attachment as i16).into();
ml.text_attachment_direction =
(s.text_attachment_direction as i16).into();
ml.text_alignment = (s.text_alignment as i16).into();
ml.text_angle_type = (s.text_angle_type as i16).into();
ml.block_content_handle = s.block_content_handle;
ml.block_content_color = s.block_content_color;
ml.block_connection_type = (s.block_content_connection as i16).into();
ml.block_rotation = s.block_content_rotation;
ml.block_scale = acadrust::types::Vector3::new(
s.block_content_scale_x,
s.block_content_scale_y,
s.block_content_scale_z,
);
ml.scale_factor = s.scale_factor;
debug_assert_eq!(h, s.handle);
crate::scene::annotative::apply_mleader_style(ml, &s);
}
}
}
@ -1955,6 +1904,52 @@ impl OpenCADStudio {
_ => {}
}
let text_style_annotative = match &entity {
acadrust::EntityType::Text(text) => {
crate::scene::annotative::text_style_is_annotative(
&self.tabs[i].scene.document,
&text.style,
)
}
acadrust::EntityType::MText(text) => {
crate::scene::annotative::text_style_is_annotative(
&self.tabs[i].scene.document,
&text.style,
)
}
acadrust::EntityType::AttributeEntity(attribute) => {
crate::scene::annotative::text_style_is_annotative(
&self.tabs[i].scene.document,
&attribute.text_style,
)
}
acadrust::EntityType::AttributeDefinition(attribute) => {
crate::scene::annotative::text_style_is_annotative(
&self.tabs[i].scene.document,
&attribute.text_style,
)
}
_ => false,
};
if text_style_annotative {
match &mut entity {
acadrust::EntityType::MText(text) => text.is_annotative = true,
acadrust::EntityType::AttributeEntity(attribute) => {
attribute.flags.annotative = true
}
acadrust::EntityType::AttributeDefinition(attribute) => {
attribute.flags.annotative = true
}
_ => {}
}
}
let needs_annotation_context = crate::scene::annotative::is_annotative(
&self.tabs[i].scene.document,
&entity,
) || crate::scene::annotative::annotation_style_is_annotative(
&self.tabs[i].scene.document,
&entity,
);
let new_handle = if matches!(&entity, acadrust::EntityType::Viewport(_))
&& self.tabs[i].scene.current_layout != "Model"
@ -2011,6 +2006,22 @@ impl OpenCADStudio {
Some(self.tabs[i].scene.add_entity(entity))
};
if needs_annotation_context {
if let (Some(handle), Some(scale)) = (
new_handle,
self.tabs[i].scene.creation_annotation_scale_handle(),
) {
crate::scene::annotative::create_annotation_context(
&mut self.tabs[i].scene.document,
handle,
scale,
);
self.tabs[i]
.scene
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
}
}
if tracks_draw_anchor {
if let Some(handle) = new_handle {
self.tabs[i].last_draw_anchor = Some(handle);

View file

@ -301,6 +301,124 @@ impl OpenCADStudio {
}
}
fn style_in_use(&self, kind: StyleKind, name: &str) -> bool {
use acadrust::entities::EntityType;
let i = self.active_tab;
let doc = &self.tabs[i].scene.document;
match kind {
StyleKind::Text => {
if doc.header.current_text_style_name.eq_ignore_ascii_case(name) {
return true;
}
let style_handle = doc.text_styles.get(name).map(|style| style.handle);
let referenced_by_entity = doc.entities().any(|entity| match entity {
EntityType::Text(text) => text.style.eq_ignore_ascii_case(name),
EntityType::MText(text) => text.style.eq_ignore_ascii_case(name),
EntityType::AttributeEntity(attribute) => {
attribute.text_style.eq_ignore_ascii_case(name)
}
EntityType::AttributeDefinition(attribute) => {
attribute.text_style.eq_ignore_ascii_case(name)
}
EntityType::Insert(insert) => insert.attributes.iter().any(|attribute| {
attribute.text_style.eq_ignore_ascii_case(name)
}),
EntityType::MultiLeader(leader) => [
leader.text_style_handle,
leader.context.text_style_handle,
]
.into_iter()
.flatten()
.any(|handle| Some(handle) == style_handle),
EntityType::Table(table) => table.rows.iter().any(|row| {
row.style
.as_ref()
.and_then(|style| style.text_style_handle)
.is_some_and(|handle| Some(handle) == style_handle)
|| row.cells.iter().any(|cell| {
cell.style
.as_ref()
.and_then(|style| style.text_style_handle)
.is_some_and(|handle| Some(handle) == style_handle)
|| cell.contents.iter().any(|content| {
content
.text_style_handle
.is_some_and(|handle| Some(handle) == style_handle)
})
})
}),
_ => false,
});
referenced_by_entity
|| doc
.dim_styles
.iter()
.any(|style| style.dimtxsty.eq_ignore_ascii_case(name))
|| doc.objects.values().any(|object| match object {
ObjectType::TableStyle(style) => [
&style.data_row_style,
&style.header_row_style,
&style.title_row_style,
]
.into_iter()
.any(|row| row.text_style_name.eq_ignore_ascii_case(name)),
ObjectType::MultiLeaderStyle(style) => {
style.text_style_handle == style_handle
}
_ => false,
})
}
StyleKind::Dim => {
doc.header.current_dimstyle_name.eq_ignore_ascii_case(name)
|| doc.entities().any(|entity| match entity {
EntityType::Dimension(dimension) => {
dimension.base().style_name.eq_ignore_ascii_case(name)
}
EntityType::Leader(leader) => {
leader.dimension_style.eq_ignore_ascii_case(name)
}
EntityType::Tolerance(tolerance) => tolerance
.dimension_style_name
.eq_ignore_ascii_case(name),
_ => false,
})
}
StyleKind::Table | StyleKind::MLeader | StyleKind::MLine => {
let Some(handle) = object_handle(doc, name, kind) else {
return false;
};
let is_current = match kind {
StyleKind::Table => {
doc.header.current_table_style_name.eq_ignore_ascii_case(name)
}
StyleKind::MLeader => {
doc.header.current_mleader_style_name.eq_ignore_ascii_case(name)
|| self.tabs[i]
.active_mleader_style
.eq_ignore_ascii_case(name)
}
StyleKind::MLine => doc.header.multiline_style.eq_ignore_ascii_case(name),
StyleKind::Text | StyleKind::Dim => false,
};
is_current
|| doc.entities().any(|entity| match (kind, entity) {
(StyleKind::Table, EntityType::Table(table)) => {
table.table_style_handle == Some(handle)
}
(StyleKind::MLeader, EntityType::MultiLeader(leader)) => {
leader.style_handle == Some(handle)
}
(StyleKind::MLine, EntityType::MLine(line)) => {
line.style_handle == Some(handle)
|| line.style_name.eq_ignore_ascii_case(name)
}
_ => false,
})
}
}
}
/// Rename `old`→`new` in the backing store, re-keying table entries and
/// rewriting name-based references + current-style pointers.
fn rename_style_storage(&mut self, kind: StyleKind, old: &str, new: &str) {
@ -331,9 +449,40 @@ impl OpenCADStudio {
{
t.style = new.to_string();
}
acadrust::entities::EntityType::AttributeEntity(a)
if a.text_style.eq_ignore_ascii_case(old) =>
{
a.text_style = new.to_string();
}
acadrust::entities::EntityType::AttributeDefinition(a)
if a.text_style.eq_ignore_ascii_case(old) =>
{
a.text_style = new.to_string();
}
acadrust::entities::EntityType::Insert(insert) => {
for attribute in &mut insert.attributes {
if attribute.text_style.eq_ignore_ascii_case(old) {
attribute.text_style = new.to_string();
}
}
}
_ => {}
}
}
for object in doc.objects.values_mut() {
let ObjectType::TableStyle(style) = object else {
continue;
};
for row in [
&mut style.data_row_style,
&mut style.header_row_style,
&mut style.title_row_style,
] {
if row.text_style_name.eq_ignore_ascii_case(old) {
row.text_style_name = new.to_string();
}
}
}
}
StyleKind::Dim => {
let doc = &mut self.tabs[i].scene.document;
@ -349,10 +498,23 @@ impl OpenCADStudio {
doc.header.current_dimstyle_name = new.to_string();
}
for e in doc.entities_mut() {
if let acadrust::entities::EntityType::Dimension(d) = e {
if d.base().style_name.eq_ignore_ascii_case(old) {
match e {
acadrust::entities::EntityType::Dimension(d)
if d.base().style_name.eq_ignore_ascii_case(old) =>
{
d.base_mut().style_name = new.to_string();
}
acadrust::entities::EntityType::Leader(l)
if l.dimension_style.eq_ignore_ascii_case(old) =>
{
l.dimension_style = new.to_string();
}
acadrust::entities::EntityType::Tolerance(t)
if t.dimension_style_name.eq_ignore_ascii_case(old) =>
{
t.dimension_style_name = new.to_string();
}
_ => {}
}
}
}
@ -366,12 +528,24 @@ impl OpenCADStudio {
if self.ribbon.active_table_style.eq_ignore_ascii_case(old) {
self.ribbon.active_table_style = new.to_string();
}
if doc.header.current_table_style_name.eq_ignore_ascii_case(old) {
doc.header.current_table_style_name = new.to_string();
}
}
StyleKind::MLeader => {
let doc = &mut self.tabs[i].scene.document;
if let Some(h) = object_handle(doc, old, kind) {
if let Some(ObjectType::MultiLeaderStyle(s)) = doc.objects.get_mut(&h) {
s.name = new.to_string();
{
let doc = &mut self.tabs[i].scene.document;
if let Some(h) = object_handle(doc, old, kind) {
if let Some(ObjectType::MultiLeaderStyle(s)) = doc.objects.get_mut(&h) {
s.name = new.to_string();
}
}
if doc
.header
.current_mleader_style_name
.eq_ignore_ascii_case(old)
{
doc.header.current_mleader_style_name = new.to_string();
}
}
if self.tabs[i].active_mleader_style.eq_ignore_ascii_case(old) {
@ -391,6 +565,13 @@ impl OpenCADStudio {
if doc.header.multiline_style.eq_ignore_ascii_case(old) {
doc.header.multiline_style = new.to_string();
}
for entity in doc.entities_mut() {
if let acadrust::entities::EntityType::MLine(line) = entity {
if line.style_name.eq_ignore_ascii_case(old) {
line.style_name = new.to_string();
}
}
}
}
}
}
@ -437,6 +618,11 @@ impl OpenCADStudio {
.push_error("Cannot delete the Standard style.");
return;
}
if self.style_in_use(kind, &name) {
self.command_line
.push_error("Cannot delete a style that is current or in use.");
return;
}
if !self.remove_style_storage(kind, &name) {
return;
}

View file

@ -1491,15 +1491,19 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
// take the resolved value directly (None = the default
// "Closed filled" / "ByBlock" option).
let doc = &self.tabs[i].scene.document;
let resolved_mleader_style = (field == "mleader_style")
.then(|| {
doc.objects.values().find_map(|object| match object {
acadrust::objects::ObjectType::MultiLeaderStyle(style)
if style.name == value => Some(style.clone()),
_ => None,
})
})
.flatten();
let resolved: Option<acadrust::Handle> = match field {
"mleader_style" => doc.objects.iter().find_map(|(h, o)| match o {
acadrust::objects::ObjectType::MultiLeaderStyle(s)
if s.name == value =>
{
Some(*h)
}
_ => None,
}),
"mleader_style" => {
resolved_mleader_style.as_ref().map(|style| style.handle)
}
"text_style_handle" => doc
.text_styles
.iter()
@ -1531,15 +1535,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
if self.tabs[i].scene.is_layer_locked(handle) {
continue;
}
if let Some(acadrust::EntityType::MultiLeader(ml)) =
let mut style_annotation = None;
if field == "mleader_style" {
if let Some(style) = &resolved_mleader_style {
crate::scene::annotative::apply_mleader_style_to_object(
&mut self.tabs[i].scene.document,
handle,
style,
);
style_annotation = Some(style.is_annotative);
}
} else if let Some(acadrust::EntityType::MultiLeader(ml)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
match field {
"mleader_style" => {
if let Some(h) = resolved {
ml.style_handle = Some(h);
}
}
"text_style_handle" => {
if let Some(h) = resolved {
ml.text_style_handle = Some(h);
@ -1550,6 +1559,24 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
_ => {}
}
}
if let Some(annotative) = style_annotation {
if annotative {
if let Some(scale) =
self.tabs[i].scene.creation_annotation_scale_handle()
{
crate::scene::annotative::create_annotation_context(
&mut self.tabs[i].scene.document,
handle,
scale,
);
}
} else {
crate::scene::annotative::clear_annotation_context(
&mut self.tabs[i].scene.document,
handle,
);
}
}
}
} else if matches!(field, "arrow_block" | "dim_line_lw" | "text_pos_vert") {
// Leader dim-var overrides picked from a dropdown. The

View file

@ -516,6 +516,7 @@ impl OpenCADStudio {
section: self.start_section,
},
statusbar: self.statusbar_config.clone(),
annotation_auto_scale: self.annotation_auto_scale,
ribbon: crate::app::config::RibbonConfig {
collapse: self.ribbon.collapse_mode(),
},
@ -545,6 +546,7 @@ impl OpenCADStudio {
// (`refresh_recent_thumbs`) — never here on the boot path.
self.start_section = cfg.start.section;
self.statusbar_config = cfg.statusbar;
self.annotation_auto_scale = cfg.annotation_auto_scale.clamp(-4, 4);
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
self.plot_dialog = cfg.plot;
}

View file

@ -2589,17 +2589,60 @@ impl OpenCADStudio {
}
Message::SetAnnotationScale(scale) => {
self.scale_popup_open = false;
let auto_scale = self.annotation_auto_scale;
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
tab.scene.annotation_scale = scale;
util::sync_annotation_scale_header(&mut tab.scene);
tab.scene.invalidate_annotation_dependencies();
let previous = tab.scene.displayed_annotation_scale_handle();
if let Some(handle) = tab.scene.set_annotation_scale_named(&scale) {
if auto_scale > 0 {
tab.scene.add_annotation_scale_to_objects(
handle,
previous,
auto_scale as u8,
);
}
tab.dirty = true;
}
}
Task::none()
}
Message::SetViewportScale(scale) => {
self.scale_popup_open = false;
let auto_scale = self.annotation_auto_scale;
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
tab.scene.set_viewport_scale(scale);
let previous = tab.scene.displayed_annotation_scale_handle();
if let Some(handle) = tab.scene.set_viewport_scale_named(&scale) {
if auto_scale > 0 {
tab.scene.add_annotation_scale_to_objects(
handle,
previous,
auto_scale as u8,
);
}
tab.dirty = true;
}
}
Task::none()
}
Message::ToggleAnnotationVisibility => {
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
let value = !tab.scene.annotation_all_visible();
tab.scene.set_annotation_all_visible(value);
tab.dirty = true;
}
Task::none()
}
Message::ToggleAnnotationAutoAdd => {
self.annotation_auto_scale = match self.annotation_auto_scale {
0 => 4,
value => -value,
};
Task::none()
}
Message::SyncViewportAnnotationScale => {
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
if tab.scene.sync_viewport_annotation_scale() {
tab.dirty = true;
}
}
Task::none()
}
@ -2647,22 +2690,17 @@ impl OpenCADStudio {
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(_)
)
);
let ok = self.tabs[i]
.scene
.document
.get_entity(handles[0])
.is_some_and(crate::scene::annotative::supports_annotation_context);
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.",
);
self.command_line
.push_info("The selected object does not support annotation scales.");
}
} else {
self.command_line
@ -2816,21 +2854,16 @@ impl OpenCADStudio {
// never rolled back when the manager closes.
let i = self.active_tab;
let sel = self.scale_manager_selected.clone();
if let Some((_, anno, _)) = self
.tabs[i]
.scene
.scale_list()
.into_iter()
.find(|(n, _, _)| n.eq_ignore_ascii_case(&sel))
{
self.tabs[i].scene.annotation_scale = anno;
self.tabs[i].scene.document.header.current_annotation_scale = sel.clone();
if let Some((p, d)) = self.tabs[i].scene.scale_paper_drawing(&sel) {
if d != 0.0 {
self.tabs[i].scene.document.header.annotation_scale_value = p / d;
}
let previous = self.tabs[i].scene.displayed_annotation_scale_handle();
if let Some(scale) = self.tabs[i].scene.set_annotation_scale_named(&sel) {
if self.annotation_auto_scale > 0 {
self.tabs[i].scene.add_annotation_scale_to_objects(
scale,
previous,
self.annotation_auto_scale as u8,
);
}
self.tabs[i].scene.invalidate_annotation_dependencies();
self.tabs[i].dirty = true;
}
Task::none()
}
@ -3657,7 +3690,7 @@ impl OpenCADStudio {
// object. Off is handled inside set_entity_*.
if !cur {
if let Some(sh) =
self.tabs[i].scene.current_annotation_scale_handle()
self.tabs[i].scene.creation_annotation_scale_handle()
{
crate::scene::annotative::create_annotation_context(
&mut self.tabs[i].scene.document,

View file

@ -299,7 +299,12 @@ impl OpenCADStudio {
set_f64!(dimcen, self.ds_dimcen);
set_f64!(dimtsz, self.ds_dimtsz);
set_f64!(dimtxt, self.ds_dimtxt);
set_f64!(dimscale, self.ds_dimscale);
if self.ds_annotative {
ds.dimscale = 0.0;
self.ds_dimscale = "0".to_string();
} else {
set_f64!(dimscale, self.ds_dimscale);
}
set_f64!(dimlfac, self.ds_dimlfac);
set_f64!(dimtp, self.ds_dimtp);
set_f64!(dimtm, self.ds_dimtm);
@ -442,7 +447,12 @@ impl OpenCADStudio {
Dimtoh => self.ds_dimtoh = !self.ds_dimtoh,
Dimtol => self.ds_dimtol = !self.ds_dimtol,
Dimlim => self.ds_dimlim = !self.ds_dimlim,
Annotative => self.ds_annotative = !self.ds_annotative,
Annotative => {
self.ds_annotative = !self.ds_annotative;
if self.ds_annotative {
self.ds_dimscale = "0".to_string();
}
}
Dimfxlon => self.ds_dimfxlon = !self.ds_dimfxlon,
Dimsah => self.ds_dimsah = !self.ds_dimsah,
Dimtxtdirection => self.ds_dimtxtdirection = !self.ds_dimtxtdirection,

View file

@ -4,9 +4,8 @@ use crate::scene::Scene;
/// Parse a scale string like "1:50" or "2:1" into (numerator, denominator).
/// Returns (1.0, 1.0) for "Fit" or unknown formats.
/// Sync the model-space annotation scale into the standard CANNOSCALE /
/// CANNOSCALEVALUE header variables before a save, so the scale round-trips
/// through the file (and is read correctly by other CAD applications).
/// Sync the model-space annotation scale into its named drawing variable and
/// numeric header mirror before a save.
pub(super) fn sync_annotation_scale_header(scene: &mut Scene) {
let anno = scene.annotation_scale;
let value = if anno.abs() > 1e-9 {
@ -14,17 +13,25 @@ pub(super) fn sync_annotation_scale_header(scene: &mut Scene) {
} else {
1.0
};
// Prefer the name of a matching scale already in the drawing's list;
// fall back to a formatted ratio when none matches.
let name = scene
.scale_list()
.into_iter()
.find(|(_, a, _)| (a - anno).abs() < 0.001 * anno.max(0.001))
.map(|(n, _, _)| n)
.unwrap_or_else(|| format_annotation_scale_name(anno));
let current = scene.document.header.current_annotation_scale.clone();
let current_matches = scene.scale_list().into_iter().any(|(name, factor, _)| {
name.eq_ignore_ascii_case(&current)
&& (factor - anno).abs() < 0.001 * anno.max(0.001)
});
let name = if current_matches {
current
} else {
scene
.scale_list()
.into_iter()
.find(|(_, factor, _)| (factor - anno).abs() < 0.001 * anno.max(0.001))
.map(|(name, _, _)| name)
.unwrap_or_else(|| format_annotation_scale_name(anno))
};
let hdr = &mut scene.document.header;
hdr.current_annotation_scale = name;
hdr.current_annotation_scale = name.clone();
hdr.annotation_scale_value = value;
crate::io::set_drawing_variable(&mut scene.document, "CANNOSCALE", &name);
}
/// Format an annotation-scale multiplier as a ratio name: 50.0 -> "1:50",
@ -101,4 +108,3 @@ pub(super) fn f4_to_u3([r, g, b, _]: [f32; 4]) -> [u8; 3] {
pub(super) fn u3_to_f4([r, g, b]: [u8; 3]) -> [f32; 4] {
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
}

View file

@ -1470,6 +1470,7 @@ impl OpenCADStudio {
layout_names: layout_names.clone(),
polar_custom_input: &self.polar_custom_input,
scale_is_model: is_model,
current_scale_name: tab.scene.displayed_annotation_scale_name(),
scale_list: tab.scene.scale_picker_list(),
has_selection: !tab.scene.selected.is_empty(),
selection_types: tab
@ -1501,6 +1502,9 @@ impl OpenCADStudio {
self.show_layout_tabs,
tab.scene.annotation_scale,
scale_pill_enabled,
tab.scene.annotation_all_visible(),
self.annotation_auto_scale > 0,
tab.scene.viewport_annotation_scale_synced(),
tab.scene.document.header.lineweight_display,
cursor_coord,
coords_mode,

View file

@ -274,6 +274,7 @@ pub(crate) fn append_insert_attribute_wires(
// above, so the text path must not scale it a second time.
1.0,
None,
None,
bg_color,
false,
);

View file

@ -96,7 +96,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
let nan = [f64::NAN; 3];
let p3 = |v: &acadrust::types::Vector3| -> [f64; 3] { [v.x, v.y, v.z] };
let arrow_size = ml.arrowhead_size;
let arrow_size = ml.context.arrowhead_size;
let draw_arrow = arrow_size > 0.0;
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
@ -191,7 +191,11 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
None
};
let dogleg = if ml.enable_landing && ml.enable_dogleg {
ml.dogleg_length.max(0.0)
ml.context
.leader_roots
.first()
.map(|root| root.landing_distance.max(0.0))
.unwrap_or_else(|| ml.dogleg_length.max(0.0))
} else {
0.0
};
@ -1317,16 +1321,17 @@ impl MultiLeaderTess for MultiLeader {
};
// ── Scaling ──────────────────────────────────────────────────────────────
// ml.scale_factor is always applied; anno_scale is only applied when the
// multileader is marked annotative.
let effective_scale = (ml.scale_factor as f32)
// Used only when a context omits an already-resolved content size.
let fallback_content_scale = (ml.scale_factor as f32)
* if crate::scene::annotative::mleader_is_annotative(document, ml) {
anno_scale
} else {
1.0
};
let arrow_size = ml.arrowhead_size as f32 * effective_scale;
// The active context stores the resolved world-space arrow size.
// Reapplying the entity scale here makes context-sized arrows grow twice.
let arrow_size = ml.context.arrowhead_size as f32;
let draw_arrow = arrow_size > 0.0;
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
// arrowhead_handle resolves through the block records to a named arrow
@ -1452,7 +1457,10 @@ impl MultiLeaderTess for MultiLeader {
ml.text_attachment_direction,
acadrust::entities::multileader::TextAttachmentDirectionType::Vertical
);
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 && !vertical_attach
if ml.enable_landing
&& ml.enable_dogleg
&& root.landing_distance > 0.0
&& !vertical_attach
{
// Horizontal landing (dogleg) from the leader elbow (connection
// point) toward the text side. The stored geometry places the
@ -1460,7 +1468,9 @@ impl MultiLeaderTess for MultiLeader {
// dogleg end, so the dogleg stops here — drawing on to
// text_location (the block's top-left insertion) would streak a
// stray line up the side of the text.
let d = ml.dogleg_length * effective_scale as f64;
// Landing distance belongs to the selected leader-root context
// and is already resolved in world units.
let d = root.landing_distance;
// The dogleg runs along the leader root's stored direction —
// for a rotated leader that is the angled baseline, not world
// X. Roots without a usable direction keep the legacy
@ -1569,6 +1579,7 @@ impl MultiLeaderTess for MultiLeader {
leader_lw_px,
1.0,
None,
None,
bg_color,
false,
);
@ -1604,7 +1615,7 @@ impl MultiLeaderTess for MultiLeader {
let height = if ctx.text_height > 0.0 {
ctx.text_height as f32
} else {
ml.text_height as f32 * effective_scale
ml.text_height as f32 * fallback_content_scale
};
let ins = &ctx.text_location;

View file

@ -1095,14 +1095,44 @@ fn vardict_value(doc: &CadDocument, name: &str) -> Option<String> {
}
}
/// Write a value into an existing variable-dictionary entry. No-op when the
/// entry is absent (e.g. a brand-new document with no variable dictionary).
fn set_vardict_value(doc: &mut CadDocument, name: &str, value: &str) {
use acadrust::objects::ObjectType;
/// Write a drawing variable, creating the variable dictionary and record when
/// needed so new drawings preserve the value too.
pub(crate) fn set_drawing_variable(doc: &mut CadDocument, name: &str, value: &str) {
use acadrust::objects::{Dictionary, DictionaryVariable, ObjectType};
if let Some(h) = vardict_handle(doc, name) {
if let Some(ObjectType::DictionaryVariable(v)) = doc.objects.get_mut(&h) {
v.value = value.to_string();
}
return;
}
let root = crate::scene::annotative::root_named_dict_handle(doc);
let variable_dictionary = crate::scene::annotative::as_dict(doc, root)
.and_then(|dictionary| dictionary.get("AcDbVariableDictionary"))
.filter(|handle| {
matches!(doc.objects.get(handle), Some(ObjectType::Dictionary(_)))
})
.unwrap_or_else(|| {
let handle = doc.allocate_handle();
let mut dictionary = Dictionary::new();
dictionary.handle = handle;
dictionary.owner = root;
doc.objects
.insert(handle, ObjectType::Dictionary(dictionary));
if let Some(ObjectType::Dictionary(root_dictionary)) = doc.objects.get_mut(&root) {
root_dictionary.add_entry("AcDbVariableDictionary", handle);
}
handle
});
let handle = doc.allocate_handle();
let mut variable = DictionaryVariable::new(name, value);
variable.handle = handle;
variable.owner_handle = variable_dictionary;
doc.objects
.insert(handle, ObjectType::DictionaryVariable(variable));
if let Some(ObjectType::Dictionary(dictionary)) = doc.objects.get_mut(&variable_dictionary) {
dictionary.add_entry(name, handle);
}
}
@ -1119,26 +1149,7 @@ pub fn saved_active_layout(doc: &CadDocument) -> Option<String> {
/// carried it (e.g. a document authored here from scratch) — otherwise the exact
/// paper layout would be lost and reopening fell back to the first paper tab.
pub fn set_saved_active_layout(doc: &mut CadDocument, name: &str) {
use acadrust::objects::{DictionaryVariable, ObjectType};
if let Some(h) = vardict_handle(doc, "CTAB") {
if let Some(ObjectType::DictionaryVariable(v)) = doc.objects.get_mut(&h) {
v.value = name.to_string();
}
return;
}
// Attach a new CTAB entry to the root named-object dictionary. Resolve it
// robustly (or synthesise one) so the current-tab record persists even on a
// from-scratch document, or a foreign DWG whose header root pointer is
// unresolvable. See `annotative::root_named_dict_handle`.
let root = crate::scene::annotative::root_named_dict_handle(doc);
let handle = doc.allocate_handle();
let mut var = DictionaryVariable::new("CTAB", name);
var.handle = handle;
var.owner_handle = root;
doc.objects.insert(handle, ObjectType::DictionaryVariable(var));
if let Some(ObjectType::Dictionary(rd)) = doc.objects.get_mut(&root) {
rd.entries.push(("CTAB".to_string(), handle));
}
set_drawing_variable(doc, "CTAB", name);
}
/// Materialise the current-style choices into their format-specific storage
@ -1181,8 +1192,10 @@ fn sync_current_styles_on_save(doc: &mut CadDocument) {
let table = doc.header.current_table_style_name.clone();
let mleader = doc.header.current_mleader_style_name.clone();
set_vardict_value(doc, "CTABLESTYLE", &table);
set_vardict_value(doc, "CMLEADERSTYLE", &mleader);
set_drawing_variable(doc, "CTABLESTYLE", &table);
set_drawing_variable(doc, "CMLEADERSTYLE", &mleader);
let annotation = doc.header.current_annotation_scale.clone();
set_drawing_variable(doc, "CANNOSCALE", &annotation);
}
// ── Corrupt-entity guard ──────────────────────────────────────────────────

View file

@ -5,12 +5,14 @@
//! scale) must agree on *which* entities are annotative — so that logic lives
//! here, once. An entity is annotative if it carries a per-object annotation
//! context, legacy annotative XDATA, or an entity-level annotative flag. Text
//! style state is consulted only while creating a new text object: changing a
//! style later must not retroactively scale existing text.
//! style state is consulted while creating an object and when an explicit
//! annotation-style update is requested; changing a style alone does not
//! retroactively scale existing text.
use acadrust::entities::{EntityCommon, EntityType};
use acadrust::objects::{
Dictionary, HatchScaleContext, MTextContext, ObjectContextData, ObjectContextKind, ObjectType,
Dictionary, DimContext, DimSubtype, EmbeddedMTextContext, HatchScaleContext,
MTextAttributeContext, MTextContext, ObjectContextData, ObjectContextKind, ObjectType,
};
use acadrust::types::{Vector2, Vector3};
use acadrust::{CadDocument, Handle};
@ -72,7 +74,7 @@ pub fn root_named_dict_handle(doc: &mut CadDocument) -> Handle {
}
/// Set the per-object annotative flag on the entity types that carry one
/// (MTEXT, MULTILEADER). Turning it off also strips the per-object annotation
/// (MTEXT, MULTILEADER, ATTRIB and ATTDEF). 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. TEXT uses a context rather
@ -82,6 +84,8 @@ pub fn set_entity_annotative(doc: &mut CadDocument, handle: Handle, want: bool)
match e {
EntityType::MText(t) => t.is_annotative = want,
EntityType::MultiLeader(m) => m.enable_annotation_scale = want,
EntityType::AttributeEntity(attribute) => attribute.flags.annotative = want,
EntityType::AttributeDefinition(attribute) => attribute.flags.annotative = want,
_ => {}
}
}
@ -92,9 +96,121 @@ pub fn set_entity_annotative(doc: &mut CadDocument, handle: Handle, want: bool)
/// Derive the per-scale context payload for an entity from its current
/// placement. Returns the concrete class name and the context kind, or `None`
/// for entity types that do not carry a per-object annotation context (their
/// annotative state comes from a style, e.g. DIMENSION/TABLE).
fn context_kind_for(entity: &EntityType) -> Option<(&'static str, ObjectContextKind)> {
/// for entity types that do not carry a per-object annotation context.
fn dimension_context_for(doc: &CadDocument, dimension: &acadrust::entities::Dimension) -> Option<DimContext> {
use acadrust::entities::Dimension;
let subtype = match dimension {
Dimension::Aligned(dim) => DimSubtype::Aligned {
dimline_pt: dim.definition_point,
},
Dimension::Linear(dim) => DimSubtype::Aligned {
dimline_pt: dim.definition_point,
},
Dimension::Angular2Ln(dim) => DimSubtype::Angular {
arc_pt: dim.dimension_arc,
},
Dimension::Angular3Pt(dim) => DimSubtype::Angular {
arc_pt: dim.definition_point,
},
Dimension::Diameter(dim) => DimSubtype::Diametric {
first_arc_pt: dim.angle_vertex,
def_pt: dim.definition_point,
},
Dimension::Radius(dim) => DimSubtype::Radial {
first_arc_pt: dim.definition_point,
},
Dimension::LargeRadial(dim) => DimSubtype::RadialLarge {
ovr_center: dim.override_center,
jog_point: dim.jog_point,
},
Dimension::Ordinate(dim) => DimSubtype::Ordinate {
feature_location_pt: dim.feature_location,
leader_endpt: dim.leader_endpoint,
},
Dimension::Arc(_) => return None,
};
let base = dimension.base();
let block = doc
.block_records
.iter()
.find(|record| record.name.eq_ignore_ascii_case(&base.block_name))
.map(|record| record.handle)
.unwrap_or(Handle::NULL);
Some(DimContext {
def_pt: Vector2::new(base.text_middle_point.x, base.text_middle_point.y),
is_def_textloc: base.text_user_positioned,
text_rotation: base.text_rotation,
block,
b293: false,
dimtofl: false,
dimosxd: false,
dimatfit: false,
dimtix: false,
dimtmove: false,
override_code: 0,
has_arrow2: false,
flip_arrow2: base.flip_arrow2,
flip_arrow1: base.flip_arrow1,
subtype,
})
}
fn mtext_context_for(m: &acadrust::entities::MText) -> MTextContext {
MTextContext {
attachment: m.attachment_point as i32,
x_axis_dir: m
.dwg_x_direction
.unwrap_or_else(|| Vector3::new(m.rotation.cos(), m.rotation.sin(), 0.0)),
insertion: m.insertion_point,
rect_width: m.rectangle_width,
rect_height: m.rectangle_height.unwrap_or(0.0),
extents_width: m.extents_width,
extents_height: m.extents_height,
column_type: m.column_data.column_type as i32,
columns: (m.column_data.column_type != 0).then(|| acadrust::objects::MTextColumns {
num_heights: m.column_data.column_count,
width: m.column_data.width,
gutter: m.column_data.gutter,
auto_height: m.column_data.auto_height,
flow_reversed: m.column_data.flow_reversed,
heights: m.column_data.heights.clone(),
}),
}
}
fn attribute_context_for(
insertion: Vector3,
alignment: Vector3,
rotation: f64,
horizontal_mode: i16,
embedded: Option<&acadrust::entities::MText>,
scale: Handle,
) -> MTextAttributeContext {
MTextAttributeContext {
horizontal_mode,
rotation,
insertion: Vector2::new(insertion.x, insertion.y),
alignment: Vector2::new(alignment.x, alignment.y),
enable_context: embedded.is_some(),
context: embedded.map(|mtext| EmbeddedMTextContext {
owner_handle: Handle::NULL,
reactors: Vec::new(),
xdictionary_handle: None,
has_binary_data: false,
class_version: 3,
is_default: false,
scale,
mtext: mtext_context_for(mtext),
}),
}
}
fn context_kind_for(
doc: &CadDocument,
entity: &EntityType,
scale: Handle,
) -> Option<(&'static str, ObjectContextKind)> {
match entity {
EntityType::Insert(ins) => Some((
"ACDB_BLKREFOBJECTCONTEXTDATA_CLASS",
@ -118,23 +234,126 @@ fn context_kind_for(entity: &EntityType) -> Option<(&'static str, ObjectContextK
)),
EntityType::MText(m) => Some((
"ACDB_MTEXTOBJECTCONTEXTDATA_CLASS",
ObjectContextKind::MText(MTextContext {
attachment: m.attachment_point as i32,
// MTEXT stores a text X-axis direction; derive it from rotation.
x_axis_dir: Vector3::new(m.rotation.cos(), m.rotation.sin(), 0.0),
insertion: m.insertion_point,
rect_width: m.rectangle_width,
rect_height: 0.0,
extents_width: 0.0,
extents_height: 0.0,
column_type: 0,
columns: None,
ObjectContextKind::MText(mtext_context_for(m)),
)),
EntityType::Dimension(dimension) => {
let context = dimension_context_for(doc, dimension)?;
Some((context.subtype.class_name(), ObjectContextKind::Dim(context)))
}
EntityType::MultiLeader(mleader) => Some((
"ACDB_MLEADEROBJECTCONTEXTDATA_CLASS",
ObjectContextKind::MLeader(mleader.context.clone()),
)),
EntityType::AttributeEntity(attribute) => Some((
"ACDB_MTEXTATTRIBUTEOBJECTCONTEXTDATA_CLASS",
ObjectContextKind::MTextAttribute(attribute_context_for(
attribute.insertion_point,
attribute.alignment_point,
attribute.rotation,
attribute.horizontal_alignment.to_value(),
attribute.embedded_mtext.as_deref(),
scale,
)),
)),
EntityType::AttributeDefinition(attribute) => Some((
"ACDB_MTEXTATTRIBUTEOBJECTCONTEXTDATA_CLASS",
ObjectContextKind::MTextAttribute(attribute_context_for(
attribute.insertion_point,
attribute.alignment_point,
attribute.rotation,
attribute.horizontal_alignment.to_value(),
attribute.embedded_mtext.as_deref(),
scale,
)),
)),
EntityType::Leader(leader) => Some((
"ACDB_LEADEROBJECTCONTEXTDATA_CLASS",
ObjectContextKind::Leader(acadrust::objects::LeaderContext {
points: leader.vertices.clone(),
x_direction: leader.horizontal_direction,
annotation_enabled: !leader.annotation_handle.is_null(),
insertion_offset: Vector3::ZERO,
endpoint_projection: leader.annotation_offset,
}),
)),
EntityType::Tolerance(tolerance) => Some((
"ACDB_FCFOBJECTCONTEXTDATA_CLASS",
ObjectContextKind::Fcf {
location: tolerance.insertion_point,
horizontal_direction: tolerance.direction,
},
)),
EntityType::Hatch(hatch) => Some((
"ACDB_HATCHSCALECONTEXTDATA_CLASS",
ObjectContextKind::HatchScale(HatchScaleContext {
pattern_lines: hatch.pattern.lines.clone(),
pattern_scale: hatch.pattern_scale,
pattern_base: Vector3::ZERO,
loop_types: hatch
.paths
.iter()
.map(|path| path.flags.bits() as i32)
.collect(),
supports_context: true,
}),
)),
_ => None,
}
}
pub fn supports_annotation_context(entity: &EntityType) -> bool {
match entity {
EntityType::Insert(_)
| EntityType::Text(_)
| EntityType::MText(_)
| EntityType::MultiLeader(_)
| EntityType::AttributeEntity(_)
| EntityType::AttributeDefinition(_)
| EntityType::Leader(_)
| EntityType::Tolerance(_)
| EntityType::Hatch(_) => true,
EntityType::Dimension(dimension) => {
!matches!(dimension, acadrust::entities::Dimension::Arc(_))
}
_ => false,
}
}
fn register_context_class(doc: &mut CadDocument, dxf_name: &str) {
doc.register_object_context_class(dxf_name);
if doc.classes.get_by_name(dxf_name).is_some() {
return;
}
let cpp_name = match dxf_name {
"ACDB_MLEADEROBJECTCONTEXTDATA_CLASS" => "AcDbMLeaderObjectContextData",
"ACDB_MTEXTATTRIBUTEOBJECTCONTEXTDATA_CLASS" => "AcDbMTextAttributeObjectContextData",
"ACDB_LEADEROBJECTCONTEXTDATA_CLASS" => "AcDbLeaderObjectContextData",
"ACDB_FCFOBJECTCONTEXTDATA_CLASS" => "AcDbFcfObjectContextData",
_ => return,
};
use acadrust::classes::{DxfClass, ProxyFlags};
let proxy_flags = ProxyFlags(
ProxyFlags::ERASE_ALLOWED.0
| ProxyFlags::CLONING_ALLOWED.0
| ProxyFlags::DISABLES_PROXY_WARNING_DIALOG.0,
);
doc.classes.add_or_update(DxfClass {
dxf_name: dxf_name.to_string(),
cpp_class_name: cpp_name.to_string(),
application_name: "ObjectDBX Classes".to_string(),
proxy_flags,
instance_count: 0,
was_zombie: false,
is_an_entity: false,
class_number: 0,
item_class_id: 0x1F3,
dwg_version: 0,
maintenance_version: 0,
unknown1: 0,
unknown2: 0,
});
}
/// Give an entity a per-object annotation context for `scale_handle`,
/// synthesizing the extension-dictionary chain it hangs from when absent:
///
@ -153,11 +372,14 @@ pub fn create_annotation_context(
entity_handle: Handle,
scale_handle: Handle,
) -> bool {
let Some((class_name, kind)) = doc.get_entity(entity_handle).and_then(context_kind_for) else {
let Some((class_name, kind)) = doc
.get_entity(entity_handle)
.and_then(|entity| context_kind_for(doc, entity, scale_handle))
else {
return false;
};
// The writer emits a 500+ class number only for registered classes.
doc.register_object_context_class(class_name);
register_context_class(doc, class_name);
// Extension dictionary (hard-owns its entries; 280 = 1). Create it if the
// entity has none, and point the entity at it.
@ -231,7 +453,15 @@ pub fn create_annotation_context(
/// False for non-annotative objects (no per-object context — the vast
/// majority) and for objects whose contexts include the current scale. Gated
/// on an extension dictionary so non-annotative entities skip the lookup.
pub fn annotative_offscale(doc: &CadDocument, common: &EntityCommon) -> bool {
pub fn annotative_offscale_for(
doc: &CadDocument,
common: &EntityCommon,
scale_handle: Option<Handle>,
all_visible: bool,
) -> bool {
if all_visible {
return false;
}
if !common
.xdictionary_handle
.map(|h| !h.is_null())
@ -243,38 +473,60 @@ pub fn annotative_offscale(doc: &CadDocument, common: &EntityCommon) -> bool {
if scales.is_empty() {
return false;
}
let cur = &doc.header.current_annotation_scale;
if scales.iter().any(|(name, _)| name.eq_ignore_ascii_case(cur)) {
return false;
match scale_handle {
Some(handle) => !scales.iter().any(|(_, member)| *member == handle),
None => !scales.iter().any(|(name, _)| {
name.eq_ignore_ascii_case(&doc.header.current_annotation_scale)
}),
}
// Off-scale (no context for the current scale). If some representation in
// the drawing DOES provide the current scale, hide this one — the matching
// representation is the one to show.
if current_scale_provided(doc) {
return true;
}
// The current scale is unsupported by any representation. Fall back to the
// base "1:1" representation: keep it, hide the enlarged copies — otherwise
// every scale representation stacks (or, if all were hidden, the object
// vanishes). Without this, opening at e.g. CANNOSCALE 10:1 shows both a 1×
// and a 10× copy of the same block.
!scales.iter().any(|(name, _)| name.eq_ignore_ascii_case("1:1"))
}
/// Whether any annotative representation in the drawing targets the current
/// annotation scale.
fn current_scale_provided(doc: &CadDocument) -> bool {
let cur = &doc.header.current_annotation_scale;
doc.objects.values().any(|o| {
if let ObjectType::ObjectContextData(cd) = o {
if let Some(ObjectType::Scale(s)) = doc.objects.get(&cd.scale) {
return s.name.eq_ignore_ascii_case(cur);
}
pub fn scale_handle_by_name(doc: &CadDocument, name: &str) -> Option<Handle> {
doc.objects.iter().find_map(|(handle, object)| match object {
ObjectType::Scale(scale)
if !scale.is_temporary && scale.name.eq_ignore_ascii_case(name) =>
{
Some(*handle)
}
false
_ => None,
})
}
pub fn ensure_scale_object(
doc: &mut CadDocument,
source: &acadrust::objects::Scale,
) -> Handle {
if let Some(handle) = scale_handle_by_name(doc, &source.name) {
return handle;
}
let root = root_named_dict_handle(doc);
let scale_dictionary = as_dict(doc, root)
.and_then(|dictionary| dictionary.get("ACAD_SCALELIST"))
.filter(|handle| matches!(doc.objects.get(handle), Some(ObjectType::Dictionary(_))))
.unwrap_or_else(|| {
let handle = doc.allocate_handle();
let mut dictionary = Dictionary::new();
dictionary.handle = handle;
dictionary.owner = root;
doc.objects
.insert(handle, ObjectType::Dictionary(dictionary));
if let Some(ObjectType::Dictionary(root_dictionary)) = doc.objects.get_mut(&root) {
root_dictionary.add_entry("ACAD_SCALELIST", handle);
}
handle
});
let handle = doc.allocate_handle();
let mut scale = source.clone();
scale.handle = handle;
scale.owner_handle = scale_dictionary;
scale.is_temporary = false;
doc.objects.insert(handle, ObjectType::Scale(scale));
if let Some(ObjectType::Dictionary(dictionary)) = doc.objects.get_mut(&scale_dictionary) {
dictionary.add_entry(source.name.clone(), handle);
}
handle
}
/// The annotation scales an object currently carries a per-object context for,
/// as `(scale name, scale handle)` pairs (one per representation). Empty when
/// the object has no per-object context chain.
@ -348,9 +600,10 @@ fn annotation_scales_dict(doc: &CadDocument, entity: Handle) -> Option<Handle> {
/// scale. Broken scale handles are ignored. When the current named scale is
/// absent, the leaf explicitly marked as the native/default representation is
/// preferred, followed by the first valid leaf.
pub fn active_object_context(
pub fn active_object_context_for_scale(
doc: &CadDocument,
entity: Handle,
scale_handle: Option<Handle>,
) -> Option<&ObjectContextData> {
let coll_h = annotation_scales_dict(doc, entity)?;
let coll = as_dict(doc, coll_h)?;
@ -364,29 +617,27 @@ pub fn active_object_context(
if leaf.is_default {
default = Some(leaf);
}
let Some(ObjectType::Scale(scale)) = doc.objects.get(&leaf.scale) else {
continue;
};
if scale
.name
.eq_ignore_ascii_case(&doc.header.current_annotation_scale)
{
return Some(leaf);
if let Some(target) = scale_handle {
if leaf.scale == target {
return Some(leaf);
}
} else if let Some(ObjectType::Scale(scale)) = doc.objects.get(&leaf.scale) {
if scale
.name
.eq_ignore_ascii_case(&doc.header.current_annotation_scale)
{
return Some(leaf);
}
}
}
default.or(first)
}
/// Resolve the display multiplier for an entity at the current annotation
/// scale. A per-object context stores geometry relative to its native/default
/// representation, so its current multiplier is the active scale's drawing
/// factor divided by the default scale's drawing factor. Falling back preserves
/// the legacy whole-drawing multiplier for annotative objects without a usable
/// default context (including style-only DIMENSION and MULTILEADER entities).
pub fn effective_annotation_scale(
pub fn effective_annotation_scale_for(
doc: &CadDocument,
entity: &EntityType,
fallback: f32,
scale_handle: Option<Handle>,
) -> f32 {
if !is_annotative(doc, entity) {
return 1.0;
@ -397,7 +648,9 @@ pub fn effective_annotation_scale(
// height as stored; make `ml.scale_factor * anno_scale` resolve to the
// active context's scale factor for arrows, doglegs, and fallback text.
if let EntityType::MultiLeader(mleader) = entity {
let Some(active) = active_object_context(doc, entity.common().handle) else {
let Some(active) =
active_object_context_for_scale(doc, entity.common().handle, scale_handle)
else {
return fallback;
};
let ObjectContextKind::MLeader(context) = &active.kind else {
@ -422,7 +675,7 @@ pub fn effective_annotation_scale(
return fallback;
};
let active = active_object_context(doc, entity.common().handle);
let active = active_object_context_for_scale(doc, entity.common().handle, scale_handle);
let native = coll.entries.iter().find_map(|(_, leaf_h)| {
match doc.objects.get(leaf_h) {
Some(ObjectType::ObjectContextData(leaf)) if leaf.is_default => Some(leaf),
@ -613,15 +866,14 @@ fn apply_hatch_context(hatch: &mut acadrust::entities::Hatch, context: &HatchSca
}
}
/// Return an ephemeral entity representation with the active scale leaf
/// overlaid on its base geometry. The source document remains unchanged, which
/// keeps save/round-trip data intact while render, picking and block expansion
/// all see the scale-specific placement.
pub fn entity_for_active_context<'a>(
pub fn entity_for_annotation_context<'a>(
doc: &'a CadDocument,
entity: &'a EntityType,
scale_handle: Option<Handle>,
) -> Cow<'a, EntityType> {
let Some(context) = active_object_context(doc, entity.common().handle) else {
let Some(context) =
active_object_context_for_scale(doc, entity.common().handle, scale_handle)
else {
return Cow::Borrowed(entity);
};
let mut placed = entity.clone();
@ -824,15 +1076,15 @@ fn sync_dimension_context(
}
}
/// Copy an edited entity's placement back into its active per-scale leaf.
/// Geometry edits therefore remain visible at the current annotation scale and
/// round-trip as genuine `AcDb*ObjectContextData`, while the base entity stays
/// usable as the default representation.
pub fn sync_active_context_from_entity(
/// Copy an edited entity's placement back into one per-scale leaf so geometry
/// edits remain attached to the representation displayed by the caller.
pub fn sync_annotation_context_from_entity(
doc: &mut CadDocument,
entity_handle: Handle,
scale_handle: Option<Handle>,
) -> bool {
let Some(leaf_handle) = active_object_context(doc, entity_handle).map(|leaf| leaf.handle)
let Some(leaf_handle) =
active_object_context_for_scale(doc, entity_handle, scale_handle).map(|leaf| leaf.handle)
else {
return false;
};
@ -978,6 +1230,81 @@ pub fn sync_active_context_from_entity(
true
}
/// Move every stored scale representation with a pasted entity. The base
/// entity has already moved when this runs; each context leaf still contains
/// its source placement, so it is materialized, translated, and written back
/// without disturbing the transformed base representation.
pub fn translate_annotation_contexts(
doc: &mut CadDocument,
entity_handle: Handle,
delta: glam::DVec3,
) -> bool {
let Some(base_entity) = doc.get_entity(entity_handle).cloned() else {
return false;
};
let leaves: Vec<_> = annotation_scales_dict(doc, entity_handle)
.and_then(|collection| as_dict(doc, collection))
.map(|collection| {
collection
.entries
.iter()
.filter_map(|(_, leaf_handle)| match doc.objects.get(leaf_handle) {
Some(ObjectType::ObjectContextData(leaf)) => {
Some((leaf.handle, leaf.scale))
}
_ => None,
})
.collect()
})
.unwrap_or_default();
if leaves.is_empty() {
return false;
}
let mut changed = false;
for (_, scale) in leaves {
let mut placed = entity_for_annotation_context(doc, &base_entity, Some(scale)).into_owned();
crate::scene::view::dispatch::apply_transform(
&mut placed,
&crate::command::EntityTransform::Translate(delta),
);
// The entity translator keeps the compatibility break list in sync,
// while the complete per-segment list is a separate persisted field.
if let EntityType::MultiLeader(mleader) = &mut placed {
let offset = Vector3::new(delta.x, delta.y, delta.z);
for root in &mut mleader.context.leader_roots {
for line in &mut root.lines {
for info in &mut line.break_infos {
for pair in &mut info.break_points {
pair.start_point = pair.start_point + offset;
pair.end_point = pair.end_point + offset;
}
}
}
}
}
// A pasted dimension owns a newly generated graphics block. A source
// context can still carry the old block handle, so retain the block
// selected for the transformed base entity before synchronizing it.
if let (EntityType::Dimension(placed), EntityType::Dimension(base)) =
(&mut placed, &base_entity)
{
placed.base_mut().block_name.clone_from(&base.base().block_name);
}
if let Some(entity) = doc.get_entity_mut(entity_handle) {
*entity = placed;
}
changed |= sync_annotation_context_from_entity(doc, entity_handle, Some(scale));
if let Some(entity) = doc.get_entity_mut(entity_handle) {
*entity = base_entity.clone();
}
}
changed
}
/// Get the child dictionary stored under `key` in `parent_h`, creating an empty
/// one (owned by `parent_h`) and registering the entry when absent.
fn get_or_create_child_dict(doc: &mut CadDocument, parent_h: Handle, key: &str) -> Handle {
@ -1026,6 +1353,7 @@ pub fn clear_annotation_context(doc: &mut CadDocument, handle: Handle) {
}
}
// Strip the legacy annotative XDATA markers the detection also honours.
crate::scene::view::dispatch::set_entity_xdata(doc, handle, "AcadAnnotative", None);
crate::scene::view::dispatch::set_entity_xdata(doc, handle, "AcAnnoPO", None);
crate::scene::view::dispatch::set_entity_xdata(doc, handle, "AcAnnotativeData", None);
}
@ -1048,7 +1376,7 @@ pub fn text_style_is_annotative(doc: &CadDocument, name: &str) -> bool {
.is_some_and(|s| s.annotative)
}
fn dim_style_annotative(doc: &CadDocument, name: &str) -> bool {
pub fn dim_style_is_annotative(doc: &CadDocument, name: &str) -> bool {
doc.dim_styles
.iter()
.find(|s| name_matches(&s.name, name))
@ -1064,15 +1392,6 @@ fn mleader_style_annotative(doc: &CadDocument, handle: Option<Handle>) -> bool {
})
}
fn table_style_annotative(doc: &CadDocument, handle: Option<Handle>) -> bool {
let Some(h) = handle else {
return false;
};
doc.objects
.iter()
.any(|(oh, o)| matches!(o, ObjectType::TableStyle(s) if *oh == h && s.annotative))
}
/// Whether an object carries a per-object annotation context with at least one
/// per-scale representation — its extension dictionary holds an
/// `AcDbContextDataManager` whose `ACDB_ANNOTATIONSCALES` collection is
@ -1105,14 +1424,241 @@ fn has_context_manager(doc: &CadDocument, common: &EntityCommon) -> bool {
}
/// Whether a MULTILEADER participates in annotation scaling through its
/// per-object context, entity flag, or assigned annotative style.
/// per-object context or entity flag. A later style edit is applied only by an
/// explicit style update, so it cannot retroactively change existing objects.
pub fn mleader_is_annotative(
doc: &CadDocument,
mleader: &acadrust::entities::MultiLeader,
) -> bool {
has_context_manager(doc, &mleader.common)
|| mleader.enable_annotation_scale
|| mleader_style_annotative(doc, mleader.style_handle)
}
pub fn annotation_style_is_annotative(doc: &CadDocument, entity: &EntityType) -> bool {
match entity {
EntityType::Text(text) => text_style_is_annotative(doc, &text.style),
EntityType::MText(text) => text_style_is_annotative(doc, &text.style),
EntityType::AttributeEntity(attribute) => {
text_style_is_annotative(doc, &attribute.text_style)
}
EntityType::AttributeDefinition(attribute) => {
text_style_is_annotative(doc, &attribute.text_style)
}
EntityType::Dimension(dimension) => {
dim_style_is_annotative(doc, &dimension.base().style_name)
}
EntityType::Leader(leader) => dim_style_is_annotative(doc, &leader.dimension_style),
EntityType::Tolerance(tolerance) => {
dim_style_is_annotative(doc, &tolerance.dimension_style_name)
}
EntityType::MultiLeader(leader) => {
mleader_style_annotative(doc, leader.style_handle)
}
_ => false,
}
}
pub fn apply_mleader_style(
entity: &mut acadrust::entities::MultiLeader,
style: &acadrust::objects::MultiLeaderStyle,
) {
entity.style_handle = Some(style.handle);
entity.content_type = (style.content_type as i16).into();
entity.path_type = (style.path_type as i16).into();
entity.line_color = style.line_color;
entity.line_type_handle = style.line_type_handle;
entity.line_weight = style.line_weight;
entity.enable_landing = style.enable_landing;
entity.enable_dogleg = style.enable_dogleg;
entity.dogleg_length = style.landing_distance;
entity.arrowhead_handle = style.arrowhead_handle;
entity.arrowhead_size = style.arrowhead_size;
entity.text_style_handle = style.text_style_handle;
entity.text_color = style.text_color;
entity.text_frame = style.text_frame;
entity.text_height = style.text_height;
entity.context.text_height = style.text_height;
entity.context.text_style_handle = style.text_style_handle;
entity.context.text_color = style.text_color;
entity.text_left_attachment = (style.text_left_attachment as i16).into();
entity.text_right_attachment = (style.text_right_attachment as i16).into();
entity.text_top_attachment = (style.text_top_attachment as i16).into();
entity.text_bottom_attachment = (style.text_bottom_attachment as i16).into();
entity.text_attachment_direction = (style.text_attachment_direction as i16).into();
entity.text_alignment = (style.text_alignment as i16).into();
entity.text_angle_type = (style.text_angle_type as i16).into();
entity.context.text_left_attachment = entity.text_left_attachment;
entity.context.text_right_attachment = entity.text_right_attachment;
entity.context.text_top_attachment = entity.text_top_attachment;
entity.context.text_bottom_attachment = entity.text_bottom_attachment;
entity.context.text_alignment = entity.text_alignment;
entity.block_content_handle = style.block_content_handle;
entity.block_content_color = style.block_content_color;
entity.block_connection_type = (style.block_content_connection as i16).into();
entity.block_rotation = style.block_content_rotation;
entity.block_scale = Vector3::new(
style.block_content_scale_x,
style.block_content_scale_y,
style.block_content_scale_z,
);
entity.scale_factor = style.scale_factor;
entity.context.block_content_handle = style.block_content_handle;
entity.context.block_content_color = style.block_content_color;
entity.context.block_connection_type = entity.block_connection_type;
entity.context.block_rotation = style.block_content_rotation;
entity.context.block_content_scale = entity.block_scale;
entity.context.scale_factor = style.scale_factor;
entity.enable_annotation_scale = style.is_annotative;
}
pub fn apply_mleader_style_to_object(
doc: &mut CadDocument,
handle: Handle,
style: &acadrust::objects::MultiLeaderStyle,
) -> bool {
let Some(EntityType::MultiLeader(original)) = doc.get_entity(handle).cloned() else {
return false;
};
let mut styled = original.clone();
apply_mleader_style(&mut styled, style);
if let Some(EntityType::MultiLeader(entity)) = doc.get_entity_mut(handle) {
*entity = styled;
}
let leaf_handles: Vec<_> = annotation_scales_dict(doc, handle)
.and_then(|collection| as_dict(doc, collection))
.map(|collection| collection.entries.iter().map(|(_, leaf)| *leaf).collect())
.unwrap_or_default();
for leaf_handle in leaf_handles {
let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get_mut(&leaf_handle) else {
continue;
};
let ObjectContextKind::MLeader(context) = &mut leaf.kind else {
continue;
};
let context_scale = context.scale_factor;
let text_height_ratio = if original.text_height.abs() > 1.0e-12 {
context.text_height / original.text_height
} else {
1.0
};
let mut per_scale = original.clone();
per_scale.context.clone_from(context);
apply_mleader_style(&mut per_scale, style);
per_scale.context.scale_factor = context_scale;
if style.text_height > 0.0 && text_height_ratio.is_finite() {
per_scale.context.text_height = style.text_height * text_height_ratio;
}
context.clone_from(&per_scale.context);
}
true
}
pub fn update_entity_from_annotation_style(
doc: &mut CadDocument,
handle: Handle,
current_scale: Option<Handle>,
) -> bool {
enum StyleUpdate {
Text { annotative: bool, height: f64 },
Dimension { annotative: bool },
MultiLeader(acadrust::objects::MultiLeaderStyle),
ContextOnly,
}
let Some(entity) = doc.get_entity(handle) else {
return false;
};
let update = match entity {
EntityType::Text(text) => doc.text_styles.get(&text.style).map(|style| {
StyleUpdate::Text {
annotative: style.annotative,
height: style.height,
}
}),
EntityType::MText(text) => doc.text_styles.get(&text.style).map(|style| {
StyleUpdate::Text {
annotative: style.annotative,
height: style.height,
}
}),
EntityType::AttributeEntity(attribute) => doc
.text_styles
.get(&attribute.text_style)
.map(|style| StyleUpdate::Text {
annotative: style.annotative,
height: style.height,
}),
EntityType::AttributeDefinition(attribute) => doc
.text_styles
.get(&attribute.text_style)
.map(|style| StyleUpdate::Text {
annotative: style.annotative,
height: style.height,
}),
EntityType::Dimension(dimension) => doc
.dim_styles
.get(&dimension.base().style_name)
.map(|style| StyleUpdate::Dimension {
annotative: style.annotative,
}),
EntityType::Leader(leader) => doc
.dim_styles
.get(&leader.dimension_style)
.map(|style| StyleUpdate::Dimension {
annotative: style.annotative,
}),
EntityType::Tolerance(tolerance) => doc
.dim_styles
.get(&tolerance.dimension_style_name)
.map(|style| StyleUpdate::Dimension {
annotative: style.annotative,
}),
EntityType::MultiLeader(leader) => leader.style_handle.and_then(|style_handle| {
match doc.objects.get(&style_handle) {
Some(ObjectType::MultiLeaderStyle(style)) => {
Some(StyleUpdate::MultiLeader(style.clone()))
}
_ => None,
}
}),
_ if is_annotative(doc, entity) => Some(StyleUpdate::ContextOnly),
_ => None,
};
let Some(update) = update else {
return false;
};
let annotative = match update {
StyleUpdate::Text { annotative, height } => {
if height > 0.0 {
if let Some(entity) = doc.get_entity_mut(handle) {
match entity {
EntityType::Text(text) => text.height = height,
EntityType::MText(text) => text.height = height,
EntityType::AttributeEntity(attribute) => attribute.height = height,
EntityType::AttributeDefinition(attribute) => attribute.height = height,
_ => {}
}
}
}
annotative
}
StyleUpdate::Dimension { annotative } => annotative,
StyleUpdate::MultiLeader(style) => {
apply_mleader_style_to_object(doc, handle, &style);
style.is_annotative
}
StyleUpdate::ContextOnly => return true,
};
set_entity_annotative(doc, handle, annotative);
if annotative {
if let Some(scale) = current_scale {
create_annotation_context(doc, handle, scale);
}
}
true
}
/// Whether an entity participates in annotation scaling.
@ -1123,19 +1669,30 @@ pub fn is_annotative(doc: &CadDocument, entity: &EntityType) -> bool {
}
// Legacy annotative XDATA markers.
let xd = &entity.common().extended_data;
if xd.get_record("AcAnnoPO").is_some() || xd.get_record("AcAnnotativeData").is_some() {
let standard_marker = xd
.get_record("AcadAnnotative")
.and_then(|record| {
record.values.iter().filter_map(|value| match value {
acadrust::xdata::XDataValue::Integer16(value) => Some(*value),
_ => None,
}).last()
})
.is_some_and(|value| value != 0);
if standard_marker
|| xd.get_record("AcAnnoPO").is_some()
|| xd.get_record("AcAnnotativeData").is_some()
{
return true;
}
// Annotative via the entity's own flag or assigned non-text style.
// Annotative via the entity's own flag.
// Text styles can be made annotative without converting existing text;
// those objects must keep their stored height until explicitly updated.
match entity {
EntityType::Text(_) => false,
EntityType::MText(t) => t.is_annotative,
EntityType::Dimension(d) => dim_style_annotative(doc, &d.base().style_name),
EntityType::Leader(l) => dim_style_annotative(doc, &l.dimension_style),
EntityType::AttributeEntity(attribute) => attribute.flags.annotative,
EntityType::AttributeDefinition(attribute) => attribute.flags.annotative,
EntityType::MultiLeader(ml) => mleader_is_annotative(doc, ml),
EntityType::Table(t) => table_style_annotative(doc, t.table_style_handle),
_ => false,
}
}

View file

@ -196,6 +196,8 @@ impl BlockCache {
pub fn build(
doc: &CadDocument,
anno_scale: f32,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
bg_color: [f32; 4],
// Scene draw-depth map ([depth, half] per handle) — source of each
// block child's in-block rank, so band depth composition agrees with
@ -228,7 +230,15 @@ impl BlockCache {
.map(|name| {
(
name.clone(),
Arc::new(build_defn(doc, name, anno_scale, bg_color, depth_map)),
Arc::new(build_defn(
doc,
name,
anno_scale,
annotation_scale_handle,
all_visible,
bg_color,
depth_map,
)),
)
})
.collect();
@ -345,6 +355,8 @@ fn build_defn(
doc: &CadDocument,
block_name: &str,
anno_scale: f32,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
bg_color: [f32; 4],
depth_map: &HashMap<u64, [f32; 2]>,
) -> BlockDefn {
@ -362,8 +374,11 @@ fn build_defn(
let Some(source_entity) = doc.get_entity(eh) else {
continue;
};
let contextual =
crate::scene::annotative::entity_for_active_context(doc, source_entity);
let contextual = crate::scene::annotative::entity_for_annotation_context(
doc,
source_entity,
annotation_scale_handle,
);
let entity = contextual.as_ref();
// Skip entities flagged invisible. Dynamic blocks (e.g. a visibility-
// state parametric block) keep the geometry for every state in one
@ -383,7 +398,12 @@ fn build_defn(
// Annotative scale representation: bake only the current scale's copy
// into the defn so off-scale representations don't stack (e.g. a 1×
// copy under a 10×). See `annotative::annotative_offscale`.
if crate::scene::annotative::annotative_offscale(doc, entity.common()) {
if crate::scene::annotative::annotative_offscale_for(
doc,
entity.common(),
annotation_scale_handle,
all_visible,
) {
continue;
}
match entity {
@ -452,7 +472,12 @@ fn build_defn(
)));
} else {
for lw in tessellate_sub_local(
doc, &placed, anno_scale, bg_color, depth_map,
doc,
&placed,
anno_scale,
annotation_scale_handle,
bg_color,
depth_map,
) {
subs.push(LocalSub::Wire(lw));
}
@ -504,7 +529,14 @@ fn build_defn(
)));
} else {
for lw in
tessellate_sub_local(doc, &placed, anno_scale, bg_color, depth_map)
tessellate_sub_local(
doc,
&placed,
anno_scale,
annotation_scale_handle,
bg_color,
depth_map,
)
{
subs.push(LocalSub::Wire(lw));
}
@ -513,7 +545,14 @@ fn build_defn(
}
if !used_baked {
for lw in
tessellate_sub_local(doc, entity, anno_scale, bg_color, depth_map)
tessellate_sub_local(
doc,
entity,
anno_scale,
annotation_scale_handle,
bg_color,
depth_map,
)
{
subs.push(LocalSub::Wire(lw));
}
@ -536,7 +575,14 @@ fn build_defn(
// the LocalWire; `emit_wire` scales it by the insert transform
// so the shader band matches the scaled geometry (same band the
// top-level path draws — depth-tested + linetype-dashed).
for lw in tessellate_sub_local(doc, entity, anno_scale, bg_color, depth_map) {
for lw in tessellate_sub_local(
doc,
entity,
anno_scale,
annotation_scale_handle,
bg_color,
depth_map,
) {
subs.push(LocalSub::Wire(lw));
}
}
@ -609,6 +655,7 @@ fn tessellate_sub_local(
doc: &CadDocument,
sub: &EntityType,
anno_scale: f32,
annotation_scale_handle: Option<Handle>,
bg_color: [f32; 4],
depth_map: &HashMap<u64, [f32; 2]>,
) -> Vec<LocalWire> {
@ -657,7 +704,19 @@ fn tessellate_sub_local(
// before casting to f32 — same precision-preservation trick used for
// top-level entities, applied per-defn.
let wires_out = tessellate::tessellate(
doc, h, sub, false, sub_color, pat_len, pat, lw_px, anno_scale, None, bg_color, false,
doc,
h,
sub,
false,
sub_color,
pat_len,
pat,
lw_px,
anno_scale,
annotation_scale_handle,
None,
bg_color,
false,
);
if wires_out.is_empty() {
return vec![];

View file

@ -920,7 +920,23 @@ impl Scene {
// Paper entities and viewport borders belong to the sheet. Model
// content projected through those viewports deliberately does not.
for wire in self.wires_for_block_culled(layout_block, None, None, None, None) {
let scale = if self.current_layout == "Model" {
crate::scene::annotative::scale_handle_by_name(
&self.document,
&self.document.header.current_annotation_scale,
)
} else {
self.paper_annotation_scale_handle()
};
for wire in self.wires_for_block_culled(
layout_block,
None,
None,
None,
None,
scale,
self.annotation_all_visible(),
) {
let is_infinite = Self::handle_from_wire_name(&wire.name)
.and_then(|handle| self.document.get_entity(handle))
.is_some_and(|entity| {
@ -1000,7 +1016,23 @@ impl Scene {
// (issue #51). `wpp = None` also tessellates at a fixed tolerance so
// the bounds don't drift with zoom-adaptive curve sampling.
let layout_block = self.current_layout_block_handle();
let mut wires = self.wires_for_block_culled(layout_block, None, None, None, None);
let scale = if self.current_layout == "Model" {
crate::scene::annotative::scale_handle_by_name(
&self.document,
&self.document.header.current_annotation_scale,
)
} else {
self.paper_annotation_scale_handle()
};
let mut wires = self.wires_for_block_culled(
layout_block,
None,
None,
None,
None,
scale,
self.annotation_all_visible(),
);
// Ray / XLine tessellate as ±DISPLAY_EXTENT display segments
// (entities/ray.rs) — their endpoints are rendering artifacts, not
// drawing extent. A construction line through the drawing defeats

View file

@ -321,6 +321,7 @@ pub fn place_block_wires(
[0.0; 8],
line_weight_px,
anno_scale,
None,
world_per_pixel,
bg_color,
false,

View file

@ -321,6 +321,7 @@ pub(crate) fn tessellate_entity_dim_text(
active_viewport,
bg_color,
anno_scale,
None,
e,
None,
view_aabb,
@ -345,6 +346,7 @@ pub(crate) fn tessellate_entity(
active_viewport: Option<Handle>,
bg_color: [f32; 4],
anno_scale: f32,
annotation_scale_handle: Option<Handle>,
e: &EntityType,
block_cache: Option<&cache::block_cache::BlockCache>,
// World-space XY view AABB (post `world_offset` subtraction). When
@ -357,7 +359,11 @@ pub(crate) fn tessellate_entity(
// by the viewport's GPU uniform so it never changes resident wire content.
paper_space: bool,
) -> Vec<WireModel> {
let contextual = crate::scene::annotative::entity_for_active_context(document, e);
let contextual = crate::scene::annotative::entity_for_annotation_context(
document,
e,
annotation_scale_handle,
);
let e = contextual.as_ref();
let h = e.common().handle;
let sel = selected.contains(&h);
@ -371,7 +377,12 @@ pub(crate) fn tessellate_entity(
| EntityType::Dimension(_)
| EntityType::MultiLeader(_)
) {
crate::scene::annotative::effective_annotation_scale(document, e, anno_scale)
crate::scene::annotative::effective_annotation_scale_for(
document,
e,
anno_scale,
annotation_scale_handle,
)
} else {
anno_scale
};
@ -505,6 +516,7 @@ pub(crate) fn tessellate_entity(
pattern,
1.5,
1.0,
annotation_scale_handle,
world_per_pixel,
bg_color,
false,
@ -788,6 +800,7 @@ pub(crate) fn tessellate_entity(
// Block contents are baked at the final WCS size —
// don't let downstream paths re-apply anno_scale.
1.0,
None,
sub,
block_cache,
view_aabb,
@ -954,6 +967,7 @@ pub(crate) fn tessellate_entity(
active_viewport,
bg_color,
anno_scale,
annotation_scale_handle,
&placed,
block_cache,
view_aabb,
@ -994,14 +1008,7 @@ pub(crate) fn tessellate_entity(
// No baked block (e.g. a table created in-app) — synthesise coloured
// geometry from the rows + TableStyle so fills/colours/borders/margins
// are honoured instead of the monochrome fallback.
// Annotative tables scale with the current annotation scale (their
// stored geometry is at paper size); non-annotative tables are already
// model-size, so pass 1.0.
let table_anno = if crate::scene::annotative::is_annotative(document, e) {
anno_scale
} else {
1.0
};
let table_anno = 1.0;
let mut wires = crate::entities::table::tessellate_table(
tab,
document,
@ -1019,6 +1026,7 @@ pub(crate) fn tessellate_entity(
active_viewport,
bg_color,
1.0,
None,
&EntityType::Insert(insert),
block_cache,
view_aabb,
@ -1198,6 +1206,7 @@ pub(crate) fn tessellate_entity(
sub_pattern,
sub_line_weight_px,
anno_scale,
annotation_scale_handle,
world_per_pixel,
bg_color,
false,
@ -1260,6 +1269,7 @@ pub(crate) fn tessellate_entity(
pattern,
line_weight_px,
anno_scale,
annotation_scale_handle,
world_per_pixel,
bg_color,
false,
@ -1398,6 +1408,7 @@ pub(crate) fn tessellate_entity(
pattern,
line_weight_px,
anno_scale,
annotation_scale_handle,
world_per_pixel,
bg_color,
false,

View file

@ -131,6 +131,7 @@ pub fn tessellate(
pattern: [f32; 8],
line_weight_px: f32,
anno_scale: f32,
annotation_scale_handle: Option<Handle>,
world_per_pixel: Option<f32>,
// Canvas background colour — used for the MTEXT background *mask* fill
// (flag 0x02, "use drawing window colour") so the mask erases geometry
@ -156,8 +157,12 @@ pub fn tessellate(
// oversized text). Annotative-ness is resolved centrally from the entity's
// per-object context, legacy XDATA, or annotative style (see
// `scene::annotative::is_annotative`) so the bake and the panel agree.
let anno_scale =
crate::scene::annotative::effective_annotation_scale(document, entity, anno_scale);
let anno_scale = crate::scene::annotative::effective_annotation_scale_for(
document,
entity,
anno_scale,
annotation_scale_handle,
);
// A HATCH is drawn as a fill by the hatch pipeline and highlighted via a
// fill tint when selected (issue #71), so it carries no boundary outline in

View file

@ -685,7 +685,7 @@ impl Scene {
entities: Vec<EntityType>,
name: &str,
base: glam::DVec3,
) -> Result<(), String> {
) -> Result<Vec<Handle>, String> {
let name = name.trim();
if name.is_empty() {
return Err("Block name cannot be empty.".into());
@ -729,20 +729,23 @@ impl Scene {
.map_err(|e| e.to_string())?;
let local = EntityTransform::Translate(-base);
let mut entity_handles = Vec::with_capacity(entities.len());
for mut entity in entities {
view::dispatch::apply_transform(&mut entity, &local);
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);
Self::reset_clone_subhandles(&mut self.document, &mut entity);
entity.common_mut().handle = Handle::NULL;
entity.common_mut().owner_handle = br_handle;
self.document
let handle = self
.document
.add_entity(entity)
.map_err(|e| e.to_string())?;
entity_handles.push(handle);
}
// Block defns don't render on their own, but the geometry cache must
// pick up the new definition so the interactive insert can preview it.
self.bump_geometry();
Ok(())
Ok(entity_handles)
}
/// Recreate a block definition verbatim — the entities are already in
@ -794,6 +797,8 @@ impl Scene {
&self,
target_block: Handle,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
) -> Vec<HatchModel> {
let layer_hidden = |layer: &str| {
self.document
@ -832,6 +837,12 @@ impl Scene {
if c.invisible
|| self.entity_temporarily_hidden(handle)
|| layer_hidden(&c.layer)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
c,
annotation_scale_handle,
all_visible,
)
{
return false;
}
@ -851,17 +862,19 @@ impl Scene {
.document
.get_entity(handle)
.map(|entity| {
crate::scene::annotative::entity_for_active_context(
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
)
});
let entity = contextual.as_deref();
let mut m = match entity {
Some(EntityType::Hatch(dxf))
if crate::scene::annotative::active_object_context(
if crate::scene::annotative::active_object_context_for_scale(
&self.document,
handle,
annotation_scale_handle,
)
.is_some() =>
{
@ -935,12 +948,7 @@ impl Scene {
if dxf.pattern.lines.is_empty() =>
{
m.angle_offset = dxf.pattern_angle as f32;
let anno = if self.current_layout == "Model" {
self.annotation_scale
} else {
1.0
};
m.scale = dxf.pattern_scale as f32 * anno;
m.scale = dxf.pattern_scale as f32;
}
model::hatch_model::HatchPattern::Gradient { angle_deg, .. } => {
*angle_deg = dxf.pattern_angle.to_degrees() as f32;
@ -975,6 +983,8 @@ impl Scene {
hatch_bg,
true,
frozen,
annotation_scale_handle,
all_visible,
));
// Wide LwPolyline / Polyline2D bands are no longer hatch fills at
@ -1007,12 +1017,16 @@ impl Scene {
hatch_bg: [f32; 4],
tint_selected: bool,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
) -> Vec<HatchModel> {
self.exploded_insert_hatch_models_filtered(
layout_block,
hatch_bg,
tint_selected,
frozen,
annotation_scale_handle,
all_visible,
None,
false,
)
@ -1049,6 +1063,8 @@ impl Scene {
hatch_bg,
true,
(!frozen.is_empty()).then_some(&frozen),
self.displayed_annotation_scale_handle(),
self.annotation_all_visible(),
Some(&targets),
true,
)
@ -1060,6 +1076,8 @@ impl Scene {
hatch_bg: [f32; 4],
tint_selected: bool,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
targets: Option<&rustc_hash::FxHashSet<Handle>>,
include_preview_hidden: bool,
) -> Vec<HatchModel> {
@ -1148,8 +1166,11 @@ impl Scene {
out
}
for entity in self.document.entities() {
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, entity);
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
);
let EntityType::Insert(ins) = contextual.as_ref() else {
continue;
};
@ -1179,9 +1200,12 @@ impl Scene {
// Off-scale annotative representation (model space only — paper
// viewports use their per-viewport frozen scale layers). Skips its
// whole fill subtree, matching the wire path.
if frozen.is_none()
&& crate::scene::annotative::annotative_offscale(&self.document, &ins.common)
{
if crate::scene::annotative::annotative_offscale_for(
&self.document,
&ins.common,
annotation_scale_handle,
all_visible,
) {
continue;
}
if !self.block_has_hatch(&ins.block_name, &mut hatch_block_memo)
@ -1232,9 +1256,13 @@ impl Scene {
// use their per-viewport frozen scale layers. `explode` preserves
// the child handle, so the membership lookup still resolves here.
let offscale = |e: &EntityType| -> bool {
frozen.is_none()
&& matches!(e, EntityType::Insert(ni)
if crate::scene::annotative::annotative_offscale(&self.document, &ni.common))
matches!(e, EntityType::Insert(ni)
if crate::scene::annotative::annotative_offscale_for(
&self.document,
&ni.common,
annotation_scale_handle,
all_visible,
))
};
type ResolvedStyle = ([f32; 4], f32, [f32; 8], f32, u8);
let mut stack: Vec<(
@ -1248,7 +1276,11 @@ impl Scene {
.into_iter()
.filter(|e| !offscale(e))
.map(|e| {
crate::scene::annotative::entity_for_active_context(&self.document, &e)
crate::scene::annotative::entity_for_annotation_context(
&self.document,
&e,
annotation_scale_handle,
)
.into_owned()
})
.map(|e| {
@ -1338,9 +1370,10 @@ impl Scene {
continue;
}
let e =
crate::scene::annotative::entity_for_active_context(
crate::scene::annotative::entity_for_annotation_context(
&self.document,
&e,
annotation_scale_handle,
)
.into_owned();
stack.push((
@ -1451,6 +1484,8 @@ impl Scene {
&self,
target_block: Handle,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
) -> Vec<HatchModel> {
let is_paper = self.current_layout != "Model";
let bg_color: [f32; 4] = if is_paper {
@ -1537,8 +1572,11 @@ impl Scene {
// apply_rotation) rather than through Insert::explode — the latter
// double-scales the u/v basis.
for entity in self.document.entities() {
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, entity);
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
);
let EntityType::Insert(ins) = contextual.as_ref() else {
continue;
};
@ -1558,9 +1596,12 @@ impl Scene {
if !self.belongs_to_visible_block(c.handle, c.owner_handle, target_block) {
continue;
}
if frozen.is_none()
&& crate::scene::annotative::annotative_offscale(&self.document, c)
{
if crate::scene::annotative::annotative_offscale_for(
&self.document,
c,
annotation_scale_handle,
all_visible,
) {
continue;
}
self.collect_block_wipeouts(
@ -1571,6 +1612,8 @@ impl Scene {
bg_color,
&depth_map,
&mut models,
annotation_scale_handle,
all_visible,
);
}
models
@ -1588,6 +1631,8 @@ impl Scene {
bg_color: [f32; 4],
depth_map: &HashMap<u64, [f32; 2]>,
models: &mut Vec<HatchModel>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
) {
if depth > 32 {
return;
@ -1601,9 +1646,15 @@ impl Scene {
return;
};
for &eh in &br.entity_handles {
let Some(e) = self.document.get_entity(eh) else {
let Some(source) = self.document.get_entity(eh) else {
continue;
};
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
source,
annotation_scale_handle,
);
let e = contextual.as_ref();
let c = e.common();
if c.invisible
|| self
@ -1613,6 +1664,12 @@ impl Scene {
.map(|l| l.flags.off || l.flags.frozen)
.unwrap_or(false)
|| self.layer_frozen_in(&c.layer, frozen)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
c,
annotation_scale_handle,
all_visible,
)
{
continue;
}
@ -1655,6 +1712,8 @@ impl Scene {
bg_color,
depth_map,
models,
annotation_scale_handle,
all_visible,
);
}
_ => {}
@ -2240,7 +2299,11 @@ impl Scene {
.document
.get_entity(handle)
.map(|entity| {
crate::scene::annotative::entity_for_active_context(&self.document, entity)
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
self.displayed_annotation_scale_handle(),
)
});
let new_model = match contextual.as_deref() {
Some(EntityType::Hatch(dxf)) => {
@ -2272,7 +2335,11 @@ impl Scene {
.filter_map(|e| match e {
EntityType::Hatch(h) => Some((
h.common.handle,
crate::scene::annotative::entity_for_active_context(&self.document, e)
crate::scene::annotative::entity_for_annotation_context(
&self.document,
e,
self.displayed_annotation_scale_handle(),
)
.into_owned(),
)),
EntityType::Solid(s) => Some((s.common.handle, e.clone())),

File diff suppressed because it is too large Load diff

View file

@ -115,6 +115,15 @@ fn mirror_true_text_flags(e: &mut EntityType) {
}
impl Scene {
pub(crate) fn sync_displayed_annotation_context(&mut self, handle: Handle) -> bool {
let scale = self.displayed_annotation_scale_handle();
crate::scene::annotative::sync_annotation_context_from_entity(
&mut self.document,
handle,
scale,
)
}
/// Invalidate a dimension's baked block while capturing every removed
/// sub-entity for an active history transaction.
pub fn invalidate_dim_block_recorded(&mut self, handle: Handle) {
@ -239,10 +248,7 @@ impl Scene {
}
}
for &h in handles {
if crate::scene::annotative::sync_active_context_from_entity(
&mut self.document,
h,
) {
if self.sync_displayed_annotation_context(h) {
self.poison_undo_recording();
}
}
@ -414,10 +420,7 @@ impl Scene {
}
for handle in changed.iter().copied() {
let _ = crate::scene::annotative::sync_active_context_from_entity(
&mut self.document,
handle,
);
let _ = self.sync_displayed_annotation_context(handle);
}
if !changed.is_empty() {
self.rebuild_derived_caches();
@ -639,10 +642,7 @@ impl Scene {
if let Some(entity) = self.document.get_entity_mut(handle) {
view::dispatch::apply_grip(entity, grip_id, apply);
}
if crate::scene::annotative::sync_active_context_from_entity(
&mut self.document,
handle,
) {
if self.sync_displayed_annotation_context(handle) {
self.poison_undo_recording();
}
// A dimension loaded from a file renders through its baked *D block;

View file

@ -385,12 +385,17 @@ impl Scene {
.unwrap_or(false)
};
let mut models: Vec<HatchModel> = Vec::new();
let annotation_scale_handle = self.paper_annotation_scale_handle();
let all_visible = self.annotation_all_visible();
for (&handle, model) in self.hatches.iter() {
let Some(source) = self.document.get_entity(handle) else {
continue;
};
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, source);
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
source,
annotation_scale_handle,
);
let entity = contextual.as_ref();
// Paper-space SOLIDs already carry WCS-aware wire fill triangles.
// Keep their cached XY HatchModel out of the sheet set so the same
@ -403,6 +408,12 @@ impl Scene {
if c.invisible
|| self.entity_temporarily_hidden(handle)
|| layer_hidden(&c.layer)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
c,
annotation_scale_handle,
all_visible,
)
{
continue;
}
@ -411,9 +422,10 @@ impl Scene {
}
let mut m = match entity {
EntityType::Hatch(dxf)
if crate::scene::annotative::active_object_context(
if crate::scene::annotative::active_object_context_for_scale(
&self.document,
handle,
annotation_scale_handle,
)
.is_some() =>
{
@ -453,7 +465,14 @@ impl Scene {
} else {
self.bg_color
};
let exploded = self.exploded_insert_hatch_models(layout_block, hatch_bg, false, None);
let exploded = self.exploded_insert_hatch_models(
layout_block,
hatch_bg,
false,
None,
annotation_scale_handle,
all_visible,
);
models.extend(exploded);
Arc::new(models)
}
@ -465,6 +484,8 @@ impl Scene {
&self,
block: Handle,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
) -> Vec<HatchModel> {
let layer_hidden = |layer: &str| {
self.document
@ -478,23 +499,33 @@ impl Scene {
let Some(source) = self.document.get_entity(handle) else {
continue;
};
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, source);
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
source,
annotation_scale_handle,
);
let entity = contextual.as_ref();
let common = entity.common();
if common.invisible
|| self.entity_temporarily_hidden(handle)
|| layer_hidden(&common.layer)
|| self.layer_frozen_in(&common.layer, frozen)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
common,
annotation_scale_handle,
all_visible,
)
|| !self.belongs_to_visible_block(handle, common.owner_handle, block)
{
continue;
}
let mut hatch = match entity {
EntityType::Hatch(dxf)
if crate::scene::annotative::active_object_context(
if crate::scene::annotative::active_object_context_for_scale(
&self.document,
handle,
annotation_scale_handle,
)
.is_some() =>
{
@ -522,6 +553,8 @@ impl Scene {
self.paper_bg_color,
false,
frozen,
annotation_scale_handle,
all_visible,
));
models
}
@ -532,6 +565,9 @@ impl Scene {
&self,
block: Handle,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
highlight_selection: bool,
) -> Vec<HatchModel> {
let depth_map = self.draw_depth_map();
let mut models = Vec::new();
@ -562,7 +598,11 @@ impl Scene {
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "WIPEOUT_FILL".into(),
color: self.paper_bg_color,
color: if highlight_selection && self.selected.contains(&common.handle) {
[0.15, 0.55, 1.00, 0.35]
} else {
self.paper_bg_color
},
aci: 0,
line_weight_px: 1.0,
angle_offset: 0.0,
@ -574,8 +614,11 @@ impl Scene {
});
}
for entity in self.document.entities() {
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, entity);
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
);
let EntityType::Insert(insert) = contextual.as_ref() else {
continue;
};
@ -589,6 +632,12 @@ impl Scene {
.map(|layer| layer.flags.off || layer.flags.frozen)
.unwrap_or(false)
|| self.layer_frozen_in(&common.layer, frozen)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
common,
annotation_scale_handle,
all_visible,
)
|| !self.belongs_to_visible_block(common.handle, common.owner_handle, block)
{
continue;
@ -598,9 +647,15 @@ impl Scene {
&insert.block_name,
0,
frozen,
self.paper_bg_color,
if highlight_selection && self.selected.contains(&common.handle) {
[0.15, 0.55, 1.00, 0.35]
} else {
self.paper_bg_color
},
&depth_map,
&mut models,
annotation_scale_handle,
all_visible,
);
}
models
@ -613,54 +668,13 @@ impl Scene {
/// copy on the paper sheet.
pub fn paper_canvas_wipeouts(&self) -> Arc<Vec<HatchModel>> {
let layout_block = self.current_layout_block_handle();
let bg_color = self.paper_bg_color;
let mut models = Vec::new();
for entity in self.document.entities() {
let EntityType::Wipeout(wo) = entity else {
continue;
};
if wo.common.invisible
|| self.entity_temporarily_hidden(wo.common.handle)
{
continue;
}
if self
.document
.layers
.get(&wo.common.layer)
.map(|l| l.flags.off || l.flags.frozen)
.unwrap_or(false)
{
continue;
}
if !self.belongs_to_visible_block(wo.common.handle, wo.common.owner_handle, layout_block)
{
continue;
}
// Paper-block wipeouts live in paper coords — no `world_offset`.
let (fill_origin, boundary) = Self::wipeout_boundary_2d(wo);
if boundary.len() < 3 {
continue;
}
let mut fill_color = bg_color;
if self.selected.contains(&wo.common.handle) {
fill_color = [0.15, 0.55, 1.00, 0.35];
}
models.push(HatchModel {
boundary: Arc::new(boundary),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "WIPEOUT_FILL".into(),
color: fill_color,
aci: 0,
line_weight_px: 1.0,
angle_offset: 0.0,
scale: 1.0,
world_origin: fill_origin,
draw_depth: 0.0,
});
}
Arc::new(models)
Arc::new(self.plot_wipeouts_for_block(
layout_block,
None,
self.paper_annotation_scale_handle(),
self.annotation_all_visible(),
true,
))
}
/// Build a Camera oriented and scaled to match a paper-space Viewport entity.
@ -805,8 +819,8 @@ impl Scene {
// Its live zoom is camera magnification, not CANNOSCALE: tying
// annotation geometry to view_height rebuilt the entire model on every
// wheel tick whenever the drawing contained one annotative object.
// Explicit annotation-scale changes still rebuild through
// `self.annotation_scale`; PSLTSCALE is a viewport GPU uniform.
// Explicit viewport annotation-scale changes still rebuild the resident
// set; PSLTSCALE is a viewport GPU uniform.
let frozen = match self.document.get_entity(vp_handle) {
Some(EntityType::Viewport(vp)) => {
let f: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
@ -815,9 +829,11 @@ impl Scene {
_ => HSet::default(),
};
let scale_handle = self.viewport_scale_handle(vp_handle);
self.resident_wires_for(
self.model_space_block_handle(),
Some(self.annotation_scale),
Some(self.viewport_annotation_multiplier(vp_handle)),
scale_handle,
Some(&frozen),
)
}

View file

@ -430,7 +430,12 @@ impl Scene {
let frozen: rustc_hash::FxHashSet<Handle> =
viewport.frozen_layers.iter().copied().collect();
let hatches = self.plot_hatches_for_block(model_block, Some(&frozen));
let hatches = self.plot_hatches_for_block(
model_block,
Some(&frozen),
self.viewport_scale_handle(viewport.common.handle),
self.annotation_all_visible(),
);
for hatch in hatches {
if matches!(&hatch.pattern, HatchPattern::Pattern(_)) {
let mut points = Vec::new();
@ -482,7 +487,13 @@ impl Scene {
}
}
for wipeout in self.plot_wipeouts_for_block(model_block, Some(&frozen)) {
for wipeout in self.plot_wipeouts_for_block(
model_block,
Some(&frozen),
self.viewport_scale_handle(viewport.common.handle),
self.annotation_all_visible(),
false,
) {
if let Some(wipeout) =
project_plot_fill(wipeout, &project, xmin, ymin, xmax, ymax)
{

View file

@ -2243,15 +2243,15 @@ impl Scene {
(hatches, wipeouts, Some(images))
} else {
(
self.hatch_models_for_viewport(&vp_frozen),
self.wipeout_models_for_viewport(&vp_frozen),
self.hatch_models_for_viewport(inst.handle, &vp_frozen),
self.wipeout_models_for_viewport(inst.handle, &vp_frozen),
None,
)
};
let images = if let Some(images) = paper_images {
images
} else {
self.images_for_viewport(&vp_frozen)
self.images_for_viewport(inst.handle, &vp_frozen)
};
// The paper sheet shows the layout's own 2-D content (fills, borders,
// annotation) — never the model's 3-D solids. Those are drawn inside
@ -2263,7 +2263,7 @@ impl Scene {
let meshes = if inst.paper_sheet {
Arc::new(Vec::new())
} else {
self.meshes_for_viewport(&vp_frozen)
self.meshes_for_viewport(inst.handle, &vp_frozen)
};
// SDF text quads (behind OCS_TEXT_SDF). The glyph quads ride on each

View file

@ -14,24 +14,28 @@ use crate::ui::statusbar::status_menu::Entry;
/// scales of its own.
pub fn menu_entries(
is_model: bool,
current_anno_scale: f32,
current_scale_name: &str,
viewport_scale: Option<f64>,
file_scales: Vec<(String, f32, f64)>,
) -> Vec<Entry<'static>> {
let mut entries: Vec<Entry<'static>> = file_scales
.into_iter()
.map(|(label, anno_scale, vp_scale)| {
.map(|(label, _anno_scale, vp_scale)| {
let active = if is_model {
(current_anno_scale - anno_scale).abs() < 0.001 * current_anno_scale.max(0.001)
label.eq_ignore_ascii_case(current_scale_name)
} else {
viewport_scale
.map(|vs| (vs - vp_scale).abs() < 0.001 * vp_scale.max(0.001))
.unwrap_or(false)
label.eq_ignore_ascii_case(current_scale_name)
|| (current_scale_name.is_empty()
&& viewport_scale
.map(|vs| {
(vs - vp_scale).abs() < 0.001 * vp_scale.max(0.001)
})
.unwrap_or(false))
};
let msg = if is_model {
Message::SetAnnotationScale(anno_scale)
Message::SetAnnotationScale(label.clone())
} else {
Message::SetViewportScale(vp_scale)
Message::SetViewportScale(label.clone())
};
Entry::close(scale_row(label, active, msg))
})

View file

@ -27,10 +27,15 @@ use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
use crate::ui::statusbar::status_menu::Entry as StatusMenuEntry;
use crate::ui::wrap_bar::WrapBar;
const ST_ANNO_VISIBILITY: &[u8] = include_bytes!("../../../assets/icons/scale_list.svg");
const ST_ANNO_AUTO_ADD: &[u8] = include_bytes!("../../../assets/icons/add_scale.svg");
const ST_VP_SCALE_SYNC: &[u8] = include_bytes!("../../../assets/icons/sync.svg");
pub struct StatusMenuData<'a> {
pub layout_names: Vec<String>,
pub polar_custom_input: &'a str,
pub scale_is_model: bool,
pub current_scale_name: String,
pub scale_list: Vec<(String, f32, f64)>,
pub has_selection: bool,
pub selection_types: Vec<String>,
@ -80,6 +85,9 @@ impl StatusBar {
annotation_scale: f32,
// True when the scale pill is interactive (always model space; paper space only when a viewport is active/selected).
scale_pill_enabled: bool,
annotation_all_visible: bool,
annotation_auto_add: bool,
viewport_scale_synced: Option<bool>,
// LWDISPLAY header flag — controls lineweight visibility in the viewport.
lineweight_display: bool,
// Live cursor position in model coordinates, for the coordinate readout.
@ -113,6 +121,7 @@ impl StatusBar {
layout_names,
polar_custom_input,
scale_is_model,
current_scale_name,
scale_list,
has_selection,
selection_types,
@ -175,19 +184,23 @@ impl StatusBar {
// Keep its text identical to the active drawing-defined scale. Rebuilding
// the label from the numeric factor turns an architectural
// `1/2" = 1'-0"` scale into `1:24`, mixing formats in the same control.
let scale_label = active_scale_label(
scale_is_model,
annotation_scale,
viewport_scale,
&scale_list,
)
.unwrap_or_else(|| {
if scale_is_model {
format_scale(Some(1.0 / annotation_scale as f64))
} else {
format_scale(viewport_scale)
}
});
let scale_label = if current_scale_name.is_empty() {
active_scale_label(
scale_is_model,
annotation_scale,
viewport_scale,
&scale_list,
)
.unwrap_or_else(|| {
if scale_is_model {
format_scale(Some(1.0 / annotation_scale as f64))
} else {
format_scale(viewport_scale)
}
})
} else {
current_scale_name.clone()
};
let scale_element: Element<'_, Message> = if scale_pill_enabled {
status_menu::menu_bar(
menu_tip(
@ -197,7 +210,7 @@ impl StatusBar {
),
crate::ui::popup::scale_popup::menu_entries(
scale_is_model,
annotation_scale,
&current_scale_name,
viewport_scale,
scale_list,
),
@ -294,6 +307,47 @@ impl StatusBar {
if vis(StatusPill::Scale) {
pills.push(scale_element);
}
if vis(StatusPill::AnnoVisibility) {
pills.push(
tip(
toggle_pill(
ST_ANNO_VISIBILITY,
annotation_all_visible,
Message::ToggleAnnotationVisibility,
),
"Show Annotation Objects",
)
.into(),
);
}
if vis(StatusPill::AnnoAutoAdd) {
pills.push(
tip(
toggle_pill(
ST_ANNO_AUTO_ADD,
annotation_auto_add,
Message::ToggleAnnotationAutoAdd,
),
"Automatically Add Scales",
)
.into(),
);
}
if vis(StatusPill::VpScaleSync) {
if let Some(synced) = viewport_scale_synced {
pills.push(
tip(
toggle_pill(
ST_VP_SCALE_SYNC,
synced,
Message::SyncViewportAnnotationScale,
),
"Viewport / Annotation Scale Sync",
)
.into(),
);
}
}
if vis(StatusPill::Units) {
pills.push(
status_menu::menu_bar(

View file

@ -20,6 +20,9 @@ pub enum StatusPill {
Osnap,
Space,
Scale,
AnnoVisibility,
AnnoAutoAdd,
VpScaleSync,
Units,
Transparency,
Isolate,
@ -43,6 +46,9 @@ impl StatusPill {
StatusPill::Osnap,
StatusPill::Space,
StatusPill::Scale,
StatusPill::AnnoVisibility,
StatusPill::AnnoAutoAdd,
StatusPill::VpScaleSync,
StatusPill::Units,
StatusPill::Transparency,
StatusPill::Isolate,
@ -65,6 +71,9 @@ impl StatusPill {
StatusPill::Osnap => "osnap",
StatusPill::Space => "space",
StatusPill::Scale => "scale",
StatusPill::AnnoVisibility => "anno_visibility",
StatusPill::AnnoAutoAdd => "anno_auto_add",
StatusPill::VpScaleSync => "vp_scale_sync",
StatusPill::Units => "units",
StatusPill::Transparency => "transparency",
StatusPill::Isolate => "isolate",
@ -88,6 +97,9 @@ impl StatusPill {
StatusPill::Osnap => "Object Snap",
StatusPill::Space => "Model/Paper Space",
StatusPill::Scale => "Annotation Scale",
StatusPill::AnnoVisibility => "Show Annotation Objects",
StatusPill::AnnoAutoAdd => "Automatically Add Scales",
StatusPill::VpScaleSync => "Viewport / Annotation Scale Sync",
StatusPill::Units => "Drawing Units",
StatusPill::Transparency => "Show Transparency",
StatusPill::Isolate => "Isolate Objects",

View file

@ -537,7 +537,15 @@ pub fn view_window<'a>(
chk("Annotative", vals.annotative, DsField::Annotative),
row![
lbl("Overall scale (DIMSCALE)"),
mk_field(DsField::Dimscale, vals.dimscale)
if vals.annotative {
text_input("", "0")
.style(field_style)
.size(11)
.width(100)
.into()
} else {
mk_field(DsField::Dimscale, vals.dimscale)
}
]
.spacing(8)
.align_y(iced::Center),

View file

@ -105,6 +105,16 @@ fn num_row<'a>(
.into()
}
fn readonly_num_row<'a>(label: &'static str, value: &'a str) -> Element<'a, Message> {
row![
text(label).size(11).style(muted_style).width(150),
text(value).size(11).style(muted_style),
]
.spacing(8)
.align_y(iced::Center)
.into()
}
/// Shared colour selector row. Reuses MLeaderStyleEdit by sending the chosen
/// colour as an ACI string; `open` shows the expanded palette.
fn color_row<'a>(
@ -288,7 +298,11 @@ pub fn view_window<'a>(
v.second_seg_angle,
"second_seg_angle"
),
num_row("Scale factor:", "1.0", v.scale_factor, "scale_factor"),
if s.is_annotative {
readonly_num_row("Scale factor:", "By annotation scale")
} else {
num_row("Scale factor:", "1.0", v.scale_factor, "scale_factor")
},
num_row("Align space:", "4.0", v.align_space, "align_space"),
enum_row(
"Leader draw order:",

View file

@ -293,11 +293,6 @@ pub fn view_window<'a>(
]
.spacing(8)
.align_y(iced::Center),
checkbox(s.annotative)
.label("Annotative")
.on_toggle(|_| Message::TableStyleToggleAnnotative)
.size(14)
.text_size(11),
row![
text("H Margin:").size(11).style(muted_style).width(160),
text_input("1.5", hmargin_buf)

View file

@ -298,7 +298,16 @@ pub fn view_window<'a>(
text("Properties").size(11).style(primary_style),
frow("Big Font:", "big-font file…", bigfont_buf, "bigfont"),
frow("TrueType Font:", "e.g. Arial", ttf_buf, "ttf"),
frow("Fixed Height:", "0 = variable", height_buf, "height"),
frow(
if annotative {
"Paper Text Height:"
} else {
"Fixed Height:"
},
"0 = variable",
height_buf,
"height",
),
frow("Width Factor:", "1.0", width_buf, "width"),
frow("Oblique (°):", "0.0", oblique_buf, "oblique"),
row![