mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 12:21:54 +00:00
Revolve: a nested group's opacity applies once to its members together
The opacity of a group inside a revolved group was multiplied into each member's surface, so where members overlap they drew darker than the group does without the effect. A member or group with opacity below 100% or a blending mode other than Normal now holds its members' surfaces in one group with that opacity and mode.
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
use vectorcraft_doc::color::{BlendMode, Color, Paint};
|
||||
use vectorcraft_doc::color::{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};
|
||||
|
||||
use crate::solids::{Profile, is_container, members, object_profile, profile, reshape_members, solid_paints, union_bounds};
|
||||
use crate::solids::{Profile, Scope, is_container, members, object_profile, profile, reshape_members, solid_paints, union_bounds};
|
||||
use crate::util::{flag, num, text};
|
||||
use crate::{GeomContext, apply_geometry_with, is_geometry, merged_params};
|
||||
|
||||
@@ -431,33 +431,34 @@ fn art(n: &Node, expand: bool) -> Option<Node> {
|
||||
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<Arc<Node>> {
|
||||
fn close(id: NodeId, run: &mut Vec<(&[(usize, BlendMode)], Node)>, key: Option<(usize, BlendMode)>, level: usize, out: &mut Vec<Arc<Node>>) {
|
||||
if let Some((_, blend)) = key {
|
||||
let mut group = Node::group(id, blended(id, std::mem::take(run), level + 1));
|
||||
group.blend = blend;
|
||||
/// The members' `surfaces` in order, from the [`Scope`] at `level` of each one's `Profile::scopes`
|
||||
/// inward: the surfaces of consecutive members that share a scope go in one group with its opacity
|
||||
/// and blending mode, so each applies once, to the members it holds together.
|
||||
fn scoped(id: NodeId, surfaces: Vec<(&[Scope], Node)>, level: usize) -> Vec<Arc<Node>> {
|
||||
fn close(id: NodeId, run: &mut Vec<(&[Scope], Node)>, scope: Option<Scope>, level: usize, out: &mut Vec<Arc<Node>>) {
|
||||
if let Some(scope) = scope {
|
||||
let mut group = Node::group(id, scoped(id, std::mem::take(run), level + 1));
|
||||
group.opacity = scope.opacity;
|
||||
group.blend = scope.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;
|
||||
let mut scope = None;
|
||||
for (scopes, s) in surfaces {
|
||||
let at = scopes.get(level).copied();
|
||||
if at.map(|a| a.key) != scope.map(|s: Scope| s.key) {
|
||||
close(id, &mut run, scope, level, &mut out);
|
||||
scope = at;
|
||||
}
|
||||
if key.is_some() {
|
||||
run.push((blends, s));
|
||||
if scope.is_some() {
|
||||
run.push((scopes, s));
|
||||
} else {
|
||||
out.push(Arc::new(s));
|
||||
}
|
||||
}
|
||||
close(id, &mut run, key, level, &mut out);
|
||||
close(id, &mut run, scope, level, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
@@ -477,13 +478,13 @@ fn group_art(n: &Node, expand: bool) -> Option<Node> {
|
||||
let mut opaque = true;
|
||||
for s in &shapes {
|
||||
let Some(item) = surface_item(&s.items) else { continue };
|
||||
// The opacity and blending of the member and the groups it is in apply through its scopes.
|
||||
let (paint, opacity, blend) = paint_of(item);
|
||||
let opacity = opacity * s.opacity;
|
||||
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();
|
||||
opaque &= opacity >= 1.0 && blend == Default::default() && s.scopes.is_empty();
|
||||
// A member with transparency of its own keeps every face, as a transparent object does.
|
||||
let expand = expand && s.scopes.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()));
|
||||
surfaces.push((depths, art, s.scopes.as_slice()));
|
||||
}
|
||||
if surfaces.is_empty() {
|
||||
return None;
|
||||
@@ -500,7 +501,7 @@ fn group_art(n: &Node, expand: bool) -> Option<Node> {
|
||||
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 {
|
||||
blended(n.id, surfaces.into_iter().map(|(_, s, blends)| (blends, s)).collect(), 0)
|
||||
scoped(n.id, surfaces.into_iter().map(|(_, s, scopes)| (scopes, 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();
|
||||
@@ -752,6 +753,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn blending_modes_apply_once_to_the_member_or_group_that_has_them() {
|
||||
use vectorcraft_doc::color::BlendMode;
|
||||
let blend = |n: Arc<Node>, mode: BlendMode| {
|
||||
let mut n = (*n).clone();
|
||||
n.blend = mode;
|
||||
@@ -775,6 +777,14 @@ mod tests {
|
||||
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");
|
||||
// A nested group's opacity applies once, to its members' surfaces together.
|
||||
let mut faded = Node::group(NodeId(6), vec![member(3, 40.0), member(4, 45.0)]);
|
||||
faded.opacity = 0.5;
|
||||
let art = revolve_art(&group_with(json!({}), vec![member(1, 0.0), Arc::new(faded)])).expect("revolved");
|
||||
let NodeKind::Group { children, .. } = &art.kind else { panic!("a group") };
|
||||
let opacities = |n: &Node| n.children().unwrap().iter().map(|c| c.opacity).collect::<Vec<_>>();
|
||||
assert_eq!(children.iter().map(|c| c.opacity).collect::<Vec<_>>(), [1.0, 0.5]);
|
||||
assert_eq!(opacities(&children[1]), [1.0, 1.0], "the faded group's members, together");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -224,17 +224,26 @@ pub(crate) fn object_profile(n: &Node, label: &str) -> Result<(PathData, FillRul
|
||||
})
|
||||
}
|
||||
|
||||
/// A group in a 3D effect's group, or a member of it, whose opacity is below 100% or whose blending
|
||||
/// mode is not Normal. The members of one group share its `key`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct Scope {
|
||||
pub(crate) key: usize,
|
||||
pub(crate) opacity: f32,
|
||||
pub(crate) blend: BlendMode,
|
||||
}
|
||||
|
||||
/// One shape a solid is built from: its outline and fill rule, the fills and strokes that paint
|
||||
/// 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.
|
||||
/// it, and the opacity of the objects it is in multiplied together (1 for the object with the
|
||||
/// effect itself). `scopes` lists those objects with transparency of their own ([`Scope`]), from
|
||||
/// the outermost group down to the member itself (none for the object itself). Group Revolve
|
||||
/// applies each once, to the surfaces of the members it holds together.
|
||||
pub(crate) struct Profile {
|
||||
pub(crate) path: PathData,
|
||||
pub(crate) rule: FillRule,
|
||||
pub(crate) items: Vec<AppearanceItem>,
|
||||
pub(crate) opacity: f32,
|
||||
pub(crate) blends: Vec<(usize, BlendMode)>,
|
||||
pub(crate) scopes: Vec<Scope>,
|
||||
}
|
||||
|
||||
/// Is `n` a group or layer (a 3D effect on it turns its members about one shared axis)?
|
||||
@@ -265,20 +274,20 @@ pub(crate) fn members(n: &Node, label: &str) -> Result<Vec<Profile>, String> {
|
||||
out: Vec<Profile>,
|
||||
keys: usize,
|
||||
}
|
||||
fn walk(n: &Node, opacity: f32, blends: &[(usize, BlendMode)], w: &mut Walk) -> Result<(), String> {
|
||||
fn walk(n: &Node, opacity: f32, scopes: &[Scope], 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 {
|
||||
let mut scopes = scopes.to_vec();
|
||||
if c.blend != BlendMode::Normal || c.opacity < 1.0 {
|
||||
w.keys += 1;
|
||||
blends.push((w.keys, c.blend));
|
||||
scopes.push(Scope { key: w.keys, opacity: c.opacity, blend: c.blend });
|
||||
}
|
||||
if is_container(c) {
|
||||
walk(c, opacity, &blends, w)?;
|
||||
walk(c, opacity, &scopes, w)?;
|
||||
continue;
|
||||
}
|
||||
let Some((path, rule)) = profile(c) else {
|
||||
@@ -296,7 +305,7 @@ pub(crate) fn members(n: &Node, label: &str) -> Result<Vec<Profile>, String> {
|
||||
}
|
||||
let path = geometry(&path, &c.appearance.effects, &GeomContext::of(c));
|
||||
if path.bounds().is_some() {
|
||||
w.out.push(Profile { path, rule, items: solid_paints(c), opacity, blends });
|
||||
w.out.push(Profile { path, rule, items: solid_paints(c), opacity, scopes });
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -539,7 +548,7 @@ pub fn solid_art(n: &Node) -> Option<Node> {
|
||||
let remaining: Vec<Effect>;
|
||||
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, blends: vec![] }])?);
|
||||
children.extend(art_for(n, fx, &[Profile { path, rule, items: paints, opacity: 1.0, scopes: vec![] }])?);
|
||||
// Later geometry effects act on the projected art as a whole.
|
||||
remaining = n
|
||||
.appearance
|
||||
@@ -555,7 +564,7 @@ pub fn solid_art(n: &Node) -> Option<Node> {
|
||||
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, blends: vec![] }])? {
|
||||
for mut art in art_for(n, fx, &[Profile { path: p, rule, items: vec![item.clone()], opacity: 1.0, scopes: 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);
|
||||
@@ -718,14 +727,14 @@ fn meshes_within(n: &Node, total: &mut usize, limit: usize) -> Result<Vec<SolidM
|
||||
let paints = solid_paints(n);
|
||||
if let Some((index, fx)) = solid(&n.appearance.effects) {
|
||||
let path = geometry(&path, n.appearance.effects.get(..index).unwrap_or_default(), &ctx);
|
||||
meshes_for(fx, &[Profile { path, rule, items: paints, opacity: 1.0, blends: vec![] }], total, limit)?
|
||||
meshes_for(fx, &[Profile { path, rule, items: paints, opacity: 1.0, scopes: vec![] }], total, limit)?
|
||||
} else {
|
||||
let path = geometry(&path, &n.appearance.effects, &ctx);
|
||||
let mut out = vec![];
|
||||
for item in &paints {
|
||||
if let Some((index, fx)) = solid(item.effects()) {
|
||||
let p = geometry(&path, item.effects().get(..index).unwrap_or_default(), &ctx.item(item));
|
||||
out.extend(meshes_for(fx, &[Profile { path: p, rule, items: vec![item.clone()], opacity: 1.0, blends: vec![] }], total, limit)?);
|
||||
out.extend(meshes_for(fx, &[Profile { path: p, rule, items: vec![item.clone()], opacity: 1.0, scopes: vec![] }], total, limit)?);
|
||||
}
|
||||
}
|
||||
out
|
||||
|
||||
@@ -51,7 +51,7 @@ fn the_meshes_of_one_object_share_one_triangle_budget() {
|
||||
assert!(keep_mesh(&mut out, &mut total, meshes[1].clone(), first).is_err());
|
||||
assert_eq!(out.len(), 1);
|
||||
// Building past the budget stops with an error instead of holding every mesh.
|
||||
let shapes = [Profile { path: profile(&n).unwrap().0, rule: FillRule::NonZero, items: solid_paints(&n), opacity: 1.0, blends: vec![] }];
|
||||
let shapes = [Profile { path: profile(&n).unwrap().0, rule: FillRule::NonZero, items: solid_paints(&n), opacity: 1.0, scopes: vec![] }];
|
||||
let Some(effect) = n.appearance.effects.first() else { panic!("no effect") };
|
||||
let mut total = 0;
|
||||
assert!(meshes_for(effect, &shapes, &mut total, MAX_MESH_TRIANGLES).is_ok());
|
||||
|
||||
Reference in New Issue
Block a user