mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
Appearance: image fills and strokes draw the shape Convert to Shape or Outline Object gives them
Squashed from #2500 (its merges of main dropped).
This commit is contained in:
+12
-10
@@ -79,9 +79,10 @@ pub(crate) fn has_fx(n: &Node) -> bool {
|
||||
|
||||
/// Does `n` carry visible object-level effects that only apply through its art (type, images,
|
||||
/// symbol instances, live objects; Crop Marks and colour adjustments on anything), is it type with
|
||||
/// a geometry effect on a fill or stroke of its own ([`effects::has_item_geometry`]), is it a path
|
||||
/// whose Transform effect copies it with a gradient ([`effects::has_gradient_copies`]), or is it a
|
||||
/// group or layer with an appearance of its own (see [`Renderer::draw_object_fx`])?
|
||||
/// a geometry effect on a fill or stroke of its own or an image with a fill or stroke of its own
|
||||
/// that paints a shape ([`effects::has_item_geometry`]), is it a path whose Transform effect copies
|
||||
/// it with a gradient ([`effects::has_gradient_copies`]), or is it a group or layer with an
|
||||
/// appearance of its own (see [`Renderer::draw_object_fx`])?
|
||||
pub(crate) fn has_object_fx(n: &Node) -> bool {
|
||||
(effects::needs_outline(n) && (visible(&n.appearance.effects) || n.projection().is_some()))
|
||||
|| effects::has_item_geometry(n)
|
||||
@@ -341,9 +342,9 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
r
|
||||
}
|
||||
// As [`cull_bounds_in`] measures them, without the object's own raster effects: the
|
||||
// reshaped art (type's outlines measured one by one with the effects of its own fills and
|
||||
// strokes), grown by how far the effects in a symbol's art and a live object's content
|
||||
// paint past it.
|
||||
// reshaped art (type's outlines, and an image and the paths of its own fills and strokes,
|
||||
// measured one by one with the effects of those fills and strokes), grown by how far the
|
||||
// effects in a symbol's art and a live object's content paint past it.
|
||||
_ if has_object_fx(n) => {
|
||||
let symbol = match &n.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => symbols.document().symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()),
|
||||
@@ -351,7 +352,7 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
|
||||
};
|
||||
let reshaped = effects::reshape(n, symbol.as_deref());
|
||||
let b = match &reshaped {
|
||||
Some(r) if matches!(n.kind, NodeKind::Text(_)) => {
|
||||
Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => {
|
||||
r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, painted_reach(symbols, c)))
|
||||
}
|
||||
Some(r) => r.visual_bounds_with(symbols),
|
||||
@@ -444,8 +445,9 @@ fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool) -> Optio
|
||||
}
|
||||
_ if has_fx(n) => visual_bounds(symbols, n),
|
||||
// The reshaped art (a symbol instance's reshaped art is made from its symbol's art; type's
|
||||
// outlines measured one by one with the geometry effects of its own fills and strokes),
|
||||
// grown by how far the effects in a symbol instance's art paint past that art.
|
||||
// outlines, and an image and the paths of its own fills and strokes, measured one by one
|
||||
// with the geometry effects of those fills and strokes), grown by how far the effects in a
|
||||
// symbol instance's art paint past that art.
|
||||
_ if has_object_fx(n) => {
|
||||
let symbol = match &n.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => symbols.document().symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()),
|
||||
@@ -453,7 +455,7 @@ fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool) -> Optio
|
||||
};
|
||||
let reshaped = effects::reshape(n, symbol.as_deref());
|
||||
let b = match &reshaped {
|
||||
Some(r) if matches!(n.kind, NodeKind::Text(_)) => {
|
||||
Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => {
|
||||
r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, inner(c)))
|
||||
}
|
||||
Some(r) => r.visual_bounds_with(symbols),
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint};
|
||||
use vectorcraft_doc::pattern::{PatternDef, pattern_paint};
|
||||
use vectorcraft_doc::text::{CharStyle, TextObject};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Effect, FillLayer, ImageBlob, ImageObject, Node, NodeKind, Symbol};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Effect, FillLayer, ImageBlob, ImageObject, Node, NodeKind, StrokeLayer, Symbol};
|
||||
use vectorcraft_geom::{Point, shapes};
|
||||
|
||||
use super::*;
|
||||
@@ -679,6 +679,65 @@ fn a_rectangle_on_a_fill_below_type_with_outline_object_fits_the_glyphs() {
|
||||
assert_eq!(lum(img.pixel(x, glyphs.y1 as u32 + 12)), WHITE);
|
||||
}
|
||||
|
||||
/// [`image_doc`] with a black 4 pt stroke of its own that carries the effects `row`, below the
|
||||
/// image when `below`.
|
||||
fn stroked_image_doc(row: Vec<Effect>, below: bool) -> Document {
|
||||
let mut d = image_doc(vec![]);
|
||||
let id = d.layers[0].children().unwrap()[0].id;
|
||||
let mut stroke = StrokeLayer::new(Paint::solid(Color::BLACK), 4.0);
|
||||
stroke.effects = row;
|
||||
let ap = &mut d.node_mut(id).unwrap().appearance;
|
||||
ap.items.push(AppearanceItem::Stroke(stroke));
|
||||
ap.set_contents_at(usize::from(below));
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_images_stroke_draws_only_the_shape_convert_to_shape_or_outline_object_gives_it() {
|
||||
// The image's frame runs from 20 to 50. Without an effect the stroke draws nothing.
|
||||
for below in [false, true] {
|
||||
let img = render(&stroked_image_doc(vec![], below));
|
||||
assert_eq!((lum(img.pixel(18, 35)), img.pixel(21, 35)), (WHITE, [255, 0, 0, 255]));
|
||||
}
|
||||
let rectangle = fx("convertToShape.rectangle", json!({"relative": true, "extraW": 0, "extraH": 0}));
|
||||
let outline = fx("path.outlineObject", json!({}));
|
||||
for effect in [rectangle, outline] {
|
||||
// Below the image only the stroke's outer half shows, above it both halves.
|
||||
let img = render(&stroked_image_doc(vec![effect.clone()], true));
|
||||
assert_eq!((lum(img.pixel(17, 35)), lum(img.pixel(18, 35)), img.pixel(21, 35)), (WHITE, 0, [255, 0, 0, 255]), "{}", effect.id);
|
||||
let img = render(&stroked_image_doc(vec![effect.clone()], false));
|
||||
assert_eq!((lum(img.pixel(18, 35)), lum(img.pixel(21, 35)), img.pixel(23, 35)), (0, 0, [255, 0, 0, 255]), "{}", effect.id);
|
||||
// An effect before it is left out.
|
||||
let first = fx("path.offsetPath", json!({"offset": 10}));
|
||||
let img = render(&stroked_image_doc(vec![first, effect.clone()], true));
|
||||
assert_eq!((lum(img.pixel(10, 35)), lum(img.pixel(18, 35))), (WHITE, 0), "{}", effect.id);
|
||||
}
|
||||
// An Offset Path of 2 pt after Outline Object moves the stroke 2 pt out.
|
||||
let img = render(&stroked_image_doc(vec![fx("path.outlineObject", json!({})), fx("path.offsetPath", json!({"offset": 2}))], true));
|
||||
assert_eq!((lum(img.pixel(15, 35)), lum(img.pixel(16, 35)), lum(img.pixel(19, 35))), (WHITE, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_images_stroke_is_drawn_when_only_it_is_in_view() {
|
||||
// The stroke runs 8 pt outside the frame, from 10 to 14; the view shows x 15 and less.
|
||||
let row = vec![fx("path.outlineObject", json!({})), fx("path.offsetPath", json!({"offset": 8}))];
|
||||
let d = stroked_image_doc(row, true);
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let img = Renderer::new().render(&d, 100, 100, Affine::translate((85.0, 0.0)), &opts);
|
||||
assert_eq!((lum(img.pixel(96, 35)), lum(img.pixel(99, 35))), (0, WHITE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rasterize_on_an_image_takes_in_its_strokes_shape() {
|
||||
// The stroke runs 8 pt outside the frame, from 10 to 14; Rasterize at 72 ppi keeps it.
|
||||
let row = vec![fx("path.outlineObject", json!({})), fx("path.offsetPath", json!({"offset": 8}))];
|
||||
let mut d = stroked_image_doc(row, true);
|
||||
let id = d.layers[0].children().unwrap()[0].id;
|
||||
d.node_mut(id).unwrap().appearance.effects.push(fx("rasterize", json!({"useDocumentResolution": false, "resolution": 72})));
|
||||
let img = render(&d);
|
||||
assert_eq!((lum(img.pixel(8, 35)), lum(img.pixel(12, 35)), img.pixel(35, 35)), (WHITE, 0, [255, 0, 0, 255]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_strokes_own_glow_paints_around_that_stroke_only() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
|
||||
Reference in New Issue
Block a user