mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Merge pull request #2095 from switch87/feat/1763-intertwine
G13: Object › Intertwine: Make, Edit, Release and the Intertwine tool (#1763)
This commit is contained in:
Generated
+1
@@ -4930,6 +4930,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-effects",
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-pathops",
|
||||
"vectorcraft-text",
|
||||
|
||||
@@ -320,7 +320,12 @@ impl<'a> Scene<'a> {
|
||||
}
|
||||
}
|
||||
// Live blends, envelopes, meshes and repeats are written as their evaluated art.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => {
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => {
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
for c in g.children().into_iter().flatten() {
|
||||
self.node(c, false);
|
||||
|
||||
+23
-3
@@ -145,7 +145,7 @@ fn objects_at_layer(layer: &Node, p: Point, opt: HitOptions, symbols: &SymbolBox
|
||||
let skip = |id| seen.contains(&id);
|
||||
let mut chain = Vec::new();
|
||||
let hit = match &c.kind {
|
||||
NodeKind::Group { .. } => hit_children(c, p, opt, symbols, &skip, &mut chain, true).is_some(),
|
||||
NodeKind::Group { .. } | NodeKind::Intertwine { .. } => hit_children(c, p, opt, symbols, &skip, &mut chain, true).is_some(),
|
||||
NodeKind::Blend { .. } => hit_children(c, p, opt, symbols, &skip, &mut chain, true).is_some() || hit_leaf(c, p, opt, symbols).is_some(),
|
||||
_ => hit_leaf(c, p, opt, symbols).is_some(),
|
||||
};
|
||||
@@ -262,7 +262,8 @@ fn hit_children(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
for (i, c) in children.iter().enumerate().rev() {
|
||||
for i in hit_order(parent, children, p) {
|
||||
let Some(c) = children.get(i) else { continue };
|
||||
if !may_hit(parent, i, c, p, opt, symbols, editable_only) || skip(c.id) {
|
||||
continue;
|
||||
}
|
||||
@@ -270,7 +271,9 @@ fn hit_children(
|
||||
let hit = match &c.kind {
|
||||
// A sublayer whose Preview is off is clicked in outline, like an outline view.
|
||||
NodeKind::Layer { preview: false, .. } => hit_children(c, p, HitOptions { outline: true, ..opt }, symbols, skip, chain, editable_only),
|
||||
NodeKind::Layer { .. } | NodeKind::Group { .. } => hit_children(c, p, opt, symbols, skip, chain, editable_only),
|
||||
NodeKind::Layer { .. } | NodeKind::Group { .. } | NodeKind::Intertwine { .. } => {
|
||||
hit_children(c, p, opt, symbols, skip, chain, editable_only)
|
||||
}
|
||||
// Edit Contents: the envelope's content hits, undistorted.
|
||||
NodeKind::Envelope { editing: true, .. } => hit_children(c, p, opt, symbols, skip, chain, editable_only),
|
||||
// A blend's key objects first (Direct and Group Selection pick them); its steps hit
|
||||
@@ -287,6 +290,23 @@ fn hit_children(
|
||||
None
|
||||
}
|
||||
|
||||
/// 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).
|
||||
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)
|
||||
}
|
||||
|
||||
/// A click on the strokes of a path or compound path `n` along `bp`: what any visible stroke
|
||||
/// paints there (arrowheads, alignment, width profile, projecting caps, miter spikes), else its
|
||||
/// outline within the tolerance. Outline mode hits the outline only.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! 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 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 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).
|
||||
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 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);
|
||||
zero_ids(&mut copy);
|
||||
out.push(Node::new(NodeId(0), NodeKind::Group { children: vec![Arc::new(clip), Arc::new(copy)], clip: true }));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `n` without its Intertwine area (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;
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
fn zero_ids(n: &mut Node) {
|
||||
n.id = NodeId(0);
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
zero_ids(Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use vectorcraft_geom::{Rect, shapes};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn square(id: u64, x: f64) -> Arc<Node> {
|
||||
Arc::new(Node::path(NodeId(id), shapes::rectangle(Rect::new(x, 0.0, x + 10.0, 10.0)), crate::Appearance::default()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn members_without_areas_expand_to_themselves() {
|
||||
let kids = vec![square(1, 0.0), square(2, 5.0)];
|
||||
let out = expand(&kids);
|
||||
assert_eq!(out.len(), 2);
|
||||
assert_eq!(out.iter().map(|n| n.id).collect::<Vec<_>>(), [NodeId(1), NodeId(2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_member_with_an_area_is_drawn_again_clipped_to_it_on_top() {
|
||||
let mut a = (*square(1, 0.0)).clone();
|
||||
a.extras_mut().intertwine = Some(Box::new(shapes::rectangle(Rect::new(5.0, 0.0, 10.0, 10.0))));
|
||||
let kids = vec![Arc::new(a), square(2, 5.0)];
|
||||
let out = expand(&kids);
|
||||
assert_eq!(out.len(), 3, "both members, then the raised copy");
|
||||
assert!(out.iter().all(|n| n.extras().intertwine.is_none()), "the expansion is plain art");
|
||||
let NodeKind::Group { children, clip: true } = &out[2].kind else { panic!("a clip group: {:?}", out[2].kind) };
|
||||
assert!(matches!(children[0].kind, NodeKind::Path { clipping: true, .. }));
|
||||
assert_eq!(children[1].path_data(), kids[0].path_data());
|
||||
assert_eq!((out[2].id, children[1].id), (NodeId(0), NodeId(0)), "generated art has no ids of its own");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hidden_member_is_not_drawn_over_the_others() {
|
||||
let mut a = (*square(1, 0.0)).clone();
|
||||
a.visible = false;
|
||||
a.extras_mut().intertwine = Some(Box::new(shapes::rectangle(Rect::new(5.0, 0.0, 10.0, 10.0))));
|
||||
assert_eq!(expand(&[Arc::new(a), square(2, 5.0)]).len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ pub mod hit;
|
||||
mod id_index;
|
||||
pub mod inks;
|
||||
mod inline;
|
||||
pub mod intertwine;
|
||||
pub mod shared;
|
||||
pub use inline::{SYMBOL_HALF, SYMBOL_SIZES};
|
||||
pub use shared::Shared;
|
||||
|
||||
+11
-2
@@ -1437,7 +1437,7 @@ fn map_node(n: &Node, w: &Warper) -> Node {
|
||||
}
|
||||
}
|
||||
NodeKind::Mesh(m) => m.map(w.f),
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) => {
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Intertwine { .. } => {
|
||||
if let Some(d) = &deeper {
|
||||
return map_node(&expanded_group_hooks(n, w.hooks), d);
|
||||
}
|
||||
@@ -1616,7 +1616,15 @@ pub fn envelope_bounds(content: &[Arc<Node>], kind: &EnvelopeKind, frame: Affine
|
||||
|
||||
/// Is this one of the live kinds?
|
||||
pub fn is_live(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_))
|
||||
matches!(
|
||||
n.kind,
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Mesh tessellation as filled quad paths (no stroke), `n`×`n` per patch.
|
||||
@@ -1651,6 +1659,7 @@ pub fn expand_live_hooks(n: &Node, hooks: Hooks) -> Vec<Node> {
|
||||
NodeKind::Mesh(m) => mesh_quad_nodes(m, 8),
|
||||
NodeKind::Repeat(r) => r.expand(),
|
||||
NodeKind::PlacedDocument(p) => p.art(n),
|
||||
NodeKind::Intertwine { children } => crate::intertwine::expand(children),
|
||||
_ => vec![n.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,10 +251,14 @@ fn node_bytes(n: &Node, other: Option<&Node>) -> usize {
|
||||
b += ALLOC + size_of::<NodeExtras>();
|
||||
b += boxed(&e.mask) + boxed(&e.graph) + boxed(&e.shaper) + boxed(&e.attrs) + boxed(&e.slice) + boxed(&e.perspective);
|
||||
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));
|
||||
}
|
||||
b += match &n.kind {
|
||||
NodeKind::Path { path, live, .. } => path_bytes(path) + boxed(live),
|
||||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => vec_bytes(children),
|
||||
NodeKind::Layer { children, .. }
|
||||
| NodeKind::Group { children, .. }
|
||||
| NodeKind::Compound { children, .. }
|
||||
| NodeKind::Intertwine { children } => vec_bytes(children),
|
||||
NodeKind::Blend { children, .. } => vec_bytes(children) + ALLOC + size_of::<BlendSpec>(),
|
||||
NodeKind::Envelope { content, .. } => vec_bytes(content) + 2 * ALLOC + size_of::<EnvelopeKind>() + size_of::<Affine>(),
|
||||
NodeKind::Repeat(r) => vec_bytes(&r.source) + OTHER_KIND,
|
||||
|
||||
+35
-4
@@ -486,6 +486,12 @@ pub enum NodeKind {
|
||||
/// A placed document: an artboard of another VectorCraft file, linked and locked (see
|
||||
/// [`crate::placed_document`]).
|
||||
PlacedDocument(Box<crate::placed_document::PlacedDocument>),
|
||||
/// Object › Intertwine: the objects keep their own stacking order, except where one of them
|
||||
/// is drawn over the others: the area [`NodeExtras::intertwine`] of that member (see
|
||||
/// [`crate::intertwine`]).
|
||||
Intertwine {
|
||||
children: Vec<Arc<Node>>,
|
||||
},
|
||||
}
|
||||
|
||||
fn yes() -> bool {
|
||||
@@ -561,6 +567,9 @@ pub struct NodeExtras {
|
||||
/// Dimension object: the live measurement it draws; its ordinary group children hold its
|
||||
/// exportable appearance (Object › Dimensions, the Dimension tool).
|
||||
pub dimension: Option<Box<crate::dimension::Dimension>>,
|
||||
/// 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>>,
|
||||
}
|
||||
|
||||
/// No extras: what [`Node::extras`] gives for a node without any.
|
||||
@@ -577,6 +586,7 @@ static NO_EXTRAS: NodeExtras = NodeExtras {
|
||||
slice: None,
|
||||
perspective: None,
|
||||
dimension: None,
|
||||
intertwine: None,
|
||||
};
|
||||
|
||||
/// A node's [`NodeExtras`], if it has any ([`Node::extras`]). Extras that are all unset equal
|
||||
@@ -709,6 +719,7 @@ impl Serialize for Node {
|
||||
is_default(&self.bbox_angle),
|
||||
e.perspective.is_none(),
|
||||
e.dimension.is_none(),
|
||||
e.intertwine.is_none(),
|
||||
];
|
||||
let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;
|
||||
let mut skip = skips.into_iter();
|
||||
@@ -745,6 +756,7 @@ impl Serialize for Node {
|
||||
field!("bbox_angle", &self.bbox_angle);
|
||||
field!("perspective", &e.perspective);
|
||||
field!("dimension", &e.dimension);
|
||||
field!("intertwine", &e.intertwine);
|
||||
st.end()
|
||||
}
|
||||
}
|
||||
@@ -800,6 +812,8 @@ struct NodeWire {
|
||||
perspective: Option<Box<crate::PerspectiveAttachment>>,
|
||||
#[serde(default)]
|
||||
dimension: Option<Box<crate::dimension::Dimension>>,
|
||||
#[serde(default)]
|
||||
intertwine: Option<Box<PathData>>,
|
||||
}
|
||||
|
||||
impl From<NodeWire> for Node {
|
||||
@@ -817,6 +831,7 @@ impl From<NodeWire> for Node {
|
||||
slice: w.slice,
|
||||
perspective: w.perspective,
|
||||
dimension: w.dimension,
|
||||
intertwine: w.intertwine,
|
||||
};
|
||||
Node {
|
||||
id: w.id,
|
||||
@@ -902,7 +917,8 @@ impl Node {
|
||||
| NodeKind::Compound { children, .. }
|
||||
| NodeKind::Blend { children, .. }
|
||||
| NodeKind::Envelope { content: children, .. }
|
||||
| NodeKind::Repeat(RepeatSpec { source: children, .. }) => Some(children),
|
||||
| NodeKind::Repeat(RepeatSpec { source: children, .. })
|
||||
| NodeKind::Intertwine { children } => Some(children),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -913,7 +929,8 @@ impl Node {
|
||||
| NodeKind::Compound { children, .. }
|
||||
| NodeKind::Blend { children, .. }
|
||||
| NodeKind::Envelope { content: children, .. }
|
||||
| NodeKind::Repeat(RepeatSpec { source: children, .. }) => Some(children),
|
||||
| NodeKind::Repeat(RepeatSpec { source: children, .. })
|
||||
| NodeKind::Intertwine { children } => Some(children),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -981,6 +998,7 @@ impl Node {
|
||||
NodeKind::Mesh(_) => "Mesh",
|
||||
NodeKind::Repeat(r) => r.kind.label(),
|
||||
NodeKind::PlacedDocument(_) => "Placed Document",
|
||||
NodeKind::Intertwine { .. } => "Intertwine",
|
||||
}
|
||||
}
|
||||
/// Name shown in the Layers panel: explicit name or `<Kind>`.
|
||||
@@ -1092,6 +1110,8 @@ impl Node {
|
||||
// copies out by the source's bounds measured without the document.
|
||||
NodeKind::Repeat(r) => r.copies_bounds(union(&r.source)?),
|
||||
NodeKind::PlacedDocument(p) => Some(p.bounds()),
|
||||
// The members drawn over the others are drawn where they are.
|
||||
NodeKind::Intertwine { children } => union(children),
|
||||
}
|
||||
}
|
||||
/// Visual bounds: what the object paints, its strokes included (paths and compound paths
|
||||
@@ -1126,6 +1146,7 @@ impl Node {
|
||||
clip.geometric(symbols)
|
||||
}
|
||||
}
|
||||
NodeKind::Intertwine { children } => children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.visual(symbols))),
|
||||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => {
|
||||
// The container's own strokes paint around its members.
|
||||
let o = self.appearance.outset();
|
||||
@@ -1221,7 +1242,10 @@ impl Node {
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => {
|
||||
NodeKind::Layer { children, .. }
|
||||
| NodeKind::Group { children, .. }
|
||||
| NodeKind::Compound { children, .. }
|
||||
| NodeKind::Intertwine { children } => {
|
||||
for c in children.iter_mut() {
|
||||
Arc::make_mut(c).transform_scaled(a, sc);
|
||||
}
|
||||
@@ -1275,6 +1299,10 @@ 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);
|
||||
}
|
||||
}
|
||||
/// Visit this node and all descendants depth first (paint order).
|
||||
pub fn walk<'a>(&'a self, f: &mut impl FnMut(&'a Node)) {
|
||||
@@ -1403,7 +1431,7 @@ impl Node {
|
||||
let frame = shapes::rectangle(p.natural()).transformed(p.xf);
|
||||
out.push((frame.to_bezpath(), FillRule::NonZero));
|
||||
}
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => {
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::Intertwine { .. } => {
|
||||
crate::live::expand_deep(self, text).push_clip_shapes(text, out);
|
||||
}
|
||||
}
|
||||
@@ -1818,6 +1846,9 @@ mod kind_de {
|
||||
Mesh(GradientMesh),
|
||||
Repeat(RepeatSpec),
|
||||
PlacedDocument(Box<crate::placed_document::PlacedDocument>),
|
||||
Intertwine {
|
||||
children: Vec<Arc<Node>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for NodeKind {
|
||||
|
||||
@@ -50,7 +50,7 @@ pub const CATEGORIES: [(&str, &str); 11] = [
|
||||
];
|
||||
|
||||
/// The object counts of `document.info` and their labels.
|
||||
const OBJECT_LABELS: [(&str, &str); 17] = [
|
||||
const OBJECT_LABELS: [(&str, &str); 18] = [
|
||||
("paths", "Paths"),
|
||||
("compoundPaths", "Compound Paths"),
|
||||
("groups", "Groups"),
|
||||
@@ -65,6 +65,7 @@ const OBJECT_LABELS: [(&str, &str); 17] = [
|
||||
("blends", "Blends"),
|
||||
("envelopes", "Envelopes"),
|
||||
("repeats", "Repeats"),
|
||||
("intertwines", "Intertwine Groups"),
|
||||
("opacityMasks", "Opacity Masks"),
|
||||
("liveEffects", "Objects with Effects"),
|
||||
("guides", "Guides"),
|
||||
@@ -284,6 +285,7 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
NodeKind::Mesh(_) => Some("meshes"),
|
||||
NodeKind::Repeat(_) => Some("repeats"),
|
||||
NodeKind::PlacedDocument(_) => Some("placedDocuments"),
|
||||
NodeKind::Intertwine { .. } => Some("intertwines"),
|
||||
};
|
||||
if let Some(k) = k {
|
||||
*counts.entry(k).or_default() += 1;
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
//! Object › Intertwine (#1763): objects that pass over and under each other.
|
||||
//!
|
||||
//! `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`).
|
||||
//!
|
||||
//! 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).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
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 super::edit::selected_roots;
|
||||
use super::pathops::Beside;
|
||||
use super::*;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"object.intertwine.make",
|
||||
"Make",
|
||||
["Object", "Intertwine"],
|
||||
None,
|
||||
"{ids?: two or more objects (default: the selection)} put the objects in an Intertwine group and choose the Intertwine tool, which arranges its overlap areas (object.intertwine.arrange) → {id}",
|
||||
has_selection,
|
||||
make
|
||||
),
|
||||
cmd!(
|
||||
"object.intertwine.edit",
|
||||
"Edit",
|
||||
["Object", "Intertwine"],
|
||||
None,
|
||||
"{} choose the Intertwine tool for the selected Intertwine group (Esc or Enter goes back to the tool before)",
|
||||
has_intertwine,
|
||||
edit
|
||||
),
|
||||
cmd!(
|
||||
"object.intertwine.release",
|
||||
"Release",
|
||||
["Object", "Intertwine"],
|
||||
None,
|
||||
"{} give the objects of the selected Intertwine groups back as they were made, dropping the arranged areas → {ids}",
|
||||
has_intertwine,
|
||||
release
|
||||
),
|
||||
cmd!(
|
||||
"object.intertwine.arrange",
|
||||
"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]}",
|
||||
has_intertwine,
|
||||
arrange
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
// ---------- predicates ----------
|
||||
|
||||
/// The Intertwine groups of the selection: selected ones, and those around a selected object.
|
||||
fn selected_groups(s: &Session) -> Vec<NodeId> {
|
||||
let Some(st) = s.active() else { return vec![] };
|
||||
let mut out = vec![];
|
||||
for id in &st.selection.objects {
|
||||
let group = st.doc.ancestry(*id).unwrap_or_default().into_iter().rev().find(|a| st.doc.node(*a).is_some_and(is_intertwine));
|
||||
if let Some(g) = group
|
||||
&& !out.contains(&g)
|
||||
{
|
||||
out.push(g);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn has_intertwine(s: &Session) -> std::result::Result<(), String> {
|
||||
has_selection(s)?;
|
||||
if selected_groups(s).is_empty() { Err("select an Intertwine group".into()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
// ---------- Make / Release ----------
|
||||
|
||||
/// Why `n` can't go into an Intertwine group, if it can't (the help page's limits: no nested
|
||||
/// Intertwine groups, Live Paint, Repeat or graphs).
|
||||
fn refused(d: &Document, n: &Node) -> Option<&'static str> {
|
||||
let around = d.ancestry(n.id).unwrap_or_default();
|
||||
if around.iter().filter(|a| **a != n.id).filter_map(|a| d.node(*a)).any(is_intertwine) {
|
||||
return Some("an object already in an Intertwine group");
|
||||
}
|
||||
let mut why = None;
|
||||
n.walk(&mut |c| {
|
||||
why = why.or(match &c.kind {
|
||||
NodeKind::Intertwine { .. } => Some("an Intertwine group"),
|
||||
NodeKind::Repeat(_) => Some("a Repeat"),
|
||||
NodeKind::Group { .. } if c.extras().graph.is_some() => Some("a graph"),
|
||||
NodeKind::Group { .. } if vectorcraft_tools::builder::is_live_paint(c) => Some("a Live Paint group"),
|
||||
_ => None,
|
||||
});
|
||||
});
|
||||
why
|
||||
}
|
||||
|
||||
fn make(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "object.intertwine.make";
|
||||
let roots = match p.get("ids") {
|
||||
Some(_) => super::edit::roots_of(&s.doc()?.doc, checked_ids_param(s, p, "ids", C)?),
|
||||
None => selected_roots(s)?,
|
||||
};
|
||||
if roots.len() < 2 {
|
||||
return Err(bad(C, "select two or more objects to intertwine"));
|
||||
}
|
||||
{
|
||||
let d = &s.doc()?.doc;
|
||||
for r in &roots {
|
||||
let n = d.node(*r).ok_or(EngineError::NoNode(*r))?;
|
||||
if n.is_layer() {
|
||||
return Err(bad(C, "layers can't be intertwined: select the objects in them"));
|
||||
}
|
||||
if let Some(what) = refused(d, n) {
|
||||
return Err(bad(C, format!("{what} can't be intertwined")));
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = s.edit("Make Intertwine", |d, sel| {
|
||||
let id = d.alloc_id();
|
||||
d.wrap(&roots, |children| Node::new(id, NodeKind::Intertwine { children }))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})?;
|
||||
choose_tool(s)?;
|
||||
Ok(json!({ "id": id.0 }))
|
||||
}
|
||||
|
||||
/// Choose the Intertwine tool, which goes back to the tool chosen now.
|
||||
fn choose_tool(s: &mut Session) -> Result<()> {
|
||||
let prev = Some(s.tool_id()).filter(|t| *t != "intertwine");
|
||||
let view = s.last_view;
|
||||
s.select_tool("intertwine", view)?;
|
||||
s.set_tool_option("prev", &json!(prev));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn edit(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let [g] = selected_groups(s)[..] else { return Err(bad("object.intertwine.edit", "select one Intertwine group")) };
|
||||
s.select(|_, sel| sel.set([g]))?;
|
||||
choose_tool(s)?;
|
||||
Ok(json!({ "id": g.0 }))
|
||||
}
|
||||
|
||||
fn release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let groups = selected_groups(s);
|
||||
let ids = s.edit("Release Intertwine", |d, sel| {
|
||||
let mut out = vec![];
|
||||
let mut released = Beside::replacing(d, &groups);
|
||||
for g in &groups {
|
||||
let Some(n) = d.node(*g).cloned() else { continue };
|
||||
let members: Vec<Node> = n
|
||||
.children()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|c| {
|
||||
let mut c = (**c).clone();
|
||||
c.extras_mut().intertwine = None;
|
||||
c
|
||||
})
|
||||
.collect();
|
||||
out.extend(members.iter().map(|c| c.id));
|
||||
released.put(d, *g, members)?;
|
||||
}
|
||||
released.finish(d)?;
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
})?;
|
||||
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
// ---------- Arrange ----------
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Order {
|
||||
Toggle,
|
||||
Front,
|
||||
Back,
|
||||
}
|
||||
|
||||
/// The most points a drawn `area` may have (a long freehand loop has a few thousand).
|
||||
const MAX_AREA_POINTS: usize = 100_000;
|
||||
|
||||
/// A closed polygon from `[[x,y]…]` (three to [`MAX_AREA_POINTS`] points, finite).
|
||||
fn polygon(v: &Value) -> Option<PathData> {
|
||||
let pts = v.as_array().filter(|a| a.len() <= MAX_AREA_POINTS)?;
|
||||
let pts: Vec<Point> = pts.iter().map(|q| Some(Point::new(q.get(0)?.as_f64()?, q.get(1)?.as_f64()?))).collect::<Option<_>>()?;
|
||||
if pts.len() < 3 || pts.iter().any(|q| !(q.x.is_finite() && q.y.is_finite())) {
|
||||
return None;
|
||||
}
|
||||
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) {
|
||||
(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);
|
||||
}
|
||||
}
|
||||
drawn_order(children, face).last().copied()
|
||||
}
|
||||
|
||||
fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "object.intertwine.arrange";
|
||||
let group = match p.get("id") {
|
||||
Some(v) => {
|
||||
let id = checked_id(s, v, C)?;
|
||||
if !s.doc()?.doc.node(id).is_some_and(is_intertwine) {
|
||||
return Err(bad(C, format!("object {} is not an Intertwine group", id.0)));
|
||||
}
|
||||
id
|
||||
}
|
||||
None => match selected_groups(s).as_slice() {
|
||||
[g] => *g,
|
||||
[] => return Err(bad(C, "select an Intertwine group")),
|
||||
_ => return Err(bad(C, "select one Intertwine group")),
|
||||
},
|
||||
};
|
||||
let at = match p.get("at") {
|
||||
Some(_) => Some(point_param(p, "at").filter(|q| q.x.is_finite() && q.y.is_finite()).ok_or_else(|| bad(C, "at must be [x, y]"))?),
|
||||
None => None,
|
||||
};
|
||||
let lasso = match p.get("area") {
|
||||
Some(v) => Some(polygon(v).ok_or_else(|| bad(C, "area must be a closed polygon of 3 to 100000 [x, y] points"))?),
|
||||
None => None,
|
||||
};
|
||||
if at.is_some() == lasso.is_some() {
|
||||
return Err(bad(C, "give either at (a click) or area (a path drawn around the overlaps)"));
|
||||
}
|
||||
let order = match str_param(p, "order").unwrap_or("toggle") {
|
||||
"toggle" => Order::Toggle,
|
||||
"front" => Order::Front,
|
||||
"back" => Order::Back,
|
||||
o => return Err(bad(C, format!("unknown order `{o}`: toggle, front 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"));
|
||||
}
|
||||
let children: Vec<Arc<Node>> = s.doc()?.doc.node(group).and_then(Node::children).cloned().unwrap_or_default();
|
||||
let member = match member {
|
||||
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 found = match (at, &lasso) {
|
||||
(Some(p), _) => face_at(&all, p).into_iter().collect(),
|
||||
(None, Some(l)) => faces_in(&all, l),
|
||||
(None, None) => vec![],
|
||||
};
|
||||
if found.is_empty() {
|
||||
return Err(bad(
|
||||
C,
|
||||
if at.is_some() {
|
||||
"no overlap there: click where two or more objects of the group overlap"
|
||||
} else {
|
||||
"the path encloses no overlap of the group's objects"
|
||||
},
|
||||
));
|
||||
}
|
||||
if let Some(m) = member
|
||||
&& !found.iter().any(|f| f.members.contains(&m))
|
||||
{
|
||||
return Err(bad(C, "that object is not in the overlap"));
|
||||
}
|
||||
let 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);
|
||||
}
|
||||
}
|
||||
Ok(tops)
|
||||
})?;
|
||||
Ok(json!({ "count": tops.len(), "top": tops }))
|
||||
}
|
||||
@@ -34,6 +34,7 @@ mod halftone;
|
||||
pub mod help;
|
||||
pub mod importmatches;
|
||||
pub(crate) mod inline;
|
||||
mod intertwine;
|
||||
mod layer;
|
||||
mod layerpanel;
|
||||
pub mod library;
|
||||
@@ -250,6 +251,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(shaper::specs());
|
||||
v.extend(brushsym::specs());
|
||||
v.extend(patterncmds::specs());
|
||||
v.extend(intertwine::specs());
|
||||
v.extend(prefscmds::specs());
|
||||
v.extend(distortcmds::specs());
|
||||
v.extend(paint::proxy_specs());
|
||||
|
||||
@@ -1904,6 +1904,8 @@ mod tests_ids_without_selection;
|
||||
#[cfg(test)]
|
||||
mod tests_inline;
|
||||
#[cfg(test)]
|
||||
mod tests_intertwine;
|
||||
#[cfg(test)]
|
||||
mod tests_journal;
|
||||
#[cfg(test)]
|
||||
mod tests_knockout;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
//! Object › Intertwine (#1763), as a tutorial does it: two thick rings that overlap twice are
|
||||
//! made into an Intertwine group, then one overlap is clicked so the rings pass over and under
|
||||
//! each other. Driven through `Session::execute`; the art is checked on screen, in the saved file
|
||||
//! and in the SVG and PDF output.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_render::Rendered;
|
||||
|
||||
use super::tests_clip::{hit, id_of, render, select, session};
|
||||
use super::*;
|
||||
|
||||
/// Ring A (red, centre 35,50) and ring B (blue, centre 65,50), radius 25, 8 pt strokes, no fill:
|
||||
/// B is in front. The rings cross at (50, 30) and (50, 70).
|
||||
fn rings(s: &mut Session) -> (NodeId, NodeId) {
|
||||
let ring = |s: &mut Session, cx: f64, color: &str| {
|
||||
let id = id_of(&s.execute("shape.ellipse", &json!({"x": cx - 25.0, "y": 25, "width": 50, "height": 50})).unwrap());
|
||||
s.execute("paint.setFill", &json!({"ids": [id.0], "none": true})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"ids": [id.0], "color": color})).unwrap();
|
||||
s.execute("stroke.set", &json!({"ids": [id.0], "weight": 8})).unwrap();
|
||||
id
|
||||
};
|
||||
let a = ring(s, 35.0, "#ff0000");
|
||||
let b = ring(s, 65.0, "#0000ff");
|
||||
(a, b)
|
||||
}
|
||||
|
||||
/// The rings made into an Intertwine group: (group, a, b).
|
||||
fn intertwined(s: &mut Session) -> (NodeId, NodeId, NodeId) {
|
||||
let (a, b) = rings(s);
|
||||
select(s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.intertwine.make", &json!({})).unwrap());
|
||||
(g, a, b)
|
||||
}
|
||||
|
||||
fn red(r: &Rendered, x: u32, y: u32) -> bool {
|
||||
let p = r.pixel(x, y);
|
||||
p[3] > 200 && p[0] > 200 && p[2] < 60
|
||||
}
|
||||
|
||||
fn blue(r: &Rendered, x: u32, y: u32) -> bool {
|
||||
let p = r.pixel(x, y);
|
||||
p[3] > 200 && p[2] > 200 && p[0] < 60
|
||||
}
|
||||
|
||||
fn arrange(s: &mut Session, p: Value) -> Value {
|
||||
s.execute("object.intertwine.arrange", &p).unwrap()
|
||||
}
|
||||
|
||||
fn area(s: &Session, id: NodeId) -> Option<vectorcraft_geom::PathData> {
|
||||
s.doc().unwrap().doc.node(id).unwrap().extras().intertwine.as_deref().cloned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_puts_the_objects_in_an_intertwine_group_drawn_as_before() {
|
||||
let mut s = session();
|
||||
let (g, a, b) = intertwined(&mut s);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let n = d.node(g).unwrap();
|
||||
assert!(matches!(n.kind, NodeKind::Intertwine { .. }));
|
||||
assert_eq!(n.kind_label(), "Intertwine");
|
||||
assert_eq!(n.children().unwrap().iter().map(|c| c.id).collect::<Vec<_>>(), [a, b], "the stacking order is kept");
|
||||
assert_eq!(s.doc().unwrap().selection.objects.to_vec(), [g]);
|
||||
let r = render(&s);
|
||||
assert!(blue(&r, 50, 30) && blue(&r, 50, 70), "B is in front at both crossings until an area is arranged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_an_overlap_brings_the_object_below_to_the_front_there_only() {
|
||||
let mut s = session();
|
||||
let (_, a, b) = intertwined(&mut s);
|
||||
let v = arrange(&mut s, json!({"at": [50, 30]}));
|
||||
assert_eq!(v["count"], 1);
|
||||
assert_eq!(v["top"], json!([a.0]));
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 30), "A passes over B at the top crossing");
|
||||
assert!(blue(&r, 50, 70), "and under it at the bottom one");
|
||||
// Elsewhere each ring looks as it did.
|
||||
assert!(red(&r, 10, 50) && blue(&r, 90, 50));
|
||||
assert!(area(&s, a).is_some() && area(&s, b).is_none());
|
||||
// A click there picks the ring drawn on top.
|
||||
assert_eq!(hit(&s, 50.0, 30.0), Some(a));
|
||||
assert_eq!(hit(&s, 50.0, 70.0), Some(b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_the_same_overlap_again_swaps_back() {
|
||||
let mut s = session();
|
||||
let (_, a, b) = intertwined(&mut s);
|
||||
arrange(&mut s, json!({"at": [50, 30]}));
|
||||
let v = arrange(&mut s, json!({"at": [50, 30]}));
|
||||
assert_eq!(v["top"], json!([b.0]));
|
||||
assert!(area(&s, a).is_none() && area(&s, b).is_none(), "no area is left over");
|
||||
assert!(blue(&render(&s), 50, 30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_drawn_around_overlaps_arranges_every_overlap_inside_it() {
|
||||
let mut s = session();
|
||||
let (_, a, _) = intertwined(&mut s);
|
||||
// Around the bottom crossing only.
|
||||
let v = arrange(&mut s, json!({"area": [[40, 60], [60, 60], [60, 80], [40, 80]]}));
|
||||
assert_eq!(v["count"], 1);
|
||||
let r = render(&s);
|
||||
assert!(blue(&r, 50, 30) && red(&r, 50, 70));
|
||||
// Around both: each one swaps.
|
||||
let v = arrange(&mut s, json!({"area": [[40, 20], [60, 20], [60, 80], [40, 80]]}));
|
||||
assert_eq!(v["count"], 2);
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||||
assert!(area(&s, a).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn front_and_back_choose_the_member_where_three_objects_overlap() {
|
||||
let mut s = session();
|
||||
let (a, b) = rings(&mut s);
|
||||
// A green bar across the top crossing, in front of both rings.
|
||||
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(&mut s, &[a, b, c]);
|
||||
s.execute("object.intertwine.make", &json!({})).unwrap();
|
||||
let green = |r: &Rendered| {
|
||||
let p = r.pixel(50, 30);
|
||||
p[1] > 200 && p[0] < 60 && p[2] < 60
|
||||
};
|
||||
assert!(green(&render(&s)));
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "front", "member": a.0}));
|
||||
assert!(red(&render(&s), 50, 30), "A is brought to the front of all three");
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "back", "member": a.0}));
|
||||
assert!(green(&render(&s)), "sent to the back, the others are over it again in their own order");
|
||||
arrange(&mut s, json!({"at": [50, 30], "order": "back"}));
|
||||
assert!(blue(&render(&s), 50, 30), "back without a member sends the top one (C) back: B is on top");
|
||||
let e = s.execute("object.intertwine.arrange", &json!({"at": [50, 30], "order": "front", "member": 999})).unwrap_err();
|
||||
assert!(e.to_string().contains("999"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_gives_the_objects_back_as_they_were() {
|
||||
let mut s = session();
|
||||
let (g, a, b) = intertwined(&mut s);
|
||||
arrange(&mut s, json!({"at": [50, 30]}));
|
||||
let v = s.execute("object.intertwine.release", &json!({})).unwrap();
|
||||
assert_eq!(v["ids"], json!([a.0, b.0]));
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(d.node(g).is_none());
|
||||
assert!(d.node(a).unwrap().extras().intertwine.is_none());
|
||||
assert!(blue(&render(&s), 50, 30));
|
||||
// Undo brings the group back, arranged.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(red(&render(&s), 50, 30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_arranged_areas_move_with_the_group() {
|
||||
let mut s = session();
|
||||
let (g, ..) = intertwined(&mut s);
|
||||
arrange(&mut s, json!({"at": [50, 30]}));
|
||||
select(&mut s, &[g]);
|
||||
s.execute("object.move", &json!({"dx": 0, "dy": 10})).unwrap();
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 40) && blue(&r, 50, 80));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_woven_rings_survive_save_and_export() {
|
||||
let mut s = session();
|
||||
intertwined(&mut s);
|
||||
arrange(&mut s, json!({"at": [50, 30]}));
|
||||
for format in ["vectorcraft", "svg", "pdf"] {
|
||||
let data = s.execute("document.export", &json!({"format": format})).unwrap()["dataBase64"].clone();
|
||||
let mut o = Session::new();
|
||||
o.execute("document.open", &json!({"name": format!("rings.{format}"), "dataBase64": data})).unwrap();
|
||||
let r = render(&o);
|
||||
assert!(red(&r, 50, 30) && blue(&r, 50, 70), "{format}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_cannot_be_intertwined_is_refused_with_a_reason() {
|
||||
let mut s = session();
|
||||
let (a, b) = rings(&mut s);
|
||||
select(&mut s, &[a]);
|
||||
let e = s.execute("object.intertwine.make", &json!({})).unwrap_err();
|
||||
assert!(e.to_string().contains("two or more"), "{e}");
|
||||
select(&mut s, &[a, b]);
|
||||
let g = id_of(&s.execute("object.intertwine.make", &json!({})).unwrap());
|
||||
let c = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 5, "height": 5})).unwrap());
|
||||
let e = s.execute("object.intertwine.make", &json!({"ids": [g.0, c.0]})).unwrap_err();
|
||||
assert!(e.to_string().contains("an Intertwine group can't") && !e.to_string().contains("already"), "{e}");
|
||||
let e = s.execute("object.intertwine.make", &json!({"ids": [a.0, c.0]})).unwrap_err();
|
||||
assert!(e.to_string().contains("already in an Intertwine group"), "{e}");
|
||||
select(&mut s, &[g]);
|
||||
let e = s.execute("object.intertwine.arrange", &json!({"at": [5, 50]})).unwrap_err();
|
||||
assert!(e.to_string().contains("no overlap"), "{e}");
|
||||
let e = s.execute("object.intertwine.arrange", &json!({"at": [50, 30], "area": [[0, 0], [1, 0], [1, 1]]})).unwrap_err();
|
||||
assert!(e.to_string().contains("either"), "{e}");
|
||||
let e = s.execute("object.intertwine.arrange", &json!({"at": [50, 30], "order": "sideways"})).unwrap_err();
|
||||
assert!(e.to_string().contains("sideways"), "{e}");
|
||||
let e = s.execute("object.intertwine.arrange", &json!({"area": [[0, 0], [1, 0]]})).unwrap_err();
|
||||
assert!(e.to_string().contains("polygon"), "{e}");
|
||||
let e = s.execute("object.intertwine.arrange", &json!({"id": c.0, "at": [1, 1]})).unwrap_err();
|
||||
assert!(e.to_string().contains("not an Intertwine group"), "{e}");
|
||||
}
|
||||
|
||||
// ---------- on the canvas ----------
|
||||
|
||||
fn press(s: &mut Session, pts: &[(f64, f64)], shift: bool) -> Vec<UiRequest> {
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||||
let view = ViewInfo::default();
|
||||
let ev = |kind, (x, y): (f64, f64)| {
|
||||
let mut e = PointerEvent::new(kind, x, y);
|
||||
e.mods = Mods { shift, ..Mods::default() };
|
||||
e
|
||||
};
|
||||
let (first, last) = (pts[0], pts[pts.len() - 1]);
|
||||
s.pointer(&ev(PointerKind::Move, first), view).unwrap();
|
||||
s.pointer(&ev(PointerKind::Down, first), view).unwrap();
|
||||
for p in &pts[1..] {
|
||||
s.pointer(&ev(PointerKind::Drag, *p), view).unwrap();
|
||||
}
|
||||
s.pointer(&ev(PointerKind::Up, last), view).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_chooses_the_intertwine_tool_and_a_click_on_an_overlap_arranges_it() {
|
||||
let mut s = session();
|
||||
let view = ViewInfo::default();
|
||||
s.select_tool("ellipse", view).unwrap();
|
||||
intertwined(&mut s);
|
||||
assert_eq!(s.tool_id(), "intertwine");
|
||||
// Hovering the overlap highlights it.
|
||||
s.pointer(&vectorcraft_tools::PointerEvent::new(vectorcraft_tools::PointerKind::Move, 50.0, 30.0), view).unwrap();
|
||||
assert_eq!(s.overlays(view).len(), 1);
|
||||
press(&mut s, &[(50.0, 30.0)], false);
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||||
// A click off any overlap changes nothing (and reports no error).
|
||||
press(&mut s, &[(5.0, 5.0)], false);
|
||||
assert!(red(&render(&s), 50, 30));
|
||||
// Esc goes back to the tool chosen before Make.
|
||||
let back = s.tool_key(vectorcraft_tools::ToolKey::Escape, Default::default(), view).unwrap();
|
||||
assert_eq!(back, [UiRequest::SwitchTool("ellipse".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_around_overlaps_arranges_them_and_shift_drags_a_rectangle() {
|
||||
let mut s = session();
|
||||
intertwined(&mut s);
|
||||
// A loop around the bottom crossing.
|
||||
press(&mut s, &[(40.0, 60.0), (60.0, 60.0), (60.0, 80.0), (40.0, 80.0), (40.0, 61.0)], false);
|
||||
let r = render(&s);
|
||||
assert!(blue(&r, 50, 30) && red(&r, 50, 70));
|
||||
// A rectangle around both.
|
||||
press(&mut s, &[(40.0, 20.0), (50.0, 50.0), (60.0, 80.0)], true);
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_chooses_the_tool_for_the_group_of_a_selected_member() {
|
||||
let mut s = session();
|
||||
let (g, a, _) = intertwined(&mut s);
|
||||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||||
select(&mut s, &[a]);
|
||||
let v = s.execute("object.intertwine.edit", &json!({})).unwrap();
|
||||
assert_eq!(v["id"], g.0);
|
||||
assert_eq!(s.tool_id(), "intertwine");
|
||||
assert_eq!(s.doc().unwrap().selection.objects.to_vec(), [g]);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ const FREEFORM: &str = "freeform gradients are written as their average colour";
|
||||
const STROKES: &str =
|
||||
"strokes a plain line can't draw (inside or outside the path, variable width, gradients, brushes, arrowheads) are written as filled outlines";
|
||||
const IMAGE_ALPHA: &str = "transparent image pixels are written over white";
|
||||
const LIVE: &str = "symbols, blends, envelopes, meshes, repeats and placed documents are written as their art";
|
||||
const LIVE: &str = "symbols, blends, envelopes, meshes, repeats, placed documents and intertwines are written as their art";
|
||||
const UNREADABLE_IMAGE: &str = "an image that can't be decoded or is too large was left out";
|
||||
const ELLIPTICAL: &str = "elliptical radial gradients are written circular";
|
||||
|
||||
@@ -605,7 +605,12 @@ impl<'a> Writer<'a> {
|
||||
self.nest -= 1;
|
||||
}
|
||||
}
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => {
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => {
|
||||
self.warn(LIVE);
|
||||
if self.nest < MAX_NEST {
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
|
||||
@@ -297,7 +297,12 @@ impl<'a> Scene<'a> {
|
||||
}
|
||||
}
|
||||
// Live blends, envelopes, meshes and repeats are written as their evaluated art.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => {
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => {
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
for c in g.children().into_iter().flatten() {
|
||||
self.node(c, false);
|
||||
|
||||
@@ -203,7 +203,12 @@ impl<'a> Scene<'a> {
|
||||
}
|
||||
}
|
||||
// Live blends, envelopes, meshes and repeats are written as their evaluated art.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => {
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => {
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
for c in g.children().into_iter().flatten() {
|
||||
self.node(c, false);
|
||||
|
||||
@@ -1050,7 +1050,12 @@ impl Exporter<'_> {
|
||||
}
|
||||
}
|
||||
// Live blends/envelopes/meshes export their evaluated (expanded) form.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => {
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => {
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
for c in g.children().into_iter().flatten() {
|
||||
self.node(s, c, page, false);
|
||||
|
||||
@@ -1314,9 +1314,12 @@ impl Renderer {
|
||||
self.made_again = outer;
|
||||
}
|
||||
}
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => {
|
||||
self.draw_live_node(ctx, f, n)
|
||||
}
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => self.draw_live_node(ctx, f, n),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,7 +249,12 @@ impl Renderer {
|
||||
match &a.kind {
|
||||
NodeKind::PlacedDocument(p) => self.draw_placed(ctx, f, p),
|
||||
NodeKind::Mesh(m) => self.draw_mesh(ctx, f, a, m, cache),
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Group { .. } | NodeKind::Layer { .. } => {
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::Intertwine { .. }
|
||||
| NodeKind::Group { .. }
|
||||
| NodeKind::Layer { .. } => {
|
||||
let items = self.live_expanded(f.doc, a, cache);
|
||||
// An evaluation the cache did not keep is made again in the next frame.
|
||||
let kept = cache && self.live.expanded.get(&(Arc::as_ptr(a) as usize), self.stamp).is_some_and(|e| Arc::ptr_eq(&e.1, &items));
|
||||
|
||||
@@ -394,9 +394,12 @@ impl Reuse {
|
||||
}
|
||||
}
|
||||
match &n.kind {
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => {
|
||||
(self.turns, self.scales) = (false, false)
|
||||
}
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => (self.turns, self.scales) = (false, false),
|
||||
NodeKind::SymbolInstance { symbol, .. } => {
|
||||
if let Some(s) = doc.symbols.iter().find(|s| s.name == *symbol) {
|
||||
self.scan(doc, &s.art, depth + 1);
|
||||
@@ -1462,7 +1465,12 @@ impl Writer<'_> {
|
||||
(self.xf, self.instance_xf) = saved;
|
||||
}
|
||||
// Live blends/envelopes/meshes export their evaluated (expanded) form.
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => {
|
||||
NodeKind::Blend { .. }
|
||||
| NodeKind::Envelope { .. }
|
||||
| NodeKind::Mesh(_)
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => {
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
self.share_images(&g);
|
||||
self.node_body(&g);
|
||||
|
||||
@@ -13,6 +13,7 @@ vectorcraft-geom = { workspace = true }
|
||||
vectorcraft-color = { workspace = true }
|
||||
vectorcraft-doc = { workspace = true }
|
||||
vectorcraft-pathops = { workspace = true }
|
||||
vectorcraft-effects = { workspace = true }
|
||||
vectorcraft-text = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
//! Object › Intertwine (#1763): the overlap areas of an Intertwine group, and the tool that
|
||||
//! arranges them on the canvas.
|
||||
//!
|
||||
//! An overlap area is a face of the planar arrangement of what the group's members paint (their
|
||||
//! fills and strokes, type outlined; other objects by their bounds): a part where two or more of
|
||||
//! them overlap, split wherever a member's edge crosses it. `object.intertwine.arrange` changes
|
||||
//! 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
|
||||
//! 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>.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use kurbo::BezPath;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::intertwine::over_area;
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind};
|
||||
use vectorcraft_effects as effects;
|
||||
use vectorcraft_geom::{FillRule, PathData, Point, Rect, SubPath, shapes};
|
||||
use vectorcraft_pathops as po;
|
||||
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
/// The area `n` paints, in document coordinates, filled non-zero: its visible fills and stroke
|
||||
/// outlines; type by its outlines; groups by their visible members (clip groups inside their
|
||||
/// clipping path); other objects by their bounds. Effects are left out. `None` when it paints
|
||||
/// nothing.
|
||||
pub fn painted_area(n: &Node) -> Option<PathData> {
|
||||
if !n.visible {
|
||||
return None;
|
||||
}
|
||||
let shape = match &n.kind {
|
||||
NodeKind::Path { guide: true, .. } | NodeKind::Path { clipping: true, .. } => return None,
|
||||
NodeKind::Path { path, rule, .. } => Some((path.clone(), *rule)),
|
||||
NodeKind::Compound { children, rule } => {
|
||||
let subs: Vec<SubPath> = children.iter().filter_map(|c| c.path_data()).flat_map(|p| p.subpaths.iter().cloned()).collect();
|
||||
Some((PathData::new(subs), *rule))
|
||||
}
|
||||
NodeKind::Group { children, clip: true } => {
|
||||
let clip = children.first().and_then(|c| match &c.kind {
|
||||
NodeKind::Path { path, rule, .. } => Some(po::normalize(path, *rule)),
|
||||
_ => c.geometric_bounds().map(shapes::rectangle),
|
||||
})?;
|
||||
let inside = unite(children.iter().skip(1).filter_map(|c| painted_area(c)).collect())?;
|
||||
return nonempty(po::boolean(&inside, FillRule::NonZero, &clip, FillRule::NonZero, po::BoolOp::Intersect));
|
||||
}
|
||||
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } | NodeKind::Intertwine { children } => {
|
||||
return unite(children.iter().filter_map(|c| painted_area(c)).collect());
|
||||
}
|
||||
NodeKind::Text(_) => return effects::outline_text(n).as_ref().and_then(painted_area),
|
||||
_ => None,
|
||||
};
|
||||
let mut parts: Vec<PathData> = vec![];
|
||||
match shape {
|
||||
Some((path, rule)) => {
|
||||
if n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !f.paint.is_none())) {
|
||||
parts.push(po::normalize(&path, rule));
|
||||
}
|
||||
for st in n.appearance.painted_strokes().filter(|st| st.width > 0.0 && st.width.is_finite()) {
|
||||
parts.push(effects::stroke::outline_region(&path, rule, st));
|
||||
}
|
||||
}
|
||||
None => parts.push(shapes::rectangle(n.visual_bounds()?)),
|
||||
}
|
||||
unite(parts)
|
||||
}
|
||||
|
||||
fn unite(parts: Vec<PathData>) -> Option<PathData> {
|
||||
let parts: Vec<PathData> = parts.into_iter().filter(|p| !p.is_empty()).collect();
|
||||
match parts.len() {
|
||||
0 => None,
|
||||
1 => parts.into_iter().next(),
|
||||
_ => nonempty(po::unite_all(&parts.iter().map(|p| (p, FillRule::NonZero)).collect::<Vec<_>>())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Areas smaller than this (pt²) are what a boolean leaves of an area taken from itself.
|
||||
pub const SLIVER: f64 = 1e-3;
|
||||
|
||||
/// `p`, unless it is empty or a sliver.
|
||||
pub fn nonempty(p: PathData) -> Option<PathData> {
|
||||
(!p.is_empty() && po::area(&p, FillRule::NonZero).abs() > SLIVER).then_some(p)
|
||||
}
|
||||
|
||||
/// Does area `a` (filled non-zero) contain `p`?
|
||||
pub fn contains(a: &PathData, p: Point) -> bool {
|
||||
vectorcraft_geom::hit::fill_contains(&a.to_bezpath(), FillRule::NonZero, p)
|
||||
}
|
||||
|
||||
/// One overlap area: its outline, a point inside it, and the members (child indices, bottom
|
||||
/// first) that paint there.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Face {
|
||||
pub path: PathData,
|
||||
pub inside: Point,
|
||||
pub members: Vec<usize>,
|
||||
}
|
||||
|
||||
/// Every overlap area of `children`.
|
||||
pub fn faces(children: &[Arc<Node>]) -> 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();
|
||||
let key = |k: usize| shapes.get(k).map(|s| s.key as usize);
|
||||
po::regions(&shapes)
|
||||
.into_iter()
|
||||
.filter(|r| r.sources.len() >= 2)
|
||||
.filter_map(|r| {
|
||||
let inside = po::interior_point(&r.path)?;
|
||||
Some(Face { members: r.sources.iter().filter_map(|k| key(*k)).collect(), path: r.path, inside })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The overlap area of `faces` under `p`, with `p` as its inside point (a click picks it).
|
||||
pub fn face_at(faces: &[Face], p: Point) -> Option<Face> {
|
||||
faces.iter().find(|f| contains(&f.path, p)).map(|f| Face { inside: p, ..f.clone() })
|
||||
}
|
||||
|
||||
/// The overlap areas of `faces` whose inside `lasso` (a closed path) encloses.
|
||||
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.
|
||||
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 mut order = face.members.clone();
|
||||
order.sort_by_key(|i| (over(i), *i));
|
||||
order
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
selected.iter().find_map(|id| doc.ancestry(*id)?.into_iter().rev().find(|a| doc.node(*a).is_some_and(is_intertwine)))
|
||||
}
|
||||
|
||||
pub fn is_intertwine(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Intertwine { .. })
|
||||
}
|
||||
|
||||
// ---------- the tool ----------
|
||||
|
||||
/// The overlap areas of an Intertwine group, kept for the document revision they were found in.
|
||||
struct Found {
|
||||
revision: (u64, u64),
|
||||
group: NodeId,
|
||||
faces: Arc<Vec<Face>>,
|
||||
}
|
||||
|
||||
/// The Intertwine tool (not in the toolbar: Make and Edit choose it).
|
||||
#[derive(Default)]
|
||||
pub struct IntertwineTool {
|
||||
/// The tool to go back to (Esc, Enter).
|
||||
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.
|
||||
hover: Option<BezPath>,
|
||||
/// A drag's points (document space); the first is where it started.
|
||||
points: Vec<Point>,
|
||||
/// The drag draws a rectangle (Shift at its start).
|
||||
rect: bool,
|
||||
dragging: bool,
|
||||
}
|
||||
|
||||
impl IntertwineTool {
|
||||
fn faces(&mut self, cx: &ToolContext) -> Option<(NodeId, Arc<Vec<Face>>)> {
|
||||
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()));
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
/// The closed path the drag drew.
|
||||
fn lasso(&self) -> Option<PathData> {
|
||||
let (first, last) = (*self.points.first()?, *self.points.last()?);
|
||||
let pts = if self.rect {
|
||||
let r = Rect::from_points(first, last);
|
||||
vec![Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)]
|
||||
} else {
|
||||
self.points.clone()
|
||||
};
|
||||
(pts.len() >= 3).then(|| PathData::single(SubPath::polyline(&pts, true)))
|
||||
}
|
||||
|
||||
fn back(&self) -> Vec<Action> {
|
||||
vec![Action::SwitchTool(self.prev.clone().unwrap_or_else(|| "selection".into()))]
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for IntertwineTool {
|
||||
fn id(&self) -> &'static str {
|
||||
"intertwine"
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
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());
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
self.dragging = true;
|
||||
self.rect = ev.mods.shift;
|
||||
self.points = vec![p];
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag if self.dragging => {
|
||||
if self.points.last().is_none_or(|q| q.distance(p) >= cx.tol(2.0)) {
|
||||
self.points.push(p);
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Up if self.dragging => {
|
||||
self.dragging = false;
|
||||
let start = self.points.first().copied().unwrap_or(p);
|
||||
self.points.push(p);
|
||||
let Some((g, faces)) = self.faces(cx) else { return vec![] };
|
||||
// A drag that never left the press is a click (a closed loop ends where it began).
|
||||
let click = self.points.iter().all(|q| q.distance(start) < cx.tol(3.0));
|
||||
let lasso = if click { None } else { self.lasso() };
|
||||
self.points.clear();
|
||||
let params = match lasso {
|
||||
// Only where there is something to arrange: a miss changes nothing.
|
||||
Some(l) if !faces_in(&faces, &l).is_empty() => {
|
||||
let area: Vec<Value> = l.subpaths.iter().flat_map(|s| s.anchors.iter()).map(|a| json!([a.p.x, a.p.y])).collect();
|
||||
json!({ "id": g.0, "area": area })
|
||||
}
|
||||
None if face_at(&faces, p).is_some() => json!({ "id": g.0, "at": [p.x, p.y] }),
|
||||
_ => return vec![],
|
||||
};
|
||||
self.hover = None;
|
||||
vec![Action::Exec("object.intertwine.arrange".into(), params)]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
match key {
|
||||
ToolKey::Escape | ToolKey::Enter if !self.dragging => self.back(),
|
||||
ToolKey::Escape => {
|
||||
self.dragging = false;
|
||||
self.points.clear();
|
||||
vec![]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
|
||||
matches!(key, ToolKey::Escape | ToolKey::Enter)
|
||||
}
|
||||
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
let color = crate::builder::HIGHLIGHT_RED;
|
||||
let mut out = vec![];
|
||||
if self.dragging && self.points.len() > 1 {
|
||||
if let Some(l) = self.lasso() {
|
||||
out.push(Overlay::Path { path: l.to_bezpath(), color, width: 1.0, dashed: true });
|
||||
}
|
||||
} else if let Some(h) = &self.hover {
|
||||
out.push(Overlay::Path { path: h.clone(), color, width: 2.0, dashed: false });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _mods: Mods) -> Cursor {
|
||||
Cursor::Arrow
|
||||
}
|
||||
|
||||
fn options(&self) -> Value {
|
||||
json!({ "prev": self.prev })
|
||||
}
|
||||
|
||||
fn set_option(&mut self, key: &str, value: &Value) {
|
||||
if key == "prev" {
|
||||
self.prev = value.as_str().filter(|t| *t != "intertwine").map(str::to_string);
|
||||
}
|
||||
}
|
||||
|
||||
fn busy(&self) -> bool {
|
||||
self.dragging
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> Option<Box<dyn Tool>> {
|
||||
(id == "intertwine").then(|| Box::new(IntertwineTool::default()) as Box<dyn Tool>)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use vectorcraft_doc::{Appearance, NodeId};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn square(id: u64, x: f64) -> Arc<Node> {
|
||||
Arc::new(Node::path(NodeId(id), shapes::rectangle(Rect::new(x, 0.0, x + 10.0, 10.0)), Appearance::default_art()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_overlapping_squares_have_one_overlap_area() {
|
||||
let kids = vec![square(1, 0.0), square(2, 5.0), square(3, 40.0)];
|
||||
let f = faces(&kids);
|
||||
assert_eq!(f.len(), 1);
|
||||
assert_eq!(f[0].members, [0, 1]);
|
||||
let at = face_at(&f, Point::new(7.0, 5.0)).unwrap();
|
||||
assert_eq!(at.inside, Point::new(7.0, 5.0));
|
||||
assert!(face_at(&f, Point::new(2.0, 5.0)).is_none(), "one square alone is no overlap");
|
||||
assert_eq!(drawn_order(&kids, &at), [0, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_member_with_an_area_there_is_drawn_last() {
|
||||
let mut a = (*square(1, 0.0)).clone();
|
||||
a.extras_mut().intertwine = Some(Box::new(shapes::rectangle(Rect::new(5.0, 0.0, 10.0, 10.0))));
|
||||
let kids = vec![Arc::new(a), square(2, 5.0)];
|
||||
let f = faces(&kids);
|
||||
assert_eq!(drawn_order(&kids, &f[0]), [1, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lasso_takes_the_areas_whose_inside_it_encloses() {
|
||||
let kids = vec![square(1, 0.0), square(2, 5.0)];
|
||||
let f = faces(&kids);
|
||||
let around = |r: Rect| shapes::rectangle(r);
|
||||
assert_eq!(faces_in(&f, &around(Rect::new(4.0, -1.0, 11.0, 11.0))).len(), 1);
|
||||
assert!(faces_in(&f, &around(Rect::new(20.0, 0.0, 30.0, 10.0))).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ pub mod distort;
|
||||
pub mod draw2;
|
||||
pub mod extra;
|
||||
pub mod guides;
|
||||
pub mod intertwine;
|
||||
pub mod meshblend;
|
||||
pub mod meshedit;
|
||||
pub mod params;
|
||||
@@ -622,6 +623,7 @@ pub fn create(id: &str) -> Box<dyn Tool> {
|
||||
.or_else(|| printtiling::create(other))
|
||||
.or_else(|| cropimage::create(other))
|
||||
.or_else(|| cut::create(other))
|
||||
.or_else(|| intertwine::create(other))
|
||||
.unwrap_or_else(|| Box::new(NoopTool(tool_info(other).map(|t| t.id).unwrap_or("selection")))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1351,6 +1351,8 @@
|
||||
Internal CSS Вътрешен CSS
|
||||
Internet Standard RGB (sRGB) Интернет стандартен RGB (sRGB)
|
||||
Intersect Пресечение
|
||||
Intertwine Преплитане
|
||||
Arrange Intertwine Area Подреждане на зоната на преплитане
|
||||
Inverse Инвертиране
|
||||
Invert Инвертиране
|
||||
Invert Colors Инвертиране на цветовете
|
||||
@@ -3829,6 +3831,7 @@ axis Both И двете
|
||||
@msg select a gradient mesh изберете мрежа на градиент (gradient mesh)
|
||||
@msg select a linked image: an embedded one has no file изберете свързано изображение: вграденото няма файл
|
||||
@msg select a repeat изберете повторение (repeat)
|
||||
@msg select an Intertwine group изберете група за преплитане (Intertwine)
|
||||
@msg select a stroke with a variable width изберете щрих с променлива дебелина
|
||||
@msg select a symbol instance изберете екземпляр на символ
|
||||
@msg select an Image Trace object изберете обект за трасиране на изображение (Image Trace)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -425,6 +425,8 @@
|
||||
Add Přidat
|
||||
@id pathfinder.add Sjednotit
|
||||
Intersect Protnout
|
||||
Intertwine Proplést
|
||||
Arrange Intertwine Area Uspořádat oblast propletení
|
||||
Exclude Vyloučit
|
||||
Subtract Odečíst
|
||||
Minus Back Odečíst zadní
|
||||
@@ -4197,6 +4199,7 @@ imageTrace Outline Obrys
|
||||
@msg select a gradient mesh vyberte přechodovou síť
|
||||
@msg select a linked image: an embedded one has no file vyberte propojený obrázek: vložený nemá soubor
|
||||
@msg select a repeat vyberte opakování
|
||||
@msg select an Intertwine group vyberte skupinu propletení
|
||||
@msg select a stroke with a variable width vyberte tah s proměnnou šířkou
|
||||
@msg select a symbol instance vyberte instanci symbolu
|
||||
@msg select an Image Trace object vyberte objekt vektorizace obrazu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1357,6 +1357,8 @@
|
||||
Internal CSS Internes CSS
|
||||
Internet Standard RGB (sRGB) Internet-Standard-RGB (sRGB)
|
||||
Intersect Schnittmenge bilden
|
||||
Intertwine Verflechten
|
||||
Arrange Intertwine Area Überlappungsbereich verflechten
|
||||
Inverse Auswahl umkehren
|
||||
Invert Umkehren
|
||||
Invert Colors Farben umkehren
|
||||
@@ -3806,6 +3808,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg select a gradient mesh wählen Sie ein Verlaufsgitter aus
|
||||
@msg select a linked image: an embedded one has no file wählen Sie ein verknüpftes Bild aus: Ein eingebettetes hat keine Datei
|
||||
@msg select a repeat wählen Sie eine Wiederholung aus
|
||||
@msg select an Intertwine group wählen Sie eine Verflechtungsgruppe aus
|
||||
@msg select a stroke with a variable width wählen Sie eine Kontur mit variabler Breite aus
|
||||
@msg select a symbol instance wählen Sie eine Symbolinstanz aus
|
||||
@msg select an Image Trace object wählen Sie ein Bildnachzeichner-Objekt aus
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1375,6 +1375,8 @@
|
||||
Internal CSS CSS interno
|
||||
Internet Standard RGB (sRGB) RGB estándar de Internet (sRGB)
|
||||
Intersect Intersecar
|
||||
Intertwine Entrelazar
|
||||
Arrange Intertwine Area Organizar área entrelazada
|
||||
Inverse Inversa
|
||||
Invert Invertir
|
||||
Invert Colors Invertir colores
|
||||
@@ -3902,6 +3904,7 @@ axis Both Ambos
|
||||
@msg select a gradient mesh selecciona una malla de degradado
|
||||
@msg select a linked image: an embedded one has no file selecciona una imagen vinculada: una incrustada no tiene archivo
|
||||
@msg select a repeat selecciona una repetición
|
||||
@msg select an Intertwine group selecciona un grupo entrelazado
|
||||
@msg select a stroke with a variable width selecciona un trazo con anchura variable
|
||||
@msg select a symbol instance selecciona una instancia de símbolo
|
||||
@msg select an Image Trace object selecciona un objeto de calco de imagen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1374,6 +1374,8 @@
|
||||
Internal CSS CSS interne
|
||||
Internet Standard RGB (sRGB) RVB standard Internet (sRGB)
|
||||
Intersect Intersection
|
||||
Intertwine Entrelacer
|
||||
Arrange Intertwine Area Disposer la zone entrelacée
|
||||
Inverse Inverser la sélection
|
||||
Invert Inverser
|
||||
Invert Colors Inverser les couleurs
|
||||
@@ -3899,6 +3901,7 @@ axis Both Les deux
|
||||
@msg select a gradient mesh sélectionnez un filet de dégradé
|
||||
@msg select a linked image: an embedded one has no file sélectionnez une image liée : une image incorporée n’a pas de fichier
|
||||
@msg select a repeat sélectionnez une répétition
|
||||
@msg select an Intertwine group sélectionnez un groupe entrelacé
|
||||
@msg select a stroke with a variable width sélectionnez un contour de largeur variable
|
||||
@msg select a symbol instance sélectionnez une instance de symbole
|
||||
@msg select an Image Trace object sélectionnez un objet de vectorisation de l’image
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1746,6 +1746,8 @@
|
||||
Internet Standard RGB (sRGB) Internetes szabványos RGB (sRGB)
|
||||
Interpolation Interpoláció
|
||||
Intersect Metszet
|
||||
Intertwine Összefonás
|
||||
Arrange Intertwine Area Összefonási terület elrendezése
|
||||
Inverse Kijelölés megfordítása
|
||||
Invert Invertálás
|
||||
Invert Colors Színek invertálása
|
||||
@@ -4549,6 +4551,7 @@ imageTrace Outline Körvonal
|
||||
@msg select a linked image: an embedded one has no file jelölj ki egy csatolt képet: a beágyazottnak nincs fájlja
|
||||
@msg select a Live Paint group jelölj ki egy élőfestés-csoportot
|
||||
@msg select a repeat jelölj ki egy ismétlést
|
||||
@msg select an Intertwine group jelölj ki egy összefonási csoportot
|
||||
@msg select a stroke with a variable width jelölj ki egy változó szélességű körvonalat
|
||||
@msg select a symbol instance jelölj ki egy szimbólumpéldányt
|
||||
@msg select an envelope jelölj ki egy burkolót
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1375,6 +1375,8 @@
|
||||
Internal CSS CSS interno
|
||||
Internet Standard RGB (sRGB) RGB standard Internet (sRGB)
|
||||
Intersect Intersezione
|
||||
Intertwine Intreccia
|
||||
Arrange Intertwine Area Disponi area intrecciata
|
||||
Inverse Inversa
|
||||
Invert Inverti
|
||||
Invert Colors Inverti colori
|
||||
@@ -3905,6 +3907,7 @@ axis Both Entrambi
|
||||
@msg select a gradient mesh seleziona una trama sfumatura
|
||||
@msg select a linked image: an embedded one has no file seleziona un'immagine collegata: un'immagine incorporata non ha un file
|
||||
@msg select a repeat seleziona una ripetizione
|
||||
@msg select an Intertwine group seleziona un gruppo intrecciato
|
||||
@msg select a stroke with a variable width seleziona una traccia con larghezza variabile
|
||||
@msg select a symbol instance seleziona un'istanza di simbolo
|
||||
@msg select an Image Trace object seleziona un oggetto di ricalco immagine
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -426,6 +426,8 @@
|
||||
Pathfinder パスファインダー
|
||||
Add 追加
|
||||
Intersect 交差
|
||||
Intertwine 織り交ぜ
|
||||
Arrange Intertwine Area 織り交ぜ領域の重ね順
|
||||
Exclude 中マド
|
||||
Subtract 前面オブジェクトで型抜き
|
||||
Minus Back 背面オブジェクトで型抜き
|
||||
@@ -3750,6 +3752,7 @@ theme Appearance 外観
|
||||
@msg not in pattern editing mode パターン編集モードではありません
|
||||
@msg pattern missing パターンが見つかりません
|
||||
@msg select a repeat リピートを選択してください
|
||||
@msg select an Intertwine group 織り交ぜグループを選択してください
|
||||
@msg select the art to repeat リピートするアートを選択してください
|
||||
@msg the document has no patterns ドキュメントにパターンがありません
|
||||
@msg no artboard アートボードがありません
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -422,6 +422,8 @@
|
||||
Pathfinder 패스파인더
|
||||
Add 더하기
|
||||
Intersect 교차
|
||||
Intertwine 교차
|
||||
Arrange Intertwine Area 교차 영역 정렬
|
||||
Exclude 제외
|
||||
Subtract 빼기
|
||||
Minus Back 뒤 개체 빼기
|
||||
@@ -3728,6 +3730,7 @@ theme Appearance 외관
|
||||
@msg not in pattern editing mode 패턴 편집 모드가 아닙니다
|
||||
@msg pattern missing 패턴이 없습니다
|
||||
@msg select a repeat 반복을 선택하세요
|
||||
@msg select an Intertwine group 교차 그룹을 선택하세요
|
||||
@msg select the art to repeat 반복할 아트웍을 선택하세요
|
||||
@msg the document has no patterns 문서에 패턴이 없습니다
|
||||
@msg no artboard 아트보드가 없습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -385,6 +385,8 @@
|
||||
Add Dodaj
|
||||
@id pathfinder.add Połącz
|
||||
Intersect Przetnij
|
||||
Intertwine Przeplatanie
|
||||
Arrange Intertwine Area Ułóż obszar przeplotu
|
||||
Exclude Wyklucz
|
||||
Subtract Odejmij
|
||||
Minus Back Odejmij tył
|
||||
@@ -4014,6 +4016,7 @@ imageTrace Outline Kontur
|
||||
@msg select a gradient mesh zaznacz siatkę gradientową
|
||||
@msg select a linked image: an embedded one has no file zaznacz obraz połączony: osadzony nie ma pliku
|
||||
@msg select a repeat zaznacz powtórzenie
|
||||
@msg select an Intertwine group zaznacz grupę przeplotu
|
||||
@msg select a stroke with a variable width zaznacz obrys o zmiennej szerokości
|
||||
@msg select a symbol instance zaznacz wystąpienie symbolu
|
||||
@msg select an Image Trace object zaznacz obiekt obrysu obrazu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1175,6 +1175,8 @@
|
||||
Interlaced Entrelaçado
|
||||
Internet Standard RGB (sRGB) RGB Padrão da Internet (sRGB)
|
||||
Intersect Interseção
|
||||
Intertwine Entrelaçar
|
||||
Arrange Intertwine Area Organizar área entrelaçada
|
||||
Inverse Inverso
|
||||
Invert Inverter
|
||||
Invert Colors Inverter Cores
|
||||
@@ -3496,6 +3498,7 @@ axis Both Ambos
|
||||
@msg select a gradient mesh selecione uma malha de gradiente
|
||||
@msg select a linked image: an embedded one has no file selecione uma imagem vinculada: uma incorporada não tem arquivo
|
||||
@msg select a repeat selecione uma repetição
|
||||
@msg select an Intertwine group selecione um grupo entrelaçado
|
||||
@msg select a stroke with a variable width selecione um traço com largura variável
|
||||
@msg select a symbol instance selecione uma instância de símbolo
|
||||
@msg select an Image Trace object selecione um objeto de Vetorização de imagem
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1373,6 +1373,8 @@
|
||||
Internal CSS Внутренний CSS
|
||||
Internet Standard RGB (sRGB) Стандартный интернет-RGB (sRGB)
|
||||
Intersect Пересечение
|
||||
Intertwine Переплетение
|
||||
Arrange Intertwine Area Упорядочить область переплетения
|
||||
Inverse Обратный
|
||||
Invert Инвертировать
|
||||
Invert Colors Инвертировать цвета
|
||||
@@ -3901,6 +3903,7 @@ axis Both Оба
|
||||
@msg select a gradient mesh выделите градиентную сетку
|
||||
@msg select a linked image: an embedded one has no file выделите связанное изображение: у внедрённого нет файла
|
||||
@msg select a repeat выделите повтор
|
||||
@msg select an Intertwine group выделите группу переплетения
|
||||
@msg select a stroke with a variable width выделите обводку с переменной шириной
|
||||
@msg select a symbol instance выделите экземпляр символа
|
||||
@msg select an Image Trace object выделите объект трассировки изображения
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1369,6 +1369,8 @@
|
||||
Internal CSS Внутрішній CSS
|
||||
Internet Standard RGB (sRGB) Стандартний інтернет-RGB (sRGB)
|
||||
Intersect Перетин
|
||||
Intertwine Переплетення
|
||||
Arrange Intertwine Area Упорядкувати ділянку переплетення
|
||||
Inverse Обернене
|
||||
Invert Інвертувати
|
||||
Invert Colors Інвертувати кольори
|
||||
@@ -3889,6 +3891,7 @@ axis Both Обидві
|
||||
@msg select a gradient mesh виділіть градієнтну сітку
|
||||
@msg select a linked image: an embedded one has no file виділіть пов’язане зображення: у вбудованого немає файлу
|
||||
@msg select a repeat виділіть повтор
|
||||
@msg select an Intertwine group виділіть групу переплетення
|
||||
@msg select a stroke with a variable width виділіть обведення зі змінною шириною
|
||||
@msg select a symbol instance виділіть екземпляр символу
|
||||
@msg select an Image Trace object виділіть об’єкт трасування зображення
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 718 and column 40.
|
@@ -1608,6 +1608,8 @@
|
||||
Internet Standard RGB (sRGB) RGB chuẩn Internet (sRGB)
|
||||
Interpolation Nội suy
|
||||
Intersect Giao nhau
|
||||
Intertwine Đan xen
|
||||
Arrange Intertwine Area Sắp xếp vùng đan xen
|
||||
Inverse Đảo ngược
|
||||
Invert Đảo ngược
|
||||
Invert Colors Đảo màu
|
||||
@@ -4182,6 +4184,7 @@ theme Appearance Giao diện
|
||||
@msg select a linked image: an embedded one has no file hãy chọn ảnh liên kết: ảnh nhúng không có tệp
|
||||
@msg select a Live Paint group hãy chọn nhóm tô trực tiếp
|
||||
@msg select a repeat hãy chọn một đối tượng lặp
|
||||
@msg select an Intertwine group hãy chọn một nhóm đan xen
|
||||
@msg select a stroke with a variable width hãy chọn nét có chiều rộng biến đổi
|
||||
@msg select a symbol instance hãy chọn đối tượng ký hiệu
|
||||
@msg select an envelope hãy chọn một Envelope
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3726 and column 10.
|
@@ -1316,6 +1316,8 @@
|
||||
Internal CSS 内部 CSS
|
||||
Internet Standard RGB (sRGB) 互联网标准 RGB (sRGB)
|
||||
Intersect 交集
|
||||
Intertwine 交织
|
||||
Arrange Intertwine Area 排列交织区域
|
||||
Inverse 反向
|
||||
Invert 反相
|
||||
Invert Colors 反相颜色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1346,6 +1346,8 @@
|
||||
Internal CSS 內部 CSS
|
||||
Internet Standard RGB (sRGB) 網際網路標準 RGB (sRGB)
|
||||
Intersect 交集
|
||||
Intertwine 交織
|
||||
Arrange Intertwine Area 排列交織區域
|
||||
Inverse 反轉
|
||||
Invert 反轉
|
||||
Invert Colors 反轉顏色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2377,6 +2377,10 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
c("Options…", "object.repeat.options"),
|
||||
],
|
||||
),
|
||||
sub(
|
||||
"Intertwine",
|
||||
vec![c("Make", "object.intertwine.make"), c("Edit", "object.intertwine.edit"), c("Release", "object.intertwine.release")],
|
||||
),
|
||||
sub(
|
||||
"Blend",
|
||||
vec![
|
||||
|
||||
+9
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user