Merge pull request #7061 from dotCooCoo/fix/dcs-transparency-clip

Place: DCS transparency interactions look only at what clipping paths show
This commit is contained in:
BFlat
2026-11-13 16:04:49 +07:00
committed by GitHub
3 changed files with 106 additions and 8 deletions
+22 -7
View File
@@ -2009,15 +2009,21 @@ fn status_rank(status: &str) -> usize {
/// holds them) or overlap an object that is, as Show DCS Transparency Interactions marks them.
fn dcs_transparency(d: &Document) -> BTreeSet<NodeId> {
/// Each visible DCS image's id, bounds and whether it is transparent, and the bounds of each
/// other visible object with its own transparency.
fn walk(d: &Document, n: &Node, held: bool, images: &mut Vec<(NodeId, Rect, bool)>, see_through: &mut Vec<Rect>) {
/// other visible object with its own transparency; bounds are cut to `clip`, the bounds of the
/// clipping paths of the clip groups and layers holding the object.
fn walk(d: &Document, n: &Node, held: bool, clip: Option<Rect>, images: &mut Vec<(NodeId, Rect, bool)>, see_through: &mut Vec<Rect>) {
if !n.visible {
return;
}
let own = n.shows_own_transparency();
// The part of the object that shows through `clip`; none when it is clipped away.
let shown = n.geometric_bounds().and_then(|b| match clip {
Some(c) => Some(c.intersect(b)).filter(|i| i.area() > 0.0),
None => Some(b),
});
match &n.kind {
NodeKind::Image(im) if d.images.get(&im.key).is_some_and(ImageBlob::is_separated) => {
if let Some(b) = n.geometric_bounds() {
if let Some(b) = shown {
images.push((n.id, b, held || own));
// A transparent DCS image is an object with its own transparency for the
// others it overlaps.
@@ -2026,16 +2032,25 @@ fn dcs_transparency(d: &Document) -> BTreeSet<NodeId> {
}
}
}
_ if own => see_through.extend(n.geometric_bounds()),
_ if own => see_through.extend(shown),
_ => {}
}
for c in n.children().into_iter().flatten() {
walk(d, c, held || own, images, see_through);
let Some(children) = n.children() else { return };
// A clip group's or clipping layer's first child clips the others.
let clip = match &n.kind {
NodeKind::Group { clip: true, .. } | NodeKind::Layer { clip: true, .. } => match children.first().and_then(|c| c.geometric_bounds()) {
Some(c) => Some(clip.map_or(c, |o| o.intersect(c))),
None => clip,
},
_ => clip,
};
for c in children {
walk(d, c, held || own, clip, images, see_through);
}
}
let (mut images, mut see_through) = (vec![], vec![]);
for l in &d.layers {
walk(d, l, false, &mut images, &mut see_through);
walk(d, l, false, None, &mut images, &mut see_through);
}
images
.into_iter()
+82
View File
@@ -773,6 +773,88 @@ fn dcs_transparency_marks_transparent_and_overlapped_dcs_images() {
assert!(error(&mut s, "links.list", json!({ "dcsTransparency": "yes" })).contains("dcsTransparency"), "a flag that isn't true or false");
}
#[test]
fn dcs_transparency_looks_only_at_the_part_of_an_image_its_clipping_path_shows() {
let dir = temp_dir("transparency-clipped");
let path = write(&dir, "pic.eps", &two_plates());
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let group = NodeId(r["ids"][0].as_u64().unwrap());
let image = image_of(&s, &r["ids"][0]).id;
let (clip, all) = {
let doc = &s.doc().unwrap().doc;
let NodeKind::Group { children, clip: true } = &doc.node(group).unwrap().kind else { panic!("a clipping group") };
(children[0].geometric_bounds().unwrap(), doc.node(image).unwrap().geometric_bounds().unwrap())
};
assert!(all.x1 - clip.x1 > 2.0, "the file's clipping path shows the left half: {clip:?} of {all:?}");
// A half-transparent square over the part of the image the clipping path hides.
let x = (clip.x1 + all.x1) / 2.0 - 1.0;
let over = s.execute("shape.rectangle", &json!({"x": x, "y": all.y0, "width": 2, "height": 2})).unwrap()["id"].as_u64().unwrap();
s.execute("transparency.set", &json!({"ids": [over], "opacity": 50})).unwrap();
let marked = |s: &mut Session| s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap()["links"][0]["dcsTransparency"] == true;
assert!(!marked(&mut s), "the square is over the hidden part only");
s.execute("transparency.set", &json!({"ids": [over], "opacity": 100})).unwrap();
let shown = s.execute("shape.rectangle", &json!({"x": clip.x0, "y": all.y0, "width": 2, "height": 2})).unwrap()["id"].as_u64().unwrap();
s.execute("transparency.set", &json!({"ids": [shown], "opacity": 50})).unwrap();
assert!(marked(&mut s), "a square over the part that shows");
}
#[test]
fn dcs_transparency_skips_a_transparent_object_its_own_clipping_paths_hide() {
let dir = temp_dir("transparency-clipped-over");
let path = write(&dir, "pic.eps", &two_plates());
// A session with the file placed, and the top-left corner of the image, inside the part its
// clipping path shows.
let placed = || {
let mut s = session();
let r = s.execute("file.place", &json!({ "path": path, "at": [100, 100] })).unwrap();
let b = image_of(&s, &r["ids"][0]).geometric_bounds().unwrap();
(s, b.x0, b.y0)
};
let rect = |s: &mut Session, x: f64, y: f64, width: f64| {
s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": width, "height": 2})).unwrap()["id"].as_u64().unwrap()
};
let make = |s: &mut Session, ids: [u64; 2]| {
s.execute("select.set", &json!({ "ids": ids })).unwrap();
s.execute("object.clippingMask.make", &json!({})).unwrap()["id"].as_u64().unwrap()
};
let marked = |s: &mut Session| s.execute("links.list", &json!({ "dcsTransparency": true })).unwrap()["links"][0]["dcsTransparency"] == true;
// A half-transparent square over the part of the image that shows.
let square = |s: &mut Session, x: f64, y: f64| {
let id = rect(s, x, y, 2.0);
s.execute("transparency.set", &json!({"ids": [id], "opacity": 50})).unwrap();
assert!(marked(s), "the square over the image");
id
};
// Its own clip group's clipping path is beside it.
let (mut s, x, y) = placed();
let over = square(&mut s, x, y);
let beside = rect(&mut s, x + 5.0, y, 2.0);
make(&mut s, [over, beside]);
assert!(!marked(&mut s), "a clip group that hides the square");
// Its layer's clipping path is beside it.
let (mut s, x, y) = placed();
s.execute("layer.new", &json!({})).unwrap();
square(&mut s, x, y);
rect(&mut s, x + 5.0, y, 2.0);
s.execute("select.none", &json!({})).unwrap();
assert!(s.execute("layer.clippingMask.toggle", &json!({})).unwrap()["clip"].as_bool().unwrap());
assert!(!marked(&mut s), "a clipping layer that hides the square");
// Each of two nested clipping paths shows part of the inner group, and only the inner one
// covers the square.
let (mut s, x, y) = placed();
let over = square(&mut s, x, y);
let inner = rect(&mut s, x, y, 40.0);
let group = make(&mut s, [over, inner]);
assert!(marked(&mut s), "the inner clipping path shows the square");
let outer = rect(&mut s, x + 10.0, y, 20.0);
make(&mut s, [group, outer]);
assert!(!marked(&mut s), "the two clipping paths together hide the square");
}
#[test]
fn dcs_transparency_counts_a_transparent_dcs_image_over_another() {
let dir = temp_dir("transparency-two");
+2 -1
View File
@@ -6817,7 +6817,8 @@ DCS Transparency Interactions, off by default) adds `dcsTransparency: true` to t
and the panel shows a warning mark on them. VectorCraft's rule for it: the image, or a group or layer holding it, has
an opacity below 100%, a blend mode other than Normal, an opacity mask or an effect, or its bounds overlap the
bounds of another visible object that has one of those or a gradient with a transparent stop (in a fill, a stroke or
a character's fill or stroke); a DCS image with transparency of its own counts as such an object. The check looks at
a character's fill or stroke); a DCS image with transparency of its own counts as such an object. Bounds are cut to the
bounds of the clipping paths of the clip groups and clipping layers holding an object. The check looks at
every object in the layers each time the list is read. A `dcsTransparency` that is not true or false is an error.
`links.goTo {id}` selects an image; `links.embed {ids?}` keeps linked files' pixels in the document (an image whose
file is missing stays linked: relink it first); `links.unembed {id, path}` writes an embedded image to a file