feat(annotative): scale text/dims/tables/blocks at the annotation scale + editable property pickers

Render: annotative-ness is resolved centrally in scene::annotative::is_annotative
(per-object context dictionary, legacy XDATA, or annotative style) so the bake and
the properties panel agree. Text, dimensions, tables and blocks now display at the
current annotation scale in model space. An annotative block scales as one uniform
unit about its insertion point — its internal geometry and attributes are carried by
that scale instead of being scaled individually, fixing the block-attribute size
regression. Tables scale their column/row/margin geometry.

Properties: the handle- and flag-backed rows are now editable pickers — MLEADER
multileader style / text style / arrowhead / leader linetype, General Material, and
Plot style (named plot-style mode only; the color-dependent mode stays read-only,
which is correct). A picked name is resolved back to its handle in the update loop,
where the document is in scope. Annotative Yes/No and the annotation-scale row are
shown per object.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-12 23:16:44 +03:00
1 parent 8ef65f59f8
commit 1b037a68aa
9 files changed
+539 -228

No files matched your search

+98
View File
@@ -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<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(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,
}
}
+21 -1
View File
@@ -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<Vec<WireModel>> {
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<String> = Vec::with_capacity(8);
+10 -1
View File
@@ -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
+10 -19
View File
@@ -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
+7 -6
View File
@@ -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