mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 05:07:51 -06:00
Gradient Mesh: select several mesh points; move, color and opacity reach them all (#4207, #4208, #4170)
This commit is contained in:
@@ -282,6 +282,20 @@ impl ToolContext<'_> {
|
||||
pub fn star_radii_label(&self, outer: f64, inner: f64) -> String {
|
||||
format!("R1: {}\nR2: {}", self.len(outer), self.len(inner))
|
||||
}
|
||||
/// The move an arrow key nudges by: General › Keyboard Increment (ten times with Shift),
|
||||
/// along General › Constrain Angle's axes; `None` for other keys.
|
||||
pub fn arrow_nudge(&self, key: crate::ToolKey, mods: crate::Mods) -> Option<Vec2> {
|
||||
use crate::ToolKey;
|
||||
let step = self.keyboard_increment * if mods.shift { 10.0 } else { 1.0 };
|
||||
let d = match key {
|
||||
ToolKey::Left => Point::new(-step, 0.0),
|
||||
ToolKey::Right => Point::new(step, 0.0),
|
||||
ToolKey::Up => Point::new(0.0, -step),
|
||||
ToolKey::Down => Point::new(0.0, step),
|
||||
_ => return None,
|
||||
};
|
||||
Some(vectorcraft_geom::constrain_axes(self.constrain_angle).map_or(d, |axes| axes * d).to_vec2())
|
||||
}
|
||||
/// A move label: `dX: …` over `dY: …`.
|
||||
pub fn offset_label(&self, dx: f64, dy: f64) -> String {
|
||||
format!("dX: {}\ndY: {}", self.len(dx), self.len(dy))
|
||||
|
||||
+64
-20
@@ -25,8 +25,11 @@
|
||||
//! pointer ([`release_guide_at`]).
|
||||
//! Direct Selection also edits a blend's spine: drag its points (a key object on a point moves
|
||||
//! with it) and, once a point is clicked, its handles; and the points of selected gradient meshes
|
||||
//! and mesh envelopes and their handles ([`MeshEdit`]; a solid fill colour recolours a gradient
|
||||
//! mesh's point last clicked). Dragging a corner or an edge of area
|
||||
//! and mesh envelopes and their handles ([`MeshEdit`]): Shift-clicks and the marquee pick several,
|
||||
//! a click on a gradient mesh's line picks its two ends and one inside a patch the four points
|
||||
//! around it, a drag of picked points moves them all, the arrow keys nudge them, and a solid fill
|
||||
//! colour or the Transparency panel's opacity goes to a gradient mesh's picked points. Dragging a
|
||||
//! corner or an edge of area
|
||||
//! type's frame reshapes the type area (`text.reshapeArea`): the text reflows at its size. A click
|
||||
//! on a frame anchor or a marquee selects frame anchors (Shift toggles them). While the selection
|
||||
//! holds only area type with selected frame anchors, those show filled, a drag of one of them moves
|
||||
@@ -49,7 +52,7 @@ use vectorcraft_geom::{Anchor, PathData, Point, Rect, Vec2};
|
||||
use crate::bbox::move_delta;
|
||||
use crate::corners::{self, CornerDrag, over_widget};
|
||||
use crate::guides::{HandleSnap, Leave, PointSnap, Targets};
|
||||
use crate::meshedit::MeshEdit;
|
||||
use crate::meshedit::{MeshEdit, grid_of};
|
||||
use crate::pathtype::{self, BracketDrag, over_bracket};
|
||||
use crate::rulerguide::GuideEdit;
|
||||
use crate::select::{MoveSnap, hold_point, is_area_type, is_path_type, matrix_json, release_guide_at};
|
||||
@@ -280,20 +283,17 @@ impl DirectSelectionTool {
|
||||
if !matches!(self.state, State::Idle) {
|
||||
return None;
|
||||
}
|
||||
let step = cx.keyboard_increment * if mods.shift { 10.0 } else { 1.0 };
|
||||
let d = match key {
|
||||
ToolKey::Left => Point::new(-step, 0.0),
|
||||
ToolKey::Right => Point::new(step, 0.0),
|
||||
ToolKey::Up => Point::new(0.0, -step),
|
||||
ToolKey::Down => Point::new(0.0, step),
|
||||
_ => return None,
|
||||
};
|
||||
let d = cx.arrow_nudge(key, mods)?;
|
||||
let items = self.frame_anchors(cx);
|
||||
if items.is_empty() {
|
||||
(!items.is_empty()).then_some((items, d))
|
||||
}
|
||||
|
||||
/// An arrow key's nudge of the picked points of the selected meshes.
|
||||
fn mesh_nudge(&self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Option<Vec<Action>> {
|
||||
if !matches!(self.state, State::Idle) {
|
||||
return None;
|
||||
}
|
||||
let d = vectorcraft_geom::constrain_axes(cx.constrain_angle).map_or(d, |axes| axes * d);
|
||||
Some((items, d.to_vec2()))
|
||||
self.mesh.nudge(cx, key, mods, &|id| cx.selection.contains(id))
|
||||
}
|
||||
|
||||
/// Is `key` the E that Group Selection takes while a marquee is being dragged? Direct
|
||||
@@ -583,6 +583,9 @@ impl Tool for DirectSelectionTool {
|
||||
/// entirely inside it, whole, and E pressed again makes it select what it selects by default.
|
||||
/// The arrow keys nudge the selected frame anchors of area type.
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec<Action> {
|
||||
if let Some(out) = self.mesh_nudge(cx, key, mods) {
|
||||
return out;
|
||||
}
|
||||
if let Some((items, d)) = self.frame_nudge(cx, key, mods) {
|
||||
return vec![Action::Exec("text.reshapeArea".into(), reshape_params(&items, d)), Action::Notify(FRAME_PICKED.into())];
|
||||
}
|
||||
@@ -609,7 +612,7 @@ impl Tool for DirectSelectionTool {
|
||||
if frame { vec![Action::Cancel, Action::Notify(FRAME_PICKED.into())] } else { vec![Action::Cancel] }
|
||||
}
|
||||
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
|
||||
self.claims_letter(key) || self.frame_nudge(cx, key, Mods::default()).is_some()
|
||||
self.claims_letter(key) || self.mesh_nudge(cx, key, Mods::default()).is_some() || self.frame_nudge(cx, key, Mods::default()).is_some()
|
||||
}
|
||||
fn notify(&mut self, cx: &ToolContext, what: &str) {
|
||||
if what == FRAME_PICKED {
|
||||
@@ -656,9 +659,12 @@ impl Tool for DirectSelectionTool {
|
||||
self.snapped = false;
|
||||
if let Some(g) = self.mesh.hit(cx, &cx.selection.objects, p) {
|
||||
self.state = State::Mesh;
|
||||
return self.mesh.press(cx, g);
|
||||
return self.mesh.press(cx, &cx.selection.objects, g, p, ev.mods.shift);
|
||||
}
|
||||
// Shift keeps the picked mesh points: it adds to the selection.
|
||||
if !ev.mods.shift {
|
||||
self.mesh.unfocus();
|
||||
}
|
||||
self.mesh.unfocus();
|
||||
if let Some((id, si, ai, out)) = hit_handle(cx, p, point) {
|
||||
self.state = State::Handle { id, si, ai, out };
|
||||
self.handle_snap = HandleSnap::default();
|
||||
@@ -772,7 +778,7 @@ impl Tool for DirectSelectionTool {
|
||||
(PointerKind::Drag, State::Mesh) => self.mesh.drag_to(p).unwrap_or_default(),
|
||||
(PointerKind::Up, State::Mesh) => {
|
||||
self.state = State::Idle;
|
||||
self.mesh.release().unwrap_or_default()
|
||||
self.mesh.release(cx).unwrap_or_default()
|
||||
}
|
||||
(PointerKind::Drag, State::Marquee { start, toggle, enclosed, .. }) => {
|
||||
self.state = State::Marquee { start, cur: p, toggle, enclosed };
|
||||
@@ -820,7 +826,7 @@ impl Tool for DirectSelectionTool {
|
||||
State::Corner(c) => c.overlays(cx),
|
||||
_ => {
|
||||
let mut out = self.spine_overlays(cx);
|
||||
out.extend(self.mesh.overlays(cx));
|
||||
out.extend(self.mesh.overlays(cx, &|id| cx.selection.contains(id)));
|
||||
if !self.group {
|
||||
out.extend(frame_overlays(cx, &self.frame_anchors(cx)));
|
||||
out.extend(pathtype::overlays(cx));
|
||||
@@ -1131,6 +1137,24 @@ impl DirectSelectionTool {
|
||||
h.ancestry.push(part);
|
||||
h.leaf = part;
|
||||
}
|
||||
// On a gradient mesh a click picks the point, the mesh line's two ends or the four points
|
||||
// around the patch under it (Shift toggles them), and selects the mesh; a drag moves the
|
||||
// picked points.
|
||||
if !ev.mods.alt
|
||||
&& let Some(NodeKind::Mesh(m)) = cx.doc.node(h.leaf).map(|n| &n.kind)
|
||||
&& let Some(points) = crate::meshedit::part_points(m, p, cx.point_tol(), tol)
|
||||
{
|
||||
let id = h.leaf;
|
||||
let mut out = match (ev.mods.shift, cx.selection.contains(id)) {
|
||||
(true, false) => vec![Action::Exec("select.add".into(), json!({"ids": [id.0]}))],
|
||||
(false, false) => vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))],
|
||||
_ => vec![],
|
||||
};
|
||||
self.frame.clear();
|
||||
self.state = State::Mesh;
|
||||
out.extend(self.mesh.press_points(cx, &cx.selection.objects, id, &points, p, ev.mods.shift));
|
||||
return out;
|
||||
}
|
||||
// A segment pick keeps its endpoints available for movement without selecting
|
||||
// the anchors themselves. Dragging reshapes a curve, else moves it. Alt picks
|
||||
// the whole path (and Alt-drag copies it), as with Group Selection.
|
||||
@@ -1344,6 +1368,26 @@ impl DirectSelectionTool {
|
||||
});
|
||||
framed.retain(|(id, _)| editable(*id));
|
||||
}
|
||||
// Direct Selection: the mesh points inside the rect are picked (with Shift, toggled), and
|
||||
// their meshes selected whole; with Shift a mesh is toggled only when it gains its first
|
||||
// picked point or loses its last one.
|
||||
let mut meshed: Vec<NodeId> = vec![];
|
||||
if !self.group {
|
||||
let mut inside: Vec<(NodeId, usize)> = vec![];
|
||||
walk_pickable(cx.doc, cx.isolation, |n| {
|
||||
if editable(n.id)
|
||||
&& let Some(grid) = grid_of(n)
|
||||
{
|
||||
inside.extend(grid.points.iter().enumerate().filter(|(_, q)| r.contains(q.p)).map(|(i, _)| (n.id, i)));
|
||||
}
|
||||
});
|
||||
for (id, any) in self.mesh.marquee(&inside, toggle) {
|
||||
meshed.push(id);
|
||||
if !toggle || cx.selection.contains(id) != any {
|
||||
sel.push((id, vec![]));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut marked: BTreeMap<NodeId, BTreeSet<AnchorRef>> =
|
||||
if toggle { self.frame_anchors(cx).into_iter().map(|(id, v)| (id, v.into_iter().collect())).collect() } else { BTreeMap::new() };
|
||||
for (id, inside) in &framed {
|
||||
@@ -1363,7 +1407,7 @@ impl DirectSelectionTool {
|
||||
// the marquee touches them, as with the Selection tool (#927).
|
||||
for id in vectorcraft_doc::hit::marquee(cx.doc, r, cx.isolation, true) {
|
||||
let whole = cx.doc.node(id).is_some_and(|n| !matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }));
|
||||
if whole && !sel.iter().any(|(s, _)| *s == id) && !framed.iter().any(|(f, _)| *f == id) {
|
||||
if whole && !sel.iter().any(|(s, _)| *s == id) && !framed.iter().any(|(f, _)| *f == id) && !meshed.contains(&id) {
|
||||
sel.push((id, vec![]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
//! the click); click inside a mesh or a mesh envelope → `object.mesh.addLine` (a gradient mesh's
|
||||
//! new point takes the current fill color in its own model, unless Shift); drag a mesh point,
|
||||
//! or a handle of the point last clicked → `object.mesh.movePoint` previews ([`MeshEdit`]);
|
||||
//! Alt-click a mesh point → `object.mesh.deletePoint`. Clicking a gradient mesh's point selects
|
||||
//! the mesh, and the next solid fill color recolors just that point. Escape during a drag
|
||||
//! cancels it.
|
||||
//! Shift-click mesh points to pick several (Shift-click a picked one to drop it): a drag of one
|
||||
//! moves them all (`object.mesh.movePoints`) and the arrow keys nudge them. Alt-click a mesh
|
||||
//! point → `object.mesh.deletePoint`. Clicking a gradient mesh's point selects the mesh, and the
|
||||
//! next solid fill color or Transparency panel opacity goes to just the picked points. Escape
|
||||
//! during a drag cancels it.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_color::Paint;
|
||||
@@ -190,6 +192,11 @@ impl MeshTool {
|
||||
fn grab_at(&self, cx: &ToolContext, p: Point) -> Option<crate::meshedit::MeshGrab> {
|
||||
self.edit.hit(cx, &Self::meshes(cx), p)
|
||||
}
|
||||
|
||||
/// An arrow key's nudge of the picked points.
|
||||
fn nudge(&self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Option<Vec<Action>> {
|
||||
self.edit.nudge(cx, key, mods, &|_| true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for MeshTool {
|
||||
@@ -200,18 +207,23 @@ impl Tool for MeshTool {
|
||||
let p = ev.pos;
|
||||
match ev.kind {
|
||||
PointerKind::Down => {
|
||||
if let Some(g) = self.grab_at(cx, p) {
|
||||
let meshes = Self::meshes(cx);
|
||||
if let Some(g) = self.edit.hit(cx, &meshes, p) {
|
||||
if ev.mods.alt && g.handle.is_none() {
|
||||
self.edit.unfocus();
|
||||
return vec![Action::Exec("object.mesh.deletePoint".into(), json!({"id": g.id.0, "index": g.index}))];
|
||||
}
|
||||
// A gradient mesh's point is picked for the next fill colour, so its mesh is
|
||||
// what gets selected.
|
||||
// what gets selected (with Shift, added to the selection).
|
||||
let mut out = vec![];
|
||||
if cx.selection.objects != [g.id] && cx.doc.node(g.id).is_some_and(|n| matches!(n.kind, NodeKind::Mesh(_))) {
|
||||
out.push(Action::Exec("select.set".into(), json!({"ids": [g.id.0]})));
|
||||
if cx.doc.node(g.id).is_some_and(|n| matches!(n.kind, NodeKind::Mesh(_))) {
|
||||
if ev.mods.shift && !cx.selection.contains(g.id) {
|
||||
out.push(Action::Exec("select.add".into(), json!({"ids": [g.id.0]})));
|
||||
} else if !ev.mods.shift && cx.selection.objects != [g.id] {
|
||||
out.push(Action::Exec("select.set".into(), json!({"ids": [g.id.0]})));
|
||||
}
|
||||
}
|
||||
out.extend(self.edit.press(cx, g));
|
||||
out.extend(self.edit.press(cx, &meshes, g, p, ev.mods.shift));
|
||||
return out;
|
||||
}
|
||||
self.edit.unfocus();
|
||||
@@ -242,7 +254,7 @@ impl Tool for MeshTool {
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => self.edit.drag_to(p).unwrap_or_default(),
|
||||
PointerKind::Up => self.edit.release().unwrap_or_default(),
|
||||
PointerKind::Up => self.edit.release(cx).unwrap_or_default(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
@@ -256,7 +268,7 @@ impl Tool for MeshTool {
|
||||
}
|
||||
}
|
||||
}
|
||||
out.extend(self.edit.overlays(cx));
|
||||
out.extend(self.edit.overlays(cx, &|_| true));
|
||||
out
|
||||
}
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, mods: Mods) -> Cursor {
|
||||
@@ -273,16 +285,19 @@ impl Tool for MeshTool {
|
||||
self.edit.dragging()
|
||||
}
|
||||
/// Escape during a drag of a mesh point or handle cancels it: the point goes back and the
|
||||
/// release does nothing.
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
/// release does nothing. The arrow keys nudge the picked points.
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec<Action> {
|
||||
if key != ToolKey::Escape {
|
||||
return vec![];
|
||||
return self.nudge(cx, key, mods).unwrap_or_default();
|
||||
}
|
||||
self.edit.cancel().unwrap_or_default()
|
||||
}
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool {
|
||||
self.nudge(cx, key, Mods::default()).is_some()
|
||||
}
|
||||
fn deactivate(&mut self, cx: &ToolContext) -> Vec<Action> {
|
||||
self.edit.unfocus();
|
||||
self.edit.release().unwrap_or_default()
|
||||
self.edit.release(cx).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+328
-34
@@ -1,16 +1,21 @@
|
||||
//! Dragging mesh points and their handles on the canvas, shared by the Mesh tool and Direct
|
||||
//! Selection: gradient meshes and mesh envelopes alike (`object.mesh.movePoint`). Clicking a
|
||||
//! point focuses it: its handles show and can be dragged too, and on a gradient mesh a solid fill
|
||||
//! colour recolours it (the tool's `meshPoint` option).
|
||||
//! Picking and dragging mesh points and their handles on the canvas, shared by the Mesh tool and
|
||||
//! Direct Selection: gradient meshes and mesh envelopes alike. A click on a point picks it alone
|
||||
//! and focuses it: its handles show and can be dragged too. Shift-clicks add points to the picked
|
||||
//! ones or take them away, and Direct Selection's marquee and its clicks on a gradient mesh's line
|
||||
//! or patch pick points too ([`part_points`]). The picked points show filled; a drag of one of
|
||||
//! them moves them all (`object.mesh.movePoint` for one, `object.mesh.movePoints` for several)
|
||||
//! and the arrow keys nudge them. On a gradient mesh a solid fill colour recolours them and the
|
||||
//! Transparency panel's opacity fades them (the tool's `meshPoints` option).
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::live::{GradientMesh, envelope_grid};
|
||||
use vectorcraft_doc::{EnvelopeKind, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{Point, Vec2};
|
||||
|
||||
use crate::{Action, Overlay, ToolContext};
|
||||
use crate::{Action, Mods, Overlay, ToolContext, ToolKey};
|
||||
|
||||
const FEEDBACK: [u8; 3] = [0x4f, 0x9d, 0xff];
|
||||
|
||||
@@ -27,6 +32,29 @@ pub fn grid_of(n: &Node) -> Option<Cow<'_, GradientMesh>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The points of gradient mesh `m` a Direct Selection click at `p` picks: the point within
|
||||
/// `point_tol`, else the two ends of the mesh line within `line_tol`, else the four points around
|
||||
/// the patch under `p`.
|
||||
pub fn part_points(m: &GradientMesh, p: Point, point_tol: f64, line_tol: f64) -> Option<Vec<usize>> {
|
||||
if let Some(i) = m.point_near(p, point_tol) {
|
||||
return Some(vec![i]);
|
||||
}
|
||||
// `lines` lists the rows' edges, then the columns' (and nothing for a malformed mesh).
|
||||
let (rows, cols) = (m.rows.max(1) as usize, m.cols.max(1) as usize);
|
||||
if let Some((si, ..)) = m.lines().nearest(p).filter(|n| n.4 <= line_tol) {
|
||||
let across = (rows + 1) * cols;
|
||||
return Some(if si < across {
|
||||
let (r, c) = (si / cols, si % cols);
|
||||
vec![m.idx(r, c), m.idx(r, c + 1)]
|
||||
} else {
|
||||
let (c, r) = ((si - across) / rows, (si - across) % rows);
|
||||
vec![m.idx(r, c), m.idx(r + 1, c)]
|
||||
});
|
||||
}
|
||||
let (r, c, ..) = m.locate(p)?;
|
||||
Some(vec![m.idx(r, c), m.idx(r, c + 1), m.idx(r + 1, c), m.idx(r + 1, c + 1)])
|
||||
}
|
||||
|
||||
/// What a press on a mesh grabs: the mesh, the point and (on the focused point) a handle.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct MeshGrab {
|
||||
@@ -35,21 +63,46 @@ pub struct MeshGrab {
|
||||
pub handle: Option<usize>,
|
||||
}
|
||||
|
||||
/// Mesh points, each mesh with its points.
|
||||
type MeshPoints = Vec<(NodeId, Vec<usize>)>;
|
||||
|
||||
/// An in-progress mesh point or handle drag.
|
||||
struct ActiveDrag {
|
||||
grab: MeshGrab,
|
||||
/// Where the grabbed point or handle was when the drag began.
|
||||
/// Where the press was.
|
||||
start: Point,
|
||||
/// Where the grabbed point or handle was when the drag began (where the press was, on a mesh
|
||||
/// line or patch).
|
||||
origin: Point,
|
||||
/// The current drag position (preview target); set on the first `drag_to`.
|
||||
at: Option<Point>,
|
||||
/// The picked points, when the drag moves several of them by its offset; empty when it moves
|
||||
/// `grab` alone to the pointer.
|
||||
many: MeshPoints,
|
||||
}
|
||||
|
||||
/// Point and handle drags on meshes.
|
||||
/// The params of `object.mesh.movePoints` moving `items` by `d`.
|
||||
fn move_params(items: &[(NodeId, Vec<usize>)], d: Vec2) -> Value {
|
||||
match items {
|
||||
[(id, points)] => json!({"id": id.0, "points": points, "dx": d.x, "dy": d.y}),
|
||||
_ => {
|
||||
let items: Vec<Value> = items.iter().map(|(id, v)| json!({"id": id.0, "points": v})).collect();
|
||||
json!({"items": items, "dx": d.x, "dy": d.y})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mesh point picks and point and handle drags on meshes.
|
||||
#[derive(Default)]
|
||||
pub struct MeshEdit {
|
||||
/// The point last clicked: its handles show and can be dragged.
|
||||
focus: Option<(NodeId, usize)>,
|
||||
/// The picked points (the focused one among them).
|
||||
picked: BTreeSet<(NodeId, usize)>,
|
||||
drag: Option<ActiveDrag>,
|
||||
/// Pressed without Shift on points already picked among others: a click (no drag) picks
|
||||
/// these alone, as a drag moves every picked point.
|
||||
click: Option<(NodeId, Vec<usize>)>,
|
||||
}
|
||||
|
||||
impl MeshEdit {
|
||||
@@ -72,23 +125,84 @@ impl MeshEdit {
|
||||
})
|
||||
}
|
||||
|
||||
/// Start dragging `grab` (its point takes the focus).
|
||||
pub fn press(&mut self, cx: &ToolContext, grab: MeshGrab) -> Vec<Action> {
|
||||
self.focus = Some((grab.id, grab.index));
|
||||
let mut origin = Point::ZERO;
|
||||
if let Some(grid) = cx.doc.node(grab.id).and_then(grid_of)
|
||||
&& let Some(q) = grid.points.get(grab.index)
|
||||
{
|
||||
origin = if let Some(h) = grab.handle { q.p + q.handles[h] } else { q.p };
|
||||
/// The picked points that still apply: of editable meshes `shown` accepts, in range.
|
||||
fn live(&self, cx: &ToolContext, shown: &dyn Fn(NodeId) -> bool) -> MeshPoints {
|
||||
let editable = cx.doc.editable();
|
||||
let mut out: Vec<(NodeId, usize, Vec<usize>)> = vec![];
|
||||
for &(id, i) in &self.picked {
|
||||
if out.last().is_none_or(|(last, ..)| *last != id) {
|
||||
let len = if shown(id) && editable(id) { cx.doc.node(id).and_then(grid_of).map_or(0, |g| g.points.len()) } else { 0 };
|
||||
out.push((id, len, vec![]));
|
||||
}
|
||||
if let Some((_, len, v)) = out.last_mut()
|
||||
&& i < *len
|
||||
{
|
||||
v.push(i);
|
||||
}
|
||||
}
|
||||
self.drag = Some(ActiveDrag { grab, origin, at: None });
|
||||
vec![Action::Begin("Move Mesh Point".into())]
|
||||
out.into_iter().filter(|(.., v)| !v.is_empty()).map(|(id, _, v)| (id, v)).collect()
|
||||
}
|
||||
|
||||
/// A press at `p` on `grab`, among the meshes `ids`: a handle drag, or a point picked as
|
||||
/// [`Self::press_points`] picks it.
|
||||
pub fn press(&mut self, cx: &ToolContext, ids: &[NodeId], grab: MeshGrab, p: Point, shift: bool) -> Vec<Action> {
|
||||
let Some(h) = grab.handle else { return self.press_points(cx, ids, grab.id, &[grab.index], p, shift) };
|
||||
let Some(q) = cx.doc.node(grab.id).and_then(grid_of).and_then(|g| g.points.get(grab.index).cloned()) else { return vec![] };
|
||||
let origin = q.p + q.handles.get(h).copied().unwrap_or_default();
|
||||
self.start(ActiveDrag { grab, start: p, origin, at: None, many: vec![] })
|
||||
}
|
||||
|
||||
/// A press at `p` on the points `points` of mesh `id` (one point, a mesh line's ends or a
|
||||
/// patch's corners), among the meshes `ids`. Shift takes them away from the picked points when
|
||||
/// all of them are picked (no drag), else adds them; without it they are picked alone unless
|
||||
/// all of them already are. A drag then moves every picked point.
|
||||
pub fn press_points(&mut self, cx: &ToolContext, ids: &[NodeId], id: NodeId, points: &[usize], p: Point, shift: bool) -> Vec<Action> {
|
||||
let keys: Vec<(NodeId, usize)> = points.iter().map(|i| (id, *i)).collect();
|
||||
let all = keys.iter().all(|k| self.picked.contains(k));
|
||||
self.click = None;
|
||||
if shift && all {
|
||||
for k in &keys {
|
||||
self.picked.remove(k);
|
||||
}
|
||||
self.focus = self.focus.filter(|f| !keys.contains(f));
|
||||
return vec![];
|
||||
}
|
||||
if !shift && !all {
|
||||
self.picked.clear();
|
||||
}
|
||||
self.picked.extend(keys.iter().copied());
|
||||
self.focus = keys.first().copied().filter(|_| keys.len() == 1);
|
||||
let many = self.live(cx, &|m| m == id || ids.contains(&m));
|
||||
let count: usize = many.iter().map(|(_, v)| v.len()).sum();
|
||||
if count == 0 {
|
||||
return vec![];
|
||||
}
|
||||
if !shift && all && count > keys.len() {
|
||||
self.click = Some((id, points.to_vec()));
|
||||
}
|
||||
let grab = MeshGrab { id, index: points.first().copied().unwrap_or_default(), handle: None };
|
||||
// One point goes where the pointer takes it; a line or a patch moves by the drag's offset.
|
||||
let origin = match points {
|
||||
[i] => cx.doc.node(id).and_then(grid_of).and_then(|g| g.points.get(*i).map(|q| q.p)).unwrap_or(p),
|
||||
_ => p,
|
||||
};
|
||||
let many = if count == 1 && keys.len() == 1 { vec![] } else { many };
|
||||
self.start(ActiveDrag { grab, start: p, origin, at: None, many })
|
||||
}
|
||||
|
||||
fn start(&mut self, d: ActiveDrag) -> Vec<Action> {
|
||||
let label = if d.many.is_empty() { "Move Mesh Point" } else { "Move Mesh Points" };
|
||||
self.drag = Some(d);
|
||||
vec![Action::Begin(label.into())]
|
||||
}
|
||||
|
||||
/// The preview of a drag to `p` (`None` when nothing is being dragged).
|
||||
pub fn drag_to(&mut self, p: Point) -> Option<Vec<Action>> {
|
||||
let d = self.drag.as_mut()?;
|
||||
d.at = Some(p);
|
||||
if !d.many.is_empty() {
|
||||
return Some(vec![Action::Preview("object.mesh.movePoints".into(), move_params(&d.many, p - d.origin))]);
|
||||
}
|
||||
let g = d.grab;
|
||||
let mut params = json!({"id": g.id.0, "index": g.index, "x": p.x, "y": p.y});
|
||||
if let Some(h) = g.handle {
|
||||
@@ -97,13 +211,21 @@ impl MeshEdit {
|
||||
Some(vec![Action::Preview("object.mesh.movePoint".into(), params)])
|
||||
}
|
||||
|
||||
/// End a drag (`None` when nothing was being dragged).
|
||||
pub fn release(&mut self) -> Option<Vec<Action>> {
|
||||
self.drag.take().map(|_| vec![Action::Commit])
|
||||
/// End a drag (`None` when nothing was being dragged). A click on one of several picked
|
||||
/// points picks it alone.
|
||||
pub fn release(&mut self, cx: &ToolContext) -> Option<Vec<Action>> {
|
||||
let d = self.drag.take()?;
|
||||
if let Some((id, points)) = self.click.take()
|
||||
&& d.at.is_none_or(|a| a.distance(d.start) < cx.tol(3.0))
|
||||
{
|
||||
self.picked = points.into_iter().map(|i| (id, i)).collect();
|
||||
}
|
||||
Some(vec![Action::Commit])
|
||||
}
|
||||
|
||||
/// Cancel a drag: the point or handle goes back (`None` when nothing was being dragged).
|
||||
pub fn cancel(&mut self) -> Option<Vec<Action>> {
|
||||
self.click = None;
|
||||
self.drag.take().map(|_| vec![Action::Cancel])
|
||||
}
|
||||
|
||||
@@ -111,26 +233,69 @@ impl MeshEdit {
|
||||
self.drag.is_some()
|
||||
}
|
||||
|
||||
/// Forget the focused point.
|
||||
/// Forget the focused and the picked points.
|
||||
pub fn unfocus(&mut self) {
|
||||
self.focus = None;
|
||||
(self.focus, self.click) = (None, None);
|
||||
self.picked.clear();
|
||||
}
|
||||
|
||||
/// `{meshPoint: {id, index}|null}`: the focused point, which a solid fill colour recolours
|
||||
/// (`paint.setFill` on a gradient mesh).
|
||||
/// A marquee's picks: the points `inside` it are picked (with `toggle`, those picked are
|
||||
/// taken away and the others added). Returns each mesh with points inside and whether any of
|
||||
/// its points are picked now.
|
||||
pub fn marquee(&mut self, inside: &[(NodeId, usize)], toggle: bool) -> Vec<(NodeId, bool)> {
|
||||
if !toggle {
|
||||
self.picked.clear();
|
||||
}
|
||||
for k in inside {
|
||||
if !(toggle && self.picked.remove(k)) {
|
||||
self.picked.insert(*k);
|
||||
}
|
||||
}
|
||||
self.focus = self.picked.first().copied().filter(|_| self.picked.len() == 1);
|
||||
let mut meshes: Vec<NodeId> = inside.iter().map(|(id, _)| *id).collect();
|
||||
meshes.dedup();
|
||||
meshes.into_iter().map(|id| (id, self.picked.range((id, 0)..=(id, usize::MAX)).next().is_some())).collect()
|
||||
}
|
||||
|
||||
/// An arrow key's nudge of the picked points of the meshes `shown` accepts
|
||||
/// (`object.mesh.movePoints`); `None` during a drag, for other keys or without picked points.
|
||||
pub fn nudge(&self, cx: &ToolContext, key: ToolKey, mods: Mods, shown: &dyn Fn(NodeId) -> bool) -> Option<Vec<Action>> {
|
||||
if self.drag.is_some() {
|
||||
return None;
|
||||
}
|
||||
let d = cx.arrow_nudge(key, mods)?;
|
||||
let live = self.live(cx, shown);
|
||||
(!live.is_empty()).then(|| vec![Action::Exec("object.mesh.movePoints".into(), move_params(&live, d))])
|
||||
}
|
||||
|
||||
/// `{meshPoints: [{id, index}…]}`: the picked points, which a solid fill colour recolours
|
||||
/// (`paint.setFill`) and the Transparency panel's opacity fades (`transparency.set`) on a
|
||||
/// gradient mesh.
|
||||
pub fn options(&self) -> Value {
|
||||
json!({"meshPoint": self.focus.map(|(id, index)| json!({"id": id.0, "index": index}))})
|
||||
json!({"meshPoints": self.picked.iter().map(|(id, index)| json!({"id": id.0, "index": index})).collect::<Vec<_>>()})
|
||||
}
|
||||
|
||||
/// The focused point's handles (lines and ends).
|
||||
pub fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
let Some((id, index)) = self.focus else { return vec![] };
|
||||
let Some(grid) = cx.doc.node(id).and_then(grid_of) else { return vec![] };
|
||||
let Some(q) = grid.points.get(index) else { return vec![] };
|
||||
let mut out = vec![Overlay::Anchor { p: q.p, color: FEEDBACK, filled: true, size: 5.0 }];
|
||||
for h in q.handles.iter().filter(|h| h.hypot() > 1e-6) {
|
||||
out.push(Overlay::Line { a: q.p, b: q.p + *h, color: FEEDBACK, dashed: false });
|
||||
out.push(Overlay::Handle { p: q.p + *h, color: FEEDBACK });
|
||||
/// The picked points of the meshes `shown` accepts, filled, and the focused point's handles
|
||||
/// (lines and ends).
|
||||
pub fn overlays(&self, cx: &ToolContext, shown: &dyn Fn(NodeId) -> bool) -> Vec<Overlay> {
|
||||
let mut out = vec![];
|
||||
for (id, points) in self.live(cx, shown) {
|
||||
let Some(grid) = cx.doc.node(id).and_then(grid_of) else { continue };
|
||||
out.extend(points.iter().filter_map(|i| grid.points.get(*i)).map(|q| Overlay::Anchor {
|
||||
p: q.p,
|
||||
color: FEEDBACK,
|
||||
filled: true,
|
||||
size: 5.0,
|
||||
}));
|
||||
}
|
||||
if let Some((id, index)) = self.focus.filter(|(id, _)| shown(*id))
|
||||
&& let Some(grid) = cx.doc.node(id).and_then(grid_of)
|
||||
&& let Some(q) = grid.points.get(index)
|
||||
{
|
||||
for h in q.handles.iter().filter(|h| h.hypot() > 1e-6) {
|
||||
out.push(Overlay::Line { a: q.p, b: q.p + *h, color: FEEDBACK, dashed: false });
|
||||
out.push(Overlay::Handle { p: q.p + *h, color: FEEDBACK });
|
||||
}
|
||||
}
|
||||
if cx.measurement_labels
|
||||
&& let Some(d) = &self.drag
|
||||
@@ -262,4 +427,133 @@ mod tests {
|
||||
assert!(t.overlays(&on).iter().any(|o| matches!(o, Overlay::Measure { text, .. } if text == &want)));
|
||||
assert!(!t.overlays(&off).iter().any(|o| matches!(o, Overlay::Measure { .. })));
|
||||
}
|
||||
|
||||
/// A 2×2 gradient mesh over the 100..200 square, its points 50 apart.
|
||||
fn doc_with_mesh() -> (Document, NodeId) {
|
||||
let (mut d, a) = doc_with_rect();
|
||||
d.remove(a).unwrap();
|
||||
let id = d.alloc_id();
|
||||
let m = GradientMesh::from_surface(2, 2, &|u, v| Point::new(100.0 + 100.0 * u, 100.0 + 100.0 * v), &|_, _| vectorcraft_color::Color::WHITE);
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Mesh(m))).unwrap();
|
||||
(d, id)
|
||||
}
|
||||
|
||||
/// The picked points of `t`, as its `meshPoints` option lists them.
|
||||
fn picked(t: &dyn Tool) -> Vec<usize> {
|
||||
t.options()["meshPoints"].as_array().unwrap().iter().map(|p| p["index"].as_u64().unwrap() as usize).collect()
|
||||
}
|
||||
|
||||
fn shift(kind: PointerKind, x: f64, y: f64) -> PointerEvent {
|
||||
let mut e = PointerEvent::new(kind, x, y);
|
||||
e.mods.shift = true;
|
||||
e
|
||||
}
|
||||
|
||||
/// With the Mesh tool and Direct Selection, Shift-clicks pick several mesh points (and take a
|
||||
/// picked one away); they show filled, a drag of one moves them all as one move, the arrow keys
|
||||
/// nudge them, and a click on one of them picks it alone (#4207).
|
||||
#[test]
|
||||
fn shift_clicks_pick_mesh_points_that_drag_and_nudge_together() {
|
||||
let (d, m) = doc_with_mesh();
|
||||
let mut s = Selection::default();
|
||||
s.set([m]);
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let at = |x: f64, y: f64| grid_of(d.node(m).unwrap()).unwrap().point_near(Point::new(x, y), 1.0).unwrap();
|
||||
let sorted = |mut v: Vec<usize>| {
|
||||
v.sort();
|
||||
v
|
||||
};
|
||||
let (a, b, e) = (at(100.0, 100.0), at(200.0, 150.0), at(150.0, 100.0));
|
||||
let tools: [(&str, Box<dyn Tool>); 2] =
|
||||
[("mesh", crate::meshblend::create("mesh").unwrap()), ("directSelection", Box::new(crate::direct::DirectSelectionTool::new(false)))];
|
||||
for (what, mut t) in tools {
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)), vec![Action::Begin("Move Mesh Point".into())], "{what}");
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 100.0, 100.0));
|
||||
assert_eq!(t.pointer(&c, &shift(PointerKind::Down, 200.0, 150.0)), vec![Action::Begin("Move Mesh Points".into())], "{what}");
|
||||
t.pointer(&c, &shift(PointerKind::Up, 200.0, 150.0));
|
||||
assert_eq!(picked(&*t), sorted(vec![a, b]), "{what}");
|
||||
let filled = t.overlays(&c).iter().filter(|o| matches!(o, Overlay::Anchor { filled: true, .. })).count();
|
||||
assert_eq!(filled, 2, "{what}: the picked points show filled");
|
||||
// A drag of either moves both.
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)), vec![Action::Begin("Move Mesh Points".into())], "{what}");
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 110.0, 95.0)),
|
||||
vec![Action::Preview("object.mesh.movePoints".into(), json!({"id": m.0, "points": sorted(vec![a, b]), "dx": 10.0, "dy": -5.0}))],
|
||||
"{what}"
|
||||
);
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 110.0, 95.0)), vec![Action::Commit], "{what}");
|
||||
assert_eq!(picked(&*t), sorted(vec![a, b]), "{what}: a drag keeps them picked");
|
||||
// Shift-clicking a picked point takes it away, without a drag.
|
||||
assert!(t.pointer(&c, &shift(PointerKind::Down, 200.0, 150.0)).is_empty(), "{what}");
|
||||
t.pointer(&c, &shift(PointerKind::Up, 200.0, 150.0));
|
||||
assert_eq!(picked(&*t), [a], "{what}");
|
||||
// The arrow keys nudge the picked points.
|
||||
assert!(t.claims_key(&c, ToolKey::Right), "{what}");
|
||||
assert_eq!(
|
||||
t.key(&c, ToolKey::Right, Mods::default()),
|
||||
vec![Action::Exec("object.mesh.movePoints".into(), json!({"id": m.0, "points": [a], "dx": 1.0, "dy": 0.0}))],
|
||||
"{what}"
|
||||
);
|
||||
// A click on one of several picked points picks it alone.
|
||||
t.pointer(&c, &shift(PointerKind::Down, 150.0, 100.0));
|
||||
t.pointer(&c, &shift(PointerKind::Up, 150.0, 100.0));
|
||||
assert_eq!(picked(&*t), sorted(vec![a, e]), "{what}");
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 100.0, 100.0)), vec![Action::Commit], "{what}");
|
||||
assert_eq!(picked(&*t), [a], "{what}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct Selection's marquee picks the mesh points inside it and selects their mesh (Shift
|
||||
/// toggles them); a click on a mesh line picks its two ends and one inside a patch the four
|
||||
/// points around it, selecting the mesh, and a drag from there moves them (#4207).
|
||||
#[test]
|
||||
fn direct_selection_picks_mesh_points_by_marquee_line_and_patch() {
|
||||
let (d, m) = doc_with_mesh();
|
||||
let mut s = Selection::default();
|
||||
s.set([m]);
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let at = |x: f64, y: f64| grid_of(d.node(m).unwrap()).unwrap().point_near(Point::new(x, y), 1.0).unwrap();
|
||||
let set = |v: &[(f64, f64)]| {
|
||||
let mut v: Vec<usize> = v.iter().map(|(x, y)| at(*x, *y)).collect();
|
||||
v.sort();
|
||||
v
|
||||
};
|
||||
let mut t = crate::direct::DirectSelectionTool::new(false);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 90.0, 90.0));
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 160.0, 120.0));
|
||||
let out = t.pointer(&c, &PointerEvent::new(PointerKind::Up, 160.0, 120.0));
|
||||
assert!(
|
||||
matches!(out.first(), Some(Action::Exec(cmd, q)) if cmd == "select.anchorsMany" && q["items"] == json!([{"id": m.0, "anchors": []}])),
|
||||
"{out:?}"
|
||||
);
|
||||
assert_eq!(picked(&t), set(&[(100.0, 100.0), (150.0, 100.0)]));
|
||||
// Shift: the picked points inside leave, the others join; the mesh stays selected.
|
||||
t.pointer(&c, &shift(PointerKind::Down, 90.0, 90.0));
|
||||
t.pointer(&c, &shift(PointerKind::Drag, 110.0, 160.0));
|
||||
let out = t.pointer(&c, &shift(PointerKind::Up, 110.0, 160.0));
|
||||
assert!(matches!(out.first(), Some(Action::Exec(cmd, q)) if cmd == "select.anchorsMany" && q["items"] == json!([])), "{out:?}");
|
||||
assert_eq!(picked(&t), set(&[(150.0, 100.0), (100.0, 150.0)]));
|
||||
// A mesh line: its two ends.
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 125.0, 150.0)), vec![Action::Begin("Move Mesh Points".into())]);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 125.0, 150.0));
|
||||
assert_eq!(picked(&t), set(&[(100.0, 150.0), (150.0, 150.0)]));
|
||||
// A patch: the four points around it, which a drag moves by its offset.
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 175.0, 175.0)), vec![Action::Begin("Move Mesh Points".into())]);
|
||||
let four = set(&[(150.0, 150.0), (200.0, 150.0), (150.0, 200.0), (200.0, 200.0)]);
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 185.0, 180.0)),
|
||||
vec![Action::Preview("object.mesh.movePoints".into(), json!({"id": m.0, "points": four, "dx": 10.0, "dy": 5.0}))]
|
||||
);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 185.0, 180.0));
|
||||
// A mesh that isn't selected yet gets selected.
|
||||
let none = Selection::default();
|
||||
let c = cx(&d, &none, &p);
|
||||
let out = t.pointer(&c, &PointerEvent::new(PointerKind::Down, 125.0, 125.0));
|
||||
assert_eq!(out, vec![Action::Exec("select.set".into(), json!({"ids": [m.0]})), Action::Begin("Move Mesh Points".into())]);
|
||||
assert_eq!(picked(&t), set(&[(100.0, 100.0), (150.0, 100.0), (100.0, 150.0), (150.0, 150.0)]));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user