G13: Object › Intertwine: Make, Release and arranging overlap areas (#1763)

An Intertwine group keeps its objects as they are, in their own stacking
order; each member can be drawn over the others in an area of its own.
The group draws, hits, saves and exports through its live expansion, so the
members stay editable and Release gives them back unchanged.

object.intertwine.arrange picks an overlap area by a click point or by a
closed path drawn around overlaps and toggles which member is on top
there, or brings a member to the front or sends it to the back where three
or more objects overlap.
This commit is contained in:
Gert Pellin
2026-10-16 04:44:52 +07:00
committed by bflatastic
parent 20325b1a89
commit 74c391c7ba
18 changed files with 838 additions and 23 deletions
+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);
+24 -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,24 @@ 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.
fn hit_order(parent: &Node, children: &[std::sync::Arc<Node>], p: Point) -> Vec<usize> {
let mut order: Vec<usize> = (0..children.len()).rev().collect();
if matches!(parent.kind, NodeKind::Intertwine { .. }) {
let over = |i: &usize| {
children
.get(*i)
.and_then(|c| crate::intertwine::over_area(c))
.is_some_and(|a| fill_contains(&a.to_bezpath(), vectorcraft_geom::FillRule::NonZero, p))
};
// A stable sort: the members drawn over the others keep their own order among them.
order.sort_by_key(|i| !over(i));
}
order
}
/// 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;
+405
View File
@@ -0,0 +1,405 @@
//! 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::intertwine::over_area;
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind};
use vectorcraft_geom::{FillRule, PathData, Point, SubPath, shapes};
use vectorcraft_pathops as po;
use vectorcraft_render::effects;
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, where overlap areas can be arranged with object.intertwine.arrange → {id}",
has_selection,
make
),
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: back moves 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 is_intertwine(n: &Node) -> bool {
matches!(n.kind, NodeKind::Intertwine { .. })
}
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> {
if d.ancestry(n.id).unwrap_or_default().iter().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)
})?;
Ok(json!({ "id": id.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 area `n` paints, in document coordinates, filled non-zero: its visible fills and stroke
/// outlines; type by its outlines; groups by their visible members; other objects by their
/// bounds. Effects are left out. `None` when it paints nothing.
pub(crate) fn painted_area(n: &Node) -> Option<PathData> {
if !n.visible {
return None;
}
let mut parts: Vec<PathData> = vec![];
let shape = match &n.kind {
NodeKind::Path { path, rule, guide: false, clipping: false, .. } => 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::Path { .. } => return None,
NodeKind::Group { children, clip: true } => {
// What the members paint inside the clipping path.
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,
};
match shape {
Some((path, rule)) => {
let filled = n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !f.paint.is_none()));
if filled {
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.
const SLIVER: f64 = 1e-3;
fn nonempty(p: PathData) -> Option<PathData> {
(!p.is_empty() && po::area(&p, FillRule::NonZero).abs() > SLIVER).then_some(p)
}
/// A closed polygon from `[[x,y]…]` (three points or more, finite).
fn polygon(v: &Value) -> Option<PathData> {
let pts: Vec<Point> = v.as_array()?.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)))
}
fn contains(area: &PathData, p: Point) -> bool {
vectorcraft_geom::hit::fill_contains(&area.to_bezpath(), FillRule::NonZero, p)
}
/// One overlap area to arrange: its outline, a point inside it, and the members (child indices,
/// bottom first) that paint there.
struct Face {
path: PathData,
inside: Point,
members: Vec<usize>,
}
/// The overlap areas of `children` that `at` lies in, or that `lasso` encloses.
fn faces(children: &[Arc<Node>], at: Option<Point>, lasso: Option<&PathData>) -> Vec<Face> {
let shapes: Vec<po::Shape> =
children.iter().enumerate().filter_map(|(i, c)| Some(po::Shape::new(painted_area(c)?, FillRule::NonZero, i as u64))).collect();
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 = match at {
Some(p) => r.contains(p).then_some(p)?,
None => po::interior_point(&r.path)?,
};
if lasso.is_some_and(|l| !contains(l, inside)) {
return None;
}
Some(Face { members: r.sources.iter().filter_map(|k| key(*k)).collect(), path: r.path, inside })
})
.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.
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
}
/// `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 three or more [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 found = faces(&children, at, lasso.as_ref());
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.last().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;
+204
View File
@@ -0,0 +1,204 @@
//! 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.iter().copied().collect::<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("Intertwine group can't"), "{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}");
}
+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);