diff --git a/crates/engine/src/vector_behaviour_tests.rs b/crates/engine/src/vector_behaviour_tests.rs index 785a03d0..9d24bc39 100644 --- a/crates/engine/src/vector_behaviour_tests.rs +++ b/crates/engine/src/vector_behaviour_tests.rs @@ -79,7 +79,11 @@ fn region_paints_survive_simplify_offset_and_bucket_normalization() { prior_world_frame.as_coeffs().iter().zip(after_world_frame.as_coeffs()).all(|(before, after)| (before - after).abs() < 1e-8), "flattening an already framed gradient preserves its source coordinates" ); +} +#[test] +fn explicit_and_inherited_region_paints_survive_refit() { + let mut s = Session::new(); let empty_id = square(&mut s, false); ex_to_vector(s.doc_mut().unwrap(), empty_id); { @@ -106,7 +110,11 @@ fn region_paints_survive_simplify_offset_and_bucket_normalization() { assert_eq!(inherited.fills, None, "inherited paints remain inherited after offset"); let inherited_space = inherited.paint_space.expect("offset binds inherited paints to the original frame"); assert_eq!((inherited_space.width, inherited_space.height), (100.0, 100.0)); +} +#[test] +fn flatten_keeps_inherited_region_paints_inherited() { + let mut s = Session::new(); let inherited_flatten = square(&mut s, false); let inherited_fills = s.doc().unwrap().get(inherited_flatten).unwrap().fills.clone(); ex_to_vector(s.doc_mut().unwrap(), inherited_flatten); @@ -142,7 +150,11 @@ fn region_paints_survive_simplify_offset_and_bucket_normalization() { s.doc_mut().unwrap().get_mut(group).unwrap().fills.clear(); let flattened = id_of(&ex(&mut s, "vector.flatten", json!({"ids": [group]}))["id"]).unwrap(); assert_eq!(net(&s, flattened).regions[0].fills, Some(source_fills), "flatten retains paints that differ from output node fills"); +} +#[test] +fn legacy_offset_preserves_the_gradients_world_paint_frame() { + let mut s = Session::new(); let legacy_gradient = square(&mut s, false); let mut gradient = figcraft_color::Gradient::default_for(figcraft_color::GradientKind::Linear, figcraft_color::Color::BLACK); gradient.from = (0.2, 0.1); @@ -164,7 +176,11 @@ fn region_paints_survive_simplify_offset_and_bucket_normalization() { before_world.as_coeffs().iter().zip(after_world.as_coeffs()).all(|(before, after)| (before - after).abs() < 1e-8), "legacy offset preserves the gradient's world paint frame" ); +} +#[test] +fn bucket_fills_an_explicitly_empty_face_and_leaves_the_other_empty() { + let mut s = Session::new(); let empty_faces = id_of(&ex(&mut s, "vector.create", json!({"path": "M0 0 L100 0 L100 100 L0 100 Z M200 0 L300 0 L300 100 L200 100 Z"}))["id"]).unwrap(); { diff --git a/crates/render/src/draw.rs b/crates/render/src/draw.rs index a5af3059..3ff68026 100644 --- a/crates/render/src/draw.rs +++ b/crates/render/src/draw.rs @@ -723,6 +723,31 @@ fn draw_outline(ctx: &Ctx, pix: &mut Pixmap, n: &Node, xf: Affine, depth: usize) /// What paints over a node's fills, under its children and strokes (its inner shadows). type OverFills<'a> = Option<&'a dyn Fn(&mut Pixmap)>; +/// A vector's network when any of its regions has paints or a paint frame of its own. +fn region_painted(n: &Node) -> Option<&figcraft_doc::VectorNetwork> { + match &n.kind { + NodeKind::Vector { network } if network.regions.iter().any(|r| r.fills.is_some() || r.paint_space.is_some()) => Some(network), + _ => None, + } +} + +/// Each region of a vector with region paints, filled with its own paints (or the layer's) in its +/// own paint frame. +fn fill_vector_regions(ctx: &Ctx, pix: &mut Pixmap, n: &Node, network: &figcraft_doc::VectorNetwork, xf: Affine) { + let rounded = network.rounded(); + let net = rounded.as_ref().unwrap_or(network); + for (i, region) in net.regions.iter().enumerate() { + let paints = region.fills.as_deref().unwrap_or(&n.fills); + if let Some(mut path) = net.region_path(i) { + let (pw, ph, frame) = region.paint_frame(n.width, n.height); + path.apply_affine(frame.inverse()); + let Some(tp) = ts_path(&path) else { continue }; + let rule = if region.winding == figcraft_doc::WindingRule::EvenOdd { FillRule::EvenOdd } else { FillRule::Winding }; + fill_with(pix, &tp, paints, pw, ph, xf * frame, rule, ctx, None); + } + } +} + /// Fills, `over_fills`, children (clipped), strokes and text of a node, without opacity/effects. fn draw_content(ctx: &Ctx, pix: &mut Pixmap, n: &Node, xf: Affine, depth: usize, over_fills: OverFills) { let doc = ctx.doc; @@ -804,27 +829,13 @@ fn draw_content(ctx: &Ctx, pix: &mut Pixmap, n: &Node, xf: Affine, depth: usize, let closed = figcraft_doc::geometry::is_closed(n) || matches!(n.kind, NodeKind::BooleanOp { .. }); let splatted = ctx.lod && closed && lod::splat_fills(pix, n, xf); let clips = n.clip && n.kind.is_frame_like() && !n.children.is_empty(); - let styled_network = match &n.kind { - NodeKind::Vector { network } if network.regions.iter().any(|r| r.fills.is_some() || r.paint_space.is_some()) => Some(network), - _ => None, - }; + let styled_network = region_painted(n); // The outline fills, clips children to rounded corners and strokes booleans. let outline = if styled_network.is_some() || (splatted && !(clips && n.corners.uniform() != Some(0.0))) { None } else { node_outline(doc, n) }; if !splatted && closed { if let Some(network) = styled_network { - let rounded = network.rounded(); - let net = rounded.as_ref().unwrap_or(network); - for (i, region) in net.regions.iter().enumerate() { - let paints = region.fills.as_deref().unwrap_or(&n.fills); - if let Some(mut path) = net.region_path(i) { - let (pw, ph, frame) = region.paint_frame(w, h); - path.apply_affine(frame.inverse()); - let Some(tp) = ts_path(&path) else { continue }; - let rule = if region.winding == figcraft_doc::WindingRule::EvenOdd { FillRule::EvenOdd } else { FillRule::Winding }; - fill_with(pix, &tp, paints, pw, ph, xf * frame, rule, ctx, None); - } - } + fill_vector_regions(ctx, pix, n, network, xf); } else if let Some((path, rule)) = &outline && let Some(tp) = ts_path(path) {