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 <[email protected]>
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;
|
||||
|
||||
Reference in new issue
Block a user