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>
2026-05-22 05:53:01 +03:00
|
|
|
|
use acadrust::types::aci_table::aci_to_rgb;
|
refactor(text): SDF for all text kinds; remove the legacy stroke text path
SDF is the only text renderer now — the stroke path and its LOD ladder are
gone, and the remaining text kinds render as SDF glyph quads too.
- ATTDEF / ATTRIB: carry a GlyphRun so single-line attribute text renders as
SDF (multi-line already did via layout_mtext). Tolerance / complex-linetype
text already SDF.
- Remove `sdf_text_enabled()` (the OCS_TEXT_SDF flag / kill switch) and take
the SDF path unconditionally everywhere: tessellate Text arm (per-group by
GlyphRun presence), hit_test fallbacks, render gather, complex_lt, table,
mleader.
- Delete the text LOD ladder: tess.rs baseline/greek block and the
block_cache greek/baseline path (emit_greeked_text / emit_text_baseline),
plus the now-dead helpers text_baseline_points / text_greek_obb_tris /
text_obb_corners_native / mtext_line_count and the LocalWire
text_height_local / text_obb_local fields. All text is crisp at every zoom;
text entities are exempt from the sub-pixel stub cull.
- Fix a latent bug: block-internal SDF text was culled because its LocalWire
aabb_local was [0,0,0,0] (no points) — fold the glyph-quad extent into
aabb_local and exempt text_verts-carrying wires from the sub-pixel cull.
- Clear the pre-existing dead-code warnings (TextPlacement.elevation, TabKind,
MTextRunKind::Tab).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 21:07:00 +03:00
|
|
|
|
use acadrust::CadDocument;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
2026-07-07 00:53:19 +03:00
|
|
|
|
use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke};
|
2026-06-16 11:33:46 +03:00
|
|
|
|
use crate::scene::text::font_face::Face;
|
|
|
|
|
|
use crate::scene::text::lff;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
|
|
pub struct ResolvedTextStyle {
|
|
|
|
|
|
pub font_name: String,
|
|
|
|
|
|
pub width_factor: f32,
|
|
|
|
|
|
pub oblique_angle: f32,
|
2026-04-26 22:10:32 +03:00
|
|
|
|
pub is_backward: bool,
|
|
|
|
|
|
pub is_upside_down: bool,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedTextStyle {
|
|
|
|
|
|
let style = document.text_styles.iter().find(|entry| {
|
|
|
|
|
|
entry.name.eq_ignore_ascii_case(style_name)
|
|
|
|
|
|
|| (style_name.trim().is_empty() && entry.name.eq_ignore_ascii_case("Standard"))
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-25 18:17:01 +01:00
|
|
|
|
let mut font_name = if let Some(style) = style {
|
|
|
|
|
|
if !style.true_type_font.trim().is_empty() {
|
|
|
|
|
|
style.true_type_font.trim().to_string()
|
|
|
|
|
|
} else if !style.font_file.trim().is_empty() {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let file = style.font_file.trim();
|
|
|
|
|
|
let basename = file.rsplit(['/', '\\']).next().unwrap_or(file);
|
|
|
|
|
|
let stem = basename.split('.').next().unwrap_or(basename).trim();
|
|
|
|
|
|
if !stem.is_empty() {
|
|
|
|
|
|
stem.to_string()
|
|
|
|
|
|
} else if !style.name.trim().is_empty() {
|
|
|
|
|
|
style.name.trim().to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"Standard".to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if !style.name.trim().is_empty() {
|
|
|
|
|
|
style.name.trim().to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"Standard".to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if style_name.trim().is_empty() {
|
|
|
|
|
|
"Standard".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
style_name.trim().to_string()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-25 18:17:01 +01:00
|
|
|
|
if !lff::is_builtin(&font_name) {
|
|
|
|
|
|
if let Some(canonical) = crate::scene::text::sysfont::canonical_family_name(&font_name) {
|
|
|
|
|
|
font_name = canonical;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
ResolvedTextStyle {
|
2026-06-26 00:27:40 +03:00
|
|
|
|
font_name,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
width_factor: style.map(|s| s.width_factor as f32).unwrap_or(1.0),
|
|
|
|
|
|
oblique_angle: style.map(|s| s.oblique_angle as f32).unwrap_or(0.0),
|
2026-04-26 22:10:32 +03:00
|
|
|
|
is_backward: style.map(|s| s.is_backward()).unwrap_or(false),
|
|
|
|
|
|
is_upside_down: style.map(|s| s.is_upside_down()).unwrap_or(false),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 18:47:45 +03:00
|
|
|
|
pub struct TextLocalBounds {
|
|
|
|
|
|
/// Inked extent (glyph strokes only) — drives vertical alignment, where
|
|
|
|
|
|
/// the cap / baseline geometry is what matters.
|
|
|
|
|
|
pub ink_min: [f32; 2],
|
|
|
|
|
|
pub ink_max: [f32; 2],
|
|
|
|
|
|
/// Pen advance along the baseline, including leading / trailing spaces and
|
|
|
|
|
|
/// inter-glyph spacing. Drives horizontal alignment so spaces in the string
|
|
|
|
|
|
/// keep their width instead of collapsing to the first / last inked glyph.
|
|
|
|
|
|
pub advance: f32,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
pub fn text_local_bounds(
|
|
|
|
|
|
font_name: &str,
|
|
|
|
|
|
text: &str,
|
|
|
|
|
|
height: f32,
|
|
|
|
|
|
width_factor: f32,
|
|
|
|
|
|
oblique_angle: f32,
|
2026-06-22 18:47:45 +03:00
|
|
|
|
) -> Option<TextLocalBounds> {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
if text.is_empty() || height <= 0.0 {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 11:18:17 +03:00
|
|
|
|
let face = Face::resolve(font_name);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let scale = height / 9.0;
|
2026-04-26 22:10:32 +03:00
|
|
|
|
let wf = width_factor.abs().clamp(0.01, 100.0);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let ob = oblique_angle.tan();
|
|
|
|
|
|
let mut cursor_x = 0.0_f32;
|
|
|
|
|
|
let mut min_x = f32::INFINITY;
|
|
|
|
|
|
let mut max_x = f32::NEG_INFINITY;
|
|
|
|
|
|
let mut min_y = f32::INFINITY;
|
|
|
|
|
|
let mut max_y = f32::NEG_INFINITY;
|
|
|
|
|
|
|
|
|
|
|
|
for ch in text.chars() {
|
|
|
|
|
|
if ch == ' ' {
|
2026-06-16 11:18:17 +03:00
|
|
|
|
cursor_x += face.word_spacing();
|
2026-03-22 14:56:27 +03:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-06-16 11:18:17 +03:00
|
|
|
|
match face.glyph(ch) {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
Some(glyph) => {
|
|
|
|
|
|
for stroke in &glyph.strokes {
|
|
|
|
|
|
for &[gx, gy] in stroke {
|
|
|
|
|
|
let sx = (cursor_x + gx) * scale * wf + gy * scale * ob;
|
|
|
|
|
|
let sy = gy * scale;
|
|
|
|
|
|
min_x = min_x.min(sx);
|
|
|
|
|
|
max_x = max_x.max(sx);
|
|
|
|
|
|
min_y = min_y.min(sy);
|
|
|
|
|
|
max_y = max_y.max(sy);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-16 11:18:17 +03:00
|
|
|
|
cursor_x += glyph.advance + face.letter_spacing();
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
None => {
|
2026-06-16 11:18:17 +03:00
|
|
|
|
cursor_x += 6.0 + face.letter_spacing();
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 18:47:45 +03:00
|
|
|
|
// Pen advance is measured at the baseline, so oblique shear (which skews x
|
|
|
|
|
|
// only by gy) does not enter it. Valid even for an all-space string.
|
|
|
|
|
|
let advance = cursor_x * scale * wf;
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() {
|
2026-06-22 18:47:45 +03:00
|
|
|
|
Some(TextLocalBounds {
|
|
|
|
|
|
ink_min: [min_x, min_y],
|
|
|
|
|
|
ink_max: [max_x, max_y],
|
|
|
|
|
|
advance,
|
|
|
|
|
|
})
|
2026-03-22 14:56:27 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-26 21:57:51 +03:00
|
|
|
|
/// Expand DXF `%%x` special-character sequences that appear in both TEXT and MTEXT values:
|
|
|
|
|
|
/// - `%%d` / `%%D` → `°`
|
|
|
|
|
|
/// - `%%p` / `%%P` → `±`
|
|
|
|
|
|
/// - `%%c` / `%%C` → `⌀`
|
|
|
|
|
|
/// - `%%u` / `%%U` → underline toggle (stripped — not renderable with stroke fonts)
|
|
|
|
|
|
/// - `%%o` / `%%O` → overline toggle (stripped)
|
|
|
|
|
|
/// - `%%%%` → `%`
|
|
|
|
|
|
/// - `%%nnn` (3 decimal digits) → Unicode scalar `nnn`
|
|
|
|
|
|
/// Any unrecognised `%%x` is passed through unchanged.
|
|
|
|
|
|
pub fn resolve_dxf_special_chars(s: &str) -> String {
|
|
|
|
|
|
let mut out = String::with_capacity(s.len());
|
|
|
|
|
|
let mut chars = s.chars().peekable();
|
|
|
|
|
|
|
|
|
|
|
|
while let Some(c) = chars.next() {
|
|
|
|
|
|
if c != '%' || chars.peek() != Some(&'%') {
|
|
|
|
|
|
out.push(c);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
chars.next(); // consume second '%'
|
|
|
|
|
|
match chars.peek().map(|c| c.to_ascii_lowercase()) {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
Some('d') => {
|
|
|
|
|
|
chars.next();
|
|
|
|
|
|
out.push('°');
|
|
|
|
|
|
}
|
|
|
|
|
|
Some('p') => {
|
|
|
|
|
|
chars.next();
|
|
|
|
|
|
out.push('±');
|
|
|
|
|
|
}
|
|
|
|
|
|
Some('c') => {
|
|
|
|
|
|
chars.next();
|
|
|
|
|
|
out.push('⌀');
|
|
|
|
|
|
}
|
|
|
|
|
|
Some('u') | Some('o') => {
|
|
|
|
|
|
chars.next();
|
|
|
|
|
|
} // toggle codes — strip silently
|
|
|
|
|
|
Some('%') => {
|
|
|
|
|
|
chars.next();
|
|
|
|
|
|
out.push('%');
|
|
|
|
|
|
}
|
2026-04-26 21:57:51 +03:00
|
|
|
|
Some(d) if d.is_ascii_digit() => {
|
|
|
|
|
|
let mut digits = String::with_capacity(3);
|
|
|
|
|
|
for _ in 0..3 {
|
|
|
|
|
|
match chars.peek() {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
Some(&ch) if ch.is_ascii_digit() => {
|
|
|
|
|
|
digits.push(chars.next().unwrap());
|
|
|
|
|
|
}
|
2026-04-26 21:57:51 +03:00
|
|
|
|
_ => break,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if digits.len() == 3 {
|
|
|
|
|
|
if let Ok(n) = digits.parse::<u32>() {
|
|
|
|
|
|
if let Some(ch) = char::from_u32(n) {
|
|
|
|
|
|
out.push(ch);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out.push('%');
|
|
|
|
|
|
out.push('%');
|
|
|
|
|
|
out.push_str(&digits);
|
|
|
|
|
|
}
|
2026-05-12 10:36:15 +03:00
|
|
|
|
_ => {
|
|
|
|
|
|
out.push('%');
|
|
|
|
|
|
out.push('%');
|
|
|
|
|
|
}
|
2026-04-26 21:57:51 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:42:22 +03:00
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// Rich MTEXT parser — full inline format-code coverage
|
|
|
|
|
|
//
|
|
|
|
|
|
// Recognised codes (DXF MTEXT inline):
|
|
|
|
|
|
// Escapes: \\ \{ \} \~ \t \P \n \N \U+XXXX \u+XXXX
|
|
|
|
|
|
// Toggles: \L\l \O\o \K\k (underline / overline / strike)
|
|
|
|
|
|
// State: \H<v>[x]; \W<v>[x]; \Q<v>; \T<v>[x]; \A<n>;
|
|
|
|
|
|
// \C<aci>; \c<rgb>;
|
|
|
|
|
|
// \f<name>|b<0/1>|i<0/1>|c<n>|p<n>; \F<file>;
|
|
|
|
|
|
// \M+<n>; \X \S<u><sep><l>;
|
|
|
|
|
|
// Paragraph: \p[xi<v>,l<v>,r<v>,q[lcrjd],t<positions>,s<v>...];
|
|
|
|
|
|
// Scope: { ... } push/pop full state
|
|
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// Paragraph alignment encoded inline via `\p...q[lcrjd]...;`.
|
|
|
|
|
|
/// `Justify` / `Distribute` render as `Left` (full inter-word redistribution
|
|
|
|
|
|
/// is not implemented in the stroke renderer).
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
|
pub enum ParagraphAlign {
|
|
|
|
|
|
Left,
|
|
|
|
|
|
Center,
|
|
|
|
|
|
Right,
|
|
|
|
|
|
Justify,
|
|
|
|
|
|
Distribute,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Inline colour override (`\C` ACI or `\c` 24-bit true colour). Resolved to
|
|
|
|
|
|
/// linear RGB at render time via the document's ACI table.
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
|
|
|
|
pub enum InlineColor {
|
|
|
|
|
|
Aci(u8),
|
|
|
|
|
|
True([f32; 3]),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(text): SDF for all text kinds; remove the legacy stroke text path
SDF is the only text renderer now — the stroke path and its LOD ladder are
gone, and the remaining text kinds render as SDF glyph quads too.
- ATTDEF / ATTRIB: carry a GlyphRun so single-line attribute text renders as
SDF (multi-line already did via layout_mtext). Tolerance / complex-linetype
text already SDF.
- Remove `sdf_text_enabled()` (the OCS_TEXT_SDF flag / kill switch) and take
the SDF path unconditionally everywhere: tessellate Text arm (per-group by
GlyphRun presence), hit_test fallbacks, render gather, complex_lt, table,
mleader.
- Delete the text LOD ladder: tess.rs baseline/greek block and the
block_cache greek/baseline path (emit_greeked_text / emit_text_baseline),
plus the now-dead helpers text_baseline_points / text_greek_obb_tris /
text_obb_corners_native / mtext_line_count and the LocalWire
text_height_local / text_obb_local fields. All text is crisp at every zoom;
text entities are exempt from the sub-pixel stub cull.
- Fix a latent bug: block-internal SDF text was culled because its LocalWire
aabb_local was [0,0,0,0] (no points) — fold the glyph-quad extent into
aabb_local and exempt text_verts-carrying wires from the sub-pixel cull.
- Clear the pre-existing dead-code warnings (TextPlacement.elevation, TabKind,
MTextRunKind::Tab).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 21:07:00 +03:00
|
|
|
|
/// Tab-stop alignment kind (from `\pt<L|C|R><pos>` entries). `Center` / `Right`
|
|
|
|
|
|
/// are DXF-spec kinds the parser does not emit yet (only `Left` is produced).
|
|
|
|
|
|
#[allow(dead_code)]
|
2026-05-21 23:42:22 +03:00
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
|
pub enum TabKind {
|
|
|
|
|
|
Left,
|
|
|
|
|
|
Center,
|
|
|
|
|
|
Right,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
|
|
|
|
pub struct TabStop {
|
|
|
|
|
|
pub position: f32,
|
|
|
|
|
|
pub kind: TabKind,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Per-run formatting state. All fields are multipliers / overrides relative
|
|
|
|
|
|
/// to the entity-level defaults; the renderer composes them with the resolved
|
|
|
|
|
|
/// text style at draw time.
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
|
|
pub struct RunState {
|
|
|
|
|
|
/// Multiplier on entity text height (`\H<v>x;` → ×v; `\H<v>;` → v / entity_h)
|
|
|
|
|
|
pub height_mul: f32,
|
|
|
|
|
|
/// Multiplier on the (signed) style width-factor (`\W<v>;` → set, `\Wx;` → ×)
|
|
|
|
|
|
pub width_mul: f32,
|
|
|
|
|
|
/// Absolute oblique angle override in radians (`\Q<deg>;`)
|
|
|
|
|
|
pub oblique_rad: f32,
|
|
|
|
|
|
/// Tracking multiplier on `font.letter_spacing` (`\T<v>;`)
|
|
|
|
|
|
pub tracking: f32,
|
|
|
|
|
|
/// Vertical alignment of the run within its line box (0=baseline / 1=center
|
|
|
|
|
|
/// / 2=top). Mainly used for fractions and superscript-like layout (`\A`).
|
|
|
|
|
|
pub valign: u8,
|
|
|
|
|
|
/// Font-name override, `None` ⇒ inherit the resolved style font.
|
|
|
|
|
|
pub font: Option<String>,
|
|
|
|
|
|
/// Colour override, `None` ⇒ inherit entity colour.
|
|
|
|
|
|
pub color: Option<InlineColor>,
|
|
|
|
|
|
pub underline: bool,
|
|
|
|
|
|
pub overline: bool,
|
|
|
|
|
|
pub strike: bool,
|
feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:18:50 +03:00
|
|
|
|
/// Bold run — rendered with a wider SDF pen (thicker strokes).
|
|
|
|
|
|
pub bold: bool,
|
2026-05-21 23:42:22 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RunState {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
height_mul: 1.0,
|
|
|
|
|
|
width_mul: 1.0,
|
|
|
|
|
|
oblique_rad: 0.0,
|
|
|
|
|
|
tracking: 1.0,
|
|
|
|
|
|
valign: 0,
|
|
|
|
|
|
font: None,
|
|
|
|
|
|
color: None,
|
|
|
|
|
|
underline: false,
|
|
|
|
|
|
overline: false,
|
|
|
|
|
|
strike: false,
|
feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:18:50 +03:00
|
|
|
|
bold: false,
|
2026-05-21 23:42:22 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
|
pub enum MTextRunKind {
|
|
|
|
|
|
/// Renderable glyph text (DXF specials resolved, decoration markers stripped).
|
|
|
|
|
|
Glyphs(String),
|
|
|
|
|
|
/// `\t` — jump the cursor to the next tab stop (or default tab interval).
|
refactor(text): SDF for all text kinds; remove the legacy stroke text path
SDF is the only text renderer now — the stroke path and its LOD ladder are
gone, and the remaining text kinds render as SDF glyph quads too.
- ATTDEF / ATTRIB: carry a GlyphRun so single-line attribute text renders as
SDF (multi-line already did via layout_mtext). Tolerance / complex-linetype
text already SDF.
- Remove `sdf_text_enabled()` (the OCS_TEXT_SDF flag / kill switch) and take
the SDF path unconditionally everywhere: tessellate Text arm (per-group by
GlyphRun presence), hit_test fallbacks, render gather, complex_lt, table,
mleader.
- Delete the text LOD ladder: tess.rs baseline/greek block and the
block_cache greek/baseline path (emit_greeked_text / emit_text_baseline),
plus the now-dead helpers text_baseline_points / text_greek_obb_tris /
text_obb_corners_native / mtext_line_count and the LocalWire
text_height_local / text_obb_local fields. All text is crisp at every zoom;
text entities are exempt from the sub-pixel stub cull.
- Fix a latent bug: block-internal SDF text was culled because its LocalWire
aabb_local was [0,0,0,0] (no points) — fold the glyph-quad extent into
aabb_local and exempt text_verts-carrying wires from the sub-pixel cull.
- Clear the pre-existing dead-code warnings (TextPlacement.elevation, TabKind,
MTextRunKind::Tab).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 21:07:00 +03:00
|
|
|
|
/// Handled in the layout matches but not emitted by the parser yet.
|
|
|
|
|
|
#[allow(dead_code)]
|
2026-05-21 23:42:22 +03:00
|
|
|
|
Tab,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
|
pub struct MTextRun {
|
|
|
|
|
|
pub kind: MTextRunKind,
|
|
|
|
|
|
pub state: RunState,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// One paragraph of MTEXT after parsing. Each line is a sequence of runs that
|
|
|
|
|
|
/// share text content + a snapshot of formatting state, plus paragraph-level
|
|
|
|
|
|
/// layout (alignment, indents, tab stops). `\P` / `\n` / `\N` start a new
|
|
|
|
|
|
/// line; paragraph properties carry forward until the next `\p...;` block.
|
|
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
|
|
pub struct MTextLine {
|
|
|
|
|
|
pub runs: Vec<MTextRun>,
|
|
|
|
|
|
pub align: Option<ParagraphAlign>,
|
|
|
|
|
|
pub indent_first: f32,
|
|
|
|
|
|
pub indent_left: f32,
|
|
|
|
|
|
pub indent_right: f32,
|
|
|
|
|
|
pub tab_stops: Vec<TabStop>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl MTextLine {
|
|
|
|
|
|
pub fn is_blank(&self) -> bool {
|
|
|
|
|
|
self.runs.iter().all(|r| match &r.kind {
|
|
|
|
|
|
MTextRunKind::Glyphs(t) => t.trim().is_empty(),
|
|
|
|
|
|
MTextRunKind::Tab => false,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 00:04:11 +03:00
|
|
|
|
/// Font-name stem: drop any path and extension, so a `\F` font path like
|
|
|
|
|
|
/// `C:\\fonts\\arial.ttf` and a plain `\f` name `arial` resolve to the same font.
|
|
|
|
|
|
fn font_stem(name: &str) -> String {
|
|
|
|
|
|
name.rsplit(['/', '\\'])
|
|
|
|
|
|
.next()
|
|
|
|
|
|
.unwrap_or(name)
|
|
|
|
|
|
.split('.')
|
|
|
|
|
|
.next()
|
|
|
|
|
|
.unwrap_or(name)
|
|
|
|
|
|
.to_string()
|
2026-05-21 23:42:22 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 00:04:11 +03:00
|
|
|
|
/// Parse an MTEXT string into the layout's `Vec<MTextLine>`, using acadrust's
|
|
|
|
|
|
/// structured `mtext_format::parse_mtext` — OCS keeps only the layout engine
|
|
|
|
|
|
/// (`layout_mtext` and callers read `MTextLine`/`RunState`), not a second MTEXT
|
|
|
|
|
|
/// inline parser.
|
2026-05-21 23:42:22 +03:00
|
|
|
|
///
|
2026-07-07 00:04:11 +03:00
|
|
|
|
/// Representation notes:
|
|
|
|
|
|
/// - DXF `%%d`/`%%p`/`%%c` arrive already resolved to Unicode from acadrust;
|
|
|
|
|
|
/// the stroke tokenizer treats those as ordinary glyphs.
|
|
|
|
|
|
/// - Stacking (`\S`) is flattened inline to `num<sep>den` (`^` for limit, else
|
|
|
|
|
|
/// `/`) since the stroke path has no fraction layout.
|
|
|
|
|
|
/// - `\H`: a relative factor (`\Hx`) applies directly; an absolute height is
|
|
|
|
|
|
/// divided by the entity height. See `acadrust::…::MTextScalar`.
|
|
|
|
|
|
pub fn adapt_mtext_paragraphs(
|
2026-05-31 13:14:35 +03:00
|
|
|
|
s: &str,
|
|
|
|
|
|
entity_height: f32,
|
|
|
|
|
|
trim_blank_edges: bool,
|
|
|
|
|
|
) -> Vec<MTextLine> {
|
2026-07-07 00:04:11 +03:00
|
|
|
|
use acadrust::entities::mtext_format::{
|
|
|
|
|
|
parse_mtext, MTextColor, MTextLineAlignment, MTextParagraphAlignment, MTextScalar,
|
|
|
|
|
|
SpanProperties, StackingType,
|
|
|
|
|
|
};
|
2026-05-21 23:42:22 +03:00
|
|
|
|
|
2026-07-07 00:04:11 +03:00
|
|
|
|
let entity_height = entity_height.max(1e-6);
|
|
|
|
|
|
let doc = parse_mtext(s, true);
|
|
|
|
|
|
|
|
|
|
|
|
fn map_align(a: MTextParagraphAlignment) -> Option<ParagraphAlign> {
|
|
|
|
|
|
match a {
|
|
|
|
|
|
MTextParagraphAlignment::Left => Some(ParagraphAlign::Left),
|
|
|
|
|
|
MTextParagraphAlignment::Right => Some(ParagraphAlign::Right),
|
|
|
|
|
|
MTextParagraphAlignment::Center => Some(ParagraphAlign::Center),
|
|
|
|
|
|
MTextParagraphAlignment::Justified => Some(ParagraphAlign::Justify),
|
|
|
|
|
|
MTextParagraphAlignment::Distributed => Some(ParagraphAlign::Distribute),
|
|
|
|
|
|
MTextParagraphAlignment::Default => None,
|
2026-05-21 23:42:22 +03:00
|
|
|
|
}
|
2026-07-07 00:04:11 +03:00
|
|
|
|
}
|
|
|
|
|
|
fn color_of(p: &SpanProperties) -> Option<InlineColor> {
|
|
|
|
|
|
if let Some((r, g, b)) = p.color_rgb {
|
|
|
|
|
|
return Some(InlineColor::True([
|
|
|
|
|
|
r as f32 / 255.0,
|
|
|
|
|
|
g as f32 / 255.0,
|
|
|
|
|
|
b as f32 / 255.0,
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
match p.color {
|
|
|
|
|
|
Some(MTextColor::Index(n)) => Some(InlineColor::Aci(n.min(255) as u8)),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-21 23:42:22 +03:00
|
|
|
|
|
2026-07-07 00:04:11 +03:00
|
|
|
|
let mut lines: Vec<MTextLine> = Vec::new();
|
|
|
|
|
|
for para in &doc.paragraphs {
|
|
|
|
|
|
let props = ¶.properties;
|
|
|
|
|
|
let mut line = MTextLine {
|
|
|
|
|
|
align: props.alignment.and_then(map_align),
|
|
|
|
|
|
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(),
|
|
|
|
|
|
runs: Vec::new(),
|
|
|
|
|
|
};
|
|
|
|
|
|
for span in ¶.spans {
|
|
|
|
|
|
let p = &span.properties;
|
|
|
|
|
|
let state = RunState {
|
|
|
|
|
|
// Relative `\H…x;` is a factor applied directly; absolute `\H…;`
|
|
|
|
|
|
// is a drawing-unit height resolved against the entity height.
|
|
|
|
|
|
height_mul: match p.height {
|
|
|
|
|
|
Some(MTextScalar::Factor(f)) => f as f32,
|
|
|
|
|
|
Some(MTextScalar::Absolute(a)) => a as f32 / entity_height,
|
|
|
|
|
|
None => 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width_mul: p.width_factor.map(|w| w as f32).unwrap_or(1.0),
|
|
|
|
|
|
oblique_rad: p
|
|
|
|
|
|
.oblique_angle
|
|
|
|
|
|
.map(|q| (q as f32).to_radians())
|
|
|
|
|
|
.unwrap_or(0.0),
|
|
|
|
|
|
tracking: p.tracking.map(|t| t as f32).unwrap_or(1.0),
|
|
|
|
|
|
valign: match p.line_align {
|
|
|
|
|
|
Some(MTextLineAlignment::Middle) => 1,
|
|
|
|
|
|
Some(MTextLineAlignment::Top) => 2,
|
|
|
|
|
|
_ => 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
font: p.font.as_ref().map(|f| font_stem(&f.name)),
|
feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:18:50 +03:00
|
|
|
|
bold: p.font.as_ref().map(|f| f.bold).unwrap_or(false),
|
2026-07-07 00:04:11 +03:00
|
|
|
|
color: color_of(p),
|
|
|
|
|
|
underline: p.stroke.underline(),
|
|
|
|
|
|
overline: p.stroke.overline(),
|
|
|
|
|
|
strike: p.stroke.strikethrough(),
|
|
|
|
|
|
};
|
|
|
|
|
|
let text = match &span.stacking {
|
|
|
|
|
|
Some(st) => {
|
|
|
|
|
|
let sep = match st.stacking_type {
|
|
|
|
|
|
StackingType::Limit => '^',
|
|
|
|
|
|
_ => '/',
|
|
|
|
|
|
};
|
|
|
|
|
|
let mut t = st.numerator.clone();
|
|
|
|
|
|
if !st.denominator.is_empty() {
|
|
|
|
|
|
t.push(sep);
|
|
|
|
|
|
t.push_str(&st.denominator);
|
2026-05-21 23:42:22 +03:00
|
|
|
|
}
|
2026-07-07 00:04:11 +03:00
|
|
|
|
t
|
2026-05-21 23:42:22 +03:00
|
|
|
|
}
|
2026-07-07 00:04:11 +03:00
|
|
|
|
None => span.text.clone(),
|
|
|
|
|
|
};
|
|
|
|
|
|
if text.is_empty() {
|
|
|
|
|
|
continue;
|
2026-05-21 23:42:22 +03:00
|
|
|
|
}
|
2026-07-07 00:04:11 +03:00
|
|
|
|
line.runs.push(MTextRun {
|
|
|
|
|
|
kind: MTextRunKind::Glyphs(text),
|
|
|
|
|
|
state,
|
|
|
|
|
|
});
|
2026-05-21 23:42:22 +03:00
|
|
|
|
}
|
2026-07-07 00:04:11 +03:00
|
|
|
|
lines.push(line);
|
2026-05-21 23:42:22 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 13:14:35 +03:00
|
|
|
|
if !trim_blank_edges {
|
|
|
|
|
|
return lines;
|
|
|
|
|
|
}
|
2026-05-21 23:42:22 +03:00
|
|
|
|
let start = lines.iter().position(|l| !l.is_blank()).unwrap_or(0);
|
|
|
|
|
|
let end = lines
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.rposition(|l| !l.is_blank())
|
|
|
|
|
|
.map(|i| i + 1)
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
lines[start..end].to_vec()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
// Legacy MText helpers (`strip_mtext_codes`, `split_mtext_lines`,
|
|
|
|
|
|
// `measure_mtext_chars`, `word_wrap`) were removed when every text-bearing
|
|
|
|
|
|
// entity switched to the run-aware pipeline below. The pipeline now owns
|
2026-07-07 00:04:11 +03:00
|
|
|
|
// per-run width measurement and word-wrap; MTEXT inline parsing now comes from
|
|
|
|
|
|
// acadrust via `adapt_mtext_paragraphs`. The supported surface for callers is
|
|
|
|
|
|
// `adapt_mtext_paragraphs`, `layout_mtext`, `mtext_line_count`,
|
|
|
|
|
|
// `text_local_bounds`, and `resolve_dxf_special_chars`.
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
// Shared MText layout / render pipeline
|
|
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// `layout_mtext` is the entry point used by every text-bearing entity that
|
|
|
|
|
|
// stores MText-formatted content (MTEXT, MLEADER text content, TABLE cell,
|
|
|
|
|
|
// ATTRIB / ATTDEF with `mtext_flag` set, and DIMENSION `text_override` when
|
|
|
|
|
|
// it carries inline codes).
|
|
|
|
|
|
//
|
|
|
|
|
|
// The pipeline mirrors the MTEXT renderer:
|
2026-07-07 00:04:11 +03:00
|
|
|
|
// 1. Parse — via `adapt_mtext_paragraphs` (acadrust `parse_mtext`).
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
// 2. Atomise — turn each MTextLine.runs into a flat sequence of atoms
|
|
|
|
|
|
// (Word / Space / Tab) so the wrapper operates at break boundaries
|
|
|
|
|
|
// while keeping per-character formatting state.
|
|
|
|
|
|
// 3. Wrap — accumulate atoms into sub-lines using paragraph indents and
|
|
|
|
|
|
// tab stops; each Tab jumps the cursor to the next user-defined stop
|
|
|
|
|
|
// (or a 4-em default grid).
|
|
|
|
|
|
// 4. Render — for each 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).
|
|
|
|
|
|
//
|
|
|
|
|
|
// In addition to the strokes, the helper returns enough geometry (line
|
|
|
|
|
|
// widths, line height, v_offset, h_anchor) for the caller to draw a frame /
|
|
|
|
|
|
// background rectangle, run a low-detail LOD path, or compute snap bounds.
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
|
pub enum AtomKind {
|
|
|
|
|
|
Word(String),
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Tab,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
|
pub struct LayoutAtom {
|
|
|
|
|
|
pub kind: AtomKind,
|
|
|
|
|
|
pub state: RunState,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub 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())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-28 18:44:38 +01:00
|
|
|
|
pub fn resolve_font<'a>(state: &'a RunState, base: &'a str) -> std::borrow::Cow<'a, str> {
|
|
|
|
|
|
let Some(font) = state.font.as_deref().map(str::trim).filter(|f| !f.is_empty()) else {
|
|
|
|
|
|
return std::borrow::Cow::Borrowed(base);
|
|
|
|
|
|
};
|
|
|
|
|
|
if lff::is_builtin(font) {
|
|
|
|
|
|
return std::borrow::Cow::Borrowed(font);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(canonical) = crate::scene::text::sysfont::canonical_family_name(font) {
|
|
|
|
|
|
std::borrow::Cow::Owned(canonical)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
std::borrow::Cow::Borrowed(base)
|
|
|
|
|
|
}
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub 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);
|
2026-06-28 18:44:38 +01:00
|
|
|
|
let face = Face::resolve(&font_name);
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
let mut w = 0.0_f32;
|
|
|
|
|
|
for ch in text.chars() {
|
2026-06-16 11:18:17 +03:00
|
|
|
|
w += match face.glyph(ch) {
|
|
|
|
|
|
Some(g) => (g.advance + face.letter_spacing() * state.tracking) * scale,
|
|
|
|
|
|
None => (6.0 + face.letter_spacing() * state.tracking) * scale,
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
w
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub 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);
|
2026-06-28 18:44:38 +01:00
|
|
|
|
Face::resolve(&font_name).word_spacing() * scale
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub 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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
|
|
pub fn next_tab_position(
|
|
|
|
|
|
cur_x: f32,
|
|
|
|
|
|
tab_stops: &[TabStop],
|
|
|
|
|
|
indent_left: f32,
|
|
|
|
|
|
entity_h: f32,
|
|
|
|
|
|
) -> f32 {
|
|
|
|
|
|
let local = cur_x - indent_left;
|
|
|
|
|
|
for ts in tab_stops {
|
|
|
|
|
|
if ts.position > local + 1e-4 {
|
|
|
|
|
|
return indent_left + ts.position;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let default_interval = entity_h * 4.0;
|
|
|
|
|
|
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.
|
|
|
|
|
|
pub 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();
|
2026-05-01 11:51:45 +03:00
|
|
|
|
}
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
sublines.push(std::mem::take(&mut cur));
|
|
|
|
|
|
cur_w = 0.0;
|
|
|
|
|
|
subline_idx += 1;
|
2026-05-01 11:51:45 +03:00
|
|
|
|
}
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
cur.push(atom);
|
|
|
|
|
|
cur_w += w;
|
|
|
|
|
|
}
|
|
|
|
|
|
AtomKind::Space => {
|
|
|
|
|
|
if cur.is_empty() {
|
|
|
|
|
|
continue;
|
2026-05-12 10:36:15 +03:00
|
|
|
|
}
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
if !cur.is_empty() {
|
|
|
|
|
|
sublines.push(cur);
|
|
|
|
|
|
}
|
|
|
|
|
|
if sublines.is_empty() {
|
|
|
|
|
|
sublines.push(Vec::new());
|
|
|
|
|
|
}
|
|
|
|
|
|
sublines
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
pub 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
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
pub 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),
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
/// Wrap a run's glyph text with MTEXT decoration markers so lff's
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
/// `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();
|
2026-04-26 21:57:51 +03:00
|
|
|
|
}
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
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
|
2026-04-26 21:57:51 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
|
pub enum MTextVAnchor {
|
|
|
|
|
|
/// Block top edge at insertion (first line's cap = insertion.y).
|
|
|
|
|
|
Top,
|
|
|
|
|
|
/// Block midpoint at insertion.
|
|
|
|
|
|
Middle,
|
|
|
|
|
|
/// Block bottom edge at insertion (last line's baseline = insertion.y).
|
|
|
|
|
|
Bottom,
|
|
|
|
|
|
/// MLEADER `MiddleOfTopLine` — first line's vertical centre at insertion.
|
|
|
|
|
|
MiddleOfTopLine,
|
|
|
|
|
|
/// MLEADER `MiddleOfBottomLine` — last line's vertical centre at insertion.
|
|
|
|
|
|
MiddleOfBottomLine,
|
|
|
|
|
|
/// MLEADER `BottomOfTopLineUnderline*` — first line's baseline at insertion.
|
|
|
|
|
|
BottomOfTopLine,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Render inputs for [`layout_mtext`]. The caller resolves the text style
|
|
|
|
|
|
/// once and feeds the entity's geometry; the helper handles the entire
|
|
|
|
|
|
/// parse → wrap → render pipeline and returns both the rendered strokes and
|
|
|
|
|
|
/// the layout metrics (so callers can also draw frames / fills / LOD
|
|
|
|
|
|
/// substitutes from the same numbers).
|
|
|
|
|
|
pub struct MTextRenderOpts<'a> {
|
|
|
|
|
|
/// Raw MText-formatted value (the string the parser walks).
|
|
|
|
|
|
pub value: &'a str,
|
|
|
|
|
|
/// World-space insertion point — strokes are emitted with this as their
|
|
|
|
|
|
/// origin (after the per-sub-line rotation + cursor offset).
|
|
|
|
|
|
pub insertion: [f64; 3],
|
|
|
|
|
|
/// Entity text height in world units.
|
|
|
|
|
|
pub height: f32,
|
|
|
|
|
|
/// Box width for word-wrap (0 → no wrap; lines flow at the insertion).
|
|
|
|
|
|
pub rect_w: f32,
|
|
|
|
|
|
/// Final rotation in radians (already composed with `is_upside_down`).
|
|
|
|
|
|
pub rotation: f32,
|
|
|
|
|
|
/// Resolved style (font + width factor + oblique). Width factor sign
|
|
|
|
|
|
/// honours `is_backward` (negative → mirror).
|
|
|
|
|
|
pub style: &'a ResolvedTextStyle,
|
|
|
|
|
|
/// Horizontal anchor of the text block at the insertion point:
|
|
|
|
|
|
/// 0.0 = left, 0.5 = center, 1.0 = right.
|
|
|
|
|
|
pub attach_h_anchor: f32,
|
|
|
|
|
|
/// Vertical anchor of the text block at the insertion point.
|
|
|
|
|
|
pub v_anchor: MTextVAnchor,
|
|
|
|
|
|
/// DXF code 44 — multiplier on the default 5/3-em baseline gap.
|
|
|
|
|
|
pub line_spacing_factor: f32,
|
|
|
|
|
|
/// `true` when the entity is laid out top-to-bottom (DXF code 71 = 2).
|
|
|
|
|
|
pub vertical_text: bool,
|
2026-05-31 02:33:47 +03:00
|
|
|
|
/// When true, `layout_mtext` also fills `MTextLayout::glyph_boxes` with
|
|
|
|
|
|
/// one world-space box per visible character (used by the MText editor's
|
|
|
|
|
|
/// click-to-select preview). Off in the hot render path.
|
|
|
|
|
|
pub want_glyph_boxes: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// One selectable character in the laid-out text: its world-space AABB plus
|
|
|
|
|
|
/// the running index of visible characters (in reading order) so the editor
|
|
|
|
|
|
/// can map a clicked box back to an offset in the value.
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
|
|
pub struct GlyphBox {
|
|
|
|
|
|
pub vis: usize,
|
|
|
|
|
|
pub xmin: f32,
|
|
|
|
|
|
pub xmax: f32,
|
|
|
|
|
|
pub ymin: f32,
|
|
|
|
|
|
pub ymax: f32,
|
2026-05-15 17:41:56 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
/// Output of [`layout_mtext`]: stroke groups + the geometry the caller
|
|
|
|
|
|
/// needs for surrounding chrome (frame / fill / LOD baseline-or-rect).
|
|
|
|
|
|
pub struct MTextLayout {
|
|
|
|
|
|
/// One TextStroke per Word atom (Tab / Space contribute only to cursor
|
|
|
|
|
|
/// advance). The `color` field on each stroke carries the inline
|
|
|
|
|
|
/// `\C` / `\c` override when one was set, otherwise `None`.
|
|
|
|
|
|
pub strokes: Vec<TextStroke>,
|
|
|
|
|
|
/// Per-sub-line width in entity-local (pre-rotation) units. Includes
|
|
|
|
|
|
/// any trailing whitespace that survived the trim — kept in sync with
|
|
|
|
|
|
/// the cursor advance so the alignment numbers and the visible glyphs
|
|
|
|
|
|
/// line up.
|
|
|
|
|
|
pub line_widths: Vec<f32>,
|
|
|
|
|
|
/// Sub-line count (≥ 1; an entity with an empty value still reports 1).
|
|
|
|
|
|
pub line_count: usize,
|
|
|
|
|
|
/// Baseline-to-baseline gap used when stepping between sub-lines.
|
|
|
|
|
|
pub line_height: f32,
|
|
|
|
|
|
/// Y of the first sub-line's baseline relative to the insertion point
|
|
|
|
|
|
/// (in the entity-local, pre-rotation frame).
|
|
|
|
|
|
pub v_offset: f32,
|
2026-05-31 02:33:47 +03:00
|
|
|
|
/// One world-space AABB per visible character — only populated when
|
|
|
|
|
|
/// `MTextRenderOpts::want_glyph_boxes` is set.
|
|
|
|
|
|
pub glyph_boxes: Vec<GlyphBox>,
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Lay out and render an MText-formatted value, returning the stroke
|
|
|
|
|
|
/// groups plus the layout metrics needed by callers that draw chrome
|
|
|
|
|
|
/// (text frame, background fill, low-detail LOD substitutes) around the
|
|
|
|
|
|
/// text block.
|
|
|
|
|
|
pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|
|
|
|
|
let base_font_name = opts.style.font_name.clone();
|
2026-06-16 11:18:17 +03:00
|
|
|
|
let base_font = Face::resolve(&base_font_name);
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
let base_wf_abs = opts.style.width_factor.max(0.01);
|
|
|
|
|
|
let base_wf = if opts.style.is_backward { -base_wf_abs } else { base_wf_abs };
|
|
|
|
|
|
let base_oblique = opts.style.oblique_angle;
|
|
|
|
|
|
let entity_h = opts.height;
|
|
|
|
|
|
let rect_w = opts.rect_w;
|
|
|
|
|
|
|
|
|
|
|
|
// ── 1. Parse ─────────────────────────────────────────────────────────
|
2026-05-31 13:14:35 +03:00
|
|
|
|
// The editor (want_glyph_boxes) keeps blank edges so a freshly typed
|
|
|
|
|
|
// trailing newline yields an empty paragraph the caret can sit on.
|
2026-07-07 00:04:11 +03:00
|
|
|
|
let paragraphs = adapt_mtext_paragraphs(opts.value, entity_h, !opts.want_glyph_boxes);
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
|
|
|
|
|
|
// ── 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,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
let mut sub_lines: Vec<SubLine> = Vec::new();
|
|
|
|
|
|
for para in ¶graphs {
|
|
|
|
|
|
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(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
// Trim leading + trailing Space atoms so line_w / cursor_start agree
|
|
|
|
|
|
// on the paragraph's visible content. Without this a stray trailing
|
|
|
|
|
|
// space measures wider than it draws and centring / right-alignment
|
|
|
|
|
|
// is off by half a space-width.
|
2026-05-31 13:14:35 +03:00
|
|
|
|
//
|
|
|
|
|
|
// Skipped when emitting glyph boxes (the MText editor) so a space the
|
|
|
|
|
|
// user just typed at the end keeps a selectable box and the caret can
|
|
|
|
|
|
// sit after it.
|
|
|
|
|
|
if !opts.want_glyph_boxes {
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
});
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
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 opts.line_spacing_factor > 0.0 {
|
|
|
|
|
|
opts.line_spacing_factor
|
|
|
|
|
|
} else {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
};
|
|
|
|
|
|
// DXF code 44 — multiplier on the default 5/3-em baseline-to-baseline gap.
|
2026-06-16 11:18:17 +03:00
|
|
|
|
let line_h = entity_h * ls_factor * (5.0 / 3.0) * base_font.line_spacing();
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
let h = entity_h;
|
|
|
|
|
|
let v_offset = match opts.v_anchor {
|
|
|
|
|
|
MTextVAnchor::Top => -h,
|
|
|
|
|
|
MTextVAnchor::Middle => ((n_lines - 1.0) * line_h - h) * 0.5,
|
|
|
|
|
|
MTextVAnchor::Bottom => (n_lines - 1.0) * line_h,
|
|
|
|
|
|
MTextVAnchor::MiddleOfTopLine => -h * 0.5,
|
|
|
|
|
|
MTextVAnchor::MiddleOfBottomLine => (n_lines - 1.0) * line_h - h * 0.5,
|
|
|
|
|
|
MTextVAnchor::BottomOfTopLine => 0.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
let attach_h_anchor = opts.attach_h_anchor;
|
|
|
|
|
|
let box_left = -attach_h_anchor * rect_w;
|
|
|
|
|
|
let rot = opts.rotation;
|
|
|
|
|
|
let (cos_r, sin_r) = (rot.cos(), rot.sin());
|
|
|
|
|
|
let ins_x = opts.insertion[0];
|
|
|
|
|
|
let ins_y = opts.insertion[1];
|
|
|
|
|
|
|
|
|
|
|
|
// ── 4. Render each sub-line ──────────────────────────────────────────
|
|
|
|
|
|
let mut all_strokes: Vec<TextStroke> = Vec::new();
|
|
|
|
|
|
let mut line_widths: Vec<f32> = Vec::with_capacity(sub_lines.len());
|
2026-05-31 02:33:47 +03:00
|
|
|
|
let mut glyph_boxes: Vec<GlyphBox> = Vec::new();
|
|
|
|
|
|
let mut vis: usize = 0;
|
|
|
|
|
|
// Transform an entity-local point to world space (mirrors the stroke
|
|
|
|
|
|
// origin maths) so glyph boxes line up with the drawn glyphs.
|
|
|
|
|
|
let to_world = |line_base_x: f32, line_base_y: f32, lx: f32, ly: f32| -> (f32, f32) {
|
|
|
|
|
|
let wdx = lx * cos_r - ly * sin_r;
|
|
|
|
|
|
let wdy = lx * sin_r + ly * cos_r;
|
|
|
|
|
|
(
|
|
|
|
|
|
ins_x as f32 + line_base_x + wdx,
|
|
|
|
|
|
ins_y as f32 + line_base_y + wdy,
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
for (i, sub) in sub_lines.iter().enumerate() {
|
|
|
|
|
|
let li = i as f32;
|
|
|
|
|
|
let (line_base_x, line_base_y) = if opts.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)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
);
|
|
|
|
|
|
line_widths.push(line_w);
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let line_max_h = sub
|
|
|
|
|
|
.atoms
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|a| a.state.height_mul * entity_h)
|
|
|
|
|
|
.fold(entity_h, f32::max);
|
|
|
|
|
|
|
2026-05-31 13:14:35 +03:00
|
|
|
|
// A paragraph break (explicit `\n` / `\P`) that started this line gets
|
|
|
|
|
|
// a zero-width caret slot at the line start, so the MText editor can
|
|
|
|
|
|
// place the caret on a fresh/empty line.
|
|
|
|
|
|
if opts.want_glyph_boxes && i > 0 && sub.is_first_in_paragraph {
|
|
|
|
|
|
let (ax, ay) = to_world(line_base_x, line_base_y, cursor_start, 0.0);
|
|
|
|
|
|
let (_, by) = to_world(line_base_x, line_base_y, cursor_start, entity_h);
|
|
|
|
|
|
glyph_boxes.push(GlyphBox {
|
|
|
|
|
|
vis,
|
|
|
|
|
|
xmin: ax,
|
|
|
|
|
|
xmax: ax,
|
|
|
|
|
|
ymin: ay.min(by),
|
|
|
|
|
|
ymax: ay.max(by),
|
|
|
|
|
|
});
|
|
|
|
|
|
vis += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
let mut cursor_x = cursor_start;
|
|
|
|
|
|
for atom in &sub.atoms {
|
|
|
|
|
|
match &atom.kind {
|
|
|
|
|
|
AtomKind::Word(text) => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
];
|
2026-06-25 18:17:01 +01:00
|
|
|
|
let (strokes, fill_tris) = lff::tessellate_text_run(
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
[0.0, 0.0],
|
|
|
|
|
|
run_h,
|
|
|
|
|
|
rot,
|
|
|
|
|
|
signed_wf,
|
|
|
|
|
|
oblique,
|
|
|
|
|
|
tracking,
|
2026-06-28 18:44:38 +01:00
|
|
|
|
&font_name,
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
&body,
|
|
|
|
|
|
);
|
|
|
|
|
|
all_strokes.push(TextStroke {
|
|
|
|
|
|
strokes,
|
|
|
|
|
|
origin,
|
|
|
|
|
|
color,
|
2026-06-25 18:17:01 +01:00
|
|
|
|
fill_tris,
|
2026-07-07 00:53:19 +03:00
|
|
|
|
// `text` is the plain word (specials already resolved,
|
|
|
|
|
|
// no decoration markers); `run_h` is raw like the strokes.
|
|
|
|
|
|
run: Some(GlyphRun {
|
|
|
|
|
|
text: text.clone(),
|
|
|
|
|
|
font: font_name.to_string(),
|
|
|
|
|
|
height: run_h,
|
|
|
|
|
|
rotation: rot,
|
|
|
|
|
|
width_factor: signed_wf,
|
|
|
|
|
|
oblique,
|
|
|
|
|
|
tracking,
|
feat(mtext): structured editor with selection, copy, and visible bold
Rewrite the in-app MTEXT editor to run on acadrust's structured
MTextDocument model end-to-end: load via parse_mtext, edit a flat Cell
model, serialize via to_mtext_string. Retires the hand-rolled format-code
string handling, the parallel visible_spans parser, the composed_value
prefix, and the Edit-raw-code tab.
Selection and copy:
- double-click selects the word, triple-click selects all (click-counting)
- Ctrl+A selects all text
- Ctrl+C copies the selected text to the clipboard
Formatting:
- per-selection Bold/Italic/Underline/Overline/Strike/Case/Alignment
- Bold renders as a visibly thicker SDF pen, keyed (family, char, bold)
through the whole text pipeline; the resolved style font name is stamped
so the \f...|b1; code survives serialization
- per-selection font and color changes
- editor color dropdown reuses the Properties panel color selector
Global toolbar defaults (font/color/oblique/width/tracking) fold onto the
document as real span properties instead of a leading code prefix; a
trailing empty paragraph is preserved so Enter at the end keeps its line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:18:50 +03:00
|
|
|
|
bold: atom.state.bold,
|
2026-07-07 00:53:19 +03:00
|
|
|
|
}),
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
});
|
2026-05-31 02:33:47 +03:00
|
|
|
|
if opts.want_glyph_boxes {
|
|
|
|
|
|
// Per-character boxes, advancing exactly as
|
|
|
|
|
|
// `measure_word` does so they track the glyphs.
|
|
|
|
|
|
let scale = run_scale(&atom.state, entity_h, base_wf);
|
2026-06-28 18:44:38 +01:00
|
|
|
|
let face = Face::resolve(&font_name);
|
2026-05-31 02:33:47 +03:00
|
|
|
|
let mut cx = cursor_x;
|
|
|
|
|
|
for ch in text.chars() {
|
2026-06-16 11:18:17 +03:00
|
|
|
|
let adv = match face.glyph(ch) {
|
2026-05-31 02:33:47 +03:00
|
|
|
|
Some(g) => {
|
2026-06-16 11:18:17 +03:00
|
|
|
|
(g.advance + face.letter_spacing() * tracking) * scale
|
2026-05-31 02:33:47 +03:00
|
|
|
|
}
|
2026-06-16 11:18:17 +03:00
|
|
|
|
None => (6.0 + face.letter_spacing() * tracking) * scale,
|
2026-05-31 02:33:47 +03:00
|
|
|
|
};
|
|
|
|
|
|
let (ax, ay) = to_world(line_base_x, line_base_y, cx, ly);
|
|
|
|
|
|
let (bx, by) = to_world(line_base_x, line_base_y, cx + adv, ly + run_h);
|
|
|
|
|
|
glyph_boxes.push(GlyphBox {
|
|
|
|
|
|
vis,
|
|
|
|
|
|
xmin: ax.min(bx),
|
|
|
|
|
|
xmax: ax.max(bx),
|
|
|
|
|
|
ymin: ay.min(by),
|
|
|
|
|
|
ymax: ay.max(by),
|
|
|
|
|
|
});
|
|
|
|
|
|
vis += 1;
|
|
|
|
|
|
cx += adv;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
cursor_x +=
|
|
|
|
|
|
measure_word(text, &atom.state, entity_h, base_wf, &base_font_name);
|
|
|
|
|
|
}
|
|
|
|
|
|
AtomKind::Space => {
|
2026-05-31 02:33:47 +03:00
|
|
|
|
let adv = measure_space(&atom.state, entity_h, base_wf, &base_font_name);
|
|
|
|
|
|
if opts.want_glyph_boxes {
|
|
|
|
|
|
let run_h = atom.state.height_mul * entity_h;
|
|
|
|
|
|
let (ax, ay) = to_world(line_base_x, line_base_y, cursor_x, 0.0);
|
|
|
|
|
|
let (bx, by) =
|
|
|
|
|
|
to_world(line_base_x, line_base_y, cursor_x + adv, run_h);
|
|
|
|
|
|
glyph_boxes.push(GlyphBox {
|
|
|
|
|
|
vis,
|
|
|
|
|
|
xmin: ax.min(bx),
|
|
|
|
|
|
xmax: ax.max(bx),
|
|
|
|
|
|
ymin: ay.min(by),
|
|
|
|
|
|
ymax: ay.max(by),
|
|
|
|
|
|
});
|
|
|
|
|
|
vis += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
cursor_x += adv;
|
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>
2026-05-22 05:53:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
AtomKind::Tab => {
|
|
|
|
|
|
cursor_x = next_tab_position(
|
|
|
|
|
|
cursor_x,
|
|
|
|
|
|
&sub.tab_stops,
|
|
|
|
|
|
sub.indent_left,
|
|
|
|
|
|
entity_h,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
MTextLayout {
|
|
|
|
|
|
strokes: all_strokes,
|
|
|
|
|
|
line_widths,
|
|
|
|
|
|
line_count: sub_lines.len(),
|
|
|
|
|
|
line_height: line_h,
|
|
|
|
|
|
v_offset,
|
2026-05-31 02:33:47 +03:00
|
|
|
|
glyph_boxes,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-23 23:26:14 +03:00
|
|
|
|
|
2026-06-28 18:44:38 +01:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
fn style(font_name: &str) -> ResolvedTextStyle {
|
|
|
|
|
|
ResolvedTextStyle {
|
|
|
|
|
|
font_name: font_name.to_string(),
|
|
|
|
|
|
width_factor: 1.0,
|
|
|
|
|
|
oblique_angle: 0.0,
|
|
|
|
|
|
is_backward: false,
|
|
|
|
|
|
is_upside_down: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn stroke_point_count(layout: &MTextLayout) -> usize {
|
|
|
|
|
|
layout
|
|
|
|
|
|
.strokes
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.strokes.iter().map(Vec::len).sum::<usize>() + s.fill_tris.len())
|
|
|
|
|
|
.sum()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn unresolved_inline_font_falls_back_to_style_font() {
|
|
|
|
|
|
let base = "txt";
|
|
|
|
|
|
let mut state = RunState::default();
|
|
|
|
|
|
state.font = Some("__definitely_not_an_installed_font__".to_string());
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(resolve_font(&state, base), base);
|
|
|
|
|
|
|
|
|
|
|
|
let layout = layout_mtext(&MTextRenderOpts {
|
|
|
|
|
|
value: "{\\f__definitely_not_an_installed_font__|b0|i0|c0|p34;Storage Units}",
|
|
|
|
|
|
insertion: [0.0, 0.0, 0.0],
|
|
|
|
|
|
height: 2.5,
|
|
|
|
|
|
rect_w: 0.0,
|
|
|
|
|
|
rotation: 0.0,
|
|
|
|
|
|
style: &style(base),
|
|
|
|
|
|
attach_h_anchor: 0.0,
|
|
|
|
|
|
v_anchor: MTextVAnchor::Top,
|
|
|
|
|
|
line_spacing_factor: 1.0,
|
|
|
|
|
|
vertical_text: false,
|
|
|
|
|
|
want_glyph_boxes: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
stroke_point_count(&layout) > 0,
|
|
|
|
|
|
"unresolvable inline \\f should render through the style font"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn block_style_font_name_from_ttf_file_renders_mtext() {
|
|
|
|
|
|
let layout = layout_mtext(&MTextRenderOpts {
|
|
|
|
|
|
value: "FERRAGAMO",
|
|
|
|
|
|
insertion: [0.0, 0.0, 0.0],
|
|
|
|
|
|
height: 20.0,
|
|
|
|
|
|
rect_w: 0.0,
|
|
|
|
|
|
rotation: 0.0,
|
|
|
|
|
|
style: &style("arial"),
|
|
|
|
|
|
attach_h_anchor: 0.0,
|
|
|
|
|
|
v_anchor: MTextVAnchor::Top,
|
|
|
|
|
|
line_spacing_factor: 1.0,
|
|
|
|
|
|
vertical_text: false,
|
|
|
|
|
|
want_glyph_boxes: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
stroke_point_count(&layout) > 0,
|
|
|
|
|
|
"style font derived from arial.ttf should produce drawable block text"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-07 00:04:11 +03:00
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod adapter_tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
/// Glyph text + state of the first renderable run.
|
|
|
|
|
|
fn first_run(lines: &[MTextLine]) -> (String, RunState) {
|
|
|
|
|
|
for l in lines {
|
|
|
|
|
|
for r in &l.runs {
|
|
|
|
|
|
if let MTextRunKind::Glyphs(t) = &r.kind {
|
|
|
|
|
|
return (t.clone(), r.state.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
panic!("no glyph run produced");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn plain_text_is_default_state() {
|
|
|
|
|
|
let lines = adapt_mtext_paragraphs("Hello", 2.5, true);
|
|
|
|
|
|
assert_eq!(lines.len(), 1);
|
|
|
|
|
|
let (t, st) = first_run(&lines);
|
|
|
|
|
|
assert_eq!(t, "Hello");
|
|
|
|
|
|
assert_eq!(st, RunState::default());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn relative_height_is_a_factor() {
|
|
|
|
|
|
// `\H2x;` multiplies the current height → height_mul 2.0, independent of
|
|
|
|
|
|
// the entity height. (This is the case that needed acadrust's MTextScalar.)
|
|
|
|
|
|
let (_, st) = first_run(&adapt_mtext_paragraphs("\\H2x;big", 2.5, true));
|
|
|
|
|
|
assert!((st.height_mul - 2.0).abs() < 1e-4, "got {}", st.height_mul);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn absolute_height_resolves_against_entity_height() {
|
|
|
|
|
|
// `\H5;` is an absolute height → divided by the entity height (2.5) → 2.0.
|
|
|
|
|
|
let (_, st) = first_run(&adapt_mtext_paragraphs("\\H5;abs", 2.5, true));
|
|
|
|
|
|
assert!((st.height_mul - 2.0).abs() < 1e-4, "got {}", st.height_mul);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn color_font_oblique_valign_decoration() {
|
|
|
|
|
|
let (_, c) = first_run(&adapt_mtext_paragraphs("\\C1;red", 2.5, true));
|
|
|
|
|
|
assert_eq!(c.color, Some(InlineColor::Aci(1)));
|
|
|
|
|
|
|
|
|
|
|
|
let (_, f) = first_run(&adapt_mtext_paragraphs("\\fArial;x", 2.5, true));
|
|
|
|
|
|
assert_eq!(f.font.as_deref(), Some("Arial"));
|
|
|
|
|
|
|
|
|
|
|
|
let (_, q) = first_run(&adapt_mtext_paragraphs("\\Q15;x", 2.5, true));
|
|
|
|
|
|
assert!((q.oblique_rad - 15f32.to_radians()).abs() < 1e-4);
|
|
|
|
|
|
|
|
|
|
|
|
let (_, w) = first_run(&adapt_mtext_paragraphs("\\W1.5;x", 2.5, true));
|
|
|
|
|
|
assert!((w.width_mul - 1.5).abs() < 1e-4);
|
|
|
|
|
|
|
|
|
|
|
|
let (_, a) = first_run(&adapt_mtext_paragraphs("\\A1;x", 2.5, true));
|
|
|
|
|
|
assert_eq!(a.valign, 1);
|
|
|
|
|
|
|
|
|
|
|
|
let (_, d) = first_run(&adapt_mtext_paragraphs("\\Lunder\\l", 2.5, true));
|
|
|
|
|
|
assert!(d.underline);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn stacking_flattens_like_the_legacy_parser() {
|
|
|
|
|
|
let (t, _) = first_run(&adapt_mtext_paragraphs("\\S1/2;", 2.5, true));
|
|
|
|
|
|
assert_eq!(t, "1/2");
|
|
|
|
|
|
let (t, _) = first_run(&adapt_mtext_paragraphs("\\S1^2;", 2.5, true));
|
|
|
|
|
|
assert_eq!(t, "1^2");
|
|
|
|
|
|
// Diagonal `#` renders with a `/` separator.
|
|
|
|
|
|
let (t, _) = first_run(&adapt_mtext_paragraphs("\\S1#2;", 2.5, true));
|
|
|
|
|
|
assert_eq!(t, "1/2");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn paragraph_breaks_split_lines() {
|
|
|
|
|
|
let lines = adapt_mtext_paragraphs("a\\Pb\\Pc", 2.5, true);
|
|
|
|
|
|
assert_eq!(lines.len(), 3);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|