feat(scene): integrate text LOD across every text-bearing entity

Moves the Dimension and Table baked-block paths up into
`tessellate_entity` so sub-Text/MText recurse through the same LOD
ladder (baseline / greek / full) the top-level entities already use,
and routes the synth-dim text path through `tessellate_entity` as well
so files that omit `block_name` (e.g. typical DXFs) also get LOD.

Extends the text-height LOD branch from Text/MText to
AttributeDefinition, AttributeEntity, and Tolerance — the OBB helpers
gained the same coverage so the baseline/greek rect lines up.

When a sub-entity inside a baked dim/table block has ByBlock colour,
the recursive call's resolved colour is replaced with the parent's so
dimension text no longer renders in the raw ByBlock fallback hue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
HakanandClaude Opus 4.7 committed 2026-05-21 15:29:57 +03:00
1 parent eab72062f9
commit 3fb23ae31b
2 files changed
+246 -76

No files matched your search

+206 -2
View File
@@ -4658,6 +4658,38 @@ fn clip_polyline_to_rect(
// requiring `Scene` (which contains `Rc<RefCell<...>>` and is `!Send`) to
// cross thread boundaries.
/// Tessellate a synthesised dimension-text entity through `tessellate_entity`
/// so it picks up the standard text LOD ladder (baseline / greek / full),
/// then re-color the returned wires with the dimension's resolved text colour
/// (so DIMCLRT / DIMSTYLE colours win over the synthetic Text's defaults).
pub(super) fn tessellate_entity_dim_text(
document: &acadrust::CadDocument,
selected: &HashSet<Handle>,
active_viewport: Option<Handle>,
world_offset: [f64; 3],
bg_color: [f32; 4],
anno_scale: f32,
e: &EntityType,
view_aabb: Option<[f32; 4]>,
world_per_pixel: Option<f32>,
text_color: [f32; 4],
) -> Vec<WireModel> {
let mut wires = tessellate_entity(
document, selected, active_viewport, world_offset, bg_color,
anno_scale, e, None, view_aabb, world_per_pixel,
);
for w in &mut wires {
// Synth dim text carries no real entity colour — paint everything
// (including greek-LOD fill tris which read `wire.color`) with the
// dim's text colour. Selection highlight already baked in by
// tessellate_entity, so leave that alone.
if !w.selected {
w.color = text_color;
}
}
wires
}
fn tessellate_entity(
document: &acadrust::CadDocument,
selected: &HashSet<Handle>,
@@ -4775,6 +4807,64 @@ fn tessellate_entity(
let pattern_length = pattern_length * pslt_factor;
let pattern = pattern.map(|v| v * pslt_factor);
// ── Dimension baked-block fast path ─────────────────────────────────────
//
// AutoCAD bakes each dimension's final geometry (extension lines, dim
// line, arrows, text MText) into a per-instance block — usually
// `*D<n>`, but custom names like `DIMBLOCK###-4NP` also occur. When the
// block exists we render its contents through `tessellate_entity` so
// sub-Text/MText get the standard baseline/greek/full LOD ladder, and
// DIMTXT × DIMSCALE isn't re-applied on already-baked geometry.
if let EntityType::Dimension(dim) = e {
let block_name = &dim.base().block_name;
if !block_name.trim().is_empty() {
if let Some(br) = document
.block_records
.iter()
.find(|br| br.name.eq_ignore_ascii_case(block_name))
{
if !br.entity_handles.is_empty() {
let mut wires: Vec<WireModel> =
Vec::with_capacity(br.entity_handles.len());
for &eh in &br.entity_handles {
let Some(sub) = document.get_entity(eh) else { continue };
// Sub-entities inside *D### / DIMBLOCK## blocks
// typically use ByBlock color/linetype/lineweight —
// they should inherit from the Dimension entity.
let sub_color_is_byblock =
sub.common().color == acadrust::types::Color::ByBlock;
let sub_wires = tessellate_entity(
document, selected, active_viewport, world_offset, bg_color,
// Block contents are baked at the final WCS size —
// don't let downstream paths re-apply anno_scale.
1.0, sub, block_cache, view_aabb, world_per_pixel,
);
for mut w in sub_wires {
w.name = h.value().to_string();
// Override ByBlock colour with the dim's resolved
// colour so text matches `DIMCLRT`-style behaviour
// (or layer colour) instead of the raw ByBlock
// fallback that render_style_for produces.
if sub_color_is_byblock {
w.color = if sel { WireModel::SELECTED } else { entity_color };
w.aci = aci;
}
wires.push(w);
}
}
if !wires.is_empty() {
let aabb = entity_aabb(e, world_offset);
for w in &mut wires {
w.aabb = aabb;
}
return wires;
}
}
}
}
// Fall through to the synthesis path below when no block is attached.
}
if let EntityType::Dimension(dim) = e {
let aabb = entity_aabb(e, world_offset);
let mut wires = tessellate::tessellate_dimension(
@@ -4786,6 +4876,11 @@ fn tessellate_entity(
line_weight_px,
world_offset,
anno_scale,
selected,
active_viewport,
bg_color,
view_aabb,
world_per_pixel,
);
for w in &mut wires {
w.aci = aci;
@@ -4835,12 +4930,18 @@ fn tessellate_entity(
Vec::with_capacity(br.entity_handles.len());
for &eh in &br.entity_handles {
let Some(sub) = document.get_entity(eh) else { continue };
let sub_color_is_byblock =
sub.common().color == acadrust::types::Color::ByBlock;
let sub_wires = tessellate_entity(
document, selected, active_viewport, world_offset, bg_color,
anno_scale, sub, block_cache, view_aabb, world_per_pixel,
);
for mut w in sub_wires {
w.name = h.value().to_string();
if sub_color_is_byblock {
w.color = if sel { WireModel::SELECTED } else { entity_color };
w.aci = aci;
}
wires.push(w);
}
}
@@ -5010,10 +5111,24 @@ fn tessellate_entity(
// < 1 px → baseline line in the text's color (text-here hint)
// 1–5 px → greeked OBB rect in the text's color
// ≥ 5 px → full per-glyph stroke tessellation
//
// Applies to every entity that is "primarily a piece of text" — Text,
// MText, ATTDEF, ATTRIB, Tolerance — so far-out drawings don't pay the
// full glyph-tessellation cost. Composite entities (Dimension, Table,
// MultiLeader) carry non-text geometry and have their own LOD paths.
if let Some(wpp) = world_per_pixel {
let text_height: Option<f64> = match e {
EntityType::Text(t) => Some(t.height * anno_scale as f64),
EntityType::MText(m) => Some(m.height * anno_scale as f64),
EntityType::AttributeDefinition(a) => Some(a.height * anno_scale as f64),
EntityType::AttributeEntity(a) => Some(a.height * anno_scale as f64),
EntityType::Tolerance(t) => {
// Tolerance text_height defaults to 0.18 from creation; treat
// 0 as missing and fall back to the AutoCAD default so the
// pixel check still kicks in for legitimately tiny dimensions.
let raw = if t.text_height > 0.0 { t.text_height } else { 2.5 };
Some(raw * anno_scale as f64)
}
_ => None,
};
if let Some(h_world) = text_height {
@@ -5155,12 +5270,33 @@ pub(crate) fn text_obb_corners_native(
anno_scale: f32,
mtext_lines_override: Option<usize>,
) -> Option<[[f64; 3]; 4]> {
use acadrust::entities::{
AttachmentPoint, TextHorizontalAlignment, TextVerticalAlignment,
use acadrust::entities::attribute_definition::{
HorizontalAlignment as AttrHA, VerticalAlignment as AttrVA,
};
use acadrust::entities::{AttachmentPoint, TextHorizontalAlignment, TextVerticalAlignment};
let anno = anno_scale as f64;
// Map Attribute alignment enums to the same h/v anchor fractions used by
// Text. Kept local because the Attribute enum is distinct from Text's.
let attr_h_anchor = |ha: AttrHA| -> f64 {
match ha {
AttrHA::Left => 0.0,
AttrHA::Center | AttrHA::Middle | AttrHA::Aligned | AttrHA::Fit => 0.5,
AttrHA::Right => 1.0,
}
};
let attr_v_anchor = |va: AttrVA| -> f64 {
match va {
AttrVA::Baseline | AttrVA::Bottom => 0.0,
AttrVA::Middle => 0.5,
AttrVA::Top => 1.0,
}
};
let attr_use_align_pt = |ha: AttrHA, va: AttrVA| -> bool {
!matches!((ha, va), (AttrHA::Left, AttrVA::Baseline))
};
let (ix, iy, iz, w, h, rot, h_anchor, v_anchor) = match e {
EntityType::Text(t) => {
let h_world = t.height * anno;
@@ -5206,6 +5342,62 @@ pub(crate) fn text_obb_corners_native(
v_anchor,
)
}
EntityType::AttributeDefinition(a) => {
let h_world = a.height * anno;
let w_factor = if a.width_factor > 0.0 { a.width_factor } else { 1.0 };
// Render the tag in preview when no default; matches `attribute.rs`.
let display = if a.default_value.is_empty() { &a.tag } else { &a.default_value };
let n = display.chars().count().max(1) as f64;
let w_world = n * h_world * w_factor * 0.6;
let h_anchor = attr_h_anchor(a.horizontal_alignment);
let v_anchor = attr_v_anchor(a.vertical_alignment);
let anchor = if attr_use_align_pt(a.horizontal_alignment, a.vertical_alignment) {
a.alignment_point
} else {
a.insertion_point
};
(
anchor.x, anchor.y, anchor.z, w_world, h_world, a.rotation, h_anchor, v_anchor,
)
}
EntityType::AttributeEntity(a) => {
let h_world = a.height * anno;
let w_factor = if a.width_factor > 0.0 { a.width_factor } else { 1.0 };
let n = a.value.chars().count().max(1) as f64;
let w_world = n * h_world * w_factor * 0.6;
let h_anchor = attr_h_anchor(a.horizontal_alignment);
let v_anchor = attr_v_anchor(a.vertical_alignment);
let anchor = if attr_use_align_pt(a.horizontal_alignment, a.vertical_alignment) {
a.alignment_point
} else {
a.insertion_point
};
(
anchor.x, anchor.y, anchor.z, w_world, h_world, a.rotation, h_anchor, v_anchor,
)
}
EntityType::Tolerance(t) => {
// Approximate: a feature control frame is roughly 1 line tall;
// width comes from char-count of the (already-stripped) text.
// GD&T symbols all advance one cell, so plain char count is close.
let raw_h = if t.text_height > 0.0 { t.text_height } else { 2.5 };
let h_world = raw_h * anno;
// Each cell ≈ 1.4 × height (matches `tolerance.rs` min_cell_w).
let n = t.text.chars().filter(|c| *c != '\n').count().max(1) as f64;
let w_world = h_world * 1.4 * n * 0.5; // rough — fine for LOD greek
// direction encodes rotation as a vector.
let rot = (t.direction.y).atan2(t.direction.x);
(
t.insertion_point.x,
t.insertion_point.y,
t.insertion_point.z,
w_world,
h_world * 1.5, // frame is taller than glyph cap by ~0.5 h
rot,
0.0, // anchored at insertion point (bottom-left)
0.0,
)
}
EntityType::MText(m) => {
let h_world = m.height * anno;
let raw_lines = (m.value.matches('\n').count() + 1) as f64;
@@ -5411,6 +5603,12 @@ fn text_baseline_points(
let line_h = match e {
EntityType::Text(t) => (t.height * anno_scale as f64) as f32,
EntityType::MText(m) => (m.height * anno_scale as f64) as f32,
EntityType::AttributeDefinition(a) => (a.height * anno_scale as f64) as f32,
EntityType::AttributeEntity(a) => (a.height * anno_scale as f64) as f32,
EntityType::Tolerance(t) => {
let raw = if t.text_height > 0.0 { t.text_height } else { 2.5 };
(raw * anno_scale as f64) as f32
}
_ => return vec![],
};
if line_h <= 0.0 {
@@ -5466,6 +5664,12 @@ fn text_greek_obb_tris(
let line_h = match e {
EntityType::Text(t) => (t.height * anno_scale as f64) as f32,
EntityType::MText(m) => (m.height * anno_scale as f64) as f32,
EntityType::AttributeDefinition(a) => (a.height * anno_scale as f64) as f32,
EntityType::AttributeEntity(a) => (a.height * anno_scale as f64) as f32,
EntityType::Tolerance(t) => {
let raw = if t.text_height > 0.0 { t.text_height } else { 2.5 };
(raw * anno_scale as f64) as f32
}
_ => return vec![],
};
if line_h <= 0.0 {
+40 -74
View File
@@ -462,60 +462,18 @@ pub fn tessellate_dimension(
line_weight_px: f32,
world_offset: [f64; 3],
anno_scale: f32,
// LOD hints — when present, synthesised dim text routes through the
// top-level LOD ladder (baseline / greek / full) instead of the truck
// path so far-out drawings collapse to a colored rect or baseline.
selected_set: &std::collections::HashSet<acadrust::Handle>,
active_viewport: Option<acadrust::Handle>,
bg_color: [f32; 4],
view_aabb: Option<[f32; 4]>,
world_per_pixel: Option<f32>,
) -> Vec<WireModel> {
let name = handle.value().to_string();
// ── Anonymous-block fast path ───────────────────────────────────────────
//
// AutoCAD bakes each dimension's final geometry (extension lines, dim
// line, arrows, text MText) into a per-instance block — usually named
// `*D<n>`, but some authoring tools use a custom name like
// `DIMBLOCK###-4NP`. When that block exists we render its contents
// directly: the text height inside the block is the FINAL displayed
// height (DIMTXT × DIMSCALE already applied at creation), so
// synthesising it from the dim style would double-apply DIMSCALE.
let dim_block_name = &dim.base().block_name;
if !dim_block_name.trim().is_empty() {
if let Some(br) = document
.block_records
.iter()
.find(|br| br.name.eq_ignore_ascii_case(dim_block_name))
{
if !br.entity_handles.is_empty() {
let mut wires: Vec<WireModel> =
Vec::with_capacity(br.entity_handles.len());
for &eh in &br.entity_handles {
let Some(sub) = document.get_entity(eh) else { continue };
// Inherit dim's color + line weight when the sub-entity is
// ByBlock; ByLayer stays on its own layer.
let sub_color = if selected {
WireModel::SELECTED
} else {
entity_color
};
let mut wire = tessellate(
document,
handle,
sub,
selected,
sub_color,
0.0,
[0.0; 8],
line_weight_px,
world_offset,
// Sub-entities are already baked at the final WCS
// size; don't re-apply anno_scale.
1.0,
);
wire.name = name.clone();
wires.push(wire);
}
if !wires.is_empty() {
return wires;
}
}
}
}
// (Baked-block fast path moved up into scene::tessellate_entity so the
// recursive call goes through the LOD ladder, not the truck path.)
let style_name = &dim.base().style_name;
let style = document.dim_styles.iter().find(|s| {
@@ -843,38 +801,46 @@ pub fn tessellate_dimension(
// Tolerance Text rendered separately so DIMTFAC scales its height
// and DIMTOLJ aligns it vertically against the primary text.
let tol_entity = dimension_tolerance_entity(dim, style, &text, dim_txt);
let mut wire = tessellate(
// Route synthesised dim text through tessellate_entity so the
// baseline/greek/full LOD ladder applies (zoom-out behaviour
// matches top-level Text). The text already has dim_scale baked
// into its height, so anno_scale stays 1.0.
let synth_text_entity = EntityType::Text(text);
let text_wires = crate::scene::tessellate_entity_dim_text(
document,
handle,
&EntityType::Text(text),
selected,
text_color,
0.0,
[0.0; 8],
line_weight_px,
selected_set,
active_viewport,
world_offset,
// dim text already baked dim_scale into its height — don't
// let the inner tessellate re-apply anno_scale.
bg_color,
1.0,
&synth_text_entity,
view_aabb,
world_per_pixel,
text_color,
);
wire.name = name.clone();
wires.push(wire);
for mut w in text_wires {
w.name = name.clone();
wires.push(w);
}
if let Some(tol_text) = tol_entity {
let mut tol_wire = tessellate(
let tol_entity_e = EntityType::Text(tol_text);
let tol_wires = crate::scene::tessellate_entity_dim_text(
document,
handle,
&EntityType::Text(tol_text),
selected,
text_color,
0.0,
[0.0; 8],
line_weight_px,
selected_set,
active_viewport,
world_offset,
bg_color,
1.0,
&tol_entity_e,
view_aabb,
world_per_pixel,
text_color,
);
tol_wire.name = name;
wires.push(tol_wire);
for mut w in tol_wires {
w.name = name.clone();
wires.push(w);
}
}
}