diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 89ba2d491..3f10343db 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -69,7 +69,7 @@ pub use live::{expand_live, expand_live_deep, expanded_live_group, text_outliner pub use marks::{CROP_MARKS, crop_marks_art, has_crop_marks}; pub use pixel::{PIXEL_EFFECTS, PixelFx, PixelSpace}; pub use raster::{RasterFx, outset, raster_effects}; -pub use reshape::{expand_outlined, has_item_geometry, needs_outline, outline_art, outline_text, reshape}; +pub use reshape::{expand_outlined, has_gradient_copies, has_item_geometry, needs_outline, outline_art, outline_text, reshape}; pub use revolve::{REVOLVE, has_revolve, revolve_art, revolve_options, validate_revolve}; pub use solids::{ EXTRUDE, INFLATE, ROTATE, ROTATION_PRESETS, SOLIDS, extrude_options, has_solid, inflate_options, is_three_d, solid_art, validate_solid, diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index ebfd22464..fa59cae55 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -32,6 +32,17 @@ pub fn needs_outline(n: &Node) -> bool { ) } +/// Is `n` a path or compound path with a visible Transform effect that makes copies and a gradient +/// in a fill or stroke? Such an object is drawn through its art ([`outline_art`]) so that each copy +/// paints its gradient by itself ([`copy_with_gradients`]), as a group's copies do. +pub fn has_gradient_copies(n: &Node) -> bool { + matches!(n.kind, NodeKind::Path { guide: false, clipping: false, .. } | NodeKind::Compound { .. }) + && (n.appearance.has_unplaced_gradient() || n.appearance.has_placed_gradient()) + && geometry_effects(n) + .iter() + .any(|e| e.id == "distort.transform" && crate::distort::transform_map(Rect::ZERO, &merged_params(&e.id, &e.params), 0).1 > 0) +} + /// Is `n` type without inline graphics that has a fill or stroke of its own that paints and has a /// visible geometry effect? Such an effect reshapes the whole outline that its fill or stroke /// paints ([`outline_text`], which leaves inline graphics out). @@ -127,6 +138,16 @@ pub fn outline_art(n: &Node, symbol_art: Option<&Node>) -> Option { a.transform(*xf, false); Node::group(n.id, vec![Arc::new(a)]) } + // The object as the one piece the copies of its Transform effect are made from. + _ if has_gradient_copies(n) => { + let mut one = n.clone(); + one.opacity = 1.0; + one.blend = Default::default(); + one.isolate = false; + one.extras_mut().mask = None; + one.appearance.effects.clear(); + Node::group(n.id, vec![Arc::new(one)]) + } _ if vectorcraft_doc::live::is_live(n) => vectorcraft_doc::live::expand_deep(n, Some(hook)), _ => return None, }; @@ -171,7 +192,7 @@ pub(crate) fn outline_nested_text(n: &mut Node, clipping: bool) { /// a geometry effect on a fill or stroke of its own ([`has_item_geometry`]), or has no art (a /// symbol instance without `symbol_art`). pub fn reshape(n: &Node, symbol_art: Option<&Node>) -> Option { - if (geometry_effects(n).is_empty() && n.projection().is_none() && !has_item_geometry(n)) || !needs_outline(n) { + if (geometry_effects(n).is_empty() && n.projection().is_none() && !has_item_geometry(n)) || !(needs_outline(n) || has_gradient_copies(n)) { return None; } as_art(n, symbol_art) diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index 23352a280..f5281fb47 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -68,11 +68,13 @@ 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`]), or is it a +/// 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`])? 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) + || effects::has_gradient_copies(n) || effects::has_container_appearance(n) || effects::has_crop_marks(n) || effects::has_adjustment(n) diff --git a/crates/render/src/tests_objectfx.rs b/crates/render/src/tests_objectfx.rs index 7a9add652..f4c72553f 100644 --- a/crates/render/src/tests_objectfx.rs +++ b/crates/render/src/tests_objectfx.rs @@ -697,3 +697,24 @@ fn a_strokes_own_glow_paints_around_that_stroke_only() { let worst = img.pixels.iter().zip(&mt.pixels).map(|(a, b)| a.abs_diff(*b)).max().unwrap(); assert!(worst <= 4, "max channel difference {worst}"); } + +#[test] +fn a_path_with_its_own_transform_paints_each_copy_with_its_own_gradient() { + use vectorcraft_color::GradientGeom; + let stops = vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))]; + let ramp = GradientPaint::new(Gradient { kind: GradientKind::Linear, stops }); + let geom = GradientGeom { start: Point::new(0.0, 50.0), end: Point::new(20.0, 50.0), aspect: 1.0, focal: None }; + // Fitted to the box on each render, and placed on the object. + for paint in [ramp.clone(), GradientPaint { geom: Some(geom), ..ramp }] { + let square = shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 40.0, 20.0, 60.0)); + let mut n = Node::path(NodeId(0), square, Appearance::basic(Paint::Gradient(Box::new(paint)), Paint::None, 0.0)); + n.appearance.effects = vec![fx("distort.transform", json!({"moveH": 40, "copies": 1}))]; + let img = render(&doc_with(Document::new(100.0, 100.0), n)); + let at = |x: usize| img.pixels.chunks(4).nth(50 * 100 + x).map(|p| (p[0], p[2])).unwrap_or_default(); + // The left edge of each copy is red, its right edge blue. + for (left, right) in [(1, 18), (41, 58)] { + assert!(at(left).0 > 200 && at(left).1 < 60, "{:?} at {left}", at(left)); + assert!(at(right).1 > 200 && at(right).0 < 60, "{:?} at {right}", at(right)); + } + } +}