mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 17:02:03 -06:00
Merge pull request #6478 from dotCooCoo/fix/3d-member-opacity-masks
3D: an opacity mask on a group member masks its solids
This commit is contained in:
@@ -463,22 +463,19 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
|
||||
/// The members' `surfaces`, or runs of their faces, in order, from the [`Scope`] at `level` of each
|
||||
/// one's `Profile::scopes` inward: consecutive surfaces that share a scope go in one group with its
|
||||
/// opacity and blending mode, so each applies once, to the faces it holds together.
|
||||
fn scoped(id: NodeId, surfaces: Vec<(&[Scope], Arc<Node>)>, level: usize) -> Vec<Arc<Node>> {
|
||||
fn close(id: NodeId, run: &mut Vec<(&[Scope], Arc<Node>)>, scope: Option<Scope>, level: usize, out: &mut Vec<Arc<Node>>) {
|
||||
/// opacity, blending mode and opacity mask, so each applies once, to the faces it holds together.
|
||||
pub(crate) fn scoped<'a>(id: NodeId, surfaces: Vec<(&'a [Scope], Arc<Node>)>, level: usize) -> Vec<Arc<Node>> {
|
||||
fn close(id: NodeId, run: &mut Vec<(&[Scope], Arc<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));
|
||||
out.push(Arc::new(scope.group(id, scoped(id, std::mem::take(run), level + 1))));
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
let mut run = vec![];
|
||||
let mut scope = None;
|
||||
let mut scope: Option<&'a 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) {
|
||||
let at = scopes.get(level);
|
||||
if at.map(|a| a.key) != scope.map(|s| s.key) {
|
||||
close(id, &mut run, scope, level, &mut out);
|
||||
scope = at;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
use vectorcraft_doc::color::{BlendMode, Paint};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, Node, NodeKind, StrokeLayer};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, Node, NodeId, NodeKind, OpacityMask, StrokeLayer};
|
||||
use vectorcraft_geom::{FillRule, PathData, Rect};
|
||||
use vectorcraft_three_d::{Bevel, BevelShape, Extrude, Facet, Inflate, Mesh, View};
|
||||
|
||||
@@ -238,20 +238,68 @@ 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)]
|
||||
/// A group in a 3D effect's group, or a member of it, whose opacity is below 100%, whose blending
|
||||
/// mode is not Normal or that has an opacity mask. The members of one group share its `key`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Scope {
|
||||
pub(crate) key: usize,
|
||||
pub(crate) opacity: f32,
|
||||
pub(crate) blend: BlendMode,
|
||||
/// The object's opacity mask, unless it has none or it is disabled. The mask art is in
|
||||
/// document coordinates, as the solids' faces are.
|
||||
pub(crate) mask: Option<Arc<OpacityMask>>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// The scope of `n` with key `key`, or `None` when `n` is opaque, blends normally and has no
|
||||
/// opacity mask that applies.
|
||||
fn of(n: &Node, key: usize) -> Option<Self> {
|
||||
let mask = n.extras().mask.as_deref().filter(|m| !m.disabled);
|
||||
(n.opacity < 1.0 || n.blend != BlendMode::Normal || mask.is_some()).then(|| Scope {
|
||||
key,
|
||||
opacity: n.opacity,
|
||||
blend: n.blend,
|
||||
mask: mask.map(|m| Arc::new(m.clone())),
|
||||
})
|
||||
}
|
||||
|
||||
/// A group of `children` with id `id` and this scope's opacity, blending mode and opacity mask.
|
||||
pub(crate) fn group(&self, id: NodeId, children: Vec<Arc<Node>>) -> Node {
|
||||
let mut group = Node::group(id, children);
|
||||
group.opacity = self.opacity;
|
||||
group.blend = self.blend;
|
||||
if let Some(mask) = &self.mask {
|
||||
group.extras_mut().mask = Some(Box::new((**mask).clone()));
|
||||
}
|
||||
group
|
||||
}
|
||||
}
|
||||
|
||||
/// `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.
|
||||
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();
|
||||
(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?
|
||||
fn has_masks(shapes: &[Profile]) -> bool {
|
||||
shapes.iter().any(|s| s.scopes.iter().any(|s| s.mask.is_some()))
|
||||
}
|
||||
|
||||
/// 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 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.
|
||||
/// effect itself). `scopes` lists those objects with transparency or an opacity mask 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; the other 3D
|
||||
/// effects apply each opacity mask that way.
|
||||
pub(crate) struct Profile {
|
||||
pub(crate) path: PathData,
|
||||
pub(crate) rule: FillRule,
|
||||
@@ -298,9 +346,9 @@ pub(crate) fn members(n: &Node, label: &str) -> Result<Vec<Profile>, String> {
|
||||
}
|
||||
let opacity = opacity * c.opacity;
|
||||
let mut scopes = scopes.to_vec();
|
||||
if c.blend != BlendMode::Normal || c.opacity < 1.0 {
|
||||
if let Some(scope) = Scope::of(c, w.keys + 1) {
|
||||
w.keys += 1;
|
||||
scopes.push(Scope { key: w.keys, opacity: c.opacity, blend: c.blend });
|
||||
scopes.push(scope);
|
||||
}
|
||||
if is_container(c) {
|
||||
let first = w.out.len();
|
||||
@@ -445,22 +493,26 @@ fn region(path: &PathData, rule: FillRule, item: &AppearanceItem) -> (PathData,
|
||||
}
|
||||
}
|
||||
|
||||
/// One part of [`solid_parts`]: a fill or stroke, its shape's opacity, the region it paints and its
|
||||
/// fill rule, and its shape's scopes.
|
||||
type Part<'a> = (&'a AppearanceItem, f32, PathData, FillRule, &'a [Scope]);
|
||||
|
||||
/// The parts Extrude & Bevel and Inflate build solids from: each shape's top fill and its strokes,
|
||||
/// a group's members from the back up, each with its shape's opacity and the region it paints.
|
||||
/// The solids share the bounds of all their parts (`fallback` when none has any): one center to
|
||||
/// turn, twist and taper about, and Inflate's grid reaches over the strokes' outlines.
|
||||
fn solid_parts(shapes: &[Profile], fallback: Rect) -> (Vec<(&AppearanceItem, f32, PathData, FillRule)>, Rect) {
|
||||
let parts: Vec<(&AppearanceItem, f32, PathData, FillRule)> = shapes
|
||||
/// a group's members from the back up, each with its shape's opacity, the region it paints and its
|
||||
/// shape's scopes. The solids share the bounds of all their parts (`fallback` when none has any):
|
||||
/// one center to turn, twist and taper about, and Inflate's grid reaches over the strokes' outlines.
|
||||
fn solid_parts(shapes: &[Profile], fallback: Rect) -> (Vec<Part<'_>>, Rect) {
|
||||
let parts: Vec<Part> = shapes
|
||||
.iter()
|
||||
.flat_map(|s| {
|
||||
let top_fill = s.items.iter().rposition(|i| matches!(i, AppearanceItem::Fill(_)));
|
||||
s.items.iter().enumerate().filter(move |(k, i)| Some(*k) == top_fill || matches!(i, AppearanceItem::Stroke(_))).map(|(_, i)| {
|
||||
let (area, rule) = region(&s.path, s.rule, i);
|
||||
(i, s.opacity, area, rule)
|
||||
(i, s.opacity, area, rule, s.scopes.as_slice())
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let bounds = parts.iter().filter_map(|(_, _, area, _)| area.bounds()).reduce(|a, c| a.union(c)).unwrap_or(fallback);
|
||||
let bounds = parts.iter().filter_map(|(_, _, area, _, _)| area.bounds()).reduce(|a, c| a.union(c)).unwrap_or(fallback);
|
||||
(parts, bounds)
|
||||
}
|
||||
|
||||
@@ -483,12 +535,14 @@ 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.
|
||||
/// 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`]).
|
||||
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);
|
||||
let classic = fx.id == CLASSIC_EXTRUDE;
|
||||
let colored = !classic && view_options(&p).has_colored_lights();
|
||||
let masks = has_masks(shapes);
|
||||
if fx.id == ROTATE {
|
||||
// Every part turns about the centre of the object's own bounds (a group's members about
|
||||
// the centre of theirs), so they stay together.
|
||||
@@ -498,24 +552,24 @@ fn art_for(n: &Node, fx: &Effect, shapes: &[Profile]) -> Option<Vec<Arc<Node>>>
|
||||
let (paint, opacity, blend) = paint_of(item, s.opacity);
|
||||
let (area, rule) = region(&s.path, s.rule, item);
|
||||
let f = facets(ROTATE, &area, rule, b, &p).ok()?;
|
||||
out.push(Arc::new(surface(n, f, &paint, opacity, blend, colored)));
|
||||
out.push((s.scopes.as_slice(), Arc::new(surface(n, f, &paint, opacity, blend, colored))));
|
||||
}
|
||||
}
|
||||
return Some(out);
|
||||
return Some(if masks { masked(n.id, out) } else { out.into_iter().map(|(_, s)| s).collect() });
|
||||
}
|
||||
let (parts, bounds) = solid_parts(shapes, b);
|
||||
let built = if fx.id == INFLATE {
|
||||
// One eye for every puff, as far away as the highest needs.
|
||||
let shapes: Vec<(&PathData, FillRule)> = parts.iter().map(|(_, _, area, rule)| (area, *rule)).collect();
|
||||
let shapes: Vec<(&PathData, FillRule)> = parts.iter().map(|(_, _, area, rule, _)| (area, *rule)).collect();
|
||||
vectorcraft_three_d::inflate_together(&shapes, bounds, inflate_options(&p), view_options(&p))
|
||||
} else {
|
||||
parts.iter().map(|(_, _, area, rule)| facets(&fx.id, area, *rule, bounds, &p)).collect()
|
||||
parts.iter().map(|(_, _, area, rule, _)| facets(&fx.id, area, *rule, bounds, &p)).collect()
|
||||
};
|
||||
let mut solids = vec![];
|
||||
for ((item, opacity, _, _), f) in parts.iter().zip(built) {
|
||||
for ((item, opacity, _, _, scopes), f) in parts.iter().zip(built) {
|
||||
let item: &AppearanceItem = item;
|
||||
match f {
|
||||
Ok(f) => solids.push((item, *opacity, f)),
|
||||
Ok(f) => solids.push((item, *opacity, *scopes, f)),
|
||||
// A stroke's outline past the limits is left out; the fill's solid still draws.
|
||||
Err(_) if matches!(item, AppearanceItem::Stroke(_)) => {}
|
||||
// A loaded file or a shape edited past the limits keeps drawing its source path
|
||||
@@ -527,30 +581,33 @@ fn art_for(n: &Node, fx: &Effect, shapes: &[Profile]) -> Option<Vec<Arc<Node>>>
|
||||
if solids.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let opaque = solids.iter().all(|(item, member, _)| {
|
||||
let opaque = solids.iter().all(|(item, member, _, _)| {
|
||||
let (_, opacity, blend) = paint_of(item, *member);
|
||||
opacity >= 1.0 && blend == BlendMode::Normal
|
||||
});
|
||||
let surfaces: Vec<(Vec<f64>, Node)> = solids
|
||||
let surfaces: Vec<(Vec<f64>, Node, &[Scope])> = solids
|
||||
.into_iter()
|
||||
.map(|(item, member, f)| {
|
||||
.map(|(item, member, scopes, f)| {
|
||||
let depths: Vec<f64> = f.iter().map(|f| f.depth).collect();
|
||||
let (paint, opacity, blend) = paint_of(item, member);
|
||||
let art = if classic { classic::surface(n, f, &paint, opacity, blend, &p) } else { surface(n, f, &paint, opacity, blend, colored) };
|
||||
(depths, art)
|
||||
(depths, art, scopes)
|
||||
})
|
||||
.collect();
|
||||
if masks {
|
||||
return Some(masked(n.id, surfaces.into_iter().map(|(_, s, scopes)| (scopes, Arc::new(s))).collect()));
|
||||
}
|
||||
// One order takes a plain group of faces from each solid, one face for each facet. With
|
||||
// colored lights a surface can instead be one clipped composite of all its facets, or carry an
|
||||
// opacity mask, and then it stays whole.
|
||||
let plain = surfaces.iter().all(|(depths, s)| {
|
||||
let plain = surfaces.iter().all(|(depths, s, _)| {
|
||||
s.extras().mask.is_none() && matches!(&s.kind, NodeKind::Group { children, clip: false } if children.len() == depths.len())
|
||||
});
|
||||
if !opaque || !plain {
|
||||
return Some(surfaces.into_iter().map(|(_, s)| Arc::new(s)).collect());
|
||||
return Some(surfaces.into_iter().map(|(_, s, _)| Arc::new(s)).collect());
|
||||
}
|
||||
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));
|
||||
}
|
||||
@@ -702,11 +759,11 @@ fn meshes_for(fx: &Effect, shapes: &[Profile], total: &mut usize, limit: usize)
|
||||
// Inflate lays out every puff at once (they share one eye); each Inflate and Extrude & Bevel
|
||||
// solid and its mesh are built as the loop reaches them.
|
||||
let mut inflated = (fx.id == INFLATE).then(|| {
|
||||
let shapes: Vec<(&PathData, FillRule)> = parts.iter().map(|(_, _, area, rule)| (area, *rule)).collect();
|
||||
let shapes: Vec<(&PathData, FillRule)> = parts.iter().map(|(_, _, area, rule, _)| (area, *rule)).collect();
|
||||
vectorcraft_three_d::inflate_meshes(&shapes, bounds, inflate_options(&p), view)
|
||||
});
|
||||
let extrude = extrude_options(&p);
|
||||
for (item, member, area, rule) in &parts {
|
||||
for (item, member, area, rule, _) in &parts {
|
||||
let item: &AppearanceItem = item;
|
||||
let mesh = match inflated.as_mut() {
|
||||
Some(built) => built.next().unwrap_or_else(|| Ok(Mesh::default())),
|
||||
|
||||
@@ -350,25 +350,22 @@ fn clip_area(first: &Node) -> (PathData, FillRule) {
|
||||
}
|
||||
|
||||
/// `pieces`, each with the [`Scope`]s of the groups it was in, back in groups of `n`'s id with
|
||||
/// those groups' opacity and blending mode: consecutive pieces sharing the scope at `level` go into
|
||||
/// one group, so its transparency applies to them together. `level` stays below the nesting the
|
||||
/// pieces came from ([`nested_past`]).
|
||||
/// those groups' opacity, blending mode and opacity mask: consecutive pieces sharing the scope at
|
||||
/// `level` go into one group, so its transparency applies to them together. `level` stays below the
|
||||
/// nesting the pieces came from ([`nested_past`]).
|
||||
fn nest(n: &Node, pieces: Vec<(Vec<Scope>, Node)>, level: usize) -> Vec<Arc<Node>> {
|
||||
let mut out = vec![];
|
||||
let mut run: Vec<(Vec<Scope>, Node)> = vec![];
|
||||
let mut key = None;
|
||||
let mut key: Option<Scope> = None;
|
||||
let close = |run: &mut Vec<(Vec<Scope>, Node)>, scope: Option<Scope>, out: &mut Vec<Arc<Node>>| {
|
||||
let Some(scope) = scope else { return };
|
||||
let mut g = Node::group(n.id, nest(n, std::mem::take(run), level + 1));
|
||||
g.opacity = scope.opacity;
|
||||
g.blend = scope.blend;
|
||||
out.push(Arc::new(g));
|
||||
out.push(Arc::new(scope.group(n.id, nest(n, std::mem::take(run), level + 1))));
|
||||
};
|
||||
for (scopes, piece) in pieces {
|
||||
let here = scopes.get(level).copied();
|
||||
if here.map(|s| s.key) != key.map(|s: Scope| s.key) {
|
||||
close(&mut run, key, &mut out);
|
||||
key = here;
|
||||
let here = scopes.get(level).cloned();
|
||||
if here.as_ref().map(|s| s.key) != key.as_ref().map(|s| s.key) {
|
||||
close(&mut run, key.take(), &mut out);
|
||||
key = here.clone();
|
||||
}
|
||||
match here {
|
||||
Some(_) => run.push((scopes, piece)),
|
||||
@@ -437,12 +434,15 @@ fn shape(n: &Node, path: PathData, rule: FillRule, items: Vec<AppearanceItem>) -
|
||||
piece
|
||||
}
|
||||
|
||||
/// A plain group of `children` with `n`'s id, visibility, opacity and blending mode.
|
||||
/// A plain group of `children` with `n`'s id, visibility, opacity, blending mode and opacity mask.
|
||||
fn wrap(n: &Node, children: Vec<Arc<Node>>) -> Node {
|
||||
let mut g = Node::group(n.id, children);
|
||||
g.name = n.name.clone();
|
||||
g.opacity = n.opacity;
|
||||
g.blend = n.blend;
|
||||
if let Some(mask) = &n.extras().mask {
|
||||
g.extras_mut().mask = Some(mask.clone());
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
|
||||
@@ -462,6 +462,67 @@ fn empty_sources_and_sources_inside_sources_build_or_refuse_cleanly() {
|
||||
assert!(close(bounds(&solid_art_in(Some(&d), &placed).unwrap()), bounds(&solid_art(&cut).unwrap()), 0.01));
|
||||
}
|
||||
|
||||
/// The id of the art of [`masked_by`]'s opacity masks.
|
||||
const MASK_ART: NodeId = NodeId(900);
|
||||
|
||||
/// `n` with an opacity mask of white art covering `r`.
|
||||
fn masked_by(mut n: Node, r: Rect) -> Node {
|
||||
let art = Node::path(MASK_ART, shapes::rectangle(r), Appearance::basic(Paint::solid(Color::WHITE), Paint::None, 0.0));
|
||||
n.extras_mut().mask = Some(Box::new(vectorcraft_doc::OpacityMask::new(art, true)));
|
||||
n
|
||||
}
|
||||
|
||||
/// The faces (paths) of `art`, and how many of them are inside an object [`masked_by`] masks with
|
||||
/// a mask that is not disabled.
|
||||
fn masked_faces(art: &Node) -> (usize, usize) {
|
||||
fn walk(n: &Node, masked: bool, out: &mut (usize, usize)) {
|
||||
let masked = masked || n.extras().mask.as_ref().is_some_and(|m| m.art.id == MASK_ART && !m.disabled);
|
||||
match n.children() {
|
||||
Some(children) => children.iter().for_each(|c| walk(c, masked, out)),
|
||||
None => {
|
||||
out.0 += 1;
|
||||
out.1 += usize::from(masked);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = (0, 0);
|
||||
walk(art, false, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_opacity_mask_on_a_member_or_a_group_in_a_three_d_group_masks_its_solids() {
|
||||
let d = document();
|
||||
let mask = Rect::new(0.0, 0.0, 400.0, 400.0);
|
||||
let mut members = vec![("a path", rect(1, 100.0, 100.0, 150.0, 200.0, 2.0))];
|
||||
members.extend(sources());
|
||||
for (what, member) in members {
|
||||
let other = || Arc::new(rect(91, 600.0, 600.0, 650.0, 650.0, 0.0));
|
||||
let in_group = Node::group(NodeId(80), vec![Arc::new(member.clone())]);
|
||||
let cases = [
|
||||
("masked", Node::group(NodeId(90), vec![Arc::new(masked_by(member.clone(), mask)), other()])),
|
||||
("in a masked group", Node::group(NodeId(90), vec![Arc::new(masked_by(in_group, mask)), other()])),
|
||||
];
|
||||
for (how, g) in cases {
|
||||
for id in TYPES {
|
||||
let g = with(g.clone(), id, json!({}));
|
||||
let (faces, masked) = masked_faces(&art(&d, &g, id).expect("drawn"));
|
||||
assert!(masked > 0, "{id}: the faces of {what} {how} are masked");
|
||||
assert!(masked < faces, "{id}: the other member's faces are not ({masked} of {faces})");
|
||||
}
|
||||
}
|
||||
// A disabled mask masks nothing.
|
||||
let mut off = masked_by(member.clone(), mask);
|
||||
if let Some(m) = off.extras_mut().mask.as_mut() {
|
||||
m.disabled = true;
|
||||
}
|
||||
for id in TYPES {
|
||||
let g = with(Node::group(NodeId(90), vec![Arc::new(off.clone()), other()]), id, json!({}));
|
||||
assert_eq!(masked_faces(&art(&d, &g, id).expect("drawn")).1, 0, "{id}: {what} with a disabled mask");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_objects_and_groups_in_a_three_d_group_keep_their_geometry_effects() {
|
||||
let mut d = document();
|
||||
|
||||
+2
-1
@@ -2611,7 +2611,8 @@ Every 3D type, `threeD.classicExtrude` and `threeD.revolve` included, also appli
|
||||
symbol instance, alone or in a group, and builds from the art it shows, the same way for the canvas, exports and
|
||||
`assets.add3D` ([three-d.md](three-d.md#clipping-groups-blends-and-symbol-instances)); like a group, it takes the
|
||||
effect on the object itself, not on a fill or stroke. An image, alone or in a group, is refused with "trace it first
|
||||
with Object › Image Trace › Make", and nothing changes.
|
||||
with Object › Image Trace › Make", and nothing changes. On a group, an opacity mask on a member, or on a group inside
|
||||
it, masks that member's solids ([three-d.md](three-d.md#groups)).
|
||||
|
||||
`threeD.extrude`: `depth` (points, 0..2000, default 50), `twist` (degrees, 0..360, 0), `taper` (1..100%, 100),
|
||||
`cap` (true solid, false hollow), `bevel` (`none`, `classic`, `rounded`, `cove`, `step`, `ogee`), `bevelWidth`
|
||||
|
||||
+4
-3
@@ -6,9 +6,10 @@ Select a path, compound path, type, clipping group, blend, symbol instance, or a
|
||||
On a group or layer, every visible member sweeps about one axis, as [the help page](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/create-3d-graphics/create-3d-objects.html)
|
||||
describes for a group. The following rules are VectorCraft's own: type stays editable and sweeps as its glyph outlines,
|
||||
the shared axis lies on the chosen edge of the members' bounds together, and each member keeps its own paint.
|
||||
The faces of all members draw in one back-to-front order. The opacity and blending mode of a member, or of a group
|
||||
inside the group, apply once to all its faces. Where a face of another member lies between its faces in that order
|
||||
and overlaps them on the page, its faces are split there, and the opacity and blending mode apply once to each part.
|
||||
The faces of all members draw in one back-to-front order. The opacity, blending mode and opacity mask of a member,
|
||||
or of a group inside the group, apply once to all its faces. Where a face of another member lies between its faces
|
||||
in that order and overlaps them on the page, its faces are split there, and the opacity, blending mode and opacity
|
||||
mask apply once to each part. The mask stays where it is on the page; it does not turn with the faces.
|
||||
Two faces overlap when the convex hulls of their outlines come within the 0.75 pt seam stroke of each other. After
|
||||
4,194,304 overlap tests on one group, each further face of such a member is drawn as a part of its own.
|
||||
The profile's horizontal distance from a vertical axis is its radius. **Axis side** chooses the left or right
|
||||
|
||||
+5
-1
@@ -122,7 +122,11 @@ 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.
|
||||
strokes, are not drawn. A member's opacity applies to its solids; its blending mode does not. 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 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
|
||||
|
||||
Reference in New Issue
Block a user