cad-editor/src/scene/annotative.rs
Hakan Seven db43c21d55 fix(annotative): scale text by its own annotativeness, not its style's
An annotative object is drawn at the annotation scale only for the scales
it has a representation for. Text was being scaled whenever its text
style carried the annotative flag, whether or not the text itself had any
representation at all.

A drawing of twenty floors here has 1144 pieces of text on a style
someone flagged annotative and no object contexts anywhere. At 1:100 they
were drawn a hundred times too tall — a 411-unit height reaching 41067 —
while every other style in the same drawing stayed put.

`is_annotative` already said the right thing, and said why: a text style
can be turned annotative long after text was drawn in it, and that text
keeps its stored height until it is explicitly converted. The scale
resolver contradicted it. Text now scales on its own annotativeness
alone: an object context, a legacy XDATA marker, or the entity's flag.

Dimensions, leaders, tolerances and tables are unchanged — there the
style is the standard signal and still counts on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 02:45:59 +03:00

1756 lines
67 KiB
Rust

//! 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, legacy annotative XDATA, or an entity-level annotative flag. 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, DimContext, DimSubtype, EmbeddedMTextContext, HatchScaleContext,
MTextAttributeContext, MTextContext, ObjectContextData, ObjectContextKind, ObjectType,
};
use acadrust::types::{Vector2, Vector3};
use acadrust::{CadDocument, Handle};
use std::borrow::Cow;
/// 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,
}
}
/// Resolve the drawing's root named-objects dictionary, creating one if the
/// file has none reachable.
///
/// The canonical pointer is `header.named_objects_dict_handle`, but DWGs
/// written by some programs leave it dangling — pointing at a handle that never
/// loaded, or at a non-dictionary — while the real named-object sub-dictionaries
/// (`ACAD_LAYOUT`, `ACAD_SCALELIST`, …) are instead owned by an unrelated handle
/// that is not a dictionary. When the pointer can't be resolved we adopt any
/// top-level (`owner == NULL`) dictionary as the root; failing that we synthesise
/// a fresh, empty root so that *registering* a new named-object entry (a page
/// setup, an annotation scale, the `CTAB` variable) actually persists instead of
/// silently no-opping against a missing dictionary.
///
/// Idempotent: the resolved or created handle is written back to the header, so
/// later calls return the same dictionary rather than minting another root. On a
/// well-formed drawing this returns the existing root untouched.
pub fn root_named_dict_handle(doc: &mut CadDocument) -> Handle {
let h = doc.header.named_objects_dict_handle;
if matches!(doc.objects.get(&h), Some(ObjectType::Dictionary(_))) {
return h;
}
// A top-level dictionary is already present (the standard root shape) — adopt
// the richest one (matching the DWG writer's own root heuristic) and repair
// the stale header pointer.
if let Some(root) = doc
.objects
.iter()
.filter_map(|(k, o)| match o {
ObjectType::Dictionary(d) if d.owner.is_null() => Some((*k, d.entries.len())),
_ => None,
})
.max_by_key(|&(_, n)| n)
.map(|(k, _)| k)
{
doc.header.named_objects_dict_handle = root;
return root;
}
// Nothing reachable — build a fresh, empty root named-objects dictionary.
let nh = doc.allocate_handle();
let mut d = Dictionary::new();
d.handle = nh;
d.owner = Handle::NULL;
doc.objects.insert(nh, ObjectType::Dictionary(d));
doc.header.named_objects_dict_handle = nh;
nh
}
/// Set the per-object annotative flag on the entity types that carry one
/// (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
/// than a native flag; other entity types are not toggled here.
pub fn set_entity_annotative(doc: &mut CadDocument, handle: Handle, want: bool) {
if let Some(e) = doc.get_entity_mut(handle) {
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,
_ => {}
}
}
if !want {
clear_annotation_context(doc, handle);
}
}
/// 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.
fn dimension_context_for(doc: &CadDocument, dimension: &acadrust::entities::Dimension) -> Option<DimContext> {
use acadrust::entities::Dimension;
let subtype = match dimension {
Dimension::Aligned(dim) => DimSubtype::Aligned {
dimline_pt: dim.definition_point,
},
Dimension::Linear(dim) => DimSubtype::Aligned {
dimline_pt: dim.definition_point,
},
Dimension::Angular2Ln(dim) => DimSubtype::Angular {
arc_pt: dim.dimension_arc,
},
Dimension::Angular3Pt(dim) => DimSubtype::Angular {
arc_pt: dim.definition_point,
},
Dimension::Diameter(dim) => DimSubtype::Diametric {
first_arc_pt: dim.angle_vertex,
def_pt: dim.definition_point,
},
Dimension::Radius(dim) => DimSubtype::Radial {
first_arc_pt: dim.definition_point,
},
Dimension::LargeRadial(dim) => DimSubtype::RadialLarge {
ovr_center: dim.override_center,
jog_point: dim.jog_point,
},
Dimension::Ordinate(dim) => DimSubtype::Ordinate {
feature_location_pt: dim.feature_location,
leader_endpt: dim.leader_endpoint,
},
Dimension::Arc(_) => return None,
};
let base = dimension.base();
let block = doc
.block_records
.iter()
.find(|record| record.name.eq_ignore_ascii_case(&base.block_name))
.map(|record| record.handle)
.unwrap_or(Handle::NULL);
Some(DimContext {
def_pt: Vector2::new(base.text_middle_point.x, base.text_middle_point.y),
is_def_textloc: base.text_user_positioned,
text_rotation: base.text_rotation,
block,
b293: false,
dimtofl: false,
dimosxd: false,
dimatfit: false,
dimtix: false,
dimtmove: false,
override_code: 0,
has_arrow2: false,
flip_arrow2: base.flip_arrow2,
flip_arrow1: base.flip_arrow1,
subtype,
})
}
fn mtext_context_for(m: &acadrust::entities::MText) -> MTextContext {
MTextContext {
attachment: m.attachment_point as i32,
x_axis_dir: m
.dwg_x_direction
.unwrap_or_else(|| Vector3::new(m.rotation.cos(), m.rotation.sin(), 0.0)),
insertion: m.insertion_point,
rect_width: m.rectangle_width,
rect_height: m.rectangle_height.unwrap_or(0.0),
extents_width: m.extents_width,
extents_height: m.extents_height,
column_type: m.column_data.column_type as i32,
columns: (m.column_data.column_type != 0).then(|| acadrust::objects::MTextColumns {
num_heights: m.column_data.column_count,
width: m.column_data.width,
gutter: m.column_data.gutter,
auto_height: m.column_data.auto_height,
flow_reversed: m.column_data.flow_reversed,
heights: m.column_data.heights.clone(),
}),
}
}
fn attribute_context_for(
insertion: Vector3,
alignment: Vector3,
rotation: f64,
horizontal_mode: i16,
embedded: Option<&acadrust::entities::MText>,
scale: Handle,
) -> MTextAttributeContext {
MTextAttributeContext {
horizontal_mode,
rotation,
insertion: Vector2::new(insertion.x, insertion.y),
alignment: Vector2::new(alignment.x, alignment.y),
enable_context: embedded.is_some(),
context: embedded.map(|mtext| EmbeddedMTextContext {
owner_handle: Handle::NULL,
reactors: Vec::new(),
xdictionary_handle: None,
has_binary_data: false,
class_version: 3,
is_default: false,
scale,
mtext: mtext_context_for(mtext),
}),
}
}
fn context_kind_for(
doc: &CadDocument,
entity: &EntityType,
scale: Handle,
) -> Option<(&'static str, ObjectContextKind)> {
match entity {
EntityType::Insert(ins) => Some((
"ACDB_BLKREFOBJECTCONTEXTDATA_CLASS",
ObjectContextKind::BlkRef {
rotation: ins.rotation,
insertion: ins.insert_point,
scale_factor: Vector3::new(ins.x_scale(), ins.y_scale(), ins.z_scale()),
},
)),
EntityType::Text(t) => Some((
"ACDB_TEXTOBJECTCONTEXTDATA_CLASS",
ObjectContextKind::Text {
horizontal_mode: t.horizontal_alignment as i16,
rotation: t.rotation,
insertion: Vector2::new(t.insertion_point.x, t.insertion_point.y),
alignment: t
.alignment_point
.map(|p| Vector2::new(p.x, p.y))
.unwrap_or(Vector2::new(0.0, 0.0)),
},
)),
EntityType::MText(m) => Some((
"ACDB_MTEXTOBJECTCONTEXTDATA_CLASS",
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:
///
/// ```text
/// entity xdict → "AcDbContextDataManager" → "ACDB_ANNOTATIONSCALES" → "*An" → leaf
/// ```
///
/// The leaf is an [`ObjectContextData`] whose placement is copied from the
/// entity's current geometry and whose `340` handle references `scale_handle`
/// (an `AcDbScale` in `ACAD_SCALELIST`). Idempotent: a leaf for that scale is
/// not duplicated. Exactly one leaf per object is marked `is_default` (the
/// first one created — the native representation). Returns `false` for entity
/// kinds that carry no per-object context (their annotative-ness is style-driven).
pub fn create_annotation_context(
doc: &mut CadDocument,
entity_handle: Handle,
scale_handle: Handle,
) -> bool {
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.
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.
let xdict_h = match doc
.get_entity(entity_handle)
.and_then(|e| e.common().xdictionary_handle)
{
Some(h) if as_dict(doc, h).is_some() => h,
_ => {
let h = doc.allocate_handle();
let mut d = Dictionary::new();
d.handle = h;
d.owner = entity_handle;
d.hard_owner = true;
doc.objects.insert(h, ObjectType::Dictionary(d));
if let Some(e) = doc.get_entity_mut(entity_handle) {
e.common_mut().xdictionary_handle = Some(h);
}
h
}
};
let mgr_h = get_or_create_child_dict(doc, xdict_h, "AcDbContextDataManager");
let coll_h = get_or_create_child_dict(doc, mgr_h, "ACDB_ANNOTATIONSCALES");
// Idempotent: if a leaf already applies to this scale, keep it.
let existing = as_dict(doc, coll_h)
.map(|d| {
d.entries.iter().any(|(_, lh)| {
matches!(
doc.objects.get(lh),
Some(ObjectType::ObjectContextData(c)) if c.scale == scale_handle
)
})
})
.unwrap_or(false);
if existing {
return true;
}
// The first representation created is the default (native) one.
let is_default = as_dict(doc, coll_h).map(|d| d.entries.is_empty()).unwrap_or(true);
let n = as_dict(doc, coll_h).map(|d| d.entries.len()).unwrap_or(0) + 1;
let key = format!("*A{n}");
let leaf_h = doc.allocate_handle();
let leaf = ObjectContextData {
handle: leaf_h,
owner_handle: coll_h,
reactors: vec![coll_h],
xdictionary_handle: None,
class_version: 3,
is_default,
scale: scale_handle,
kind,
};
doc.objects
.insert(leaf_h, ObjectType::ObjectContextData(leaf));
if let Some(ObjectType::Dictionary(coll)) = doc.objects.get_mut(&coll_h) {
coll.add_entry(key, leaf_h);
}
true
}
/// True when `common` belongs to an annotative object whose per-object scale
/// contexts are *all* bound to scales other than the current one — an
/// off-scale representation that must stay hidden. A drawing may materialise
/// each annotation scale as its own object (e.g. a block insert per scale on a
/// "0 @ <scale>" layer); only the current scale's representation should draw.
///
/// 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_for(
doc: &CadDocument,
common: &EntityCommon,
scale_handle: Option<Handle>,
all_visible: bool,
) -> bool {
if all_visible {
return false;
}
if !common
.xdictionary_handle
.map(|h| !h.is_null())
.unwrap_or(false)
{
return false;
}
let scales = object_scale_memberships(doc, common.handle);
if scales.is_empty() {
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)
}),
}
}
pub fn scale_handle_by_name(doc: &CadDocument, name: &str) -> Option<Handle> {
doc.objects.iter().find_map(|(handle, object)| match object {
ObjectType::Scale(scale)
if !scale.is_temporary && scale.name.eq_ignore_ascii_case(name) =>
{
Some(*handle)
}
_ => 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.
pub fn object_scale_memberships(doc: &CadDocument, entity: Handle) -> Vec<(String, Handle)> {
let mut out = Vec::new();
let Some(coll_h) = annotation_scales_dict(doc, entity) else {
return out;
};
if let Some(coll) = as_dict(doc, coll_h) {
for (_, lh) in &coll.entries {
if let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get(lh) {
if let Some(ObjectType::Scale(s)) = doc.objects.get(&leaf.scale) {
out.push((s.name.clone(), leaf.scale));
}
}
}
}
out
}
/// Remove the per-object context representation that applies to `scale_handle`,
/// keeping the object's other representations. When that was the object's last
/// representation the whole context chain (and the annotative markers) are torn
/// down via [`clear_annotation_context`] so the object becomes non-annotative.
/// Returns `true` if a representation was removed.
pub fn remove_annotation_context_for_scale(
doc: &mut CadDocument,
entity: Handle,
scale_handle: Handle,
) -> bool {
let Some(coll_h) = annotation_scales_dict(doc, entity) else {
return false;
};
// Find the leaf that applies to this scale.
let leaf = as_dict(doc, coll_h).and_then(|c| {
c.entries.iter().find_map(|(_, lh)| {
matches!(
doc.objects.get(lh),
Some(ObjectType::ObjectContextData(o)) if o.scale == scale_handle
)
.then_some(*lh)
})
});
let Some(leaf_h) = leaf else {
return false;
};
// If this is the object's only representation, fully de-annotate it (drop the
// whole chain AND the native flag, like the Yes→No toggle) so it stops
// resolving annotative; otherwise drop just this leaf.
let last = as_dict(doc, coll_h).map(|c| c.entries.len() <= 1).unwrap_or(true);
if last {
set_entity_annotative(doc, entity, false);
return true;
}
doc.objects.remove(&leaf_h);
if let Some(ObjectType::Dictionary(coll)) = doc.objects.get_mut(&coll_h) {
coll.entries.retain(|(_, h)| *h != leaf_h);
}
true
}
/// Resolve an entity's `ACDB_ANNOTATIONSCALES` collection dictionary handle, if
/// its context chain exists.
fn annotation_scales_dict(doc: &CadDocument, entity: Handle) -> Option<Handle> {
let xd = doc.get_entity(entity).and_then(|e| e.common().xdictionary_handle)?;
let mgr = as_dict(doc, xd).and_then(|d| d.get("AcDbContextDataManager"))?;
as_dict(doc, mgr).and_then(|d| d.get("ACDB_ANNOTATIONSCALES"))
}
/// Resolve the representation leaf used by the drawing's current annotation
/// 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_for_scale(
doc: &CadDocument,
entity: Handle,
scale_handle: Option<Handle>,
) -> Option<&ObjectContextData> {
let coll_h = annotation_scales_dict(doc, entity)?;
let coll = as_dict(doc, coll_h)?;
let mut default = None;
let mut first = None;
for (_, leaf_h) in &coll.entries {
let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get(leaf_h) else {
continue;
};
first.get_or_insert(leaf);
if leaf.is_default {
default = 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)
}
pub fn effective_annotation_scale_for(
doc: &CadDocument,
entity: &EntityType,
fallback: f32,
scale_handle: Option<Handle>,
) -> f32 {
let context_annotative = is_annotative(doc, entity);
let style_annotative = annotation_style_is_annotative(doc, entity);
// An annotative object is scaled at the scales it has a representation
// for. Text is the case where the style alone proves nothing: a text style
// can be turned annotative long after text was drawn in it, and that text
// keeps its stored height until it is explicitly converted — which is what
// `is_annotative` already says by ignoring the style for TEXT. Scaling on
// the style regardless contradicted it, and a drawing whose text sits on a
// style someone flagged annotative, with no object contexts anywhere, came
// out at the whole annotation scale too large.
//
// A dimension, leader, tolerance or table is different: there the style is
// the standard signal and still counts on its own.
let text_like = matches!(
entity,
EntityType::Text(_)
| EntityType::MText(_)
| EntityType::AttributeEntity(_)
| EntityType::AttributeDefinition(_)
);
if !context_annotative && (text_like || !style_annotative) {
return 1.0;
}
if matches!(
entity,
EntityType::Dimension(_)
| EntityType::Leader(_)
| EntityType::Tolerance(_)
| EntityType::Table(_)
) {
return fallback;
}
// Unlike MTEXT / DIMENSION contexts, an MLEADER context carries its
// already-scaled text height and overall scale factor. Keep the text
// 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_for_scale(doc, entity.common().handle, scale_handle)
else {
return fallback;
};
let ObjectContextKind::MLeader(context) = &active.kind else {
return fallback;
};
let base = mleader.scale_factor;
if base.abs() <= 1.0e-12 {
return fallback;
}
let relative = context.scale_factor / base;
return if relative.is_finite() && relative > 0.0 {
relative as f32
} else {
fallback
};
}
let Some(coll_h) = annotation_scales_dict(doc, entity.common().handle) else {
return fallback;
};
let Some(coll) = as_dict(doc, coll_h) else {
return fallback;
};
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),
_ => None,
}
});
let (Some(active), Some(native)) = (active, native) else {
return fallback;
};
let (Some(ObjectType::Scale(active_scale)), Some(ObjectType::Scale(native_scale))) = (
doc.objects.get(&active.scale),
doc.objects.get(&native.scale),
) else {
return fallback;
};
let native_factor = native_scale.inverse_factor();
if native_factor.abs() <= 1.0e-12 {
return fallback;
}
let relative = active_scale.inverse_factor() / native_factor;
if relative.is_finite() && relative > 0.0 {
relative as f32
} else {
fallback
}
}
fn text_horizontal(value: i16) -> acadrust::entities::TextHorizontalAlignment {
use acadrust::entities::TextHorizontalAlignment;
match value {
1 => TextHorizontalAlignment::Center,
2 => TextHorizontalAlignment::Right,
3 => TextHorizontalAlignment::Aligned,
4 => TextHorizontalAlignment::Middle,
5 => TextHorizontalAlignment::Fit,
_ => TextHorizontalAlignment::Left,
}
}
fn mtext_attachment(value: i32) -> acadrust::entities::AttachmentPoint {
use acadrust::entities::AttachmentPoint;
match value {
2 => AttachmentPoint::TopCenter,
3 => AttachmentPoint::TopRight,
4 => AttachmentPoint::MiddleLeft,
5 => AttachmentPoint::MiddleCenter,
6 => AttachmentPoint::MiddleRight,
7 => AttachmentPoint::BottomLeft,
8 => AttachmentPoint::BottomCenter,
9 => AttachmentPoint::BottomRight,
_ => AttachmentPoint::TopLeft,
}
}
fn apply_mtext_context(entity: &mut acadrust::entities::MText, context: &MTextContext) {
entity.attachment_point = mtext_attachment(context.attachment);
entity.insertion_point = context.insertion;
entity.rectangle_width = context.rect_width;
entity.rectangle_height = (context.rect_height > 0.0).then_some(context.rect_height);
entity.extents_width = context.extents_width;
entity.extents_height = context.extents_height;
entity.rotation = context.x_axis_dir.y.atan2(context.x_axis_dir.x);
entity.dwg_x_direction = Some(context.x_axis_dir);
if let Some(columns) = &context.columns {
entity.column_data.column_type = context.column_type as i16;
entity.column_data.column_count = columns.num_heights;
entity.column_data.width = columns.width;
entity.column_data.gutter = columns.gutter;
entity.column_data.auto_height = columns.auto_height;
entity.column_data.flow_reversed = columns.flow_reversed;
entity.column_data.heights.clone_from(&columns.heights);
} else {
entity.column_data.column_type = context.column_type as i16;
entity.column_data.column_count = 0;
entity.column_data.heights.clear();
}
}
fn apply_dimension_context(
dimension: &mut acadrust::entities::Dimension,
context: &acadrust::objects::DimContext,
doc: &CadDocument,
) {
use acadrust::entities::Dimension;
use acadrust::objects::DimSubtype;
{
let base = dimension.base_mut();
base.text_middle_point.x = context.def_pt.x;
base.text_middle_point.y = context.def_pt.y;
base.text_rotation = context.text_rotation;
base.text_user_positioned = context.is_def_textloc;
base.flip_arrow1 = context.flip_arrow1;
base.flip_arrow2 = context.flip_arrow2;
if let Some(record) = doc.block_records.iter().find(|r| r.handle == context.block) {
base.block_name.clone_from(&record.name);
}
}
match (&context.subtype, dimension) {
(DimSubtype::Aligned { dimline_pt }, Dimension::Aligned(dim)) => {
dim.definition_point = *dimline_pt;
dim.base.definition_point = *dimline_pt;
}
(DimSubtype::Aligned { dimline_pt }, Dimension::Linear(dim)) => {
dim.definition_point = *dimline_pt;
dim.base.definition_point = *dimline_pt;
}
(DimSubtype::Angular { arc_pt }, Dimension::Angular2Ln(dim)) => {
dim.dimension_arc = *arc_pt;
dim.base.definition_point = *arc_pt;
}
(DimSubtype::Angular { arc_pt }, Dimension::Angular3Pt(dim)) => {
dim.definition_point = *arc_pt;
dim.base.definition_point = *arc_pt;
}
(
DimSubtype::Diametric {
first_arc_pt,
def_pt,
},
Dimension::Diameter(dim),
) => {
dim.angle_vertex = *first_arc_pt;
dim.definition_point = *def_pt;
dim.base.definition_point = *def_pt;
}
(DimSubtype::Radial { first_arc_pt }, Dimension::Radius(dim)) => {
dim.definition_point = *first_arc_pt;
dim.base.definition_point = *first_arc_pt;
}
(
DimSubtype::RadialLarge {
ovr_center,
jog_point,
},
Dimension::LargeRadial(dim),
) => {
dim.override_center = *ovr_center;
dim.jog_point = *jog_point;
}
(
DimSubtype::Ordinate {
feature_location_pt,
leader_endpt,
},
Dimension::Ordinate(dim),
) => {
dim.feature_location = *feature_location_pt;
dim.leader_endpoint = *leader_endpt;
}
_ => {}
}
}
fn apply_attribute_context(
insertion_point: &mut Vector3,
alignment_point: &mut Vector3,
rotation: &mut f64,
horizontal_alignment: &mut acadrust::entities::HorizontalAlignment,
embedded_mtext: &mut Option<Box<acadrust::entities::MText>>,
context: &acadrust::objects::MTextAttributeContext,
) {
insertion_point.x = context.insertion.x;
insertion_point.y = context.insertion.y;
alignment_point.x = context.alignment.x;
alignment_point.y = context.alignment.y;
*rotation = context.rotation;
*horizontal_alignment =
acadrust::entities::HorizontalAlignment::from_value(context.horizontal_mode);
if context.enable_context {
if let (Some(embedded), Some(mtext)) = (&context.context, embedded_mtext.as_mut()) {
apply_mtext_context(mtext, &embedded.mtext);
}
}
}
fn apply_hatch_context(hatch: &mut acadrust::entities::Hatch, context: &HatchScaleContext) {
hatch.pattern.lines.clone_from(&context.pattern_lines);
hatch.pattern_scale = context.pattern_scale;
for line in &mut hatch.pattern.lines {
line.base_point.x += context.pattern_base.x;
line.base_point.y += context.pattern_base.y;
}
for (path, bits) in hatch.paths.iter_mut().zip(&context.loop_types) {
path.flags = acadrust::entities::BoundaryPathFlags::from_bits(*bits as u32);
}
}
pub fn entity_for_annotation_context<'a>(
doc: &'a CadDocument,
entity: &'a EntityType,
scale_handle: Option<Handle>,
) -> Cow<'a, EntityType> {
let Some(context) =
active_object_context_for_scale(doc, entity.common().handle, scale_handle)
else {
return Cow::Borrowed(entity);
};
let mut placed = entity.clone();
match (&context.kind, &mut placed) {
(
ObjectContextKind::BlkRef {
rotation,
insertion,
scale_factor,
},
EntityType::Insert(insert),
) => {
insert.rotation = *rotation;
insert.insert_point = *insertion;
insert.set_x_scale(scale_factor.x);
insert.set_y_scale(scale_factor.y);
insert.set_z_scale(scale_factor.z);
}
(
ObjectContextKind::Text {
horizontal_mode,
rotation,
insertion,
alignment,
},
EntityType::Text(text),
) => {
text.horizontal_alignment = text_horizontal(*horizontal_mode);
text.rotation = *rotation;
text.insertion_point.x = insertion.x;
text.insertion_point.y = insertion.y;
let point = text.alignment_point.get_or_insert(Vector3::ZERO);
point.x = alignment.x;
point.y = alignment.y;
}
(ObjectContextKind::MText(value), EntityType::MText(mtext)) => {
apply_mtext_context(mtext, value);
}
(ObjectContextKind::Dim(value), EntityType::Dimension(dimension)) => {
apply_dimension_context(dimension, value, doc);
}
(ObjectContextKind::MLeader(value), EntityType::MultiLeader(mleader)) => {
mleader.context.clone_from(value);
}
(
ObjectContextKind::MTextAttribute(value),
EntityType::AttributeEntity(attribute),
) => {
apply_attribute_context(
&mut attribute.insertion_point,
&mut attribute.alignment_point,
&mut attribute.rotation,
&mut attribute.horizontal_alignment,
&mut attribute.embedded_mtext,
value,
);
}
(
ObjectContextKind::MTextAttribute(value),
EntityType::AttributeDefinition(attribute),
) => {
apply_attribute_context(
&mut attribute.insertion_point,
&mut attribute.alignment_point,
&mut attribute.rotation,
&mut attribute.horizontal_alignment,
&mut attribute.embedded_mtext,
value,
);
}
(ObjectContextKind::Leader(value), EntityType::Leader(leader)) => {
leader.vertices = value
.points
.iter()
.map(|point| *point + value.insertion_offset)
.collect();
leader.horizontal_direction = value.x_direction;
leader.annotation_offset = value.endpoint_projection;
}
(
ObjectContextKind::Fcf {
location,
horizontal_direction,
},
EntityType::Tolerance(tolerance),
) => {
tolerance.insertion_point = *location;
tolerance.direction = *horizontal_direction;
}
(ObjectContextKind::HatchScale(value), EntityType::Hatch(hatch)) => {
apply_hatch_context(hatch, value);
}
(ObjectContextKind::HatchView(value), EntityType::Hatch(hatch)) => {
apply_hatch_context(hatch, &value.hatch);
hatch.normal = value.view_normal;
hatch.pattern_angle += value.view_rotation;
}
_ => {}
}
Cow::Owned(placed)
}
fn sync_mtext_context(context: &mut MTextContext, entity: &acadrust::entities::MText) {
context.attachment = entity.attachment_point as i32;
context.x_axis_dir = entity.dwg_x_direction.unwrap_or_else(|| {
Vector3::new(entity.rotation.cos(), entity.rotation.sin(), 0.0)
});
context.insertion = entity.insertion_point;
context.rect_width = entity.rectangle_width;
context.rect_height = entity.rectangle_height.unwrap_or(0.0);
context.extents_width = entity.extents_width;
context.extents_height = entity.extents_height;
context.column_type = entity.column_data.column_type as i32;
if context.column_type == 0 {
context.columns = None;
} else {
let columns = context
.columns
.get_or_insert_with(|| acadrust::objects::MTextColumns {
num_heights: 0,
width: 0.0,
gutter: 0.0,
auto_height: false,
flow_reversed: false,
heights: Vec::new(),
});
columns.num_heights = entity.column_data.column_count;
columns.width = entity.column_data.width;
columns.gutter = entity.column_data.gutter;
columns.auto_height = entity.column_data.auto_height;
columns.flow_reversed = entity.column_data.flow_reversed;
columns.heights.clone_from(&entity.column_data.heights);
}
}
fn sync_dimension_context(
context: &mut acadrust::objects::DimContext,
dimension: &acadrust::entities::Dimension,
block_handle: Option<Handle>,
) {
use acadrust::entities::Dimension;
use acadrust::objects::DimSubtype;
let base = dimension.base();
context.def_pt = Vector2::new(base.text_middle_point.x, base.text_middle_point.y);
context.is_def_textloc = base.text_user_positioned;
context.text_rotation = base.text_rotation;
context.flip_arrow1 = base.flip_arrow1;
context.flip_arrow2 = base.flip_arrow2;
if let Some(handle) = block_handle {
context.block = handle;
}
match (&mut context.subtype, dimension) {
(DimSubtype::Aligned { dimline_pt }, Dimension::Aligned(dim)) => {
*dimline_pt = dim.definition_point;
}
(DimSubtype::Aligned { dimline_pt }, Dimension::Linear(dim)) => {
*dimline_pt = dim.definition_point;
}
(DimSubtype::Angular { arc_pt }, Dimension::Angular2Ln(dim)) => {
*arc_pt = dim.dimension_arc;
}
(DimSubtype::Angular { arc_pt }, Dimension::Angular3Pt(dim)) => {
*arc_pt = dim.definition_point;
}
(
DimSubtype::Diametric {
first_arc_pt,
def_pt,
},
Dimension::Diameter(dim),
) => {
*first_arc_pt = dim.angle_vertex;
*def_pt = dim.definition_point;
}
(DimSubtype::Radial { first_arc_pt }, Dimension::Radius(dim)) => {
*first_arc_pt = dim.definition_point;
}
(
DimSubtype::RadialLarge {
ovr_center,
jog_point,
},
Dimension::LargeRadial(dim),
) => {
*ovr_center = dim.override_center;
*jog_point = dim.jog_point;
}
(
DimSubtype::Ordinate {
feature_location_pt,
leader_endpt,
},
Dimension::Ordinate(dim),
) => {
*feature_location_pt = dim.feature_location;
*leader_endpt = dim.leader_endpoint;
}
_ => {}
}
}
/// Copy an edited entity's placement back into one per-scale leaf so geometry
/// edits remain attached to the representation displayed by the caller.
pub fn sync_annotation_context_from_entity(
doc: &mut CadDocument,
entity_handle: Handle,
scale_handle: Option<Handle>,
) -> bool {
let Some(leaf_handle) =
active_object_context_for_scale(doc, entity_handle, scale_handle).map(|leaf| leaf.handle)
else {
return false;
};
let Some(entity) = doc.get_entity(entity_handle).cloned() else {
return false;
};
let dim_block_handle = match &entity {
EntityType::Dimension(dimension) => doc
.block_records
.iter()
.find(|record| record.name.eq_ignore_ascii_case(&dimension.base().block_name))
.map(|record| record.handle),
_ => None,
};
let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get_mut(&leaf_handle) else {
return false;
};
match (&entity, &mut leaf.kind) {
(
EntityType::Insert(insert),
ObjectContextKind::BlkRef {
rotation,
insertion,
scale_factor,
},
) => {
*rotation = insert.rotation;
*insertion = insert.insert_point;
*scale_factor =
Vector3::new(insert.x_scale(), insert.y_scale(), insert.z_scale());
}
(
EntityType::Text(text),
ObjectContextKind::Text {
horizontal_mode,
rotation,
insertion,
alignment,
},
) => {
*horizontal_mode = match text.horizontal_alignment {
acadrust::entities::TextHorizontalAlignment::Left => 0,
acadrust::entities::TextHorizontalAlignment::Center => 1,
acadrust::entities::TextHorizontalAlignment::Right => 2,
acadrust::entities::TextHorizontalAlignment::Aligned => 3,
acadrust::entities::TextHorizontalAlignment::Middle => 4,
acadrust::entities::TextHorizontalAlignment::Fit => 5,
};
*rotation = text.rotation;
*insertion = Vector2::new(text.insertion_point.x, text.insertion_point.y);
let point = text.alignment_point.unwrap_or(Vector3::ZERO);
*alignment = Vector2::new(point.x, point.y);
}
(EntityType::MText(mtext), ObjectContextKind::MText(context)) => {
sync_mtext_context(context, mtext);
}
(EntityType::Dimension(dimension), ObjectContextKind::Dim(context)) => {
sync_dimension_context(context, dimension, dim_block_handle);
}
(EntityType::MultiLeader(mleader), ObjectContextKind::MLeader(context)) => {
context.clone_from(&mleader.context);
}
(
EntityType::AttributeEntity(attribute),
ObjectContextKind::MTextAttribute(context),
) => {
context.horizontal_mode = attribute.horizontal_alignment.to_value();
context.rotation = attribute.rotation;
context.insertion =
Vector2::new(attribute.insertion_point.x, attribute.insertion_point.y);
context.alignment =
Vector2::new(attribute.alignment_point.x, attribute.alignment_point.y);
if let (Some(embedded), Some(mtext)) =
(&mut context.context, &attribute.embedded_mtext)
{
sync_mtext_context(&mut embedded.mtext, mtext);
}
}
(
EntityType::AttributeDefinition(attribute),
ObjectContextKind::MTextAttribute(context),
) => {
context.horizontal_mode = attribute.horizontal_alignment.to_value();
context.rotation = attribute.rotation;
context.insertion =
Vector2::new(attribute.insertion_point.x, attribute.insertion_point.y);
context.alignment =
Vector2::new(attribute.alignment_point.x, attribute.alignment_point.y);
if let (Some(embedded), Some(mtext)) =
(&mut context.context, &attribute.embedded_mtext)
{
sync_mtext_context(&mut embedded.mtext, mtext);
}
}
(EntityType::Leader(leader), ObjectContextKind::Leader(context)) => {
context.points = leader
.vertices
.iter()
.map(|point| *point - context.insertion_offset)
.collect();
context.x_direction = leader.horizontal_direction;
context.endpoint_projection = leader.annotation_offset;
}
(
EntityType::Tolerance(tolerance),
ObjectContextKind::Fcf {
location,
horizontal_direction,
},
) => {
*location = tolerance.insertion_point;
*horizontal_direction = tolerance.direction;
}
(EntityType::Hatch(hatch), ObjectContextKind::HatchScale(context)) => {
context.pattern_lines.clone_from(&hatch.pattern.lines);
for line in &mut context.pattern_lines {
line.base_point.x -= context.pattern_base.x;
line.base_point.y -= context.pattern_base.y;
}
context.pattern_scale = hatch.pattern_scale;
context.loop_types = hatch
.paths
.iter()
.map(|path| path.flags.bits() as i32)
.collect();
}
(EntityType::Hatch(hatch), ObjectContextKind::HatchView(context)) => {
context.hatch.pattern_lines.clone_from(&hatch.pattern.lines);
for line in &mut context.hatch.pattern_lines {
line.base_point.x -= context.hatch.pattern_base.x;
line.base_point.y -= context.hatch.pattern_base.y;
}
context.hatch.pattern_scale = hatch.pattern_scale;
context.hatch.loop_types = hatch
.paths
.iter()
.map(|path| path.flags.bits() as i32)
.collect();
context.view_normal = hatch.normal;
}
_ => {}
}
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 {
if let Some(h) = as_dict(doc, parent_h).and_then(|d| d.get(key)) {
return h;
}
let h = doc.allocate_handle();
let mut d = Dictionary::new();
d.handle = h;
d.owner = parent_h;
doc.objects.insert(h, ObjectType::Dictionary(d));
if let Some(ObjectType::Dictionary(p)) = doc.objects.get_mut(&parent_h) {
p.add_entry(key, h);
}
h
}
/// Remove an entity's per-object annotation context — the extension-dictionary
/// `AcDbContextDataManager` → `ACDB_ANNOTATIONSCALES` → per-scale leaf subtree —
/// and the legacy annotative XDATA markers, so [`is_annotative`] no longer fires
/// on it. The shared `SCALE` objects in `ACAD_SCALELIST` are document-level and
/// left intact.
pub fn clear_annotation_context(doc: &mut CadDocument, handle: Handle) {
if let Some(xdict_h) = doc.get_entity(handle).and_then(|e| e.common().xdictionary_handle) {
// Collect the manager subtree (manager dict, its scales dict, the leaves)
// before mutating, then drop them.
let mut remove = Vec::new();
if let Some(mgr_h) = as_dict(doc, xdict_h).and_then(|d| d.get("AcDbContextDataManager")) {
remove.push(mgr_h);
if let Some(scales_h) =
as_dict(doc, mgr_h).and_then(|d| d.get("ACDB_ANNOTATIONSCALES"))
{
remove.push(scales_h);
if let Some(scales) = as_dict(doc, scales_h) {
for (_, leaf) in &scales.entries {
remove.push(*leaf);
}
}
}
}
if let Some(ObjectType::Dictionary(xd)) = doc.objects.get_mut(&xdict_h) {
xd.entries.retain(|(k, _)| k != "AcDbContextDataManager");
}
for h in remove {
doc.objects.remove(&h);
}
}
// 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);
}
/// 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"))
}
/// Whether `name` currently names an annotative text style.
///
/// Creation paths use this to stamp a new TEXT/MTEXT with its own annotation
/// context. Render-time detection deliberately does not use it: an existing
/// non-annotative object may still reference a style later made annotative.
pub fn text_style_is_annotative(doc: &CadDocument, name: &str) -> bool {
doc.text_styles
.iter()
.find(|s| name_matches(&s.name, name))
.is_some_and(|s| s.annotative)
}
pub fn dim_style_is_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<Handle>) -> 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<Handle>) -> bool {
let Some(handle) = handle else {
return false;
};
doc.objects.iter().any(|(object_handle, object)| {
matches!(object, ObjectType::TableStyle(style) if *object_handle == handle && style.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
/// non-empty. This catches objects that are annotative by context even when
/// their style is not.
///
/// The non-empty requirement matters: a context manager with an *empty*
/// `ACDB_ANNOTATIONSCALES` is a single-representation marker with no per-scale
/// reps (common in files where objects were flagged annotative but never given
/// a scale). Such an object has nothing to scale *to*, so it must render at its
/// base geometry — treating it as annotative would (mis)scale it by the
/// annotation factor in annotation-scaled viewports, ballooning the text.
fn has_context_manager(doc: &CadDocument, common: &EntityCommon) -> bool {
let key = |d: &Dictionary, name: &str| {
d.entries
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, h)| *h)
};
let Some(xd) = common.xdictionary_handle.and_then(|h| as_dict(doc, h)) else {
return false;
};
let Some(mgr) = key(xd, "AcDbContextDataManager").and_then(|h| as_dict(doc, h)) else {
return false;
};
key(mgr, "ACDB_ANNOTATIONSCALES")
.and_then(|h| as_dict(doc, h))
.map(|coll| !coll.entries.is_empty())
.unwrap_or(false)
}
/// Whether a MULTILEADER participates in annotation scaling through its
/// 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
}
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)
}
EntityType::Table(table) => table_style_annotative(doc, table.table_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.context.arrowhead_size = style.arrowhead_size;
entity.context.landing_gap = style.landing_gap;
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;
for root in &mut entity.context.leader_roots {
root.landing_distance = style.landing_distance;
root.text_attachment_direction = entity.text_attachment_direction;
for line in &mut root.lines {
if line.override_flags.is_empty() {
line.path_type = entity.path_type;
line.line_color = style.line_color;
line.line_type_handle = style.line_type_handle;
line.line_weight = style.line_weight;
line.arrowhead_handle = style.arrowhead_handle;
line.arrowhead_size = style.arrowhead_size;
}
}
}
}
pub fn apply_mleader_style_to_object(
doc: &mut CadDocument,
handle: Handle,
style: &acadrust::objects::MultiLeaderStyle,
) -> bool {
let Some(EntityType::MultiLeader(original)) = doc.get_entity(handle).cloned() else {
return false;
};
let mut styled = original.clone();
apply_mleader_style(&mut styled, style);
if let Some(EntityType::MultiLeader(entity)) = doc.get_entity_mut(handle) {
*entity = styled;
}
let leaf_handles: Vec<_> = annotation_scales_dict(doc, handle)
.and_then(|collection| as_dict(doc, collection))
.map(|collection| collection.entries.iter().map(|(_, leaf)| *leaf).collect())
.unwrap_or_default();
for leaf_handle in leaf_handles {
let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get_mut(&leaf_handle) else {
continue;
};
let ObjectContextKind::MLeader(context) = &mut leaf.kind else {
continue;
};
let context_scale = context.scale_factor;
let text_height_ratio = if original.text_height.abs() > 1.0e-12 {
context.text_height / original.text_height
} else {
1.0
};
let mut per_scale = original.clone();
per_scale.context.clone_from(context);
apply_mleader_style(&mut per_scale, style);
per_scale.context.scale_factor = context_scale;
if style.text_height > 0.0 && text_height_ratio.is_finite() {
per_scale.context.text_height = style.text_height * text_height_ratio;
}
context.clone_from(&per_scale.context);
}
true
}
pub fn update_entity_from_annotation_style(
doc: &mut CadDocument,
handle: Handle,
current_scale: Option<Handle>,
) -> bool {
enum StyleUpdate {
Text { annotative: bool, height: f64 },
Dimension { annotative: bool },
MultiLeader(acadrust::objects::MultiLeaderStyle),
ContextOnly,
}
let Some(entity) = doc.get_entity(handle) else {
return false;
};
let update = match entity {
EntityType::Text(text) => doc.text_styles.get(&text.style).map(|style| {
StyleUpdate::Text {
annotative: style.annotative,
height: style.height,
}
}),
EntityType::MText(text) => doc.text_styles.get(&text.style).map(|style| {
StyleUpdate::Text {
annotative: style.annotative,
height: style.height,
}
}),
EntityType::AttributeEntity(attribute) => doc
.text_styles
.get(&attribute.text_style)
.map(|style| StyleUpdate::Text {
annotative: style.annotative,
height: style.height,
}),
EntityType::AttributeDefinition(attribute) => doc
.text_styles
.get(&attribute.text_style)
.map(|style| StyleUpdate::Text {
annotative: style.annotative,
height: style.height,
}),
EntityType::Dimension(dimension) => doc
.dim_styles
.get(&dimension.base().style_name)
.map(|style| StyleUpdate::Dimension {
annotative: style.annotative,
}),
EntityType::Leader(leader) => doc
.dim_styles
.get(&leader.dimension_style)
.map(|style| StyleUpdate::Dimension {
annotative: style.annotative,
}),
EntityType::Tolerance(tolerance) => doc
.dim_styles
.get(&tolerance.dimension_style_name)
.map(|style| StyleUpdate::Dimension {
annotative: style.annotative,
}),
EntityType::MultiLeader(leader) => leader.style_handle.and_then(|style_handle| {
match doc.objects.get(&style_handle) {
Some(ObjectType::MultiLeaderStyle(style)) => {
Some(StyleUpdate::MultiLeader(style.clone()))
}
_ => None,
}
}),
_ if is_annotative(doc, entity) => Some(StyleUpdate::ContextOnly),
_ => None,
};
let Some(update) = update else {
return false;
};
let annotative = match update {
StyleUpdate::Text { annotative, height } => {
if height > 0.0 {
if let Some(entity) = doc.get_entity_mut(handle) {
match entity {
EntityType::Text(text) => text.height = height,
EntityType::MText(text) => text.height = height,
EntityType::AttributeEntity(attribute) => attribute.height = height,
EntityType::AttributeDefinition(attribute) => attribute.height = height,
_ => {}
}
}
}
annotative
}
StyleUpdate::Dimension { annotative } => annotative,
StyleUpdate::MultiLeader(style) => {
apply_mleader_style_to_object(doc, handle, &style);
style.is_annotative
}
StyleUpdate::ContextOnly => return true,
};
set_entity_annotative(doc, handle, annotative);
if annotative {
if let Some(scale) = current_scale {
create_annotation_context(doc, handle, scale);
}
}
true
}
/// Whether an entity participates in annotation scaling.
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;
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.
// 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::AttributeEntity(attribute) => attribute.flags.annotative,
EntityType::AttributeDefinition(attribute) => attribute.flags.annotative,
EntityType::MultiLeader(ml) => mleader_is_annotative(doc, ml),
_ => false,
}
}