Contour: keep a Contour's corners when dragged again; ignore a no-distance drag

Two fixes from review of #5694, each with a test that fails without it:

- Dragging a selected Contour sent the tool's own corner options, so a
  Round or Square Contour came back Sharp and lost its miter limit, and
  the Contour bar (which reads the effect) disagreed with the shape. The
  press now reads the Contour's own corners and miter limit and every
  preview of that drag carries them; a new Contour still takes the tool's
  options.
- A drag that never moved the distance (along the edge, or rounded to
  nothing by Shift) previewed offset 0 and committed an unchanged copy
  and an undo step. Nothing is previewed until the distance leaves where
  it started.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Kindmark
2026-11-05 12:57:51 +07:00
committed by bflatastic
co-authored by Claude Sonnet 5.5
parent 8584bf1ab6
commit 8ce22e71eb
2 changed files with 63 additions and 5 deletions
+34
View File
@@ -299,6 +299,40 @@ mod tool {
assert_eq!(effects_of(&s, contour)[0].1["joins"], json!("round"));
}
/// A Contour keeps its own corner style and miter limit when it is dragged again, whatever the
/// tool's options are by then (they said Sharp when the Contour was made Square).
#[test]
fn dragging_a_contour_again_keeps_its_corners_and_miter_limit() {
let (mut s, _) = ready();
s.execute("object.contour", &json!({"offset": 5, "corners": "square", "miterLimit": 7})).unwrap();
let contour = s.doc().unwrap().selection.objects[0];
s.set_tool_option("corners", &json!("sharp"));
at(&mut s, PointerKind::Down, 150.0, 120.0, Mods::default());
at(&mut s, PointerKind::Drag, 158.0, 120.0, Mods::default());
at(&mut s, PointerKind::Up, 158.0, 120.0, Mods::default());
let fx = effects_of(&s, contour);
assert_eq!(fx.len(), 1);
assert!((offset_of(&s, contour) - 13.0).abs() < 1e-6, "{}", offset_of(&s, contour));
assert_eq!(fx[0].1["joins"], json!("bevel"), "{:?}", fx[0].1);
assert_eq!(fx[0].1["miterLimit"], json!(7.0), "{:?}", fx[0].1);
}
/// A drag that never moves the distance (along the edge, or rounded back to nothing by Shift)
/// leaves no unchanged copy and no undo step.
#[test]
fn a_drag_along_the_edge_changes_nothing() {
let (mut s, _) = ready();
let (before, nodes) = (undo_depth(&s), node_count(&s));
let shift = Mods { shift: true, ..Mods::default() };
for (mods, to) in [(Mods::default(), (150.0, 135.0)), (shift, (150.3, 120.0))] {
at(&mut s, PointerKind::Down, 150.0, 120.0, mods);
at(&mut s, PointerKind::Drag, to.0, to.1, mods);
at(&mut s, PointerKind::Up, to.0, to.1, mods);
assert_eq!((undo_depth(&s), node_count(&s)), (before, nodes), "{to:?}");
assert!(!s.in_interaction());
}
}
/// The outline a Contour was made from shows as a dashed guide over a solid halo (so it reads
/// over any fill); a shape that is not a Contour yet has none.
#[test]
+29 -5
View File
@@ -51,6 +51,22 @@ fn contour_offset(n: &Node) -> Option<f64> {
Some(e.params.get("offset").and_then(Value::as_f64).unwrap_or(0.0))
}
/// The corner style and miter limit of a Contour's Offset Path effect, as `object.contour` takes
/// them.
fn contour_style(n: &Node) -> Option<Value> {
let e = n.appearance.effects.iter().rev().find(|e| e.id == EFFECT)?;
let corners = match e.params.get("joins").and_then(Value::as_str) {
Some("round") => "round",
Some("bevel") => "square",
_ => "sharp",
};
let mut style = json!({ "corners": corners });
if let Some(m) = e.params.get("miterLimit").and_then(Value::as_f64) {
style["miterLimit"] = json!(m);
}
Some(style)
}
/// How far `p` is from the nearest of `bases`' edges: positive outside, negative inside.
fn signed_distance(bases: &[Base], p: Point) -> Option<f64> {
let mut nearest = f64::INFINITY;
@@ -69,6 +85,9 @@ struct Drag {
pressed_at: f64,
/// The Contour's distance at the press (0 for a shape that is not one yet).
start: f64,
/// The corner style and miter limit every preview carries: the pressed Contour's own, else the
/// tool's options.
style: Value,
/// The distance last previewed.
last: Option<f64>,
/// Where the pointer is, for the distance label.
@@ -108,8 +127,9 @@ impl Corners {
}
impl ContourTool {
fn params(&self, offset: f64) -> Value {
let mut p = json!({"offset": offset, "corners": self.corners.id()});
/// The corner style and miter limit of a new Contour: the tool's options.
fn style(&self) -> Value {
let mut p = json!({ "corners": self.corners.id() });
if let Some(m) = self.miter_limit {
p["miterLimit"] = json!(m);
}
@@ -142,21 +162,25 @@ impl Tool for ContourTool {
PointerKind::Down => {
let shapes = selected_shapes(cx);
let start = shapes.first().and_then(|n| contour_offset(n)).unwrap_or(0.0);
// A Contour keeps its own corners; a new one takes the tool's.
let style = shapes.first().and_then(|n| contour_style(n)).unwrap_or_else(|| self.style());
let bases: Vec<Base> = shapes.into_iter().filter_map(base_of).collect();
let Some(pressed_at) = signed_distance(&bases, ev.pos) else { return vec![] };
self.drag = Some(Drag { bases, pressed_at, start, last: None, pos: ev.pos });
self.drag = Some(Drag { bases, pressed_at, start, style, last: None, pos: ev.pos });
vec![Action::Begin("Contour".into())]
}
PointerKind::Drag => {
let mut params = self.params(0.0);
let Some(drag) = self.drag.as_mut() else { return vec![] };
drag.pos = ev.pos;
let Some(now) = signed_distance(&drag.bases, ev.pos) else { return vec![] };
let offset = round_distance(drag.start + now - drag.pressed_at, ev.mods);
if !offset.is_finite() || drag.last == Some(offset) {
// Nothing is previewed until the distance has left where it started: a drag along
// the edge (or one Shift rounds back) would otherwise leave an unchanged copy.
if !offset.is_finite() || drag.last == Some(offset) || (drag.last.is_none() && offset == drag.start) {
return vec![];
}
drag.last = Some(offset);
let mut params = drag.style.clone();
params["offset"] = json!(offset);
vec![Action::Preview("object.contour".into(), params)]
}