From e03e091b3cd1791758e51d974be91add600c9495 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 2 Aug 2026 12:00:15 +0300 Subject: [PATCH] feat(annotation): integrate object contexts --- src/app/command_driver.rs | 124 ++- src/app/commands/blocks.rs | 29 +- src/app/commands/display.rs | 138 +++- src/app/commands/mod.rs | 3 + src/app/config.rs | 3 + src/app/mod.rs | 29 +- src/app/mtext_editor.rs | 1 + src/app/properties.rs | 135 ++-- src/app/style_ops.rs | 198 ++++- src/app/update/command.rs | 55 +- src/app/update/file.rs | 2 + src/app/update/mod.rs | 95 ++- src/app/update/style.rs | 14 +- src/app/update/util.rs | 32 +- src/app/view/mod.rs | 4 + src/entities/insert.rs | 1 + src/entities/multileader.rs | 29 +- src/io/mod.rs | 65 +- src/scene/annotative.rs | 743 +++++++++++++++--- src/scene/cache/block_cache.rs | 77 +- src/scene/camera_ops.rs | 36 +- src/scene/convert/dgn_linestyle.rs | 1 + src/scene/convert/tess.rs | 31 +- src/scene/convert/tessellate.rs | 9 +- src/scene/entity.rs | 125 ++- src/scene/mod.rs | 1085 +++++++++++++++++++++++--- src/scene/modify.rs | 24 +- src/scene/paper.rs | 140 ++-- src/scene/project.rs | 15 +- src/scene/view/render.rs | 8 +- src/ui/popup/scale_popup.rs | 20 +- src/ui/statusbar/mod.rs | 82 +- src/ui/statusbar/statusbar_config.rs | 12 + src/ui/style/dimstyle.rs | 10 +- src/ui/style/mleaderstyle.rs | 16 +- src/ui/style/tablestyle.rs | 5 - src/ui/style/textstyle.rs | 11 +- 37 files changed, 2823 insertions(+), 584 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index fd52dcd5..67d1831d 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -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). diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index b567a863..2cf3f70c 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -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 diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index ad976261..8a93a483 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -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 = 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::() { + 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::(), b.trim().parse::()) { - (Ok(a), Ok(b)) if a != 0.0 => Some((b / a) as f32), - _ => None, - } - } else { - arg.parse::().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 e.g. 1:50, 2:1, or a factor"), } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index a6e81750..72e8c766 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -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. diff --git a/src/app/config.rs b/src/app/config.rs index 2d88f6a6..e83bda40 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -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(), } diff --git a/src/app/mod.rs b/src/app/mod.rs index fff02f6b..18a02b2a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -349,6 +349,8 @@ pub(super) struct OpenCADStudio { cycle_candidates: Option<(iced::Point, Vec)>, /// 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, @@ -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, diff --git a/src/app/mtext_editor.rs b/src/app/mtext_editor.rs index 21c81167..921e5ad1 100644 --- a/src/app/mtext_editor.rs +++ b/src/app/mtext_editor.rs @@ -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). diff --git a/src/app/properties.rs b/src/app/properties.rs index 97d475d7..0f239a31 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -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); diff --git a/src/app/style_ops.rs b/src/app/style_ops.rs index d3ae43a9..7ca0e6db 100644 --- a/src/app/style_ops.rs +++ b/src/app/style_ops.rs @@ -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; } diff --git a/src/app/update/command.rs b/src/app/update/command.rs index ff2010e2..1e3f8c89 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1491,15 +1491,19 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // 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 = 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 { 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 { _ => {} } } + 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 diff --git a/src/app/update/file.rs b/src/app/update/file.rs index da61faf1..065cffbe 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -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; } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index a0b3ddfa..22b26039 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -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, diff --git a/src/app/update/style.rs b/src/app/update/style.rs index a8b6576d..c20da113 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -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, diff --git a/src/app/update/util.rs b/src/app/update/util.rs index c09a82ed..3a01d394 100644 --- a/src/app/update/util.rs +++ b/src/app/update/util.rs @@ -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(¤t) + && (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] } - diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 27565ee5..845ef34b 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -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, diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 10a81ff6..f16888a4 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -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, ); diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 4be67b1c..c62fdd66 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -96,7 +96,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option [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 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; diff --git a/src/io/mod.rs b/src/io/mod.rs index 957cdae4..d879a303 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -1095,14 +1095,44 @@ fn vardict_value(doc: &CadDocument, name: &str) -> Option { } } -/// 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 { /// 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 ────────────────────────────────────────────────── diff --git a/src/scene/annotative.rs b/src/scene/annotative.rs index 4ba9554a..3b023c48 100644 --- a/src/scene/annotative.rs +++ b/src/scene/annotative.rs @@ -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 { + 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, + 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 { + 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 { /// 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, ) -> 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, ) -> 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, ) -> 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, ) -> 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) -> bool { }) } -fn table_style_annotative(doc: &CadDocument, handle: Option) -> 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, +) -> 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, } } diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index e5f414eb..9a2977ed 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -196,6 +196,8 @@ impl BlockCache { pub fn build( doc: &CadDocument, anno_scale: f32, + annotation_scale_handle: Option, + 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, + all_visible: bool, bg_color: [f32; 4], depth_map: &HashMap, ) -> 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, bg_color: [f32; 4], depth_map: &HashMap, ) -> Vec { @@ -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![]; diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 20963762..d3bd1500 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -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 diff --git a/src/scene/convert/dgn_linestyle.rs b/src/scene/convert/dgn_linestyle.rs index 9cb6f310..b44116a3 100644 --- a/src/scene/convert/dgn_linestyle.rs +++ b/src/scene/convert/dgn_linestyle.rs @@ -321,6 +321,7 @@ pub fn place_block_wires( [0.0; 8], line_weight_px, anno_scale, + None, world_per_pixel, bg_color, false, diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 32f463a9..9e86575a 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -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, bg_color: [f32; 4], anno_scale: f32, + annotation_scale_handle: Option, 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 { - 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, diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 84a2f2c8..1426660e 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -131,6 +131,7 @@ pub fn tessellate( pattern: [f32; 8], line_weight_px: f32, anno_scale: f32, + annotation_scale_handle: Option, world_per_pixel: Option, // 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 diff --git a/src/scene/entity.rs b/src/scene/entity.rs index c4a1c621..0ec12b36 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -685,7 +685,7 @@ impl Scene { entities: Vec, name: &str, base: glam::DVec3, - ) -> Result<(), String> { + ) -> Result, 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>, + annotation_scale_handle: Option, + all_visible: bool, ) -> Vec { 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>, + annotation_scale_handle: Option, + all_visible: bool, ) -> Vec { 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>, + annotation_scale_handle: Option, + all_visible: bool, targets: Option<&rustc_hash::FxHashSet>, include_preview_hidden: bool, ) -> Vec { @@ -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>, + annotation_scale_handle: Option, + all_visible: bool, ) -> Vec { 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, models: &mut Vec, + annotation_scale_handle: Option, + 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())), diff --git a/src/scene/mod.rs b/src/scene/mod.rs index bf0a9992..5e20dae3 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -707,7 +707,11 @@ fn build_derived_caches_impl( .par_iter() .filter_map(|&handle| { let source = doc.get_entity(handle)?; - let contextual = annotative::entity_for_active_context(doc, source); + let contextual = annotative::entity_for_annotation_context( + doc, + source, + annotative::scale_handle_by_name(doc, &doc.header.current_annotation_scale), + ); let e = contextual.as_ref(); let (raw, ..) = view::render::render_style_for(doc, e); let color = view::render::adapt_to_bg(raw, LOAD_BG); @@ -1774,7 +1778,7 @@ pub struct Scene { /// background and block epoch. Model and Paper adapt black/white colours /// differently; retaining both variants prevents a full block rebuild on /// every layout-tab switch. - block_defn_cache: RefCell)>>, + block_defn_cache: RefCell)>>, /// Spatial index + always-emit list for top-level entities /// (Phase 2.1). Lazily rebuilt by `entity_index()` on /// `geometry_epoch` change. See `EntityIndex` for what each side @@ -2048,13 +2052,21 @@ impl Scene { /// Get (or build on miss) the block-definition cache for the current epoch. /// Built single-threaded — recursive nested expansion makes parallelization /// fiddly and the cache only rebuilds when geometry actually changes. - pub(super) fn block_cache_arc(&self) -> Arc { + pub(super) fn block_cache_arc_for( + &self, + annotation_scale_handle: Option, + all_visible: bool, + ) -> Arc { let bg = if self.current_layout == "Model" { self.bg_color } else { self.paper_bg_color }; - let key = bg.map(f32::to_bits); + let mut key = annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0); + for component in bg { + key = key.rotate_left(13) ^ component.to_bits() as u64; + } + key = key.rotate_left(13) ^ all_visible as u64; { let cache = self.block_defn_cache.borrow(); if let Some((epoch, arc)) = cache.get(&key) { @@ -2066,10 +2078,15 @@ impl Scene { // Block definitions are cached at block-local size (annotation scale // 1.0). An annotative block scales as ONE unit at the INSERT level, so // its internal geometry / text / attributes must NOT be scaled - // individually (that would double-scale — AutoCAD even forbids - // annotative attributes inside annotative blocks for this reason). - let built = - cache::block_cache::BlockCache::build(&self.document, 1.0, bg, &self.draw_depth_map()); + // individually because that would apply the same scale twice. + let built = cache::block_cache::BlockCache::build( + &self.document, + 1.0, + annotation_scale_handle, + all_visible, + bg, + &self.draw_depth_map(), + ); let arc = Arc::new(built); let mut cache = self.block_defn_cache.borrow_mut(); cache.retain(|_, (epoch, _)| *epoch == self.block_epoch); @@ -2084,7 +2101,18 @@ impl Scene { /// optional interaction index is cached against the exact same `Arc`. pub fn install_prepared_open_geometry(&self, prepared: PreparedOpenGeometry) { let block = self.model_space_block_handle(); - let key = Self::resident_wire_key(block, self.bg_color, None, None); + let scale = crate::scene::annotative::scale_handle_by_name( + &self.document, + &self.document.header.current_annotation_scale, + ); + let key = Self::resident_wire_key( + block, + self.bg_color, + None, + scale, + self.annotation_all_visible(), + None, + ); let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed); self.last_model_wire_gen.set(gen); self.resident_wire_sets.borrow_mut().insert( @@ -3454,29 +3482,18 @@ impl Scene { } } - /// Scale of the first user viewport (id > 1) in the current paper layout, - /// used for the status-bar display. Returns `None` in Model space or if - /// no user viewport exists. + /// Scale of the active, selected, or first content viewport in the current + /// paper layout. Returns `None` in Model space or when no viewport exists. pub fn first_viewport_scale(&self) -> Option { - if self.current_layout == "Model" { + let handle = self.target_viewport_handle()?; + let EntityType::Viewport(vp) = self.document.get_entity(handle)? else { return None; - } - let layout_block = self.current_layout_block_handle(); - if layout_block.is_null() { - return None; - } - self.document.entities().find_map(|e| { - if let EntityType::Viewport(vp) = e { - if self.is_content_viewport_in_layout(vp, layout_block) { - return Some(vp_effective_scale( - vp.custom_scale, - vp.view_height, - vp.height, - )); - } - } - None - }) + }; + Some(vp_effective_scale( + vp.custom_scale, + vp.view_height, + vp.height, + )) } /// Annotation/viewport scales defined in the drawing's scale list @@ -3742,6 +3759,182 @@ impl Scene { self.scale_handle_ensuring(&name) } + pub(crate) fn paper_annotation_scale_handle(&self) -> Option { + self.scale_object_handle("1:1").or_else(|| { + self.document.objects.iter().find_map(|(handle, object)| match object { + ObjectType::Scale(scale) + if !scale.is_temporary && (scale.factor() - 1.0).abs() <= 1.0e-9 => + { + Some(*handle) + } + _ => None, + }) + }) + } + + pub(crate) fn creation_annotation_scale_handle(&mut self) -> Option { + if self.current_layout == "Model" { + return self.current_annotation_scale_handle(); + } + if let Some(viewport) = self.active_viewport { + if let Some(scale) = self.viewport_scale_handle(viewport) { + return Some(scale); + } + } + if let Some(scale) = self.paper_annotation_scale_handle() { + return Some(scale); + } + self.scale_handle_ensuring("1:1") + } + + pub fn set_annotation_scale_named(&mut self, name: &str) -> Option { + let handle = self.scale_handle_ensuring(name)?; + let ObjectType::Scale(scale) = self.document.objects.get(&handle)? else { + return None; + }; + let multiplier = scale.inverse_factor(); + self.annotation_scale = multiplier as f32; + self.document.header.current_annotation_scale = scale.name.clone(); + self.document.header.annotation_scale_value = scale.factor(); + self.invalidate_annotation_dependencies(); + Some(handle) + } + + fn current_layout_object_handle(&self) -> Option { + self.document.objects.iter().find_map(|(handle, object)| match object { + ObjectType::Layout(layout) + if layout.name.eq_ignore_ascii_case(&self.current_layout) => + { + Some(*handle) + } + _ => None, + }) + } + + pub fn annotation_all_visible(&self) -> bool { + use acadrust::objects::XRecordValue; + let Some(layout) = self.current_layout_object_handle() else { + return true; + }; + self.document + .xrecord(layout, "OPEN_CAD_ANNOTATION_STATE") + .and_then(|record| { + record.entries.iter().find_map(|entry| match entry.value { + XRecordValue::Bool(value) if entry.code == 290 => Some(value), + _ => None, + }) + }) + .unwrap_or(true) + } + + pub fn set_annotation_all_visible(&mut self, value: bool) { + use acadrust::objects::{XRecordEntry, XRecordValue}; + let Some(layout) = self.current_layout_object_handle() else { + return; + }; + self.document + .ensure_xrecord(layout, "OPEN_CAD_ANNOTATION_STATE"); + if let Some(record) = self + .document + .xrecord_mut(layout, "OPEN_CAD_ANNOTATION_STATE") + { + if let Some(entry) = record.entries.iter_mut().find(|entry| entry.code == 290) { + entry.value = XRecordValue::Bool(value); + } else { + record.entries.push(XRecordEntry::bool(290, value)); + } + } + self.invalidate_annotation_dependencies(); + } + + pub fn add_annotation_scale_to_objects( + &mut self, + scale: Handle, + previous_scale: Option, + mode: u8, + ) -> usize { + if previous_scale == Some(scale) || !(1..=4).contains(&mode) { + return 0; + } + let viewport_frozen: HashSet = self + .target_viewport_handle() + .and_then(|handle| match self.document.get_entity(handle) { + Some(EntityType::Viewport(viewport)) => { + Some(viewport.frozen_layers.iter().copied().collect()) + } + _ => None, + }) + .unwrap_or_default(); + let handles: Vec<_> = self + .document + .entities() + .filter(|entity| { + if !crate::scene::annotative::is_annotative(&self.document, entity) { + return false; + } + let memberships = crate::scene::annotative::object_scale_memberships( + &self.document, + entity.common().handle, + ); + if !previous_scale + .is_some_and(|current| memberships.iter().any(|(_, member)| *member == current)) + { + return false; + } + let layer = self.document.layers.get(&entity.common().layer); + let (off, frozen, locked, viewport_frozen) = layer + .map(|layer| { + ( + layer.flags.off, + layer.flags.frozen, + layer.flags.locked, + viewport_frozen.contains(&layer.handle), + ) + }) + .unwrap_or((false, false, false, false)); + match mode { + 1 => !(off || frozen || locked || viewport_frozen), + 2 => !(off || frozen || viewport_frozen), + 3 => !locked, + 4 => true, + _ => false, + } + }) + .map(|entity| entity.common().handle) + .collect(); + let mut added = 0; + for handle in handles { + let existed = crate::scene::annotative::object_scale_memberships( + &self.document, + handle, + ) + .iter() + .any(|(_, member)| *member == scale); + if !existed + && crate::scene::annotative::create_annotation_context( + &mut self.document, + handle, + scale, + ) + { + added += 1; + } + } + if added > 0 { + self.bump_geometry(); + } + added + } + + pub(crate) fn displayed_annotation_scale_handle(&self) -> Option { + if self.current_layout == "Model" { + return self.scale_object_handle(&self.document.header.current_annotation_scale); + } + self.explicit_viewport_handle() + .and_then(|viewport| self.viewport_scale_handle(viewport)) + .or_else(|| self.paper_annotation_scale_handle()) + } + /// Resolve a named annotation scale to a real `Scale` object handle, /// materializing the object from the scale list when the drawing names the /// scale (e.g. a virtual fallback scale) but has no `Scale` object for it. @@ -3749,7 +3942,22 @@ impl Scene { if let Some(h) = self.scale_object_handle(name) { return Some(h); } - let (paper, drawing) = self.scale_paper_drawing(name).unwrap_or((1.0, 1.0)); + let fallback = self + .default_scales() + .iter() + .find(|(label, _)| label.eq_ignore_ascii_case(name)) + .map(|(_, factor)| (1.0, 1.0 / factor)) + .or_else(|| { + let (paper, drawing) = name.split_once(':')?; + let paper = paper.trim().parse::().ok()?; + let drawing = drawing.trim().parse::().ok()?; + (paper > 0.0 && drawing > 0.0).then_some((paper, drawing)) + }) + .or_else(|| { + let drawing = name.trim().parse::().ok()?; + (drawing > 0.0).then_some((1.0, drawing)) + })?; + let (paper, drawing) = self.scale_paper_drawing(name).unwrap_or(fallback); self.add_scale(name, paper, drawing); self.scale_object_handle(name) } @@ -3804,6 +4012,53 @@ impl Scene { let Some(sh) = self.scale_object_handle(name) else { return false; }; + if self + .document + .header + .current_annotation_scale + .eq_ignore_ascii_case(name) + { + return false; + } + let entity_handles: Vec<_> = self + .document + .entities() + .map(|entity| entity.common().handle) + .collect(); + for entity in entity_handles { + crate::scene::annotative::remove_annotation_context_for_scale( + &mut self.document, + entity, + sh, + ); + } + let replacement = self.document.objects.iter().find_map(|(handle, object)| match object { + ObjectType::Scale(scale) if *handle != sh && !scale.is_temporary => Some(*handle), + _ => None, + }); + let viewports: Vec<_> = self + .document + .entities() + .filter_map(|entity| match entity { + EntityType::Viewport(viewport) + if self.document.viewport_annotation_scale(viewport.common.handle) == Some(sh) => + { + Some(viewport.common.handle) + } + _ => None, + }) + .collect(); + for viewport in viewports { + if let Some(replacement) = replacement { + self.document + .set_viewport_annotation_scale(viewport, replacement); + } else if let Some(record) = self + .document + .xrecord_mut(viewport, "ASDK_XREC_ANNOTATION_SCALE_INFO") + { + record.entries.retain(|entry| entry.code != 340); + } + } // Resolve the dictionary before removing the object (the fallback lookup // reads a surviving scale's owner). let dict_h = self.scalelist_dict_handle(); @@ -3814,6 +4069,7 @@ impl Scene { sl.entries.retain(|(_, h)| *h != sh); } } + self.invalidate_annotation_dependencies(); true } @@ -4043,31 +4299,151 @@ impl Scene { }) } - /// Set the scale of the active/selected viewport. - /// Priority: active_viewport → first selected viewport → first viewport in layout. - pub fn set_viewport_scale(&mut self, scale: f64) { - let target = - self.active_viewport - .or_else(|| { - self.selected.iter().copied().find(|&h| { - matches!(self.document.get_entity(h), Some(EntityType::Viewport(_))) - }) - }) - .or_else(|| self.first_viewport_handle()); - - if let Some(handle) = target { - if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) { - if !vp.status.locked && scale > 1e-9 { - vp.custom_scale = scale; - vp.view_height = vp.height / scale; - } - } - // A viewport scale change alters its anno key in the unified - // resident map; drop everything so the abandoned entry can't - // linger for the rest of the epoch. - self.resident_wire_sets.borrow_mut().clear(); - self.bump_geometry(); + fn target_viewport_handle(&self) -> Option { + if self.current_layout == "Model" { + return None; } + self.active_viewport + .filter(|handle| { + matches!(self.document.get_entity(*handle), Some(EntityType::Viewport(_))) + }) + .or_else(|| { + self.selected.iter().copied().find(|handle| { + matches!(self.document.get_entity(*handle), Some(EntityType::Viewport(_))) + }) + }) + .or_else(|| self.first_viewport_handle()) + } + + fn explicit_viewport_handle(&self) -> Option { + if self.current_layout == "Model" { + return None; + } + self.active_viewport + .filter(|handle| { + matches!(self.document.get_entity(*handle), Some(EntityType::Viewport(_))) + }) + .or_else(|| { + self.selected.iter().copied().find(|handle| { + matches!(self.document.get_entity(*handle), Some(EntityType::Viewport(_))) + }) + }) + } + + fn viewport_scale_handle(&self, viewport: Handle) -> Option { + if let Some(handle) = self.document.viewport_annotation_scale(viewport) { + if matches!(self.document.objects.get(&handle), Some(ObjectType::Scale(_))) { + return Some(handle); + } + } + let EntityType::Viewport(vp) = self.document.get_entity(viewport)? else { + return None; + }; + let factor = vp_effective_scale(vp.custom_scale, vp.view_height, vp.height); + self.document + .objects + .iter() + .filter_map(|(handle, object)| match object { + ObjectType::Scale(scale) if !scale.is_temporary => { + Some((*handle, (scale.factor() - factor).abs())) + } + _ => None, + }) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) + .map(|(handle, _)| handle) + } + + fn viewport_annotation_multiplier(&self, viewport: Handle) -> f32 { + self.viewport_scale_handle(viewport) + .and_then(|handle| match self.document.objects.get(&handle) { + Some(ObjectType::Scale(scale)) => Some(scale.inverse_factor() as f32), + _ => None, + }) + .unwrap_or(self.annotation_scale) + } + + pub fn displayed_annotation_scale_name(&self) -> String { + if self.current_layout == "Model" { + return self.document.header.current_annotation_scale.clone(); + } + self.explicit_viewport_handle() + .and_then(|viewport| self.viewport_scale_handle(viewport)) + .and_then(|handle| match self.document.objects.get(&handle) { + Some(ObjectType::Scale(scale)) => Some(scale.name.clone()), + _ => None, + }) + .or_else(|| { + self.paper_annotation_scale_handle() + .and_then(|handle| match self.document.objects.get(&handle) { + Some(ObjectType::Scale(scale)) => Some(scale.name.clone()), + _ => None, + }) + }) + .unwrap_or_else(|| "1:1".to_string()) + } + + pub fn set_viewport_scale_named(&mut self, name: &str) -> Option { + let scale_handle = self.scale_handle_ensuring(name)?; + let factor = match self.document.objects.get(&scale_handle) { + Some(ObjectType::Scale(scale)) => scale.factor(), + _ => return None, + }; + let viewport = self.explicit_viewport_handle()?; + let locked = matches!( + self.document.get_entity(viewport), + Some(EntityType::Viewport(vp)) if vp.status.locked + ); + if locked || factor <= 1.0e-9 { + return None; + } + if let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(viewport) { + vp.custom_scale = factor; + vp.view_height = vp.height / factor; + } + self.document + .set_viewport_annotation_scale(viewport, scale_handle); + self.resident_wire_sets.borrow_mut().clear(); + self.bump_geometry(); + Some(scale_handle) + } + + pub fn viewport_annotation_scale_synced(&self) -> Option { + let viewport = self.explicit_viewport_handle()?; + let EntityType::Viewport(vp) = self.document.get_entity(viewport)? else { + return None; + }; + let scale = self.viewport_scale_handle(viewport)?; + let ObjectType::Scale(scale) = self.document.objects.get(&scale)? else { + return None; + }; + let effective = vp_effective_scale(vp.custom_scale, vp.view_height, vp.height); + Some((effective - scale.factor()).abs() <= 1.0e-6 * scale.factor().max(1.0)) + } + + pub fn sync_viewport_annotation_scale(&mut self) -> bool { + let Some(viewport) = self.explicit_viewport_handle() else { + return false; + }; + let Some(scale_handle) = self.viewport_scale_handle(viewport) else { + return false; + }; + let factor = match self.document.objects.get(&scale_handle) { + Some(ObjectType::Scale(scale)) => scale.factor(), + _ => return false, + }; + let Some(EntityType::Viewport(vp)) = self.document.get_entity_mut(viewport) else { + return false; + }; + if vp.status.locked || factor <= 1.0e-9 { + return false; + } + vp.custom_scale = factor; + vp.view_height = vp.height / factor; + self.document + .set_viewport_annotation_scale(viewport, scale_handle); + self.resident_wire_sets.borrow_mut().clear(); + self.bump_geometry(); + true } /// Sorted list of layout names: "Model" first, then paper layouts by tab order. @@ -4173,7 +4549,11 @@ impl Scene { let block = self .block_edit_block .unwrap_or_else(|| self.model_space_block_handle()); - self.resident_wires_for(block, None, None) + let scale = crate::scene::annotative::scale_handle_by_name( + &self.document, + &self.document.header.current_annotation_scale, + ); + self.resident_wires_for(block, None, scale, None) } /// Unified static-hold wire builder — the ONE tessellation path every @@ -4219,6 +4599,7 @@ impl Scene { &self, block: Handle, anno_scale_override: Option, + annotation_scale_handle: Option, frozen_layers: Option<&HashSet>, ) -> Arc> { // Normalize an inert anno override away so distinct viewport scales @@ -4235,7 +4616,15 @@ impl Scene { } else { self.paper_bg_color }; - let key = Self::resident_wire_key(block, bg, anno_scale_override, frozen_layers); + let all_visible = self.annotation_all_visible(); + let key = Self::resident_wire_key( + block, + bg, + anno_scale_override, + annotation_scale_handle, + all_visible, + frozen_layers, + ); { let sets = self.resident_wire_sets.borrow(); if let Some(set) = sets.get(&key) { @@ -4250,7 +4639,15 @@ impl Scene { // set. Falls back to the full build below when it can't (no cache entry, // journal un-replayable, or a structural assumption violated). if let Some(arc) = - self.try_resident_patch(key, block, bg, anno_scale_override, frozen_layers) + self.try_resident_patch( + key, + block, + bg, + anno_scale_override, + annotation_scale_handle, + all_visible, + frozen_layers, + ) { return arc; } @@ -4258,7 +4655,15 @@ impl Scene { // (wpp = None) — for every space, exactly like the Model static-hold. let t_tess = iced::time::Instant::now(); let mut wires = - self.wires_for_block_culled(block, None, None, frozen_layers, anno_scale_override); + self.wires_for_block_culled( + block, + None, + None, + frozen_layers, + anno_scale_override, + annotation_scale_handle, + all_visible, + ); // Synthesized nonprint markers (geo-location daisy) live in model space // only and are derived from document objects, not entities — append them // to the freshly built resident set (incremental patches preserve them). @@ -4302,6 +4707,8 @@ impl Scene { block: Handle, bg: [f32; 4], anno_scale_override: Option, + annotation_scale_handle: Option, + all_visible: bool, frozen_layers: Option<&HashSet>, ) -> u64 { let mut key: u64 = 0xcbf2_9ce4_8422_2325; @@ -4314,6 +4721,8 @@ impl Scene { mix(anno_scale_override .map(|scale| scale.to_bits() as u64) .unwrap_or(u64::MAX)); + mix(annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0)); + mix(all_visible as u64); match frozen_layers { Some(frozen) => { let mut signature = 0u64; @@ -4410,6 +4819,8 @@ impl Scene { block: Handle, bg: [f32; 4], anno_scale_override: Option, + annotation_scale_handle: Option, + all_visible: bool, frozen_layers: Option<&HashSet>, ) -> Option>> { let perf = crate::perf::enabled(); @@ -4459,7 +4870,7 @@ impl Scene { } else { 1.0 }; - let blk = self.block_cache_arc(); + let blk = self.block_cache_arc_for(annotation_scale_handle, all_visible); let empty_sel: HashSet = HashSet::default(); let mut new_runs: HashMap> = HashMap::default(); let mut memo_updates: Vec<(Handle, Arc>)> = Vec::new(); @@ -4471,7 +4882,13 @@ impl Scene { let Some(e) = self.document.get_entity(*h) else { continue; }; - if !self.resident_entity_visible(e, block, frozen_layers) { + if !self.resident_entity_visible( + e, + block, + frozen_layers, + annotation_scale_handle, + all_visible, + ) { continue; } visible_changed.insert(*h); @@ -4481,6 +4898,7 @@ impl Scene { self.active_viewport, bg, anno, + annotation_scale_handle, e, Some(&blk), None, @@ -4765,7 +5183,8 @@ impl Scene { } } let layout_block = self.current_layout_block_handle(); - let base = self.resident_wires_for(layout_block, None, None); + let scale = self.paper_annotation_scale_handle(); + let base = self.resident_wires_for(layout_block, None, scale, None); let mut wires = (*base).clone(); // The overall "sheet" viewport now IS the paper view itself, so its own // border rectangle must not be drawn as an entity on the sheet. @@ -5140,7 +5559,17 @@ impl Scene { return arc; } } - let arc = Arc::new(self.synced_hatch_models(target_block, None)); + let scale = if self.current_layout == "Model" { + self.displayed_annotation_scale_handle() + } else { + self.paper_annotation_scale_handle() + }; + let arc = Arc::new(self.synced_hatch_models( + target_block, + None, + scale, + self.annotation_all_visible(), + )); self.hatch_cache.borrow_mut().insert( key, (self.geometry_epoch, sel_sig, Arc::clone(&arc)), @@ -5207,7 +5636,17 @@ impl Scene { return arc; } } - let arc = Arc::new(self.wipeout_models(target_block, None)); + let scale = if self.current_layout == "Model" { + self.displayed_annotation_scale_handle() + } else { + self.paper_annotation_scale_handle() + }; + let arc = Arc::new(self.wipeout_models( + target_block, + None, + scale, + self.annotation_all_visible(), + )); self.wipeout_cache .borrow_mut() .insert(key, (self.geometry_epoch, Arc::clone(&arc))); @@ -5241,7 +5680,17 @@ impl Scene { return arc; } } - let arc = Arc::new(self.image_models(target_block, None)); + let scale = if self.current_layout == "Model" { + self.displayed_annotation_scale_handle() + } else { + self.paper_annotation_scale_handle() + }; + let arc = Arc::new(self.image_models( + target_block, + None, + scale, + self.annotation_all_visible(), + )); self.image_cache .borrow_mut() .insert(target_block, (self.geometry_epoch, Arc::clone(&arc))); @@ -5254,9 +5703,11 @@ impl Scene { &self, target_block: Handle, frozen: Option<&HashSet>, + annotation_scale_handle: Option, + all_visible: bool, ) -> Vec { let depth_map = self.draw_depth_map(); - self.images + let mut models: Vec = self.images .iter() .filter_map(|(handle, model)| { let entity = self.document.get_entity(*handle)?; @@ -5277,7 +5728,139 @@ impl Scene { m.draw_depth = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]); Some(m) }) - .collect() + .collect(); + for source in self.document.entities() { + let contextual = crate::scene::annotative::entity_for_annotation_context( + &self.document, + source, + annotation_scale_handle, + ); + let EntityType::Insert(insert) = contextual.as_ref() else { + continue; + }; + let common = &insert.common; + if common.invisible + || self.entity_temporarily_hidden(common.handle) + || self.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( + common.handle, + common.owner_handle, + target_block, + ) + { + continue; + } + self.collect_block_images( + &insert.block_name, + &insert.get_transform(), + 0, + frozen, + annotation_scale_handle, + all_visible, + &depth_map, + &mut models, + ); + } + models + } + + #[allow(clippy::too_many_arguments)] + fn collect_block_images( + &self, + block_name: &str, + transform: &acadrust::types::Transform, + depth: usize, + frozen: Option<&HashSet>, + annotation_scale_handle: Option, + all_visible: bool, + depth_map: &HashMap, + out: &mut Vec, + ) { + if depth > 32 { + return; + } + let Some(record) = self.document.block_records.get(block_name) else { + return; + }; + for handle in record.entity_handles.iter().copied() { + let Some(source) = self.document.get_entity(handle) else { + continue; + }; + 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.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, + ) + { + continue; + } + if let EntityType::Insert(insert) = entity { + let nested = insert.get_transform().then(transform); + self.collect_block_images( + &insert.block_name, + &nested, + depth + 1, + frozen, + annotation_scale_handle, + all_visible, + depth_map, + out, + ); + continue; + } + let Some(model) = self.images.get(&handle) else { + continue; + }; + let mut placed = model.clone(); + for (corner, low) in placed.corners.iter_mut().zip(&mut placed.corners_low) { + let point = acadrust::types::Vector3::new( + corner[0] as f64 + low[0] as f64, + corner[1] as f64 + low[1] as f64, + corner[2] as f64 + low[2] as f64, + ); + let point = transform.apply(point); + *corner = [point.x as f32, point.y as f32, point.z as f32]; + *low = [ + (point.x - corner[0] as f64) as f32, + (point.y - corner[1] as f64) as f32, + (point.z - corner[2] as f64) as f32, + ]; + } + for vertex in &mut placed.verts { + let point = acadrust::types::Vector3::new( + vertex.pos[0] as f64 + vertex.pos_low[0] as f64, + vertex.pos[1] as f64 + vertex.pos_low[1] as f64, + vertex.pos[2] as f64 + vertex.pos_low[2] as f64, + ); + let point = transform.apply(point); + vertex.pos = [point.x as f32, point.y as f32, point.z as f32]; + vertex.pos_low = [ + (point.x - vertex.pos[0] as f64) as f32, + (point.y - vertex.pos[1] as f64) as f32, + (point.z - vertex.pos[2] as f64) as f32, + ]; + } + placed.draw_depth = depth_map.get(&handle.value()).map_or(0.0, |value| value[0]); + out.push(placed); + } } /// Images owned by the active paper layout block only. The full-canvas @@ -5285,26 +5868,12 @@ impl Scene { /// paper sheet (mirrors `paper_canvas_hatches`). pub(super) fn paper_sheet_images(&self) -> Arc> { let layout_block = self.current_layout_block_handle(); - let depth_map = self.draw_depth_map(); - Arc::new( - self.images - .iter() - .filter_map(|(&handle, model)| { - let entity = self.document.get_entity(handle)?; - let c = entity.common(); - if c.invisible - || self.entity_temporarily_hidden(handle) - || self.layer_hidden(&c.layer) - || !self.belongs_to_visible_block(handle, c.owner_handle, layout_block) - { - return None; - } - let mut m = model.clone(); - m.draw_depth = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]); - Some(m) - }) - .collect(), - ) + Arc::new(self.image_models( + layout_block, + None, + self.paper_annotation_scale_handle(), + self.annotation_all_visible(), + )) } pub(super) fn meshes_arc(&self) -> Arc> { @@ -5343,7 +5912,17 @@ impl Scene { return arc; } } - let arc = Arc::new(self.mesh_models(target_block, None)); + let scale = if self.current_layout == "Model" { + self.displayed_annotation_scale_handle() + } else { + self.paper_annotation_scale_handle() + }; + let arc = Arc::new(self.mesh_models( + target_block, + None, + scale, + self.annotation_all_visible(), + )); self.mesh_cache .borrow_mut() .insert(key, (self.geometry_epoch, Arc::clone(&arc))); @@ -5423,7 +6002,8 @@ impl Scene { } } let frozen: HashSet = frozen.iter().copied().collect(); - let meshes = self.meshes_for_viewport(&frozen); + let viewport = self.active_viewport.unwrap_or(Handle::NULL); + let meshes = self.meshes_for_viewport(viewport, &frozen); *self.interaction_mesh_cache.borrow_mut() = Some((self.geometry_epoch, key, Arc::clone(&meshes))); return meshes; @@ -5434,7 +6014,12 @@ impl Scene { return Arc::clone(meshes); } } - let meshes = Arc::new(self.mesh_models(block, None)); + let meshes = Arc::new(self.mesh_models( + block, + None, + self.displayed_annotation_scale_handle(), + self.annotation_all_visible(), + )); *self.interaction_mesh_cache.borrow_mut() = Some((self.geometry_epoch, key, Arc::clone(&meshes))); meshes @@ -5481,6 +6066,8 @@ impl Scene { &self, target_block: Handle, frozen: Option<&HashSet>, + annotation_scale_handle: Option, + all_visible: bool, ) -> Vec { // Top-level solids: drop those whose layer is off/frozen or that are // flagged invisible / isolated-hidden, mirroring the 2D wire path, plus @@ -5509,7 +6096,12 @@ impl Scene { // block so a block placed at an INSERT scale renders at the right size // (#123) — model space normally, the edited block in a BEDIT editor so // model-space solids don't leak into it (#261). - all.extend(self.instanced_block_meshes(target_block, frozen)); + all.extend(self.instanced_block_meshes( + target_block, + frozen, + annotation_scale_handle, + all_visible, + )); all } @@ -5554,13 +6146,18 @@ impl Scene { /// share the build). No frozen layers → the shared unfiltered set. pub(super) fn hatch_models_for_viewport( &self, + viewport: Handle, frozen: &HashSet, ) -> Arc> { - if frozen.is_empty() { + if viewport.is_null() && frozen.is_empty() { return self.hatch_models_arc(); } let target_block = self.content_render_block_handle(); - let sig = Self::frozen_layers_sig(frozen); + let scale = self.viewport_scale_handle(viewport); + let all_visible = self.annotation_all_visible(); + let context_sig = scale.map_or(0, |handle| handle.value()) + ^ u64::from(all_visible).rotate_left(61); + let sig = Self::frozen_layers_sig(frozen) ^ context_sig; let key = (target_block, self.current_layout.clone(), sig); let sel = self.selected_hatch_sig(); if let Some((e, s, arc)) = self.frozen_hatch_cache.borrow().get(&key) { @@ -5568,7 +6165,12 @@ impl Scene { return Arc::clone(arc); } } - let arc = Arc::new(self.synced_hatch_models(target_block, Some(frozen))); + let arc = Arc::new(self.synced_hatch_models( + target_block, + Some(frozen), + scale, + all_visible, + )); self.frozen_hatch_cache .borrow_mut() .insert(key, (self.geometry_epoch, sel, Arc::clone(&arc))); @@ -5578,20 +6180,30 @@ impl Scene { /// Wipeout fills for a content viewport, with its frozen layers removed. pub(super) fn wipeout_models_for_viewport( &self, + viewport: Handle, frozen: &HashSet, ) -> Arc> { - if frozen.is_empty() { + if viewport.is_null() && frozen.is_empty() { return self.wipeout_models_arc(); } let target_block = self.content_render_block_handle(); - let sig = Self::frozen_layers_sig(frozen); + let scale = self.viewport_scale_handle(viewport); + let all_visible = self.annotation_all_visible(); + let context_sig = scale.map_or(0, |handle| handle.value()) + ^ u64::from(all_visible).rotate_left(61); + let sig = Self::frozen_layers_sig(frozen) ^ context_sig; let key = (target_block, self.current_layout.clone(), sig); if let Some((e, arc)) = self.frozen_wipeout_cache.borrow().get(&key) { if *e == self.geometry_epoch { return Arc::clone(arc); } } - let arc = Arc::new(self.wipeout_models(target_block, Some(frozen))); + let arc = Arc::new(self.wipeout_models( + target_block, + Some(frozen), + scale, + all_visible, + )); self.frozen_wipeout_cache .borrow_mut() .insert(key, (self.geometry_epoch, Arc::clone(&arc))); @@ -5599,19 +6211,32 @@ impl Scene { } /// Image / OLE models for a content viewport, with its frozen layers removed. - pub(super) fn images_for_viewport(&self, frozen: &HashSet) -> Arc> { - if frozen.is_empty() { + pub(super) fn images_for_viewport( + &self, + viewport: Handle, + frozen: &HashSet, + ) -> Arc> { + if viewport.is_null() && frozen.is_empty() { return self.images_arc(); } let target_block = self.content_render_block_handle(); - let sig = Self::frozen_layers_sig(frozen); + let scale = self.viewport_scale_handle(viewport); + let all_visible = self.annotation_all_visible(); + let context_sig = scale.map_or(0, |handle| handle.value()) + ^ u64::from(all_visible).rotate_left(61); + let sig = Self::frozen_layers_sig(frozen) ^ context_sig; let key = (target_block, sig); if let Some((e, arc)) = self.frozen_image_cache.borrow().get(&key) { if *e == self.geometry_epoch { return Arc::clone(arc); } } - let arc = Arc::new(self.image_models(target_block, Some(frozen))); + let arc = Arc::new(self.image_models( + target_block, + Some(frozen), + scale, + all_visible, + )); self.frozen_image_cache .borrow_mut() .insert(key, (self.geometry_epoch, Arc::clone(&arc))); @@ -5619,19 +6244,32 @@ impl Scene { } /// Solid meshes for a content viewport, with its frozen layers removed. - pub(super) fn meshes_for_viewport(&self, frozen: &HashSet) -> Arc> { - if frozen.is_empty() { + pub(super) fn meshes_for_viewport( + &self, + viewport: Handle, + frozen: &HashSet, + ) -> Arc> { + if viewport.is_null() && frozen.is_empty() { return self.meshes_arc(); } let target_block = self.content_render_block_handle(); - let sig = Self::frozen_layers_sig(frozen); + let scale = self.viewport_scale_handle(viewport); + let all_visible = self.annotation_all_visible(); + let context_sig = scale.map_or(0, |handle| handle.value()) + ^ u64::from(all_visible).rotate_left(61); + let sig = Self::frozen_layers_sig(frozen) ^ context_sig; let key = (target_block, self.current_layout.clone(), sig); if let Some((e, arc)) = self.frozen_mesh_cache.borrow().get(&key) { if *e == self.geometry_epoch { return Arc::clone(arc); } } - let arc = Arc::new(self.mesh_models(target_block, Some(frozen))); + let arc = Arc::new(self.mesh_models( + target_block, + Some(frozen), + scale, + all_visible, + )); self.frozen_mesh_cache .borrow_mut() .insert(key, (self.geometry_epoch, Arc::clone(&arc))); @@ -5708,14 +6346,19 @@ impl Scene { &self, layout_block: Handle, frozen: Option<&HashSet>, + annotation_scale_handle: Option, + all_visible: bool, ) -> Vec { if self.block_meshes.is_empty() { return Vec::new(); } let mut out = Vec::new(); for source in self.document.entities() { - 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 e = contextual.as_ref(); if e.common().owner_handle != layout_block { continue; @@ -5726,6 +6369,12 @@ impl Scene { // frozen only in the requesting content viewport. if !self.mesh_entity_visible(ins.common.handle) || self.layer_frozen_in(&ins.common.layer, frozen) + || crate::scene::annotative::annotative_offscale_for( + &self.document, + &ins.common, + annotation_scale_handle, + all_visible, + ) { continue; } @@ -5769,6 +6418,9 @@ impl Scene { 0, Some(inherit), &mut out, + frozen, + annotation_scale_handle, + all_visible, ); // Tag the instanced meshes with the parent INSERT handle so the // hover / selection highlight (keyed on the mesh name) tints the @@ -5796,6 +6448,9 @@ impl Scene { // colour and AcDbMaterial inheritance for ByBlock/layer-0 children. inherit: Option, out: &mut Vec, + frozen: Option<&HashSet>, + annotation_scale_handle: Option, + all_visible: bool, ) { if depth > 16 { return; @@ -5808,12 +6463,25 @@ impl Scene { let Some(source) = self.document.get_entity(h) 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 e = contextual.as_ref(); // A block-internal solid / nested INSERT on an off/frozen layer // (or flagged invisible) must not render, same as a top-level one. - if !self.mesh_definition_entity_visible(h) { + if !self.mesh_definition_entity_visible(h) + || self.layer_frozen_in(&e.common().layer, frozen) + { + continue; + } + if crate::scene::annotative::annotative_offscale_for( + &self.document, + e.common(), + annotation_scale_handle, + all_visible, + ) { continue; } if let EntityType::Insert(ins) = e { @@ -5821,7 +6489,16 @@ impl Scene { let child = inherit .as_ref() .map(|parent| self.chain_mesh_inherit(ins, parent)); - self.expand_block_meshes(&ins.block_name, &composed, depth + 1, child, out); + self.expand_block_meshes( + &ins.block_name, + &composed, + depth + 1, + child, + out, + frozen, + annotation_scale_handle, + all_visible, + ); } else if let Some(set) = self.block_meshes.get(&h) { // The solid's own transparency (baked into the cached colour). let own_alpha = set.lods.first().map(|m| m.color[3]).unwrap_or(1.0); @@ -6038,6 +6715,12 @@ impl Scene { || self.entity_temporarily_hidden(handle) || self.layer_hidden(&common.layer) || self.interaction_layer_frozen(&common.layer) + || crate::scene::annotative::annotative_offscale_for( + &self.document, + common, + self.displayed_annotation_scale_handle(), + self.annotation_all_visible(), + ) { return false; } @@ -6171,15 +6854,26 @@ impl Scene { // only blocks). The hatch-presence test is memoised across inserts. let mut hatch_memo: std::collections::HashMap = std::collections::HashMap::new(); + let annotation_scale_handle = self.displayed_annotation_scale_handle(); + let all_visible = self.annotation_all_visible(); 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; }; if ins.common.invisible || self.entity_temporarily_hidden(ins.common.handle) || layer_hidden(&ins.common.layer) + || crate::scene::annotative::annotative_offscale_for( + &self.document, + &ins.common, + annotation_scale_handle, + all_visible, + ) { continue; } @@ -6197,9 +6891,10 @@ impl Scene { .explode_from_document(&self.document) .into_iter() .map(|sub| { - crate::scene::annotative::entity_for_active_context( + crate::scene::annotative::entity_for_annotation_context( &self.document, &sub, + annotation_scale_handle, ) .into_owned() }) @@ -7319,6 +8014,8 @@ impl Scene { e: &EntityType, block_handle: Handle, frozen_layers: Option<&HashSet>, + annotation_scale_handle: Option, + all_visible: bool, ) -> bool { let c = e.common(); if c.invisible { @@ -7348,14 +8045,12 @@ impl Scene { } } } - // Annotative scale representation: draw only the current scale's copy. - // Model-space only (`frozen_layers` is None): a paper-space viewport - // renders at its own annotation scale and already hides the off-scale - // representations through its per-viewport frozen "0 @ " layers, - // so applying the model-space scale here would fight that. - if frozen_layers.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, + ) { return false; } self.belongs_to_visible_block(c.handle, c.owner_handle, block_handle) @@ -7375,6 +8070,8 @@ impl Scene { // sheet paths use `self.annotation_scale` / 1.0 respectively. // `None` selects the default branch on `current_layout`. anno_scale_override: Option, + annotation_scale_handle: Option, + all_visible: bool, ) -> Vec { use acadrust::objects::ObjectType; @@ -7409,8 +8106,15 @@ impl Scene { // Visibility test reused by both paths below — and by the resident // incremental patch, so a changed entity is included/excluded exactly as // a from-scratch build would (no divergence). - let visibility_ok = - |e: &EntityType| self.resident_entity_visible(e, block_handle, frozen_layers); + let visibility_ok = |e: &EntityType| { + self.resident_entity_visible( + e, + block_handle, + frozen_layers, + annotation_scale_handle, + all_visible, + ) + }; // Phase 2.1 — quadtree-driven candidate selection. When a view // AABB exists (Model layout with a settled camera), only iterate @@ -7494,7 +8198,7 @@ impl Scene { // Content shown inside a paper-space viewport carries a scale override; // only there does PSLTSCALE resize linetypes by the viewport scale. let paper = anno_scale_override.is_some(); - let blk_cache = self.block_cache_arc(); + let blk_cache = self.block_cache_arc_for(annotation_scale_handle, all_visible); let blk_ref: &cache::block_cache::BlockCache = &blk_cache; // Zoom-adaptive curve sampling for top-level Edge tessellation. Target // ~0.5 px chord height — far-out arcs that used to emit hundreds of @@ -7553,6 +8257,8 @@ impl Scene { } } mix(anno.to_bits() as u64); + mix(annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0)); + mix(all_visible as u64); for c in bg { mix(c.to_bits() as u64); } @@ -7601,6 +8307,7 @@ impl Scene { avp, bg, anno, + annotation_scale_handle, e, Some(blk_ref), view_aabb, @@ -7629,6 +8336,7 @@ impl Scene { avp, bg, anno, + annotation_scale_handle, e, Some(blk_ref), view_aabb, @@ -7766,6 +8474,12 @@ impl Scene { .map(move |handle| (handle, record.handle)) }) .collect(); + let text_style_names: HashMap = self + .document + .text_styles + .iter() + .map(|style| (style.handle, style.name.clone())) + .collect(); let mut roots: HashMap> = HashMap::default(); let mut parents: HashMap> = HashMap::default(); @@ -7888,13 +8602,92 @@ impl Scene { } } EntityType::Dimension(dimension) => { - add(&mut index.dim_styles, &dimension.base().style_name) + add(&mut index.dim_styles, &dimension.base().style_name); + if let Some(style) = self + .document + .dim_styles + .get(&dimension.base().style_name) + { + add(&mut index.text_styles, &style.dimtxsty); + } + } + EntityType::Leader(leader) => { + add(&mut index.dim_styles, &leader.dimension_style); + if let Some(style) = self.document.dim_styles.get(&leader.dimension_style) { + add(&mut index.text_styles, &style.dimtxsty); + } + } + EntityType::Tolerance(tolerance) => { + add(&mut index.dim_styles, &tolerance.dimension_style_name); + if let Some(style) = self + .document + .dim_styles + .get(&tolerance.dimension_style_name) + { + add(&mut index.text_styles, &style.dimtxsty); + } } EntityType::Table(table) => { - add_handle(&mut index.object_styles, table.table_style_handle) + add_handle(&mut index.object_styles, table.table_style_handle); + if let Some(ObjectType::TableStyle(style)) = table + .table_style_handle + .and_then(|handle| self.document.objects.get(&handle)) + { + for row in [ + &style.data_row_style, + &style.header_row_style, + &style.title_row_style, + ] { + add(&mut index.text_styles, &row.text_style_name); + } + } + for row in &table.rows { + if let Some(style) = &row.style { + if let Some(name) = style + .text_style_handle + .and_then(|handle| text_style_names.get(&handle)) + { + add(&mut index.text_styles, name); + } + } + for cell in &row.cells { + if let Some(style) = &cell.style { + if let Some(name) = style + .text_style_handle + .and_then(|handle| text_style_names.get(&handle)) + { + add(&mut index.text_styles, name); + } + } + for content in &cell.contents { + if let Some(name) = content + .text_style_handle + .and_then(|handle| text_style_names.get(&handle)) + { + add(&mut index.text_styles, name); + } + } + } + } } EntityType::MultiLeader(leader) => { - add_handle(&mut index.object_styles, leader.style_handle) + add_handle(&mut index.object_styles, leader.style_handle); + for handle in [leader.text_style_handle, leader.context.text_style_handle] { + if let Some(name) = handle.and_then(|handle| text_style_names.get(&handle)) { + add(&mut index.text_styles, name); + } + } + if let Some(ObjectType::MultiLeaderStyle(style)) = leader + .style_handle + .and_then(|handle| self.document.objects.get(&handle)) + { + if let Some(name) = style + .text_style_handle + .and_then(|handle| text_style_names.get(&handle)) + { + add(&mut index.text_styles, name); + } + } } EntityType::MLine(line) => add_handle(&mut index.object_styles, line.style_handle), _ => {} @@ -8164,10 +8957,25 @@ impl Scene { }; let anno = if self.current_layout == "Model" { self.annotation_scale + } else if let Some(viewport) = self.active_viewport { + self.viewport_annotation_multiplier(viewport) } else { 1.0 }; - let blk_cache = self.block_cache_arc(); + let annotation_scale_handle = if self.current_layout == "Model" { + crate::scene::annotative::scale_handle_by_name( + &self.document, + &self.document.header.current_annotation_scale, + ) + } else if let Some(viewport) = self.active_viewport { + self.viewport_scale_handle(viewport) + } else { + self.paper_annotation_scale_handle() + }; + let blk_cache = self.block_cache_arc_for( + annotation_scale_handle, + self.annotation_all_visible(), + ); // tessellate_one is used for one-off lookups (hit test, properties). // Skip culling here so the caller always gets the full geometry. tessellate_entity( @@ -8176,6 +8984,7 @@ impl Scene { self.active_viewport, bg, anno, + annotation_scale_handle, e, Some(&blk_cache), None, @@ -8232,7 +9041,23 @@ impl Scene { return None; } let block = self.current_layout_block_handle(); - let wires = self.wires_for_block_culled(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 wires = self.wires_for_block_culled( + block, + None, + None, + None, + None, + scale, + self.annotation_all_visible(), + ); let mut min = glam::DVec2::splat(f64::INFINITY); let mut max = glam::DVec2::splat(f64::NEG_INFINITY); let mut any = false; diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 2521300b..1e0da58b 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -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; diff --git a/src/scene/paper.rs b/src/scene/paper.rs index 72838338..111b36f1 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -385,12 +385,17 @@ impl Scene { .unwrap_or(false) }; let mut models: Vec = 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>, + annotation_scale_handle: Option, + all_visible: bool, ) -> Vec { 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>, + annotation_scale_handle: Option, + all_visible: bool, + highlight_selection: bool, ) -> Vec { 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> { 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 = 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), ) } diff --git a/src/scene/project.rs b/src/scene/project.rs index ef9e2a3b..f4ab774d 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -430,7 +430,12 @@ impl Scene { let frozen: rustc_hash::FxHashSet = 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) { diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index b66ca78b..d61186f5 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -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 diff --git a/src/ui/popup/scale_popup.rs b/src/ui/popup/scale_popup.rs index 33375ae4..b1a74d28 100644 --- a/src/ui/popup/scale_popup.rs +++ b/src/ui/popup/scale_popup.rs @@ -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, file_scales: Vec<(String, f32, f64)>, ) -> Vec> { let mut entries: Vec> = 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)) }) diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs index 0e2cd4d5..ec4cd578 100644 --- a/src/ui/statusbar/mod.rs +++ b/src/ui/statusbar/mod.rs @@ -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, 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, @@ -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, // 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, + ¤t_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( diff --git a/src/ui/statusbar/statusbar_config.rs b/src/ui/statusbar/statusbar_config.rs index a7ecb384..84dee3ca 100644 --- a/src/ui/statusbar/statusbar_config.rs +++ b/src/ui/statusbar/statusbar_config.rs @@ -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", diff --git a/src/ui/style/dimstyle.rs b/src/ui/style/dimstyle.rs index 31f9e5b2..f046c483 100644 --- a/src/ui/style/dimstyle.rs +++ b/src/ui/style/dimstyle.rs @@ -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), diff --git a/src/ui/style/mleaderstyle.rs b/src/ui/style/mleaderstyle.rs index 354eb047..a984b444 100644 --- a/src/ui/style/mleaderstyle.rs +++ b/src/ui/style/mleaderstyle.rs @@ -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:", diff --git a/src/ui/style/tablestyle.rs b/src/ui/style/tablestyle.rs index ebf4406e..3e089d2f 100644 --- a/src/ui/style/tablestyle.rs +++ b/src/ui/style/tablestyle.rs @@ -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) diff --git a/src/ui/style/textstyle.rs b/src/ui/style/textstyle.rs index 823ab571..89dd526d 100644 --- a/src/ui/style/textstyle.rs +++ b/src/ui/style/textstyle.rs @@ -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![