mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Intertwine: a right-click where three or more objects overlap arranges the nearest one
Squashed from #4862 onto main.
This commit is contained in:
+8
-13
@@ -538,20 +538,15 @@ fn hit_children(
|
||||
}
|
||||
|
||||
/// The order a click at `p` tries the children of `parent` in, front first: their stacking order
|
||||
/// from the top, except that in an Intertwine group the members drawn over the others at `p`
|
||||
/// come first (from the top among them too).
|
||||
/// from the top, except that in an Intertwine group the members are tried in the order they are
|
||||
/// drawn at `p`, from the top ([`crate::intertwine::tier_at`]).
|
||||
fn hit_order<'a>(parent: &Node, children: &'a [std::sync::Arc<Node>], p: Point) -> impl Iterator<Item = usize> + 'a {
|
||||
let over: Vec<usize> = if matches!(parent.kind, NodeKind::Intertwine { .. }) {
|
||||
let contains = |a: &vectorcraft_geom::PathData| fill_contains(&a.to_bezpath(), vectorcraft_geom::FillRule::NonZero, p);
|
||||
(0..children.len()).rev().filter(|i| children.get(*i).and_then(|c| crate::intertwine::over_area(c)).is_some_and(contains)).collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let rest = (0..children.len()).rev().filter({
|
||||
let over = over.clone();
|
||||
move |i| !over.contains(i)
|
||||
});
|
||||
over.into_iter().chain(rest)
|
||||
let mut order: Vec<usize> = (0..children.len()).rev().collect();
|
||||
if matches!(parent.kind, NodeKind::Intertwine { .. }) {
|
||||
let tier = |i: usize| children.get(i).map_or(0, |c| crate::intertwine::tier_at(c, p));
|
||||
order.sort_by_cached_key(|i| std::cmp::Reverse((tier(*i), *i)));
|
||||
}
|
||||
order.into_iter()
|
||||
}
|
||||
|
||||
/// A click on the strokes of a path or compound path `n` along `bp`: what any visible stroke
|
||||
|
||||
@@ -1,36 +1,51 @@
|
||||
//! Object › Intertwine: objects that pass over and under each other.
|
||||
//!
|
||||
//! An Intertwine group ([`NodeKind::Intertwine`]) keeps its members as they are, in their own
|
||||
//! stacking order. A member can be drawn over the other members in part of the group: that area
|
||||
//! is the member's [`crate::node::NodeExtras::intertwine`], in document coordinates. The group
|
||||
//! draws (and exports, and is hit) as its live expansion ([`expand`]): the members, then, for
|
||||
//! each member with an area, a copy of it clipped to that area. Where several members are drawn
|
||||
//! over the others in one place, the upper one in the stacking order ends on top.
|
||||
//! stacking order. A member can be drawn over other members in part of the group, at a tier: the
|
||||
//! area of tier 1 is the member's [`crate::node::NodeExtras::intertwine`], and the areas of tiers 2
|
||||
//! and up are its [`crate::node::NodeExtras::intertwine_tiers`], in document coordinates. In each
|
||||
//! place the members are drawn by their tier there (0 outside their areas), and members of one
|
||||
//! tier in their stacking order, so any order of the members can be set for each overlap area.
|
||||
//! The group draws (and exports, and is hit) as its live expansion ([`expand`]): the members,
|
||||
//! then, tier by tier, a copy of each member with an area of that tier clipped to it.
|
||||
//!
|
||||
//! The members stay editable, and Release gives them back as they were (the areas let go).
|
||||
//! Behaviour: <https://helpx.adobe.com/illustrator/using/intertwine-objects.html>.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_geom::{FillRule, PathData};
|
||||
use vectorcraft_geom::hit::fill_contains;
|
||||
use vectorcraft_geom::{FillRule, PathData, Point};
|
||||
|
||||
use crate::{Node, NodeId, NodeKind};
|
||||
|
||||
/// The area where `n` is drawn over the other members of its Intertwine group (empty: nowhere).
|
||||
pub fn over_area(n: &Node) -> Option<&PathData> {
|
||||
n.extras().intertwine.as_deref().filter(|a| !a.is_empty())
|
||||
/// The areas where `n` is drawn over other members of its Intertwine group, each with its tier
|
||||
/// (1 and up); empty ones are left out.
|
||||
pub fn tier_areas(n: &Node) -> impl Iterator<Item = (usize, &PathData)> {
|
||||
let e = n.extras();
|
||||
e.intertwine
|
||||
.as_deref()
|
||||
.map(|a| (1, a))
|
||||
.into_iter()
|
||||
.chain(e.intertwine_tiers.iter().enumerate().map(|(k, a)| (k + 2, a)))
|
||||
.filter(|(_, a)| !a.is_empty())
|
||||
}
|
||||
|
||||
/// The members of an Intertwine group evaluated as plain art: the members (ids kept), then each
|
||||
/// member with an area drawn over the others, as a clip group of that area around a copy of it
|
||||
/// (ids 0: generated art).
|
||||
/// The tier `n` is drawn at at `p`: the highest tier whose area holds `p` (0: none does).
|
||||
pub fn tier_at(n: &Node, p: Point) -> usize {
|
||||
tier_areas(n).filter(|(_, a)| fill_contains(&a.to_bezpath(), FillRule::NonZero, p)).map(|(t, _)| t).max().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The members of an Intertwine group evaluated as plain art: the members (ids kept), then, for
|
||||
/// each tier from 1 up, each member with an area of that tier, as a clip group of that area
|
||||
/// around a copy of it (ids 0: generated art).
|
||||
pub fn expand(children: &[Arc<Node>]) -> Vec<Node> {
|
||||
let mut out: Vec<Node> = children.iter().map(|c| plain(c)).collect();
|
||||
for c in children {
|
||||
let Some(area) = over_area(c) else { continue };
|
||||
if !c.visible {
|
||||
continue;
|
||||
}
|
||||
let mut over: Vec<(usize, usize, &PathData)> =
|
||||
children.iter().enumerate().filter(|(_, c)| c.visible).flat_map(|(i, c)| tier_areas(c).map(move |(t, a)| (t, i, a))).collect();
|
||||
over.sort_by_key(|(t, i, _)| (*t, *i));
|
||||
for (_, i, area) in over {
|
||||
let Some(c) = children.get(i) else { continue };
|
||||
let mut clip = Node::new(NodeId(0), NodeKind::Path { path: area.clone(), rule: FillRule::NonZero, live: None, clipping: true, guide: false });
|
||||
clip.appearance = crate::Appearance::default().into();
|
||||
let mut copy = plain(c);
|
||||
@@ -40,11 +55,13 @@ pub fn expand(children: &[Arc<Node>]) -> Vec<Node> {
|
||||
out
|
||||
}
|
||||
|
||||
/// `n` without its Intertwine area (the expansion is plain art).
|
||||
/// `n` without its Intertwine areas (the expansion is plain art).
|
||||
fn plain(n: &Node) -> Node {
|
||||
let mut n = n.clone();
|
||||
if n.extras().intertwine.is_some() {
|
||||
n.extras_mut().intertwine = None;
|
||||
if n.extras().intertwine.is_some() || !n.extras().intertwine_tiers.is_empty() {
|
||||
let mut e = n.extras_mut();
|
||||
e.intertwine = None;
|
||||
e.intertwine_tiers = Vec::new();
|
||||
}
|
||||
n
|
||||
}
|
||||
@@ -88,6 +105,28 @@ mod tests {
|
||||
assert_eq!((out[2].id, children[1].id), (NodeId(0), NodeId(0)), "generated art has no ids of its own");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_are_drawn_tier_by_tier() {
|
||||
let over = || shapes::rectangle(Rect::new(5.0, 0.0, 10.0, 10.0));
|
||||
// The lower member at tier 2, the upper one at tier 1: the lower one ends on top.
|
||||
let mut a = (*square(1, 0.0)).clone();
|
||||
a.extras_mut().intertwine_tiers = vec![over()];
|
||||
let mut b = (*square(2, 5.0)).clone();
|
||||
b.extras_mut().intertwine = Some(Box::new(over()));
|
||||
let kids = vec![Arc::new(a), Arc::new(b)];
|
||||
assert_eq!((tier_at(&kids[0], Point::new(7.0, 5.0)), tier_at(&kids[1], Point::new(7.0, 5.0))), (2, 1));
|
||||
assert_eq!(tier_at(&kids[0], Point::new(2.0, 5.0)), 0);
|
||||
let out = expand(&kids);
|
||||
assert_eq!(out.len(), 4, "both members, then the copy of tier 1, then the one of tier 2");
|
||||
assert!(out.iter().all(|n| n.extras().intertwine.is_none() && n.extras().intertwine_tiers.is_empty()), "the expansion is plain art");
|
||||
let copied = |n: &Node| match &n.kind {
|
||||
NodeKind::Group { children, clip: true } => children[1].path_data().cloned(),
|
||||
k => panic!("a clip group: {k:?}"),
|
||||
};
|
||||
assert_eq!(copied(&out[2]).as_ref(), kids[1].path_data());
|
||||
assert_eq!(copied(&out[3]).as_ref(), kids[0].path_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hidden_member_is_not_drawn_over_the_others() {
|
||||
let mut a = (*square(1, 0.0)).clone();
|
||||
|
||||
@@ -253,6 +253,9 @@ fn node_bytes(n: &Node, other: Option<&Node>) -> usize {
|
||||
b += boxed(&e.dimension) + boxed(&e.live_paint_gaps) + boxed(&e.colorize);
|
||||
b += e.trace.as_ref().map_or(0, |t| ALLOC + size_of::<Value>() + json_bytes(t));
|
||||
b += e.intertwine.as_deref().map_or(0, |a| ALLOC + size_of::<PathData>() + path_bytes(a));
|
||||
if e.intertwine_tiers.capacity() > 0 {
|
||||
b += ALLOC + e.intertwine_tiers.capacity() * size_of::<PathData>() + e.intertwine_tiers.iter().map(path_bytes).sum::<usize>();
|
||||
}
|
||||
b += e.objects_on_path.as_deref().map_or(0, |o| {
|
||||
ALLOC + size_of::<crate::objects_on_path::ObjectsOnPath>() + o.members.capacity() * size_of::<crate::objects_on_path::Member>()
|
||||
});
|
||||
|
||||
@@ -44,6 +44,11 @@ pub struct NodeExtras {
|
||||
/// Object › Intertwine: as a member of an Intertwine group, the area (document coordinates,
|
||||
/// filled non-zero) where this object is drawn over the group's other members.
|
||||
pub intertwine: Option<Box<PathData>>,
|
||||
/// Object › Intertwine: as a member of an Intertwine group, the areas (document coordinates,
|
||||
/// filled non-zero) where this object is drawn over members that are themselves drawn over
|
||||
/// others there. Item `k` is the area of tier `k + 2`; [`NodeExtras::intertwine`] is tier 1
|
||||
/// (see [`crate::intertwine`]).
|
||||
pub intertwine_tiers: Vec<PathData>,
|
||||
/// Live Paint group: its Gap Options (Object › Live Paint › Gap Options; `None`: the defaults).
|
||||
pub live_paint_gaps: Option<Box<crate::live_paint::GapOptions>>,
|
||||
/// Object › Objects on Path: the group's path and the objects attached to it (see
|
||||
@@ -76,6 +81,7 @@ pub(super) static NO_EXTRAS: NodeExtras = NodeExtras {
|
||||
perspective: None,
|
||||
dimension: None,
|
||||
intertwine: None,
|
||||
intertwine_tiers: Vec::new(),
|
||||
live_paint_gaps: None,
|
||||
objects_on_path: None,
|
||||
colorize: None,
|
||||
|
||||
@@ -496,8 +496,13 @@ impl Node {
|
||||
Arc::make_mut(&mut m.art).transform_scaled(a, sc);
|
||||
}
|
||||
// The area where an Intertwine member is drawn over the others moves with it.
|
||||
if let Some(area) = self.extras_if_any().and_then(|e| e.intertwine.as_mut()) {
|
||||
area.transform(a);
|
||||
if let Some(e) = self.extras_if_any() {
|
||||
if let Some(area) = e.intertwine.as_mut() {
|
||||
area.transform(a);
|
||||
}
|
||||
for area in &mut e.intertwine_tiers {
|
||||
area.transform(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -484,6 +484,23 @@ fn objects_are_read_within_the_node_budget() {
|
||||
assert!(with_node_budget(3, read).is_ok());
|
||||
}
|
||||
|
||||
/// An Intertwine member's areas of tier 2 and up are written and read back, and each one counts
|
||||
/// against [`with_node_budget`] as an object does, so a file of many empty areas is refused.
|
||||
#[test]
|
||||
fn intertwine_tiers_are_read_within_the_node_budget() {
|
||||
let mut n = Node::group(NodeId(1), vec![]);
|
||||
let area = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 10.0, 10.0));
|
||||
n.extras_mut().intertwine_tiers = vec![PathData::default(), area];
|
||||
let json = serde_json::to_string(&n).unwrap();
|
||||
assert!(json.contains("\"intertwine_tiers\""), "{json}");
|
||||
let read = || serde_json::from_str::<Node>(&json);
|
||||
assert_eq!(with_node_budget(3, read).unwrap(), n, "the object and its two areas fit a budget of three");
|
||||
let why = with_node_budget(2, read).unwrap_err().to_string();
|
||||
assert!(why.contains("more than 2 objects"), "{why}");
|
||||
let plain = serde_json::to_string(&Node::group(NodeId(1), vec![])).unwrap();
|
||||
assert!(!plain.contains("intertwine_tiers"), "an object without tiers writes none: {plain}");
|
||||
}
|
||||
|
||||
/// #3587: fields read before an object's `type` are buffered value by value within
|
||||
/// [`with_buffered_value_budget`], each counted once: a group read back from the buffer buffers
|
||||
/// its own fields again without counting them twice. VectorCraft writes `type` first, which
|
||||
|
||||
@@ -46,6 +46,7 @@ impl Serialize for Node {
|
||||
e.perspective.is_none(),
|
||||
e.dimension.is_none(),
|
||||
e.intertwine.is_none(),
|
||||
e.intertwine_tiers.is_empty(),
|
||||
e.live_paint_gaps.is_none(),
|
||||
e.objects_on_path.is_none(),
|
||||
e.colorize.is_none(),
|
||||
@@ -85,6 +86,7 @@ impl Serialize for Node {
|
||||
field!("perspective", &e.perspective);
|
||||
field!("dimension", &e.dimension);
|
||||
field!("intertwine", &e.intertwine);
|
||||
field!("intertwine_tiers", &e.intertwine_tiers);
|
||||
field!("live_paint_gaps", &e.live_paint_gaps);
|
||||
field!("objects_on_path", &e.objects_on_path);
|
||||
field!("colorize", &e.colorize);
|
||||
@@ -123,11 +125,39 @@ pub fn with_node_budget<T>(max: usize, f: impl FnOnce() -> T) -> T {
|
||||
/// each counts once, and the id usually comes first, before the node's children are read.
|
||||
fn counted_id<'de, D: serde::Deserializer<'de>>(d: D) -> Result<NodeId, D::Error> {
|
||||
let id = NodeId::deserialize(d)?;
|
||||
count_one()?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Count one object against [`with_node_budget`].
|
||||
fn count_one<E: serde::de::Error>() -> Result<(), E> {
|
||||
if let Some((left, max)) = NODES_LEFT.get() {
|
||||
let left = left.checked_sub(1).ok_or_else(|| serde::de::Error::custom(format!("the document has more than {max} objects")))?;
|
||||
let left = left.checked_sub(1).ok_or_else(|| E::custom(format!("the document has more than {max} objects")))?;
|
||||
NODES_LEFT.set(Some((left, max)));
|
||||
}
|
||||
Ok(id)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A member's Intertwine areas of tier 2 and up, each counting against [`with_node_budget`] as
|
||||
/// an object does: a compressed file of repeated empty areas stays small while they take memory
|
||||
/// once read.
|
||||
fn counted_tiers<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<PathData>, D::Error> {
|
||||
struct Tiers;
|
||||
impl<'de> serde::de::Visitor<'de> for Tiers {
|
||||
type Value = Vec<PathData>;
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str("a list of areas")
|
||||
}
|
||||
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<PathData>, A::Error> {
|
||||
let mut out = Vec::new();
|
||||
while let Some(area) = seq.next_element::<PathData>()? {
|
||||
count_one()?;
|
||||
out.push(area);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
d.deserialize_seq(Tiers)
|
||||
}
|
||||
|
||||
/// How a [`Node`] is read: the flat field list of the file format. Fields move into the node
|
||||
@@ -186,6 +216,8 @@ pub(super) struct NodeWire {
|
||||
dimension: Option<Box<crate::dimension::Dimension>>,
|
||||
#[serde(default)]
|
||||
intertwine: Option<Box<PathData>>,
|
||||
#[serde(default, deserialize_with = "counted_tiers")]
|
||||
intertwine_tiers: Vec<PathData>,
|
||||
#[serde(default)]
|
||||
live_paint_gaps: Option<Box<crate::live_paint::GapOptions>>,
|
||||
#[serde(default)]
|
||||
@@ -215,6 +247,7 @@ impl From<NodeWire> for Node {
|
||||
perspective: w.perspective,
|
||||
dimension: w.dimension,
|
||||
intertwine: w.intertwine,
|
||||
intertwine_tiers: w.intertwine_tiers,
|
||||
live_paint_gaps: w.live_paint_gaps,
|
||||
objects_on_path: w.objects_on_path,
|
||||
colorize: w.colorize,
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
//! `object.intertwine.make` puts the selected objects in an Intertwine group
|
||||
//! (`NodeKind::Intertwine`), `object.intertwine.release` gives them back as they were. In between,
|
||||
//! `object.intertwine.arrange` picks overlap areas, by a click (`at`) or by an enclosed path drawn
|
||||
//! around them (`area`), and changes which member is drawn on top there: each member keeps the
|
||||
//! area where it is drawn over the others (`NodeExtras::intertwine`, see
|
||||
//! `vectorcraft_doc::intertwine`). Object › Expand turns the group into a plain group of what it
|
||||
//! draws ([`expand_intertwines`]), which Ungroup takes apart.
|
||||
//! around them (`area`), and changes the order the members are drawn in there: each member keeps
|
||||
//! the areas where it is drawn over others, by tier (`NodeExtras::intertwine` and
|
||||
//! `NodeExtras::intertwine_tiers`, see `vectorcraft_doc::intertwine`). Object › Expand turns the
|
||||
//! group into a plain group of what it draws ([`expand_intertwines`]), which Ungroup takes apart.
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! Behaviour: <https://helpx.adobe.com/illustrator/using/intertwine-objects.html> (Make,
|
||||
//! Release, clicking or drawing around an overlap, Bring to Front and Send to Back where three or
|
||||
//! more objects overlap).
|
||||
//! Release, clicking or drawing around an overlap, Bring to Front, Bring Forward, Send Backward
|
||||
//! and Send to Back where three or more objects overlap:
|
||||
//! <https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-intertwined-objects.html>).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -62,7 +63,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Arrange Intertwine Area",
|
||||
[],
|
||||
None,
|
||||
"{id?: Intertwine group (default: the selected one), at?: [x,y] (the overlap area under this point, as a click picks it) | area?: [[x,y]…] (a closed polygon drawn around overlap areas: every area whose inside it encloses), order?: toggle|front|back (toggle: the object drawn right below the top comes to the front, so two objects swap), member?: id (front and back: the object to move; default: front brings the object right below the top, back sends the top one)} → {count: areas changed, top: [id of the object now on top in each area]}",
|
||||
"{id?: Intertwine group (default: the selected one), at?: [x,y] (the overlap area under this point, as a click picks it) | area?: [[x,y]…] (a closed polygon drawn around overlap areas: every area whose inside it encloses), order?: toggle|front|forward|backward|back (toggle: the object drawn right below the top comes to the front, so two objects swap; forward and backward move an object one place up or down among the objects in that area; the others keep their order there), member?: id (front, forward, backward and back: the object to move; default: front and forward move the object right below the top, back and backward the top one)} → {count: areas changed (an area already in that order is not changed, and with none changed there is no undo step), top: [id of the object now on top in each changed area]}",
|
||||
has_doc,
|
||||
arrange
|
||||
),
|
||||
@@ -173,7 +174,10 @@ fn release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
.flatten()
|
||||
.map(|c| {
|
||||
let mut c = (**c).clone();
|
||||
c.extras_mut().intertwine = None;
|
||||
let mut e = c.extras_mut();
|
||||
e.intertwine = None;
|
||||
e.intertwine_tiers = Vec::new();
|
||||
drop(e);
|
||||
c
|
||||
})
|
||||
.collect();
|
||||
@@ -228,6 +232,8 @@ pub(crate) fn expand_intertwines(d: &mut Document, roots: &[NodeId]) -> Result<u
|
||||
enum Order {
|
||||
Toggle,
|
||||
Front,
|
||||
Forward,
|
||||
Backward,
|
||||
Back,
|
||||
}
|
||||
|
||||
@@ -244,39 +250,86 @@ fn polygon(v: &Value) -> Option<PathData> {
|
||||
Some(PathData::single(SubPath::polyline(&pts, true)))
|
||||
}
|
||||
|
||||
/// `n`'s area with `face` added (`add`) or taken out.
|
||||
fn with_area(n: &mut Node, face: &PathData, add: bool) {
|
||||
let old = n.extras().intertwine.as_deref().cloned();
|
||||
let new = match (old, add) {
|
||||
/// Area `old` with `face` added (`add`) or taken out.
|
||||
fn with_face(old: Option<PathData>, face: &PathData, add: bool) -> Option<PathData> {
|
||||
match (old, add) {
|
||||
(None, true) => nonempty(face.clone()),
|
||||
(None, false) => None,
|
||||
(Some(a), true) => nonempty(po::boolean(&a, FillRule::NonZero, face, FillRule::NonZero, po::BoolOp::Union)),
|
||||
(Some(a), false) => nonempty(po::boolean(&a, FillRule::NonZero, face, FillRule::NonZero, po::BoolOp::Difference)),
|
||||
};
|
||||
if new.as_ref() != n.extras().intertwine.as_deref() {
|
||||
n.extras_mut().intertwine = new.map(Box::new);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bring member `x` to the front of `face` (`front`) or send it to the back there; returns the
|
||||
/// member now on top there.
|
||||
fn arrange_face(children: &mut [Arc<Node>], face: &Face, x: usize, front: bool) -> Option<usize> {
|
||||
let lowest = face.members.iter().min().copied()?;
|
||||
let highest = face.members.iter().max().copied()?;
|
||||
for &i in &face.members {
|
||||
let Some(c) = children.get_mut(i) else { continue };
|
||||
let c = Arc::make_mut(c);
|
||||
if front {
|
||||
// Only `x` is drawn over the others here, unless it is on top in its own order.
|
||||
with_area(c, &face.path, i == x && x != highest);
|
||||
} else if i == x {
|
||||
with_area(c, &face.path, false);
|
||||
} else if x != lowest {
|
||||
// Every other member is drawn over `x`, keeping their own order among them.
|
||||
with_area(c, &face.path, true);
|
||||
/// Draw `n` at `tier` in `face` (0: in its own order): `face` joins its area of that tier and
|
||||
/// leaves its areas of the other tiers.
|
||||
fn set_tier(n: &mut Node, face: &PathData, tier: usize) {
|
||||
let e = n.extras();
|
||||
let mut areas: Vec<Option<PathData>> =
|
||||
std::iter::once(e.intertwine.as_deref().cloned()).chain(e.intertwine_tiers.iter().map(|a| (!a.is_empty()).then(|| a.clone()))).collect();
|
||||
if areas.len() < tier {
|
||||
areas.resize(tier, None);
|
||||
}
|
||||
for (k, a) in areas.iter_mut().enumerate() {
|
||||
*a = with_face(a.take(), face, k + 1 == tier);
|
||||
}
|
||||
while areas.len() > 1 && areas.last().is_some_and(Option::is_none) {
|
||||
areas.pop();
|
||||
}
|
||||
let mut areas = areas.into_iter();
|
||||
let first = areas.next().flatten();
|
||||
let rest: Vec<PathData> = areas.map(Option::unwrap_or_default).collect();
|
||||
if first.as_ref() != n.extras().intertwine.as_deref() || rest != n.extras().intertwine_tiers {
|
||||
let mut e = n.extras_mut();
|
||||
e.intertwine = first.map(Box::new);
|
||||
e.intertwine_tiers = rest;
|
||||
}
|
||||
}
|
||||
|
||||
/// The tiers that draw members in `order` (bottom first): each member one tier above the member
|
||||
/// below it when it comes before that member in the group's order, else at the same tier.
|
||||
fn tiers(order: &[usize]) -> Vec<(usize, usize)> {
|
||||
let mut tier = 0;
|
||||
let mut below: Option<usize> = None;
|
||||
order
|
||||
.iter()
|
||||
.map(|&i| {
|
||||
if below.is_some_and(|b| i < b) {
|
||||
tier += 1;
|
||||
}
|
||||
below = Some(i);
|
||||
(i, tier)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 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]) {
|
||||
for (i, tier) in tiers(order) {
|
||||
if let Some(c) = children.get_mut(i) {
|
||||
set_tier(Arc::make_mut(c), &face.path, tier);
|
||||
}
|
||||
}
|
||||
drawn_order(children, face).last().copied()
|
||||
}
|
||||
|
||||
/// `drawn` (the members of an area, bottom first) with member `x` moved as `order` says.
|
||||
fn moved(drawn: &[usize], x: usize, order: Order) -> Vec<usize> {
|
||||
let mut out = drawn.to_vec();
|
||||
let Some(at) = out.iter().position(|i| *i == x) else { return out };
|
||||
match order {
|
||||
Order::Toggle | Order::Front => {
|
||||
out.remove(at);
|
||||
out.push(x);
|
||||
}
|
||||
Order::Back => {
|
||||
out.remove(at);
|
||||
out.insert(0, x);
|
||||
}
|
||||
Order::Forward if at + 1 < out.len() => out.swap(at, at + 1),
|
||||
Order::Backward if at > 0 => out.swap(at - 1, at),
|
||||
Order::Forward | Order::Backward => {}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -309,15 +362,17 @@ fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let order = match str_param(p, "order").unwrap_or("toggle") {
|
||||
"toggle" => Order::Toggle,
|
||||
"front" => Order::Front,
|
||||
"forward" => Order::Forward,
|
||||
"backward" => Order::Backward,
|
||||
"back" => Order::Back,
|
||||
o => return Err(bad(C, format!("unknown order `{o}`: toggle, front or back"))),
|
||||
o => return Err(bad(C, format!("unknown order `{o}`: toggle, front, forward, backward or back"))),
|
||||
};
|
||||
let member = match p.get("member") {
|
||||
Some(v) => Some(checked_id(s, v, C)?),
|
||||
None => None,
|
||||
};
|
||||
if member.is_some() && order == Order::Toggle {
|
||||
return Err(bad(C, "member is for order front or back"));
|
||||
return Err(bad(C, "member is for order front, forward, backward or back"));
|
||||
}
|
||||
let children: Vec<Arc<Node>> = s.doc()?.doc.node(group).and_then(Node::children).cloned().unwrap_or_default();
|
||||
let member = match member {
|
||||
@@ -345,25 +400,32 @@ fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
{
|
||||
return Err(bad(C, "that object is not in the overlap"));
|
||||
}
|
||||
let tops = s.edit("Intertwine", |d, _| {
|
||||
// The new order of each area that changes; areas already in that order are left out.
|
||||
let mut plans: Vec<(&Face, Vec<usize>)> = vec![];
|
||||
for face in found.iter().filter(|f| member.is_none_or(|m| f.members.contains(&m))) {
|
||||
let drawn = drawn_order(&children, face);
|
||||
let x = match (order, member) {
|
||||
(_, Some(m)) => Some(m),
|
||||
(Order::Toggle | Order::Front | Order::Forward, None) => drawn.iter().rev().nth(1).copied(),
|
||||
(Order::Back | Order::Backward, None) => drawn.last().copied(),
|
||||
};
|
||||
let Some(x) = x else { continue };
|
||||
let new = moved(&drawn, x, order);
|
||||
if new != drawn {
|
||||
plans.push((face, new));
|
||||
}
|
||||
}
|
||||
let tops: Vec<u64> = plans.iter().filter_map(|(_, new)| children.get(*new.last()?)).map(|c| c.id.0).collect();
|
||||
if plans.is_empty() {
|
||||
return Ok(json!({ "count": 0, "top": tops }));
|
||||
}
|
||||
s.edit("Intertwine", |d, _| {
|
||||
let n = d.node_mut(group).ok_or(EngineError::NoNode(group))?;
|
||||
let NodeKind::Intertwine { children } = &mut n.kind else { return Err(EngineError::NoNode(group)) };
|
||||
let mut tops = vec![];
|
||||
for face in found.iter().filter(|f| member.is_none_or(|m| f.members.contains(&m))) {
|
||||
let drawn = drawn_order(children, face);
|
||||
let (x, front) = match (order, member) {
|
||||
(Order::Toggle, _) => (drawn.iter().rev().nth(1).copied(), true),
|
||||
(Order::Front, m) => (m.or(drawn.iter().rev().nth(1).copied()), true),
|
||||
(Order::Back, m) => (m.or(drawn.last().copied()), false),
|
||||
};
|
||||
let Some(x) = x else { continue };
|
||||
if let Some(top) = arrange_face(children, face, x, front)
|
||||
&& let Some(c) = children.get(top)
|
||||
{
|
||||
tops.push(c.id.0);
|
||||
}
|
||||
for (face, new) in &plans {
|
||||
arrange_face(children, face, new);
|
||||
}
|
||||
Ok(tops)
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "count": tops.len(), "top": tops }))
|
||||
Ok(json!({ "count": plans.len(), "top": tops }))
|
||||
}
|
||||
|
||||
@@ -135,6 +135,133 @@ fn front_and_back_choose_the_member_where_three_objects_overlap() {
|
||||
assert!(e.to_string().contains("999"), "{e}");
|
||||
}
|
||||
|
||||
/// The rings with a green bar across their top crossing, in front of both, made into an
|
||||
/// Intertwine group: (group, a, b, c). A, B and C overlap at (50, 30); only A and B at (50, 70).
|
||||
fn rings_and_bar(s: &mut Session) -> (NodeId, NodeId, NodeId, NodeId) {
|
||||
let (a, b) = rings(s);
|
||||
let c = id_of(&s.execute("shape.rectangle", &json!({"x": 46, "y": 20, "width": 8, "height": 20})).unwrap());
|
||||
s.execute("paint.setFill", &json!({"ids": [c.0], "color": "#00ff00"})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"ids": [c.0], "none": true})).unwrap();
|
||||
select(s, &[a, b, c]);
|
||||
let g = id_of(&s.execute("object.intertwine.make", &json!({})).unwrap());
|
||||
(g, a, b, c)
|
||||
}
|
||||
|
||||
/// The members of the overlap area of Intertwine group `g` at (x, y), in the order they are drawn
|
||||
/// there, bottom first.
|
||||
fn order_at(d: &Document, g: NodeId, x: f64, y: f64) -> Vec<NodeId> {
|
||||
use vectorcraft_tools::intertwine::{drawn_order, face_at, faces};
|
||||
let children = d.node(g).unwrap().children().unwrap();
|
||||
let face = face_at(&faces(children), vectorcraft_geom::Point::new(x, y)).unwrap();
|
||||
drawn_order(children, &face).into_iter().map(|i| children[i].id).collect()
|
||||
}
|
||||
|
||||
/// Bring Forward and Send Backward move an object one place among the objects of one overlap
|
||||
/// area, keeping the others' order, so every order of three objects can be set there (C, B, A
|
||||
/// included); the other areas stay as they are.
|
||||
#[test]
|
||||
fn forward_and_backward_move_an_object_one_place_in_an_area() {
|
||||
let mut s = session();
|
||||
let (g, a, b, c) = rings_and_bar(&mut s);
|
||||
let order = |s: &Session| order_at(&s.doc().unwrap().doc, g, 50.0, 30.0);
|
||||
let v = arrange(&mut s, json!({"at": [50, 30], "order": "forward", "member": a.0}));
|
||||
assert_eq!((&v["count"], &v["top"]), (&json!(1), &json!([c.0])));
|
||||
assert_eq!(order(&s), [b, a, c]);
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "backward", "member": c.0}));
|
||||
assert_eq!(order(&s), [b, c, a]);
|
||||
assert!(red(&render(&s), 50, 30), "A is on top");
|
||||
let v = arrange(&mut s, json!({"at": [50, 30], "order": "forward", "member": b.0}));
|
||||
assert_eq!(v["top"], json!([a.0]));
|
||||
assert_eq!(order(&s), [c, b, a], "the reverse of the group's order");
|
||||
assert_eq!(hit(&s, 50.0, 30.0), Some(a), "a click picks the object drawn on top");
|
||||
// Without a member: forward moves the object right below the top, backward the top one.
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "backward"}));
|
||||
assert_eq!(order(&s), [c, a, b]);
|
||||
assert!(blue(&render(&s), 50, 30));
|
||||
assert_eq!(hit(&s, 50.0, 30.0), Some(b));
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "forward"}));
|
||||
assert_eq!(order(&s), [c, b, a]);
|
||||
// Bring to Front and Send to Back keep the order of the others.
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "front", "member": c.0}));
|
||||
assert_eq!(order(&s), [b, a, c]);
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "back", "member": a.0}));
|
||||
assert_eq!(order(&s), [a, b, c]);
|
||||
// The crossing where only the rings overlap is as it was.
|
||||
assert_eq!(order_at(&s.doc().unwrap().doc, g, 50.0, 70.0), [a, b]);
|
||||
assert!(blue(&render(&s), 50, 70));
|
||||
// There only the rings count: forward moves A over B, and C is not in that area.
|
||||
let v = arrange(&mut s, json!({"at": [50, 70], "order": "forward"}));
|
||||
assert_eq!(v["top"], json!([a.0]));
|
||||
assert!(red(&render(&s), 50, 70));
|
||||
let e = s.execute("object.intertwine.arrange", &json!({"at": [50, 70], "order": "backward", "member": c.0})).unwrap_err();
|
||||
assert!(e.to_string().contains("not in the overlap"), "{e}");
|
||||
assert_eq!(order(&s), [a, b, c]);
|
||||
}
|
||||
|
||||
/// An object already at the end it is moved to is not moved: no area changes and no undo step is
|
||||
/// recorded. One move is one undo step, and undo, redo and a saved file keep the order.
|
||||
#[test]
|
||||
fn a_move_that_changes_nothing_records_nothing_and_orders_survive_undo_and_save() {
|
||||
let mut s = session();
|
||||
let (g, a, b, c) = rings_and_bar(&mut s);
|
||||
let undo = |s: &Session| s.doc().unwrap().history.undo.len();
|
||||
let before = undo(&s);
|
||||
for (order, member) in [("forward", c), ("front", c), ("backward", a), ("back", a)] {
|
||||
let v = arrange(&mut s, json!({"at": [50, 30], "order": order, "member": member.0}));
|
||||
assert_eq!((&v["count"], &v["top"]), (&json!(0), &json!([])), "{order}");
|
||||
}
|
||||
assert_eq!(undo(&s), before, "nothing to undo");
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "forward", "member": b.0}));
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "backward", "member": c.0}));
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "forward", "member": a.0}));
|
||||
assert_eq!(undo(&s), before + 3, "one undo step a move");
|
||||
let order = |s: &Session| order_at(&s.doc().unwrap().doc, g, 50.0, 30.0);
|
||||
assert_eq!(order(&s), [c, b, a]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(order(&s), [c, a, b]);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(order(&s), [c, b, a]);
|
||||
let data = s.execute("document.export", &json!({"format": "vectorcraft"})).unwrap()["dataBase64"].clone();
|
||||
let mut o = Session::new();
|
||||
o.execute("document.open", &json!({"name": "rings.vectorcraft", "dataBase64": data})).unwrap();
|
||||
assert_eq!(order_at(&o.doc().unwrap().doc, g, 50.0, 30.0), [c, b, a]);
|
||||
assert!(red(&render(&o), 50, 30) && blue(&render(&o), 50, 70));
|
||||
// Expand draws the same.
|
||||
s.execute("object.expand", &json!({"fill": false, "stroke": false})).unwrap();
|
||||
assert!(red(&render(&s), 50, 30) && blue(&render(&s), 50, 70));
|
||||
}
|
||||
|
||||
/// Where three or more objects overlap, hovering highlights the outline of the object nearest to
|
||||
/// the pointer.
|
||||
#[test]
|
||||
fn hovering_where_three_overlap_highlights_the_nearest_object() {
|
||||
use kurbo::Shape;
|
||||
let mut s = session();
|
||||
let view = ViewInfo::default();
|
||||
// Squares from x = 0, 20 and 40, 60 wide: all three overlap from x = 40 to 60.
|
||||
let ids: Vec<NodeId> =
|
||||
[0, 20, 40].iter().map(|x| id_of(&s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 60, "height": 60})).unwrap())).collect();
|
||||
select(&mut s, &ids);
|
||||
s.execute("object.intertwine.make", &json!({})).unwrap();
|
||||
let highlighted = |s: &mut Session, x: f64| {
|
||||
s.pointer(&vectorcraft_tools::PointerEvent::new(vectorcraft_tools::PointerKind::Move, x, 30.0), view).unwrap();
|
||||
let o = s.overlays(view);
|
||||
let [vectorcraft_tools::Overlay::Path { path, .. }] = o.as_slice() else { panic!("one highlight: {o:?}") };
|
||||
let r = path.bounding_box();
|
||||
(r.x0, r.x1)
|
||||
};
|
||||
// Within the half point a 1 pt stroke reaches past each edge.
|
||||
let near = |(x0, x1): (f64, f64), (e0, e1): (f64, f64)| (x0 - e0).abs() <= 0.6 && (x1 - e1).abs() <= 0.6;
|
||||
// By the left edge of the third square, then by the right edge of the first one.
|
||||
let h = highlighted(&mut s, 41.0);
|
||||
assert!(near(h, (40.0, 100.0)), "{h:?}");
|
||||
let h = highlighted(&mut s, 59.0);
|
||||
assert!(near(h, (0.0, 60.0)), "{h:?}");
|
||||
// Where two overlap, the area is highlighted.
|
||||
let h = highlighted(&mut s, 30.0);
|
||||
assert!(near(h, (20.0, 40.0)), "{h:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_gives_the_objects_back_as_they_were() {
|
||||
let mut s = session();
|
||||
|
||||
+106
-18
@@ -7,7 +7,9 @@
|
||||
//! which member is drawn on top there.
|
||||
//!
|
||||
//! The Intertwine tool is what `object.intertwine.make` and `object.intertwine.edit` leave the
|
||||
//! canvas in: hovering highlights the overlap area under the pointer, a click arranges it, a drag
|
||||
//! canvas in: hovering highlights the overlap area under the pointer (where three or more members
|
||||
//! overlap, the outline of the member nearest to the pointer, which the right-click menu
|
||||
//! arranges: [`menu_target`]), a click arranges it, a drag
|
||||
//! draws a path around overlap areas (Shift: a rectangle) and arranges every one inside it.
|
||||
//! Esc or Enter goes back to the tool chosen before. Behaviour:
|
||||
//! <https://helpx.adobe.com/illustrator/using/intertwine-objects.html>.
|
||||
@@ -16,7 +18,7 @@ use std::sync::Arc;
|
||||
|
||||
use kurbo::BezPath;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::intertwine::over_area;
|
||||
use vectorcraft_doc::intertwine::tier_at;
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind};
|
||||
use vectorcraft_effects as effects;
|
||||
use vectorcraft_geom::{FillRule, PathData, Point, Rect, SubPath, shapes};
|
||||
@@ -101,8 +103,18 @@ pub struct Face {
|
||||
|
||||
/// Every overlap area of `children`.
|
||||
pub fn faces(children: &[Arc<Node>]) -> Vec<Face> {
|
||||
faces_of(&painted_areas(children))
|
||||
}
|
||||
|
||||
/// What each of `children` paints ([`painted_area`]), by child index.
|
||||
pub fn painted_areas(children: &[Arc<Node>]) -> Vec<Option<PathData>> {
|
||||
children.iter().map(|c| painted_area(c)).collect()
|
||||
}
|
||||
|
||||
/// Every overlap area of the members that paint `areas` ([`painted_areas`]).
|
||||
pub fn faces_of(areas: &[Option<PathData>]) -> Vec<Face> {
|
||||
let shapes: Vec<po::Shape> =
|
||||
children.iter().enumerate().filter_map(|(i, c)| Some(po::Shape::new(painted_area(c)?, FillRule::NonZero, i as u64))).collect();
|
||||
areas.iter().enumerate().filter_map(|(i, a)| Some(po::Shape::new(a.clone()?, FillRule::NonZero, i as u64))).collect();
|
||||
let key = |k: usize| shapes.get(k).map(|s| s.key as usize);
|
||||
po::regions(&shapes)
|
||||
.into_iter()
|
||||
@@ -124,15 +136,37 @@ pub fn faces_in(faces: &[Face], lasso: &PathData) -> Vec<Face> {
|
||||
faces.iter().filter(|f| contains(lasso, f.inside)).cloned().collect()
|
||||
}
|
||||
|
||||
/// The members of `face` in the order they are drawn there, bottom first: those drawn in their
|
||||
/// own order, then those drawn over the others there, each run in the group's order.
|
||||
/// The members of `face` in the order they are drawn there, bottom first: by their tier there
|
||||
/// ([`tier_at`]), and members of one tier in the group's order.
|
||||
pub fn drawn_order(children: &[Arc<Node>], face: &Face) -> Vec<usize> {
|
||||
let over = |i: &usize| children.get(*i).and_then(|c| over_area(c)).is_some_and(|a| contains(a, face.inside));
|
||||
let tier = |i: usize| children.get(i).map_or(0, |c| tier_at(c, face.inside));
|
||||
let mut order = face.members.clone();
|
||||
order.sort_by_key(|i| (over(i), *i));
|
||||
order.sort_by_cached_key(|i| (tier(*i), *i));
|
||||
order
|
||||
}
|
||||
|
||||
/// The fewest members an overlap area needs for the right-click menu that arranges one of them.
|
||||
pub const MENU_MEMBERS: usize = 3;
|
||||
|
||||
/// The member of `face` whose outline (what it paints, `areas` from [`painted_areas`]) is nearest
|
||||
/// to `p`.
|
||||
pub fn nearest_member(areas: &[Option<PathData>], face: &Face, p: Point) -> Option<usize> {
|
||||
let dist = |i: usize| areas.get(i).and_then(Option::as_ref).map(|a| vectorcraft_geom::hit::distance_to_outline(&a.to_bezpath(), p));
|
||||
face.members.iter().filter_map(|i| Some((*i, dist(*i)?))).filter(|(_, d)| d.is_finite()).min_by(|a, b| a.1.total_cmp(&b.1)).map(|(i, _)| i)
|
||||
}
|
||||
|
||||
/// What a right-click at `p` arranges with the Intertwine tool: the Intertwine group of the
|
||||
/// selection, and the member nearest to `p` (by id) in the overlap area under `p`, when
|
||||
/// [`MENU_MEMBERS`] or more members overlap there.
|
||||
pub fn menu_target(doc: &Document, selected: &[NodeId], p: Point) -> Option<(NodeId, NodeId)> {
|
||||
let g = selected_group(doc, selected)?;
|
||||
let children = doc.node(g)?.children()?;
|
||||
let areas = painted_areas(children);
|
||||
let face = face_at(&faces_of(&areas), p).filter(|f| f.members.len() >= MENU_MEMBERS)?;
|
||||
let m = nearest_member(&areas, &face, p)?;
|
||||
Some((g, children.get(m)?.id))
|
||||
}
|
||||
|
||||
/// The Intertwine group the selection is in, or is (the innermost one around the first selected
|
||||
/// object that is in one).
|
||||
pub fn selected_group(doc: &Document, selected: &[NodeId]) -> Option<NodeId> {
|
||||
@@ -149,6 +183,8 @@ pub fn is_intertwine(n: &Node) -> bool {
|
||||
struct Found {
|
||||
revision: (u64, u64),
|
||||
group: NodeId,
|
||||
/// What each member paints, by child index.
|
||||
areas: Arc<Vec<Option<PathData>>>,
|
||||
faces: Arc<Vec<Face>>,
|
||||
}
|
||||
|
||||
@@ -159,7 +195,8 @@ pub struct IntertwineTool {
|
||||
prev: Option<String>,
|
||||
/// The overlap areas of the group, for the document revision they were found in.
|
||||
cache: Option<Found>,
|
||||
/// The overlap area under the pointer.
|
||||
/// What the pointer highlights: the overlap area under it, or where [`MENU_MEMBERS`] or more
|
||||
/// members overlap, the outline of the member nearest to it.
|
||||
hover: Option<BezPath>,
|
||||
/// A drag's points (document space); the first is where it started.
|
||||
points: Vec<Point>,
|
||||
@@ -170,17 +207,31 @@ pub struct IntertwineTool {
|
||||
|
||||
impl IntertwineTool {
|
||||
fn faces(&mut self, cx: &ToolContext) -> Option<(NodeId, Arc<Vec<Face>>)> {
|
||||
self.found(cx).map(|c| (c.group, c.faces.clone()))
|
||||
}
|
||||
|
||||
/// The overlap areas of the selection's group, found again when the document changed.
|
||||
fn found(&mut self, cx: &ToolContext) -> Option<&Found> {
|
||||
let g = selected_group(cx.doc, &cx.selection.objects)?;
|
||||
if let Some(c) = &self.cache
|
||||
&& c.revision == cx.revision
|
||||
&& c.group == g
|
||||
{
|
||||
return Some((g, c.faces.clone()));
|
||||
if !self.cache.as_ref().is_some_and(|c| c.revision == cx.revision && c.group == g) {
|
||||
let children = cx.doc.node(g)?.children()?;
|
||||
let areas = painted_areas(children);
|
||||
let faces = Arc::new(faces_of(&areas));
|
||||
self.cache = Some(Found { revision: cx.revision, group: g, areas: Arc::new(areas), faces });
|
||||
}
|
||||
let children = cx.doc.node(g)?.children()?;
|
||||
let f = Arc::new(faces(children));
|
||||
self.cache = Some(Found { revision: cx.revision, group: g, faces: f.clone() });
|
||||
Some((g, f))
|
||||
self.cache.as_ref()
|
||||
}
|
||||
|
||||
/// What hovering `p` highlights ([`IntertwineTool::hover`]).
|
||||
fn highlight(&mut self, cx: &ToolContext, p: Point) -> Option<BezPath> {
|
||||
let found = self.found(cx)?;
|
||||
let face = face_at(&found.faces, p)?;
|
||||
if face.members.len() >= MENU_MEMBERS
|
||||
&& let Some(a) = nearest_member(&found.areas, &face, p).and_then(|m| found.areas.get(m)).and_then(Option::as_ref)
|
||||
{
|
||||
return Some(a.to_bezpath());
|
||||
}
|
||||
Some(face.path.to_bezpath())
|
||||
}
|
||||
|
||||
/// The closed path the drag drew.
|
||||
@@ -209,7 +260,7 @@ impl Tool for IntertwineTool {
|
||||
let p = ev.pos;
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
self.hover = self.faces(cx).and_then(|(_, f)| face_at(&f, p)).map(|f| f.path.to_bezpath());
|
||||
self.hover = self.highlight(cx, p);
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
@@ -332,6 +383,43 @@ mod tests {
|
||||
assert_eq!(drawn_order(&kids, &f[0]), [1, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn members_drawn_at_higher_tiers_are_drawn_later() {
|
||||
// Three squares over one another; the bottom one at tier 2, the middle one at tier 1.
|
||||
let over = || shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0));
|
||||
let mut a = (*square(1, 0.0)).clone();
|
||||
a.extras_mut().intertwine_tiers = vec![over()];
|
||||
let mut b = (*square(2, 0.0)).clone();
|
||||
b.extras_mut().intertwine = Some(Box::new(over()));
|
||||
let kids = vec![Arc::new(a), Arc::new(b), square(3, 0.0)];
|
||||
let f = faces(&kids);
|
||||
assert_eq!(f.len(), 1);
|
||||
assert_eq!(drawn_order(&kids, &f[0]), [2, 1, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_member_nearest_to_the_pointer_is_picked_where_three_overlap() {
|
||||
// Squares 20 wide from x = 0, 4 and 8: all three overlap from x = 8 to 20.
|
||||
let wide =
|
||||
|id: u64, x: f64| Arc::new(Node::path(NodeId(id), shapes::rectangle(Rect::new(x, 0.0, x + 20.0, 20.0)), Appearance::default_art()));
|
||||
let kids = vec![wide(1, 0.0), wide(2, 4.0), wide(3, 8.0)];
|
||||
let areas = painted_areas(&kids);
|
||||
let all = faces_of(&areas);
|
||||
let face = face_at(&all, Point::new(14.0, 10.0)).unwrap();
|
||||
assert_eq!(face.members, [0, 1, 2]);
|
||||
// By the left edge of the third square, then by the right edge of the first one.
|
||||
assert_eq!(nearest_member(&areas, &face, Point::new(8.5, 10.0)), Some(2));
|
||||
assert_eq!(nearest_member(&areas, &face, Point::new(19.5, 10.0)), Some(0));
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let l = d.layers[0].id;
|
||||
let g = d.alloc_id();
|
||||
d.insert(Some(l), 0, Node::new(g, NodeKind::Intertwine { children: kids })).unwrap();
|
||||
assert_eq!(menu_target(&d, &[g], Point::new(8.5, 10.0)), Some((g, NodeId(3))));
|
||||
assert_eq!(menu_target(&d, &[g], Point::new(19.5, 10.0)), Some((g, NodeId(1))));
|
||||
assert_eq!(menu_target(&d, &[g], Point::new(6.0, 10.0)), None, "only two overlap there");
|
||||
assert_eq!(menu_target(&d, &[], Point::new(8.5, 10.0)), None, "no Intertwine group selected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lasso_takes_the_areas_whose_inside_it_encloses() {
|
||||
let kids = vec![square(1, 0.0), square(2, 5.0)];
|
||||
|
||||
@@ -4,7 +4,9 @@ use super::{Ui, VectorcraftApp, Xf, json, widgets};
|
||||
/// Right-click: the object under the pointer is selected first unless it already is, then the
|
||||
/// context menu lists what applies to the selection ([`crate::menus::context_items`]). The locked
|
||||
/// objects under the pointer ([`vectorcraft_doc::hit::locked_objects_at`]) are kept in
|
||||
/// `ui.context_locked` while the menu is open, for its Unlock submenu.
|
||||
/// `ui.context_locked` while the menu is open, for its Unlock submenu. With the Intertwine tool, a
|
||||
/// right-click where three or more objects of the group overlap keeps the selection and puts what
|
||||
/// it arranges in `ui.context_intertwine` ([`vectorcraft_tools::intertwine::menu_target`]).
|
||||
pub(super) fn context_menu(app: &mut VectorcraftApp, resp: &egui::Response, xf: &Xf) {
|
||||
if resp.secondary_clicked()
|
||||
&& let Some(p) = resp.interact_pointer_pos()
|
||||
@@ -12,7 +14,13 @@ pub(super) fn context_menu(app: &mut VectorcraftApp, resp: &egui::Response, xf:
|
||||
let p = xf.to_doc(p);
|
||||
let locked = hit_with(app, p, xf.zoom, |doc, p, opt| Some(vectorcraft_doc::hit::locked_objects_at(doc, p, opt)));
|
||||
app.ui.context_locked = app.session.active().map(|st| st.uid).zip(locked);
|
||||
if let Some(st) = app.session.active()
|
||||
app.ui.context_intertwine = app
|
||||
.session
|
||||
.active()
|
||||
.filter(|_| app.session.tool_id() == "intertwine")
|
||||
.and_then(|st| vectorcraft_tools::intertwine::menu_target(&st.doc, &st.selection.objects, p).map(|(g, m)| (st.uid, g, p, m)));
|
||||
if app.ui.context_intertwine.is_none()
|
||||
&& let Some(st) = app.session.active()
|
||||
&& let Some(top) = selection_hit_at(app, p, xf.zoom).map(|h| h.top_object(st.isolation))
|
||||
&& !st.selection.contains(top)
|
||||
{
|
||||
@@ -23,6 +31,7 @@ pub(super) fn context_menu(app: &mut VectorcraftApp, resp: &egui::Response, xf:
|
||||
let mut clicked = None;
|
||||
if resp.context_menu(|ui| crate::menus::context_menu_body(app, ui, &mut clicked)).is_none() {
|
||||
app.ui.context_locked = None;
|
||||
app.ui.context_intertwine = None;
|
||||
}
|
||||
if let Some((id, p)) = clicked {
|
||||
crate::menus::invoke(app, &id, p);
|
||||
|
||||
@@ -15,6 +15,21 @@ pub fn context_items(app: &VectorcraftApp) -> Vec<Item> {
|
||||
if st.isolation.is_some() {
|
||||
v.extend([c("Exit Isolation Mode", "object.exitIsolation"), Sep]);
|
||||
}
|
||||
// With the Intertwine tool, where three or more objects overlap: the object nearest to the
|
||||
// right-click moves in that overlap area only (`object.intertwine.arrange`), in place of the
|
||||
// Arrange submenu, which moves objects in the whole group.
|
||||
let intertwine = app.ui.context_intertwine.filter(|(uid, ..)| *uid == st.uid && app.session.tool_id() == "intertwine");
|
||||
if let Some((_, group, at, member)) = intertwine {
|
||||
let item =
|
||||
|label, order| cp(label, "object.intertwine.arrange", json!({"id": group.0, "at": [at.x, at.y], "order": order, "member": member.0}));
|
||||
v.extend([
|
||||
item("Bring to Front", "front"),
|
||||
item("Bring Forward", "forward"),
|
||||
item("Send Backward", "backward"),
|
||||
item("Send to Back", "back"),
|
||||
Sep,
|
||||
]);
|
||||
}
|
||||
// Managing artboards from the canvas: with the Artboard tool, the menu locks and unlocks them
|
||||
// (`artboard.setProps` with `locked` for each artboard named; an entry with none to change is left out).
|
||||
if app.session.tool_id() == "artboard" {
|
||||
@@ -107,11 +122,11 @@ pub fn context_items(app: &VectorcraftApp) -> Vec<Item> {
|
||||
if crate::panels::character::text_style(app).is_some() {
|
||||
v.push(sub("Bullets and Numbering", vec![c("Apply Bullets", "type.applyBullets"), c("Apply Numbers", "type.applyNumbers")]));
|
||||
}
|
||||
v.extend([c("Make Guides", "view.guides.make"), Sep, sub("Transform", transform_items())]);
|
||||
if intertwine.is_none() {
|
||||
v.push(sub("Arrange", arrange_items()));
|
||||
}
|
||||
v.extend([
|
||||
c("Make Guides", "view.guides.make"),
|
||||
Sep,
|
||||
sub("Transform", transform_items()),
|
||||
sub("Arrange", arrange_items()),
|
||||
sub(
|
||||
"Select",
|
||||
vec![c("Next Object Above", "select.nextAbove"), c("Next Object Below", "select.nextBelow"), Sep, c("Deselect", "select.none")],
|
||||
|
||||
@@ -357,6 +357,13 @@ pub struct UiState {
|
||||
/// saved.
|
||||
#[serde(skip)]
|
||||
pub context_locked: Option<(u64, Vec<vectorcraft_doc::NodeId>)>,
|
||||
/// What the last right-click on the canvas with the Intertwine tool arranges, with the
|
||||
/// document's `uid`: the Intertwine group, the point and the object nearest to it where three
|
||||
/// or more objects overlap ([`vectorcraft_tools::intertwine::menu_target`]). The context menu
|
||||
/// lists Bring to Front, Bring Forward, Send Backward and Send to Back for that object while it
|
||||
/// is open. Not saved.
|
||||
#[serde(skip)]
|
||||
pub context_intertwine: Option<(u64, vectorcraft_doc::NodeId, vectorcraft_geom::Point, vectorcraft_doc::NodeId)>,
|
||||
/// Last tool shown in each toolbar slot (keyed by the slot's first tool id).
|
||||
#[serde(default)]
|
||||
pub slot_tool: std::collections::BTreeMap<String, String>,
|
||||
@@ -833,6 +840,7 @@ impl Default for UiState {
|
||||
task_bar: true,
|
||||
task_bar_place: TaskBarPlace::default(),
|
||||
context_locked: None,
|
||||
context_intertwine: None,
|
||||
slot_tool: Default::default(),
|
||||
floating_flyouts: vec![],
|
||||
toolbar_edits: ToolbarEdits::default(),
|
||||
|
||||
@@ -288,6 +288,61 @@ fn selected_type_has_apply_numbers() {
|
||||
assert_eq!(text.para_at(0).list.and_then(|l| l.number), Some(vectorcraft_doc::text::NumberStyle::Arabic));
|
||||
}
|
||||
|
||||
/// The members of the overlap area of Intertwine group `g` at (x, y), in the order they are drawn
|
||||
/// there, bottom first.
|
||||
fn intertwine_order(app: &VectorcraftApp, g: NodeId, x: f64, y: f64) -> Vec<NodeId> {
|
||||
use vectorcraft_tools::intertwine::{drawn_order, face_at, faces};
|
||||
let st = app.session.active().unwrap();
|
||||
let children = st.doc.node(g).unwrap().children().unwrap();
|
||||
let face = face_at(&faces(children), Point::new(x, y)).unwrap();
|
||||
drawn_order(children, &face).into_iter().map(|i| children[i].id).collect()
|
||||
}
|
||||
|
||||
/// With the Intertwine tool, a right-click where three or more objects of the group overlap lists
|
||||
/// Bring to Front, Bring Forward, Send Backward and Send to Back for the object nearest to the
|
||||
/// pointer in place of the Arrange submenu, and they move it in that overlap area only. Elsewhere
|
||||
/// the menu is the usual one.
|
||||
#[test]
|
||||
fn right_click_where_three_intertwined_objects_overlap_arranges_the_nearest_one() {
|
||||
let mut app = app();
|
||||
// Rectangles from x = 50, 90 and 130, 120 wide: all three overlap from x = 130 to 170.
|
||||
let wide = |app: &mut VectorcraftApp, x: f64| {
|
||||
NodeId(app.run("shape.rectangle", json!({"x": x, "y": 50, "width": 120, "height": 100})).unwrap()["id"].as_u64().unwrap())
|
||||
};
|
||||
let (a, b, c) = (wide(&mut app, 50.0), wide(&mut app, 90.0), wide(&mut app, 130.0));
|
||||
app.run("select.set", json!({"ids": [a.0, b.0, c.0]})).unwrap();
|
||||
let g = NodeId(app.run("object.intertwine.make", json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
assert_eq!(app.session.tool_id(), "intertwine");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let items = ["Bring to Front", "Bring Forward", "Send Backward", "Send to Back"];
|
||||
// By the right edge of A (the first rectangle), where all three overlap.
|
||||
let p = screen(&app, 169.0, 100.0);
|
||||
let texts = click(&mut app, &ctx, p, PointerButton::Secondary);
|
||||
assert_eq!(app.session.active().unwrap().selection.objects, [g], "the selection stays the group");
|
||||
for label in items {
|
||||
assert!(has(&texts, label), "{label} in {texts:?}");
|
||||
}
|
||||
assert!(!has(&texts, "Arrange"), "no Arrange submenu: {texts:?}");
|
||||
let rows: Vec<f32> = items.iter().map(|l| at(&texts, l).y).collect();
|
||||
assert!(rows.windows(2).all(|w| w[0] < w[1]), "in that order: {rows:?}");
|
||||
let Some(Item::Cmd(_, id, params)) = menus::context_items(&app).into_iter().find(|i| matches!(i, Item::Cmd("Bring Forward", ..))) else {
|
||||
panic!("no Bring Forward item")
|
||||
};
|
||||
assert_eq!(id, "object.intertwine.arrange");
|
||||
assert_eq!((¶ms["id"], ¶ms["order"], ¶ms["member"]), (&json!(g.0), &json!("forward"), &json!(a.0)));
|
||||
// Bring Forward moves A one place up in that area only.
|
||||
assert_eq!(intertwine_order(&app, g, 150.0, 100.0), [a, b, c]);
|
||||
let texts = click(&mut app, &ctx, at(&texts, "Bring Forward"), PointerButton::Primary);
|
||||
assert!(!has(&texts, "Bring Forward"), "the menu closes");
|
||||
assert_eq!(intertwine_order(&app, g, 150.0, 100.0), [b, a, c]);
|
||||
assert_eq!(intertwine_order(&app, g, 110.0, 100.0), [a, b], "where only A and B overlap");
|
||||
// Where only two overlap, the menu is the usual one.
|
||||
let p = screen(&app, 110.0, 100.0);
|
||||
let texts = click(&mut app, &ctx, p, PointerButton::Secondary);
|
||||
assert!(has(&texts, "Arrange") && !has(&texts, "Bring Forward"), "{texts:?}");
|
||||
}
|
||||
|
||||
/// Every label the context menu can show is a menu string, so complete catalogs must translate it.
|
||||
#[test]
|
||||
fn every_context_label_is_a_menu_string() {
|
||||
|
||||
+5
-3
@@ -439,9 +439,11 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
- **Done:** Object › Intertwine (#1763): Make, Edit and Release; the Intertwine tool highlights the overlap area under
|
||||
the pointer, a click swaps which object is on top there, and a path (Shift: a rectangle) drawn around overlaps
|
||||
arranges each one inside it; `object.intertwine.arrange` also brings an object to the front or sends it to the back
|
||||
where three or more overlap. The group renders, saves and exports through its live expansion. Still missing: the
|
||||
right-click Bring Forward / Send Backward menu where three or more objects overlap; adding objects to the group by
|
||||
arranges each one inside it. Where three or more objects overlap, hovering highlights the object nearest to the
|
||||
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
|
||||
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
|
||||
|
||||
+13
@@ -1433,6 +1433,19 @@ Group. Construction mode ends when the group was in it.
|
||||
outside the bounds the group's original shapes had before the move, in the move's undo step. A shape whose bounds still
|
||||
overlap them only moves. The Selection tool sends it at the end of a move in Construction mode.
|
||||
|
||||
## Intertwine
|
||||
|
||||
`object.intertwine.arrange {id?, at?|area?, order?, member?}` changes the order the objects of an Intertwine group are
|
||||
drawn in, in the overlap areas under `at` or enclosed by the polygon `area`. `order` is `toggle` (the default: the
|
||||
object right below the top comes to the front), `front`, `forward`, `backward` or `back`. `forward` and `backward` move
|
||||
`member` one place up or down among the objects that overlap in that area, and `front` and `back` move it to the top or
|
||||
the bottom there; the other objects keep their order. Without `member`, `front` and `forward` move the object right
|
||||
below the top, and `back` and `backward` move the top one. The command returns `{count, top}`: the number of areas
|
||||
whose order changed and the id of the object now on top in each. A move that changes no area returns
|
||||
`{count: 0, top: []}` and records no undo step. With the Intertwine tool, a right-click where three or more objects
|
||||
overlap offers Bring to Front, Bring Forward, Send Backward and Send to Back, which run this command for the object
|
||||
nearest to the pointer.
|
||||
|
||||
## Global Edit
|
||||
|
||||
Select › Start Global Edit is `select.globalEdit {appearance?, size?, artboards?, includeCanvas?}`. With one path,
|
||||
|
||||
Reference in New Issue
Block a user