Files
vectorcraft/crates/doc/src/inline.rs
T
dotCooCoo a1949095c6 Type: an option fits a text object's bounds to its glyph outlines
Measure Type by Glyph Bounds (`typeGlyphBounds`, off by default) is in
Preferences > Type > Glyph Bounds and the Transform panel menu. With it
on, the bounding box, the Transform, Control bar and Properties X, Y, W
and H, `object.setBounds`, the transform tools' reference point and the
Arrange commands measure point type selected on its own by the ink of
its glyphs instead of its em box. A typed W or H scales the live type
until its ink has that size.

- The engine caches each type object's ink in text space with its
  layout bounds; the cache is not saved.
- Use Preview Bounds adds the characters' strokes, and type with
  geometry effects is measured as drawn. Skewed type is measured by its
  outlines.
- Type that draws nothing, area type, type on a path and type inside a
  selected group are measured as before. Align and Distribute keep
  following Align to Glyph Bounds.

Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md,
object.setBounds params text.
2026-11-09 21:07:55 -08:00

110 lines
4.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Inline graphics in text ([`crate::TextRun::inline`]): resolving each inline symbol's art bounds
//! from the document's symbols, which the text layout needs to size and place it.
use std::borrow::Cow;
use vectorcraft_geom::{Affine, Rect};
use crate::{Document, NodeId, NodeKind, TextObject};
/// The document key under which the app records each symbol's natural size (`{name: [w, h]}`).
pub const SYMBOL_SIZES: &str = "symbolSizes";
/// Half the side of the square symbol art is normalised to.
pub const SYMBOL_HALF: f64 = 10.0;
/// Usable art bounds: finite, non-degenerate.
fn usable(r: Option<Rect>) -> Option<Rect> {
r.filter(|r| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()) && r.width() >= 0.0 && r.height() > 1e-9)
}
impl Document {
/// Symbol space → the symbol's art at its natural size. Symbols made in the app keep their
/// art in a 20 × 20 square around the origin and their size as drawn in the document's
/// `symbolSizes` (an instance is placed with that scale); other symbols are as they are.
pub fn symbol_natural_xf(&self, name: &str) -> Affine {
let size = self.unknown.get(SYMBOL_SIZES).and_then(|m| m.get(name)).and_then(|v| Some((v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)));
match size {
Some((w, h)) if w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0 => {
Affine::scale_non_uniform(w / (2.0 * SYMBOL_HALF), h / (2.0 * SYMBOL_HALF))
}
_ => Affine::IDENTITY,
}
}
/// Visual bounds of the art of the symbol named `name` at its natural size (see
/// [`Self::symbol_natural_xf`]), if it exists and has a usable extent.
pub fn symbol_art_bounds(&self, name: &str) -> Option<Rect> {
let s = self.symbols.iter().find(|s| s.name == name)?;
let b = usable(s.art.visual_bounds().or_else(|| s.art.geometric_bounds()))?;
usable(Some(self.symbol_natural_xf(name).transform_rect_bbox(b)))
}
/// Does `t` have inline graphics whose resolved bounds differ from the document's symbols?
fn inline_stale(&self, t: &TextObject) -> bool {
t.runs.iter().filter_map(|r| r.inline.as_ref()).any(|a| a.bounds != self.symbol_art_bounds(&a.symbol))
}
/// `t` with its inline graphics' art bounds resolved from this document's symbols (borrowed
/// when nothing changes). Renderers and exporters lay text out through this, so they never
/// depend on the cached bounds being current.
pub fn inline_resolved<'a>(&self, t: &'a TextObject) -> Cow<'a, TextObject> {
if !self.inline_stale(t) {
return Cow::Borrowed(t);
}
let mut t = t.clone();
self.resolve_text_inline(&mut t);
Cow::Owned(t)
}
/// Fill in the art bounds of `t`'s inline graphics; returns whether any changed (the text's
/// cached layout bounds are then cleared).
pub fn resolve_text_inline(&self, t: &mut TextObject) -> bool {
let mut changed = false;
for r in &mut t.runs {
if let Some(a) = &mut r.inline {
let b = self.symbol_art_bounds(&a.symbol);
if a.bounds != b {
a.bounds = b;
changed = true;
}
}
}
if changed {
t.cached_bounds = None;
t.cached_ink = None;
}
changed
}
/// Resolve the inline graphics of every text object in the layers (after opening a document
/// or changing its symbols). Returns the ids of the texts that changed: their cached layout
/// bounds were cleared and need recomputing.
pub fn resolve_inline_art(&mut self) -> Vec<NodeId> {
let mut stale = vec![];
self.walk(|n| {
if let NodeKind::Text(t) = &n.kind
&& t.runs.iter().any(|r| r.inline.is_some())
&& self.inline_stale(t)
{
stale.push(n.id);
}
});
let mut out = vec![];
for id in stale {
let Some(mut t) = self.node(id).and_then(|n| match &n.kind {
NodeKind::Text(t) => Some(t.clone()),
_ => None,
}) else {
continue;
};
if self.resolve_text_inline(&mut t)
&& let Some(n) = self.node_mut(id)
{
n.kind = NodeKind::Text(t);
out.push(id);
}
}
out
}
}