Fix #504: Convert corner/smooth for a path selected as a whole with the anchor tools
Reported: Alt-click with the Pen should toggle an anchor between corner and smooth, and the Control bar had no way to make a corner anchor smooth. v0.5.0 already has Pen + Alt as the Anchor Point tool (#473) and the Control bar's and the Properties panel's Convert buttons for direct-selected anchors, the path being drawn included (its last anchor is selected). The remaining gap: with Direct Selection, the Pen, the Curvature or the anchor tools, a path selected as a whole shows all its anchors selected, yet the Convert buttons only appeared once anchors were direct-selected. - chrome::anchor_controls decides what the Control bar and the Properties panel show: Convert and Anchors for direct-selected anchors (as before), Convert alone for paths selected as a whole with a tool that edits anchors (path.convertAnchors already converts every anchor of those); Remove and Cut still wait for anchors direct-selected, as they act on those only. - Tests: the Control bar and the Properties panel offer Convert for a whole rectangle with Direct Selection and the Pen, not with the Selection tool, and smooth converts every corner; Pen + Alt on the path being drawn makes a smooth anchor a corner (click) and a corner smooth (drag), and drawing goes on. ROADMAP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f6cd4588ee
commit
ac63002df9
File diff suppressed because one or more lines are too long
|
|
@ -766,3 +766,27 @@ fn pen_drag_from_the_first_anchor_closes_the_path() {
|
|||
gesture(&mut s, &[(500.0, 500.0)], none);
|
||||
assert_eq!(paths(&s).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pen_with_alt_converts_the_anchors_of_the_path_being_drawn() {
|
||||
// #504: while drawing, Alt-click on a smooth anchor of the path makes it a corner and Alt-drag
|
||||
// on a corner one pulls out smooth handles; the next click goes on drawing the path.
|
||||
let (none, alt) = (Mods::default(), Mods { alt: true, ..Mods::default() });
|
||||
let v = view();
|
||||
let mut s = session();
|
||||
s.select_tool("pen", v).unwrap();
|
||||
gesture(&mut s, &[(100.0, 300.0)], none);
|
||||
gesture(&mut s, &[(200.0, 300.0), (240.0, 300.0)], none);
|
||||
gesture(&mut s, &[(300.0, 400.0)], none);
|
||||
gesture(&mut s, &[(400.0, 300.0)], none);
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
assert!(smooth(path(&s, id).subpaths[0].anchors[1]));
|
||||
assert_eq!(gesture(&mut s, &[(200.0, 300.0)], alt), 1);
|
||||
let a = path(&s, id).subpaths[0].anchors[1];
|
||||
assert!(!a.has_in() && !a.has_out(), "a corner: {a:?}");
|
||||
assert_eq!(gesture(&mut s, &[(300.0, 400.0), (320.0, 400.0), (340.0, 400.0)], alt), 1);
|
||||
let a = path(&s, id).subpaths[0].anchors[2];
|
||||
assert!(smooth(a) && a.h_out == Point::new(340.0, 400.0), "smooth: {a:?}");
|
||||
gesture(&mut s, &[(500.0, 400.0)], none);
|
||||
assert_eq!((paths(&s).len(), path(&s, id).subpaths[0].anchors.len()), (1, 5));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,10 +108,32 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
/// An anchor button: icon, tooltip, command and the convert command's `to`.
|
||||
type AnchorButton<'a> = (&'a str, &'a str, &'a str, Option<&'a str>);
|
||||
|
||||
/// What the Control bar and the Properties panel offer for direct-selected anchors: "Convert:"
|
||||
/// corner or smooth, then "Anchors:" remove, connect (Join) and cut. Each group is a row of its
|
||||
/// own: inline in the Control bar, one under the other in a panel.
|
||||
pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
/// The anchor controls the Control bar and the Properties panel show.
|
||||
#[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.
|
||||
Convert,
|
||||
/// Anchors direct-selected: "Convert:" and "Anchors:".
|
||||
All,
|
||||
}
|
||||
|
||||
/// Which anchor controls show for the selection and the active tool.
|
||||
pub fn anchor_controls(app: &VectorcraftApp) -> AnchorControls {
|
||||
let Some(st) = app.session.active() else { return AnchorControls::None };
|
||||
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 { .. })));
|
||||
if paths && vectorcraft_tools::catalog::edits_anchors(app.session.tool_id()) { AnchorControls::Convert } else { AnchorControls::None }
|
||||
}
|
||||
|
||||
/// What the Control bar and the Properties panel offer for selected anchors ([`anchor_controls`]):
|
||||
/// "Convert:" corner or smooth, then "Anchors:" remove, connect (Join) and cut. Each group is a
|
||||
/// row of its own: inline in the Control bar, one under the other in a panel.
|
||||
pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui, controls: AnchorControls) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut run = None;
|
||||
let groups: [(&str, &[AnchorButton]); 2] = [
|
||||
|
|
@ -131,7 +153,12 @@ pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
],
|
||||
),
|
||||
];
|
||||
for (label, buttons) in groups {
|
||||
let shown = match controls {
|
||||
AnchorControls::None => 0,
|
||||
AnchorControls::Convert => 1,
|
||||
AnchorControls::All => groups.len(),
|
||||
};
|
||||
for (label, buttons) in groups.into_iter().take(shown) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(label).size(12.0).color(t.text));
|
||||
for &(icon, tip, id, to) in buttons {
|
||||
|
|
@ -166,9 +193,9 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let sel = st.selection.objects.clone();
|
||||
let units = app.session.general_unit();
|
||||
let first = sel.first().and_then(|id| st.doc.node(*id)).cloned();
|
||||
let anchor_mode = !st.selection.anchors.is_empty();
|
||||
let anchors = anchor_controls(app);
|
||||
let label = match &first {
|
||||
Some(_) if sel.len() == 1 && anchor_mode => tl!("Anchor Point"),
|
||||
Some(_) if sel.len() == 1 && anchors != AnchorControls::None => tl!("Anchor Point"),
|
||||
Some(vectorcraft_doc::Node { kind: NodeKind::Image(im), .. }) if sel.len() == 1 => {
|
||||
if im.link.is_some() {
|
||||
tl!("Linked File")
|
||||
|
|
@ -180,8 +207,8 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
_ => tl!(crate::panels::appearance::object_label(app)),
|
||||
};
|
||||
ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text));
|
||||
if anchor_mode {
|
||||
anchor_buttons(app, ui);
|
||||
if anchors != AnchorControls::None {
|
||||
anchor_buttons(app, ui, anchors);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
// An image or an Image Trace object shows its own controls in place of Fill and Stroke.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
return;
|
||||
};
|
||||
let n_sel = st.selection.len();
|
||||
let anchor_mode = !st.selection.anchors.is_empty();
|
||||
let anchors = crate::chrome::anchor_controls(app);
|
||||
let first = first_selected(app);
|
||||
let label = match (&first, n_sel) {
|
||||
(None, _) => tl!("Document").to_string(),
|
||||
|
|
@ -85,10 +85,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
});
|
||||
}
|
||||
if anchor_mode {
|
||||
if anchors != crate::chrome::AnchorControls::None {
|
||||
divider(ui);
|
||||
section_header(ui, tl!("Anchor Point"));
|
||||
crate::chrome::anchor_buttons(app, ui);
|
||||
crate::chrome::anchor_buttons(app, ui, anchors);
|
||||
}
|
||||
divider(ui);
|
||||
section_header(ui, tl!("Quick Actions"));
|
||||
|
|
|
|||
|
|
@ -205,3 +205,26 @@ fn the_control_bar_converts_connects_and_cuts_selected_anchors() {
|
|||
assert!(app.session.active().unwrap().doc.node(id).unwrap().path_data().unwrap().subpaths[0].closed);
|
||||
assert_eq!(anchor_count(&app, id), 4, "the coincident ends merge");
|
||||
}
|
||||
|
||||
/// #504: with a tool that edits anchors (Direct Selection, the Pen…), a path selected as a whole
|
||||
/// shows all its anchors selected, and the Control bar and the Properties panel offer to convert
|
||||
/// them; removing and cutting wait for anchors direct-selected. The Selection tool shows neither.
|
||||
#[test]
|
||||
fn a_whole_path_converts_with_the_tools_that_edit_anchors() {
|
||||
let mut app = app();
|
||||
let id = rect(&mut app);
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
app.select_tool("selection");
|
||||
assert!(!has(&control_frame(&mut app, &ctx, vec![]), "Convert:"));
|
||||
for tool in ["directSelection", "pen"] {
|
||||
app.select_tool(tool);
|
||||
let texts = control_frame(&mut app, &ctx, vec![]);
|
||||
assert!(has(&texts, "Convert:") && !has(&texts, "Anchors:"), "{tool}: {texts:?}");
|
||||
let props = crate::tests_labels::painted_text(&mut app, crate::panels::properties::show);
|
||||
assert!(props.contains("Convert:") && !props.contains("Anchors:"), "{tool}: {props}");
|
||||
}
|
||||
click_button(&mut app, &ctx, "Convert:", 1);
|
||||
let sp = app.session.active().unwrap().doc.node(id).unwrap().path_data().unwrap().subpaths[0].clone();
|
||||
assert!(sp.anchors.iter().all(|a| a.has_in() && a.has_out()), "every corner made smooth: {sp:?}");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue