forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
0de2f76921
commit
3da678b2ca
2 files changed
+99
-13
No files matched your search
Generated
+1
-1
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user