From 62876b108124744adf974c1f6e8884c3dc38d4f5 Mon Sep 17 00:00:00 2001 From: Hakan Date: Thu, 21 May 2026 12:38:03 +0300 Subject: [PATCH] fix(text): integrate ignored fields across text-bearing entities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MultiLeader: stop double-scaling when ctx.text_height (WCS-baked) is set; use it as-is and only multiply by scale_factor + anno on the fallback path. ctx.text_width gets the same treatment. - Text / Attribute: width_factor and oblique_angle are entity-authoritative. The previous entity*style multiplication / addition mis-rendered any text whose TXSTYLE had a non-default width_factor or oblique_angle (RR 0.6, STYLE2 15°, Romans/ARIEL/A32 0.65-0.8 in the sample file). - MText: honour TXSTYLE is_backward (negative width_factor mirror) and is_upside_down (180° rotation). measure_mtext_chars uses positive magnitude so anchor math stays correct under backward text. - Tolerance: use tol.dimension_gap when set instead of the hard-coded 0.35 * height heuristic. - Table: cell text height / font / alignment resolve through CellContent -> CellStyle -> TableStyle.{title|header|data}_row_style -> compiled-in defaults. Row classification respects the suppressed flags. Cell + content rotation applied. Co-Authored-By: Claude Opus 4.7 --- src/entities/attribute.rs | 21 +++++-- src/entities/mtext.rs | 20 +++++-- src/entities/multileader.rs | 38 +++++++++---- src/entities/table.rs | 107 ++++++++++++++++++++++++++++++------ src/entities/text.rs | 19 +++++-- src/entities/tolerance.rs | 8 ++- src/scene/tessellate.rs | 40 ++++++++++---- 7 files changed, 198 insertions(+), 55 deletions(-) diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs index d4914028c..020785a0a 100644 --- a/src/entities/attribute.rs +++ b/src/entities/attribute.rs @@ -111,11 +111,16 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) let resolved = resolve_text_style(input.text_style, document); - // width_factor sign also flips on text_generation_flags bit 2 (backward). - // The TextStyle's own is_backward flag is multiplicative — if both are set - // they cancel (mirror twice → forward), matching AutoCAD. - let base_wf = - ((input.width_factor as f32).max(0.01) * resolved.width_factor.max(0.01)).clamp(0.01, 100.0); + // The entity stores the FINAL width factor / oblique angle (same rule + // as TEXT). Use it as-is; fall back to the style only when the parser + // reports a default-omitted 0.0. + let base_wf = if input.width_factor.abs() > 1e-9 { + (input.width_factor as f32).clamp(0.01, 100.0) + } else { + resolved.width_factor.max(0.01) + }; + // text_generation_flags bit 2 (backward) flips width-factor sign; the + // TextStyle's own is_backward is XOR-combined so mirror-twice cancels. let attr_backward = (input.text_generation_flags & 2) != 0; let mut width_factor = base_wf; if attr_backward ^ resolved.is_backward { @@ -132,7 +137,11 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) } else { input.rotation as f32 }; - let oblique_angle = input.oblique_angle as f32 + resolved.oblique_angle; + let oblique_angle = if input.oblique_angle.abs() > 1e-9 { + input.oblique_angle as f32 + } else { + resolved.oblique_angle + }; // Anchor selection mirrors Text: only Left/Baseline uses insertion_point; // every other alignment uses alignment_point. diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index f2de20b73..cf18f2806 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -79,12 +79,19 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity { let resolved_style = resolve_text_style(&t.style, document); let font_name = resolved_style.font_name; let font = cxf::get_font(&font_name); - let style_width_factor = resolved_style.width_factor.max(0.01); + // MText has no entity-level width/oblique override (those are MText format + // codes \W and \Q inside the value string, handled by `strip_mtext_codes`). + // Default rendering inherits both from the style. `is_backward` flips the + // width-factor sign so text reads right-to-left. + let base_wf = resolved_style.width_factor.max(0.01); + let style_width_factor = if resolved_style.is_backward { -base_wf } else { base_wf }; let style_oblique = resolved_style.oblique_angle; let plain = strip_mtext_codes(&t.value); let explicit_lines = split_mtext_lines(&plain); let lines: Vec = if t.rectangle_width > 0.0 { - let scale = t.height as f32 / 9.0 * style_width_factor; + // Measurement uses the magnitude only; the sign on style_width_factor + // is a render-time mirror flag, not a width scalar. + let scale = t.height as f32 / 9.0 * style_width_factor.abs(); let max_w = t.rectangle_width as f32; explicit_lines .iter() @@ -131,7 +138,12 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity { _ => 0.0, }; let vertical_text = matches!(t.drawing_direction, DrawingDirection::TopToBottom); - let rot = t.rotation as f32; + // is_upside_down flips the text 180° around the insertion point. + let rot = if resolved_style.is_upside_down { + t.rotation as f32 + std::f32::consts::PI + } else { + t.rotation as f32 + }; let (cos_r, sin_r) = (rot.cos(), rot.sin()); let ins_x = t.insertion_point.x; let ins_y = t.insertion_point.y; @@ -151,7 +163,7 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity { (line_y * (-sin_r), line_y * cos_r) }; let line_w = if h_anchor > 0.0 { - let scale = t.height as f32 / 9.0 * style_width_factor; + let scale = t.height as f32 / 9.0 * style_width_factor.abs(); measure_mtext_chars(line, scale, font) } else { 0.0 diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 14cbc7601..77dbc8a79 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -187,13 +187,14 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option 0.0 { - ctx.text_height - } else { - ml.text_height - } as f32; + // `ctx.text_height` (when > 0) is already in WCS — scale_factor is + // baked in. Only the fallback path needs to multiply. let scale_factor = ml.scale_factor as f32; - let height = raw_height * scale_factor; + let height = if ctx.text_height > 0.0 { + ctx.text_height as f32 + } else { + ml.text_height as f32 * scale_factor + }; let ins = &ctx.text_location; let ins_x = ins.x; @@ -202,12 +203,11 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option 1e-9 || td.y.abs() > 1e-9 { + let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 { (td.y as f32).atan2(td.x as f32) } else { ctx.text_rotation as f32 }; - let (cos_r, sin_r) = (rot.cos(), rot.sin()); snap_pts.push(node([ins_x, ins_y, z])); // Resolve text style via handle when available, falling back to STANDARD. @@ -225,16 +225,28 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option = if ctx.text_width > 0.0 { - let scale = height / 9.0 * width_factor; - let max_w = ctx.text_width as f32 * scale_factor; + // Width-factor sign is a render-time mirror flag; measurement uses + // the magnitude only. + let scale = height / 9.0 * width_factor.abs(); + // ctx.text_width is in the same WCS frame as ctx.text_height — + // do not re-multiply by scale_factor. + let max_w = ctx.text_width as f32; explicit_lines .iter() .flat_map(|line| { @@ -261,7 +273,9 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option Vec3 { } impl TruckConvertible for Table { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { + fn to_truck(&self, document: &acadrust::CadDocument) -> Option { if self.rows.is_empty() || self.columns.is_empty() { return None; } @@ -121,9 +121,42 @@ impl TruckConvertible for Table { // is gone — col/row offsets still feed cell drawing below. let _ = (total_w, total_h); - // Cell text — lifted into Lines points via 2D strokes - let text_height = 0.18_f32; - let margin = text_height * 0.5_f32; + // Cell text — resolve defaults via TableStyle, then layer per-cell + // overrides on top. Resolution order (text height, text style, alignment): + // 1. CellContent.* (per-content explicit override) + // 2. CellStyle.* (per-cell explicit override) + // 3. TableStyle._row_style.* (table-wide default for this row class) + // 4. compiled-in fallback (0.18 / "txt" / MiddleCenter) + // + // Row classification: row 0 is Title (when not suppressed), row 1 is + // Header (when not suppressed), everything else is Data. The two + // suppressed flags shift the leading rows down to Data. + let lookup_style = |h: acadrust::Handle| -> Option<&acadrust::tables::TextStyle> { + document.text_styles.iter().find(|s| s.handle == h) + }; + let table_style: Option<&acadrust::objects::TableStyle> = + self.table_style_handle.and_then(|h| { + document.objects.get(&h).and_then(|obj| match obj { + acadrust::objects::ObjectType::TableStyle(ts) => Some(ts), + _ => None, + }) + }); + let title_suppressed = table_style.map(|t| t.title_suppressed).unwrap_or(false); + let header_suppressed = table_style.map(|t| t.header_suppressed).unwrap_or(false); + + let font_for_handle = |handle: Option| -> Option { + handle.and_then(|h| lookup_style(h)).and_then(|s| { + let file = s.font_file.trim(); + if !file.is_empty() { + let basename = file.rsplit(['/', '\\']).next().unwrap_or(file); + let stem = basename.split('.').next().unwrap_or(basename).trim(); + if !stem.is_empty() { + return Some(stem.to_string()); + } + } + None + }) + }; for (ri, row) in self.rows.iter().enumerate() { let row_top = row_offsets[ri]; @@ -133,6 +166,22 @@ impl TruckConvertible for Table { .unwrap_or(row_top + row.height as f32); let row_mid = (row_top + row_bot) * 0.5; + // Pick the appropriate row_style from TableStyle for this row's role. + let row_style: Option<&acadrust::objects::RowCellStyle> = table_style + .map(|ts| { + let kind = match (title_suppressed, header_suppressed, ri) { + (false, _, 0) => 0, // title + (false, false, 1) => 1, // header + (true, false, 0) => 1, // header pulled up + _ => 2, // data + }; + match kind { + 0 => &ts.title_row_style, + 1 => &ts.header_row_style, + _ => &ts.data_row_style, + } + }); + for (ci, cell) in row.cells.iter().enumerate() { let text = cell.text_value(); if text.is_empty() { @@ -143,16 +192,39 @@ impl TruckConvertible for Table { let col_width = self.columns.get(ci).map(|c| c.width as f32).unwrap_or(1.0); let col_right = col_left + col_width; - // Alignment: CellStyle.alignment i32 encodes 1-9 (AutoCAD convention): - // 1=TopLeft 2=TopCenter 3=TopRight - // 4=MiddleLeft 5=MiddleCenter 6=MiddleRight - // 7=BottomLeft 8=BottomCenter 9=BottomRight - // 0/default = MiddleCenter (5) - let align = cell.style.as_ref().map_or(5, |s| s.alignment); + // Resolve text height: content → cell-style → row-style → 0.18. + let content = cell.contents.first(); + let cell_h = content + .map(|c| c.text_height) + .filter(|h| *h > 1e-6) + .or_else(|| cell.style.as_ref().map(|s| s.text_height).filter(|h| *h > 1e-6)) + .or_else(|| row_style.map(|s| s.text_height).filter(|h| *h > 1e-6)) + .map(|h| h as f32) + .unwrap_or(0.18); + let margin = cell_h * 0.5_f32; + + // Resolve text-style handle: content → cell-style → row-style. + let style_handle = content + .and_then(|c| c.text_style_handle) + .or_else(|| cell.style.as_ref().and_then(|s| s.text_style_handle)) + .or_else(|| row_style.and_then(|s| s.text_style_handle)); + let font_owned = + font_for_handle(style_handle).unwrap_or_else(|| "txt".to_string()); + let font_name: &str = &font_owned; + + // Alignment resolution: cell.style.alignment (1-9) overrides; + // otherwise fall back to row_style.alignment, then MiddleCenter. + let align = cell + .style + .as_ref() + .map(|s| s.alignment) + .filter(|a| *a != 0) + .or_else(|| row_style.map(|s| s.alignment as i32)) + .unwrap_or(5); let horiz = ((align - 1).rem_euclid(3)) + 1; // 1=left, 2=center, 3=right let vert = ((align - 1) / 3) + 1; // 1=top, 2=middle, 3=bottom - let text_w = cxf::measure_text(text, text_height, 1.0, "txt"); + let text_w = cxf::measure_text(text, cell_h, 1.0, font_name); let x_offset = match horiz { 1 => col_left + margin, // left @@ -161,19 +233,22 @@ impl TruckConvertible for Table { }; let y_offset = match vert { 1 => row_top + margin, // top - 3 => row_bot - margin - text_height, // bottom - _ => row_mid - text_height * 0.5, // middle (default) + 3 => row_bot - margin - cell_h, // bottom + _ => row_mid - cell_h * 0.5, // middle (default) }; let text_origin = origin + h * x_offset + v_down * y_offset; + // Content rotation (radians) on top of table cell rotation. + let rot = content.map(|c| c.rotation as f32).unwrap_or(0.0) + + cell.rotation as f32; let strokes = cxf::tessellate_text_ex( [text_origin.x, text_origin.y], - text_height, - 0.0, + cell_h, + rot, 1.0, 0.0, - "txt", + font_name, text, ); for stroke in strokes { diff --git a/src/entities/text.rs b/src/entities/text.rs index 8529f0851..69c833ff7 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -61,12 +61,15 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { let snap_pt = Vec3::new(wsx as f32, wsy as f32, wsz as f32); let resolved_style = resolve_text_style(&t.style, document); let font_name = resolved_style.font_name; - let base_wf = (if t.width_factor > 0.0 { - t.width_factor as f32 + // AutoCAD text geometry rule: the entity stores the FINAL width factor / + // oblique angle, copied from the style at creation and persisting through + // style edits. Use it as-is. Only fall back to the style when the entity + // value is missing (the parser reports 0.0 for default-omitted fields). + let base_wf = if t.width_factor.abs() > 1e-9 { + (t.width_factor as f32).clamp(0.01, 100.0) } else { - 1.0 - } * resolved_style.width_factor.max(0.01)) - .clamp(0.01, 100.0); + resolved_style.width_factor.max(0.01) + }; // is_backward mirrors text left-right via negative width factor. let width_factor = if resolved_style.is_backward { -base_wf @@ -79,7 +82,11 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { } else { t.rotation as f32 }; - let oblique_angle = t.oblique_angle as f32 + resolved_style.oblique_angle; + let oblique_angle = if t.oblique_angle.abs() > 1e-9 { + t.oblique_angle as f32 + } else { + resolved_style.oblique_angle + }; let anchor = match ( &t.horizontal_alignment, &t.vertical_alignment, diff --git a/src/entities/tolerance.rs b/src/entities/tolerance.rs index 1e276192e..5a96df070 100644 --- a/src/entities/tolerance.rs +++ b/src/entities/tolerance.rs @@ -130,7 +130,13 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec> { } else { 2.5_f32 }; - let gap = (h * 0.35).max(0.1); + // DIMGAP — stored on the entity (resolved from the dim style at creation). + // Fall back to AutoCAD's 0.35 × height convention only when missing. + let gap = if tol.dimension_gap > 1e-6 { + tol.dimension_gap as f32 + } else { + (h * 0.35).max(0.1) + }; let cell_h = h + 2.0 * gap; let char_w = h * 0.65; let min_cell_w = h * 1.4; diff --git a/src/scene/tessellate.rs b/src/scene/tessellate.rs index 1cf072774..8c6f7f692 100644 --- a/src/scene/tessellate.rs +++ b/src/scene/tessellate.rs @@ -1552,12 +1552,18 @@ pub fn tessellate_multileader( // text_left_attachment (vertical), with text_rotation/text_direction applied. if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() { let ctx = &ml.context; - let raw_height = if ctx.text_height > 0.0 { - ctx.text_height + // `ctx.text_height` (when > 0) is the already-resolved WCS text height + // stored in the per-instance context — style × scale_factor × anno + // scale are all already baked in. Multiplying by `effective_scale` + // again would double-scale (e.g., a context height of 100 in a file + // with scale_factor=20 was rendering at 2000 units — 20× too big). + // Only the fallback path (file omits the context value) needs + // scale_factor + annotation scale applied. + let height = if ctx.text_height > 0.0 { + ctx.text_height as f32 } else { - ml.text_height - } as f32; - let height = raw_height * effective_scale; + ml.text_height as f32 * effective_scale + }; let ins = &ctx.text_location; // Subtract world_offset in f64 before casting to f32: drawings often @@ -1595,7 +1601,6 @@ pub fn tessellate_multileader( if ml.text_direction_negative { rot += std::f32::consts::PI; } - let (cos_r, sin_r) = (rot.cos(), rot.sin()); // Resolve text style via handle when available, falling back to STANDARD. let style_name = ctx @@ -1612,16 +1617,29 @@ pub fn tessellate_multileader( let style = crate::entities::text_support::resolve_text_style(&style_name, document); let font_name = style.font_name; let font = crate::scene::cxf::get_font(&font_name); - let width_factor = style.width_factor.max(0.01); + // MultiLeader text takes its width factor / oblique from the style + // (no entity-level override exists for MText-content multileaders). + // is_backward flips the sign on width_factor; is_upside_down adds π + // to the text rotation — same convention as Text / MText. + let base_wf = style.width_factor.max(0.01); + let width_factor = if style.is_backward { -base_wf } else { base_wf }; let oblique = style.oblique_angle; + if style.is_upside_down { + rot += std::f32::consts::PI; + } + let (cos_r, sin_r) = (rot.cos(), rot.sin()); // Strip MText format codes (e.g. `{\fArial Black|b0|i0|c162|p34;...}`), // then split on \P / \n / \N and optionally word-wrap to text_width. let plain = crate::entities::text_support::strip_mtext_codes(&ctx.text_string); let explicit_lines = crate::entities::text_support::split_mtext_lines(&plain); let lines: Vec = if ctx.text_width > 0.0 { - let scale = height / 9.0 * width_factor; - let max_w = ctx.text_width as f32 * effective_scale; + // Measurement uses positive width magnitude; the sign on + // width_factor is a render-time mirror flag. + let scale = height / 9.0 * width_factor.abs(); + // ctx.text_width lives in the same WCS frame as ctx.text_height; + // it does not need a second multiplication by effective_scale. + let max_w = ctx.text_width as f32; explicit_lines .iter() .flat_map(|line| { @@ -1676,7 +1694,9 @@ pub fn tessellate_multileader( }; let v_offset = v_offset_for_attachment(vertical_attach, n_lines, height, line_h); - let scale = height / 9.0 * width_factor; + // line_widths use positive magnitude for measurement; tessellate_text_ex + // gets the signed width_factor below to honour the backward flag. + let scale = height / 9.0 * width_factor.abs(); let line_widths: Vec = lines .iter() .map(|line| crate::entities::text_support::measure_mtext_chars(line, scale, font))