diff --git a/crates/svg/src/export/nodes.rs b/crates/svg/src/export/nodes.rs index 73135df262..ea1d5ab95b 100644 --- a/crates/svg/src/export/nodes.rs +++ b/crates/svg/src/export/nodes.rs @@ -449,17 +449,24 @@ impl Writer<'_> { /// Do `n`, its fills and strokes, its members at any depth, its symbols' art or the art its /// live objects are written as have a visible raster effect (shadow, glow, blur, feather)? - /// Objects that aren't written (hidden ones, unless the options keep them) are skipped. The - /// walk keeps its own stack, and looks at each symbol's art once. + /// Objects that aren't written (hidden ones, unless the options keep them) are skipped, and so + /// are fills and strokes that [`Self::shape`] doesn't write (hidden, unpainted or zero-width). + /// An instance with overrides is looked at in the art it is written as + /// ([`vectorcraft_brush::instance_art`]). The walk keeps its own stack, and looks at each + /// symbol's art once, and at each instance's overridden art once. fn holds_raster(&self, n: &Node) -> bool { let mut stack: Vec> = vec![]; - let mut symbols = std::collections::HashSet::new(); + let mut symbols: std::collections::HashSet<(String, Option)> = std::collections::HashSet::new(); + let paints = |i: &AppearanceItem| match i { + AppearanceItem::Fill(f) => f.visible && !f.paint.is_none(), + AppearanceItem::Stroke(s) => s.visible && !s.paint.is_none() && s.width > 0.0, + }; // Whether `n` itself has one; else its members, symbol art or expanded art go on the stack. let mut visit = |n: &Node, stack: &mut Vec>| { if !(n.visible || self.opts.hidden_layers) { return false; } - if has_raster(&n.appearance.effects) || n.appearance.items.iter().any(|i| has_raster(i.effects())) { + if has_raster(&n.appearance.effects) || n.appearance.items.iter().any(|i| paints(i) && has_raster(i.effects())) { return true; } // A live object is written as its expanded art ([`Self::node_body`]), which holds no @@ -471,11 +478,13 @@ impl Writer<'_> { return false; } } - if let NodeKind::SymbolInstance { symbol, .. } = &n.kind - && symbols.insert(symbol.clone()) - && let Some(s) = self.doc.symbols.iter().find(|s| s.name == *symbol) - { - stack.push(s.art.clone()); + if let NodeKind::SymbolInstance { symbol, .. } = &n.kind { + let custom = !n.extras().symbol_overrides.is_empty(); + if symbols.insert((symbol.clone(), custom.then_some(n.id))) + && let Some(s) = self.doc.symbols.iter().find(|s| s.name == *symbol) + { + stack.push(if custom { std::sync::Arc::new(vectorcraft_brush::instance_art(&s.art, n)) } else { s.art.clone() }); + } } stack.extend(n.children().into_iter().flatten().cloned()); false diff --git a/crates/svg/tests/it/blend_export.rs b/crates/svg/tests/it/blend_export.rs index 1a46a43a4c..ec947041b0 100644 --- a/crates/svg/tests/it/blend_export.rs +++ b/crates/svg/tests/it/blend_export.rs @@ -261,9 +261,12 @@ fn a_masked_group_spans_its_region_when_its_art_paints_past_its_shapes() { InGroup, InNestedGroup, FillEffect, + HiddenFill, + UnpaintedStroke, HiddenInGroup, Brushed, InSymbol, + OverriddenSymbol, InBlend, } use Case::*; @@ -275,11 +278,17 @@ fn a_masked_group_spans_its_region_when_its_art_paints_past_its_shapes() { (InNestedGroup, true, true), (FillEffect, true, true), (FillEffect, false, false), + // A fill or stroke that isn't written doesn't count. + (HiddenFill, true, false), + (UnpaintedStroke, true, false), (HiddenInGroup, true, false), (Brushed, false, false), (Brushed, true, true), (InSymbol, true, true), (InSymbol, false, false), + // An unpainted fill in a dynamic symbol's art that the instance paints counts. + (OverriddenSymbol, true, true), + (OverriddenSymbol, false, false), (InBlend, true, true), (InBlend, false, false), ] { @@ -290,15 +299,28 @@ fn a_masked_group_spans_its_region_when_its_art_paints_past_its_shapes() { stroke.brush = Some("Bristle Round".into()); red.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(stroke)); } + if matches!(case, UnpaintedStroke) { + red.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(vectorcraft_doc::StrokeLayer::new(Paint::None, 8.0))); + } if shadowed { match case { - FillEffect => red.appearance.items[0].effects_mut().push(drop_shadow()), + FillEffect | HiddenFill | OverriddenSymbol => red.appearance.items[0].effects_mut().push(drop_shadow()), + UnpaintedStroke => red.appearance.items.last_mut().unwrap().effects_mut().push(drop_shadow()), _ => red.appearance.effects.push(drop_shadow()), } } + if matches!(case, HiddenFill) + && let vectorcraft_doc::AppearanceItem::Fill(f) = &mut red.appearance.items[0] + { + f.visible = false; + } + if matches!(case, OverriddenSymbol) { + red.appearance.set_fill(Paint::None); + } let group = |d: &mut Document, n: Node| Node::group(d.alloc_id(), vec![n.into()]); let mut masked = match case { Alone | FillEffect => red, + HiddenFill | UnpaintedStroke => group(&mut d, red), InGroup | Brushed => group(&mut d, red), InNestedGroup => { let inner = group(&mut d, red); @@ -313,6 +335,14 @@ fn a_masked_group_spans_its_region_when_its_art_paints_past_its_shapes() { let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Square".into(), xf: vectorcraft_geom::Affine::IDENTITY }); group(&mut d, instance) } + OverriddenSymbol => { + d.symbols.push(vectorcraft_doc::Symbol { dynamic: true, name: "Square".into(), art: std::sync::Arc::new(red) }); + let mut instance = + Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Square".into(), xf: vectorcraft_geom::Affine::IDENTITY }); + let fill = Some(Paint::solid(Color::rgb8(255, 0, 0))); + instance.extras_mut().symbol_overrides.push(vectorcraft_doc::symbol_overrides::SymbolOverride { fill, ..Default::default() }); + group(&mut d, instance) + } InBlend => { let mut blue = rect(&mut d, Rect::new(60.0, 10.0, 80.0, 30.0), [0, 0, 255]); blue.appearance.effects = red.appearance.effects.clone();