fix: 修复多选矢量点时变换框的交互逻辑

修复了多选矢量点时,变换框角点会误判为可拖动的点的问题,同时更新了相关注释和测试用例,确保未被点占据的角点和边缘仍可用于缩放操作。
This commit is contained in:
Nicek
2026-10-22 13:52:06 +08:00
parent d3a1538614
commit 90004dc7cd
4 changed files with 48 additions and 21 deletions
+5 -3
View File
@@ -85,14 +85,16 @@ pub enum BoxHit {
}
/// The box's part under page point `p` when it wins over `h`, the network's part there: Bezier
/// handles come first, then the box's corners, points, the box's edges, segments, the rotation
/// zones, and the inside of the box last. Edges along an axis without extent are left out.
/// handles come first, then points, the box's corners, its edges, segments and the rotation zones,
/// and the inside of the box last. A point under a corner or an edge is dragged with the others
/// (the box draws no handles of its own), so it keeps the box off. Edges along an axis without
/// extent are left out.
pub fn box_hit(doc: &Document, ve: &VectorEdit, h: Option<VHit>, p: Point, zoom: f64) -> Option<BoxHit> {
let b = shown_box(doc, ve)?;
match handle_at_corners(&b.corners(), p, zoom, false) {
Some(handle) => {
let wins = match (handle, h) {
(_, Some(VHit::Handle { .. })) => false,
(_, Some(VHit::Handle { .. } | VHit::Vertex { .. })) => false,
(Handle::Corner(_), _) => true,
(Handle::Edge(e), None | Some(VHit::Segment { .. })) => {
if e % 2 == 0 {
+39 -16
View File
@@ -7,6 +7,7 @@ use super::tests::{click, drag, ev, ex, net, pt, rect, ve, vertex_at};
use super::transform::{BoxHit, box_hit, shown_box};
use super::*;
use crate::EngineError;
use crate::params::id_of;
use crate::tools::{Handle, PointerKind, ToolId};
/// Page position of point `v` of vector `id`.
@@ -47,13 +48,15 @@ fn the_box_shows_for_two_or_more_points_with_the_move_tool() {
let b = shown_box(s.doc().unwrap(), &ve(&s)).unwrap();
assert_eq!((b.width, b.height, b.points.len()), (100.0, 100.0, 4));
assert!(near(b.corners()[2], pt(100.0, 100.0)));
// Corners win over the points under them, points over edges, edges over segments.
// A point under a corner is dragged with the others, not scaled (the box draws no handles of
// its own), so it keeps the box off; corners and edges clear of points still scale, and edges
// win over segments.
let doc = s.doc().unwrap().clone();
let at = |x, y| {
let h = hit(&doc, &ve(&s), pt(x, y), 6.0);
box_hit(&doc, &ve(&s), h, pt(x, y), 1.0)
};
assert_eq!(at(100.0, 100.0), Some(BoxHit::Handle(Handle::Corner(2))));
assert_eq!(at(100.0, 100.0), None, "a point under the corner: a drag moves the points");
assert_eq!(at(100.0, 50.0), Some(BoxHit::Handle(Handle::Edge(1))));
assert_eq!(at(110.0, -10.0), Some(BoxHit::Handle(Handle::Rotate(1))));
assert_eq!(at(40.0, 60.0), Some(BoxHit::Inside));
@@ -64,27 +67,47 @@ fn the_box_shows_for_two_or_more_points_with_the_move_tool() {
}
#[test]
fn corners_scale_points_in_one_undo_step() {
fn corners_clear_of_points_scale_them_in_one_undo_step() {
let mut s = Session::new();
let id = rect(&mut s);
// An L of three points: the box's bottom-right corner (50, 50) has no point under it.
let id = id_of(&ex(&mut s, "vector.create", json!({"points": [[0, 0], [50, 0], [0, 50]], "closed": false}))["id"]).unwrap();
ex(&mut s, "vector.edit.begin", json!({"id": id}));
select_all(&mut s);
drag(&mut s, (100.0, 100.0), (150.0, 120.0));
let b = s.doc().unwrap().world_bounds(id).unwrap();
assert_eq!((b.x0, b.y0, b.x1, b.y1), (0.0, 0.0, 150.0, 120.0));
assert_eq!(ve(&s).vertices.len(), 4, "the points stay selected");
drag(&mut s, (50.0, 50.0), (150.0, 120.0));
assert!(near(page_pt(&s, id, 1), pt(150.0, 0.0)) && near(page_pt(&s, id, 2), pt(0.0, 120.0)), "{:?}", page_pt(&s, id, 1));
assert_eq!(ve(&s).vertices.len(), 3, "the points stay selected");
s.undo().unwrap();
let b = s.doc().unwrap().world_bounds(id).unwrap();
assert_eq!((b.width(), b.height()), (100.0, 100.0), "one undo step");
assert!(near(page_pt(&s, id, 1), pt(50.0, 0.0)), "one undo step");
// Shift keeps proportions; Alt scales from the centre.
select_all(&mut s);
drag_with(&mut s, (100.0, 100.0), (150.0, 110.0), true, false);
let b = s.doc().unwrap().world_bounds(id).unwrap();
assert_eq!((b.x0, b.y0, b.x1, b.y1), (0.0, 0.0, 150.0, 150.0));
drag_with(&mut s, (50.0, 50.0), (150.0, 110.0), true, false);
assert!(near(page_pt(&s, id, 1), pt(150.0, 0.0)) && near(page_pt(&s, id, 2), pt(0.0, 150.0)), "{:?}", page_pt(&s, id, 2));
s.undo().unwrap();
select_all(&mut s);
drag_with(&mut s, (100.0, 100.0), (110.0, 120.0), false, true);
let b = s.doc().unwrap().world_bounds(id).unwrap();
assert_eq!((b.x0, b.y0, b.x1, b.y1), (-10.0, -20.0, 110.0, 120.0));
drag_with(&mut s, (50.0, 50.0), (60.0, 70.0), false, true);
assert!(
near(page_pt(&s, id, 0), pt(-10.0, -20.0)) && near(page_pt(&s, id, 1), pt(60.0, -20.0)) && near(page_pt(&s, id, 2), pt(-10.0, 70.0)),
"{:?}",
page_pt(&s, id, 2)
);
}
#[test]
fn dragging_one_of_several_selected_points_moves_them_all_freely() {
let mut s = Session::new();
let id = rect(&mut s);
let n = net(&s, id);
let (a, b) = (vertex_at(&n, 0.0, 0.0), vertex_at(&n, 100.0, 0.0));
// Two points on one side: their box is a flat line, and the press is on a corner of it.
ex(&mut s, "vector.edit.select", json!({"vertices": [a, b]}));
drag(&mut s, (100.0, 0.0), (130.0, 40.0));
assert!(
near(page_pt(&s, id, a), pt(30.0, 40.0)) && near(page_pt(&s, id, b), pt(130.0, 40.0)),
"{:?} {:?}",
page_pt(&s, id, a),
page_pt(&s, id, b)
);
assert_eq!(ve(&s).vertices.len(), 2, "both stay selected");
}
#[test]
+3 -1
View File
@@ -1039,8 +1039,10 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
if width_tool {
width_overlay(app, painter, &doc, &ve, zoom);
} else if let Some(c) = app.session.vector_edit_box() {
// The box around several selected points draws no corner handles: they would read as
// points. Its corners and edges still scale the points when dragged.
let label = format!("{} × {}", fmt((c[1] - c[0]).hypot()), fmt((c[3] - c[0]).hypot()));
size_label = transform_box(app, painter, c.map(|p| app.to_screen(p.x, p.y)), accent(painter), true, 4, Some(label));
size_label = transform_box(app, painter, c.map(|p| app.to_screen(p.x, p.y)), accent(painter), true, 0, Some(label));
}
}
// Selection.
+1 -1
View File
@@ -82,7 +82,7 @@ interaction feel not yet compared.
| Rectangle, ellipse (arc handles and Design panel arc fields), polygon, star, line, arrow; Shift/Alt | done | Alt draws lines from the centre too (`shapes_behaviour_tests`) |
| Corner radius handles (inside each corner of a rectangle or frame, a polygon's or star's top point; Alt: one corner), star ratio handle; Count and Ratio fields | done | handles hide under 48 px on screen; no count handle on the canvas (the Count field instead); corner smoothing rounds rectangles and frames only |
| Pen: vector networks, branch, close, bend; pencil | done | pen pressure on Windows and from browser styluses only; outside edit mode the Pen closes on its first point, connects to its other points and continues a selected open path from an end point (`vector_multi_behaviour_tests`); Escape or Enter ends the path and keeps its layer in edit mode with the pen, so the next path joins the same vector and another Escape leaves edit mode (#760, `pen_end_tests`) |
| Vector edit mode: bend, paint bucket, lasso, cut, eraser, transform box, point snapping, mirroring | done | a double-click a few pixels off a thin stroke enters edit mode (#646); a point dropped on another point merges with it, and dropping one end of an open path on the other closes it (a 2-point line's end just moves, #401); Flatten keeps open paths open (#377); paint bucket drags across areas; cut drags divide a shape into layers; delete and heal follows the curve; rounded points are drawn; Shift-click deselects a point at a corner of the box (`vector_behaviour_tests`); several vectors edited at once, Shift-click handles to drag them together, a cap and join per point, rounded points beside curves (`vector_multi_behaviour_tests`); filled regions with the Move tool: the area under the pointer is hatched ("/" lines in the selection colour, clipped by the area's own fill rule, holes left out, gone 3.5 px from a point or 2 px from a segment at any zoom), the line spacing follows octave folding (fitted to measurements at several zoom levels: 8 px at every power-of-two zoom, growing with the zoom and stepping back at √2 · 2ⁿ) and stays fixed to the shape when panning; a click selects the area and its points (moved with it but drawn unselected), Shift adds or removes an area, a press on a selected area drags the whole selection, no hover hatch during drags, a click on an empty spot (a hole) clears it and stays in edit mode; `vector.edit.select {regions}` (`regions_tests`, `vector_regions_tests`). A click on empty canvas only clears the points and starts a marquee (edit mode stays; a double-click or Escape leaves it), while a click on another layer leaves it and selects that layer. Not yet: the transform box around points of several layers; areas of other layers edited at once |
| Vector edit mode: bend, paint bucket, lasso, cut, eraser, transform box, point snapping, mirroring | done | a double-click a few pixels off a thin stroke enters edit mode (#646); a point dropped on another point merges with it, and dropping one end of an open path on the other closes it (a 2-point line's end just moves, #401); Flatten keeps open paths open (#377); paint bucket drags across areas; cut drags divide a shape into layers; delete and heal follows the curve; rounded points are drawn; Shift-click deselects a point at a corner of the box (`vector_behaviour_tests`); several vectors edited at once, Shift-click handles to drag them together, a cap and join per point, rounded points beside curves (`vector_multi_behaviour_tests`); filled regions with the Move tool: the area under the pointer is hatched ("/" lines in the selection colour, clipped by the area's own fill rule, holes left out, gone 3.5 px from a point or 2 px from a segment at any zoom), the line spacing follows octave folding (fitted to measurements at several zoom levels: 8 px at every power-of-two zoom, growing with the zoom and stepping back at √2 · 2ⁿ) and stays fixed to the shape when panning; a click selects the area and its points (moved with it but drawn unselected), Shift adds or removes an area, a press on a selected area drags the whole selection, no hover hatch during drags, a click on an empty spot (a hole) clears it and stays in edit mode; `vector.edit.select {regions}` (`regions_tests`, `vector_regions_tests`). A click on empty canvas only clears the points and starts a marquee (edit mode stays; a double-click or Escape leaves it), while a click on another layer leaves it and selects that layer. The box around several selected points draws its outline and size only, without corner squares (they would read as points); a press on any selected point, corners included, drags them all freely, while its corners and edges clear of points still scale by drag. Not yet: the transform box around points of several layers; areas of other layers edited at once |
| Shape builder (Shift+M) on fills, strokes and open paths; variable width (Shift+W), one side with Alt | done | |
| Boolean groups: clicked where the result is, double-click or Cmd/Ctrl-click into operands, live results while dragging or editing points, per-operand operations | done | |
| Text: point, auto height, fixed; caret, words, lines, IME | done | the font picker previews a family on the canvas while hovered (no undo step; Escape restores, #653); a selection highlight after a line break starts at that line's start and an empty last line shows its mark (#665); carets step over whole emoji and accented letters; IME composition replayed in tests as macOS sends it, not yet checked on a Mac (#747); CJK words by script run (no dictionary); vertical trim in Type settings; no spell check |