From 292e23f44f77e19cee17c7b717f426337e2a1f3b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 17 Jul 2026 18:26:29 +0300 Subject: [PATCH] fix(mtext): size `\S` fractions to fill and persist paragraph alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two layout fixes on the same MTEXT (budweiser2018.dwg handle 2609 / 8782). Fraction size: `\H0.7x;\S1/2;\H1.429x;` is how a 70 %-scaled fraction is spelled, so the 0.7 already *is* the fraction's size — the earlier constants shrank each half again on top of it and buried the stack inside one line. A half is now near the run height and the stack stands about twice that: the numerator rises past the cap and the denominator sits on the baseline, the way a fraction is meant to grow past its line rather than be squeezed into it. Alignment persistence: a `\p...;` block stays in force until the next `\p`, so a paragraph opened by a bare `\P` / `\N` inherits it. The parser records only the literal codes, so the second line of a centred or right column (`\pxqc;col2\P≡`) fell back to the left edge while its heading stayed aligned. Layout now carries a paragraph's whole property block forward to any paragraph that sets none of its own — measured, the column symbols land on the same centre / right edge as their headings. Kept in the layout adapter rather than the parser: acadrust faithfully records which paragraphs carry an explicit `\p`, so persistence is a rendering interpretation, and the literal record stays available to an editor. Co-Authored-By: Claude Opus 4.8 --- src/entities/text_support.rs | 83 +++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index 84d47411e..e4136e6b2 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -381,7 +381,7 @@ pub fn adapt_mtext_paragraphs( ) -> Vec { use acadrust::entities::mtext_format::{ parse_mtext, MTextColor, MTextLineAlignment, MTextParagraphAlignment, MTextScalar, - SpanProperties, StackingType, + ParagraphProperties, SpanProperties, StackingType, }; let entity_height = entity_height.max(1e-6); @@ -411,23 +411,54 @@ pub fn adapt_mtext_paragraphs( } } + // A `\p...;` block sets the paragraph's alignment/indents/tabs and stays in + // force until the next `\p` — a paragraph opened by a bare `\P` / `\N` + // inherits it. The parser records only the literal codes (a paragraph with + // no `\p` carries none), so persistence is applied here: without it, the + // second line of a centred or right column (`\pxqc;col2\P≡`) would fall back + // to the left edge while its heading stayed aligned. + let has_own_props = |p: &ParagraphProperties| { + p.alignment + .as_ref() + .is_some_and(|a| !matches!(a, MTextParagraphAlignment::Default)) + || p.first_line_indent.is_some() + || p.left_margin.is_some() + || p.right_margin.is_some() + || !p.tab_stops.is_empty() + }; + let mut lines: Vec = Vec::new(); + let mut carried: Option<(Option, f32, f32, f32, Vec)> = None; for para in &doc.paragraphs { let props = ¶.properties; + let (align, indent_first, indent_left, indent_right, tab_stops) = + if has_own_props(props) || carried.is_none() { + let v = ( + props.alignment.and_then(map_align), + props.first_line_indent.unwrap_or(0.0) as f32, + props.left_margin.unwrap_or(0.0) as f32, + props.right_margin.unwrap_or(0.0) as f32, + props + .tab_stops + .iter() + .map(|&p| TabStop { + position: p as f32, + kind: TabKind::Left, + }) + .collect::>(), + ); + carried = Some(v.clone()); + v + } else { + carried.clone().unwrap() + }; let mut line = MTextLine { - align: props.alignment.and_then(map_align), + align, starts_column: para.starts_column, - indent_first: props.first_line_indent.unwrap_or(0.0) as f32, - indent_left: props.left_margin.unwrap_or(0.0) as f32, - indent_right: props.right_margin.unwrap_or(0.0) as f32, - tab_stops: props - .tab_stops - .iter() - .map(|&p| TabStop { - position: p as f32, - kind: TabKind::Left, - }) - .collect(), + indent_first, + indent_left, + indent_right, + tab_stops, runs: Vec::new(), }; for span in ¶.spans { @@ -558,13 +589,25 @@ pub enum AtomKind { }, } +// A `\S` stack is measured against the height of the run it sits in, and that +// run has usually already been dropped for the purpose — `\H0.7x;\S1/2;\H1.429x;` +// is how a 70 %-scaled fraction is spelled, so the 0.7 *is* the fraction's size. +// Shrinking again on top of it is what buries the halves. +// +// So a half is near the full run height and the stack ends up about twice that: +// it is meant to grow past the line, numerator above the cap and denominator on +// the baseline — not to be squeezed into one line's worth of height. +// +// The three must stay ordered — denominator cap, then rule, then numerator +// baseline — or the rule cuts through a half. /// Each half of a `\S` stack, as a fraction of the run's own height. -pub const STACK_HALF_SCALE: f32 = 0.55; -/// Baseline of the numerator, as a fraction of the run's height above the -/// denominator's. Leaves a gap the fraction rule sits in. -pub const STACK_RAISE: f32 = 0.68; -/// Height of the fraction rule above the denominator's baseline. -pub const STACK_BAR_Y: f32 = 0.60; +pub const STACK_HALF_SCALE: f32 = 0.85; +/// Height of the fraction rule above the denominator's baseline, as a fraction +/// of the run's height. Sits just clear of the denominator's cap. +pub const STACK_BAR_Y: f32 = 0.95; +/// Baseline of the numerator above the denominator's, as a fraction of the run's +/// height. Clears the rule. +pub const STACK_RAISE: f32 = 1.05; #[derive(Clone)] pub struct LayoutAtom { @@ -1692,5 +1735,3 @@ mod v_anchor_tests { } } } - -