#1163: Release splits a path of several subpaths into one path each

A path of several subpaths (an SVG `d` with several `M`s, PDF paths, a
path drawn in one go over MCP) is a compound path in Illustrator, and
Object › Compound Path › Release splits it into separate paths. Here
Release only acted on compound path objects, so such a path couldn't be
broken apart. Release now gives each subpath a path of its own, with the
path's paint, in stacking order; the first keeps the path's id. One undo
step, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-11 17:36:12 +07:00
parent e428f38767
commit 9b12fff215
3 changed files with 70 additions and 11 deletions

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@ use vectorcraft_color::{BlendMode, Paint};
use vectorcraft_doc::corners::set_corners;
use vectorcraft_doc::{Appearance, Document, Knockout, LiveCorners, LiveShape, Node, NodeId, NodeKind, OrientedBox, Scaling};
use vectorcraft_geom::shapes::CornerKind;
use vectorcraft_geom::{Affine, FillRule, Point, Rect, Vec2};
use vectorcraft_geom::{Affine, FillRule, PathData, Point, Rect, Vec2};
use super::edit::{duplicate_in, selected_roots};
use super::opacitymask::percent;
@ -106,7 +106,15 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!("object.hide", "Selection", ["Object", "Hide"], Some("Cmd+3"), "{}", has_selection, hide),
cmd!("object.showAll", "Show All", ["Object"], Some("Cmd+Alt+3"), "{}", has_doc, show_all),
cmd!("object.compoundPath.make", "Make", ["Object", "Compound Path"], Some("Cmd+8"), "{}", has_selection, compound_make),
cmd!("object.compoundPath.release", "Release", ["Object", "Compound Path"], Some("Cmd+Alt+Shift+8"), "{}", has_selection, compound_release),
cmd!(
"object.compoundPath.release",
"Release",
["Object", "Compound Path"],
Some("Cmd+Alt+Shift+8"),
"{} release the selected compound paths into their paths, each with the compound path's paint; a path of several subpaths (an SVG `d` with several `M`s) splits into one path per subpath",
has_selection,
compound_release
),
cmd!(
"object.clippingMask.make",
"Make",
@ -658,26 +666,52 @@ fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
Ok(json!({ "id": id.0 }))
}
/// Is `n` a compound path to release: a compound path, or a path of several subpaths (an SVG `d`
/// with several `M`s, a PDF path), which is one in Illustrator too (#1163)?
fn releases(n: &Node) -> bool {
match &n.kind {
NodeKind::Compound { .. } => true,
NodeKind::Path { path, .. } => path.subpaths.len() > 1,
_ => false,
}
}
fn compound_release(s: &mut Session, _: &Value) -> Result<Value> {
let ids = selected_roots(s)?;
// With no compound path selected, the document, the selection and the undo history stay as
// they are.
let d = &s.doc()?.doc;
if !ids.iter().any(|id| d.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Compound { .. }))) {
if !ids.iter().any(|id| d.node(*id).is_some_and(releases)) {
return ok();
}
s.edit("Release Compound Path", |d, sel| {
let mut out = vec![];
for id in &ids {
let Some(n) = d.node(*id).cloned() else { continue };
let NodeKind::Compound { children, .. } = &n.kind else { continue };
let Some(n) = d.node(*id).cloned().filter(releases) else { continue };
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
d.remove(*id)?;
for (k, c) in children.iter().enumerate() {
let mut c = (**c).clone();
c.appearance = n.appearance.clone();
out.push(c.id);
d.insert(par, idx + k, c)?;
match &n.kind {
NodeKind::Compound { children, .. } => {
for (k, c) in children.iter().enumerate() {
let mut c = (**c).clone();
c.appearance = n.appearance.clone();
out.push(c.id);
d.insert(par, idx + k, c)?;
}
}
// Each subpath becomes a path of its own with the path's paint, in the same
// stacking order; the first keeps the path's id.
NodeKind::Path { path, .. } => {
for (k, sp) in path.subpaths.iter().enumerate() {
let mut c = if k == 0 { n.clone() } else { d.reid(&n) };
if let NodeKind::Path { path, live, .. } = &mut c.kind {
(*path, *live) = (PathData::single(sp.clone()), None);
}
out.push(c.id);
d.insert(par, idx + k, c)?;
}
}
_ => {}
}
}
sel.set(out);

View File

@ -381,6 +381,31 @@ fn clipping_and_compound() {
assert_eq!(s.doc().unwrap().selection.objects, vec![b]);
}
/// #1163: a path of several subpaths (an SVG `d` with several `M`s) is a compound path to Release:
/// one path per subpath, each with the path's paint, in its stacking order, in one undo step.
#[test]
fn release_splits_a_path_of_several_subpaths() {
let mut s = session();
let id = NodeId(
s.execute("path.create", &json!({"d": "M0 0 L10 0 L10 10 Z M20 0 L30 0 L30 10 Z M40 0 L50 0 L50 10 Z"})).unwrap()["id"].as_u64().unwrap(),
);
s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap();
let undo = s.doc().unwrap().history.undo.len();
s.execute("object.compoundPath.release", &json!({})).unwrap();
let st = s.doc().unwrap();
assert_eq!(st.history.undo.len(), undo + 1);
let sel = st.selection.objects.clone();
assert_eq!((sel.len(), sel[0]), (3, id), "the first keeps its id");
let layer = st.doc.layers[0].children().unwrap();
for (k, n) in sel.iter().map(|i| st.doc.node(*i).unwrap()).enumerate() {
let NodeKind::Path { path, .. } = &n.kind else { panic!("a path") };
assert_eq!(path.subpaths.len(), 1);
assert_eq!(path.bounds().unwrap().x0, 20.0 * k as f64, "in order");
assert_eq!(n.appearance.fill_paint().color().unwrap().to_hex(), "#ff0000", "the path's paint");
assert_eq!(layer[k].id, n.id, "stacked bottom to top");
}
}
/// `object.align` without `horizontal` or `vertical` (an unknown key such as `align` counts as
/// neither) returns an error: nothing moves, and the undo and redo stacks stay as they were.
#[test]