diff --git a/src/app/properties.rs b/src/app/properties.rs index e17d73fb..5a63b66c 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -95,95 +95,177 @@ impl OpenCADStudio { let mut sections = dispatch::properties_sectioned(handle, entity, &text_style_names); - // Resolve a custom material handle (material_flags == 3) to - // its name in the 3D Visualization group. + // Turn the Material row into an editable picker: the source + // options (ByLayer / ByBlock) plus every named material the + // drawing defines. A custom flag (3) shows the stored handle's + // material name as the selection. { + let doc = &self.tabs[i].scene.document; let common = entity.common(); - if common.material_flags == 3 { - if let Some(mh) = common.material_handle { - if let Some(name) = self.tabs[i] - .scene - .document - .objects - .iter() - .find_map(|(h, o)| match o { + let mat_names: Vec = doc + .objects + .iter() + .filter_map(|(_, o)| match o { + acadrust::objects::ObjectType::Material(m) => Some(m.name.clone()), + _ => None, + }) + .collect(); + let selected = match common.material_flags { + 0 => "ByLayer".to_string(), + 1 => "ByBlock".to_string(), + _ => common + .material_handle + .and_then(|mh| { + doc.objects.iter().find_map(|(h, o)| match o { acadrust::objects::ObjectType::Material(m) if *h == mh => { Some(m.name.clone()) } _ => None, }) + }) + .unwrap_or_else(|| "ByLayer".to_string()), + }; + let mut options = vec!["ByLayer".to_string(), "ByBlock".to_string()]; + options.extend(mat_names); + for section in sections.iter_mut() { + if let Some(row) = + section.props.iter_mut().find(|p| p.field == "material") + { + row.value = crate::scene::model::object::PropValue::Choice { + selected: selected.clone(), + options: options.clone(), + }; + } + } + } + + // In named plot-style mode (PSTYLEMODE=1) the Plot style row + // picks a named style from the drawing's plot style table. In + // the color-dependent mode it stays read-only (the object's + // color drives the plot style), so it is left untouched. + if self.tabs[i].scene.document.header.plotstyle_mode { + let doc = &self.tabs[i].scene.document; + let dict_h = doc.header.acad_plotstylename_dict_handle; + if let Some(dict) = crate::scene::annotative::as_dict(doc, dict_h) { + let common = entity.common(); + let mut options = vec!["ByLayer".to_string(), "ByBlock".to_string()]; + options.extend(dict.entries.iter().map(|(n, _)| n.clone())); + let selected = match common.plotstyle_flags { + 0 => "ByLayer".to_string(), + 1 => "ByBlock".to_string(), + _ => common + .plotstyle_handle + .and_then(|ph| { + dict.entries + .iter() + .find(|(_, h)| *h == ph) + .map(|(n, _)| n.clone()) + }) + .unwrap_or_else(|| "ByLayer".to_string()), + }; + for section in sections.iter_mut() { + if let Some(row) = + section.props.iter_mut().find(|p| p.field == "plot_style") { - for section in sections.iter_mut() { - if let Some(row) = - section.props.iter_mut().find(|p| p.field == "material") - { - row.value = - crate::scene::model::object::PropValue::ReadOnly( - name.clone(), - ); - } - } + row.value = crate::scene::model::object::PropValue::Choice { + selected: selected.clone(), + options: options.clone(), + }; } } } } - // Resolve MLEADER handle-backed rows (multileader style, text - // style, arrowhead block, leader linetype) to display names. + // Turn the MLEADER handle-backed rows (multileader style, text + // style, arrowhead block, leader linetype) into editable name + // pickers. The current handle resolves to a display name; the + // options list every candidate the drawing offers. Applying a + // pick resolves the name back to a handle in the update loop. if let acadrust::EntityType::MultiLeader(ml) = entity { let doc = &self.tabs[i].scene.document; - let mut set_named = |field: &str, name: String| { - for section in sections.iter_mut() { - if let Some(row) = - section.props.iter_mut().find(|p| p.field == field) - { - row.value = - crate::scene::model::object::PropValue::ReadOnly( - name.clone(), - ); - } - } - }; - if let Some(h) = ml.style_handle { - if let Some(name) = doc.objects.iter().find_map(|(oh, o)| match o { - acadrust::objects::ObjectType::MultiLeaderStyle(s) if *oh == h => { + // Option lists. + let mleader_styles: Vec = doc + .objects + .iter() + .filter_map(|(_, o)| match o { + acadrust::objects::ObjectType::MultiLeaderStyle(s) => { Some(s.name.clone()) } _ => None, - }) { - set_named("mleader_style", name); - } - } - if let Some(h) = ml.text_style_handle { - if let Some(name) = doc - .text_styles - .iter() - .find(|s| s.handle == h) - .map(|s| s.name.clone()) - { - set_named("text_style_handle", name); - } - } - if let Some(h) = ml.arrowhead_handle { - if let Some(name) = doc - .block_records - .iter() - .find(|b| b.handle == h) - .map(|b| b.name.clone()) - { - set_named("arrowhead_handle", name); - } - } - if let Some(h) = ml.line_type_handle { - if let Some(name) = doc - .line_types - .iter() - .find(|l| l.handle == h) - .map(|l| l.name.clone()) - { - set_named("line_type_handle", name); - } - } + }) + .collect(); + // A leader with no linetype handle draws ByBlock; expose that + // as the first option so the default is selectable. + let ltype_names: Vec = std::iter::once("ByBlock".to_string()) + .chain( + doc.line_types + .iter() + .map(|l| l.name.clone()) + .filter(|n| !n.is_empty()), + ) + .collect(); + // Arrowheads are blocks; the closed-filled default has no + // block, so seed the list with it and add the arrowhead + // blocks (leading underscore) present in the drawing. + let arrow_names: Vec = std::iter::once("Closed filled".to_string()) + .chain( + doc.block_records + .iter() + .filter(|b| b.name.starts_with('_')) + .map(|b| b.name.clone()), + ) + .collect(); + let tstyle_names = text_style_names.clone(); + // Currently selected names. + let cur_style = ml + .style_handle + .and_then(|h| { + doc.objects.iter().find_map(|(oh, o)| match o { + acadrust::objects::ObjectType::MultiLeaderStyle(s) + if *oh == h => + { + Some(s.name.clone()) + } + _ => None, + }) + }) + .unwrap_or_else(|| "Standard".to_string()); + let cur_tstyle = ml + .text_style_handle + .and_then(|h| { + doc.text_styles.iter().find(|s| s.handle == h).map(|s| s.name.clone()) + }) + .unwrap_or_else(|| "Standard".to_string()); + let cur_arrow = ml + .arrowhead_handle + .and_then(|h| { + doc.block_records.iter().find(|b| b.handle == h).map(|b| b.name.clone()) + }) + .unwrap_or_else(|| "Closed filled".to_string()); + let cur_ltype = ml + .line_type_handle + .and_then(|h| { + doc.line_types.iter().find(|l| l.handle == h).map(|l| l.name.clone()) + }) + .unwrap_or_else(|| "ByBlock".to_string()); + let mut set_choice = + |field: &str, selected: String, options: Vec| { + for section in sections.iter_mut() { + if let Some(row) = + section.props.iter_mut().find(|p| p.field == field) + { + row.value = + crate::scene::model::object::PropValue::Choice { + selected: selected.clone(), + options: options.clone(), + }; + } + } + }; + set_choice("mleader_style", cur_style, mleader_styles); + set_choice("text_style_handle", cur_tstyle, tstyle_names); + set_choice("arrowhead_handle", cur_arrow, arrow_names); + set_choice("line_type_handle", cur_ltype, ltype_names); } // Inject viewport-only properties that require doc access. @@ -487,27 +569,42 @@ impl OpenCADStudio { // are walked from the entity's extension dictionary — both // need the document, so they are resolved here. { - let anno = match entity { - acadrust::EntityType::Text(t) => { - Some((text_style_annotative(doc, &t.style), "annotative")) + // Which entities show an Annotative row, the field it uses, + // and — for those that don't already carry the row + // (dimension / table) — the existing field to insert it + // after. MLeader uses its editable toggle field. + let anno: Option<(&str, Option<&str>)> = match entity { + acadrust::EntityType::Text(_) + | acadrust::EntityType::MText(_) + | acadrust::EntityType::Leader(_) => Some(("annotative", None)), + acadrust::EntityType::MultiLeader(_) => { + Some(("enable_annotation_scale", None)) } - acadrust::EntityType::MText(t) => Some(( - t.is_annotative || text_style_annotative(doc, &t.style), - "annotative", - )), - acadrust::EntityType::Leader(l) => { - Some((dim_style_annotative(doc, &l.dimension_style), "annotative")) + acadrust::EntityType::Dimension(_) => { + Some(("annotative", Some("style_name"))) + } + acadrust::EntityType::Table(_) => { + Some(("annotative", Some("tbl_style_handle"))) } - acadrust::EntityType::MultiLeader(ml) => Some(( - ml.enable_annotation_scale - || mleader_style_annotative(doc, ml.style_handle), - "enable_annotation_scale", - )), _ => None, }; - if let Some((is_anno, anno_field)) = anno { - // MLeader keeps its editable toggle; the read-only - // text rows get an explicit Yes/No. + if let Some((anno_field, insert_after)) = anno { + let is_anno = crate::scene::annotative::is_annotative(doc, entity); + // Dimensions/tables carry no Annotative row yet — add one + // right after their style row. + if let Some(anchor) = insert_after { + insert_row_after( + &mut sections, + anchor, + crate::entities::common::ro_prop( + "Annotative", + "annotative", + "No", + ), + ); + } + // The read-only text rows get an explicit Yes/No; MLeader + // keeps its editable toggle. if anno_field == "annotative" { set_row( &mut sections, @@ -516,19 +613,16 @@ impl OpenCADStudio { ); } if is_anno { - let names = entity_scale_names(doc, entity.common()); - let display = if names.is_empty() { - doc.header.current_annotation_scale.clone() - } else { - names.join(", ") - }; + // The applied annotation scale follows the current + // annotation scale (CANNOSCALE / the status-bar + // scale pill), not a per-object stored value. insert_row_after( &mut sections, anno_field, crate::entities::common::ro_prop( "Annotative scale", "annotative_scale", - display, + doc.header.current_annotation_scale.clone(), ), ); } @@ -1243,93 +1337,6 @@ fn find_dim_style<'a>( }) } -/// Is the named text style annotative? -fn text_style_annotative(doc: &acadrust::CadDocument, name: &str) -> bool { - doc.text_styles - .iter() - .find(|s| { - s.name.eq_ignore_ascii_case(name) - || (name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard")) - }) - .map_or(false, |s| s.annotative) -} - -/// Is the named dimension style annotative? -fn dim_style_annotative(doc: &acadrust::CadDocument, name: &str) -> bool { - find_dim_style(doc, name).map_or(false, |s| s.annotative) -} - -/// Is the multileader style (by handle) annotative? -fn mleader_style_annotative(doc: &acadrust::CadDocument, handle: Option) -> bool { - let Some(h) = handle else { - return false; - }; - doc.objects.iter().any(|(oh, o)| { - matches!(o, acadrust::objects::ObjectType::MultiLeaderStyle(s) if *oh == h && s.is_annotative) - }) -} - -/// Resolve a handle to a `Dictionary` object, if it is one. -fn as_dict( - doc: &acadrust::CadDocument, - handle: acadrust::Handle, -) -> Option<&acadrust::objects::Dictionary> { - match doc.objects.get(&handle) { - Some(acadrust::objects::ObjectType::Dictionary(d)) => Some(d), - _ => None, - } -} - -/// Case-insensitive dictionary entry lookup by key. -fn dict_get(dict: &acadrust::objects::Dictionary, key: &str) -> Option { - dict.entries - .iter() - .find(|(k, _)| k.eq_ignore_ascii_case(key)) - .map(|(_, h)| *h) -} - -/// The AcDbScale object's ratio name (e.g. "1:50"), by handle. -fn scale_name(doc: &acadrust::CadDocument, handle: acadrust::Handle) -> Option { - match doc.objects.get(&handle) { - Some(acadrust::objects::ObjectType::Scale(s)) => Some(s.name.clone()), - _ => None, - } -} - -/// The annotation-scale name(s) assigned to an entity, walked from its extension -/// dictionary → AcDbContextDataManager → ACDB_ANNOTATIONSCALES. -/// -/// The collection's entry keys are internal anonymous names (e.g. "*A1"); the -/// real scale is each leaf's group-340 SCALE handle, which acadrust surfaces via -/// `doc.context_scales` (context-leaf handle → AcDbScale handle). We resolve each -/// leaf handle to its scale, then the scale to its ratio name. Empty when the -/// entity has no per-scale context (it then uses the drawing's current scale). -fn entity_scale_names( - doc: &acadrust::CadDocument, - common: &acadrust::entities::EntityCommon, -) -> Vec { - let Some(xd) = common.xdictionary_handle else { - return Vec::new(); - }; - let Some(coll) = as_dict(doc, xd) - .and_then(|d| dict_get(d, "AcDbContextDataManager")) - .and_then(|h| as_dict(doc, h)) - .and_then(|m| dict_get(m, "ACDB_ANNOTATIONSCALES")) - .and_then(|h| as_dict(doc, h)) - else { - return Vec::new(); - }; - let mut names: Vec = Vec::new(); - for (_, leaf) in &coll.entries { - if let Some(name) = doc.context_scales.get(leaf).and_then(|sh| scale_name(doc, *sh)) { - if !names.contains(&name) { - names.push(name); - } - } - } - names -} - /// Insert `row` immediately after the first property whose field matches. fn insert_row_after( sections: &mut [crate::scene::model::object::PropSection], diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 87c85254..6286b36b 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1243,6 +1243,158 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.tabs[i].scene.camera_generation += 1; } + } else if matches!( + field, + "mleader_style" + | "text_style_handle" + | "arrowhead_handle" + | "line_type_handle" + ) { + // Resolve a picked name back to the handle the MLEADER + // stores. The style/text-style rows keep their existing + // handle on a failed lookup; the arrowhead/linetype rows + // take the resolved value directly (None = the default + // "Closed filled" / "ByBlock" option). + let doc = &self.tabs[i].scene.document; + 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, + }), + "text_style_handle" => doc + .text_styles + .iter() + .find(|s| s.name == value) + .map(|s| s.handle), + "arrowhead_handle" => { + if value == "Closed filled" { + None + } else { + doc.block_records + .iter() + .find(|b| b.name == value) + .map(|b| b.handle) + } + } + "line_type_handle" => { + if value == "ByBlock" { + None + } else { + doc.line_types + .iter() + .find(|l| l.name == value) + .map(|l| l.handle) + } + } + _ => None, + }; + for &handle in &handles { + if self.tabs[i].scene.is_layer_locked(handle) { + continue; + } + 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); + } + } + "arrowhead_handle" => ml.arrowhead_handle = resolved, + "line_type_handle" => ml.line_type_handle = resolved, + _ => {} + } + } + } + } else if field == "plot_style" { + // Named plot-style pick: ByLayer / ByBlock clear the + // handle; a named style resolves through the drawing's + // ACAD_PLOTSTYLENAME dictionary to its placeholder handle. + let dict_h = + self.tabs[i].scene.document.header.acad_plotstylename_dict_handle; + let ph: Option = + crate::scene::annotative::as_dict(&self.tabs[i].scene.document, dict_h) + .and_then(|d| { + d.entries + .iter() + .find(|(n, _)| *n == value) + .map(|(_, h)| *h) + }); + for &handle in &handles { + if self.tabs[i].scene.is_layer_locked(handle) { + continue; + } + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) + { + let common = entity.common_mut(); + match value.as_str() { + "ByLayer" => { + common.plotstyle_flags = 0; + common.plotstyle_handle = None; + } + "ByBlock" => { + common.plotstyle_flags = 1; + common.plotstyle_handle = None; + } + _ => { + if let Some(h) = ph { + common.plotstyle_flags = 3; + common.plotstyle_handle = Some(h); + } + } + } + } + } + } else if field == "material" { + // Material source: ByLayer / ByBlock clear the handle; a + // named material sets flag 3 + its handle (resolved here + // because the update loop holds the document). + let mat_handle: Option = self.tabs[i] + .scene + .document + .objects + .iter() + .find_map(|(h, o)| match o { + acadrust::objects::ObjectType::Material(m) if m.name == value => { + Some(*h) + } + _ => None, + }); + for &handle in &handles { + if self.tabs[i].scene.is_layer_locked(handle) { + continue; + } + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) + { + let common = entity.common_mut(); + match value.as_str() { + "ByLayer" => { + common.material_flags = 0; + common.material_handle = None; + } + "ByBlock" => { + common.material_flags = 1; + common.material_handle = None; + } + _ => { + if let Some(h) = mat_handle { + common.material_flags = 3; + common.material_handle = Some(h); + } + } + } + } + } } else { for &handle in &handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 1a2ff04f..2910cc75 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -37,7 +37,7 @@ fn properties(ins: &Insert) -> Vec { }, }) .collect(); - vec![ + let mut sections = vec![ PropSection { title: "Geometry".into(), props: vec![ @@ -54,24 +54,24 @@ fn properties(ins: &Insert) -> Vec { props: vec![ ro("Name", "block", ins.block_name.clone()), edit("Rotation", "rotation", ins.rotation.to_degrees()), - ro("Block Unit", "block_unit", String::new()), - ro("Unit factor", "unit_factor", String::new()), ro( "Annotative", "annotative", if annotative { "Yes" } else { "No" }, ), + ro("Block Unit", "block_unit", String::new()), + ro("Unit factor", "unit_factor", String::new()), ], }, - PropSection { + ]; + // The Attributes group appears only when the block actually has attributes. + if !attrs.is_empty() { + sections.push(PropSection { title: "Attributes".into(), props: attrs, - }, - PropSection { - title: "Custom".into(), - props: Vec::new(), - }, - ] + }); + } + sections } fn apply_geom_prop(ins: &mut Insert, field: &str, value: &str) { @@ -206,12 +206,39 @@ pub(crate) fn append_insert_attribute_wires( if attmode == 0 { return; } + // An annotative block scales as ONE uniform unit about its insertion point; + // its attributes are carried by that scale (AutoCAD even forbids annotative + // attributes inside annotative blocks, to avoid double-scaling). So pre-scale + // the annotative attribute about the insertion point and tessellate it at a + // neutral 1.0 — the text path must never scale it a second time. + let annotative = ins + .common + .extended_data + .get_record("AcAnnotativeData") + .is_some(); + let block_scale = if annotative { anno_scale as f64 } else { 1.0 }; + let ip = ins.insert_point; + let scale_about = |q: acadrust::types::Vector3| { + acadrust::types::Vector3::new( + ip.x + (q.x - ip.x) * block_scale, + ip.y + (q.y - ip.y) * block_scale, + ip.z + (q.z - ip.z) * block_scale, + ) + }; for attr in &ins.attributes { let per_attr_hidden = attr.common.invisible || attr.flags.invisible; if attmode == 1 && per_attr_hidden { continue; } - let attr_entity = EntityType::AttributeEntity(attr.clone()); + let attr_entity = EntityType::AttributeEntity({ + let mut a = attr.clone(); + if (block_scale - 1.0).abs() > 1e-6 { + a.height *= block_scale; + a.insertion_point = scale_about(a.insertion_point); + a.alignment_point = scale_about(a.alignment_point); + } + a + }); let (sub_color, sub_plen, sub_pat, sub_lw_px, sub_aci) = render::render_style_for_block_sub( document, &attr_entity, @@ -237,7 +264,9 @@ pub(crate) fn append_insert_attribute_wires( sub_plen * pslt_factor, sub_pat.map(|v| v * pslt_factor), sub_lw_px, - anno_scale, + // Neutral: the attribute is already scaled as part of the block unit + // above, so the text path must not scale it a second time. + 1.0, None, bg_color, false, diff --git a/src/entities/table.rs b/src/entities/table.rs index 63856eb6..a527fdb7 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -367,6 +367,10 @@ pub fn tessellate_table( selected: bool, entity_color: [f32; 4], line_weight_px: f32, + // Annotation scale: multiplies the table's paper-size geometry so an + // annotative table renders at the current annotation scale. 1.0 for a + // non-annotative table (its geometry is already at model size). + anno_scale: f32, ) -> Vec { use crate::scene::convert::tess_util::aci_to_rgba; use crate::scene::model::wire_model::WireModel; @@ -431,7 +435,7 @@ pub fn tessellate_table( let mut off = 0.0f32; let mut v = vec![0.0f32]; for col in &tab.columns { - off += col.width as f32; + off += col.width as f32 * anno_scale; v.push(off); } v @@ -440,7 +444,7 @@ pub fn tessellate_table( let mut off = 0.0f32; let mut v = vec![0.0f32]; for row in &tab.rows { - off += row.height as f32; + off += row.height as f32 * anno_scale; v.push(off); } v @@ -450,8 +454,8 @@ pub fn tessellate_table( let header_suppressed = table_style.map(|t| t.header_suppressed).unwrap_or(false); let h_margin = table_style .map(|t| t.horizontal_margin as f32) - .unwrap_or(0.0); - let v_margin = table_style.map(|t| t.vertical_margin as f32).unwrap_or(0.0); + .unwrap_or(0.0) * anno_scale; + let v_margin = table_style.map(|t| t.vertical_margin as f32).unwrap_or(0.0) * anno_scale; let lookup_style = |hh: acadrust::Handle| -> Option<&acadrust::tables::TextStyle> { document.text_styles.iter().find(|s| s.handle == hh) @@ -526,7 +530,7 @@ pub fn tessellate_table( let row_bot = row_offsets .get(ri + 1) .copied() - .unwrap_or(row_top + row.height as f32); + .unwrap_or(row_top + row.height as f32 * anno_scale); let row_mid = (row_top + row_bot) * 0.5; let row_style: Option<&acadrust::objects::RowCellStyle> = table_style.map(|ts| { let kind = match (title_suppressed, header_suppressed, ri) { @@ -544,7 +548,7 @@ pub fn tessellate_table( for (ci, cell) in row.cells.iter().enumerate() { let col_left = col_offsets[ci]; - let col_width = tab.columns.get(ci).map(|c| c.width as f32).unwrap_or(1.0); + let col_width = tab.columns.get(ci).map(|c| c.width as f32).unwrap_or(1.0) * anno_scale; let col_right = col_left + col_width; let tl = origin + h * col_left + v_flow * row_top; let tr = origin + h * col_right + v_flow * row_top; @@ -648,7 +652,7 @@ pub fn tessellate_table( }) .or_else(|| row_style.map(|s| s.text_height).filter(|h| *h > 1e-6)) .map(|h| h as f32) - .unwrap_or(0.18); + .unwrap_or(0.18) * anno_scale; let m_h = if h_margin > 1e-6 { h_margin } else { diff --git a/src/scene/annotative.rs b/src/scene/annotative.rs new file mode 100644 index 00000000..c1712693 --- /dev/null +++ b/src/scene/annotative.rs @@ -0,0 +1,98 @@ +//! Shared annotative-object detection + annotation-scale resolution. +//! +//! Both the Properties panel (Annotative row / applied scale name) and the +//! tessellation bake (which scales annotative content by the current annotation +//! 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, the legacy annotative XDATA, or an annotative style. + +use acadrust::entities::{EntityCommon, EntityType}; +use acadrust::objects::{Dictionary, ObjectType}; +use acadrust::{CadDocument, Handle}; + +/// Resolve a handle to a `Dictionary` object, if it is one. +pub fn as_dict(doc: &CadDocument, handle: Handle) -> Option<&Dictionary> { + match doc.objects.get(&handle) { + Some(ObjectType::Dictionary(d)) => Some(d), + _ => None, + } +} + +/// Does a style name resolve to `name` (or to "Standard" when `name` is blank)? +fn name_matches(style_name: &str, name: &str) -> bool { + style_name.eq_ignore_ascii_case(name) + || (name.trim().is_empty() && style_name.eq_ignore_ascii_case("Standard")) +} + +fn text_style_annotative(doc: &CadDocument, name: &str) -> bool { + doc.text_styles + .iter() + .find(|s| name_matches(&s.name, name)) + .is_some_and(|s| s.annotative) +} + +fn dim_style_annotative(doc: &CadDocument, name: &str) -> bool { + doc.dim_styles + .iter() + .find(|s| name_matches(&s.name, name)) + .is_some_and(|s| s.annotative) +} + +fn mleader_style_annotative(doc: &CadDocument, handle: Option) -> bool { + let Some(h) = handle else { + return false; + }; + doc.objects.iter().any(|(oh, o)| { + matches!(o, ObjectType::MultiLeaderStyle(s) if *oh == h && s.is_annotative) + }) +} + +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 — its extension +/// dictionary holds an `AcDbContextDataManager`. This catches objects that are +/// annotative by context even when their style is not. +fn has_context_manager(doc: &CadDocument, common: &EntityCommon) -> bool { + common + .xdictionary_handle + .and_then(|h| as_dict(doc, h)) + .map(|d| { + d.entries + .iter() + .any(|(k, _)| k.eq_ignore_ascii_case("AcDbContextDataManager")) + }) + .unwrap_or(false) +} + +/// Whether an entity participates in annotation scaling. +pub fn is_annotative(doc: &CadDocument, entity: &EntityType) -> bool { + // Per-object annotation context (works regardless of style). + if has_context_manager(doc, entity.common()) { + return true; + } + // Legacy annotative XDATA markers. + let xd = &entity.common().extended_data; + if xd.get_record("AcAnnoPO").is_some() || xd.get_record("AcAnnotativeData").is_some() { + return true; + } + // Annotative via the assigned style (or the entity's own flag). + match entity { + EntityType::Text(t) => text_style_annotative(doc, &t.style), + EntityType::MText(t) => t.is_annotative || text_style_annotative(doc, &t.style), + EntityType::Dimension(d) => dim_style_annotative(doc, &d.base().style_name), + EntityType::Leader(l) => dim_style_annotative(doc, &l.dimension_style), + EntityType::MultiLeader(ml) => { + ml.enable_annotation_scale || mleader_style_annotative(doc, ml.style_handle) + } + 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 aac62dca..26e170db 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -591,9 +591,29 @@ pub fn expand_insert( // from native content. is_xref: bool, bg_color: [f32; 4], + // Current annotation scale. An annotative block scales as one uniform unit + // about its insertion point; a non-annotative block is unaffected. + anno_scale: f32, ) -> Option> { let defn = cache.defn(&ins.block_name)?; - let xform = ins.get_transform(); + let mut xform = ins.get_transform(); + // Annotative blocks (the flag lives on the block definition; the instance is + // marked with the AcAnnotativeData XDATA) scale as ONE uniform unit about + // their insertion point — internal geometry/text/attributes are carried by + // this transform, never scaled individually (which would double-scale). + if (anno_scale - 1.0).abs() > 1e-6 + && ins + .common + .extended_data + .get_record("AcAnnotativeData") + .is_some() + { + let p = ins.insert_point; + let scale_about_p = Transform::from_translation(Vector3::new(-p.x, -p.y, -p.z)) + .then(&Transform::from_scale(anno_scale as f64)) + .then(&Transform::from_translation(Vector3::new(p.x, p.y, p.z))); + xform = xform.then(&scale_about_p); + } let name = ins_handle.value().to_string(); let mut batches = Batches::default(); let mut visited: Vec = Vec::with_capacity(8); diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 9bd201af..1c954e92 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -438,8 +438,16 @@ 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 mut wires = crate::entities::table::tessellate_table( - tab, document, sel, entity_color, line_weight_px, + tab, document, sel, entity_color, line_weight_px, table_anno, ); if !wires.is_empty() { let aabb = entity_aabb(e); @@ -516,6 +524,7 @@ pub(crate) fn tessellate_entity( world_per_pixel, is_xref, bg_color, + anno_scale, ) { // XCLIP: if this INSERT carries an enabled spatial filter, // clip the expanded block geometry to the boundary polygon so diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 2111eb4b..29c7e22b 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -103,26 +103,17 @@ pub fn tessellate( }; let name = handle.value().to_string(); - // Determine effective annotation scale for this entity. + // Determine the effective annotation scale for this entity. // - // AutoCAD's R2007+ "annotative" system marks objects via extension- - // dictionary records or "AcAnnoPO" / "AcAnnotativeData" xdata. Only - // entities so marked should be auto-scaled by the viewport's - // paper-scale; everything else is treated as manually pre-scaled - // (old DXF/DWG convention with $DIMSCALE and oversized text). - // - // Default: NOT annotative (anno_scale = 1.0). Opt-in via explicit - // xdata marker. Files that mark every entity annotative are rare; the - // pre-R2007 manual-scale convention is far more common in field data. - let anno_scale = { - let xdata = &entity.common().extended_data; - let is_annotative = xdata.get_record("AcAnnoPO").is_some() - || xdata.get_record("AcAnnotativeData").is_some(); - if is_annotative { - anno_scale - } else { - 1.0 - } + // Only annotative entities are auto-scaled by the current annotation scale; + // everything else is manually pre-scaled (old convention with $DIMSCALE and + // 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 = if crate::scene::annotative::is_annotative(document, entity) { + anno_scale + } else { + 1.0 }; // A HATCH is drawn as a fill by the hatch pipeline and highlighted via a diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 7816871d..f55eafed 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -5,6 +5,7 @@ // pick — hit-testing, selection, grips, spatial index, xclip // view — camera, transforms, viewport, render pipeline driver // cache — block-definition and property caches +pub mod annotative; pub mod cache; pub mod convert; pub mod model; @@ -1158,12 +1159,12 @@ impl Scene { } else { self.paper_bg_color }; - let anno = if self.current_layout == "Model" { - self.annotation_scale - } else { - 1.0 - }; - let built = cache::block_cache::BlockCache::build(&self.document, anno, bg); + // 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); let arc = Arc::new(built); *self.block_defn_cache.borrow_mut() = Some((self.block_epoch, Arc::clone(&arc))); arc