forked from 1b-agent/opencadstudio-upstream
feat(text): route every text-bearing entity through the run-aware MText pipeline
Lift the MText parser + renderer into a reusable `layout_mtext` helper in `text_support` and re-point the remaining text-bearing entities at it, so every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`, `\p…`, decorations, stacked fractions, paragraph indents, tab stops, …) reaches the stroke output uniformly. Per-entity changes: - TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds `layout_mtext`; `\C7;Red`-style codes no longer render as literal glyph noise. - MLEADER MText content (both entity path in [entities/multileader.rs] and the LOD-aware path in [scene/tessellate.rs]) — drops the `strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0 baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame / bg-fill rectangle now read line widths from the layout. - ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine the value goes through the MText pipeline (SingleLine stays on the Text-style anchor maths because the DXF spec disallows codes there). - DIMENSION text_override — `dimension_text_entity` emits a synthetic MText (instead of Text) whenever the value contains MText escapes; the LOD ladder via `tessellate_entity_dim_text` handles both kinds. Infrastructure: - `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`, `BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly. - `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so callers that draw chrome (frames, fills, baselines, greek rects) share the same numbers as the rendered strokes. - `mtext_line_count` re-routed through the new parser so LOD row counts and rendered row counts can never disagree. - Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`, `measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the `v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`) deleted — all callers migrated. Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented here. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c0c144275
commit
4e5d2c8d04
8 files changed
+1027
-1070
No files matched your search
+32
-496
@@ -1,16 +1,13 @@
|
||||
use acadrust::entities::{AttachmentPoint, DrawingDirection, MText};
|
||||
use acadrust::types::aci_table::aci_to_rgb;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip, triangle_grip};
|
||||
use crate::entities::text_support::{
|
||||
parse_mtext_paragraphs, resolve_text_style, InlineColor, MTextRunKind, ParagraphAlign,
|
||||
RunState, TabStop,
|
||||
layout_mtext, resolve_text_style, MTextRenderOpts, MTextVAnchor,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::acad_to_truck::{TextStroke, TruckEntity, TruckObject};
|
||||
use crate::scene::cxf;
|
||||
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
|
||||
@@ -77,358 +74,8 @@ fn drawing_dir_str(d: &DrawingDirection) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Run-based MTEXT layout ──────────────────────────────────────────────────
|
||||
//
|
||||
// Layout proceeds in three passes:
|
||||
// 1. Atomise — turn each MTextLine.runs into a flat sequence of atoms
|
||||
// (Word / Space / Tab) so the wrapper can operate at break boundaries
|
||||
// while keeping per-character formatting state.
|
||||
// 2. Wrap — accumulate atoms into sub-lines using paragraph indents
|
||||
// (`indent_first` for the first sub-line, `indent_left` for
|
||||
// continuations, `indent_right` shrinks the right edge). Each Tab
|
||||
// jumps the cursor to the next tab stop (or the next 4-em default
|
||||
// stop when none is defined).
|
||||
// 3. Render — for every sub-line: pick paragraph alignment + indent,
|
||||
// walk atoms left → right, emit one TextStroke per Word using the
|
||||
// atom's RunState (height / width / oblique / tracking / font /
|
||||
// colour / decorations / valign).
|
||||
|
||||
#[derive(Clone)]
|
||||
enum AtomKind {
|
||||
Word(String),
|
||||
Space,
|
||||
Tab,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LayoutAtom {
|
||||
kind: AtomKind,
|
||||
state: RunState,
|
||||
}
|
||||
|
||||
fn run_scale(state: &RunState, entity_h: f32, base_wf: f32) -> f32 {
|
||||
(state.height_mul * entity_h / 9.0) * (state.width_mul * base_wf.abs())
|
||||
}
|
||||
|
||||
fn resolve_font<'a>(state: &'a RunState, base: &'a str) -> &'a str {
|
||||
state.font.as_deref().unwrap_or(base)
|
||||
}
|
||||
|
||||
fn measure_word(text: &str, state: &RunState, entity_h: f32, base_wf: f32, base_font: &str) -> f32 {
|
||||
let scale = run_scale(state, entity_h, base_wf);
|
||||
let font_name = resolve_font(state, base_font);
|
||||
let font = cxf::get_font(font_name);
|
||||
let mut w = 0.0_f32;
|
||||
for ch in text.chars() {
|
||||
w += match font.glyph(ch) {
|
||||
Some(g) => (g.advance + font.letter_spacing * state.tracking) * scale,
|
||||
None => (6.0 + font.letter_spacing * state.tracking) * scale,
|
||||
};
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
fn measure_space(state: &RunState, entity_h: f32, base_wf: f32, base_font: &str) -> f32 {
|
||||
let scale = run_scale(state, entity_h, base_wf);
|
||||
let font_name = resolve_font(state, base_font);
|
||||
cxf::get_font(font_name).word_spacing * scale
|
||||
}
|
||||
|
||||
fn atom_width(atom: &LayoutAtom, entity_h: f32, base_wf: f32, base_font: &str) -> f32 {
|
||||
match &atom.kind {
|
||||
AtomKind::Word(t) => measure_word(t, &atom.state, entity_h, base_wf, base_font),
|
||||
AtomKind::Space => measure_space(&atom.state, entity_h, base_wf, base_font),
|
||||
AtomKind::Tab => 0.0, // tabs jump the cursor explicitly during wrap / render
|
||||
}
|
||||
}
|
||||
|
||||
/// Cursor position after a `\t` atom: advance to the next user-defined tab
|
||||
/// stop that lies past `cur_x`, falling back to a 4-em default grid when no
|
||||
/// stop is reached.
|
||||
fn next_tab_position(cur_x: f32, tab_stops: &[TabStop], indent_left: f32, entity_h: f32) -> f32 {
|
||||
let local = cur_x - indent_left; // tabs are measured from the content area's left edge
|
||||
for ts in tab_stops {
|
||||
if ts.position > local + 1e-4 {
|
||||
return indent_left + ts.position;
|
||||
}
|
||||
}
|
||||
let default_interval = entity_h * 4.0; // 4 em fallback, matches AutoCAD's "no tab stops"
|
||||
let n = (local / default_interval).floor() + 1.0;
|
||||
indent_left + n * default_interval
|
||||
}
|
||||
|
||||
/// Break a flat MText paragraph atom stream into wrap-fit sub-lines.
|
||||
fn wrap_paragraph(
|
||||
atoms: Vec<LayoutAtom>,
|
||||
rect_w: f32,
|
||||
indent_first: f32,
|
||||
indent_left: f32,
|
||||
indent_right: f32,
|
||||
tab_stops: &[TabStop],
|
||||
entity_h: f32,
|
||||
base_wf: f32,
|
||||
base_font: &str,
|
||||
) -> Vec<Vec<LayoutAtom>> {
|
||||
if rect_w <= 0.0 {
|
||||
return vec![atoms];
|
||||
}
|
||||
let mut sublines: Vec<Vec<LayoutAtom>> = Vec::new();
|
||||
let mut cur: Vec<LayoutAtom> = Vec::new();
|
||||
let mut cur_w = 0.0_f32;
|
||||
let mut subline_idx: usize = 0;
|
||||
let line_start_x = |idx: usize| if idx == 0 { indent_first } else { indent_left };
|
||||
let line_max_w = |idx: usize| (rect_w - indent_right - line_start_x(idx)).max(0.0);
|
||||
|
||||
for atom in atoms {
|
||||
match &atom.kind {
|
||||
AtomKind::Word(_) => {
|
||||
let w = atom_width(&atom, entity_h, base_wf, base_font);
|
||||
let max_w = line_max_w(subline_idx);
|
||||
if !cur.is_empty() && cur_w + w > max_w {
|
||||
while matches!(cur.last().map(|a| &a.kind), Some(AtomKind::Space)) {
|
||||
cur.pop();
|
||||
}
|
||||
sublines.push(std::mem::take(&mut cur));
|
||||
cur_w = 0.0;
|
||||
subline_idx += 1;
|
||||
}
|
||||
cur.push(atom);
|
||||
cur_w += w;
|
||||
}
|
||||
AtomKind::Space => {
|
||||
if cur.is_empty() {
|
||||
continue;
|
||||
}
|
||||
cur_w += atom_width(&atom, entity_h, base_wf, base_font);
|
||||
cur.push(atom);
|
||||
}
|
||||
AtomKind::Tab => {
|
||||
let start_x = line_start_x(subline_idx);
|
||||
let new_w = next_tab_position(cur_w + start_x, tab_stops, indent_left, entity_h)
|
||||
- start_x;
|
||||
let max_w = line_max_w(subline_idx);
|
||||
if new_w > max_w && !cur.is_empty() {
|
||||
sublines.push(std::mem::take(&mut cur));
|
||||
cur_w = 0.0;
|
||||
subline_idx += 1;
|
||||
} else {
|
||||
cur.push(atom);
|
||||
cur_w = new_w.min(max_w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !cur.is_empty() {
|
||||
sublines.push(cur);
|
||||
}
|
||||
if sublines.is_empty() {
|
||||
sublines.push(Vec::new());
|
||||
}
|
||||
sublines
|
||||
}
|
||||
|
||||
fn line_total_width(
|
||||
atoms: &[LayoutAtom],
|
||||
entity_h: f32,
|
||||
base_wf: f32,
|
||||
base_font: &str,
|
||||
line_start_x: f32,
|
||||
indent_left: f32,
|
||||
tab_stops: &[TabStop],
|
||||
) -> f32 {
|
||||
let mut x = line_start_x;
|
||||
for atom in atoms {
|
||||
match atom.kind {
|
||||
AtomKind::Tab => {
|
||||
x = next_tab_position(x, tab_stops, indent_left, entity_h);
|
||||
}
|
||||
_ => x += atom_width(atom, entity_h, base_wf, base_font),
|
||||
}
|
||||
}
|
||||
x - line_start_x
|
||||
}
|
||||
|
||||
fn resolve_inline_color(c: &InlineColor) -> Option<[f32; 3]> {
|
||||
match c {
|
||||
InlineColor::Aci(idx) => {
|
||||
aci_to_rgb(*idx).map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0])
|
||||
}
|
||||
InlineColor::True(rgb) => Some(*rgb),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap a run's glyph text with MTEXT decoration markers so cxf's
|
||||
/// `tessellate_text_run` emits the underline / overline / strikethrough
|
||||
/// strokes for us — keeps decoration geometry in one place rather than
|
||||
/// duplicating the y-position constants.
|
||||
fn decorated(text: &str, state: &RunState) -> String {
|
||||
if !(state.underline || state.overline || state.strike) {
|
||||
return text.to_string();
|
||||
}
|
||||
let mut s = String::with_capacity(text.len() + 6);
|
||||
if state.underline {
|
||||
s.push_str("\\L");
|
||||
}
|
||||
if state.overline {
|
||||
s.push_str("\\O");
|
||||
}
|
||||
if state.strike {
|
||||
s.push_str("\\K");
|
||||
}
|
||||
s.push_str(text);
|
||||
if state.underline {
|
||||
s.push_str("\\l");
|
||||
}
|
||||
if state.overline {
|
||||
s.push_str("\\o");
|
||||
}
|
||||
if state.strike {
|
||||
s.push_str("\\k");
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity {
|
||||
let resolved_style = resolve_text_style(&t.style, document);
|
||||
let base_font_name = resolved_style.font_name.clone();
|
||||
let base_font = cxf::get_font(&base_font_name);
|
||||
let base_wf_abs = resolved_style.width_factor.max(0.01);
|
||||
let base_wf = if resolved_style.is_backward { -base_wf_abs } else { base_wf_abs };
|
||||
let base_oblique = resolved_style.oblique_angle;
|
||||
let entity_h = t.height as f32;
|
||||
let rect_w = t.rectangle_width as f32;
|
||||
|
||||
// ── 1. Parse ────────────────────────────────────────────────────────────
|
||||
let paragraphs = parse_mtext_paragraphs(&t.value, entity_h);
|
||||
|
||||
// ── 2. Atomise + wrap each paragraph into sub-lines ─────────────────────
|
||||
struct SubLine {
|
||||
atoms: Vec<LayoutAtom>,
|
||||
align: Option<ParagraphAlign>,
|
||||
indent_first: f32,
|
||||
indent_left: f32,
|
||||
indent_right: f32,
|
||||
tab_stops: Vec<TabStop>,
|
||||
is_first_in_paragraph: bool,
|
||||
}
|
||||
|
||||
let mut sub_lines: Vec<SubLine> = Vec::new();
|
||||
for para in ¶graphs {
|
||||
// Flatten runs into atoms.
|
||||
let mut atoms: Vec<LayoutAtom> = Vec::new();
|
||||
for run in ¶.runs {
|
||||
match &run.kind {
|
||||
MTextRunKind::Glyphs(text) => {
|
||||
let mut word = String::new();
|
||||
for ch in text.chars() {
|
||||
if ch == ' ' || ch == '\u{00A0}' {
|
||||
if !word.is_empty() {
|
||||
atoms.push(LayoutAtom {
|
||||
kind: AtomKind::Word(std::mem::take(&mut word)),
|
||||
state: run.state.clone(),
|
||||
});
|
||||
}
|
||||
atoms.push(LayoutAtom {
|
||||
kind: AtomKind::Space,
|
||||
state: run.state.clone(),
|
||||
});
|
||||
} else {
|
||||
word.push(ch);
|
||||
}
|
||||
}
|
||||
if !word.is_empty() {
|
||||
atoms.push(LayoutAtom {
|
||||
kind: AtomKind::Word(word),
|
||||
state: run.state.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
MTextRunKind::Tab => {
|
||||
atoms.push(LayoutAtom {
|
||||
kind: AtomKind::Tab,
|
||||
state: run.state.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim leading + trailing Space atoms — the legacy renderer dropped
|
||||
// edge whitespace via `String::trim()` after `strip_mtext_codes`, so
|
||||
// line_w / cursor_start agree on what counts as the paragraph's
|
||||
// visible content. Without this, a paragraph that ends with a stray
|
||||
// space measures wider than it draws and centering / right-alignment
|
||||
// appears off by half a space-width.
|
||||
let first_word = atoms
|
||||
.iter()
|
||||
.position(|a| !matches!(a.kind, AtomKind::Space))
|
||||
.unwrap_or(atoms.len());
|
||||
atoms.drain(..first_word);
|
||||
while matches!(atoms.last().map(|a| &a.kind), Some(AtomKind::Space)) {
|
||||
atoms.pop();
|
||||
}
|
||||
|
||||
let wrapped = wrap_paragraph(
|
||||
atoms,
|
||||
rect_w,
|
||||
para.indent_first,
|
||||
para.indent_left,
|
||||
para.indent_right,
|
||||
¶.tab_stops,
|
||||
entity_h,
|
||||
base_wf,
|
||||
&base_font_name,
|
||||
);
|
||||
for (idx, atoms) in wrapped.into_iter().enumerate() {
|
||||
sub_lines.push(SubLine {
|
||||
atoms,
|
||||
align: para.align,
|
||||
indent_first: para.indent_first,
|
||||
indent_left: para.indent_left,
|
||||
indent_right: para.indent_right,
|
||||
tab_stops: para.tab_stops.clone(),
|
||||
is_first_in_paragraph: idx == 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
if sub_lines.is_empty() {
|
||||
sub_lines.push(SubLine {
|
||||
atoms: Vec::new(),
|
||||
align: None,
|
||||
indent_first: 0.0,
|
||||
indent_left: 0.0,
|
||||
indent_right: 0.0,
|
||||
tab_stops: Vec::new(),
|
||||
is_first_in_paragraph: true,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 3. Block geometry (line spacing, attachment, rotation) ──────────────
|
||||
let n_lines = sub_lines.len().max(1) as f32;
|
||||
let ls_factor = if t.line_spacing_factor > 0.0 {
|
||||
t.line_spacing_factor as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
// DXF code 44 — multiplier on the *default* baseline-to-baseline gap,
|
||||
// which AutoCAD defines as 5/3 × text height (≈ 1.667). 1.0 → single
|
||||
// spacing, 2.0 → double, etc.
|
||||
let line_h = entity_h * ls_factor * (5.0 / 3.0) * base_font.line_spacing;
|
||||
let h = entity_h;
|
||||
// CXF glyphs sit on the baseline (y=0) and extend UP by `h`. v_offset
|
||||
// is the Y of the first sub-line's baseline relative to the insertion
|
||||
// point; pick it from the attachment-point's vertical anchor:
|
||||
// Top → block top at insertion → v_offset = −h
|
||||
// Bottom → block bottom at insertion → v_offset = (n−1) · line_h
|
||||
// Middle → midpoint of the two above
|
||||
let v_offset = match t.attachment_point {
|
||||
AttachmentPoint::TopLeft | AttachmentPoint::TopCenter | AttachmentPoint::TopRight => -h,
|
||||
AttachmentPoint::MiddleLeft
|
||||
| AttachmentPoint::MiddleCenter
|
||||
| AttachmentPoint::MiddleRight => ((n_lines - 1.0) * line_h - h) * 0.5,
|
||||
AttachmentPoint::BottomLeft
|
||||
| AttachmentPoint::BottomCenter
|
||||
| AttachmentPoint::BottomRight => (n_lines - 1.0) * line_h,
|
||||
};
|
||||
let attach_h_anchor: f32 = match t.attachment_point {
|
||||
AttachmentPoint::TopCenter
|
||||
| AttachmentPoint::MiddleCenter
|
||||
@@ -438,152 +85,41 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity {
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
let box_left = -attach_h_anchor * rect_w;
|
||||
let vertical_text = matches!(t.drawing_direction, DrawingDirection::TopToBottom);
|
||||
let rot = if resolved_style.is_upside_down {
|
||||
let v_anchor = match t.attachment_point {
|
||||
AttachmentPoint::TopLeft | AttachmentPoint::TopCenter | AttachmentPoint::TopRight => {
|
||||
MTextVAnchor::Top
|
||||
}
|
||||
AttachmentPoint::MiddleLeft
|
||||
| AttachmentPoint::MiddleCenter
|
||||
| AttachmentPoint::MiddleRight => MTextVAnchor::Middle,
|
||||
AttachmentPoint::BottomLeft
|
||||
| AttachmentPoint::BottomCenter
|
||||
| AttachmentPoint::BottomRight => MTextVAnchor::Bottom,
|
||||
};
|
||||
let rotation = 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;
|
||||
let insertion = Vec3::new(ins_x as f32, ins_y as f32, t.insertion_point.z as f32);
|
||||
let mut all_strokes: Vec<TextStroke> = Vec::new();
|
||||
|
||||
// ── 4. Render each sub-line ─────────────────────────────────────────────
|
||||
for (i, sub) in sub_lines.iter().enumerate() {
|
||||
let li = i as f32;
|
||||
let (line_base_x, line_base_y) = if vertical_text {
|
||||
let col_offset = li * entity_h * 1.2;
|
||||
(
|
||||
col_offset * cos_r + v_offset * (-sin_r),
|
||||
col_offset * sin_r + v_offset * cos_r,
|
||||
)
|
||||
} else {
|
||||
let ly = -(li * line_h) + v_offset;
|
||||
(ly * (-sin_r), ly * cos_r)
|
||||
};
|
||||
|
||||
// Paragraph-content area: [content_left, content_right] relative to
|
||||
// insertion point. With rect_w == 0 we have no box, so fall back to
|
||||
// anchoring at the insertion point (legacy behaviour).
|
||||
let content_left = if rect_w > 0.0 {
|
||||
box_left + if sub.is_first_in_paragraph { sub.indent_first } else { sub.indent_left }
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let content_right = if rect_w > 0.0 {
|
||||
box_left + rect_w - sub.indent_right
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Effective horizontal anchor for this sub-line: paragraph alignment
|
||||
// (when explicitly set inline) wins, otherwise inherit from the
|
||||
// entity attachment.
|
||||
let line_anchor: f32 = match sub.align {
|
||||
Some(ParagraphAlign::Left)
|
||||
| Some(ParagraphAlign::Justify)
|
||||
| Some(ParagraphAlign::Distribute) => 0.0,
|
||||
Some(ParagraphAlign::Center) => 0.5,
|
||||
Some(ParagraphAlign::Right) => 1.0,
|
||||
None => attach_h_anchor,
|
||||
};
|
||||
|
||||
let line_w = line_total_width(
|
||||
&sub.atoms,
|
||||
entity_h,
|
||||
base_wf,
|
||||
&base_font_name,
|
||||
0.0,
|
||||
sub.indent_left,
|
||||
&sub.tab_stops,
|
||||
);
|
||||
|
||||
// Cursor X (relative to insertion point, pre-rotation) where the
|
||||
// first atom starts. With a box: lay the line inside the paragraph
|
||||
// content area at `line_anchor`. Without a box: anchor the line at
|
||||
// the insertion point using the entity's attachment.
|
||||
let cursor_start = if rect_w > 0.0 {
|
||||
let content_w = (content_right - content_left).max(0.0);
|
||||
content_left + (content_w - line_w) * line_anchor
|
||||
} else if line_anchor > 0.0 {
|
||||
-line_w * line_anchor
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Line's tallest run height (for valign offsets).
|
||||
let line_max_h = sub
|
||||
.atoms
|
||||
.iter()
|
||||
.map(|a| a.state.height_mul * entity_h)
|
||||
.fold(entity_h, f32::max);
|
||||
|
||||
// Walk atoms left → right, emitting one TextStroke per Word atom.
|
||||
let mut cursor_x = cursor_start;
|
||||
for atom in &sub.atoms {
|
||||
match &atom.kind {
|
||||
AtomKind::Word(text) => {
|
||||
// Per-atom render parameters (composed with style baseline).
|
||||
let run_h = atom.state.height_mul * entity_h;
|
||||
let signed_wf = base_wf.signum() * atom.state.width_mul * base_wf.abs();
|
||||
let oblique = base_oblique + atom.state.oblique_rad;
|
||||
let font_name = resolve_font(&atom.state, &base_font_name);
|
||||
let tracking = atom.state.tracking;
|
||||
let valign_dy = match atom.state.valign {
|
||||
1 => (line_max_h - run_h) * 0.5,
|
||||
2 => line_max_h - run_h,
|
||||
_ => 0.0,
|
||||
};
|
||||
let color = atom.state.color.as_ref().and_then(resolve_inline_color);
|
||||
let body = decorated(text, &atom.state);
|
||||
|
||||
// Translate (cursor_x, valign_dy) into world space at this
|
||||
// sub-line's baseline.
|
||||
let lx = cursor_x;
|
||||
let ly = valign_dy;
|
||||
let world_dx = lx * cos_r - ly * sin_r;
|
||||
let world_dy = lx * sin_r + ly * cos_r;
|
||||
let origin: [f64; 2] = [
|
||||
ins_x + (line_base_x + world_dx) as f64,
|
||||
ins_y + (line_base_y + world_dy) as f64,
|
||||
];
|
||||
let strokes = cxf::tessellate_text_run(
|
||||
[0.0, 0.0],
|
||||
run_h,
|
||||
rot,
|
||||
signed_wf,
|
||||
oblique,
|
||||
tracking,
|
||||
font_name,
|
||||
&body,
|
||||
);
|
||||
all_strokes.push(TextStroke {
|
||||
strokes,
|
||||
origin,
|
||||
color,
|
||||
});
|
||||
cursor_x += measure_word(text, &atom.state, entity_h, base_wf, &base_font_name);
|
||||
}
|
||||
AtomKind::Space => {
|
||||
cursor_x += measure_space(&atom.state, entity_h, base_wf, &base_font_name);
|
||||
}
|
||||
AtomKind::Tab => {
|
||||
cursor_x = next_tab_position(
|
||||
cursor_x,
|
||||
&sub.tab_stops,
|
||||
sub.indent_left,
|
||||
entity_h,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let layout = layout_mtext(&MTextRenderOpts {
|
||||
value: &t.value,
|
||||
insertion: [t.insertion_point.x, t.insertion_point.y, t.insertion_point.z],
|
||||
height: t.height as f32,
|
||||
rect_w: t.rectangle_width as f32,
|
||||
rotation,
|
||||
style: &resolved_style,
|
||||
attach_h_anchor,
|
||||
v_anchor,
|
||||
line_spacing_factor: t.line_spacing_factor as f32,
|
||||
vertical_text: matches!(t.drawing_direction, DrawingDirection::TopToBottom),
|
||||
});
|
||||
let insertion = Vec3::new(
|
||||
t.insertion_point.x as f32,
|
||||
t.insertion_point.y as f32,
|
||||
t.insertion_point.z as f32,
|
||||
);
|
||||
TruckEntity {
|
||||
object: TruckObject::Text(all_strokes),
|
||||
object: TruckObject::Text(layout.strokes),
|
||||
snap_pts: vec![(insertion, SnapHint::Insertion)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
|
||||
Reference in new issue
Block a user