M2.1: avoid discarded path copies during deletion (#3380)

This commit is contained in:
ruchirguitar
2026-11-08 19:35:48 +05:30
parent 2dc6991cf3
commit 3984874728
6 changed files with 64 additions and 38 deletions
+13 -7
View File
@@ -764,6 +764,10 @@ fn cut(s: &mut Session, p: &Value) -> Result<Value> {
/// Remove the geometry copied by a direct-selection Cut, including selected guides in one edit.
/// Select every surviving piece while retaining intentional compound-path structure.
/// `roots` contains the copied targets: whole-object roots are removed in full, while partial paths
/// lose only the copied geometry. An exhausted partial path is removed, along with its compound
/// parent if now empty; layers remain. Returns JSON `null` on success, replacing direct-selection
/// references with surviving object IDs.
fn cut_direct_selection(s: &mut Session, roots: &[NodeId]) -> Result<Value> {
let st = s.doc()?;
let sel = st.selection.clone();
@@ -780,20 +784,22 @@ fn cut_direct_selection(s: &mut Session, roots: &[NodeId]) -> Result<Value> {
d.remove(*id)?;
continue;
}
let Some(path) = node.path_data() else {
if node.path_data().is_none() {
d.remove(*id)?;
continue;
};
}
let original = super::path::deletion_source(d, *id)?;
let seg_refs = sel.segments.get(id);
let anchor_refs = sel.anchors.get(id);
let mut subpaths = Vec::new();
for (si, sp) in path.subpaths.iter().enumerate() {
let cuts = super::clipboard::subpath_selected_segments(sp, si, seg_refs, anchor_refs);
let owned = std::mem::take(&mut super::path::path_mut(d, *id)?.subpaths);
for (si, sp) in owned.into_iter().enumerate() {
let cuts = super::clipboard::subpath_selected_segments(&sp, si, seg_refs, anchor_refs);
if cuts.is_empty() {
subpaths.push(sp.clone());
subpaths.push(sp);
} else {
subpaths.extend(super::path::without_segments(sp.clone(), &cuts));
subpaths.extend(super::path::without_segments(sp, &cuts));
}
}
subpaths.retain(|sp| sp.anchors.len() >= 2);
@@ -806,7 +812,7 @@ fn cut_direct_selection(s: &mut Session, roots: &[NodeId]) -> Result<Value> {
d.remove(p)?;
}
} else {
survivors.extend(super::path::replace_deleted_pieces(d, &mut pieces, *id, subpaths)?);
survivors.extend(super::path::replace_deleted_pieces(d, &mut pieces, original, subpaths)?);
}
}
pieces.finish(d)?;
+43 -15
View File
@@ -1,6 +1,7 @@
//! Path editing commands (used by the Pen and Direct Selection tools and Object → Path).
use std::collections::BTreeSet;
use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_doc::{Document, Node, NodeId, NodeKind, Scaling};
@@ -1191,6 +1192,9 @@ fn convert_anchors(s: &mut Session, p: &Value) -> Result<Value> {
/// Delete direct-selected anchors or segments and whole selected objects in one edit.
/// Leave surviving pieces selected, splitting ordinary contours into independent sibling objects.
/// `p.guides: true` also removes the selected ruler guides in that edit; otherwise guides remain.
/// Layers are retained, and an object removed with a selected ancestor is not removed twice.
/// Returns JSON `null` on success; selection is replaced by the surviving object IDs.
pub(crate) fn delete_anchors(s: &mut Session, p: &Value) -> Result<Value> {
// `guides: true` (Cut, #3226): the selected ruler guides go in the same undo step.
let with_guides = switch_or(p, "guides", false, "path.deleteAnchors")?;
@@ -1222,13 +1226,14 @@ pub(crate) fn delete_anchors(s: &mut Session, p: &Value) -> Result<Value> {
if d.node(*id).is_none() {
continue;
}
let original = deletion_source(d, *id)?;
let subpaths: Vec<SubPath> = {
let path = d.node(*id).and_then(|n| n.path_data()).ok_or(EngineError::NoNode(*id))?;
let path = path_mut(d, *id)?;
let subpaths = std::mem::take(&mut path.subpaths);
if let Some(refs) = segments.get(id) {
// A segment pick cuts its connection, keeping every neighboring cubic.
path.subpaths
.iter()
.cloned()
subpaths
.into_iter()
.enumerate()
.flat_map(|(si, sp)| {
let cuts = refs.iter().filter_map(|&(s, seg)| (s == si).then_some(seg)).collect();
@@ -1239,9 +1244,8 @@ pub(crate) fn delete_anchors(s: &mut Session, p: &Value) -> Result<Value> {
} else {
// Remove every selected anchor against the original indexes, keeping surviving
// stretches separate rather than joining across deleted anchors.
path.subpaths
.iter()
.cloned()
subpaths
.into_iter()
.enumerate()
.flat_map(|(si, sp)| {
let gone = set.iter().filter_map(|&(s, ai)| (s == si).then_some(ai)).collect();
@@ -1255,7 +1259,7 @@ pub(crate) fn delete_anchors(s: &mut Session, p: &Value) -> Result<Value> {
emptied.put(d, *id, vec![])?;
} else {
// Every remaining piece is selected whole, so the next Delete removes them.
survivors.extend(replace_deleted_pieces(d, &mut emptied, *id, subpaths)?);
survivors.extend(replace_deleted_pieces(d, &mut emptied, original, subpaths)?);
}
}
emptied.finish(d)?;
@@ -1265,15 +1269,39 @@ pub(crate) fn delete_anchors(s: &mut Session, p: &Value) -> Result<Value> {
ok()
}
/// Deletion splits an ordinary contour into sibling objects. Existing multi-subpath paths and
/// explicit compound members keep their compound semantics, even when a member has one contour.
/// Separate objects retain the gradient placement of the original path.
pub(super) fn replace_deleted_pieces(d: &mut Document, beside: &mut Beside, id: NodeId, subpaths: Vec<SubPath>) -> Result<Vec<NodeId>> {
let single = d.node(id).and_then(|n| n.path_data()).is_some_and(|p| p.subpaths.len() == 1);
/// Retain path `id` from `d` without copying its geometry before deletion takes its subpaths.
/// The shared snapshot supplies the original path classification and, only if needed, gradient
/// bounds after the working node's geometry has moved. Returns `NoNode` for a missing/non-path ID.
pub(super) fn deletion_source(d: &Document, id: NodeId) -> Result<Arc<Node>> {
let (parent, index, _) = d.position(id).ok_or(EngineError::NoNode(id))?;
// Verify the cached position still names this path before retaining the shared node.
d.children(parent)
.and_then(|children| children.get(index))
.filter(|n| n.id == id && n.path_data().is_some())
.cloned()
.ok_or(EngineError::NoNode(id))
}
/// Replace the deleted path in `d` with the nonempty surviving runs in `subpaths`.
/// `original` must be retained by [`deletion_source`] before taking the working node's geometry.
/// An ordinary single-contour source becomes siblings; multi-subpath paths and compound members
/// retain their node. Only actual splits pin unplaced gradients to `original`'s bounds; already
/// placed gradients and connected remainders retain their placement behavior.
///
/// Returns the surviving IDs in `subpaths` order (the first keeps the source ID). The original
/// node's geometry is updated immediately. Replacements are handed to `beside`, which may defer
/// insertion; the caller must finish that [`Beside`] before using new IDs in `d`.
/// Missing/invalid paths return an error.
pub(super) fn replace_deleted_pieces(d: &mut Document, beside: &mut Beside, original: Arc<Node>, subpaths: Vec<SubPath>) -> Result<Vec<NodeId>> {
let id = original.id;
let single = original.path_data().is_some_and(|p| p.subpaths.len() == 1);
if single && compound_or_self(d, id) == id {
// Pin only actual splits: each new object's smaller bounds would otherwise refit its gradients.
if subpaths.len() > 1 {
d.node_mut(id).ok_or(EngineError::NoNode(id))?.pin_gradients();
if subpaths.len() > 1
&& original.appearance.has_unplaced_gradient()
&& let Some(bounds) = original.geometric_bounds()
{
d.node_mut(id).ok_or(EngineError::NoNode(id))?.appearance.pin_gradients(bounds);
}
super::draw2::replace_with_pieces(d, beside, id, subpaths.into_iter().map(PathData::single).collect())
} else {