mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Type: colour glyphs go with type that effects reshape and Expand Appearance
COLR and SVG glyphs, which have no outlines, were left out wherever type was taken as its outlines: geometry effects, Expand Appearance, EMF/WMF export.
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Effect, Node, NodeKind};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Effect, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Rect, Shape, shapes};
|
||||
|
||||
use crate::{Budget, GeomContext, apply_one, is_geometry, merged_params};
|
||||
@@ -140,12 +140,13 @@ pub fn image_paint(n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> {
|
||||
/// The glyph outlines of type `t` in text space with its underline and strikethrough bars: one
|
||||
/// path per run, and all of them as one.
|
||||
pub(crate) fn glyph_outlines(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath) {
|
||||
let (runs, all, _) = laid_out(t);
|
||||
let (runs, all, ..) = laid_out(t);
|
||||
(runs, all)
|
||||
}
|
||||
|
||||
/// [`glyph_outlines`] and the bounds of `t`'s layout in text space.
|
||||
fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath, Rect) {
|
||||
/// [`glyph_outlines`], the bounds of `t`'s layout in text space, and the art of its colour glyphs
|
||||
/// (COLR layers, SVG documents), which have no outlines, placed in the document, with their runs.
|
||||
fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath, Rect, Vec<(usize, Node)>) {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let layout = vectorcraft_text::layout(db, t);
|
||||
let mut runs = vec![kurbo::BezPath::new(); t.runs.len()];
|
||||
@@ -163,34 +164,36 @@ fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::Bez
|
||||
}
|
||||
all.extend(bar.iter());
|
||||
}
|
||||
(runs, all, layout.bounds)
|
||||
let colours = layout.glyphs.iter().filter_map(|g| Some((g.run, g.placed_colour(t.xf)?))).collect();
|
||||
(runs, all, layout.bounds, colours)
|
||||
}
|
||||
|
||||
/// Type as glyph outlines in document space: one path per run with the run's fill and stroke,
|
||||
/// and (when the object has its own fills/strokes) the whole outline with the object's items below
|
||||
/// the characters under them and the others over them, as the renderer paints them. Each run's
|
||||
/// path is placed by the type's transform, which also scales its stroke weights and dashes and
|
||||
/// transforms its pattern tiles; its unplaced gradients are first fixed to the layout's bounds in
|
||||
/// text space. The object's own unplaced gradients are fixed to the bounding box of those bounds
|
||||
/// in the document. The group is marked as type's outlines
|
||||
/// its colour glyphs' art, and (when the object has its own fills/strokes) the whole outline with
|
||||
/// the object's items below the characters under them and the others over them, as the renderer
|
||||
/// paints them. Each run's path is placed by the type's transform, which also scales its stroke
|
||||
/// weights and dashes and transforms its pattern tiles; its unplaced gradients are first fixed to
|
||||
/// the layout's bounds in text space. The object's own unplaced gradients are fixed to the
|
||||
/// bounding box of those bounds in the document. The group is marked as type's outlines
|
||||
/// ([`vectorcraft_doc::NodeExtras::type_outlines`]).
|
||||
pub fn outline_text(n: &Node) -> Option<Node> {
|
||||
let NodeKind::Text(t) = &n.kind else { return None };
|
||||
let (runs, all, bounds) = laid_out(t);
|
||||
let (runs, all, bounds, colours) = laid_out(t);
|
||||
let path = |bp: &kurbo::BezPath| PathData::from_bezpath(bp).transformed(t.xf);
|
||||
let scaling = vectorcraft_doc::Scaling { strokes: true, patterns: true, ..Default::default() };
|
||||
let mut children: Vec<Arc<Node>> = runs
|
||||
.iter()
|
||||
.zip(&t.runs)
|
||||
.filter(|(bp, _)| !bp.elements().is_empty())
|
||||
.map(|(bp, run)| {
|
||||
let mut ap = run.style.appearance();
|
||||
ap.pin_gradients(bounds);
|
||||
let mut piece = Node::path(n.id, PathData::from_bezpath(bp), ap);
|
||||
piece.transform(t.xf, scaling);
|
||||
Arc::new(piece)
|
||||
})
|
||||
.collect();
|
||||
let mut children: Vec<Arc<Node>> = vec![];
|
||||
for (i, (bp, run)) in runs.iter().zip(&t.runs).enumerate() {
|
||||
// Colour glyphs paint themselves, under their run's fill, as the renderer paints them.
|
||||
children.extend(colours.iter().filter(|(r, _)| *r == i).map(|(_, art)| Arc::new(with_id(art, n.id))));
|
||||
if bp.elements().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut ap = run.style.appearance();
|
||||
ap.pin_gradients(bounds);
|
||||
let mut piece = Node::path(n.id, PathData::from_bezpath(bp), ap);
|
||||
piece.transform(t.xf, scaling);
|
||||
children.push(Arc::new(piece));
|
||||
}
|
||||
if !n.appearance.items.is_empty() && !all.elements().is_empty() {
|
||||
let (below, above) = n.appearance.split_contents();
|
||||
let tb = t.xf.transform_rect_bbox(bounds);
|
||||
@@ -211,6 +214,17 @@ pub fn outline_text(n: &Node) -> Option<Node> {
|
||||
Some(g)
|
||||
}
|
||||
|
||||
/// `art` with `id` for it and all its members, as the other pieces of outlined type have the type's
|
||||
/// (Expand Appearance gives them ids of their own).
|
||||
fn with_id(art: &Node, id: NodeId) -> Node {
|
||||
let mut art = art.clone();
|
||||
art.id = id;
|
||||
for c in art.children_mut().into_iter().flatten() {
|
||||
*c = Arc::new(with_id(c, id));
|
||||
}
|
||||
art
|
||||
}
|
||||
|
||||
/// `n` as plain art that geometry effects can reshape, without its transparency and object
|
||||
/// effects: type outlined ([`outline_text`]), live objects evaluated (type inside them outlined),
|
||||
/// a symbol instance as `symbol_art` (the symbol's art, stained as the caller draws it) placed by
|
||||
|
||||
@@ -132,3 +132,22 @@ fn pdf_draws_an_svg_glyphs_art_once_when_its_font_cant_be_embedded() {
|
||||
let red = vectorcraft_color::Paint::solid(vectorcraft_color::Color::rgb8(255, 0, 0));
|
||||
assert_eq!(fills_of(&pdf).iter().filter(|(p, _)| *p == red).count(), 1);
|
||||
}
|
||||
|
||||
/// Expand Appearance of type with an effect of its own (drawn reshaped as its outlines) keeps its
|
||||
/// colour glyphs, as their art.
|
||||
#[test]
|
||||
fn expand_appearance_keeps_the_colour_glyphs_of_type_with_effects() {
|
||||
use vectorcraft_text::test_fonts::{COLOUR_CHAR, colr_family, colr_font};
|
||||
vectorcraft_text::FontDb::global().add_font(colr_font(true).unwrap());
|
||||
let mut s = session();
|
||||
let r = s.execute("text.create", &json!({"x": 10, "y": 50, "text": format!("{COLOUR_CHAR}F"), "size": 32, "font": colr_family(true)})).unwrap();
|
||||
let t = NodeId(r["id"].as_u64().unwrap());
|
||||
s.execute("effect.apply", &json!({"ids": [t.0], "effect": "distort.transform", "params": {"moveH": 5}})).unwrap();
|
||||
let r = ids(&s.execute("effect.expandAppearance", &json!({})).unwrap());
|
||||
let mut gradients = 0;
|
||||
for id in r {
|
||||
node(&s, id).walk(&mut |n| gradients += usize::from(matches!(n.appearance.fill_paint(), vectorcraft_color::Paint::Gradient(_))));
|
||||
}
|
||||
assert!(gradients > 0, "the colour glyph's gradient layers");
|
||||
vectorcraft_testkit::invariants::check_document(&s.doc().unwrap().doc).unwrap();
|
||||
}
|
||||
|
||||
@@ -87,3 +87,37 @@ fn a_sweep_glyph_draws_its_image() {
|
||||
assert!(ink(&img, |p| p[0] > 180 && p[1] < 60 && p[2] < 120).is_some(), "red where the sweep starts");
|
||||
assert!(ink(&img, magenta).is_none());
|
||||
}
|
||||
|
||||
/// Type with a geometry effect is drawn reshaped as its outlines: its colour glyphs go with them,
|
||||
/// as their art (here moved 40 pt by a Transform effect).
|
||||
#[test]
|
||||
fn colour_glyphs_go_with_type_that_a_geometry_effect_reshapes() {
|
||||
let _adding = crate::tests_fontchange::adding_fonts();
|
||||
vectorcraft_text::FontDb::global().add_font(colr_font(false).unwrap());
|
||||
let mut d = Document::new(160.0, 160.0);
|
||||
let st = CharStyle { font_family: colr_family(false).into(), size: 100.0, ..Default::default() };
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(20.0, 120.0), &COLOUR_CHAR.to_string(), st))));
|
||||
let moved = vectorcraft_doc::Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 40}), visible: true };
|
||||
let mut appearance = (*n.appearance).clone();
|
||||
appearance.effects.push(moved);
|
||||
n.appearance = appearance.into();
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
let img = Renderer::new().render(&d, 160, 160, Affine::IDENTITY, &RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() });
|
||||
let red = ink(&img, |p| p == [255, 0, 0, 255]).expect("the `O` layer, in red");
|
||||
assert!(red.0 >= 58, "moved right with the type: {red:?}");
|
||||
}
|
||||
|
||||
/// Type's outlines stack as the canvas paints type: a run's colour glyphs under its fill, so a
|
||||
/// plain character set over a colour one stays on top.
|
||||
#[test]
|
||||
fn outlined_colour_glyphs_stay_under_their_runs_fill() {
|
||||
let _adding = crate::tests_fontchange::adding_fonts();
|
||||
vectorcraft_text::FontDb::global().add_font(colr_font(false).unwrap());
|
||||
let mut d = Document::new(160.0, 160.0);
|
||||
let st = CharStyle { font_family: colr_family(false).into(), size: 100.0, ..Default::default() };
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(20.0, 120.0), &format!("{COLOUR_CHAR}F"), st))));
|
||||
let outlined = vectorcraft_effects::outline_text(&n).unwrap();
|
||||
let kinds: Vec<bool> = outlined.children().unwrap().iter().map(|c| matches!(c.kind, NodeKind::Group { .. })).collect();
|
||||
assert_eq!(kinds, [true, false], "the colour glyph's group, then the run's path");
|
||||
}
|
||||
|
||||
+5
-1
File diff suppressed because one or more lines are too long
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user