#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:
parent
e428f38767
commit
9b12fff215
File diff suppressed because one or more lines are too long
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in New Issue