mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 17:32:42 -06:00
M2.1: avoid discarded path copies during deletion (#3380)
This commit is contained in:
@@ -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)?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user