diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 8305462f0..4be67b1c7 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1320,7 +1320,7 @@ impl MultiLeaderTess for MultiLeader { // ml.scale_factor is always applied; anno_scale is only applied when the // multileader is marked annotative. let effective_scale = (ml.scale_factor as f32) - * if ml.enable_annotation_scale { + * if crate::scene::annotative::mleader_is_annotative(document, ml) { anno_scale } else { 1.0 diff --git a/src/scene/annotative.rs b/src/scene/annotative.rs index 39f73b66e..4ba9554ac 100644 --- a/src/scene/annotative.rs +++ b/src/scene/annotative.rs @@ -377,6 +377,80 @@ pub fn active_object_context( 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( + doc: &CadDocument, + entity: &EntityType, + fallback: f32, +) -> f32 { + if !is_annotative(doc, entity) { + return 1.0; + } + + // 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(doc, entity.common().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(doc, entity.common().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 { @@ -1030,6 +1104,17 @@ fn has_context_manager(doc: &CadDocument, common: &EntityCommon) -> bool { .unwrap_or(false) } +/// Whether a MULTILEADER participates in annotation scaling through its +/// per-object context, entity flag, or assigned annotative style. +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) +} + /// Whether an entity participates in annotation scaling. pub fn is_annotative(doc: &CadDocument, entity: &EntityType) -> bool { // Per-object annotation context (works regardless of style). @@ -1049,9 +1134,7 @@ pub fn is_annotative(doc: &CadDocument, entity: &EntityType) -> bool { 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::MultiLeader(ml) => { - ml.enable_annotation_scale || mleader_style_annotative(doc, ml.style_handle) - } + 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/convert/tess.rs b/src/scene/convert/tess.rs index 32eee54de..32f463a96 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -361,6 +361,20 @@ pub(crate) fn tessellate_entity( let e = contextual.as_ref(); let h = e.common().handle; let sel = selected.contains(&h); + // Per-object annotation contexts store each representation relative to the + // native/default scale. Resolve that ratio once before TEXT/MTEXT, + // DIMENSION, and MULTILEADER reach their independent tessellation paths. + let anno_scale = if matches!( + e, + EntityType::Text(_) + | EntityType::MText(_) + | EntityType::Dimension(_) + | EntityType::MultiLeader(_) + ) { + crate::scene::annotative::effective_annotation_scale(document, e, anno_scale) + } else { + anno_scale + }; // Frustum + LOD cull for non-Insert, non-Viewport entities. Insert is // handled separately (its WCS bbox depends on the block defn AABB × diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 7ad3bf40d..84a2f2c86 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -156,11 +156,8 @@ 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 = if crate::scene::annotative::is_annotative(document, entity) { - anno_scale - } else { - 1.0 - }; + let anno_scale = + crate::scene::annotative::effective_annotation_scale(document, entity, anno_scale); // 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