From 8edcc02aee7993e9cb38bf1a2ac5cf41a2ab0bcf Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Sun, 8 Nov 2026 07:57:54 -0500 Subject: [PATCH] M8: points moved by one offset still carry the regions' paint frames Setting each point's own coordinate moved the points one at a time, so when every point of a layer moved by the same offset (`{"x": {"add": 10}}` on all of them) `ops::move_vertices` no longer saw the whole network move and left the regions' paint frames behind. Points with the same offset now move together. Verified: `moving_every_point_by_one_offset_carries_the_regions_paint_frame` failed before this and passes; vectoredit tests 92 passed, junk sweep 49 passed, clippy and fmt clean. Co-Authored-By: Claude Opus 5.5 Model-Effort: Opus 5.5 drove and wrote --- crates/engine/src/canvas/vectoredit.rs | 13 ++++++++-- .../src/canvas/vectoredit/position_tests.rs | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/canvas/vectoredit.rs b/crates/engine/src/canvas/vectoredit.rs index 5dcd8c0ba..47597ea92 100644 --- a/crates/engine/src/canvas/vectoredit.rs +++ b/crates/engine/src/canvas/vectoredit.rs @@ -1017,9 +1017,18 @@ pub fn specs() -> Vec { if moves.is_empty() { return Err(bad("vector.edit.position", "select points")); } + // Points with the same offset move together, so moving every point by one offset + // still carries the regions' paint frames, as a drag of all of them does. + let mut groups: Vec<(Vec2, Vec)> = Vec::new(); + for (i, d) in moves { + match groups.iter_mut().find(|(g, _)| (*g - d).hypot() < 1e-9) { + Some((_, v)) => v.push(i), + None => groups.push((d, vec![i])), + } + } edit(s.doc_mut()?, ve.id, |net| { - for (i, d) in &moves { - ops::move_vertices(net, &[*i], *d); + for (d, v) in &groups { + ops::move_vertices(net, v, *d); } })?; Ok(json!({"ok": true})) diff --git a/crates/engine/src/canvas/vectoredit/position_tests.rs b/crates/engine/src/canvas/vectoredit/position_tests.rs index f64da34d5..fb6af9105 100644 --- a/crates/engine/src/canvas/vectoredit/position_tests.rs +++ b/crates/engine/src/canvas/vectoredit/position_tests.rs @@ -100,6 +100,32 @@ fn the_values_are_in_the_parents_coordinates() { assert!(near(after[0].1, before[0].1)); } +#[test] +fn moving_every_point_by_one_offset_carries_the_regions_paint_frame() { + // `{add}` on every point is a plain translation: the region's paint frame goes with the + // points, as when they are dragged together. + let mut s = Session::new(); + let id = rect(&mut s); + { + let node = s.doc_mut().unwrap().get_mut(id).unwrap(); + let NodeKind::Vector { network } = &mut node.kind else { panic!("expected a vector") }; + assert!(!network.regions.is_empty(), "the rectangle has a region"); + for region in &mut network.regions { + region.paint_space = Some(figcraft_doc::RegionPaintSpace { width: 100.0, height: 100.0, transform: Affine::IDENTITY }); + } + } + let frame = |s: &Session| { + let world = s.doc().unwrap().world_transform(id); + world * net(s, id).regions[0].paint_space.as_ref().unwrap().transform + }; + let before = frame(&s); + let all: Vec = (0..net(&s, id).vertices.len()).collect(); + ex(&mut s, "vector.edit.select", json!({"vertices": all})); + ex(&mut s, "vector.edit.position", json!({"x": {"add": 10}})); + let (want, got) = ((Affine::translate((10.0, 0.0)) * before).as_coeffs(), frame(&s).as_coeffs()); + assert!(want.iter().zip(got).all(|(a, b)| near(*a, b)), "{got:?} vs {want:?}"); +} + #[test] fn junk_is_refused_and_changes_nothing() { let mut s = Session::new();