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:
BFlat
2026-10-16 05:10:43 +07:00
committed by GitHub
41 changed files with 1244 additions and 27 deletions
Generated
+1
View File
@@ -4930,6 +4930,7 @@ dependencies = [
"serde_json",
"vectorcraft-color",
"vectorcraft-doc",
"vectorcraft-effects",
"vectorcraft-geom",
"vectorcraft-pathops",
"vectorcraft-text",
+6 -1
View File
@@ -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
View File
@@ -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.
+98
View File
@@ -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);
}
}
+1
View File
@@ -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
View File
@@ -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()],
}
}
+5 -1
View File
@@ -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
View File
@@ -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 {
+3 -1
View File
@@ -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;
+333
View File
@@ -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 }))
}
+2
View File
@@ -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());
+2
View File
@@ -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;
+270
View File
@@ -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]);
}
+7 -2
View File
@@ -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);
+6 -1
View File
@@ -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);
+6 -1
View File
@@ -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);
+6 -1
View File
@@ -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);
+6 -3
View File
@@ -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),
}
}
+6 -1
View File
@@ -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));
+12 -4
View File
@@ -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);
+1
View File
@@ -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 }
+343
View File
@@ -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());
}
}
+2
View File
@@ -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")))),
}
}
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+3
View File
@@ -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.
+2
View File
@@ -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.
+2
View File
@@ -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.
+4
View File
@@ -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
View File
File diff suppressed because one or more lines are too long