From ea87b3db762e0ee9d842ff1d89ca9ed13368b376 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 14 May 2026 20:49:33 +0300 Subject: [PATCH] fix(scene): greek text OBB honors attachment point, propagates into blocks Top-level OBB used insertion_point as anchor and hardcoded MText anchor to bottom-left, so greeked rects landed on the opposite side of the actual text whenever MText used a non-default attachment_point or TEXT used a non-Left/Baseline alignment. Block-defn subs never set or read `text_obb_local`, so greeked text inside Inserts fell back to the axis-aligned AABB and ignored entity rotation entirely. - Share OBB corner computation via `text_obb_corners_native`. - Map all 9 MText AttachmentPoint variants to (h_anchor, v_anchor). - For TEXT, anchor on `alignment_point` (DXF 11) when alignment != Left+Baseline. - Populate `LocalWire.text_obb_local` in `tessellate_sub_local`. - `emit_greeked_text` transforms OBB corners by `accum_xform`, falls back to AABB only when no OBB was precomputed. Co-Authored-By: Claude Opus 4.7 --- src/scene/block_cache.rs | 69 ++++++++++++++++++++++------- src/scene/mod.rs | 95 +++++++++++++++++++++++++++++----------- 2 files changed, 122 insertions(+), 42 deletions(-) diff --git a/src/scene/block_cache.rs b/src/scene/block_cache.rs index 65824a6f0..482533e6d 100644 --- a/src/scene/block_cache.rs +++ b/src/scene/block_cache.rs @@ -304,6 +304,19 @@ fn tessellate_sub_local( _ => None, }; + // Pre-compute the OBB corners (block-local f32) for Text / MText so the + // greek path can emit a rect that matches the entity's rotation instead + // of falling back to the axis-aligned `aabb_local`. + let text_obb_local: Option<[[f32; 3]; 4]> = + crate::scene::text_obb_corners_native(sub, anno_scale).map(|c| { + [ + [c[0][0] as f32, c[0][1] as f32, c[0][2] as f32], + [c[1][0] as f32, c[1][1] as f32, c[1][2] as f32], + [c[2][0] as f32, c[2][1] as f32, c[2][2] as f32], + [c[3][0] as f32, c[3][1] as f32, c[3][2] as f32], + ] + }); + Some(LocalWire { points: wire.points, key_vertices: wire.key_vertices, @@ -321,6 +334,7 @@ fn tessellate_sub_local( lw_is_byblock, aabb_local, text_height_local, + text_obb_local, }) } @@ -483,21 +497,44 @@ pub fn expand_insert( Some(batches.finalize(&name, selected)) } -/// Emit a greeked rectangle for a text LocalWire — same color, AABB-sized -/// fill, no per-glyph stroke geometry. Mirrors the top-level text greek in -/// scene/mod.rs (including the 0.45 dim pre-boost the face3d pipeline -/// applies to fill_tris). -fn emit_greeked_text(lw: &LocalWire, local_aabb: [f32; 4], ctx: &ExpandCtx, out: &mut Batches) { - let [x0, y0, x1, y1] = local_aabb; - let z = 0.0_f32; - let tris = [ - [x0, y0, z], - [x1, y0, z], - [x1, y1, z], - [x0, y0, z], - [x1, y1, z], - [x0, y1, z], - ]; +/// Emit a greeked rectangle for a text LocalWire — same color, OBB-sized +/// fill (rotated to match the entity), no per-glyph stroke geometry. +/// Mirrors the top-level text greek in scene/mod.rs (including the 0.45 +/// dim pre-boost the face3d pipeline applies to fill_tris). Falls back to +/// the axis-aligned `local_aabb` when no OBB was precomputed. +fn emit_greeked_text( + lw: &LocalWire, + local_aabb: [f32; 4], + accum_xform: &Transform, + ctx: &ExpandCtx, + out: &mut Batches, +) { + let [ox, oy, oz] = ctx.world_offset; + let tris: [[f32; 3]; 6] = if let Some(obb) = lw.text_obb_local { + // Transform block-local corners → world via accum_xform, then + // subtract world_offset so the fill_tris sit in the same f32 space + // as everything else in the batch. + let xf = |p: [f32; 3]| -> [f32; 3] { + let w = accum_xform.apply(Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64)); + [(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32] + }; + let p00 = xf(obb[0]); + let p10 = xf(obb[1]); + let p11 = xf(obb[2]); + let p01 = xf(obb[3]); + [p00, p10, p11, p00, p11, p01] + } else { + let [x0, y0, x1, y1] = local_aabb; + let z = 0.0_f32; + [ + [x0, y0, z], + [x1, y0, z], + [x1, y1, z], + [x0, y0, z], + [x1, y1, z], + [x0, y1, z], + ] + }; let final_color = if ctx.selected { WireModel::SELECTED @@ -734,7 +771,7 @@ fn expand_defn( continue; } if h_px < 4.0 { - emit_greeked_text(lw, local, ctx, out); + emit_greeked_text(lw, local, accum_xform, ctx, out); continue; } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index aff901f51..60a29b353 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -4191,18 +4191,19 @@ fn tessellate_entity( vec![base] } -/// Build the 6-vertex (2-triangle) fill for a greeked Text / MText entity -/// in **world-offset-subtracted** f32 space, oriented to match the text's -/// rotation. Width is approximated from the glyph height × character count -/// (TEXT) or from `rectangle_width` (MTEXT). -fn text_greek_obb_tris( +/// Build the 4 OBB corners (CCW: bl, br, tr, tl) of a Text / MText entity +/// in its **native frame** — for top-level entities this is world coords, +/// for block-defn subs it's block-local. No offset/transform applied. +/// Width is approximated from glyph height × character count (TEXT) or +/// from `rectangle_width` (MTEXT). Returns `None` for non-text entities. +pub(crate) fn text_obb_corners_native( e: &EntityType, anno_scale: f32, - world_offset: [f64; 3], -) -> Vec<[f32; 3]> { - use acadrust::entities::{TextHorizontalAlignment, TextVerticalAlignment}; +) -> Option<[[f64; 3]; 4]> { + use acadrust::entities::{ + AttachmentPoint, TextHorizontalAlignment, TextVerticalAlignment, + }; - let [ox, oy, oz] = world_offset; let anno = anno_scale as f64; let (ix, iy, iz, w, h, rot, h_anchor, v_anchor) = match e { @@ -4225,10 +4226,24 @@ fn text_greek_obb_tris( TextVerticalAlignment::Middle => 0.5, TextVerticalAlignment::Top => 1.0, }; + // AutoCAD writes `alignment_point` (DXF 11) whenever the text + // isn't simple Left+Baseline; in that case it — not the + // insertion_point — is the anchor the alignment fractions map to. + let use_alignment_pt = !matches!( + (t.horizontal_alignment, t.vertical_alignment), + ( + TextHorizontalAlignment::Left, + TextVerticalAlignment::Baseline, + ) + ); + let anchor = match (use_alignment_pt, t.alignment_point) { + (true, Some(p)) => p, + _ => t.insertion_point, + }; ( - t.insertion_point.x, - t.insertion_point.y, - t.insertion_point.z, + anchor.x, + anchor.y, + anchor.z, w_world, h_world, t.rotation, @@ -4247,6 +4262,20 @@ fn text_greek_obb_tris( let total_h = m .rectangle_height .unwrap_or(h_world * lines.max(1.0) * m.line_spacing_factor.max(0.5)); + // MText `attachment_point` puts `insertion_point` at one of the + // 9 corners/midpoints of the text bbox. h_anchor / v_anchor are + // fractions from (left, bottom) of the bbox. + let (h_anchor, v_anchor) = match m.attachment_point { + AttachmentPoint::TopLeft => (0.0, 1.0), + AttachmentPoint::TopCenter => (0.5, 1.0), + AttachmentPoint::TopRight => (1.0, 1.0), + AttachmentPoint::MiddleLeft => (0.0, 0.5), + AttachmentPoint::MiddleCenter => (0.5, 0.5), + AttachmentPoint::MiddleRight => (1.0, 0.5), + AttachmentPoint::BottomLeft => (0.0, 0.0), + AttachmentPoint::BottomCenter => (0.5, 0.0), + AttachmentPoint::BottomRight => (1.0, 0.0), + }; ( m.insertion_point.x, m.insertion_point.y, @@ -4254,35 +4283,49 @@ fn text_greek_obb_tris( w_world, total_h, m.rotation, - 0.0, - 0.0, + h_anchor, + v_anchor, ) } - _ => return vec![], + _ => return None, }; - // Local-frame corners with anchor offsets applied. let x0 = -h_anchor * w; let x1 = (1.0 - h_anchor) * w; let y0 = -v_anchor * h; let y1 = (1.0 - v_anchor) * h; let (s, c) = (rot.sin(), rot.cos()); - let rot_pt = |lx: f64, ly: f64| -> [f32; 3] { + let rot_pt = |lx: f64, ly: f64| -> [f64; 3] { let rx = lx * c - ly * s; let ry = lx * s + ly * c; - [ - (ix + rx - ox) as f32, - (iy + ry - oy) as f32, - (iz - oz) as f32, - ] + [ix + rx, iy + ry, iz] }; - let p00 = rot_pt(x0, y0); - let p10 = rot_pt(x1, y0); - let p11 = rot_pt(x1, y1); - let p01 = rot_pt(x0, y1); + Some([ + rot_pt(x0, y0), + rot_pt(x1, y0), + rot_pt(x1, y1), + rot_pt(x0, y1), + ]) +} +/// 6-vertex (2-triangle) fill for a greeked top-level Text / MText, in +/// world-offset-subtracted f32 space. +fn text_greek_obb_tris( + e: &EntityType, + anno_scale: f32, + world_offset: [f64; 3], +) -> Vec<[f32; 3]> { + let Some(corners) = text_obb_corners_native(e, anno_scale) else { + return vec![]; + }; + let [ox, oy, oz] = world_offset; + let cast = |p: [f64; 3]| -> [f32; 3] { + [(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32] + }; + let [p00, p10, p11, p01] = corners; + let (p00, p10, p11, p01) = (cast(p00), cast(p10), cast(p11), cast(p01)); vec![p00, p10, p11, p00, p11, p01] }