mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Merge pull request #4857 from dotCooCoo/fix/issue-4819
Fix #4819: Convert to Smooth and Corner convert a selected blend's spine
This commit is contained in:
@@ -112,7 +112,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Convert Anchor Points",
|
||||
[],
|
||||
None,
|
||||
"{to: \"corner\"|\"smooth\"} convert the direct-selected anchors (all anchors of fully selected paths) as one undo step: corner retracts both handles, smooth pulls handles in line with the neighbours (smooth anchors keep theirs)",
|
||||
"{to: \"corner\"|\"smooth\"} convert the direct-selected anchors (all anchors of fully selected paths, every point of a selected blend's spine) as one undo step: corner retracts both handles, smooth pulls handles in line with the neighbours (smooth anchors keep theirs)",
|
||||
has_selection,
|
||||
convert_anchors
|
||||
),
|
||||
@@ -1123,7 +1123,11 @@ fn convert_anchors(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => return Err(bad(C, "`to` must be \"corner\" or \"smooth\"")),
|
||||
};
|
||||
let t = anchor_targets(s)?;
|
||||
if t.is_empty() {
|
||||
// A selected blend converts every point of its spine, as a path selected whole does.
|
||||
let st = s.doc()?;
|
||||
let blends: Vec<NodeId> =
|
||||
st.selection.objects.iter().copied().filter(|id| st.doc.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Blend { .. }))).collect();
|
||||
if t.is_empty() && blends.is_empty() {
|
||||
return Err(bad(C, "select paths or anchor points"));
|
||||
}
|
||||
s.edit("Convert Anchor Points", |d, _| {
|
||||
@@ -1138,6 +1142,24 @@ fn convert_anchors(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in &blends {
|
||||
let Some(NodeKind::Blend { children, spec }) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
|
||||
let Some((mut path, anchors)) = vectorcraft_doc::live::pin_spine(children, spec) else { continue };
|
||||
let pinned = path.clone();
|
||||
let Some(sp) = path.subpaths.first_mut() else { continue };
|
||||
for ai in 0..sp.anchors.len() {
|
||||
if smooth {
|
||||
sp.smooth_anchor(ai);
|
||||
} else if let Some(a) = sp.anchors.get_mut(ai) {
|
||||
a.retract();
|
||||
}
|
||||
}
|
||||
// A spine the conversion leaves as it was stays as the blend has it.
|
||||
if path != pinned {
|
||||
spec.key_anchors = anchors.iter().map(|a| u32::try_from(*a).unwrap_or(u32::MAX)).collect();
|
||||
spec.spine = Some(path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
|
||||
@@ -115,6 +115,35 @@ fn spine_conversion_rejects_invalid_arguments_without_mutation() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #4819: Convert Selected Anchor Points to Smooth or Corner (the Control bar's and the Contextual
|
||||
/// Task Bar's buttons) converts the points of a selected blend's spine, as it converts every
|
||||
/// anchor of a path selected whole, in one undo step, the keys staying put.
|
||||
#[test]
|
||||
fn converting_anchors_of_a_selected_blend_converts_its_spine() {
|
||||
let mut s = session();
|
||||
let (a, b, g) = blend(&mut s);
|
||||
s.execute("object.blend.spine.addAnchor", &json!({"id": g.0, "x": 55, "y": 5})).unwrap();
|
||||
s.execute("object.blend.spine.moveAnchor", &json!({"id": g.0, "anchor": 1, "x": 55, "y": 105})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [g.0]})).unwrap();
|
||||
let keys = spec(&s, g).key_anchors;
|
||||
s.execute("path.convertAnchors", &json!({"to": "smooth"})).unwrap();
|
||||
let sp = spec(&s, g);
|
||||
assert_eq!(sp.key_anchors, keys);
|
||||
let middle = sp.spine.as_ref().unwrap().subpaths[0].anchors[1];
|
||||
assert_eq!(middle.p, Point::new(55.0, 105.0));
|
||||
assert!(middle.has_in() && middle.has_out(), "{middle:?}");
|
||||
assert!((middle.h_in.y - 105.0).abs() < 1e-9 && (middle.h_out.y - 105.0).abs() < 1e-9, "tangent to the chord between its neighbors: {middle:?}");
|
||||
assert!(middle.h_in.x < 55.0 && middle.h_out.x > 55.0, "{middle:?}");
|
||||
assert_eq!(center(&node(&s, a)), Point::new(5.0, 5.0));
|
||||
assert_eq!(center(&node(&s, b)), Point::new(105.0, 5.0));
|
||||
|
||||
s.execute("path.convertAnchors", &json!({"to": "corner"})).unwrap();
|
||||
let corner = spec(&s, g).spine.unwrap().subpaths[0].anchors.clone();
|
||||
assert!(corner.iter().all(|a| !a.has_in() && !a.has_out()), "{corner:?}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(spec(&s, g).spine.unwrap().subpaths[0].anchors[1], middle, "one undo step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_a_key_point_moves_the_key_and_the_other_way_round() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -7,9 +7,9 @@ type AnchorButton<'a> = (&'a str, &'a str, &'a str, Option<&'a str>);
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AnchorControls {
|
||||
None,
|
||||
/// Paths selected as a whole with a tool that edits anchors (Direct Selection, the Pen…),
|
||||
/// which shows all their anchors selected: "Convert:" only, as removing or cutting at every
|
||||
/// anchor isn't what they're for.
|
||||
/// Paths or blends selected as a whole with a tool that edits anchors (Direct Selection, the
|
||||
/// Pen…), which shows all their anchors selected: "Convert:" only, as removing or cutting at
|
||||
/// every anchor isn't what they're for. A blend converts the points of its spine.
|
||||
Convert,
|
||||
/// Anchors direct-selected: "Convert:" and "Anchors:".
|
||||
All,
|
||||
@@ -23,7 +23,8 @@ pub fn anchor_controls(app: &VectorcraftApp) -> AnchorControls {
|
||||
if !st.selection.anchors.is_empty() {
|
||||
return AnchorControls::All;
|
||||
}
|
||||
let paths = st.selection.objects.iter().any(|id| st.doc.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Path { .. })));
|
||||
let paths =
|
||||
st.selection.objects.iter().any(|id| st.doc.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Path { .. } | NodeKind::Blend { .. })));
|
||||
if paths && vectorcraft_tools::catalog::edits_anchors(app.session.tool_id()) { AnchorControls::Convert } else { AnchorControls::None }
|
||||
}
|
||||
|
||||
|
||||
@@ -1031,6 +1031,23 @@ fn the_control_bar_isolates_the_selected_object_and_exits_isolation_mode() {
|
||||
assert_eq!(isolation_button(&mut app, &ctx, vec![]), None, "the isolation bar ends pattern editing");
|
||||
}
|
||||
|
||||
/// #4819: with Direct Selection, a selected blend shows the Control bar's and the Properties
|
||||
/// panel's Convert buttons, as a path selected whole does; with the Selection tool it doesn't.
|
||||
#[test]
|
||||
fn a_blend_selected_with_direct_selection_shows_the_convert_buttons() {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 200})).unwrap();
|
||||
let rect = |app: &mut crate::VectorcraftApp, x: f64| {
|
||||
app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 30, "height": 20})).unwrap()["id"].as_u64().unwrap()
|
||||
};
|
||||
let (a, b) = (rect(&mut app, 10.0), rect(&mut app, 200.0));
|
||||
let blend = app.run("object.blend.make", json!({"ids": [a, b], "steps": 1})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [blend]})).unwrap();
|
||||
assert!(super::anchor_controls(&app) == super::AnchorControls::None, "the Selection tool");
|
||||
app.select_tool("directSelection");
|
||||
assert!(super::anchor_controls(&app) == super::AnchorControls::Convert);
|
||||
}
|
||||
|
||||
/// The Zoom tool's hint follows Performance › Animated Zoom (#394): a drag zooms as it goes,
|
||||
/// or (off) zooms into the area dragged across.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user