mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 20:23:35 +00:00
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:
@@ -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(())
|
||||
})?;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user