M4: vector edit transform box and point snapping (#64)

- Transform box around 2+ selected points (Move tool): corners/edges scale
  them (Shift keeps proportions, Alt from the centre), just outside a corner
  rotates them (Shift: 15° steps, the box turns with them), dragging inside
  moves them; one undo step each. Corners win over points, points over edges,
  edges over segments.
- Commands: vector.edit.bounds, vector.edit.resize, vector.edit.rotate,
  vector.edit.transform; object.flipH/V and the 90°/180° rotations act on the
  selected points while editing (unless ids are given).
- Design panel in edit mode: W/H for the selected points next to X/Y (the
  layer's dimensions are hidden while points are selected).
- Point snapping for dragged points and handles and both pens: points and
  segment midpoints of this and other vectors, the usual layer targets and
  the pixel grid, with guides and a marker on the point landed on; Ctrl/Cmd
  turns object snapping off. Snap code generalised in snap.rs (Targets spans).
- Fix: handle/bend drags read positions in the gesture snapshot's coordinates,
  so handles no longer lag when the preview re-fits the vector.
- Shared resize math (tools::resized_box) and handle hit-testing for layer
  and point boxes; canvas draws both boxes through one helper.
- Tests in vectoredit/transform_tests.rs (incl. junk sweep for the
  edit-aware commands).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BFlat
2026-10-10 10:44:12 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 5f54fecca2
commit 8dff0089e5
16 changed files with 1245 additions and 238 deletions
+21 -5
View File
@@ -27,11 +27,17 @@ pub fn specs() -> Vec<CommandSpec> {
spec!("arrange.forward", "Bring forward", "{ids?}", true, |s, p| zorder(s, p, Z::Forward)),
spec!("arrange.backward", "Send backward", "{ids?}", true, |s, p| zorder(s, p, Z::Backward)),
spec!("arrange.back", "Send to back", "{ids?}", true, |s, p| zorder(s, p, Z::Back)),
spec!("object.flipH", "Flip horizontal", "{ids?}", true, |s, p| flip(s, p, true)),
spec!("object.flipV", "Flip vertical", "{ids?}", true, |s, p| flip(s, p, false)),
spec!("object.rotate180", "Rotate 180°", "{ids?}", true, |s, p| rotate_by(s, p, 180.0)),
spec!("object.rotateLeft", "Rotate 90° left", "{ids?}", true, |s, p| rotate_by(s, p, 90.0)),
spec!("object.rotateRight", "Rotate 90° right", "{ids?}", true, |s, p| rotate_by(s, p, -90.0)),
spec!("object.flipH", "Flip horizontal", "{ids?} (in vector edit mode without ids: the selected points)", true, |s, p| flip(s, p, true)),
spec!("object.flipV", "Flip vertical", "{ids?} (in vector edit mode without ids: the selected points)", true, |s, p| flip(s, p, false)),
spec!("object.rotate180", "Rotate 180°", "{ids?} (in vector edit mode without ids: the selected points)", true, |s, p| rotate_by(
s, p, 180.0
)),
spec!("object.rotateLeft", "Rotate 90° left", "{ids?} (in vector edit mode without ids: the selected points)", true, |s, p| rotate_by(
s, p, 90.0
)),
spec!("object.rotateRight", "Rotate 90° right", "{ids?} (in vector edit mode without ids: the selected points)", true, |s, p| {
rotate_by(s, p, -90.0)
}),
spec!("transform.move", "Move", "{ids?, dx, dy} (page units)", true, |s, p| {
let (dx, dy) = (num_or(p, "dx", 0.0), num_or(p, "dy", 0.0));
let ids = req_targets("transform.move", s, p)?;
@@ -290,6 +296,13 @@ fn zorder(s: &mut Session, p: &Value, z: Z) -> Result<Value> {
}
fn flip(s: &mut Session, p: &Value, horizontal: bool) -> Result<Value> {
// Editing a vector with points selected: flip them in their box.
let points = crate::vectoredit::transform::transform_in_edit_mode(s, p, |b| {
if horizontal { b.onto(b.width, 0.0, 0.0, b.height) } else { b.onto(0.0, b.height, b.width, 0.0) }
});
if let Some(r) = points {
return r;
}
let ids = req_targets("flip", s, p)?;
let doc = s.doc_mut()?;
let ids = roots(doc, &ids);
@@ -304,6 +317,9 @@ fn flip(s: &mut Session, p: &Value, horizontal: bool) -> Result<Value> {
}
fn rotate_by(s: &mut Session, p: &Value, deg: f64) -> Result<Value> {
if let Some(r) = crate::vectoredit::transform::transform_in_edit_mode(s, p, |b| figcraft_geom::rotate_about(Affine::IDENTITY, b.center(), deg)) {
return r;
}
let ids = req_targets("rotate", s, p)?;
let doc = s.doc_mut()?;
let ids = roots(doc, &ids);