From 90004dc7cd1cb1ccaad971af75b81f72bbbcab62 Mon Sep 17 00:00:00 2001 From: Nicek Date: Thu, 22 Oct 2026 13:52:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A=E9=80=89?= =?UTF-8?q?=E7=9F=A2=E9=87=8F=E7=82=B9=E6=97=B6=E5=8F=98=E6=8D=A2=E6=A1=86?= =?UTF-8?q?=E7=9A=84=E4=BA=A4=E4=BA=92=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复了多选矢量点时,变换框角点会误判为可拖动的点的问题,同时更新了相关注释和测试用例,确保未被点占据的角点和边缘仍可用于缩放操作。 --- crates/engine/src/vectoredit/transform.rs | 8 ++- .../engine/src/vectoredit/transform_tests.rs | 55 +++++++++++++------ crates/ui-egui/src/canvas.rs | 4 +- docs/ui-parity.md | 2 +- 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/crates/engine/src/vectoredit/transform.rs b/crates/engine/src/vectoredit/transform.rs index 1d5f27a6..8ec9826a 100644 --- a/crates/engine/src/vectoredit/transform.rs +++ b/crates/engine/src/vectoredit/transform.rs @@ -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, p: Point, zoom: f64) -> Option { 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 { diff --git a/crates/engine/src/vectoredit/transform_tests.rs b/crates/engine/src/vectoredit/transform_tests.rs index d69baf52..c608c3aa 100644 --- a/crates/engine/src/vectoredit/transform_tests.rs +++ b/crates/engine/src/vectoredit/transform_tests.rs @@ -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] diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index fe9a0344..e6dbc3c8 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -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. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 2355de89..5421cd68 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -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 |