mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Part of #1429: path, Pathfinder and live-object commands add and remove many objects in one pass
Offset Path, Outline Stroke, the Pathfinder panel, Clean Up, Make Blend, Rasterize and the other commands that #1429's table marks 3, except Split Into Grid, added, removed or replaced their objects one at a time, and so did Remove Anchor Points, Redefine Symbol and the removal of Shaper groups whose source art is gone. Each insertion or removal moved the later objects of the layer or group. The lookups by id that followed walked the tree, and every fourth walk rebuilt the lookup cache. On 25,000 rectangles a release build of vectorcraft-cli took 29.1 s for Offset Path. These commands now make their edits with Document::wrap, Document::insert_beside and Document::remove_many, in one pass over the children of each layer or group, and Outline Stroke and Expand write each new object into the place of the object it replaces. A list that holds an object twice, or an object and another inside it, and that was edited without an error before, is still edited one object at a time. The documents, selections and undo steps are the same. On the same rectangles Offset Path now takes 1.3 s. Docs: docs/target-app-parity.md.
This commit is contained in:
1 parent
6f9e2d5c16
commit
88bd4812e0
20 files changed
+420
-132
No files matched your search
@@ -15,6 +15,7 @@ use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Node, NodeId, NodeKin
|
||||
use vectorcraft_geom::{Affine, Point, Vec2};
|
||||
|
||||
use super::edit::selected_roots;
|
||||
use super::pathops::{remove_each, separate};
|
||||
use super::*;
|
||||
use crate::EngineError;
|
||||
|
||||
@@ -569,6 +570,10 @@ fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
/// Replace the root nodes `ids` with one instance of `name` (art already stored). Returns the id.
|
||||
fn replace_with_instance(d: &mut Document, ids: &[NodeId], name: &str, xf: Affine) -> Result<NodeId> {
|
||||
let top = *ids.last().ok_or_else(|| EngineError::Other("nothing selected".into()))?;
|
||||
if separate(d, ids) {
|
||||
let id = d.alloc_id();
|
||||
return Ok(d.wrap(ids, |_| Node::new(id, NodeKind::SymbolInstance { symbol: name.into(), xf }))?);
|
||||
}
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
let id = d.alloc_id();
|
||||
d.insert(par, idx + 1, Node::new(id, NodeKind::SymbolInstance { symbol: name.into(), xf }))?;
|
||||
@@ -742,9 +747,7 @@ fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if expand {
|
||||
break_links(d, &inst)?;
|
||||
} else {
|
||||
for i in inst {
|
||||
d.remove(i)?;
|
||||
}
|
||||
remove_each(d, &inst)?;
|
||||
}
|
||||
d.symbols.retain(|x| x.name != name);
|
||||
set_natural_size(d, &name, None);
|
||||
@@ -871,12 +874,7 @@ fn symbol_spray(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.collect();
|
||||
let n = doomed.len();
|
||||
if n > 0 {
|
||||
s.edit("Symbol Sprayer", |d, _| {
|
||||
for id in &doomed {
|
||||
d.remove(*id)?;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
s.edit("Symbol Sprayer", |d, _| remove_each(d, &doomed))?;
|
||||
}
|
||||
return Ok(json!({ "count": n }));
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use vectorcraft_trace as tr;
|
||||
|
||||
use super::edit::selected_roots;
|
||||
use super::paint::paint_from;
|
||||
use super::pathops::shape_node;
|
||||
use super::pathops::{Beside, shape_node};
|
||||
use super::*;
|
||||
|
||||
/// Name of an Image Trace group.
|
||||
@@ -341,19 +341,16 @@ fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// Remove the roots and insert `nodes` where the front-most root was. Returns the new ids.
|
||||
/// (Roots go first: the new nodes may reuse their ids, e.g. Live Paint sources.)
|
||||
fn replace_roots(d: &mut Document, roots: &[NodeId], nodes: Vec<Node>) -> Result<Vec<NodeId>> {
|
||||
let top = *roots.last().ok_or_else(|| EngineError::Other("nothing selected".into()))?;
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
let before = roots.iter().filter(|r| **r != top && d.position(**r).is_some_and(|(p, i, _)| p == par && i < idx)).count();
|
||||
for r in roots {
|
||||
let Some((&top, rest)) = roots.split_last() else { return Err(EngineError::Other("nothing selected".into())) };
|
||||
let ids = nodes.iter().map(|n| n.id).collect();
|
||||
let mut replaced = Beside::replacing(d, roots);
|
||||
for r in rest {
|
||||
if d.node(*r).is_some() {
|
||||
d.remove(*r)?;
|
||||
replaced.put(d, *r, vec![])?;
|
||||
}
|
||||
}
|
||||
let at = idx - before;
|
||||
let mut ids = vec![];
|
||||
for (k, n) in nodes.into_iter().enumerate() {
|
||||
ids.push(d.insert(par, at + k, n)?);
|
||||
}
|
||||
replaced.put(d, top, nodes)?;
|
||||
replaced.finish(d)?;
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
@@ -508,14 +505,14 @@ fn lp_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Arc::make_mut(e).appearance = app.into();
|
||||
}
|
||||
}
|
||||
let mut merged = Beside::replacing(d, &roots);
|
||||
for r in roots.iter().filter(|r| **r != target) {
|
||||
if d.node(*r).is_some() {
|
||||
d.remove(*r)?;
|
||||
merged.put(d, *r, vec![])?;
|
||||
}
|
||||
}
|
||||
let (par, idx, _) = d.position(target).ok_or(EngineError::NoNode(target))?;
|
||||
d.remove(target)?;
|
||||
d.insert(par, idx, g)?;
|
||||
merged.put(d, target, vec![g])?;
|
||||
merged.finish(d)?;
|
||||
sel.set([target]);
|
||||
Ok((target, nf, ne))
|
||||
})?;
|
||||
@@ -536,11 +533,14 @@ fn lp_release(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let targets = live_targets(s, p)?;
|
||||
let ids = s.edit("Release Live Paint", |d, sel| {
|
||||
let mut out = vec![];
|
||||
let mut released = Beside::replacing(d, &targets);
|
||||
for g in &targets {
|
||||
let n = d.node(*g).cloned().ok_or(EngineError::NoNode(*g))?;
|
||||
let srcs: Vec<Node> = source_nodes(&n).iter().map(|c| (**c).clone()).collect();
|
||||
out.extend(replace_roots(d, &[*g], srcs)?);
|
||||
out.extend(srcs.iter().map(|c| c.id));
|
||||
released.put(d, *g, srcs)?;
|
||||
}
|
||||
released.finish(d)?;
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
})?;
|
||||
@@ -836,12 +836,15 @@ fn trace_release(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let targets = trace_targets(s, p)?;
|
||||
let ids = s.edit("Release Image Trace", |d, sel| {
|
||||
let mut out = vec![];
|
||||
let mut released = Beside::replacing(d, &targets);
|
||||
for g in &targets {
|
||||
let n = d.node(*g).cloned().ok_or(EngineError::NoNode(*g))?;
|
||||
let mut img = (**n.children().and_then(|c| c.first()).ok_or(EngineError::NoNode(*g))?).clone();
|
||||
img.visible = true;
|
||||
out.extend(replace_roots(d, &[*g], vec![img])?);
|
||||
out.push(img.id);
|
||||
released.put(d, *g, vec![img])?;
|
||||
}
|
||||
released.finish(d)?;
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
})?;
|
||||
|
||||
@@ -14,7 +14,7 @@ use vectorcraft_pathops::BoolOp;
|
||||
|
||||
use super::draw2::{components, erase_outline, ids_json, replace_with_pieces};
|
||||
use super::edit::selected_roots;
|
||||
use super::pathops::node_path;
|
||||
use super::pathops::{Beside, node_path};
|
||||
use super::*;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -163,10 +163,12 @@ fn apply(s: &mut Session, label: &str, plan: Plan) -> Result<Value> {
|
||||
}
|
||||
let (ids, removed) = s.edit(label, |d: &mut Document, sel| {
|
||||
let (mut all, mut removed) = (vec![], 0);
|
||||
let mut cut = Beside::replacing(d, &plan.iter().map(|(id, _)| *id).collect::<Vec<_>>());
|
||||
for (id, pieces) in plan {
|
||||
removed += pieces.is_empty() as usize;
|
||||
all.extend(replace_with_pieces(d, id, pieces)?);
|
||||
all.extend(replace_with_pieces(d, &mut cut, id, pieces)?);
|
||||
}
|
||||
cut.finish(d)?;
|
||||
sel.set(all.iter().copied());
|
||||
Ok((all, removed))
|
||||
})?;
|
||||
|
||||
@@ -10,6 +10,7 @@ use vectorcraft_doc::{CharStyle, Guide, Node, NodeId, NodeKind, SavedSelection,
|
||||
use vectorcraft_geom::Vec2;
|
||||
|
||||
use super::edit::selected_roots;
|
||||
use super::pathops::Beside;
|
||||
use super::select::{selectable_ids, walk_selectable};
|
||||
use super::*;
|
||||
|
||||
@@ -504,10 +505,13 @@ fn clear_guides(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
s.edit("Clear Guides", |d, _| {
|
||||
d.guides.clear();
|
||||
let mut gone = Beside::replacing(d, &paths);
|
||||
for id in &paths {
|
||||
let _ = d.remove(*id);
|
||||
if d.node(*id).is_some() {
|
||||
gone.put(d, *id, vec![])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
gone.finish(d)
|
||||
})?;
|
||||
Ok(json!({ "count": n }))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use vectorcraft_pathops as po;
|
||||
use vectorcraft_pathops::{BoolOp, Cap, Join, SimplifyOptions};
|
||||
|
||||
use super::create::{add_look, add_node};
|
||||
use super::pathops::{Beside, remove_each};
|
||||
use super::*;
|
||||
use crate::EngineError;
|
||||
|
||||
@@ -310,20 +311,25 @@ fn sibling_with(d: &mut Document, id: NodeId, pd: PathData) -> Result<Node> {
|
||||
}
|
||||
|
||||
/// Replace path or compound path `id` with `pieces` (first keeps the id, the rest go right above
|
||||
/// it; none removes it). Returns ids.
|
||||
pub(super) fn replace_with_pieces(d: &mut Document, id: NodeId, pieces: Vec<PathData>) -> Result<Vec<NodeId>> {
|
||||
/// it; none removes it). `cut` removes the path or puts the other pieces above it. Returns ids.
|
||||
pub(super) fn replace_with_pieces(d: &mut Document, cut: &mut Beside, id: NodeId, pieces: Vec<PathData>) -> Result<Vec<NodeId>> {
|
||||
let mut it = pieces.into_iter();
|
||||
let Some(first) = it.next() else {
|
||||
d.remove(id)?;
|
||||
cut.put(d, id, vec![])?;
|
||||
return Ok(vec![]);
|
||||
};
|
||||
let (parent, idx, _) = d.position(id).ok_or(EngineError::NoNode(id))?;
|
||||
let mut ids = vec![id];
|
||||
for (k, pd) in it.enumerate() {
|
||||
let mut above = vec![];
|
||||
for pd in it {
|
||||
let n = sibling_with(d, id, pd)?;
|
||||
ids.push(d.insert(parent, idx + 1 + k, n)?);
|
||||
ids.push(n.id);
|
||||
above.push(n);
|
||||
}
|
||||
set_geometry(d, id, first)?;
|
||||
if !above.is_empty() {
|
||||
let n = d.node(id).cloned().ok_or(EngineError::NoNode(id))?;
|
||||
cut.put(d, id, [n].into_iter().chain(above).collect())?;
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
@@ -733,7 +739,10 @@ fn remove_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if ai >= sp.anchors.len() {
|
||||
return Err(EngineError::Other("no such anchor".into()));
|
||||
}
|
||||
remove_anchors_of(d, sel, id, [(si, ai)])
|
||||
let mut emptied = Beside::replacing(d, &[id]);
|
||||
let removed = remove_anchors_of(d, sel, &mut emptied, id, [(si, ai)])?;
|
||||
emptied.finish(d)?;
|
||||
Ok(removed)
|
||||
})?;
|
||||
Ok(json!({ "removedObject": removed }))
|
||||
}
|
||||
@@ -752,18 +761,26 @@ fn remove_anchors(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
let removed = s.edit("Remove Anchor Points", |d, sel| {
|
||||
let mut removed = 0;
|
||||
let mut emptied = Beside::replacing(d, &jobs.iter().map(|(id, _)| *id).collect::<Vec<_>>());
|
||||
for (id, anchors) in jobs {
|
||||
removed += usize::from(remove_anchors_of(d, sel, id, anchors)?);
|
||||
removed += usize::from(remove_anchors_of(d, sel, &mut emptied, id, anchors)?);
|
||||
}
|
||||
emptied.finish(d)?;
|
||||
Ok(removed)
|
||||
})?;
|
||||
Ok(json!({ "removedObjects": removed }))
|
||||
}
|
||||
|
||||
/// Remove `anchors` (subpath, anchor) of path `id` without opening it, refitting the curve round
|
||||
/// each one; a subpath left with one anchor goes, and the path with its last subpath. True when
|
||||
/// the path went.
|
||||
fn remove_anchors_of(d: &mut Document, sel: &mut Selection, id: NodeId, anchors: impl IntoIterator<Item = (usize, usize)>) -> Result<bool> {
|
||||
/// each one. A subpath left with one anchor is removed, and a path left with no subpath is removed
|
||||
/// through `emptied`. True when the path went.
|
||||
fn remove_anchors_of(
|
||||
d: &mut Document,
|
||||
sel: &mut Selection,
|
||||
emptied: &mut Beside,
|
||||
id: NodeId,
|
||||
anchors: impl IntoIterator<Item = (usize, usize)>,
|
||||
) -> Result<bool> {
|
||||
let mut anchors: Vec<(usize, usize)> = anchors.into_iter().collect();
|
||||
// The last first, so the indexes still to come stay valid.
|
||||
anchors.sort_unstable_by(|a, b| b.cmp(a));
|
||||
@@ -777,7 +794,7 @@ fn remove_anchors_of(d: &mut Document, sel: &mut Selection, id: NodeId, anchors:
|
||||
path.subpaths.retain(|sp| sp.anchors.len() >= 2);
|
||||
let gone = path.is_empty();
|
||||
if gone {
|
||||
d.remove(id)?;
|
||||
emptied.put(d, id, vec![])?;
|
||||
sel.remove(id);
|
||||
} else {
|
||||
sel.anchors.remove(&id);
|
||||
@@ -920,9 +937,11 @@ fn knife(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let ids = s.edit("Knife", |d, sel| {
|
||||
let mut all = vec![];
|
||||
let mut cut = Beside::replacing(d, &plan.iter().map(|(id, _)| *id).collect::<Vec<_>>());
|
||||
for (id, pieces) in plan {
|
||||
all.extend(replace_with_pieces(d, id, pieces)?);
|
||||
all.extend(replace_with_pieces(d, &mut cut, id, pieces)?);
|
||||
}
|
||||
cut.finish(d)?;
|
||||
sel.set(all.iter().copied());
|
||||
Ok(all)
|
||||
})?;
|
||||
@@ -960,9 +979,11 @@ fn apply_plan(s: &mut Session, label: &str, plan: Vec<(NodeId, Vec<PathData>)>)
|
||||
}
|
||||
let ids = s.edit(label, |d, sel| {
|
||||
let mut all = vec![];
|
||||
let mut cut = Beside::replacing(d, &plan.iter().map(|(id, _)| *id).collect::<Vec<_>>());
|
||||
for (id, pieces) in plan {
|
||||
all.extend(replace_with_pieces(d, id, pieces)?);
|
||||
all.extend(replace_with_pieces(d, &mut cut, id, pieces)?);
|
||||
}
|
||||
cut.finish(d)?;
|
||||
sel.set(all.iter().copied());
|
||||
Ok(all)
|
||||
})?;
|
||||
@@ -1256,9 +1277,7 @@ fn blob(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let others: Vec<NodeId> = rest.iter().map(|m| m.0).collect();
|
||||
s.edit("Blob Brush", |d, sel| {
|
||||
*path_mut(d, keep)? = merged;
|
||||
for id in others {
|
||||
d.remove(id)?;
|
||||
}
|
||||
remove_each(d, &others)?;
|
||||
sel.set([keep]);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -35,7 +35,7 @@ use vectorcraft_render::effects;
|
||||
|
||||
use super::edit::roots_of;
|
||||
use super::menucmds::{MAX_PIXELS, isolated_doc, unique_key};
|
||||
use super::pathops::{node_path, outline_strokes, outline_strokes_under, shape_node};
|
||||
use super::pathops::{node_path, outline_strokes, outline_strokes_under, separate, shape_node};
|
||||
use super::*;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -1038,7 +1038,10 @@ pub(crate) fn flatten_document(doc: &Document, o: &FlattenOptions) -> Result<Opt
|
||||
|
||||
fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan: Plan, o: &FlattenOptions) -> Result<(Vec<NodeId>, usize, usize)> {
|
||||
let (mut vector, mut rasterized) = (0, 0);
|
||||
let mut ids = vec![];
|
||||
let (mut ids, mut flat) = (vec![], vec![]);
|
||||
// The groups replace their objects in one pass at the end. When an object is listed twice or
|
||||
// lies inside another listed one, each group replaces its objects as it is made instead.
|
||||
let one_pass = separate(d, &plan.flat.iter().flat_map(|f| f.roots.iter().copied()).collect::<Vec<_>>());
|
||||
for f in plan.flat {
|
||||
// A group always has objects.
|
||||
let Some(&top) = f.roots.last() else { continue };
|
||||
@@ -1060,13 +1063,23 @@ fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan: Plan, o:
|
||||
children.push(Arc::new(n));
|
||||
vector += 1;
|
||||
}
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
let g = Node::group(d.alloc_id(), children);
|
||||
ids.push(d.insert(par, idx + 1, g)?);
|
||||
for r in &f.roots {
|
||||
d.remove(*r)?;
|
||||
ids.push(g.id);
|
||||
if one_pass {
|
||||
// The group replaces the topmost object, and the other objects are removed.
|
||||
flat.extend(f.roots.iter().filter(|r| **r != top).map(|r| (*r, vec![])));
|
||||
flat.push((top, vec![g]));
|
||||
} else {
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
d.insert(par, idx + 1, g)?;
|
||||
for r in &f.roots {
|
||||
d.remove(*r)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !flat.is_empty() {
|
||||
d.insert_beside(flat, true)?;
|
||||
}
|
||||
let brushes = if o.strokes_to_outlines { vectorcraft_brush::library(d) } else { vec![] };
|
||||
for mut id in plan.kept {
|
||||
if o.text_to_outlines {
|
||||
@@ -1113,9 +1126,8 @@ fn outline_texts_under(d: &mut Document, root: NodeId) -> Result<NodeId> {
|
||||
for t in texts {
|
||||
let Some(o) = outlined_text(&t) else { continue };
|
||||
let o = d.reid(&o);
|
||||
let (par, idx, _) = d.position(t.id).ok_or(EngineError::NoNode(t.id))?;
|
||||
let nid = d.insert(par, idx, o)?;
|
||||
d.remove(t.id)?;
|
||||
let nid = o.id;
|
||||
*d.node_mut(t.id).ok_or(EngineError::NoNode(t.id))? = o;
|
||||
if t.id == root {
|
||||
out = nid;
|
||||
}
|
||||
|
||||
@@ -232,12 +232,11 @@ fn halftone(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let gid = d.alloc_id();
|
||||
let mut g = Node::group(gid, children);
|
||||
g.name = Some("Vector Halftone".into());
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
d.insert(par, idx + 1, g)?;
|
||||
if !keep {
|
||||
for r in &roots {
|
||||
d.remove(*r)?;
|
||||
}
|
||||
if keep {
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
d.insert(par, idx + 1, g)?;
|
||||
} else {
|
||||
d.wrap(&roots, |_| g)?;
|
||||
}
|
||||
sel.set([gid]);
|
||||
Ok(gid)
|
||||
|
||||
@@ -18,6 +18,7 @@ use vectorcraft_geom::{Affine, PathData, Point, Vec2};
|
||||
|
||||
use super::edit::selected_roots;
|
||||
use super::expand::gradient_sampler;
|
||||
use super::pathops::Beside;
|
||||
use super::*;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -360,14 +361,11 @@ fn has_mesh(s: &Session) -> std::result::Result<(), String> {
|
||||
|
||||
/// Wrap `roots` (paint order) into a new node built by `make`, placed where the topmost root was.
|
||||
fn wrap(d: &mut Document, sel: &mut Selection, roots: &[NodeId], make: impl FnOnce(NodeId, Vec<Arc<Node>>) -> Node) -> Result<NodeId> {
|
||||
let top = *roots.last().ok_or_else(|| EngineError::Other("nothing to wrap".into()))?;
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
let nodes: Vec<Arc<Node>> = roots.iter().filter_map(|id| d.node(*id).cloned()).map(Arc::new).collect();
|
||||
let id = d.alloc_id();
|
||||
d.insert(par, idx + 1, make(id, nodes))?;
|
||||
for r in roots {
|
||||
d.remove(*r)?;
|
||||
if roots.is_empty() {
|
||||
return Err(EngineError::Other("nothing to wrap".into()));
|
||||
}
|
||||
let id = d.alloc_id();
|
||||
d.wrap(roots, |nodes| make(id, nodes))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
}
|
||||
@@ -546,6 +544,7 @@ fn blend_release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let blends = selected_of(s, is_blend);
|
||||
let (ids, spines, groups) = s.edit("Release Blend", |d, sel| {
|
||||
let (mut keys, mut spines, mut groups) = (vec![], vec![], vec![]);
|
||||
let mut released = Beside::replacing(d, &blends);
|
||||
for b in &blends {
|
||||
let Some(n) = d.node(*b).cloned() else { continue };
|
||||
let NodeKind::Blend { children, spec } = &n.kind else { continue };
|
||||
@@ -572,8 +571,9 @@ fn blend_release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
groups.push(g.id);
|
||||
nodes = vec![g];
|
||||
}
|
||||
replace_with(d, *b, nodes)?;
|
||||
released.put(d, *b, nodes)?;
|
||||
}
|
||||
released.finish(d)?;
|
||||
if groups.is_empty() {
|
||||
sel.set(spines.iter().chain(&keys).copied());
|
||||
} else {
|
||||
@@ -1171,6 +1171,7 @@ fn env_release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let envs = selected_of(s, is_envelope);
|
||||
let ids = s.edit("Release Envelope", |d, sel| {
|
||||
let mut out = vec![];
|
||||
let mut released = Beside::replacing(d, &envs);
|
||||
for e in &envs {
|
||||
let Some(n) = d.node(*e).cloned() else { continue };
|
||||
let NodeKind::Envelope { content, kind, .. } = &n.kind else { continue };
|
||||
@@ -1179,8 +1180,10 @@ fn env_release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
shape.id = d.alloc_id();
|
||||
nodes.push(shape);
|
||||
}
|
||||
out.extend(replace_with(d, *e, nodes)?);
|
||||
out.extend(nodes.iter().map(|c| c.id));
|
||||
released.put(d, *e, nodes)?;
|
||||
}
|
||||
released.finish(d)?;
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
})?;
|
||||
|
||||
@@ -443,9 +443,10 @@ fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.map(|(bp, rule)| (PathData::from_bezpath(&bp), rule));
|
||||
let tmp = isolated_doc(&st.doc, nodes);
|
||||
let (png, w, h) = render_png(&tmp, region, scale, &raster.render_options(RasterFormat::Png), look.color_model)?;
|
||||
let top = *roots.last().ok_or_else(|| bad(C, "nothing selected"))?;
|
||||
if roots.is_empty() {
|
||||
return Err(bad(C, "nothing selected"));
|
||||
}
|
||||
let id = s.edit("Rasterize", |d, sel| {
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
let key = unique_key(d, "raster");
|
||||
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", png));
|
||||
let id = d.alloc_id();
|
||||
@@ -455,11 +456,7 @@ fn rasterize(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some((path, rule)) = clip {
|
||||
node = super::rasterfx::clip_group(d, path, rule, node);
|
||||
}
|
||||
let id = node.id;
|
||||
d.insert(par, idx + 1, node)?;
|
||||
for r in &roots {
|
||||
d.remove(*r)?;
|
||||
}
|
||||
let id = d.wrap(&roots, |_| node)?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})?;
|
||||
@@ -829,7 +826,7 @@ fn convert_to_shape(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
fn convert_to_artboards(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let roots = selected_roots(s)?;
|
||||
let n = s.edit("Convert to Artboards", |d, sel| {
|
||||
let mut n = 0;
|
||||
let mut converted = vec![];
|
||||
for id in &roots {
|
||||
let Some(b) = d.node(*id).and_then(|n| n.geometric_bounds()) else { continue };
|
||||
if b.width() <= 0.0 || b.height() <= 0.0 {
|
||||
@@ -843,14 +840,14 @@ fn convert_to_artboards(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
show_center_mark: false,
|
||||
show_cross_hairs: false,
|
||||
});
|
||||
d.remove(*id)?;
|
||||
n += 1;
|
||||
converted.push(*id);
|
||||
}
|
||||
if n == 0 {
|
||||
if converted.is_empty() {
|
||||
return Err(EngineError::Other("Convert to Artboards: selection has no area".into()));
|
||||
}
|
||||
d.remove_many(&converted)?;
|
||||
sel.clear();
|
||||
Ok(n)
|
||||
Ok(converted.len())
|
||||
})?;
|
||||
Ok(json!({ "artboards": n }))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use vectorcraft_color::BlendMode;
|
||||
use vectorcraft_doc::{Document, Knockout, Node, NodeId, OpacityMask, Selection};
|
||||
|
||||
use super::maskedit::{begin, finish, mask_group, mask_parts, show_editing};
|
||||
use super::pathops::{Beside, separate};
|
||||
use super::*;
|
||||
use crate::EngineError;
|
||||
|
||||
@@ -189,6 +190,9 @@ fn make(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Several objects are grouped so they share one mask.
|
||||
let target = if let [one] = art {
|
||||
*one
|
||||
} else if separate(d, art) {
|
||||
let gid = d.alloc_id();
|
||||
d.wrap(art, |children| Node::group(gid, children))?
|
||||
} else {
|
||||
let last = art[art.len() - 1];
|
||||
let (par, idx, _) = d.position(last).ok_or(EngineError::NoNode(last))?;
|
||||
@@ -223,16 +227,16 @@ fn release(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Leave mask editing first: its art goes back into the mask, which is released below.
|
||||
finish(d, sel);
|
||||
let mut out = vec![];
|
||||
let mut released = Beside::above(d, &ids);
|
||||
for id in &ids {
|
||||
let Some(m) = d.node_mut(*id).and_then(|n| n.extras_mut().mask.take()) else { continue };
|
||||
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
|
||||
out.push(*id);
|
||||
for (k, part) in mask_parts(&m.art).iter().enumerate() {
|
||||
// Fresh ids: copies of a masked object share the ids inside their mask art.
|
||||
let art = d.reid(part);
|
||||
out.push(d.insert(par, idx + 1 + k, art)?);
|
||||
}
|
||||
// Fresh ids: copies of a masked object share the ids inside their mask art.
|
||||
let parts: Vec<Node> = mask_parts(&m.art).iter().map(|part| d.reid(part)).collect();
|
||||
out.extend(parts.iter().map(|p| p.id));
|
||||
released.put(d, *id, parts)?;
|
||||
}
|
||||
released.finish(d)?;
|
||||
sel.set(out);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -7,6 +7,7 @@ use vectorcraft_doc::{NodeId, NodeKind, Scaling};
|
||||
use vectorcraft_geom::{AnchorKind, PathData, Point, SubPath, Vec2};
|
||||
|
||||
use super::create::anchor_from_json;
|
||||
use super::pathops::Beside;
|
||||
use super::*;
|
||||
use crate::EngineError;
|
||||
|
||||
@@ -466,6 +467,7 @@ pub(crate) fn delete_anchors(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let segments = st.selection.segments.clone();
|
||||
s.edit("Clear", |d, selection| {
|
||||
let mut survivors = vec![];
|
||||
let mut emptied = Beside::replacing(d, &sel.keys().copied().collect::<Vec<_>>());
|
||||
for (id, set) in &sel {
|
||||
let remove_node = {
|
||||
let path = path_mut(d, *id)?;
|
||||
@@ -499,11 +501,12 @@ pub(crate) fn delete_anchors(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
path.subpaths.is_empty()
|
||||
};
|
||||
if remove_node {
|
||||
d.remove(*id)?;
|
||||
emptied.put(d, *id, vec![])?;
|
||||
} else if segments.contains_key(id) {
|
||||
survivors.push(*id);
|
||||
}
|
||||
}
|
||||
emptied.finish(d)?;
|
||||
// Another Delete removes the remaining path after a segment cut. Anchor Delete keeps
|
||||
// its existing deselection behavior.
|
||||
selection.set(survivors);
|
||||
@@ -625,11 +628,12 @@ fn cut_at_anchors(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
let ids = s.edit("Cut Path", |d, sel| {
|
||||
let (mut all, mut picked) = (vec![], vec![]);
|
||||
let mut cut = Beside::replacing(d, &cuts.iter().map(|(id, ..)| *id).collect::<Vec<_>>());
|
||||
for (id, subs, starts) in cuts {
|
||||
// A path of one subpath becomes one path per piece (the first keeps the id); the
|
||||
// pieces of a compound shape stay subpaths of it.
|
||||
if d.node(id).and_then(|n| n.path_data()).is_some_and(|pd| pd.subpaths.len() == 1) {
|
||||
let ids = super::draw2::replace_with_pieces(d, id, subs.into_iter().map(PathData::single).collect())?;
|
||||
let ids = super::draw2::replace_with_pieces(d, &mut cut, id, subs.into_iter().map(PathData::single).collect())?;
|
||||
picked.extend(ids.iter().zip(&starts).filter(|(_, s)| **s).map(|(id, _)| (*id, (0, 0))));
|
||||
all.extend(ids);
|
||||
} else {
|
||||
@@ -638,6 +642,7 @@ fn cut_at_anchors(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
all.push(id);
|
||||
}
|
||||
}
|
||||
cut.finish(d)?;
|
||||
// Each cut leaves one of its two coincident anchors selected, so a drag pulls the ends apart.
|
||||
sel.clear();
|
||||
for (id, a) in picked {
|
||||
|
||||
@@ -286,6 +286,73 @@ fn shape_node_styled(d: &mut Document, path: PathData, style: &Style) -> Node {
|
||||
n
|
||||
}
|
||||
|
||||
/// Whether `ids` are separate objects, as [`Document::remove_many`] and [`Document::insert_beside`]
|
||||
/// take them: each one is in the tree and listed once, and none lies inside another listed one.
|
||||
pub(crate) fn separate(d: &Document, ids: &[NodeId]) -> bool {
|
||||
let paths = d.index_paths(ids.iter().copied());
|
||||
let mut found: Vec<_> = ids.iter().filter_map(|id| paths.get(id)).collect();
|
||||
found.sort_unstable();
|
||||
found.len() == ids.len() && found.windows(2).all(|w| !w[1].starts_with(w[0]))
|
||||
}
|
||||
|
||||
/// Lists of nodes that go in place of objects, or directly above them; an empty list in place of
|
||||
/// an object removes it. When the objects are [`separate`], [`Beside::put`] holds each list and
|
||||
/// leaves the tree as it is, and [`Beside::finish`] inserts them all in one pass over the children
|
||||
/// of each parent ([`Document::insert_beside`]). Otherwise [`Beside::put`] inserts each list at
|
||||
/// once, where its object is at that moment.
|
||||
pub(crate) struct Beside {
|
||||
replace: bool,
|
||||
waiting: Option<Vec<(NodeId, Vec<Node>)>>,
|
||||
}
|
||||
|
||||
impl Beside {
|
||||
/// Lists that go in place of objects among `ids`.
|
||||
pub(crate) fn replacing(d: &Document, ids: &[NodeId]) -> Self {
|
||||
Beside { replace: true, waiting: separate(d, ids).then(Vec::new) }
|
||||
}
|
||||
|
||||
/// Lists that go directly above objects among `ids`.
|
||||
pub(crate) fn above(d: &Document, ids: &[NodeId]) -> Self {
|
||||
Beside { replace: false, waiting: separate(d, ids).then(Vec::new) }
|
||||
}
|
||||
|
||||
/// Put `nodes`, in their order, in place of object `id` or directly above it.
|
||||
pub(crate) fn put(&mut self, d: &mut Document, id: NodeId, nodes: Vec<Node>) -> Result<()> {
|
||||
if let Some(waiting) = &mut self.waiting {
|
||||
waiting.push((id, nodes));
|
||||
return Ok(());
|
||||
}
|
||||
let (parent, idx, _) = d.position(id).ok_or(EngineError::NoNode(id))?;
|
||||
let at = if self.replace {
|
||||
d.remove(id)?;
|
||||
idx
|
||||
} else {
|
||||
idx + 1
|
||||
};
|
||||
for (k, n) in nodes.into_iter().enumerate() {
|
||||
d.insert(parent, at + k, n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert the lists that [`Beside::put`] held.
|
||||
pub(crate) fn finish(self, d: &mut Document) -> Result<()> {
|
||||
match self.waiting {
|
||||
Some(waiting) if !waiting.is_empty() => Ok(d.insert_beside(waiting, self.replace)?),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove `ids` through a [`Beside`], which removes them in one pass when they are [`separate`].
|
||||
pub(crate) fn remove_each(d: &mut Document, ids: &[NodeId]) -> Result<()> {
|
||||
let mut gone = Beside::replacing(d, ids);
|
||||
for id in ids {
|
||||
gone.put(d, *id, vec![])?;
|
||||
}
|
||||
gone.finish(d)
|
||||
}
|
||||
|
||||
// ---------- Pathfinder ----------
|
||||
|
||||
fn pf_label(op: PathfinderOp) -> &'static str {
|
||||
@@ -305,7 +372,9 @@ fn pf_label(op: PathfinderOp) -> &'static str {
|
||||
|
||||
fn run_pf(s: &mut Session, op: PathfinderOp) -> Result<Value> {
|
||||
let roots = selected_roots(s)?;
|
||||
let Some(&top) = roots.last() else { return Err(EngineError::Other("nothing selected".into())) };
|
||||
if roots.is_empty() {
|
||||
return Err(EngineError::Other("nothing selected".into()));
|
||||
}
|
||||
let shape_mode =
|
||||
matches!(op, PathfinderOp::Unite | PathfinderOp::MinusFront | PathfinderOp::Intersect | PathfinderOp::Exclude | PathfinderOp::MinusBack);
|
||||
let label = pf_label(op);
|
||||
@@ -371,7 +440,6 @@ fn run_pf(s: &mut Session, op: PathfinderOp) -> Result<Value> {
|
||||
if results.iter().all(|r| r.path.is_empty()) {
|
||||
return Err(EngineError::Other(format!("{label} produced an empty result")));
|
||||
}
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
let new_node = if shape_mode {
|
||||
let r =
|
||||
results.into_iter().find(|r| !r.path.is_empty()).ok_or_else(|| EngineError::Other(format!("{label} produced an empty result")))?;
|
||||
@@ -403,11 +471,7 @@ fn run_pf(s: &mut Session, op: PathfinderOp) -> Result<Value> {
|
||||
let gid = d.alloc_id();
|
||||
Node::group(gid, children)
|
||||
};
|
||||
let nid = new_node.id;
|
||||
d.insert(par, idx + 1, new_node)?;
|
||||
for id in &roots {
|
||||
d.remove(*id)?;
|
||||
}
|
||||
let nid = d.wrap(&roots, |_| new_node)?;
|
||||
sel.set([nid]);
|
||||
Ok(vec![nid])
|
||||
})?;
|
||||
@@ -433,7 +497,7 @@ fn offset_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let miter = num_param(p, "miterLimit").unwrap_or(4.0).clamp(1.0, 500.0);
|
||||
let roots = selected_roots(s)?;
|
||||
let ids = s.edit("Offset Path", |d, sel| {
|
||||
let mut out = vec![];
|
||||
let (mut out, mut above) = (vec![], vec![]);
|
||||
for id in &roots {
|
||||
let Some(n) = d.node(*id).cloned() else { continue };
|
||||
let mut lv = vec![];
|
||||
@@ -460,12 +524,13 @@ fn offset_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let gid = d.alloc_id();
|
||||
Node::group(gid, made.into_iter().map(Arc::new).collect())
|
||||
};
|
||||
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
|
||||
out.push(d.insert(par, idx + 1, new)?);
|
||||
out.push(new.id);
|
||||
above.push((*id, vec![new]));
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(EngineError::Other("Offset Path: select paths".into()));
|
||||
}
|
||||
d.insert_beside(above, false)?;
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
})?;
|
||||
@@ -614,9 +679,8 @@ pub(crate) fn replace_leaves(d: &mut Document, root: NodeId, mut f: impl FnMut(&
|
||||
let (mut replacement, mut changed) = (root, 0);
|
||||
for l in lv {
|
||||
let Some(new) = f(d, &l) else { continue };
|
||||
let (par, idx, _) = d.position(l.id).ok_or(EngineError::NoNode(l.id))?;
|
||||
let nid = d.insert(par, idx, new)?;
|
||||
d.remove(l.id)?;
|
||||
let nid = new.id;
|
||||
*d.node_mut(l.id).ok_or(EngineError::NoNode(l.id))? = new;
|
||||
changed += 1;
|
||||
if l.id == root {
|
||||
replacement = nid;
|
||||
@@ -840,12 +904,13 @@ fn clean_up(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Ok(json!({ "removed": 0 }));
|
||||
}
|
||||
s.edit("Clean Up", |d, _| {
|
||||
let mut gone = Beside::replacing(d, &found);
|
||||
for id in &found {
|
||||
if d.node(*id).is_some() {
|
||||
d.remove(*id)?;
|
||||
gone.put(d, *id, vec![])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
gone.finish(d)
|
||||
})?;
|
||||
Ok(json!({ "removed": found.len() }))
|
||||
}
|
||||
@@ -885,6 +950,7 @@ fn divide_objects_below(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
&& d.node(*id).and_then(Node::geometric_bounds).is_some_and(|b| b.intersect(cb).area() > 0.0)
|
||||
});
|
||||
let mut out = vec![];
|
||||
let mut cut = Beside::replacing(d, &[targets.as_slice(), &[cutter_id]].concat());
|
||||
for t in targets {
|
||||
let Some(n) = d.node(t).cloned() else { continue };
|
||||
let Some((tp, tr)) = node_path(&n) else { continue };
|
||||
@@ -893,14 +959,12 @@ fn divide_objects_below(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
if inside.is_empty() || outside.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (par, idx, _) = d.position(t).ok_or(EngineError::NoNode(t))?;
|
||||
d.remove(t)?;
|
||||
for (k, piece) in [outside, inside].into_iter().enumerate() {
|
||||
let node = shape_node(d, piece, Some(&n));
|
||||
out.push(d.insert(par, idx + k, node)?);
|
||||
}
|
||||
let pieces: Vec<Node> = [outside, inside].into_iter().map(|piece| shape_node(d, piece, Some(&n))).collect();
|
||||
out.extend(pieces.iter().map(|p| p.id));
|
||||
cut.put(d, t, pieces)?;
|
||||
}
|
||||
d.remove(cutter_id)?;
|
||||
cut.put(d, cutter_id, vec![])?;
|
||||
cut.finish(d)?;
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
})?;
|
||||
|
||||
@@ -18,6 +18,7 @@ use vectorcraft_doc::{Document, Node, NodeId, NodeKind, Selection};
|
||||
use vectorcraft_geom::Rect;
|
||||
|
||||
use super::edit::selected_roots;
|
||||
use super::pathops::Beside;
|
||||
use super::*;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -633,15 +634,12 @@ fn make_repeat(s: &mut Session, p: &Value, label: &str, make: impl Fn(Vec<Arc<No
|
||||
return Ok(json!({ "id": r.0 }));
|
||||
}
|
||||
let roots = roots(s, p)?;
|
||||
let Some(&top) = roots.last() else { return Err(EngineError::Other("select the art to repeat".into())) };
|
||||
if roots.is_empty() {
|
||||
return Err(EngineError::Other("select the art to repeat".into()));
|
||||
}
|
||||
let id = s.edit(label, |d, sel| {
|
||||
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
|
||||
let nodes: Vec<Arc<Node>> = roots.iter().filter_map(|id| d.node(*id).cloned()).map(Arc::new).collect();
|
||||
let id = d.alloc_id();
|
||||
d.insert(par, idx + 1, Node::new(id, NodeKind::Repeat(make(nodes))))?;
|
||||
for r in &roots {
|
||||
d.remove(*r)?;
|
||||
}
|
||||
d.wrap(&roots, |nodes| Node::new(id, NodeKind::Repeat(make(nodes))))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})?;
|
||||
@@ -682,15 +680,14 @@ fn repeat_release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let reps = selected_repeats(s);
|
||||
let ids = s.edit("Release Repeat", |d, sel| {
|
||||
let mut out = vec![];
|
||||
let mut released = Beside::replacing(d, &reps);
|
||||
for r in &reps {
|
||||
let Some(n) = d.node(*r).cloned() else { continue };
|
||||
let (par, idx, _) = d.position(*r).ok_or(EngineError::NoNode(*r))?;
|
||||
d.remove(*r)?;
|
||||
for (k, c) in n.children().into_iter().flatten().enumerate() {
|
||||
out.push(c.id);
|
||||
d.insert(par, idx + k, (**c).clone())?;
|
||||
}
|
||||
let art: Vec<Node> = n.children().into_iter().flatten().map(|c| (**c).clone()).collect();
|
||||
out.extend(art.iter().map(|c| c.id));
|
||||
released.put(d, *r, art)?;
|
||||
}
|
||||
released.finish(d)?;
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
})?;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Shaper compositions are groups of generated fills/strokes with transparent, editable source
|
||||
//! art retained underneath. Recipes select topology regions; source edits re-evaluate them in
|
||||
//! the same undo transaction. Rendering and exporters only see ordinary groups and paths.
|
||||
use super::pathops::Beside;
|
||||
use super::*;
|
||||
use serde_json::{Value, json};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
@@ -380,7 +381,10 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> {
|
||||
ids.push(n.id)
|
||||
}
|
||||
});
|
||||
for id in ids {
|
||||
// Groups whose source art is all gone are removed through `emptied`, which is made when the
|
||||
// first one is found.
|
||||
let mut emptied: Option<Beside> = None;
|
||||
for &id in &ids {
|
||||
let Some(current) = d.node(id).cloned() else { continue };
|
||||
let Some(old) = before.node(id) else { continue };
|
||||
let src = sources(¤t);
|
||||
@@ -395,7 +399,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
if src.is_empty() {
|
||||
d.remove(id)?;
|
||||
emptied.get_or_insert_with(|| Beside::replacing(d, &ids)).put(d, id, vec![])?;
|
||||
continue;
|
||||
}
|
||||
let map = map_of(&src)?;
|
||||
@@ -494,7 +498,7 @@ pub(crate) fn refresh(before: &Document, d: &mut Document) -> Result<()> {
|
||||
*ch = children;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
emptied.map_or(Ok(()), |e| e.finish(d))
|
||||
}
|
||||
|
||||
fn candidates(s: &Session, p: &Value, points: &[Point], tolerance: f64) -> Result<Vec<NodeId>> {
|
||||
@@ -670,9 +674,7 @@ fn scribble(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut group = Node::group(id, children);
|
||||
group.name = Some("Shaper Group".into());
|
||||
group.extras_mut().shaper = Some(Box::new(spec));
|
||||
for id in &roots {
|
||||
d.remove(*id)?;
|
||||
}
|
||||
d.remove_many(&roots)?;
|
||||
d.insert(parent, index.saturating_sub(earlier), group)?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
|
||||
@@ -398,6 +398,25 @@ fn new_symbol_replaces_selection_with_instance_of_same_bounds() {
|
||||
assert_eq!(s.doc().unwrap().doc.symbols.len(), 1);
|
||||
}
|
||||
|
||||
/// New Symbol given `ids` that list an object and then the group that holds it puts the instance
|
||||
/// where the group was and removes both, as removing one object at a time does.
|
||||
#[test]
|
||||
fn new_symbol_of_an_object_and_the_group_that_holds_it_keeps_order_and_ids() {
|
||||
let mut s = session();
|
||||
let (a, b) = (rect(&mut s, 0.0, 0.0, 10.0, 10.0), rect(&mut s, 100.0, 0.0, 10.0, 10.0));
|
||||
select(&mut s, &[a, b]);
|
||||
let group = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let c = rect(&mut s, 200.0, 0.0, 10.0, 10.0);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let r = s.execute("symbol.new", &json!({"name": "Pair", "ids": [a.0, group.0]})).unwrap();
|
||||
let inst = NodeId(r["id"].as_u64().unwrap());
|
||||
let ids = |n: &Node| n.children().unwrap().iter().map(|c| c.id).collect::<Vec<_>>();
|
||||
assert_eq!(ids(&s.doc().unwrap().doc.layers[0]), [inst, c]);
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [inst]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(*s.doc().unwrap().doc == *before, "Undo gives back the document");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_and_break_link_round_trip_geometry() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -310,6 +310,23 @@ fn flatten_is_one_undo_step() {
|
||||
assert!(!exists(&s, a));
|
||||
}
|
||||
|
||||
/// Flattening a document for export in which two objects have the same id (a damaged file)
|
||||
/// removes both objects and puts one group in their place.
|
||||
#[test]
|
||||
fn flattening_objects_that_share_an_id_removes_both() {
|
||||
let _settings = settings_lock();
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0, 10.0, 100.0, 100.0, "#ff0000");
|
||||
let b = rect(&mut s, 60.0, 60.0, 100.0, 100.0, "#0000ff");
|
||||
run(&mut s, "transparency.set", json!({"ids": [a, b], "opacity": 50}));
|
||||
let mut doc = (*s.doc().unwrap().doc).clone();
|
||||
doc.node_mut(NodeId(b)).unwrap().id = NodeId(a);
|
||||
let flat = crate::cmd::flatten::flatten_document(&doc, &crate::cmd::FlattenOptions::default()).unwrap().unwrap();
|
||||
let art = flat.layers[0].children().unwrap();
|
||||
assert_eq!(art.len(), 1, "one group where the two objects were");
|
||||
assert!(matches!(art[0].kind, NodeKind::Group { .. }) && art[0].id != NodeId(a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objects_without_transparency_stay_unless_outlined() {
|
||||
let _settings = settings_lock();
|
||||
|
||||
@@ -123,6 +123,17 @@ fn delete_selection_of_100000_rows_finishes_within_20_s() {
|
||||
undo_gives_back(&mut s, &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_up_of_100000_unpainted_paths_finishes_within_20_s() {
|
||||
let s = session(100_000, |d, k| Node::path(d.alloc_id(), shapes::rectangle(square_at(k)), Appearance::default()));
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let (mut s, r) = run_soon(s, "object.path.cleanUp", json!({}));
|
||||
assert_eq!(r["removed"], json!(100_000));
|
||||
assert!(layer(&s, 0).is_empty());
|
||||
assert!(selected(&s, &[]));
|
||||
undo_gives_back(&mut s, &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_of_50000_objects_finishes_within_20_s() {
|
||||
let s = session(50_000, square);
|
||||
@@ -195,6 +206,21 @@ fn transform_each_with_copy_of_50000_objects_finishes_within_20_s() {
|
||||
undo_gives_back(&mut s, &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_path_of_50000_objects_finishes_within_20_s() {
|
||||
let s = session(50_000, square);
|
||||
let (before, ids, first) = (s.doc().unwrap().doc.clone(), layer(&s, 0), s.doc().unwrap().doc.peek_next_id());
|
||||
let (mut s, r) = run_soon(s, "object.path.offsetPath", json!({"offset": 1}));
|
||||
// Each offset path goes directly above its square and takes the next id, in the squares' order.
|
||||
let offsets = ids_from(first, ids.len());
|
||||
assert!(ids_in(&r) == offsets);
|
||||
assert!(layer(&s, 0) == in_turn(&ids, &offsets), "each offset path directly above its square");
|
||||
assert!(selected(&s, &offsets));
|
||||
let grown = bounds(&s, offsets[1]);
|
||||
assert!((grown.width() - 12.0).abs() < 1e-6 && (grown.x0 - 19.0).abs() < 1e-6, "{grown:?}");
|
||||
undo_gives_back(&mut s, &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compound_path_of_100000_objects_finishes_within_20_s() {
|
||||
let s = session(100_000, square);
|
||||
@@ -302,6 +328,22 @@ fn release_to_layers_of_100000_objects_finishes_within_20_s() {
|
||||
undo_gives_back(&mut s, &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_stroke_of_25000_objects_finishes_within_20_s() {
|
||||
let s = session(25_000, square);
|
||||
let (before, first) = (s.doc().unwrap().doc.clone(), s.doc().unwrap().doc.peek_next_id());
|
||||
let (mut s, r) = run_soon(s, "object.path.outlineStroke", json!({}));
|
||||
// Each square, filled and stroked, becomes a group of its fill and the outline of its stroke,
|
||||
// where the square was, and the groups take new ids in the squares' order.
|
||||
let groups = ids_in(&r);
|
||||
assert_eq!(groups.len(), 25_000);
|
||||
assert!(layer(&s, 0) == groups, "each group where its square was");
|
||||
assert!(groups[0].0 >= first && groups.windows(2).all(|w| w[0] < w[1]));
|
||||
assert!(groups.iter().all(|g| children(&s, *g).len() == 2));
|
||||
assert!(selected(&s, &groups));
|
||||
undo_gives_back(&mut s, &before);
|
||||
}
|
||||
|
||||
/// With Alt held, a Selection tool press on one of 30,000 selected squares, four moves of 20 pt
|
||||
/// and the release copy the squares once, as one undo step.
|
||||
#[test]
|
||||
@@ -541,6 +583,53 @@ fn artboard_copies_and_cut_keep_order_and_ids() {
|
||||
undo_gives_back(&mut s, &before);
|
||||
}
|
||||
|
||||
/// After Offset Path, Outline Stroke, Unite and Clean Up of objects in two layers and inside a
|
||||
/// group, the layers hold the objects and the new ones in the order, and with the ids, that each
|
||||
/// check lists, and Undo gives back the document.
|
||||
#[test]
|
||||
fn path_commands_across_layers_and_inside_a_group_keep_order_and_ids() {
|
||||
let (mut s, [a, b, c, d, e, f, g, h, group]) = two_layers();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let n = before.peek_next_id();
|
||||
|
||||
let r = s.execute("object.path.offsetPath", &json!({"offset": 1})).unwrap();
|
||||
assert_eq!(r["ids"], json!([n, n + 1, n + 2, n + 3, n + 4]));
|
||||
assert_eq!(layers(&s), [json!([a, n, b, c, n + 1, [group, d, e, n + 2, f]]), json!([g, n + 3, h, n + 4])]);
|
||||
assert!(selected(&s, &ids_from(n, 5)));
|
||||
undo_gives_back(&mut s, &before);
|
||||
|
||||
// Each object becomes a group of its fill and the outline of its stroke, where it was.
|
||||
let r = s.execute("object.path.outlineStroke", &json!({})).unwrap();
|
||||
let [a2, c2, e2, g2, h2] = <[NodeId; 5]>::try_from(ids_in(&r)).unwrap();
|
||||
assert!(n <= a2.0 && a2 < c2 && c2 < e2 && e2 < g2 && g2 < h2);
|
||||
assert_eq!(layer(&s, 0), [a2, NodeId(b), c2, NodeId(group)]);
|
||||
assert_eq!(children(&s, NodeId(group)), [NodeId(d), e2, NodeId(f)]);
|
||||
assert_eq!(layer(&s, 1), [g2, h2]);
|
||||
assert!(selected(&s, &[a2, c2, e2, g2, h2]));
|
||||
undo_gives_back(&mut s, &before);
|
||||
|
||||
// The result goes where the topmost object, h, was.
|
||||
let r = s.execute("object.pathfinder.unite", &json!({})).unwrap();
|
||||
assert_eq!(r["ids"], json!([n]));
|
||||
assert_eq!(layers(&s)[0], json!([b, [group, d, f]]));
|
||||
assert_eq!(layer(&s, 1), [NodeId(n)]);
|
||||
assert!(selected(&s, &[NodeId(n)]));
|
||||
undo_gives_back(&mut s, &before);
|
||||
|
||||
s.edit("Paint nothing", |d, _| {
|
||||
for id in [a, e, h] {
|
||||
d.node_mut(NodeId(id)).unwrap().appearance = Appearance::default().into();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let r = s.execute("object.path.cleanUp", &json!({})).unwrap();
|
||||
assert_eq!(r["removed"], json!(3));
|
||||
assert_eq!(layers(&s), [json!([b, c, [group, d, f]]), json!([g])]);
|
||||
undo_gives_back(&mut s, &before);
|
||||
}
|
||||
|
||||
/// `object.transform` copies ids listed twice, and an object listed before a group around it.
|
||||
/// Each copy goes directly above its original, a later copy of the same object goes below an
|
||||
/// earlier one, and the copy of the group holds a copy of the copy made inside the group before it.
|
||||
|
||||
@@ -217,6 +217,35 @@ fn blend_release_restores_keys() {
|
||||
assert!(s.execute("object.blend.release", &json!({})).is_ok() || s.execute("object.blend.release", &json!({})).is_err());
|
||||
}
|
||||
|
||||
/// Release Blend of a blend and of a blend inside a group that is a key object of the first puts
|
||||
/// the outer blend's spine and keys where it was, then the inner blend's spine and keys where it
|
||||
/// was inside that group, as releasing one blend at a time does.
|
||||
#[test]
|
||||
fn blend_release_of_a_blend_and_a_blend_inside_it_keeps_order_and_ids() {
|
||||
let mut s = session();
|
||||
let (r1, r2) = (rect(&mut s, 0.0, 0.0, 10.0, 10.0), rect(&mut s, 100.0, 0.0, 10.0, 10.0));
|
||||
sel(&mut s, &[r1, r2]);
|
||||
let inner = id_of(&s.execute("object.blend.make", &json!({})).unwrap());
|
||||
let r3 = rect(&mut s, 200.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[inner, r3]);
|
||||
let group = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
let r4 = rect(&mut s, 400.0, 0.0, 10.0, 10.0);
|
||||
sel(&mut s, &[group, r4]);
|
||||
let outer = id_of(&s.execute("object.blend.make", &json!({})).unwrap());
|
||||
sel(&mut s, &[outer, inner]);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let n = before.peek_next_id();
|
||||
let r = s.execute("object.blend.release", &json!({})).unwrap();
|
||||
assert_eq!(r["ids"], json!([group.0, r4.0, r1.0, r2.0]));
|
||||
assert_eq!(r["spines"], json!([n, n + 1]));
|
||||
let ids = |n: &Node| n.children().unwrap().iter().map(|c| c.id).collect::<Vec<_>>();
|
||||
assert_eq!(ids(&s.doc().unwrap().doc.layers[0]), [NodeId(n), group, r4]);
|
||||
assert_eq!(ids(&node(&s, group)), [NodeId(n + 1), r1, r2, r3]);
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [NodeId(n), NodeId(n + 1), group, r4, r1, r2]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(*s.doc().unwrap().doc == *before, "Undo gives back the document");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blend_expand_makes_group_with_unique_ids() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -152,6 +152,30 @@ fn release_while_editing_leaves_editing_first() {
|
||||
assert!(mask(&s, obj).is_some() && doc(&s).mask_edit.is_some());
|
||||
}
|
||||
|
||||
/// Releasing the masks of a group and of an object inside it puts each mask's art directly above
|
||||
/// its object, as releasing one mask at a time does.
|
||||
#[test]
|
||||
fn release_of_a_masked_group_and_a_masked_object_inside_it_keeps_order_and_ids() {
|
||||
let mut s = session();
|
||||
let (a, m1) = (rect(&mut s, 0.0, 100.0), rect(&mut s, 0.0, 50.0));
|
||||
s.execute("transparency.makeOpacityMask", &json!({"ids": [a.0, m1.0]})).unwrap();
|
||||
let b = rect(&mut s, 150.0, 100.0);
|
||||
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
let group = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
let m2 = rect(&mut s, 0.0, 300.0);
|
||||
s.execute("transparency.makeOpacityMask", &json!({"ids": [group.0, m2.0]})).unwrap();
|
||||
let before = doc(&s).clone();
|
||||
let n = before.peek_next_id();
|
||||
s.execute("transparency.releaseOpacityMask", &json!({"ids": [group.0, a.0]})).unwrap();
|
||||
let ids = |n: &vectorcraft_doc::Node| n.children().unwrap().iter().map(|c| c.id).collect::<Vec<_>>();
|
||||
assert_eq!(ids(&doc(&s).layers[0]), [group, NodeId(n)]);
|
||||
assert_eq!(ids(doc(&s).node(group).unwrap()), [a, NodeId(n + 1), b]);
|
||||
assert!(mask(&s, group).is_none() && mask(&s, a).is_none());
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [group, NodeId(n), a, NodeId(n + 1)]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(*doc(&s) == before, "Undo gives back the document");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_object_gets_an_empty_mask_drawn_in_editing_mode() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -508,6 +508,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
cursors. DPI/hotspot and preference checks in `cursors.rs` and `tests_prefs.rs` cover #1269;
|
||||
actual OS display and Caps Lock remain unverified. Behaviour reference: [public tool-pointer documentation](https://helpx.adobe.com/illustrator/using/tools.html#change_tool_pointers).
|
||||
- **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped.
|
||||
The Pathfinder panel, Offset Path, Clean Up, Divide Objects Below, Make and Release Blend, Envelope Distort's Make and Release, Repeat (Radial, Grid, Mirror and Release), Vector Halftone, New and Redefine Symbol, Delete Symbol, erasing with the Symbol Sprayer, Rasterize, Convert to Artboards, Flatten Transparency, Clear Guides, Shape Builder, Live Paint, Release Image Trace, Make and Release Opacity Mask, Delete with direct-selected anchor points, Remove Anchor Points, Cut Path, and the Shaper, Knife, Eraser, Path Eraser, Line Cut, Rectangle Cut, Mirror & Cut and Blob Brush tools add and remove objects in one pass over each layer or group they change, and Outline Stroke and Expand write each new object in the place of the object it replaces (#1429).
|
||||
Delete, Cut, Group, Ungroup, Make and Release Compound Path, Make Clipping Mask, Send to Current Layer, Duplicate, Copy in the Move, Rotate, Scale, Reflect and Shear dialogs, Transform Each with Copy and each move of an Alt-drag take time in proportion to the size of the document (#1429).
|
||||
An Alt-drag with the Selection, Direct Selection, Group Selection, Rotate, Scale, Reflect or Shear tool copies the selection on its first move, and the moves after it transform those copies; the previews of the Move, Rotate, Scale, Reflect and Shear dialogs with Copy and Preview on copy the selection once in the same way (#1429).
|
||||
The Layers panel's Delete Selection, Duplicate Selection, Collect in New Layer, Merge Selected, Flatten Artwork, Release to Layers (Sequence) and row drags also take time in proportion to the size of the document, and so do Cut Artboard and Duplicate Artboard with their art and each move of an Alt-drag of one artboard with the Artboard tool while Move/Copy Artwork with Artboard is on (#1429).
|
||||
|
||||
Reference in new issue
Block a user