mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 11:47:21 -06:00
M3.97: freeform points drag out to delete, spread ring drags, Lines mode draws lines
With the Gradient tool on a freeform gradient the selected point shows its spread as a dashed ring with a handle; dragging either sets the spread. A point dragged out of the object is removed on release, and double-clicking a point opens the gradientStop popover, which now edits the selected freeform point (colour, opacity, spread). In Lines mode successive clicks draw one smooth line (with a rubber band to the pointer); clicking an existing point continues the line from it, and Escape or a click off the art ends it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
37acaebf95
commit
b8793eeaef
@@ -1,17 +1,22 @@
|
||||
//! The Gradient tool on a freeform gradient.
|
||||
//!
|
||||
//! The annotator shows each point as a colour chip, the lines through them and, around the
|
||||
//! selected point, its spread. Click a point to select it (`paint.freeform.selectPoint`; the
|
||||
//! Gradient and Color panels edit the selected point) and drag it to move it. Click inside the
|
||||
//! selected art to add a point there, coloured as the gradient is there; in Lines mode (the
|
||||
//! Gradient panel's Draw toggle) the new point joins the selected one by a line. Click a line to add
|
||||
//! a point on it. Delete or Backspace removes the selected point.
|
||||
//! selected point, its spread: a dashed ring with a handle. Click a point to select it
|
||||
//! (`paint.freeform.selectPoint`; the Gradient and Color panels edit the selected point), drag it
|
||||
//! to move it, drag it out of the object to remove it, and double-click it for its popover. Drag
|
||||
//! the ring or its handle to change the spread. Click a line to add a point on it.
|
||||
//!
|
||||
//! Points mode (the Gradient panel's Draw toggle): a click inside the selected art adds a point
|
||||
//! there, coloured as the gradient is there. Lines mode: successive clicks draw a smooth line
|
||||
//! through the points they add, until Escape (or a click off the art) ends it; clicking an
|
||||
//! existing point first starts the line from it. Delete or Backspace removes the selected point.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::freeform::LineHit;
|
||||
use vectorcraft_color::{Freeform, FreeformMode};
|
||||
use vectorcraft_color::{Color, Freeform, FreeformMode, FreeformPoint};
|
||||
use vectorcraft_doc::NodeId;
|
||||
use vectorcraft_doc::hit::hit_test;
|
||||
use vectorcraft_geom::{Point, Shape};
|
||||
use vectorcraft_geom::{Point, Shape, Vec2};
|
||||
|
||||
use super::paint_owner;
|
||||
use crate::{Action, Cursor, Overlay, ToolContext, ToolKey};
|
||||
@@ -20,12 +25,19 @@ const LINE: [u8; 3] = [0x20, 0x20, 0x20];
|
||||
/// Hit radius of a point's chip and of a line, screen pixels.
|
||||
const POINT_HIT: f64 = 7.0;
|
||||
const LINE_HIT: f64 = 4.0;
|
||||
/// Hit distance of the spread ring and its handle, screen pixels.
|
||||
const RING_HIT: f64 = 4.0;
|
||||
/// The spread handle sits at least this far from its point (so a spread of 0 can be dragged
|
||||
/// out), screen pixels.
|
||||
const RING_MIN: f64 = 16.0;
|
||||
/// A press becomes a drag after this much movement, screen pixels.
|
||||
const DRAG_START: f64 = 3.0;
|
||||
|
||||
/// The freeform gradient behind the active proxy of the first selected object that has one: its
|
||||
/// points in document coordinates and the length spreads are fractions of there.
|
||||
pub(super) struct Annotator {
|
||||
/// The object it paints.
|
||||
pub id: NodeId,
|
||||
pub freeform: Freeform,
|
||||
pub scale: f64,
|
||||
}
|
||||
@@ -34,7 +46,7 @@ impl Annotator {
|
||||
pub fn of(cx: &ToolContext) -> Option<Self> {
|
||||
cx.selection.objects.iter().find_map(|id| {
|
||||
let (freeform, scale) = cx.doc.node(*id)?.proxy_freeform(!cx.fill_active, cx.appearance_item)?;
|
||||
Some(Self { freeform, scale })
|
||||
Some(Self { id: *id, freeform, scale })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,17 +65,42 @@ impl Annotator {
|
||||
cx.freeform_point.filter(|i| *i < self.freeform.points.len())
|
||||
}
|
||||
|
||||
pub fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
/// Point `i`'s spread radius (document units) and where its spread handle sits: on the ring,
|
||||
/// to the right of the point (at least [`RING_MIN`] pixels out).
|
||||
fn ring(&self, cx: &ToolContext, i: usize) -> (f64, Point) {
|
||||
let pt = &self.freeform.points[i];
|
||||
let r = pt.spread as f64 * self.scale;
|
||||
(r, pt.at + Vec2::new(r.max(cx.tol(RING_MIN)), 0.0))
|
||||
}
|
||||
|
||||
/// The selected point when `p` is on its spread ring or handle.
|
||||
fn ring_at(&self, cx: &ToolContext, p: Point) -> Option<usize> {
|
||||
let i = self.selected(cx)?;
|
||||
let (r, handle) = self.ring(cx, i);
|
||||
let on_ring = r > 0.0 && (self.freeform.points[i].at.distance(p) - r).abs() <= cx.tol(RING_HIT);
|
||||
(on_ring || handle.distance(p) <= cx.tol(RING_HIT)).then_some(i)
|
||||
}
|
||||
|
||||
/// Is a line being drawn from the selected point (Lines mode)?
|
||||
fn line_open(&self, cx: &ToolContext, lines: &Lines) -> Option<usize> {
|
||||
self.selected(cx).filter(|_| lines.open && self.freeform.mode == FreeformMode::Lines)
|
||||
}
|
||||
|
||||
pub fn overlays(&self, cx: &ToolContext, lines: &Lines) -> Vec<Overlay> {
|
||||
let f = &self.freeform;
|
||||
let mut o: Vec<Overlay> =
|
||||
(0..f.lines.len()).map(|l| Overlay::Path { path: f.line_path(l), color: LINE, width: 1.0, dashed: false }).collect();
|
||||
// The line being drawn, on to the pointer.
|
||||
if let (Some(from), Some(hover)) = (self.line_open(cx, lines), lines.hover) {
|
||||
o.push(Overlay::Path { path: rubber_band(f, from, hover), color: LINE, width: 1.0, dashed: true });
|
||||
}
|
||||
if let Some(i) = self.selected(cx) {
|
||||
let pt = &f.points[i];
|
||||
let r = pt.spread as f64 * self.scale;
|
||||
let (r, handle) = self.ring(cx, i);
|
||||
if r > 0.0 {
|
||||
let path = vectorcraft_geom::kurbo::Circle::new(pt.at, r).to_path(cx.tol(0.25));
|
||||
let path = vectorcraft_geom::kurbo::Circle::new(f.points[i].at, r).to_path(cx.tol(0.25));
|
||||
o.push(Overlay::Path { path, color: LINE, width: 1.0, dashed: true });
|
||||
}
|
||||
o.push(Overlay::Handle { p: handle, color: LINE });
|
||||
}
|
||||
o.extend(f.points.iter().enumerate().map(|(i, pt)| Overlay::Swatch {
|
||||
p: pt.at,
|
||||
@@ -74,11 +111,35 @@ impl Annotator {
|
||||
}
|
||||
}
|
||||
|
||||
/// The segment a click at `to` would add to the line drawn from point `from`.
|
||||
fn rubber_band(f: &Freeform, from: usize, to: Point) -> vectorcraft_geom::BezPath {
|
||||
let mut f = f.clone();
|
||||
let i = f.add_point(FreeformPoint::new(to, Color::BLACK));
|
||||
let mut bp = vectorcraft_geom::BezPath::new();
|
||||
if let Some(c) = f.connect(from, i).and_then(|l| f.segments(l).into_iter().find(|c| c.p0 == to || c.p3 == to)) {
|
||||
bp.move_to(c.p0);
|
||||
bp.curve_to(c.p1, c.p2, c.p3);
|
||||
}
|
||||
bp
|
||||
}
|
||||
|
||||
/// The Gradient tool's freeform state between gestures: the line Lines mode is drawing.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct Lines {
|
||||
/// A line is being drawn from the selected point: the next click on the art extends it.
|
||||
open: bool,
|
||||
/// Where the pointer hovers over the art (the line's rubber band runs there).
|
||||
hover: Option<Point>,
|
||||
}
|
||||
|
||||
/// What a press on a freeform annotator does.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) enum Grab {
|
||||
/// A point: a drag moves it.
|
||||
enum Grab {
|
||||
/// A point: a drag moves it, out of the object removes it.
|
||||
Point(usize),
|
||||
/// The selected point's spread ring: a drag resizes it from radius `r` (the press `d` from
|
||||
/// the point at `at`).
|
||||
Spread { index: usize, at: Point, r: f64, d: f64, scale: f64 },
|
||||
/// A line: a click adds a point there.
|
||||
Line(LineHit),
|
||||
/// Inside selected art: a click adds a point there.
|
||||
@@ -90,8 +151,14 @@ pub(super) enum Grab {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) struct Gesture {
|
||||
grab: Grab,
|
||||
/// The object whose gradient it edits and its point count at the press.
|
||||
id: NodeId,
|
||||
points: usize,
|
||||
at: Point,
|
||||
began: bool,
|
||||
/// A dragged point is out of the object (releasing removes it); `left`: it has been.
|
||||
out: bool,
|
||||
left: bool,
|
||||
}
|
||||
|
||||
impl Gesture {
|
||||
@@ -111,6 +178,11 @@ fn select(i: Option<usize>) -> Action {
|
||||
Action::Exec("paint.freeform.selectPoint".into(), json!({ "index": i }))
|
||||
}
|
||||
|
||||
/// Is `p` on selected art (where a click adds a point)?
|
||||
fn on_art(cx: &ToolContext, p: Point) -> bool {
|
||||
hit_test(cx.doc, p, cx.hit_options()).is_some_and(|h| cx.selection.objects.contains(&paint_owner(cx.doc, h.leaf)))
|
||||
}
|
||||
|
||||
pub(super) fn press(cx: &ToolContext, a: &Annotator, p: Point) -> (Vec<Action>, Gesture) {
|
||||
let mut out = vec![];
|
||||
let grab = if let Some(i) = a.point_at(cx, p) {
|
||||
@@ -118,18 +190,23 @@ pub(super) fn press(cx: &ToolContext, a: &Annotator, p: Point) -> (Vec<Action>,
|
||||
out.push(select(Some(i)));
|
||||
}
|
||||
Grab::Point(i)
|
||||
} else if let Some(index) = a.ring_at(cx, p) {
|
||||
let at = a.freeform.points[index].at;
|
||||
Grab::Spread { index, at, r: a.ring(cx, index).0, d: at.distance(p), scale: a.scale }
|
||||
} else if let Some(h) = a.line_at(cx, p) {
|
||||
Grab::Line(h)
|
||||
} else if hit_test(cx.doc, p, cx.hit_options()).is_some_and(|h| cx.selection.objects.contains(&paint_owner(cx.doc, h.leaf))) {
|
||||
} else if on_art(cx, p) {
|
||||
Grab::Add
|
||||
} else {
|
||||
Grab::Clear
|
||||
};
|
||||
(out, Gesture { grab, at: p, began: false })
|
||||
(out, Gesture { grab, id: a.id, points: a.freeform.points.len(), at: p, began: false, out: false, left: false })
|
||||
}
|
||||
|
||||
pub(super) fn drag(cx: &ToolContext, g: &mut Gesture, p: Point) -> Vec<Action> {
|
||||
let Grab::Point(index) = g.grab else { return vec![] };
|
||||
if !matches!(g.grab, Grab::Point(_) | Grab::Spread { .. }) {
|
||||
return vec![];
|
||||
}
|
||||
let mut out = vec![];
|
||||
if !g.began {
|
||||
if p.distance(g.at) < cx.tol(DRAG_START) {
|
||||
@@ -138,50 +215,124 @@ pub(super) fn drag(cx: &ToolContext, g: &mut Gesture, p: Point) -> Vec<Action> {
|
||||
g.began = true;
|
||||
out.push(Action::Begin("Gradient".into()));
|
||||
}
|
||||
out.push(Action::Preview("paint.freeform.setPoint".into(), on_proxy(cx, json!({ "index": index, "at": [p.x, p.y] }))));
|
||||
let (cmd, params) = match g.grab {
|
||||
Grab::Point(index) => {
|
||||
// Out of the object the point goes (never the last one).
|
||||
g.out = g.points > 1 && cx.doc.node(g.id).is_some_and(|n| !n.contains_fn()(p));
|
||||
g.left |= g.out;
|
||||
if g.out {
|
||||
("paint.freeform.deletePoint", json!({ "index": index }))
|
||||
} else {
|
||||
("paint.freeform.setPoint", json!({ "index": index, "at": [p.x, p.y] }))
|
||||
}
|
||||
}
|
||||
Grab::Spread { index, at, r, d, scale } => {
|
||||
let spread = if scale > 0.0 { ((r + at.distance(p) - d) / scale).clamp(0.0, 1.0) } else { 0.0 };
|
||||
("paint.freeform.setPoint", json!({ "index": index, "spread": spread }))
|
||||
}
|
||||
_ => unreachable!("checked above"),
|
||||
};
|
||||
out.push(Action::Preview(cmd.into(), on_proxy(cx, params)));
|
||||
out
|
||||
}
|
||||
|
||||
pub(super) fn release(cx: &ToolContext, a: Option<&Annotator>, g: Gesture) -> Vec<Action> {
|
||||
pub(super) fn release(cx: &ToolContext, a: Option<&Annotator>, g: Gesture, lines: &mut Lines) -> Vec<Action> {
|
||||
let mode = a.map_or(FreeformMode::Points, |a| a.freeform.mode);
|
||||
if g.began {
|
||||
return vec![Action::Commit];
|
||||
let mut out = vec![Action::Commit];
|
||||
// A point dragged out and back was deselected by the removal its drag previewed.
|
||||
if let Grab::Point(i) = g.grab
|
||||
&& g.left
|
||||
&& !g.out
|
||||
{
|
||||
out.push(select(Some(i)));
|
||||
}
|
||||
lines.open &= !g.out;
|
||||
return out;
|
||||
}
|
||||
match g.grab {
|
||||
Grab::Point(_) => vec![],
|
||||
// In Lines mode a line can start from an existing point.
|
||||
Grab::Point(_) => {
|
||||
lines.open = mode == FreeformMode::Lines;
|
||||
vec![]
|
||||
}
|
||||
Grab::Spread { .. } => vec![],
|
||||
Grab::Line(h) => {
|
||||
lines.open = false;
|
||||
vec![Action::Exec("paint.freeform.splitLine".into(), on_proxy(cx, json!({ "line": h.line, "segment": h.segment, "t": h.t })))]
|
||||
}
|
||||
Grab::Add => {
|
||||
let lines = a.is_some_and(|a| a.freeform.mode == FreeformMode::Lines);
|
||||
vec![Action::Exec("paint.freeform.addPoint".into(), on_proxy(cx, json!({ "at": [g.at.x, g.at.y], "line": lines })))]
|
||||
let join = a.is_some_and(|a| a.line_open(cx, lines).is_some());
|
||||
lines.open = mode == FreeformMode::Lines;
|
||||
vec![Action::Exec("paint.freeform.addPoint".into(), on_proxy(cx, json!({ "at": [g.at.x, g.at.y], "line": join })))]
|
||||
}
|
||||
Grab::Clear => {
|
||||
lines.open = false;
|
||||
if cx.freeform_point.is_some() { vec![select(None)] } else { vec![] }
|
||||
}
|
||||
Grab::Clear if cx.freeform_point.is_some() => vec![select(None)],
|
||||
Grab::Clear => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete / Backspace remove the selected point (never the last one).
|
||||
pub(super) fn claims_key(cx: &ToolContext, a: &Annotator, key: ToolKey) -> bool {
|
||||
matches!(key, ToolKey::Delete | ToolKey::Backspace) && a.selected(cx).is_some() && a.freeform.points.len() > 1
|
||||
/// Escape during a drag: the point a drag-out deselected is selected again.
|
||||
pub(super) fn cancel(g: &Gesture) -> Vec<Action> {
|
||||
match g.grab {
|
||||
Grab::Point(i) if g.out => vec![Action::Cancel, select(Some(i))],
|
||||
_ => vec![Action::Cancel],
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn key(cx: &ToolContext, a: &Annotator, key: ToolKey) -> Vec<Action> {
|
||||
if !claims_key(cx, a, key) {
|
||||
/// Double-clicking a point opens its popover beside it.
|
||||
pub(super) fn double_click(cx: &ToolContext, a: &Annotator, p: Point) -> Vec<Action> {
|
||||
let Some(i) = a.point_at(cx, p) else { return vec![] };
|
||||
let at = a.freeform.points[i].at;
|
||||
let mut out = if cx.freeform_point != Some(i) { vec![select(Some(i))] } else { vec![] };
|
||||
out.push(Action::Dialog("gradientStop".into(), json!({ "index": i, "x": at.x, "y": at.y })));
|
||||
out
|
||||
}
|
||||
|
||||
/// Pointer moves: Lines mode's rubber band follows the pointer over the art (where a click
|
||||
/// extends the line).
|
||||
pub(super) fn hover(cx: &ToolContext, lines: &mut Lines, p: Point) {
|
||||
lines.hover = (lines.open && on_art(cx, p)).then_some(p);
|
||||
}
|
||||
|
||||
/// Delete / Backspace remove the selected point (never the last one); Escape ends the line being
|
||||
/// drawn.
|
||||
pub(super) fn claims_key(cx: &ToolContext, a: &Annotator, lines: &Lines, key: ToolKey) -> bool {
|
||||
match key {
|
||||
ToolKey::Delete | ToolKey::Backspace => a.selected(cx).is_some() && a.freeform.points.len() > 1,
|
||||
ToolKey::Escape => a.line_open(cx, lines).is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn key(cx: &ToolContext, a: &Annotator, lines: &mut Lines, key: ToolKey) -> Vec<Action> {
|
||||
if !claims_key(cx, a, lines, key) {
|
||||
return vec![];
|
||||
}
|
||||
if key == ToolKey::Escape {
|
||||
lines.open = false;
|
||||
return vec![];
|
||||
}
|
||||
vec![Action::Exec("paint.freeform.deletePoint".into(), on_proxy(cx, json!({ "index": a.selected(cx) })))]
|
||||
}
|
||||
|
||||
pub(super) fn cursor(cx: &ToolContext, a: &Annotator, g: Option<&Gesture>, p: Point) -> Cursor {
|
||||
if g.is_some_and(|g| g.began) || a.point_at(cx, p).is_some() {
|
||||
return Cursor::Move;
|
||||
match g.filter(|g| g.began).map(|g| (&g.grab, g.out)) {
|
||||
Some((Grab::Point(_), true)) => return Cursor::RemoveStop,
|
||||
Some((Grab::Spread { .. }, _)) => return Cursor::ResizeH,
|
||||
Some(_) => return Cursor::Move,
|
||||
None => {}
|
||||
}
|
||||
if a.line_at(cx, p).is_some()
|
||||
|| hit_test(cx.doc, p, cx.hit_options()).is_some_and(|h| cx.selection.objects.contains(&paint_owner(cx.doc, h.leaf)))
|
||||
{
|
||||
return Cursor::AddStop;
|
||||
if a.point_at(cx, p).is_some() {
|
||||
Cursor::Move
|
||||
} else if a.ring_at(cx, p).is_some() {
|
||||
Cursor::ResizeH
|
||||
} else if a.line_at(cx, p).is_some() || on_art(cx, p) {
|
||||
Cursor::AddStop
|
||||
} else {
|
||||
Cursor::Crosshair
|
||||
}
|
||||
Cursor::Crosshair
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -246,6 +397,8 @@ mod tests {
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
cx.freeform_point = Some(1);
|
||||
let mut t = GradientTool::default();
|
||||
// Clicking the selected point starts a line from it; a click on the art extends it.
|
||||
assert!(click(&mut t, &cx, 180.0, 120.0).is_empty());
|
||||
let a = click(&mut t, &cx, 190.0, 190.0);
|
||||
assert_eq!(a, vec![Action::Exec("paint.freeform.addPoint".into(), json!({"at": [190.0, 190.0], "line": true, "stroke": false}))]);
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 120.0, 121.0));
|
||||
@@ -261,4 +414,109 @@ mod tests {
|
||||
vec![Action::Exec("paint.freeform.deletePoint".into(), json!({"index": 1, "stroke": false}))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successive_clicks_draw_a_line_until_escape() {
|
||||
let (d, s) = doc(FreeformMode::Lines);
|
||||
let p = paint();
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
let mut t = GradientTool::default();
|
||||
let add = |x: f64, y: f64, line: bool| Action::Exec("paint.freeform.addPoint".into(), json!({"at": [x, y], "line": line, "stroke": false}));
|
||||
// The first click starts a line; the engine selects each new point, which the next joins.
|
||||
assert_eq!(click(&mut t, &cx, 140.0, 150.0), vec![add(140.0, 150.0, false)]);
|
||||
// (The engine selects the new point; any existing one stands in for it here.)
|
||||
cx.freeform_point = Some(2);
|
||||
assert!(t.claims_key(&cx, ToolKey::Escape));
|
||||
// The rubber band follows the pointer from the line's end.
|
||||
t.pointer(&cx, &ev(PointerKind::Move, 170.0, 160.0));
|
||||
let dashed = |t: &GradientTool, cx: &ToolContext| t.overlays(cx).iter().filter(|o| matches!(o, Overlay::Path { dashed: true, .. })).count();
|
||||
assert_eq!(dashed(&t, &cx), 2, "the rubber band and the selected point's spread ring");
|
||||
// Off the art (where a click ends the line) it goes.
|
||||
t.pointer(&cx, &ev(PointerKind::Move, 400.0, 400.0));
|
||||
assert_eq!(dashed(&t, &cx), 1);
|
||||
assert_eq!(click(&mut t, &cx, 170.0, 160.0), vec![add(170.0, 160.0, true)]);
|
||||
// Escape ends the line: the next click starts another.
|
||||
assert!(t.key(&cx, ToolKey::Escape, Default::default()).is_empty());
|
||||
assert!(!t.claims_key(&cx, ToolKey::Escape));
|
||||
assert_eq!(dashed(&t, &cx), 1);
|
||||
assert_eq!(click(&mut t, &cx, 130.0, 170.0), vec![add(130.0, 170.0, false)]);
|
||||
// Points mode never joins.
|
||||
let (d, s) = doc(FreeformMode::Points);
|
||||
let mut cx = crate::testutil::cx(&d, &s, &p);
|
||||
cx.freeform_point = Some(0);
|
||||
let mut t = GradientTool::default();
|
||||
click(&mut t, &cx, 120.0, 120.0);
|
||||
assert_eq!(click(&mut t, &cx, 140.0, 150.0), vec![add(140.0, 150.0, false)]);
|
||||
assert!(!t.claims_key(&cx, ToolKey::Escape));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_point_dragged_out_of_the_object_is_removed() {
|
||||
let (d, s) = doc(FreeformMode::Points);
|
||||
let p = paint();
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
cx.freeform_point = Some(0);
|
||||
let mut t = GradientTool::default();
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 120.0, 120.0));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 60.0, 60.0));
|
||||
assert_eq!(a[1], Action::Preview("paint.freeform.deletePoint".into(), json!({"index": 0, "stroke": false})));
|
||||
assert_eq!(t.cursor(&cx, Point::new(60.0, 60.0), Default::default()), Cursor::RemoveStop);
|
||||
// Back inside it moves again.
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 110.0, 110.0));
|
||||
assert_eq!(a[0], Action::Preview("paint.freeform.setPoint".into(), json!({"index": 0, "at": [110.0, 110.0], "stroke": false})));
|
||||
// Released there it is selected again (the removal previewed on the way deselected it).
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 110.0, 110.0)), vec![Action::Commit, select(Some(0))]);
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 120.0, 120.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 60.0, 60.0));
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 60.0, 60.0)), vec![Action::Commit]);
|
||||
// Escape mid-drag restores the point and its selection.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 120.0, 120.0));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 60.0, 60.0));
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Default::default()), vec![Action::Cancel, select(Some(0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_spread_ring_drags_the_spread() {
|
||||
let (d, s) = doc(FreeformMode::Points);
|
||||
let p = paint();
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
cx.freeform_point = Some(2);
|
||||
let mut t = GradientTool::default();
|
||||
// Spread 0.2 of half the 100 pt box: a 10 pt ring around (150, 180), its handle on it.
|
||||
assert_eq!(t.cursor(&cx, Point::new(150.0, 170.0), Default::default()), Cursor::ResizeH);
|
||||
assert!(t.overlays(&cx).contains(&Overlay::Handle { p: Point::new(166.0, 180.0), color: LINE }), "at least 16 px out");
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::Down, 160.0, 180.0)).is_empty());
|
||||
let spread = |a: &[Action]| match a.last() {
|
||||
Some(Action::Preview(c, v)) if c == "paint.freeform.setPoint" && v["index"] == 2 => (v["spread"].as_f64().unwrap() * 1e4).round() / 1e4,
|
||||
a => panic!("{a:?}"),
|
||||
};
|
||||
assert_eq!(spread(&t.pointer(&cx, &ev(PointerKind::Drag, 170.0, 180.0))), 0.4);
|
||||
// Dragged in, it shrinks with the pointer's distance.
|
||||
assert_eq!(spread(&t.pointer(&cx, &ev(PointerKind::Drag, 145.0, 180.0))), 0.1);
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 145.0, 180.0)), vec![Action::Commit]);
|
||||
// Grabbed by the handle (16 px out), it reaches no spread before the pointer reaches the point.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 166.0, 180.0));
|
||||
assert_eq!(spread(&t.pointer(&cx, &ev(PointerKind::Drag, 152.0, 180.0))), 0.0);
|
||||
t.pointer(&cx, &ev(PointerKind::Up, 152.0, 180.0));
|
||||
// The handle of a point without spread drags one out.
|
||||
let mut d0 = d.clone();
|
||||
let n = d0.node_mut(s.objects[0]).unwrap();
|
||||
let Paint::Gradient(mut g) = n.appearance.fill_paint() else { unreachable!() };
|
||||
g.freeform.as_mut().unwrap().points[2].spread = 0.0;
|
||||
n.appearance.set_fill(Paint::Gradient(g));
|
||||
let mut cx = crate::testutil::cx(&d0, &s, &p);
|
||||
cx.freeform_point = Some(2);
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 166.0, 180.0));
|
||||
assert_eq!(spread(&t.pointer(&cx, &ev(PointerKind::Drag, 176.0, 180.0))), 0.2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_a_point_opens_its_popover() {
|
||||
let (d, s) = doc(FreeformMode::Points);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let a = GradientTool::default().pointer(&cx, &ev(PointerKind::DoubleClick, 181.0, 119.0));
|
||||
assert_eq!(a, vec![select(Some(1)), Action::Dialog("gradientStop".into(), json!({"index": 1, "x": 180.0, "y": 120.0}))]);
|
||||
assert!(GradientTool::default().pointer(&cx, &ev(PointerKind::DoubleClick, 140.0, 150.0)).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,8 @@ pub struct GradientTool {
|
||||
gesture: Option<Gesture>,
|
||||
/// A press on a freeform gradient's annotator.
|
||||
free: Option<freeform::Gesture>,
|
||||
/// The line Lines mode is drawing on a freeform gradient.
|
||||
lines: freeform::Lines,
|
||||
/// Smart-guide feedback of the handle being dragged.
|
||||
guides: Vec<Overlay>,
|
||||
}
|
||||
@@ -402,9 +404,14 @@ impl Tool for GradientTool {
|
||||
(PointerKind::Drag, Some(g)) => return freeform::drag(cx, g, ev.pos),
|
||||
(PointerKind::Up, Some(_)) => {
|
||||
let g = self.free.take().expect("checked");
|
||||
return freeform::release(cx, FreeformAnnotator::of(cx).as_ref(), g);
|
||||
return freeform::release(cx, FreeformAnnotator::of(cx).as_ref(), g, &mut self.lines);
|
||||
}
|
||||
(PointerKind::Move, None) => freeform::hover(cx, &mut self.lines, ev.pos),
|
||||
(PointerKind::DoubleClick, _) => {
|
||||
if let Some(a) = FreeformAnnotator::of(cx) {
|
||||
return freeform::double_click(cx, &a, ev.pos);
|
||||
}
|
||||
}
|
||||
(PointerKind::DoubleClick, free) if free.is_some() || FreeformAnnotator::of(cx).is_some() => return vec![],
|
||||
_ => {}
|
||||
}
|
||||
match ev.kind {
|
||||
@@ -423,7 +430,7 @@ impl Tool for GradientTool {
|
||||
|
||||
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
|
||||
if let Some(a) = FreeformAnnotator::of(cx) {
|
||||
return freeform::claims_key(cx, &a, key);
|
||||
return freeform::claims_key(cx, &a, &self.lines, key);
|
||||
}
|
||||
matches!(key, ToolKey::Delete | ToolKey::Backspace | ToolKey::Left | ToolKey::Right)
|
||||
&& cx.gradient_stop.is_some_and(|i| Annotator::of(cx).is_some_and(|a| i < a.gradient.stops.len()))
|
||||
@@ -432,15 +439,14 @@ impl Tool for GradientTool {
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape && self.busy() {
|
||||
self.gesture = None;
|
||||
self.free = None;
|
||||
self.guides.clear();
|
||||
return vec![Action::Cancel];
|
||||
return self.free.take().map_or_else(|| vec![Action::Cancel], |g| freeform::cancel(&g));
|
||||
}
|
||||
if self.busy() || !self.claims_key(cx, key) {
|
||||
return vec![];
|
||||
}
|
||||
if let Some(a) = FreeformAnnotator::of(cx) {
|
||||
return freeform::key(cx, &a, key);
|
||||
return freeform::key(cx, &a, &mut self.lines, key);
|
||||
}
|
||||
let (Some(a), Some(i)) = (Annotator::of(cx), cx.gradient_stop) else { return vec![] };
|
||||
let stops = &a.gradient.stops;
|
||||
@@ -463,7 +469,7 @@ impl Tool for GradientTool {
|
||||
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
if let Some(a) = FreeformAnnotator::of(cx) {
|
||||
return a.overlays(cx);
|
||||
return a.overlays(cx, &self.lines);
|
||||
}
|
||||
let mut o = match (Annotator::of(cx), self.gesture.as_ref().and_then(|g| g.vector)) {
|
||||
(Some(a), _) => annotator_overlays(cx, &a),
|
||||
|
||||
Reference in New Issue
Block a user