mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 15:25:16 +00:00
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.
766 lines
34 KiB
Rust
766 lines
34 KiB
Rust
use super::expand::{Hooks, Outliner, Warper, map_node};
|
||
use super::mesh::{GradientMesh, MeshPoint, grid_len, grid_points, surface_of};
|
||
use super::{Affine, Arc, Color, Deserialize, Node, NodeId, NodeKind, PathData, Point, Rect, Serialize, Vec2, yes};
|
||
use vectorcraft_geom::Homography;
|
||
|
||
/// How an envelope distorts its content.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
#[serde(tag = "type", rename_all = "camelCase")]
|
||
pub enum EnvelopeKind {
|
||
/// One of the 15 warp styles (`arc`, `arcLower`, … — see [`WarpStyle::from_id`]); bend and
|
||
/// distortions in percent (-100..100).
|
||
Warp {
|
||
style: String,
|
||
#[serde(default)]
|
||
bend: f64,
|
||
#[serde(default)]
|
||
h: f64,
|
||
#[serde(default)]
|
||
v: f64,
|
||
#[serde(default = "yes")]
|
||
horizontal: bool,
|
||
},
|
||
/// A `(rows+1)×(cols+1)` grid of points (row-major) the content's bounding box maps onto.
|
||
Mesh {
|
||
rows: u32,
|
||
cols: u32,
|
||
points: Vec<Point>,
|
||
/// Each point's bezier handles (offsets, see [`H_RIGHT`] …), row-major like `points`.
|
||
/// Empty: the smooth (Catmull-Rom) mesh through the points, as before handles existed;
|
||
/// editing the mesh's points or handles on the canvas fills them in.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
handles: Vec<[Vec2; 4]>,
|
||
},
|
||
/// The content's bounding box maps onto the outline of this path.
|
||
TopObject {
|
||
path: PathData,
|
||
/// The anchors (indices into the outline, the path's largest subpath, in its own order)
|
||
/// at the top left, top right, bottom right and bottom left corners, as a saved file gives
|
||
/// them. `None`: the corners are found from the outline.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
corners: Option<[u32; 4]>,
|
||
},
|
||
}
|
||
|
||
/// Envelope Options → Preserve Shape Using: how a distorted raster keeps the envelope's shape.
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub enum PreserveShape {
|
||
#[default]
|
||
ClippingMask,
|
||
Transparency,
|
||
}
|
||
|
||
impl PreserveShape {
|
||
pub fn id(self) -> &'static str {
|
||
match self {
|
||
Self::ClippingMask => "clippingMask",
|
||
Self::Transparency => "transparency",
|
||
}
|
||
}
|
||
pub fn parse(s: &str) -> Option<Self> {
|
||
match s {
|
||
"clippingMask" => Some(Self::ClippingMask),
|
||
"transparency" => Some(Self::Transparency),
|
||
_ => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Envelope Options besides Fidelity. `Default` is what envelopes saved before these options
|
||
/// existed do (their appearance applies after the distortion); new envelopes start from
|
||
/// [`EnvelopeOptions::NEW`], as in the reference app.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase", default)]
|
||
pub struct EnvelopeOptions {
|
||
/// Rasters: Anti-Alias.
|
||
pub anti_alias: bool,
|
||
/// Rasters: Preserve Shape Using.
|
||
pub preserve_shape: PreserveShape,
|
||
/// Distort Appearance: strokes and live effects are applied before the distortion, so they
|
||
/// bend with the art (off: they apply to the distorted paths).
|
||
pub distort_appearance: bool,
|
||
/// Distort Linear Gradients (with Distort Appearance): placed gradients bend with the art.
|
||
pub distort_linear_gradients: bool,
|
||
/// Distort Pattern Fills (with Distort Appearance): pattern tiles bend with the art.
|
||
pub distort_pattern_fills: bool,
|
||
/// With Distort Appearance, how far the box the content maps from reaches past its geometric
|
||
/// bounds at the top, bottom, left and right, as shares of their height and width. An envelope
|
||
/// opened from a `.ai` whose saved size includes its strokes has these margins. New envelopes
|
||
/// have none. Leaving Edit Contents measures them again when the strokes reach differently.
|
||
#[serde(skip_serializing_if = "crate::skip::is_default")]
|
||
pub bounds_margin: [f32; 4],
|
||
}
|
||
|
||
impl Default for EnvelopeOptions {
|
||
fn default() -> Self {
|
||
Self {
|
||
anti_alias: true,
|
||
preserve_shape: PreserveShape::ClippingMask,
|
||
distort_appearance: false,
|
||
distort_linear_gradients: false,
|
||
distort_pattern_fills: false,
|
||
bounds_margin: [0.0; 4],
|
||
}
|
||
}
|
||
}
|
||
|
||
impl EnvelopeOptions {
|
||
/// The options a new envelope gets (the reference app's defaults).
|
||
pub const NEW: Self = Self {
|
||
anti_alias: true,
|
||
preserve_shape: PreserveShape::ClippingMask,
|
||
distort_appearance: true,
|
||
distort_linear_gradients: false,
|
||
distort_pattern_fills: false,
|
||
bounds_margin: [0.0; 4],
|
||
};
|
||
/// Do gradients bend with the art?
|
||
pub fn gradients(&self) -> bool {
|
||
self.distort_appearance && self.distort_linear_gradients
|
||
}
|
||
/// Do pattern tiles bend with the art?
|
||
pub fn patterns(&self) -> bool {
|
||
self.distort_appearance && self.distort_pattern_fills
|
||
}
|
||
/// The box content with geometric bounds `r` maps from. Margins that aren't finite, are
|
||
/// negative or give a box that isn't finite count as none.
|
||
fn source_box(&self, r: Rect) -> Rect {
|
||
let [top, bottom, left, right] = self.bounds_margin.map(f64::from);
|
||
if !self.distort_appearance || !self.bounds_margin.iter().all(|m| m.is_finite() && *m >= 0.0) {
|
||
return r;
|
||
}
|
||
let grown = crate::marks::outset(r, [top * r.height(), bottom * r.height(), left * r.width(), right * r.width()]);
|
||
if grown.is_finite() { grown } else { r }
|
||
}
|
||
}
|
||
|
||
/// The bounds of `content` in the axes of `frame`, with its strokes and geometric, type measured by
|
||
/// its glyph outlines (see [`envelope_content_bounds`]). Strokes scale with the frame as
|
||
/// [`Node::transform`] scales them.
|
||
pub fn envelope_content_boxes(content: &[Arc<Node>], frame: Affine) -> Option<(Rect, Rect)> {
|
||
let frame = if frame.is_finite() && frame.determinant().abs() > 1e-12 { frame } else { Affine::IDENTITY };
|
||
let to_frame = frame.inverse();
|
||
let (stroked, plain) = content.iter().fold((None, None), |(stroked, plain), c| {
|
||
(vectorcraft_geom::union_opt(stroked, content_box(c, true, to_frame)), vectorcraft_geom::union_opt(plain, content_box(c, false, to_frame)))
|
||
});
|
||
Some((stroked?, plain?))
|
||
}
|
||
|
||
/// The geometric bounds an envelope around `content` on the page axes maps from: type, alone or
|
||
/// in groups and layers, is measured by its glyph outlines, underlines and strikethroughs instead
|
||
/// of its laid-out box. Type keeps its laid-out box while nothing is registered with
|
||
/// [`set_type_ink`], and when it draws nothing.
|
||
pub fn envelope_content_bounds(content: &[Arc<Node>]) -> Option<Rect> {
|
||
content.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, content_box(c, false, Affine::IDENTITY)))
|
||
}
|
||
|
||
/// The box of type's glyph outlines, underlines and strikethroughs with its text space mapped by
|
||
/// the given transform (outlines mapped point by point) and, for type in perspective, then drawn
|
||
/// through its projection and mapped by the second transform. `None` when it draws nothing. Type is
|
||
/// laid out above this crate, so the text crate registers one with [`set_type_ink`].
|
||
pub type TypeInk = fn(&crate::TextObject, Affine, Option<(Homography, Affine)>) -> Option<Rect>;
|
||
|
||
/// What [`TypeInk`] maps type's text space by: a transform, and a projection with the transform
|
||
/// after it.
|
||
type TypeMap = (Affine, Option<(Homography, Affine)>);
|
||
|
||
static TYPE_INK: std::sync::OnceLock<(TypeInk, crate::hit::LiveInputs)> = std::sync::OnceLock::new();
|
||
|
||
/// Register how envelopes measure type, and a number that changes whenever what it reads besides
|
||
/// the type changes, such as the installed fonts (the first registration stays).
|
||
pub fn set_type_ink(f: TypeInk, inputs: crate::hit::LiveInputs) {
|
||
// Already set: the same measure, registered by another session, an import or a renderer.
|
||
let _ = TYPE_INK.set((f, inputs));
|
||
}
|
||
|
||
/// How many entries each table of [`TypeInks`] holds before it is emptied.
|
||
const TYPE_INK_KEPT: usize = 1024;
|
||
|
||
/// `map`, bit for bit, for a table key.
|
||
fn map_bits(map: &TypeMap) -> [u64; 21] {
|
||
let mut bits = [0; 21];
|
||
let (m, projection) = map;
|
||
for (b, v) in bits.iter_mut().zip(m.as_coeffs()) {
|
||
*b = v.to_bits();
|
||
}
|
||
if let Some((h, after)) = projection {
|
||
for (b, v) in bits[6..].iter_mut().zip(h.to_array().into_iter().chain(after.as_coeffs())) {
|
||
*b = v.to_bits();
|
||
}
|
||
}
|
||
bits
|
||
}
|
||
|
||
/// A key of [`TypeInks::nodes`]: the object's address, the map its outlines are measured in (an
|
||
/// affine map without its move) and the inputs number registered with [`set_type_ink`].
|
||
type NodeKey = (usize, [u64; 21], u64);
|
||
|
||
/// A key of [`TypeInks::content`]: two 64-bit hashes of the type without its transform's move and
|
||
/// its layout caches ([`content_hash`]), the map its outlines are measured in, and the inputs number.
|
||
type ContentKey = ([u64; 2], [u64; 21], u64);
|
||
|
||
/// Type measured by [`type_ink`] on one thread, by the map it is measured in and the inputs
|
||
/// number: by the object (an unchanged object is found without being read),
|
||
/// and by its content (a moved or copied object is found without laying it out again). Neither
|
||
/// table keeps a copy of the type.
|
||
#[derive(Default)]
|
||
struct TypeInks {
|
||
/// By the object's address, with a weak pointer that keeps the address from being reused.
|
||
nodes: std::collections::HashMap<NodeKey, (std::sync::Weak<Node>, Option<Rect>)>,
|
||
/// By the type's content.
|
||
content: std::collections::HashMap<ContentKey, Option<Rect>>,
|
||
}
|
||
|
||
/// Where [`TypeInks::find`] did not find type: its keys in both tables (no content key for type
|
||
/// that could not be hashed whole).
|
||
struct Missing {
|
||
by_node: NodeKey,
|
||
content: Option<ContentKey>,
|
||
}
|
||
|
||
impl TypeInks {
|
||
/// The kept box of type node `n` (holding `t`) mapped by `map`, or the keys to keep it under
|
||
/// once it is measured.
|
||
fn find(&mut self, n: &Arc<Node>, t: &crate::TextObject, map: &TypeMap, inputs: u64) -> Result<Option<Rect>, Box<Missing>> {
|
||
// While the table holds a weak pointer to `n`'s allocation, no other object can be at that
|
||
// address, and `n` can't be changed in place (`Arc::get_mut` refuses it and `Arc::make_mut`
|
||
// moves it).
|
||
let by_node = (Arc::as_ptr(n) as usize, map_bits(map), inputs);
|
||
if let Some((_, ink)) = self.nodes.get(&by_node) {
|
||
return Ok(*ink);
|
||
}
|
||
// Shrink Text to Fit lays type out by its size on the page, so only a move keeps its layout.
|
||
let [a, b, c, d, _, _] = t.xf.as_coeffs();
|
||
let mut text = t.clone();
|
||
text.xf = Affine::new([a, b, c, d, 0.0, 0.0]);
|
||
text.cached_bounds = None;
|
||
text.cached_baselines.clear();
|
||
text.cached_ink = None;
|
||
let content = content_hash(&text).map(|hash| (hash, by_node.1, inputs));
|
||
match content.and_then(|key| self.content.get(&key)) {
|
||
Some(ink) => {
|
||
let ink = *ink;
|
||
self.keep_node(n, by_node, ink);
|
||
Ok(ink)
|
||
}
|
||
None => Err(Box::new(Missing { by_node, content })),
|
||
}
|
||
}
|
||
|
||
/// Keep the box `ink` measured for type node `n` under the keys `missing`.
|
||
fn keep(&mut self, n: &Arc<Node>, missing: Missing, ink: Option<Rect>) {
|
||
if let Some(key) = missing.content {
|
||
if self.content.len() >= TYPE_INK_KEPT {
|
||
self.content.clear();
|
||
}
|
||
self.content.insert(key, ink);
|
||
}
|
||
self.keep_node(n, missing.by_node, ink);
|
||
}
|
||
|
||
fn keep_node(&mut self, n: &Arc<Node>, by_node: NodeKey, ink: Option<Rect>) {
|
||
if self.nodes.len() >= TYPE_INK_KEPT {
|
||
self.nodes.retain(|_, (weak, _)| weak.strong_count() > 0);
|
||
if self.nodes.len() >= TYPE_INK_KEPT {
|
||
self.nodes.clear();
|
||
}
|
||
}
|
||
self.nodes.insert(by_node, (Arc::downgrade(n), ink));
|
||
}
|
||
}
|
||
|
||
/// Two independent 64-bit hashes of `text` as saved (the layout caches are not saved), with what
|
||
/// layout reads that is not saved: whether its first paragraph continues a thread and the bounds
|
||
/// of its inline graphics. `None` when it can't be written whole.
|
||
fn content_hash(text: &crate::TextObject) -> Option<[u64; 2]> {
|
||
use std::hash::Hasher;
|
||
/// Feeds what is written to two hashers.
|
||
struct Feed([std::collections::hash_map::DefaultHasher; 2]);
|
||
impl std::io::Write for Feed {
|
||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||
for h in &mut self.0 {
|
||
h.write(buf);
|
||
}
|
||
Ok(buf.len())
|
||
}
|
||
fn flush(&mut self) -> std::io::Result<()> {
|
||
Ok(())
|
||
}
|
||
}
|
||
let mut feed = Feed(Default::default());
|
||
// The second hasher starts from another state.
|
||
feed.0[1].write_u8(1);
|
||
serde_json::to_writer(&mut feed, text).ok()?;
|
||
let inline_bounds = text.runs.iter().filter_map(|r| r.inline.as_ref()).map(|i| i.bounds.map(|b| [b.x0, b.y0, b.x1, b.y1].map(f64::to_bits)));
|
||
for h in &mut feed.0 {
|
||
h.write_u8(u8::from(text.first_para_continues));
|
||
for b in inline_bounds.clone() {
|
||
match b {
|
||
None => h.write_u8(0),
|
||
Some(bits) => {
|
||
h.write_u8(1);
|
||
bits.into_iter().for_each(|v| h.write_u64(v));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let [a, b] = feed.0;
|
||
Some([a.finish(), b.finish()])
|
||
}
|
||
|
||
std::thread_local! {
|
||
/// The type [`type_ink`] measured on this thread.
|
||
static TYPE_INKS: std::cell::RefCell<TypeInks> = std::cell::RefCell::new(TypeInks::default());
|
||
}
|
||
|
||
/// The box of the glyph outlines of type node `n` (holding `t`) with its text space mapped by `m`
|
||
/// ([`set_type_ink`]) and, for type in perspective, drawn through `projection` and mapped by its
|
||
/// second transform; measured again when `t`, the maps (without a move of `m` when there is no
|
||
/// projection) or the registered inputs number change. `None` while nothing is registered.
|
||
fn type_ink(n: &Arc<Node>, t: &crate::TextObject, m: Affine, projection: Option<(Homography, Affine)>) -> Option<Option<Rect>> {
|
||
let (f, inputs) = TYPE_INK.get()?;
|
||
let inputs = inputs();
|
||
// Without a projection the type is measured without the move, which is added to the box.
|
||
let [a, b, c, d, e, g] = m.as_coeffs();
|
||
let (map, offset) = match projection {
|
||
None => ((Affine::new([a, b, c, d, 0.0, 0.0]), None), Vec2::new(e, g)),
|
||
Some(p) => ((m, Some(p)), Vec2::ZERO),
|
||
};
|
||
let ink = match TYPE_INKS.with_borrow_mut(|kept| kept.find(n, t, &map, inputs)) {
|
||
Ok(ink) => ink,
|
||
Err(missing) => {
|
||
// Measured outside the borrow: laying type out may measure other type.
|
||
let ink = f(t, map.0, map.1);
|
||
TYPE_INKS.with_borrow_mut(|kept| kept.keep(n, *missing, ink));
|
||
ink
|
||
}
|
||
};
|
||
Some(ink.map(|r| r + offset))
|
||
}
|
||
|
||
/// Add to `typed` the address of `n` and of each group and layer in it that holds type, which
|
||
/// [`content_box`] measures by its glyphs; is there type in `n`? One walk of the tree.
|
||
fn find_type(n: &Node, typed: &mut std::collections::HashSet<usize>) -> bool {
|
||
let holds = match &n.kind {
|
||
NodeKind::Text(_) => true,
|
||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => children.iter().fold(false, |holds, c| find_type(c, typed) | holds),
|
||
_ => false,
|
||
};
|
||
if holds {
|
||
typed.insert(n as *const Node as usize);
|
||
}
|
||
holds
|
||
}
|
||
|
||
/// The box of `n` an envelope maps from, in the axes `to` takes the page to: its geometric bounds,
|
||
/// or with `stroked` its visual bounds (strokes scaled by `to` as [`Node::transform`] scales them),
|
||
/// with type measured by its glyph outlines ([`type_ink`]) instead of its laid-out box, in groups
|
||
/// and layers too. Type is laid out as it is on the page, and its outlines are then taken into
|
||
/// those axes. A character's strokes reach as far past its glyphs as past the laid-out box.
|
||
fn content_box(n: &Arc<Node>, stroked: bool, to: Affine) -> Option<Rect> {
|
||
let mut typed = std::collections::HashSet::new();
|
||
find_type(n, &mut typed);
|
||
content_box_in(n, stroked, to, &typed)
|
||
}
|
||
|
||
/// [`content_box`], with the addresses of the objects in `n` that hold type ([`find_type`]).
|
||
fn content_box_in(n: &Arc<Node>, stroked: bool, to: Affine, typed: &std::collections::HashSet<usize>) -> Option<Rect> {
|
||
// `n` in the axes of `to`.
|
||
let moved = || {
|
||
if to == Affine::IDENTITY {
|
||
return std::borrow::Cow::Borrowed(&**n);
|
||
}
|
||
let mut m = (**n).clone();
|
||
m.transform(to, true);
|
||
std::borrow::Cow::Owned(m)
|
||
};
|
||
let own = |m: &Node| if stroked { m.visual_bounds() } else { m.geometric_bounds() };
|
||
if !typed.contains(&(Arc::as_ptr(n) as usize)) {
|
||
return own(&moved());
|
||
}
|
||
match &n.kind {
|
||
NodeKind::Text(t) => {
|
||
// Type in perspective: its outlines as its projection draws them.
|
||
let ink = match n.projection() {
|
||
None => type_ink(n, t, to * t.xf, None),
|
||
Some(h) => type_ink(n, t, t.xf, Some((h, to))),
|
||
}
|
||
.flatten();
|
||
let m = moved();
|
||
let Some(ink) = ink else { return own(&m) };
|
||
if !stroked {
|
||
return Some(ink);
|
||
}
|
||
let (Some(v), Some(g)) = (m.visual_bounds(), m.geometric_bounds()) else { return Some(ink) };
|
||
Some(Rect::new(
|
||
ink.x0 - (g.x0 - v.x0).max(0.0),
|
||
ink.y0 - (g.y0 - v.y0).max(0.0),
|
||
ink.x1 + (v.x1 - g.x1).max(0.0),
|
||
ink.y1 + (v.y1 - g.y1).max(0.0),
|
||
))
|
||
}
|
||
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => {
|
||
// A clipping path that paints strokes over the clip edge, as in `Node::visual_bounds`.
|
||
let clip = children.first()?;
|
||
content_box_in(clip, stroked && matches!(clip.kind, NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Text(_)), to, typed)
|
||
}
|
||
NodeKind::Layer { .. } | NodeKind::Group { .. } => {
|
||
let layer = matches!(n.kind, NodeKind::Layer { .. });
|
||
let members = n.editing_children().iter();
|
||
if stroked {
|
||
// The container's own strokes, scaled by `to` as `Node::transform` scales them,
|
||
// without copying its members.
|
||
let mut own_strokes = Node::new(n.id, NodeKind::Group { children: Vec::new(), clip: false });
|
||
own_strokes.appearance = n.appearance.clone();
|
||
own_strokes.transform(to, true);
|
||
own_strokes.visual_of_members(members.map(|c| content_box_in(c, true, to, typed)))
|
||
} else {
|
||
members.filter(|c| c.visible || !layer).fold(None, |acc, c| vectorcraft_geom::union_opt(acc, content_box_in(c, false, to, typed)))
|
||
}
|
||
}
|
||
_ => own(&moved()),
|
||
}
|
||
}
|
||
|
||
/// The [`EnvelopeOptions::bounds_margin`] of content with bounds `stroked` and `plain`. `None` for
|
||
/// content with no width or no height, or for margins an `f32` can't hold.
|
||
pub fn envelope_stroke_margin(stroked: Rect, plain: Rect) -> Option<[f32; 4]> {
|
||
if plain.width() < 1e-6 || plain.height() < 1e-6 {
|
||
return None;
|
||
}
|
||
let margin = [
|
||
(plain.y0 - stroked.y0) / plain.height(),
|
||
(stroked.y1 - plain.y1) / plain.height(),
|
||
(plain.x0 - stroked.x0) / plain.width(),
|
||
(stroked.x1 - plain.x1) / plain.width(),
|
||
]
|
||
.map(|m| m as f32);
|
||
margin.iter().all(|m| m.is_finite()).then_some(margin)
|
||
}
|
||
|
||
/// Handle slots of a [`MeshPoint`]: towards the next column, previous column, next row, previous row.
|
||
pub const H_RIGHT: usize = 0;
|
||
|
||
pub const H_LEFT: usize = 1;
|
||
|
||
pub const H_DOWN: usize = 2;
|
||
|
||
pub const H_UP: usize = 3;
|
||
|
||
/// The envelope's point map for content whose bounding box is `src` (page axes).
|
||
pub fn envelope_mapper<'a>(kind: &'a EnvelopeKind, src: Rect) -> Box<dyn Fn(Point) -> Point + 'a> {
|
||
let s = surface_of(kind, src, Affine::IDENTITY);
|
||
let (w, h) = (src.width().max(1e-9), src.height().max(1e-9));
|
||
Box::new(move |q: Point| s((q.x - src.x0) / w, (q.y - src.y0) / h))
|
||
}
|
||
|
||
/// Catmull-Rom tangents of a `(rows+1)×(cols+1)` point grid (row-major) as mesh handles, a third
|
||
/// of the tangent towards each neighbour (past the borders the grid goes on straight): the curves
|
||
/// an envelope mesh without handles of its own runs along between its points.
|
||
pub fn smooth_handles(rows: u32, cols: u32, points: &[Point]) -> Vec<[Vec2; 4]> {
|
||
let (rows, cols) = (rows as usize, cols as usize);
|
||
let at = |r: usize, c: usize| points.get(r * (cols + 1) + c).copied().unwrap_or_default();
|
||
let tangent = |prev: Option<Point>, p: Point, next: Option<Point>| match (prev, next) {
|
||
(Some(a), Some(b)) => (b - a) / 2.0,
|
||
(None, Some(b)) => b - p,
|
||
(Some(a), None) => p - a,
|
||
(None, None) => Vec2::ZERO,
|
||
};
|
||
let mut out = Vec::with_capacity(points.len());
|
||
for r in 0..=rows {
|
||
for c in 0..=cols {
|
||
let p = at(r, c);
|
||
let du = tangent(c.checked_sub(1).map(|c| at(r, c)), p, (c < cols).then(|| at(r, c + 1))) / 3.0;
|
||
let dv = tangent(r.checked_sub(1).map(|r| at(r, c)), p, (r < rows).then(|| at(r + 1, c))) / 3.0;
|
||
// Handles pointing out of the grid are unused: they stay at the point.
|
||
let keep = |inside: bool, d: Vec2| if inside { d } else { Vec2::ZERO };
|
||
let mut h = [Vec2::ZERO; 4];
|
||
(h[H_RIGHT], h[H_LEFT], h[H_DOWN], h[H_UP]) = (keep(c < cols, du), keep(c > 0, -du), keep(r < rows, dv), keep(r > 0, -dv));
|
||
out.push(h);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
/// A mesh envelope's grid (`rows`×`cols` patches, `points` and their `handles`; without handles
|
||
/// the smooth ones, see [`smooth_handles`]) as a gradient mesh with black points: what the
|
||
/// canvas draws and the Mesh tool and Direct Selection edit. `None` for a malformed grid.
|
||
pub fn envelope_grid(rows: u32, cols: u32, points: &[Point], handles: &[[Vec2; 4]]) -> Option<GradientMesh> {
|
||
if rows == 0 || cols == 0 || grid_len(rows, cols) != Some(points.len()) {
|
||
return None;
|
||
}
|
||
let smooth;
|
||
let handles = if handles.len() == points.len() {
|
||
handles
|
||
} else {
|
||
smooth = smooth_handles(rows, cols, points);
|
||
&smooth
|
||
};
|
||
let points = points.iter().zip(handles).map(|(p, h)| MeshPoint { p: *p, color: Color::BLACK, opacity: 1.0, handles: *h }).collect();
|
||
Some(GradientMesh { rows, cols, points })
|
||
}
|
||
|
||
/// The point of mesh `m` at (u, v) over the whole mesh (each patch an equal share; past the edges
|
||
/// the outer patches go on).
|
||
pub(super) fn mesh_at(m: &GradientMesh, u: f64, v: f64) -> Point {
|
||
let (rows, cols) = (m.rows.max(1) as i64, m.cols.max(1) as i64);
|
||
let (su, sv) = (u * cols as f64, v * rows as f64);
|
||
let (c, r) = ((su.floor() as i64).clamp(0, cols - 1), (sv.floor() as i64).clamp(0, rows - 1));
|
||
m.eval(r as usize, c as usize, su - c as f64, sv - r as f64)
|
||
}
|
||
|
||
/// The envelope `id` is, or the innermost one it sits in.
|
||
pub fn envelope_of(doc: &crate::Document, id: NodeId) -> Option<&Node> {
|
||
doc.ancestry(id)?.into_iter().rev().find_map(|a| doc.node(a).filter(|n| matches!(n.kind, NodeKind::Envelope { .. })))
|
||
}
|
||
|
||
/// What a selected envelope shows on the canvas: its mesh lines (a warp's surface on a 4×4 grid,
|
||
/// a mesh envelope's own grid) or its top object's outline, and for a mesh envelope the grid to
|
||
/// edit (its points and handles).
|
||
pub fn envelope_overlay(n: &Node) -> Option<(PathData, Option<GradientMesh>)> {
|
||
let NodeKind::Envelope { kind, .. } = &n.kind else { return None };
|
||
match &**kind {
|
||
EnvelopeKind::Mesh { rows, cols, points, handles } => {
|
||
let grid = envelope_grid(*rows, *cols, points, handles)?;
|
||
Some((grid.lines(), Some(grid)))
|
||
}
|
||
EnvelopeKind::Warp { .. } => Some((EnvelopeMap::of(n)?.surface_mesh(4, 4, Color::BLACK).lines(), None)),
|
||
EnvelopeKind::TopObject { path, .. } => Some((path.clone(), None)),
|
||
}
|
||
}
|
||
|
||
/// The frame an envelope keeps after transform `m` (its frame so far composed with the
|
||
/// transform): identity when its axes stay square to the page, unflipped, since evaluating on
|
||
/// the page axes gives the same result then.
|
||
pub fn envelope_frame(m: Affine) -> Affine {
|
||
let [a, b, c, d, _, _] = m.as_coeffs();
|
||
if b.abs() < 1e-12 && c.abs() < 1e-12 && a > 0.0 && d > 0.0 { Affine::IDENTITY } else { m }
|
||
}
|
||
|
||
/// An envelope's point map: content placed in the envelope's own frame maps, by its bounds there,
|
||
/// onto the unit square and from there onto the envelope's surface ([`surface_of`]).
|
||
pub struct EnvelopeMap<'a> {
|
||
src: Rect,
|
||
frame: Affine,
|
||
to_frame: Affine,
|
||
surface: Box<dyn Fn(f64, f64) -> Point + 'a>,
|
||
}
|
||
|
||
impl<'a> EnvelopeMap<'a> {
|
||
/// The map of an envelope of `kind` around `content` with `frame` and `options`, from the
|
||
/// content's bounds with type measured by its glyph outlines ([`envelope_content_bounds`]).
|
||
/// `None` when the content has no bounds.
|
||
pub fn new(content: &[Arc<Node>], kind: &'a EnvelopeKind, frame: Affine, options: EnvelopeOptions) -> Option<Self> {
|
||
let frame = if frame.is_finite() && frame.determinant().abs() > 1e-12 { frame } else { Affine::IDENTITY };
|
||
let to_frame = frame.inverse();
|
||
let src = content.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, content_box(c, false, to_frame)))?;
|
||
let src = options.source_box(src);
|
||
Some(Self { src, frame, to_frame, surface: surface_of(kind, src, frame) })
|
||
}
|
||
|
||
/// The map of envelope node `n` (`None` for other nodes).
|
||
pub fn of(n: &'a Node) -> Option<Self> {
|
||
match &n.kind {
|
||
NodeKind::Envelope { content, kind, frame, options, .. } => Self::new(content, kind, **frame, *options),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// The content's bounds in the envelope's frame.
|
||
pub fn src(&self) -> Rect {
|
||
self.src
|
||
}
|
||
|
||
/// The surface point at (u, v) ∈ [0,1]².
|
||
pub fn at(&self, u: f64, v: f64) -> Point {
|
||
(self.surface)(u, v)
|
||
}
|
||
|
||
/// Where the envelope puts document point `p`.
|
||
pub fn map(&self, p: Point) -> Point {
|
||
let q = self.to_frame * p;
|
||
let (w, h) = (self.src.width().max(1e-9), self.src.height().max(1e-9));
|
||
self.at((q.x - self.src.x0) / w, (q.y - self.src.y0) / h)
|
||
}
|
||
|
||
/// A flat `(rows+1)×(cols+1)` grid over the content, in the envelope's frame (row-major).
|
||
pub fn grid(&self, rows: u32, cols: u32) -> Vec<Point> {
|
||
grid_points(self.src, rows, cols).into_iter().map(|p| self.frame * p).collect()
|
||
}
|
||
|
||
/// The surface sampled on a `rows`×`cols` grid as a mesh whose handles follow it and whose
|
||
/// points are `color`: Release gives it back as the envelope's shape, Reset with Mesh starts
|
||
/// from it.
|
||
pub fn surface_mesh(&self, rows: u32, cols: u32, color: Color) -> GradientMesh {
|
||
GradientMesh::from_surface(rows, cols, &|u, v| self.at(u, v), &|_, _| color)
|
||
}
|
||
|
||
/// The surface's bounds (sampled; a mesh's points and a top object's path bound it exactly).
|
||
fn bounds(&self) -> Option<Rect> {
|
||
const N: usize = 16;
|
||
let mut r: Option<Rect> = None;
|
||
for j in 0..=N {
|
||
for i in 0..=N {
|
||
let q = self.at(i as f64 / N as f64, j as f64 / N as f64);
|
||
r = Some(r.map_or(Rect::from_points(q, q), |r| r.union_pt(q)));
|
||
}
|
||
}
|
||
r
|
||
}
|
||
}
|
||
|
||
/// Evaluate an envelope of `kind` around `content` on the page axes with the options envelopes
|
||
/// had before Envelope Options (see [`expand_live_hooks`] for an envelope node).
|
||
pub fn envelope_expand(content: &[Arc<Node>], kind: &EnvelopeKind, fidelity: f64, outline: Outliner) -> Vec<Node> {
|
||
expand_envelope(content, kind, fidelity, EnvelopeOptions::default(), Affine::IDENTITY, outline.into())
|
||
}
|
||
|
||
/// An envelope's content pushed through its map: Fidelity sets how finely curves are cut (and
|
||
/// into how many pieces images are), the options what bends with the art.
|
||
pub(super) fn expand_envelope(
|
||
content: &[Arc<Node>],
|
||
kind: &EnvelopeKind,
|
||
fidelity: f64,
|
||
options: EnvelopeOptions,
|
||
frame: Affine,
|
||
hooks: Hooks,
|
||
) -> Vec<Node> {
|
||
let Some(map) = EnvelopeMap::new(content, kind, frame, options) else { return content.iter().map(|c| (**c).clone()).collect() };
|
||
let src = map.src();
|
||
let fid = if fidelity.is_finite() { fidelity.clamp(0.0, 100.0) / 100.0 } else { 0.5 };
|
||
let f = |p: Point| map.map(p);
|
||
let w = Warper {
|
||
f: &f,
|
||
piece: src.width().hypot(src.height()).max(1e-6) / (8.0 + fid * 56.0),
|
||
eps: (src.width().max(src.height()) * 0.05).max(0.5),
|
||
cells: (4.0 + fid * 12.0).round() as usize,
|
||
hooks,
|
||
options,
|
||
depth: 0,
|
||
};
|
||
content.iter().map(|c| map_node(c, &w)).collect()
|
||
}
|
||
|
||
/// Envelope bounds: a mesh's points, a top object's path, the warp's surface (sampled).
|
||
pub fn envelope_bounds(content: &[Arc<Node>], kind: &EnvelopeKind, frame: Affine, options: EnvelopeOptions) -> Option<Rect> {
|
||
match kind {
|
||
EnvelopeKind::Mesh { rows, cols, points, handles } if !handles.is_empty() => envelope_grid(*rows, *cols, points, handles)?.outline().bounds(),
|
||
EnvelopeKind::Mesh { points, .. } => {
|
||
let first = *points.first()?;
|
||
Some(points.iter().fold(Rect::from_points(first, first), |r, p| r.union_pt(*p)))
|
||
}
|
||
EnvelopeKind::TopObject { path, .. } => path.bounds(),
|
||
EnvelopeKind::Warp { .. } => EnvelopeMap::new(content, kind, frame, options)?.bounds(),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::{CharStyle, TextObject};
|
||
|
||
/// Look `n` up in `kept`, measuring it (and counting the measures) when it isn't there.
|
||
fn look_up(kept: &mut TypeInks, n: &Arc<Node>, linear: Affine, measured: &mut usize) -> Option<Rect> {
|
||
let NodeKind::Text(t) = &n.kind else { panic!("type") };
|
||
match kept.find(n, t, &(linear, None), 0) {
|
||
Ok(ink) => ink,
|
||
Err(missing) => {
|
||
*measured += 1;
|
||
let ink = Some(Rect::new(0.0, 0.0, *measured as f64, 1.0));
|
||
kept.keep(n, *missing, ink);
|
||
ink
|
||
}
|
||
}
|
||
}
|
||
|
||
fn type_node(i: usize, text: &str) -> Arc<Node> {
|
||
let t = TextObject::point(Point::new(i as f64 * 10.0, 0.0), text, CharStyle::default());
|
||
Arc::new(Node::new(NodeId(i as u64), NodeKind::Text(Box::new(t))))
|
||
}
|
||
|
||
/// Many type objects are each measured once however often they are looked up; a moved or copied
|
||
/// object is found by its content, and a changed one or another transform is measured again.
|
||
#[test]
|
||
fn measured_type_is_kept_for_many_objects() {
|
||
let mut kept = TypeInks::default();
|
||
let mut measured = 0;
|
||
let nodes: Vec<_> = (0..200).map(|i| type_node(i, &format!("Type {i}"))).collect();
|
||
for _ in 0..5 {
|
||
for n in &nodes {
|
||
look_up(&mut kept, n, Affine::IDENTITY, &mut measured);
|
||
}
|
||
}
|
||
assert_eq!(measured, 200);
|
||
let mut moved = (*nodes[7]).clone();
|
||
moved.transform(Affine::translate((300.0, 40.0)), true);
|
||
let moved = Arc::new(moved);
|
||
assert_eq!(look_up(&mut kept, &moved, Affine::IDENTITY, &mut measured), look_up(&mut kept, &nodes[7], Affine::IDENTITY, &mut measured));
|
||
assert_eq!(measured, 200, "a moved object is found by its content");
|
||
let changed = type_node(7, "Changed");
|
||
look_up(&mut kept, &changed, Affine::IDENTITY, &mut measured);
|
||
assert_eq!(measured, 201, "a changed object is measured again");
|
||
look_up(&mut kept, &nodes[7], Affine::rotate(0.5), &mut measured);
|
||
assert_eq!(measured, 202, "another transform is measured again");
|
||
}
|
||
|
||
/// Type that differs only in what layout reads but files don't save (a first paragraph that
|
||
/// continues a thread, the resolved bounds of an inline graphic) is measured again.
|
||
#[test]
|
||
fn type_that_differs_in_what_is_not_saved_is_measured_again() {
|
||
let mut kept = TypeInks::default();
|
||
let mut measured = 0;
|
||
let mut t = TextObject::point(Point::ZERO, "Type", CharStyle::default());
|
||
t.runs.push(crate::TextRun::new("", CharStyle::default()));
|
||
t.runs[1].inline = Some(crate::InlineArt { symbol: "S".into(), scale: 1.0, baseline_shift: 0.0, bounds: None });
|
||
let node = |t: &TextObject| Arc::new(Node::new(NodeId(1), NodeKind::Text(Box::new(t.clone()))));
|
||
look_up(&mut kept, &node(&t), Affine::IDENTITY, &mut measured);
|
||
look_up(&mut kept, &node(&t), Affine::IDENTITY, &mut measured);
|
||
assert_eq!(measured, 1, "a copy is found by its content");
|
||
t.first_para_continues = true;
|
||
look_up(&mut kept, &node(&t), Affine::IDENTITY, &mut measured);
|
||
assert_eq!(measured, 2, "a first paragraph that continues a thread");
|
||
if let Some(inline) = &mut t.runs[1].inline {
|
||
inline.bounds = Some(Rect::new(0.0, 0.0, 10.0, 20.0));
|
||
}
|
||
look_up(&mut kept, &node(&t), Affine::IDENTITY, &mut measured);
|
||
assert_eq!(measured, 3, "an inline graphic's bounds");
|
||
}
|
||
|
||
/// A stroked group of type in a scaled frame reaches past its members by its own strokes scaled
|
||
/// with the frame, as the group moved into the frame does; deeply nested groups are measured in
|
||
/// one walk each.
|
||
#[test]
|
||
fn a_stroked_group_of_type_is_measured_with_its_strokes_in_the_frame() {
|
||
let text = type_node(1, "Group");
|
||
let mut group = Node::group(NodeId(2), vec![text.clone()]);
|
||
group.appearance = crate::Appearance::basic(vectorcraft_color::Paint::None, vectorcraft_color::Paint::solid(Color::BLACK), 4.0).into();
|
||
let group = Arc::new(group);
|
||
let to = Affine::scale(2.0);
|
||
let mut moved = (*group).clone();
|
||
moved.transform(to, true);
|
||
let want = moved.visual_of_members(std::iter::once(content_box(&text, true, to)));
|
||
assert!(want.is_some());
|
||
assert_eq!(content_box(&group, true, to), want);
|
||
let mut nested = group;
|
||
for i in 0..300 {
|
||
nested = Arc::new(Node::group(NodeId(10 + i), vec![nested]));
|
||
}
|
||
assert!(content_box(&nested, false, Affine::IDENTITY).is_some());
|
||
}
|
||
|
||
/// A table that fills up is emptied instead of growing, and what is looked up after that is
|
||
/// measured again.
|
||
#[test]
|
||
fn the_tables_stay_bounded() {
|
||
let mut kept = TypeInks::default();
|
||
let mut measured = 0;
|
||
let nodes: Vec<_> = (0..TYPE_INK_KEPT + 10).map(|i| type_node(i, &format!("Type {i}"))).collect();
|
||
for n in &nodes {
|
||
look_up(&mut kept, n, Affine::IDENTITY, &mut measured);
|
||
}
|
||
assert!(kept.content.len() <= TYPE_INK_KEPT && kept.nodes.len() <= TYPE_INK_KEPT, "{} {}", kept.content.len(), kept.nodes.len());
|
||
assert_eq!(measured, TYPE_INK_KEPT + 10);
|
||
}
|
||
}
|