prioritize bounding-box handles over mirror axis

This commit is contained in:
AJ Richardson
2026-10-18 19:02:44 +07:00
committed by bflatastic
parent 63cebd392a
commit 411f5f7680
7 changed files with 52 additions and 8 deletions
+6 -1
View File
@@ -56,6 +56,11 @@ impl MirrorAxis {
if axis.ends.iter().any(|q| q.distance(p) <= cx.tol(9.0)) {
return Some((axis, true));
}
// The center is its own move handle. Keep it ahead of bounding-box handles when the two
// happen to overlap; the rest of the axis line yields to those handles below.
if axis.center.distance(p) <= cx.tol(7.0) {
return Some((axis, false));
}
let d = axis.ends[1] - axis.ends[0];
let length = d.hypot();
if !length.is_finite() || length <= 0.0 {
@@ -63,7 +68,7 @@ impl MirrorAxis {
}
let unit = d / length;
let along = (p - axis.ends[0]).dot(unit).clamp(0.0, length);
((axis.ends[0] + unit * along).distance(p) <= cx.tol(7.0)).then_some((axis, false))
((axis.ends[0] + unit * along).distance(p) <= cx.tol(7.0) && !crate::select::bbox_handle_at(cx, p)).then_some((axis, false))
}
}
+6
View File
@@ -296,6 +296,12 @@ fn box_hit(cx: &ToolContext, b: &OrientedBox, p: Point) -> Option<BoxHit> {
in_rotate_zone(b.rect, lp, tol, cx.tol(18.0)).map(|_| BoxHit::Rotate)
}
/// Whether the current selection's bounding-box handle is under `p`, using the same geometry as
/// the Selection tool's press handling.
pub(crate) fn bbox_handle_at(cx: &ToolContext, p: Point) -> bool {
cx.show_bbox && selection_box(cx).is_some_and(|b| matches!(box_hit(cx, &b, p), Some(BoxHit::Handle(_))))
}
/// How near (screen points) the pointer must come to an object's centre or anchor for Smart Guides
/// to label it while hovering.
const HOVER_REACH: f64 = 6.0;
+28
View File
@@ -5041,6 +5041,34 @@ mod tests {
}
}
#[test]
fn top_bounding_box_handle_scales_a_default_mirror_repeat() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 100})).unwrap();
app.run("object.repeat.mirror", json!({})).unwrap();
app.ui.task_bar = false;
app.ui.view.rulers = false;
let ctx = egui::Context::default();
frame(&mut app, &ctx, vec![]);
let initial = app.selection_bounds().unwrap();
let bx = app.selection_box().unwrap();
let top = bx.to_doc() * vectorcraft_tools::bbox::Handle::Top.pos(bx.rect);
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
let from = xf.to_screen(top);
let to = from + vec2(0.0, -30.0);
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(from), button(from, true)]);
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(to)]);
frame(&mut app, &ctx, vec![button(to, false)]);
let scaled = app.selection_bounds().unwrap();
assert!(scaled.height() > initial.height() + 10.0, "Top handle should scale the repeat: {initial:?} -> {scaled:?}");
assert!((scaled.width() - initial.width()).abs() < 0.01, "Top handle should not scale width: {initial:?} -> {scaled:?}");
}
/// #1236: Curvature edits points, so existing paths show one circular marker per point and
/// no direction handles; switching back to Direct Selection keeps its normal handles.
#[test]
+3 -2
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -2
View File
File diff suppressed because one or more lines are too long