Paths: Reverse Path Direction turns a compound path's own stroke width profile and pen records

Reverse Path Direction reversed each member path of a compound path and
turned the members' own stroke records, but left the compound path's
strokes as they were. A compound path's strokes run along its members'
subpaths in order, so its width profile and pen pressure, tilt, bearing
and rotation then ran the old way and the stroke drew differently.

reverse_stroke_records now also takes a compound path, reading its
members' subpaths in order, and path.reverse turns each compound path's
strokes with the combined reversal of its members before they reverse.
As for a path, the strokes turn only when every subpath turns alike.
This commit is contained in:
dotCooCoo
2026-11-14 14:52:36 -08:00
parent 8f8956f385
commit 5fcccce7ce
4 changed files with 70 additions and 14 deletions
+24 -5
View File
@@ -77,7 +77,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Reverse Path Direction",
["Object", "Path"],
None,
"{ids?|id?, subpath?: usize (reverse only this subpath of each path, 0 included; default: every subpath), reversed?: bool (true: every subpath runs counter-clockwise on screen, false: clockwise, the Attributes panel's Reverse Path Direction On / Off; omitted: flip each path; with subpath, the direction is not read: that subpath is flipped)} the paths in ids or the selection (groups and compound paths: the paths inside), as one undo step; when every subpath of a path turns alike, its strokes' width profiles (sides swapped) and pen pressure, tilt, bearing and rotation turn with it, so they draw as before → {changed: paths}",
"{ids?|id?, subpath?: usize (reverse only this subpath of each path, 0 included; default: every subpath), reversed?: bool (true: every subpath runs counter-clockwise on screen, false: clockwise, the Attributes panel's Reverse Path Direction On / Off; omitted: flip each path; with subpath, the direction is not read: that subpath is flipped)} the paths in ids or the selection (groups and compound paths: the paths inside), as one undo step; when every subpath of a path or compound path turns alike, its strokes' width profiles (sides swapped) and pen pressure, tilt, bearing and rotation turn with it, so they draw as before → {changed: paths}",
has_selected_path,
reverse
),
@@ -458,14 +458,33 @@ fn reverse(s: &mut Session, p: &Value) -> Result<Value> {
if ids.is_empty() {
return Err(bad(C, "select paths"));
}
let flip_of = |path: &PathData| -> Vec<bool> {
match subpath {
Some(si) => (0..path.subpaths.len()).map(|i| i == si).collect(),
None => path.subpaths.iter().map(|sp| want.is_none_or(|ccw| (sp.signed_area() < 0.0) != ccw)).collect(),
}
};
let changed = super::overprint::edit_counted(s, "Reverse Path Direction", |d| {
// A compound path's own strokes run along its members' subpaths in order, so they turn
// with them, read before any member turns.
let compounds: BTreeSet<NodeId> = ids.iter().map(|id| compound_or_self(d, *id)).filter(|c| ids.binary_search(c).is_err()).collect();
for c in compounds {
let Some(NodeKind::Compound { children, .. }) = d.node(c).map(|n| &n.kind) else { continue };
let flip: Vec<bool> = children
.iter()
.filter_map(|m| m.path_data().map(|pd| if ids.binary_search(&m.id).is_ok() { flip_of(pd) } else { vec![false; pd.subpaths.len()] }))
.flatten()
.collect();
if flip.contains(&true)
&& let Some(n) = d.node_mut(c)
{
vectorcraft_doc::blend::reverse_stroke_records(n, &flip);
}
}
let mut changed = 0;
for id in &ids {
let path = path_mut(d, *id)?;
let flip: Vec<bool> = match subpath {
Some(si) => (0..path.subpaths.len()).map(|i| i == si).collect(),
None => path.subpaths.iter().map(|sp| want.is_none_or(|ccw| (sp.signed_area() < 0.0) != ccw)).collect(),
};
let flip = flip_of(path);
if !flip.contains(&true) {
continue;
}
+33
View File
@@ -218,6 +218,39 @@ fn reverse_path_direction_turns_the_width_profile_and_pen_records_with_the_path(
}
}
#[test]
fn reverse_path_direction_turns_a_compound_paths_own_width_profile_and_pen_records() {
use vectorcraft_doc::{AngleProfile, PressureProfile, WidthProfile};
let mut s = session();
let (a, b) = (rect(&mut s, 0.0), rect(&mut s, 50.0));
run(&mut s, "select.set", json!({"ids": [a.0, b.0]}));
run(&mut s, "object.compoundPath.make", json!({}));
let compound = s.doc().unwrap().selection.objects[0];
// A lone rectangle of the same shape, whose strokes Reverse Path Direction already turns.
let lone = rect(&mut s, 0.0);
let profile = WidthProfile { points: vec![(0.0, 0.2, 0.6), (0.3, 1.0, 0.4), (1.0, 0.5, 0.9)] };
let pressure = PressureProfile { points: vec![(0.0, 0.1), (0.25, 0.9), (1.0, 0.4)] };
let bearing = AngleProfile { points: vec![(0.0, 10.0), (0.6, 80.0), (1.0, 170.0)] };
s.edit("set", |d, _| {
for id in [compound, lone] {
let st = d.node_mut(id).unwrap().appearance.stroke_mut().unwrap();
st.profile = Some(profile.clone());
st.pressure = Some(pressure.clone());
st.bearing = Some(bearing.clone());
}
Ok(())
})
.unwrap();
run(&mut s, "select.set", json!({"ids": [compound.0, lone.0]}));
assert_eq!(run(&mut s, "path.reverse", json!({}))["changed"], 3);
let stroke = |id: NodeId| s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().unwrap().clone();
let (got, want) = (stroke(compound), stroke(lone));
assert_ne!(want.profile.as_ref(), Some(&profile), "the lone rectangle's profile turned");
assert_eq!(got.profile, want.profile, "both members turned alike, so the compound path's profile turns as the rectangle's does");
assert_eq!(got.pressure, want.pressure);
assert_eq!(got.bearing, want.bearing);
}
#[test]
fn attributes_survive_a_native_round_trip() {
let mut s = session();