diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index 58125c661..0a8479d61 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use serde_json::Value; -use vectorcraft_doc::color::{Color, Paint}; -use vectorcraft_doc::{Appearance, AppearanceItem, Effect, FillLayer, LineJoin, Node, NodeKind, StrokeLayer}; +use vectorcraft_doc::color::{BlendMode, Color, Paint}; +use vectorcraft_doc::{Appearance, AppearanceItem, Effect, FillLayer, LineJoin, Node, NodeId, NodeKind, StrokeLayer}; use vectorcraft_geom::{PathData, Rect}; use vectorcraft_three_d::{Edge, Revolve}; @@ -431,6 +431,36 @@ fn art(n: &Node, expand: bool) -> Option { Some(art) } +/// The members' `surfaces` in order, from the blending mode at `level` of each one's +/// `Profile::blends` inward: the surfaces of consecutive members that share a group with a mode +/// other than Normal go in one group with that mode, so each mode applies once, at its own level. +fn blended(id: NodeId, surfaces: Vec<(&[(usize, BlendMode)], Node)>, level: usize) -> Vec> { + fn close(id: NodeId, run: &mut Vec<(&[(usize, BlendMode)], Node)>, key: Option<(usize, BlendMode)>, level: usize, out: &mut Vec>) { + if let Some((_, blend)) = key { + let mut group = Node::group(id, blended(id, std::mem::take(run), level + 1)); + group.blend = blend; + out.push(Arc::new(group)); + } + } + let mut out = vec![]; + let mut run = vec![]; + let mut key = None; + for (blends, s) in surfaces { + let at = blends.get(level).copied(); + if at != key { + close(id, &mut run, key, level, &mut out); + key = at; + } + if key.is_some() { + run.push((blends, s)); + } else { + out.push(Arc::new(s)); + } + } + close(id, &mut run, key, level, &mut out); + out +} + /// [`art`] of group or layer `n`: every member swept about one axis on the bounds of all of them, /// each in its own paint. Opaque members' faces are drawn in one back-to-front order. `None` when /// the effect is only on the group's own fills or strokes or when its members can't be revolved. @@ -449,16 +479,19 @@ fn group_art(n: &Node, expand: bool) -> Option { let Some(item) = surface_item(&s.items) else { continue }; let (paint, opacity, blend) = paint_of(item); let opacity = opacity * s.opacity; - opaque &= opacity >= 1.0 && blend == Default::default(); - surfaces.push(surface(n, &s.path, b, options, fx, paint, opacity, blend, expand, &mut copies)?); + opaque &= opacity >= 1.0 && blend == Default::default() && s.blends.is_empty(); + // A member with non-normal blending keeps every face, as a blended object does. + let expand = expand && s.blends.is_empty(); + let (depths, art) = surface(n, &s.path, b, options, fx, paint, opacity, blend, expand, &mut copies)?; + surfaces.push((depths, art, s.blends.as_slice())); } if surfaces.is_empty() { return None; } - let plain = surfaces.iter().all(|(depths, s)| matches!(&s.kind, NodeKind::Group { children, clip: false } if children.len() == depths.len())); + let plain = surfaces.iter().all(|(depths, s, _)| matches!(&s.kind, NodeKind::Group { children, clip: false } if children.len() == depths.len())); let children = if opaque && plain { let mut faces = vec![]; - for (depths, s) in surfaces { + for (depths, s, _) in surfaces { if let NodeKind::Group { children, .. } = s.kind { faces.extend(depths.into_iter().zip(children)); } @@ -467,7 +500,7 @@ fn group_art(n: &Node, expand: bool) -> Option { faces.sort_by(|a, b| a.0.total_cmp(&b.0)); vec![Arc::new(Node::group(n.id, faces.into_iter().map(|(_, face)| face).collect()))] } else { - surfaces.into_iter().map(|(_, s)| Arc::new(s)).collect() + blended(n.id, surfaces.into_iter().map(|(_, s, blends)| (blends, s)).collect(), 0) }; let remaining = n.appearance.effects.iter().enumerate().filter(|(i, e)| *i != index && (!is_geometry(&e.id) || *i > index)).map(|(_, e)| e.clone()).collect(); @@ -717,6 +750,33 @@ mod tests { assert_eq!(revolve_art(&both), None, "past the limits the source draws"); } + #[test] + fn blending_modes_apply_once_to_the_member_or_group_that_has_them() { + let blend = |n: Arc, mode: BlendMode| { + let mut n = (*n).clone(); + n.blend = mode; + Arc::new(n) + }; + let inner = blend(member(4, 60.0), BlendMode::Multiply); + let screened = blend(Arc::new(Node::group(NodeId(5), vec![member(3, 40.0), inner])), BlendMode::Screen); + let group = group_with(json!({}), vec![blend(member(1, 0.0), BlendMode::Multiply), member(2, 20.0), screened]); + let art = revolve_art(&group).expect("revolved"); + let modes = |n: &Node| match &n.kind { + NodeKind::Group { children, .. } => children.iter().map(|c| c.blend).collect::>(), + _ => panic!("a group"), + }; + assert_eq!(modes(&art), [BlendMode::Multiply, BlendMode::Normal, BlendMode::Screen], "a surface or group for each member"); + let NodeKind::Group { children, .. } = &art.kind else { panic!("a group") }; + let screened = &children[2]; + assert_eq!(modes(screened), [BlendMode::Normal, BlendMode::Multiply], "the screened group's members, together, in their own modes"); + let NodeKind::Group { children: inner, .. } = &screened.kind else { panic!("a group") }; + assert_eq!(modes(&inner[1]).len(), 1, "the multiplied member's surface"); + let plain = group_with(json!({}), vec![member(1, 0.0)]); + assert_ne!(expand_revolve_art(&plain), revolve_art(&plain), "an opaque member's hidden faces are removed"); + let multiplied = group_with(json!({}), vec![blend(member(1, 0.0), BlendMode::Multiply)]); + assert_eq!(expand_revolve_art(&multiplied), revolve_art(&multiplied), "a blended member keeps every face"); + } + #[test] fn a_3d_effect_on_a_groups_fill_is_refused() { let mut group = Node::group(NodeId(9), vec![member(1, 0.0)]); diff --git a/crates/effects/src/solids.rs b/crates/effects/src/solids.rs index 568bf2be9..c4b55fd5e 100644 --- a/crates/effects/src/solids.rs +++ b/crates/effects/src/solids.rs @@ -225,12 +225,16 @@ pub(crate) fn object_profile(n: &Node, label: &str) -> Result<(PathData, FillRul } /// One shape a solid is built from: its outline and fill rule, the fills and strokes that paint -/// it, and the opacity of the objects it is in (1 for the object with the effect itself). +/// it, the opacity of the objects it is in (1 for the object with the effect itself) and their +/// blending modes other than Normal (none for the object itself). `blends` runs from the outermost +/// group in the effect's group down to the member itself, each with a key that the members of +/// one group share. Group Revolve draws the surfaces in those modes. pub(crate) struct Profile { pub(crate) path: PathData, pub(crate) rule: FillRule, pub(crate) items: Vec, pub(crate) opacity: f32, + pub(crate) blends: Vec<(usize, BlendMode)>, } /// Is `n` a group or layer (a 3D effect on it turns its members about one shared axis)? @@ -256,14 +260,25 @@ const MAX_MESH_TRIANGLES: usize = 2_000_000; /// reference app, they turn about one shared axis (the centre of their bounds together) rather /// than each about its own. An error names what the group holds that can't become a solid. pub(crate) fn members(n: &Node, label: &str) -> Result, String> { - fn walk(n: &Node, opacity: f32, label: &str, out: &mut Vec) -> Result<(), String> { + struct Walk<'a> { + label: &'a str, + out: Vec, + keys: usize, + } + fn walk(n: &Node, opacity: f32, blends: &[(usize, BlendMode)], w: &mut Walk) -> Result<(), String> { + let label = w.label; for c in n.children().into_iter().flatten() { if !c.visible || matches!(c.kind, NodeKind::Path { guide: true, .. }) { continue; } let opacity = opacity * c.opacity; + let mut blends = blends.to_vec(); + if c.blend != BlendMode::Normal { + w.keys += 1; + blends.push((w.keys, c.blend)); + } if is_container(c) { - walk(c, opacity, label, out)?; + walk(c, opacity, &blends, w)?; continue; } let Some((path, rule)) = profile(c) else { @@ -276,19 +291,19 @@ pub(crate) fn members(n: &Node, label: &str) -> Result, String> { }; return Err(format!("{label} on a group needs paths, compound paths and type: release or expand its {what} first.")); }; - if out.len() >= MAX_MEMBERS { + if w.out.len() >= MAX_MEMBERS { return Err(format!("{label} on a group takes at most {MAX_MEMBERS} objects")); } let path = geometry(&path, &c.appearance.effects, &GeomContext::of(c)); if path.bounds().is_some() { - out.push(Profile { path, rule, items: solid_paints(c), opacity }); + w.out.push(Profile { path, rule, items: solid_paints(c), opacity, blends }); } } Ok(()) } - let mut out = vec![]; - walk(n, 1.0, label, &mut out)?; - Ok(out) + let mut w = Walk { label, out: vec![], keys: 0 }; + walk(n, 1.0, &[], &mut w)?; + Ok(w.out) } /// Can 3D effect `id` (Revolve included, [`is_three_d`]) with parameters `p` draw `n`? Checked @@ -524,7 +539,7 @@ pub fn solid_art(n: &Node) -> Option { let remaining: Vec; if let Some((index, fx)) = solid(&n.appearance.effects) { let path = geometry(&path, n.appearance.effects.get(..index)?, &ctx); - children.extend(art_for(n, fx, &[Profile { path, rule, items: paints, opacity: 1.0 }])?); + children.extend(art_for(n, fx, &[Profile { path, rule, items: paints, opacity: 1.0, blends: vec![] }])?); // Later geometry effects act on the projected art as a whole. remaining = n .appearance @@ -540,7 +555,7 @@ pub fn solid_art(n: &Node) -> Option { for item in &paints { if let Some((index, fx)) = solid(item.effects()) { let p = geometry(&path, item.effects().get(..index)?, &ctx.item(item)); - for mut art in art_for(n, fx, &[Profile { path: p, rule, items: vec![item.clone()], opacity: 1.0 }])? { + for mut art in art_for(n, fx, &[Profile { path: p, rule, items: vec![item.clone()], opacity: 1.0, blends: vec![] }])? { Arc::make_mut(&mut art).appearance.effects = item.effects().iter().skip(index + 1).filter(|e| !is_three_d(&e.id)).cloned().collect(); children.push(art); @@ -703,14 +718,14 @@ fn meshes_within(n: &Node, total: &mut usize, limit: usize) -> Result