forked from 1b-agent/opencadstudio-upstream
Fields: exact field boxes for rotated, multi-line and shaped text
Layout glyph cells carry their unrotated cell, so a field box in rotated MTEXT is placed exactly; multi-line attributes box each line of a field from their own layout cells and single-line attributes split on \P per line; shaped words (TrueType fonts, right-to-left and complex scripts) take the shaper's glyph cells for their characters instead of evenly divided slots. The field background is also drawn for text rendered as shaped strokes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
383eba14e5
commit
3830a69ca7
4 files changed
+273
-75
No files matched your search
@@ -101,7 +101,21 @@ fn mtext_flag_str(f: MTextFlag) -> &'static str {
|
||||
/// Render text strokes for an attribute, honouring alignment, oblique angle,
|
||||
/// width factor, generation flags (backward / upside-down), text-style
|
||||
/// resolution, and basic multiline splitting on `\n` / `\\P`.
|
||||
fn build_attr_render(mut input: AttrTextInputs<'_>, document: &codec::CadDocument) -> RenderEntity {
|
||||
/// Laid-out character cells of a multi-line (MTEXT) attribute: the layout's
|
||||
/// glyph boxes, the point they are relative to and the layout rotation.
|
||||
pub(crate) type AttrCells = (Vec<crate::entities::text_support::GlyphBox>, [f64; 2], f32);
|
||||
|
||||
fn build_attr_render(input: AttrTextInputs<'_>, document: &codec::CadDocument) -> RenderEntity {
|
||||
build_attr_layout(input, document, None)
|
||||
}
|
||||
|
||||
/// [`build_attr_render`], also handing back a multi-line attribute's
|
||||
/// character cells in `cells` (left `None` for a single-line one).
|
||||
fn build_attr_layout(
|
||||
mut input: AttrTextInputs<'_>,
|
||||
document: &codec::CadDocument,
|
||||
mut cells: Option<&mut Option<AttrCells>>,
|
||||
) -> RenderEntity {
|
||||
let normal = (input.normal.x, input.normal.y, input.normal.z);
|
||||
let (wsx, wsy, wsz) = transform::ocs_point_to_wcs(
|
||||
(
|
||||
@@ -248,8 +262,11 @@ fn build_attr_render(mut input: AttrTextInputs<'_>, document: &codec::CadDocumen
|
||||
exact_line_spacing: false,
|
||||
rectangle_height: 0.0,
|
||||
vertical_text: false,
|
||||
want_glyph_boxes: false,
|
||||
want_glyph_boxes: cells.is_some(),
|
||||
});
|
||||
if let Some(out) = cells.as_deref_mut() {
|
||||
*out = Some((layout.glyph_boxes.clone(), [anchor_pt.x, anchor_pt.y], rotation));
|
||||
}
|
||||
let _ = input.line_count;
|
||||
let _ = input.is_multiline;
|
||||
return RenderEntity {
|
||||
@@ -839,6 +856,35 @@ impl Transformable for AttributeDefinition {
|
||||
|
||||
// ── AttributeEntity ───────────────────────────────────────────────────────────
|
||||
|
||||
/// The laid-out character cells of a multi-line (MTEXT) attribute, the
|
||||
/// point they are relative to and the layout rotation; `None` for a
|
||||
/// single-line attribute.
|
||||
pub(crate) fn attribute_cells(a: &AttributeEntity, document: &codec::CadDocument) -> Option<AttrCells> {
|
||||
let mut out = None;
|
||||
build_attr_layout(attr_inputs(a), document, Some(&mut out));
|
||||
out
|
||||
}
|
||||
|
||||
fn attr_inputs(a: &AttributeEntity) -> AttrTextInputs<'_> {
|
||||
AttrTextInputs {
|
||||
value: &a.value,
|
||||
insertion_point: a.insertion_point,
|
||||
alignment_point: a.alignment_point,
|
||||
height: a.height,
|
||||
rotation: a.rotation,
|
||||
width_factor: a.width_factor,
|
||||
oblique_angle: a.oblique_angle,
|
||||
text_style: &a.text_style,
|
||||
text_generation_flags: a.text_generation_flags,
|
||||
horizontal_alignment: a.horizontal_alignment,
|
||||
vertical_alignment: a.vertical_alignment,
|
||||
normal: a.normal,
|
||||
mtext_flag: a.mtext_flag,
|
||||
is_multiline: a.is_multiline,
|
||||
line_count: a.line_count,
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderConvertible for AttributeEntity {
|
||||
fn to_render(&self, document: &codec::CadDocument) -> Option<RenderEntity> {
|
||||
Some(build_attr_render(
|
||||
|
||||
@@ -237,6 +237,42 @@ pub fn text_local_bounds(
|
||||
}
|
||||
}
|
||||
|
||||
/// Each character's [left, right] pen cell along a TEXT line (DXF specials
|
||||
/// already resolved), measured as [`text_local_bounds`] lays the line out:
|
||||
/// the shaper's glyph cells for a TrueType font or a script that needs
|
||||
/// shaping, else the stroke font's advances.
|
||||
pub fn text_char_cells(font_name: &str, text: &str, height: f32, width_factor: f32) -> Vec<(f32, f32)> {
|
||||
let face = Face::resolve(font_name);
|
||||
let scale = height / 9.0;
|
||||
let wf = width_factor.abs().clamp(0.01, 100.0);
|
||||
let shaping_family = face.ttf_family().or_else(|| {
|
||||
crate::scene::text::web_font::requires_shaping(text)
|
||||
.then(|| crate::scene::text::web_font::primary_script().family())
|
||||
});
|
||||
if let Some(run) = shaping_family.and_then(|family| crate::scene::text::ttf_glyph::shape_run(family, text)) {
|
||||
return crate::scene::text::ttf_glyph::char_cells(&run, text)
|
||||
.into_iter()
|
||||
.map(|(x0, x1)| (x0 * scale * wf, x1 * scale * wf))
|
||||
.collect();
|
||||
}
|
||||
let mut cursor = 0.0_f32;
|
||||
text.chars()
|
||||
.map(|ch| {
|
||||
let advance = if ch == ' ' {
|
||||
face.word_spacing()
|
||||
} else {
|
||||
match face.glyph(ch) {
|
||||
Some(glyph) => glyph.advance + face.spacing_after(ch),
|
||||
None => 6.0 + face.letter_spacing(),
|
||||
}
|
||||
};
|
||||
let cell = (cursor * scale * wf, (cursor + advance) * scale * wf);
|
||||
cursor += advance;
|
||||
cell
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Expand DXF `%%x` special-character sequences that appear in both TEXT and MTEXT values:
|
||||
/// - `%%d` / `%%D` → `°`
|
||||
/// - `%%p` / `%%P` → `±`
|
||||
@@ -998,6 +1034,34 @@ pub fn measure_word(
|
||||
w
|
||||
}
|
||||
|
||||
/// Per-character cells of a word laid out by the shaper — the visual
|
||||
/// [left, right] of each character in logical order, from the word start,
|
||||
/// in drawing units — when the word is shaped (a TrueType font or a script
|
||||
/// that needs shaping), as [`measure_word`] measures it. A glyph that
|
||||
/// covers several characters (a ligature) shares its cell among them.
|
||||
pub fn word_cells(
|
||||
text: &str,
|
||||
state: &RunState,
|
||||
entity_h: f32,
|
||||
base_wf: f32,
|
||||
base_font: &str,
|
||||
) -> Option<Vec<(f32, f32)>> {
|
||||
let scale = run_scale(state, entity_h, base_wf);
|
||||
let font_name = resolve_font(state, base_font);
|
||||
let face = Face::resolve(&font_name);
|
||||
let family = face.ttf_family().or_else(|| {
|
||||
crate::scene::text::web_font::requires_shaping(text)
|
||||
.then(|| crate::scene::text::web_font::primary_script().family())
|
||||
})?;
|
||||
let run = crate::scene::text::ttf_glyph::shape_run(family, text)?;
|
||||
Some(
|
||||
crate::scene::text::ttf_glyph::char_cells(&run, text)
|
||||
.into_iter()
|
||||
.map(|(x0, x1)| (x0 * scale, x1 * scale))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -1356,6 +1420,10 @@ pub struct GlyphBox {
|
||||
pub ymin: f32,
|
||||
pub ymax: f32,
|
||||
pub is_rtl: bool,
|
||||
/// The same cell unrotated, relative to the insertion point
|
||||
/// [left, bottom, right, top]: exact for rotated text, where the world
|
||||
/// box above is only the bounds of two corners.
|
||||
pub local: [f32; 4],
|
||||
}
|
||||
|
||||
/// Output of [`layout_mtext`]: stroke groups + the geometry the caller
|
||||
@@ -2098,6 +2166,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
ymin: ay.min(by),
|
||||
ymax: ay.max(by),
|
||||
is_rtl: sub.is_rtl,
|
||||
local: [line_lx + cursor_start, line_ly, line_lx + cursor_start, line_ly + caret_h],
|
||||
});
|
||||
vis += 1;
|
||||
}
|
||||
@@ -2324,25 +2393,41 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
} else {
|
||||
measure_word(text, &atom.state, entity_h, base_wf, &base_font_name)
|
||||
};
|
||||
// The shaper's own glyph cells (visual positions,
|
||||
// per character in logical order); evenly divided
|
||||
// slots only when the word is not shaped.
|
||||
let slot_w = word_w / count as f32;
|
||||
for ci in 0..count {
|
||||
let cx = cursor_x + ci as f32 * slot_w;
|
||||
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 + slot_w, ly + run_h);
|
||||
let cells = word_cells(text, &atom.state, entity_h, base_wf, &base_font_name)
|
||||
.unwrap_or_else(|| {
|
||||
(0..count)
|
||||
.map(|ci| {
|
||||
let x = (count - 1 - ci) as f32 * slot_w;
|
||||
(x, x + slot_w)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
for (ci, (x0, x1)) in cells.into_iter().enumerate() {
|
||||
let (cx0, cx1) = (cursor_x + x0, cursor_x + x1);
|
||||
let (ax, ay) = to_world(line_base_x, line_base_y, cx0, ly);
|
||||
let (bx, by) = to_world(line_base_x, line_base_y, cx1, ly + run_h);
|
||||
glyph_boxes.push(GlyphBox {
|
||||
vis: atom.char_offset + count - 1 - ci,
|
||||
vis: atom.char_offset + ci,
|
||||
xmin: ax.min(bx),
|
||||
xmax: ax.max(bx),
|
||||
ymin: ay.min(by),
|
||||
ymax: ay.max(by),
|
||||
is_rtl: true,
|
||||
local: [line_lx + cx0, line_ly + ly, line_lx + cx1, line_ly + ly + run_h],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Per-character boxes, advancing exactly as
|
||||
// `measure_word` does so they track the glyphs.
|
||||
// A shaped word (TrueType or a complex script)
|
||||
// takes the shaper's cells, as it draws.
|
||||
let scale = run_scale(&atom.state, entity_h, base_wf);
|
||||
let face = Face::resolve(&font_name);
|
||||
let shaped = word_cells(text, &atom.state, entity_h, base_wf, &base_font_name);
|
||||
let mut cx = cursor_x;
|
||||
for (ci, ch) in text.chars().enumerate() {
|
||||
let adv = match face.glyph(ch) {
|
||||
@@ -2351,8 +2436,12 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
}
|
||||
None => (6.0 + face.letter_spacing() * tracking) * scale,
|
||||
};
|
||||
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);
|
||||
let (cx0, cx1) = match shaped.as_ref().and_then(|cells| cells.get(ci)) {
|
||||
Some(&(x0, x1)) => (cursor_x + x0, cursor_x + x1),
|
||||
None => (cx, cx + adv),
|
||||
};
|
||||
let (ax, ay) = to_world(line_base_x, line_base_y, cx0, ly);
|
||||
let (bx, by) = to_world(line_base_x, line_base_y, cx1, ly + run_h);
|
||||
glyph_boxes.push(GlyphBox {
|
||||
vis: atom.char_offset + ci,
|
||||
xmin: ax.min(bx),
|
||||
@@ -2360,6 +2449,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
ymin: ay.min(by),
|
||||
ymax: ay.max(by),
|
||||
is_rtl: false,
|
||||
local: [line_lx + cx0, line_ly + ly, line_lx + cx1, line_ly + ly + run_h],
|
||||
});
|
||||
cx += adv;
|
||||
}
|
||||
@@ -2487,6 +2577,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
ymin,
|
||||
ymax,
|
||||
is_rtl: false,
|
||||
local: [line_lx + x0, line_ly + valign_dy, line_lx + x1, line_ly + top],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2603,6 +2694,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
ymin: ay.min(by),
|
||||
ymax: ay.max(by),
|
||||
is_rtl: false,
|
||||
local: [line_lx + cursor_x, line_ly, line_lx + cursor_x + adv, line_ly + run_h],
|
||||
});
|
||||
}
|
||||
cursor_x += adv;
|
||||
|
||||
@@ -85,12 +85,14 @@ const FIELD_BOX_BELOW: f64 = 0.345;
|
||||
const FIELD_BOX_ABOVE: f64 = 1.19;
|
||||
|
||||
/// FIELDDISPLAY boxes: each field's shown text, one box per line it spans,
|
||||
/// placed from the text layout itself — MTEXT's per-character layout cells
|
||||
/// (the editor's glyph boxes) and TEXT / attribute pen advances, the same
|
||||
/// layout and shaping the glyphs are drawn with — so formatting codes,
|
||||
/// missing glyphs or shaped scripts before a field do not shift it. A box
|
||||
/// spans the field's character cells horizontally and FIELD_BOX_BELOW /
|
||||
/// FIELD_BOX_ABOVE vertically. `None` when the host's fields cannot be
|
||||
/// placed from the text layout itself, in the text's own (rotated) frame —
|
||||
/// MTEXT and multi-line attributes from the layout's character cells, TEXT
|
||||
/// and single-line attributes from each line's character cells (the
|
||||
/// shaper's glyph cells for TrueType fonts and shaped scripts, else the
|
||||
/// stroke font's advances) — so formatting codes, missing glyphs, shaped or
|
||||
/// right-to-left scripts before a field do not shift it. A box spans the
|
||||
/// field's character cells along the line and FIELD_BOX_BELOW /
|
||||
/// FIELD_BOX_ABOVE across it. `None` when the host's fields cannot be
|
||||
/// located in its text (the whole text is boxed then).
|
||||
fn field_run_corner_groups(
|
||||
document: &codec::CadDocument,
|
||||
@@ -98,44 +100,61 @@ fn field_run_corner_groups(
|
||||
stroke_groups: &[crate::scene::convert::acad_to_render::TextStroke],
|
||||
anno: f64,
|
||||
) -> Option<Vec<[[f64; 2]; 4]>> {
|
||||
use crate::entities::text_support::{mtext_visible_count, resolve_dxf_special_chars, text_local_bounds};
|
||||
use crate::entities::text_support::{
|
||||
mtext_visible_count, resolve_dxf_special_chars, resolve_text_style, text_char_cells, GlyphBox,
|
||||
};
|
||||
let handle = entity.common().handle;
|
||||
// Lines of a field in a local frame: [left, baseline, right, height].
|
||||
let mut lines: Vec<[f64; 4]> = Vec::new();
|
||||
let (origin, rotation) = match entity {
|
||||
// Field lines as (origin, rotation, [left, baseline, right, height]) in
|
||||
// the origin's frame.
|
||||
let mut lines: Vec<([f64; 2], f64, [f64; 4])> = Vec::new();
|
||||
// Group a field's layout cells by line (a shared baseline).
|
||||
let from_cells = |lines: &mut Vec<([f64; 2], f64, [f64; 4])>,
|
||||
cells: &[GlyphBox],
|
||||
text: &str,
|
||||
spans: &[std::ops::Range<usize>],
|
||||
origin: [f64; 2],
|
||||
rotation: f64|
|
||||
-> Option<()> {
|
||||
for span in spans {
|
||||
let first = mtext_visible_count(text.get(..span.start)?);
|
||||
let last = mtext_visible_count(text.get(..span.end)?);
|
||||
let mut line: Option<[f64; 4]> = None;
|
||||
for b in cells.iter().filter(|b| (first..last).contains(&b.vis)) {
|
||||
let [l, base, r, top] = b.local.map(|v| v as f64 * anno);
|
||||
let h = top - base;
|
||||
line = Some(match line {
|
||||
Some(c) if (c[1] - base).abs() <= 1e-3 * h.max(c[3]) => {
|
||||
[c[0].min(l), c[1], c[2].max(r), c[3].max(h)]
|
||||
}
|
||||
Some(c) => {
|
||||
lines.push((origin, rotation, c));
|
||||
[l, base, r, h]
|
||||
}
|
||||
None => [l, base, r, h],
|
||||
});
|
||||
}
|
||||
lines.extend(line.map(|c| (origin, rotation, c)));
|
||||
}
|
||||
Some(())
|
||||
};
|
||||
match entity {
|
||||
EntityType::MText(m) => {
|
||||
let spans = codec::fields::field_spans(document, handle, &m.value)?;
|
||||
let mut local = m.clone();
|
||||
local.insertion_point = codec::types::Vector3::new(0.0, 0.0, 0.0);
|
||||
local.rotation = 0.0;
|
||||
let boxes = crate::entities::mtext::glyph_boxes(&local, document);
|
||||
for span in spans {
|
||||
let first = mtext_visible_count(m.value.get(..span.start)?);
|
||||
let last = mtext_visible_count(m.value.get(..span.end)?);
|
||||
let mut line: Option<[f64; 4]> = None;
|
||||
for b in boxes.iter().filter(|b| (first..last).contains(&b.vis)) {
|
||||
let (l, base, r, h) = (b.xmin as f64, b.ymin as f64, b.xmax as f64, (b.ymax - b.ymin) as f64);
|
||||
line = Some(match line {
|
||||
Some(c) if (c[1] - base).abs() <= 1e-3 * h.max(c[3]) => {
|
||||
[c[0].min(l), c[1], c[2].max(r), c[3].max(h)]
|
||||
}
|
||||
Some(c) => {
|
||||
lines.push(c);
|
||||
[l, base, r, h]
|
||||
}
|
||||
None => [l, base, r, h],
|
||||
});
|
||||
}
|
||||
lines.extend(line);
|
||||
}
|
||||
let rotation = stroke_groups
|
||||
.iter()
|
||||
.find_map(|g| g.run.as_ref().map(|run| run.rotation as f64))
|
||||
.unwrap_or(m.rotation);
|
||||
for line in &mut lines {
|
||||
*line = line.map(|v| v * anno);
|
||||
}
|
||||
([m.insertion_point.x, m.insertion_point.y], rotation)
|
||||
let cells = crate::entities::mtext::glyph_boxes(m, document);
|
||||
let flip = if resolve_text_style(&m.style, document).is_upside_down { std::f64::consts::PI } else { 0.0 };
|
||||
from_cells(
|
||||
&mut lines,
|
||||
&cells,
|
||||
&m.value,
|
||||
&spans,
|
||||
[m.insertion_point.x, m.insertion_point.y],
|
||||
m.rotation + flip,
|
||||
)?;
|
||||
}
|
||||
EntityType::AttributeEntity(a) if crate::entities::attribute::attribute_cells(a, document).is_some() => {
|
||||
let spans = codec::fields::field_spans(document, handle, &a.value)?;
|
||||
let (cells, origin, rotation) = crate::entities::attribute::attribute_cells(a, document)?;
|
||||
from_cells(&mut lines, &cells, &a.value, &spans, origin, rotation as f64)?;
|
||||
}
|
||||
EntityType::Text(_) | EntityType::AttributeEntity(_) => {
|
||||
let text = match entity {
|
||||
@@ -144,28 +163,38 @@ fn field_run_corner_groups(
|
||||
_ => return None,
|
||||
};
|
||||
let spans = codec::fields::field_spans(document, handle, text)?;
|
||||
// One laid-out line: a multi-line attribute keeps the whole box.
|
||||
let [group] = stroke_groups else { return None };
|
||||
let run = group.run.as_ref()?;
|
||||
let advance = |s: &str| {
|
||||
text_local_bounds(&run.font, &resolve_dxf_special_chars(s), run.height, run.width_factor, run.oblique)
|
||||
.map_or(0.0, |b| b.advance as f64)
|
||||
};
|
||||
for span in spans {
|
||||
let (l, r) = (advance(text.get(..span.start)?), advance(text.get(..span.end)?));
|
||||
lines.push([l * anno, 0.0, r * anno, run.height as f64 * anno]);
|
||||
// One laid-out line per `\P`-separated line, in order.
|
||||
let mut start = 0;
|
||||
for (line_text, group) in text.split("\\P").zip(stroke_groups) {
|
||||
let range = start..start + line_text.len();
|
||||
start = range.end + 2;
|
||||
let run = group.run.as_ref()?;
|
||||
let resolved = resolve_dxf_special_chars(line_text);
|
||||
let cells = text_char_cells(&run.font, &resolved, run.height, run.width_factor);
|
||||
let char_at = |byte: usize| resolve_dxf_special_chars(&line_text[..byte]).chars().count();
|
||||
for span in &spans {
|
||||
let (a, b) = (span.start.max(range.start), span.end.min(range.end));
|
||||
if a >= b {
|
||||
continue;
|
||||
}
|
||||
let picked = cells.get(char_at(a - range.start)..char_at(b - range.start)).unwrap_or_default();
|
||||
let l = picked.iter().map(|c| c.0).fold(f32::INFINITY, f32::min) as f64;
|
||||
let r = picked.iter().map(|c| c.1).fold(f32::NEG_INFINITY, f32::max) as f64;
|
||||
if l.is_finite() && r.is_finite() {
|
||||
lines.push((group.origin, run.rotation as f64, [l * anno, 0.0, r * anno, run.height as f64 * anno]));
|
||||
}
|
||||
}
|
||||
}
|
||||
(group.origin, run.rotation as f64)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let (sin_r, cos_r) = rotation.sin_cos();
|
||||
let to_world = |x: f64, y: f64| [origin[0] + x * cos_r - y * sin_r, origin[1] + x * sin_r + y * cos_r];
|
||||
}
|
||||
Some(
|
||||
lines
|
||||
.into_iter()
|
||||
.filter(|[l, _, r, _]| r > l)
|
||||
.map(|[l, base, r, h]| {
|
||||
.filter(|(_, _, [l, _, r, _])| r > l)
|
||||
.map(|(origin, rotation, [l, base, r, h])| {
|
||||
let (sin_r, cos_r) = rotation.sin_cos();
|
||||
let to_world = |x: f64, y: f64| [origin[0] + x * cos_r - y * sin_r, origin[1] + x * sin_r + y * cos_r];
|
||||
let (b, t) = (base - FIELD_BOX_BELOW * h, base + FIELD_BOX_ABOVE * h);
|
||||
[to_world(l, b), to_world(r, b), to_world(r, t), to_world(l, t)]
|
||||
})
|
||||
@@ -1508,17 +1537,16 @@ pub fn tessellate(
|
||||
// entity still has a hit-test target via snap_pts. With SDF on
|
||||
// this is the normal path (strokes suppressed) and the wire
|
||||
// also carries the glyph quads built above.
|
||||
if bins.is_empty() {
|
||||
let mut wires: Vec<WireModel> = Vec::new();
|
||||
// FIELDDISPLAY: a field shows on a gray box behind its
|
||||
// glyphs, on screen only.
|
||||
if text_aabb != WireModel::UNBOUNDED_AABB
|
||||
&& crate::entities::field::display()
|
||||
// FIELDDISPLAY: a field shows on a gray box behind its glyphs,
|
||||
// on screen only — for SDF glyphs and shaped stroke text alike.
|
||||
let mut field_background: Option<WireModel> = None;
|
||||
if crate::entities::field::display()
|
||||
&& crate::entities::field::hosts_field(document, entity)
|
||||
{
|
||||
let runs = field_run_corner_groups(document, entity, &stroke_groups, anno);
|
||||
let corner_groups = match entity {
|
||||
_ if runs.is_some() => runs.unwrap_or_default(),
|
||||
_ if text_aabb == WireModel::UNBOUNDED_AABB => Vec::new(),
|
||||
EntityType::MText(m) => {
|
||||
let rotation = stroke_groups
|
||||
.iter()
|
||||
@@ -1550,7 +1578,7 @@ pub fn tessellate(
|
||||
}
|
||||
}
|
||||
if !ft.is_empty() {
|
||||
wires.push(WireModel {
|
||||
field_background = Some(WireModel {
|
||||
display_visible: true,
|
||||
plot_visible: false,
|
||||
name: name.clone(),
|
||||
@@ -1565,6 +1593,9 @@ pub fn tessellate(
|
||||
});
|
||||
}
|
||||
}
|
||||
if bins.is_empty() {
|
||||
let mut wires: Vec<WireModel> = Vec::new();
|
||||
wires.extend(field_background.take());
|
||||
// MTEXT background and frame follow the glyph bounds.
|
||||
if text_aabb != WireModel::UNBOUNDED_AABB {
|
||||
if let EntityType::MText(m) = entity {
|
||||
@@ -1801,6 +1832,7 @@ pub fn tessellate(
|
||||
}
|
||||
|
||||
let mut out: Vec<WireModel> = Vec::new();
|
||||
out.extend(field_background);
|
||||
let mut is_first = true;
|
||||
for bin in bins {
|
||||
let wire_color = match bin.color {
|
||||
|
||||
@@ -577,6 +577,30 @@ pub struct ShapedRun {
|
||||
pub glyphs: Vec<PlacedGlyph>,
|
||||
/// Total pen advance of the run (9-unit).
|
||||
pub advance: f32,
|
||||
/// Every laid-out glyph (blank ones too) as the byte range of the text it
|
||||
/// shows and its visual [left, right] pen cell, 9-unit.
|
||||
pub cells: Vec<(usize, usize, f32, f32)>,
|
||||
}
|
||||
|
||||
/// Each character's visual [left, right] cell (9-unit) in logical order:
|
||||
/// the cell of the glyph that shows it; a glyph showing several characters
|
||||
/// shares its cell among them in reading order.
|
||||
pub fn char_cells(run: &ShapedRun, text: &str) -> Vec<(f32, f32)> {
|
||||
text.char_indices()
|
||||
.map(|(byte, _)| {
|
||||
let Some(&(start, end, x0, x1)) =
|
||||
run.cells.iter().find(|(start, end, _, _)| (*start..(*end).max(start + 1)).contains(&byte))
|
||||
else {
|
||||
return (0.0, 0.0);
|
||||
};
|
||||
let chars: Vec<usize> = text[start..end.min(text.len())].char_indices().map(|(i, _)| start + i).collect();
|
||||
let (n, k) = (chars.len().max(1), chars.iter().position(|&b| b == byte).unwrap_or(0));
|
||||
let rtl = text[start..end.min(text.len())].chars().any(crate::entities::text_support::is_rtl_char);
|
||||
let slot = (x1 - x0) / n as f32;
|
||||
let k = if rtl { n - 1 - k } else { k };
|
||||
(x0 + slot * k as f32, x0 + slot * (k + 1) as f32)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
type ShapeCache = HashMap<(String, String), Option<Arc<ShapedRun>>>;
|
||||
@@ -881,10 +905,12 @@ fn build_shaped(_family: &str, text: &str) -> Option<ShapedRun> {
|
||||
buffer.shape_until_scroll(&mut font_system, false);
|
||||
|
||||
let mut glyphs = Vec::new();
|
||||
let mut cells = Vec::new();
|
||||
let mut advance = 0.0_f32;
|
||||
for run in buffer.layout_runs() {
|
||||
advance = advance.max(run.line_w * px_to_9);
|
||||
for glyph in run.glyphs.iter() {
|
||||
cells.push((glyph.start, glyph.end, glyph.x * px_to_9, (glyph.x + glyph.w) * px_to_9));
|
||||
let face_index = font_system
|
||||
.db_mut()
|
||||
.face(glyph.font_id)
|
||||
@@ -923,7 +949,7 @@ fn build_shaped(_family: &str, text: &str) -> Option<ShapedRun> {
|
||||
if advance <= 0.0 && glyphs.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ShapedRun { glyphs, advance })
|
||||
Some(ShapedRun { glyphs, advance, cells })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -986,10 +1012,12 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
|
||||
let px_to_9 = CAP_UNITS * upem_p / (SHAPE_FS * cap_p);
|
||||
|
||||
let mut glyphs: Vec<PlacedGlyph> = Vec::new();
|
||||
let mut cells = Vec::new();
|
||||
let mut advance = 0.0_f32;
|
||||
for run in buf.layout_runs() {
|
||||
advance = advance.max(run.line_w * px_to_9);
|
||||
for g in run.glyphs.iter() {
|
||||
cells.push((g.start, g.end, g.x * px_to_9, (g.x + g.w) * px_to_9));
|
||||
let face_index = fs.db_mut().face(g.font_id).map(|f| f.index).unwrap_or(0);
|
||||
let Some(font) = fs.get_font(g.font_id, g.font_weight) else {
|
||||
continue;
|
||||
@@ -1028,7 +1056,7 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
|
||||
if advance <= 0.0 && glyphs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(ShapedRun { glyphs, advance })
|
||||
Some(ShapedRun { glyphs, advance, cells })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user