feat(mtext): align tabbed fields on center/right/decimal tab stops

A `\t` reaching a center, right, or decimal (`D`) tab stop now aligns the
field that follows it — the run of words up to the next space/tab — instead
of just advancing the pen:

- decimal: the field's first `.` lands on the stop (a column of numbers
  aligns on the decimal point; a field with no `.` aligns its end),
- right: the field's end lands on the stop,
- center: its middle.

The stop wins even when the field is wider than the room before it, so the
dot/edge stays put and the field overruns to the left, keeping the column
aligned. Left stops and the default 4-em grid are unchanged. Picks up
acadrust 162f194, which carries the tab-stop kinds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-19 00:45:26 +03:00
1 parent 0de2f76921
commit 3da678b2ca
2 files changed
+99 -13

No files matched your search

Generated
+1 -1
View File
@@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.4.0"
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#504281a468f0218ede6a5437e2f0d90d9c7c243f"
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#162f1940ef9f7faaa9b6597e4b190be913b70d37"
dependencies = [
"ahash 0.8.12",
"anyhow",
+98 -12
View File
@@ -244,6 +244,8 @@ pub enum TabKind {
Left,
Center,
Right,
/// Dot-aligned: the text after the tab places its decimal point on the stop.
Decimal,
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -482,9 +484,18 @@ pub fn adapt_mtext_paragraphs(
props
.tab_stops
.iter()
.map(|&p| TabStop {
position: p as f32,
kind: TabKind::Left,
.map(|ts| {
use acadrust::entities::mtext_format::TabStop as ATab;
let kind = match ts {
ATab::Left(_) => TabKind::Left,
ATab::Center(_) => TabKind::Center,
ATab::Right(_) => TabKind::Right,
ATab::Decimal(_) => TabKind::Decimal,
};
TabStop {
position: ts.position() as f32,
kind,
}
})
.collect::<Vec<_>>(),
);
@@ -1047,15 +1058,22 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
MTextRunKind::Glyphs(text) => {
let mut word = String::new();
for ch in text.chars() {
if ch == ' ' || ch == '\u{00A0}' {
if ch == ' ' || ch == '\u{00A0}' || ch == '\t' {
if !word.is_empty() {
atoms.push(LayoutAtom {
kind: AtomKind::Word(std::mem::take(&mut word)),
state: run.state.clone(),
});
}
// A literal tab (acadrust keeps `^I` / `\t` as a tab
// char in the span) advances to the paragraph's next
// tab stop, aligning the field that follows it.
atoms.push(LayoutAtom {
kind: AtomKind::Space,
kind: if ch == '\t' {
AtomKind::Tab
} else {
AtomKind::Space
},
state: run.state.clone(),
});
} else {
@@ -1446,7 +1464,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
};
let mut cursor_x = cursor_start;
for atom in &sub.atoms {
for (ai, atom) in sub.atoms.iter().enumerate() {
match &atom.kind {
AtomKind::Word(text) => {
let run_h = atom.state.height_mul * entity_h;
@@ -1737,12 +1755,80 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
cursor_x += adv;
}
AtomKind::Tab => {
cursor_x = next_tab_position(
cursor_x,
&sub.tab_stops,
sub.indent_left,
entity_h,
);
// A center/right/decimal stop aligns the field that follows
// the tab (the run of words up to the next space/tab) rather
// than just advancing the pen. Decimal places the first `.`
// on the stop; right the field's end; center its middle.
let local = cursor_x - sub.indent_left;
let stop = sub
.tab_stops
.iter()
.find(|ts| ts.position > local + 1e-4)
.copied();
match stop.map(|s| s.kind) {
Some(TabKind::Center | TabKind::Right | TabKind::Decimal) => {
let sx = sub.indent_left + stop.unwrap().position;
let mut field_w = 0.0_f32;
let mut before_dot: Option<f32> = None;
for next in &sub.atoms[ai + 1..] {
match &next.kind {
AtomKind::Word(t) => {
if before_dot.is_none() {
if let Some(dp) = t.find('.') {
let pre: String = t[..dp].to_string();
before_dot = Some(
field_w
+ measure_word(
&pre,
&next.state,
entity_h,
base_wf,
&base_font_name,
),
);
}
}
field_w += measure_word(
t,
&next.state,
entity_h,
base_wf,
&base_font_name,
);
}
AtomKind::Stack { .. } => {
field_w += atom_width(
next,
entity_h,
base_wf,
&base_font_name,
);
}
_ => break,
}
}
let offset = match stop.unwrap().kind {
TabKind::Right => field_w,
TabKind::Center => field_w * 0.5,
// No dot in the field → align its end, as AutoCAD
// does for a decimal tab with no decimal point.
TabKind::Decimal => before_dot.unwrap_or(field_w),
TabKind::Left => 0.0,
};
// The stop wins even when the field is wider than the
// room before it — the dot/edge lands on the stop and
// the field overruns to the left, matching AutoCAD.
cursor_x = sx - offset;
}
_ => {
cursor_x = next_tab_position(
cursor_x,
&sub.tab_stops,
sub.indent_left,
entity_h,
);
}
}
}
}
}