Merge pull request #6602 from dotCooCoo/fix/3d-group-member-blend-mode

3D effects: Extrude & Bevel, Inflate and Rotate on a group keep each member's blending mode
This commit is contained in:
BFlat
2026-11-11 23:20:20 +07:00
committed by GitHub
3 changed files with 65 additions and 10 deletions
+15 -7
View File
@@ -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<Node>)>) -> Vec<Arc<Node>> {
let masks: Vec<(Vec<Scope>, Arc<Node>)> = 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<Vec<Arc<Node>>> {
let b = union_bounds(shapes)?;
let p = crate::merged_params(&fx.id, &fx.params);
@@ -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();
+4 -3
View File
@@ -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