Intertwine: the object brought over leaves no hairline of the one below (#5337)

The area an Intertwine member is drawn over another in was exactly the
overlap face, so its edge lay on the edge of the member below. Both edges
were anti-aliased, and the member below showed through as a thin line along
them. The area now reaches 2 pt past the face into the parts where only the
raised member paints (where drawing it again changes nothing), so its edge
lies inside that member.
This commit is contained in:
Don Omondi
2026-11-06 07:23:35 +07:00
committed by bflatastic
parent c15383c510
commit f987c2bbb0
3 changed files with 69 additions and 9 deletions
+45 -8
View File
@@ -10,7 +10,10 @@
//!
//! An overlap area is a face of the planar arrangement of what the members paint (their fills and
//! strokes, type outlined; other objects by their bounds): the part where two or more of them
//! overlap, split wherever a member's edge crosses it.
//! overlap, split wherever a member's edge crosses it. The area a member is raised in is that
//! face grown a little ([`SEAM`]) into the parts where only that member paints ([`raised_area`]),
//! so its edge does not lie on the edge of the member it passes over: there, both edges would be
//! anti-aliased and the member below would show through as a hairline (#5337).
//!
//! Behaviour: <https://helpx.adobe.com/illustrator/using/intertwine-objects.html> (Make,
//! Release, clicking or drawing around an overlap, Bring to Front, Bring Forward, Send Backward
@@ -23,7 +26,7 @@ use serde_json::{Value, json};
use vectorcraft_doc::{Document, Node, NodeId, NodeKind};
use vectorcraft_geom::{FillRule, PathData, Point, SubPath};
use vectorcraft_pathops as po;
use vectorcraft_tools::intertwine::{Face, drawn_order, face_at, faces, faces_in, is_intertwine, nonempty};
use vectorcraft_tools::intertwine::{Face, drawn_order, face_at, faces_in, faces_of, is_intertwine, nonempty, painted_areas};
use super::edit::selected_roots;
use super::pathops::Beside;
@@ -302,13 +305,46 @@ fn tiers(order: &[usize]) -> Vec<(usize, usize)> {
.collect()
}
/// How far (pt) the area a member is raised in reaches past its overlap face, into the parts only
/// that member paints ([`raised_area`]).
const SEAM: f64 = 2.0;
/// The area member `i` is raised in for `face`: the face, and the parts within [`SEAM`] of it
/// where only `i` paints (`areas`: what each member paints, [`painted_areas`]). Drawing `i` again
/// there changes nothing, and the edge of the area then lies inside `i`, not on the edge of a
/// member it passes over (where the anti-aliased edges of both would let that member show
/// through as a hairline).
fn raised_area(face: &PathData, i: usize, areas: &[Option<PathData>]) -> PathData {
let Some(own) = areas.get(i).and_then(Option::as_ref) else { return face.clone() };
let ring = po::offset_path(face, SEAM, po::Join::Round, 4.0);
let Some(near) = ring.bounds() else { return face.clone() };
let mut paths = vec![(&ring, FillRule::NonZero), (own, FillRule::NonZero)];
paths.extend(
areas
.iter()
.enumerate()
.filter(|(k, _)| *k != i)
.filter_map(|(_, a)| a.as_ref())
.filter(|a| a.bounds().is_some_and(|b| b.overlaps(near)))
.map(|a| (a, FillRule::NonZero)),
);
let only = po::boolean_n(&paths, |inside| inside.first() == Some(&true) && inside.get(1) == Some(&true) && !inside.iter().skip(2).any(|b| *b));
if only.is_empty() {
return face.clone();
}
nonempty(po::boolean(face, FillRule::NonZero, &only, FillRule::NonZero, po::BoolOp::Union)).unwrap_or_else(|| face.clone())
}
/// Draw the members of `face` in `order` there (bottom first), leaving the other areas as they
/// are.
fn arrange_face(children: &mut [Arc<Node>], face: &Face, order: &[usize]) {
/// are (`areas`: what each member paints, [`painted_areas`]).
fn arrange_face(children: &mut [Arc<Node>], face: &Face, order: &[usize], areas: &[Option<PathData>]) {
for (i, tier) in tiers(order) {
if let Some(c) = children.get_mut(i) {
set_tier(Arc::make_mut(c), &face.path, tier);
let Some(c) = children.get_mut(i) else { continue };
// A member drawn in its own order there with no areas has nothing to change.
if tier == 0 && c.extras().intertwine.is_none() && c.extras().intertwine_tiers.is_empty() {
continue;
}
set_tier(Arc::make_mut(c), &raised_area(&face.path, i, areas), tier);
}
}
@@ -379,7 +415,8 @@ fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
Some(m) => Some(children.iter().position(|c| c.id == m).ok_or_else(|| bad(C, format!("object {} is not in this Intertwine group", m.0)))?),
None => None,
};
let all = faces(&children);
let painted = painted_areas(&children);
let all = faces_of(&painted);
let found = match (at, &lasso) {
(Some(p), _) => face_at(&all, p).into_iter().collect(),
(None, Some(l)) => faces_in(&all, l),
@@ -423,7 +460,7 @@ fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
let n = d.node_mut(group).ok_or(EngineError::NoNode(group))?;
let NodeKind::Intertwine { children } = &mut n.kind else { return Err(EngineError::NoNode(group)) };
for (face, new) in &plans {
arrange_face(children, face, new);
arrange_face(children, face, new, &painted);
}
Ok(())
})?;
+22
View File
@@ -82,6 +82,28 @@ fn clicking_an_overlap_brings_the_object_below_to_the_front_there_only() {
assert_eq!(hit(&s, 50.0, 70.0), Some(b));
}
/// #5337: where A is brought over B, the edges of B that cross A leave no blue hairline in A.
#[test]
fn the_object_brought_over_shows_no_seam_of_the_one_below() {
let mut s = session();
intertwined(&mut s);
arrange(&mut s, json!({"at": [50, 30]}));
let zoom = 8.0;
let r = vectorcraft_render::Renderer::new().render(&s.doc().unwrap().doc, 800, 800, Affine::scale(zoom), &Default::default());
let mut worst = 0;
for y in 160..360 {
for x in 320..480 {
let (px, py) = ((x as f64 + 0.5) / zoom, (y as f64 + 0.5) / zoom);
// Well inside ring A's 8 pt stroke (radius 21 to 29), A covers every pixel.
let d = ((px - 35.0).powi(2) + (py - 50.0).powi(2)).sqrt();
if (22.0..28.0).contains(&d) {
worst = worst.max(r.pixel(x, y)[2]);
}
}
}
assert!(worst < 8, "blue shows through ring A at the top crossing (blue {worst})");
}
#[test]
fn clicking_the_same_overlap_again_swaps_back() {
let mut s = session();
+2 -1
View File
@@ -495,7 +495,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
pointer, and a right-click offers Bring to Front, Bring Forward, Send Backward and Send to Back for it in that area
(`object.intertwine.arrange` with `order` front, forward, backward or back;
<https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-intertwined-objects.html>).
The group renders, saves and exports through its live expansion. Still missing: adding objects to the group by
The group renders, saves and exports through its live expansion; an object brought over another shows no hairline
of the one below along that object's edges (#5337). Still missing: adding objects to the group by
dragging them in the Layers panel is untested.
- **Done (from users' reports):** Object › Create Gradient Mesh on an embedded image turns it into a mesh over its
rectangle, with the image's transform kept; each point takes the average colour and opacity of the picture around it