diff --git a/crates/effects/src/solids.rs b/crates/effects/src/solids.rs index 3c433d344..95ef265cc 100644 --- a/crates/effects/src/solids.rs +++ b/crates/effects/src/solids.rs @@ -275,23 +275,30 @@ impl Scope { } } +/// Does `scope` draw its members' faces other than plainly: does it have an opacity mask or a +/// blending mode other than Normal? +fn composites(scope: &Scope) -> bool { + scope.mask.is_some() || scope.blend != BlendMode::Normal +} + /// `surfaces`, each with the scopes of the member it was built from, in groups with the opacity -/// masks of those scopes, so each mask applies to the faces of the members it holds. The members' -/// opacity is already in their paint ([`Profile::opacity`]), so these groups are opaque. +/// masks and blending modes of those scopes, so each applies to the faces of the members it holds. +/// The members' opacity is already in their paint ([`Profile::opacity`]), so these groups are +/// opaque. fn masked(id: NodeId, surfaces: Vec<(&[Scope], Arc)>) -> Vec> { let masks: Vec<(Vec, Arc)> = surfaces .into_iter() .map(|(scopes, s)| { - let masks = scopes.iter().filter(|s| s.mask.is_some()).map(|s| Scope { opacity: 1.0, blend: BlendMode::Normal, ..s.clone() }).collect(); + let masks = scopes.iter().filter(|s| composites(s)).map(|s| Scope { opacity: 1.0, ..s.clone() }).collect(); (masks, s) }) .collect(); crate::revolve::scoped(id, masks.iter().map(|(scopes, s)| (scopes.as_slice(), s.clone())).collect(), 0) } -/// Does any of `shapes` sit in a scope with an opacity mask? +/// Does any of `shapes` sit in a scope with an opacity mask or a blending mode other than Normal? fn has_masks(shapes: &[Profile]) -> bool { - shapes.iter().any(|s| s.scopes.iter().any(|s| s.mask.is_some())) + shapes.iter().any(|s| s.scopes.iter().any(composites)) } /// One shape a solid is built from: its outline and fill rule, the fills and strokes that paint @@ -550,8 +557,9 @@ pub(crate) fn reshape_members(n: &Node, before: &[Effect], shapes: &mut [Profile /// each in its own paint. When every one is opaque with normal blending and no colored light makes /// a surface one composite or masks it, their faces are drawn in one back-to-front order (where /// depths tie, as for the caps turned toward the viewer, in the order of the appearance); -/// otherwise each solid is a surface of its own, in that order. A member's opacity mask, or that of -/// a group it is in, masks the surfaces of the members it holds ([`masked`]). +/// otherwise each solid is a surface of its own, in that order. A member's opacity mask and +/// blending mode, and those of a group it is in, apply to the surfaces of the members it holds +/// ([`masked`]). fn art_for(n: &Node, fx: &Effect, shapes: &[Profile]) -> Option>> { let b = union_bounds(shapes)?; let p = crate::merged_params(&fx.id, &fx.params); diff --git a/crates/effects/src/solids/tests_sources.rs b/crates/effects/src/solids/tests_sources.rs index 2973b7113..b2f6daf1c 100644 --- a/crates/effects/src/solids/tests_sources.rs +++ b/crates/effects/src/solids/tests_sources.rs @@ -523,6 +523,52 @@ fn an_opacity_mask_on_a_member_or_a_group_in_a_three_d_group_masks_its_solids() } } +/// The leaf faces of `art` that draw something, each with its visual bounds and whether it is drawn +/// inside a group with blending mode `mode`. +fn faces_in_mode(art: &Node, mode: BlendMode) -> Vec<(Rect, bool)> { + fn walk(n: &Node, inside: bool, mode: BlendMode, out: &mut Vec<(Rect, bool)>) { + let inside = inside || (n.children().is_some() && n.blend == mode); + match n.children() { + Some(children) => children.iter().for_each(|c| walk(c, inside, mode, out)), + None => out.extend(n.visual_bounds().map(|b| (b, inside))), + } + } + let mut out = Vec::new(); + walk(art, false, mode, &mut out); + out +} + +#[test] +fn a_blending_mode_on_a_member_or_a_group_in_a_three_d_group_applies_to_its_solids() { + let d = document(); + let mode = |mut n: Node, mode: BlendMode| { + n.blend = mode; + n + }; + let other = || Arc::new(rect(91, 600.0, 600.0, 650.0, 650.0, 0.0)); + let member = || rect(1, 100.0, 100.0, 150.0, 200.0, 2.0); + let cases = [ + ("a multiplied member", Node::group(NodeId(90), vec![Arc::new(mode(member(), BlendMode::Multiply)), other()])), + ( + "a member of a multiplied group", + Node::group(NodeId(90), vec![Arc::new(mode(Node::group(NodeId(80), vec![Arc::new(member())]), BlendMode::Multiply)), other()]), + ), + ]; + for (what, g) in cases { + for id in TYPES { + let g = with(g.clone(), id, json!({})); + // The member spans y 100 to 200 and the other member y 600 to 650, and every type keeps + // its faces near those heights (Revolve turns them around a vertical axis): a face whose + // center is above y 400 belongs to the member. + let (mine, others): (Vec<_>, Vec<_>) = + faces_in_mode(&art(&d, &g, id).expect("drawn"), BlendMode::Multiply).into_iter().partition(|(b, _)| b.center().y < 400.0); + assert!(!mine.is_empty() && !others.is_empty(), "{id}: both members draw faces ({} and {})", mine.len(), others.len()); + assert!(mine.iter().all(|(_, m)| *m), "{id}: every face of {what} is drawn in Multiply"); + assert!(others.iter().all(|(_, m)| !m), "{id}: no face of the other member is drawn in Multiply"); + } + } +} + #[test] fn resolved_objects_and_groups_in_a_three_d_group_keep_their_geometry_effects() { let mut d = document(); diff --git a/docs/three-d.md b/docs/three-d.md index e6dc279da..635c1285e 100644 --- a/docs/three-d.md +++ b/docs/three-d.md @@ -122,11 +122,12 @@ them all. The members stay editable: moving or editing one reshapes the solids. - Each member's own geometry effects shape it first, then those of each group inside the 3D group that holds it, applied to that group's members together; the 3D group's geometry effects before the 3D effect reshape the members, and those after it bend the result as a whole. The members' other effects, and the group's own fills and - strokes, are not drawn. A member's opacity applies to its solids; its blending mode does not. An opacity mask on + strokes, are not drawn. A member's opacity and blending mode apply to its solids, and the blending mode of a group + inside the 3D group applies to the solids of that group's members together. An opacity mask on a member masks its solids, and one on a group inside the 3D group masks the solids of that group's members together. The mask stays where it is on the page; it does not turn with the solids. A disabled mask is ignored. -- When a member, or a group inside the 3D group, has an opacity mask, each solid is drawn on its own, the backmost - member's first, as when a solid is transparent. +- When a member, or a group inside the 3D group, has an opacity mask or a blending mode other than Normal, each + solid is drawn on its own, the backmost member's first, as when a solid is transparent. - When every solid is opaque, all members' faces are drawn in one back-to-front order; otherwise each solid is drawn on its own, the backmost member's first. - Hidden members and guides are left out. A group holding a live object other than a blend (an envelope, a mesh, a