Merge pull request #4639 from storytold/feat/mesh-multi-points

Gradient Mesh: select several mesh points; move, color and opacity reach them all (#4207, #4208, #4170)
This commit is contained in:
BFlat
2026-10-30 04:09:51 +07:00
committed by GitHub
37 changed files with 774 additions and 116 deletions
+11 -2
View File
@@ -7,7 +7,7 @@ use super::envelope::{
env_edit_contents, env_expand, env_info, env_make_mesh, env_make_top, env_make_warp, env_options, env_release, env_reset_mesh, env_reset_warp,
env_set_mesh_point,
};
use super::mesh::{mesh_add_line, mesh_create, mesh_delete_point, mesh_expand, mesh_move_point, mesh_set_color};
use super::mesh::{mesh_add_line, mesh_create, mesh_delete_point, mesh_expand, mesh_move_point, mesh_move_points, mesh_set_color};
use super::{CommandSpec, always, cmd, has_doc, has_multi, has_selection};
pub fn specs() -> Vec<CommandSpec> {
@@ -245,7 +245,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Set Mesh Point Color",
[],
None,
"{id, index, color, keepModel?: bool (as paint.setFill), opacity?: 0..1} colour of one mesh point",
"{id, index | points: [index…] | items: [{id, index | points}…], color?, keepModel?: bool (as paint.setFill), opacity?: 0..1} colour and/or opacity of gradient mesh points (one undo step; an index out of range changes nothing)",
has_doc,
mesh_set_color
),
@@ -258,6 +258,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
mesh_move_point
),
cmd!(
"object.mesh.movePoints",
"Move Mesh Points",
[],
None,
"{id, points: [index…] | items: [{id, index | points}…], dx, dy} move points of gradient meshes or mesh envelopes by (dx, dy), their handles with them (one undo step; an index out of range changes nothing)",
has_doc,
mesh_move_points
),
cmd!(
"object.mesh.addLine",
"Add Mesh Line",
+89 -11
View File
@@ -1,7 +1,9 @@
use std::collections::{BTreeMap, BTreeSet};
use super::common::{fix_ids, ids_json, is_mesh, replace_with, roots_param, selected_of};
use super::envelope::{outline_of, rows_cols};
use super::{
Color, Document, EngineError, EnvelopeKind, GradientMesh, MeshAppearance, Node, NodeKind, Paint, Point, Result, Session, Value, bad,
Color, Document, EngineError, EnvelopeKind, GradientMesh, MeshAppearance, Node, NodeId, NodeKind, Paint, Point, Result, Session, Value, bad,
check_numbers, color_value, f64_or, f64_req, finite_numbers, gradient_sampler, id_param, json, live, ok, str_param,
};
@@ -106,25 +108,86 @@ fn image_mesh(d: &Document, im: &vectorcraft_doc::ImageObject, rows: u32, cols:
}
/// Run `f` on the gradient mesh `p.id` (one undo step `label`); with `envelopes`, a mesh
/// envelope's grid too (its points and handles are written back: editing fills in the handles).
/// envelope's grid too (see [`edit_grid`]).
fn with_mesh<T>(s: &mut Session, p: &Value, cmd: &str, label: &str, envelopes: bool, f: impl FnOnce(&mut GradientMesh) -> Result<T>) -> Result<T> {
let id = id_param(p, "id").ok_or_else(|| bad(cmd, "missing `id`"))?;
let c = cmd.to_string();
s.edit(label, move |d, _| match d.node_mut(id).map(|n| &mut n.kind) {
s.edit(label, move |d, _| edit_grid(d, id, cmd, envelopes, f))
}
/// Run `f` on the gradient mesh `id` of `d`; with `envelopes`, a mesh envelope's grid too (its
/// points and handles are written back: editing fills in the handles).
fn edit_grid<T>(d: &mut Document, id: NodeId, cmd: &str, envelopes: bool, f: impl FnOnce(&mut GradientMesh) -> Result<T>) -> Result<T> {
match d.node_mut(id).map(|n| &mut n.kind) {
Some(NodeKind::Mesh(m)) => f(m),
Some(NodeKind::Envelope { kind, .. }) if envelopes && matches!(**kind, EnvelopeKind::Mesh { .. }) => {
let EnvelopeKind::Mesh { rows, cols, points, handles } = &mut **kind else {
return Err(bad(&c, "not a gradient mesh or mesh envelope"));
return Err(bad(cmd, "not a gradient mesh or mesh envelope"));
};
let mut m = live::envelope_grid(*rows, *cols, points, handles).ok_or_else(|| bad(&c, "the envelope's mesh is malformed"))?;
let mut m = live::envelope_grid(*rows, *cols, points, handles).ok_or_else(|| bad(cmd, "the envelope's mesh is malformed"))?;
let r = f(&mut m)?;
(*rows, *cols) = (m.rows, m.cols);
*points = m.points.iter().map(|q| q.p).collect();
*handles = m.points.iter().map(|q| q.handles).collect();
Ok(r)
}
Some(_) => Err(bad(&c, if envelopes { "not a gradient mesh or mesh envelope" } else { "not a gradient mesh" })),
Some(_) => Err(bad(cmd, if envelopes { "not a gradient mesh or mesh envelope" } else { "not a gradient mesh" })),
None => Err(EngineError::NoNode(id)),
}
}
/// Most mesh points one command names, over all its meshes.
const MAX_POINTS: usize = 1 << 20;
/// The mesh points `p` names: `{id, index}`, `{id, points: [index…]}` or `{items: [{id, index |
/// points}…]}`. Each mesh comes once, with its points in order and without repeats.
fn point_items(p: &Value, cmd: &str) -> Result<Vec<(NodeId, BTreeSet<usize>)>> {
let one = |q: &Value| -> Result<(NodeId, BTreeSet<usize>)> {
let id = id_param(q, "id").ok_or_else(|| bad(cmd, "missing `id`"))?;
let points = match q.get("points") {
None | Some(Value::Null) => BTreeSet::from([index_param(q, "index", cmd)?]),
Some(Value::Array(v)) if v.len() <= MAX_POINTS => v
.iter()
.map(|i| index_of(i.as_f64().ok_or_else(|| bad(cmd, "`points` must be point indexes"))?, "points", cmd))
.collect::<Result<_>>()?,
Some(_) => return Err(bad(cmd, "`points` must be a list of point indexes")),
};
Ok((id, points))
};
let items: Vec<(NodeId, BTreeSet<usize>)> = match p.get("items") {
None | Some(Value::Null) => vec![one(p)?],
Some(Value::Array(v)) if v.len() <= MAX_POINTS => v.iter().map(one).collect::<Result<_>>()?,
Some(_) => return Err(bad(cmd, "`items` must be a list of {id, points}")),
};
let mut merged: BTreeMap<NodeId, BTreeSet<usize>> = BTreeMap::new();
for (id, points) in items {
merged.entry(id).or_default().extend(points);
}
if merged.values().map(BTreeSet::len).sum::<usize>() > MAX_POINTS {
return Err(bad(cmd, format!("at most {MAX_POINTS} points")));
}
Ok(merged.into_iter().collect())
}
/// Run `f` on each mesh point `items` names, as one undo step `label` (an index out of range
/// fails it, changing nothing); with `envelopes`, on mesh envelopes' points too.
fn with_points(
s: &mut Session,
items: &[(NodeId, BTreeSet<usize>)],
cmd: &str,
label: &str,
envelopes: bool,
mut f: impl FnMut(&mut live::MeshPoint) -> Result<()>,
) -> Result<()> {
s.edit(label, |d, _| {
for (id, points) in items {
edit_grid(d, *id, cmd, envelopes, |m| {
for i in points {
f(m.points.get_mut(*i).ok_or_else(|| bad(cmd, "index out of range"))?)?;
}
Ok(())
})?;
}
Ok(())
})
}
@@ -140,7 +203,11 @@ pub(super) fn handle_param(p: &Value, cmd: &str) -> Result<Option<usize>> {
}
pub(super) fn index_param(p: &Value, key: &str, cmd: &str) -> Result<usize> {
let i = f64_req(p, key, cmd)?;
index_of(f64_req(p, key, cmd)?, key, cmd)
}
/// `i`, the value of `key`, as an index: a whole number ≥ 0.
fn index_of(i: f64, key: &str, cmd: &str) -> Result<usize> {
if i < 0.0 {
return Err(bad(cmd, "index must be ≥ 0"));
}
@@ -156,14 +223,13 @@ pub(super) fn mesh_set_color(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.mesh.setPointColor";
check_numbers(p, &["opacity"], C)?;
crate::cmd::paint::check_keep_model(p, C)?;
let index = index_param(p, "index", C)?;
let items = point_items(p, C)?;
let color = p.get("color").and_then(color_value).map(|c| crate::cmd::paint::new_color(s, p, c));
let opacity = p.get("opacity").and_then(Value::as_f64);
if color.is_none() && opacity.is_none() {
return Err(bad(C, "missing `color` or `opacity`"));
}
with_mesh(s, p, C, "Mesh Point Color", false, |m| {
let pt = m.points.get_mut(index).ok_or_else(|| bad(C, "index out of range"))?;
with_points(s, &items, C, "Mesh Point Color", false, |pt| {
if let Some(c) = color {
pt.color = c;
}
@@ -191,6 +257,18 @@ pub(super) fn mesh_move_point(s: &mut Session, p: &Value) -> Result<Value> {
ok()
}
pub(super) fn mesh_move_points(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.mesh.movePoints";
let items = point_items(p, C)?;
let (dx, dy) = (f64_req(p, "dx", C)?, f64_req(p, "dy", C)?);
// `Session::edit` refuses a move past the canvas's coordinate range.
with_points(s, &items, C, "Move Mesh Points", true, |pt| {
pt.p = Point::new(pt.p.x + dx, pt.p.y + dy);
Ok(())
})?;
ok()
}
pub(super) fn mesh_add_line(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.mesh.addLine";
let q = Point::new(f64_req(p, "x", C)?, f64_req(p, "y", C)?);
+18 -11
View File
@@ -10,7 +10,7 @@ use std::collections::HashSet;
use serde_json::{Value, json};
use vectorcraft_color::BlendMode;
use vectorcraft_doc::{Document, Knockout, Node, NodeId, OpacityMask, Selection};
use vectorcraft_doc::{Document, Knockout, Node, NodeId, NodeKind, OpacityMask, Selection};
use super::maskedit::{begin, finish, mask_group, mask_parts, show_editing};
use super::pathops::{Beside, separate};
@@ -368,12 +368,19 @@ impl TransparencyInfo {
}
}
/// The opacity of the focused gradient mesh `point` (see `transparency.set`) instead of its
/// object's.
fn with_mesh_point(mut self, point: Option<(NodeId, usize, f32)>) -> Self {
if let Some((_, _, o)) = point {
self.opacity = Some((o as f64 * 10_000.0).round() / 100.0);
}
/// The opacity of the picked gradient mesh `points` (see `transparency.set`) instead of their
/// objects': `None` where they differ.
fn with_mesh_points(mut self, d: &Document, points: Option<Vec<(NodeId, Vec<usize>)>>) -> Self {
let Some(points) = points else { return self };
let mut all = points.iter().flat_map(|(id, v)| {
let m = match d.node(*id).map(|n| &n.kind) {
Some(NodeKind::Mesh(m)) => Some(m),
_ => None,
};
v.iter().filter_map(move |i| m?.points.get(*i)).map(|q| (q.opacity as f64 * 10_000.0).round() as i64)
});
let first = all.next();
self.opacity = first.filter(|f| all.all(|o| o == *f)).map(|o| o as f64 / 100.0);
self
}
}
@@ -404,8 +411,8 @@ impl Session {
pub fn transparency_info(&self) -> TransparencyInfo {
match (self.active(), transparency_targets(self, &Value::Null)) {
(Some(st), Ok(ids)) => {
let point = super::paint::focused_mesh_point_of(self, &ids);
TransparencyInfo::of(&st.doc, ids).with_mesh_point(point)
let points = super::paint::picked_mesh_points_of(self, &ids);
TransparencyInfo::of(&st.doc, ids).with_mesh_points(&st.doc, points)
}
_ => TransparencyInfo::default(),
}
@@ -415,8 +422,8 @@ impl Session {
fn info(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let ids = transparency_targets(s, p)?;
let point = super::paint::focused_mesh_point_of(s, &ids);
let i = TransparencyInfo::of(&st.doc, ids).with_mesh_point(point);
let points = super::paint::picked_mesh_points_of(s, &ids);
let i = TransparencyInfo::of(&st.doc, ids).with_mesh_points(&st.doc, points);
Ok(json!({
"ids": i.ids.iter().map(|id| id.0).collect::<Vec<_>>(),
"opacity": i.opacity,
+36 -21
View File
@@ -462,9 +462,9 @@ pub(crate) fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool)
let selected = item.targets(s, p)?;
let ids = super::graph::paint_targets(&s.doc()?.doc, &selected);
let paint = if span { placed_across(&s.doc()?.doc, &ids, paint, p, fill) } else { paint };
// The gradient mesh point last clicked with the Mesh tool or Direct Selection: a solid fill
// recolours just that point of its mesh.
let mesh_point = if fill && matches!(paint, Paint::Solid { .. }) { focused_mesh_point(s) } else { None };
// The gradient mesh points picked with the Mesh tool or Direct Selection: a solid fill
// recolours just those points of their meshes.
let mesh_points = if fill && matches!(paint, Paint::Solid { .. }) { picked_mesh_points(s, &ids) } else { vec![] };
// Without `item`, unless the Appearance panel's active fill item takes the paint.
let colorized = if fill && p.get("item").is_none() && item == ItemTarget::Top {
colorized_images(&s.doc()?.doc, &ids, &paint).map_err(|e| bad(cmd, e))?
@@ -486,11 +486,14 @@ pub(crate) fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool)
}
if index.is_none()
&& let Paint::Solid { color, .. } = &paint
&& let Some((_, i)) = mesh_point.filter(|(id, _)| *id == n.id)
&& let Some((_, points)) = mesh_points.iter().find(|(id, _)| *id == n.id)
&& let NodeKind::Mesh(m) = &mut n.kind
&& let Some(q) = m.points.get_mut(i)
{
q.color = *color;
for i in points {
if let Some(q) = m.points.get_mut(*i) {
q.color = *color;
}
}
return Ok(());
}
if index.is_none()
@@ -689,20 +692,31 @@ fn black_or_white(c: &Color) -> bool {
}
}
/// The mesh point the active tool has focused (its `meshPoint` option: the Mesh tool's or Direct
/// Selection's point last clicked).
fn focused_mesh_point(s: &Session) -> Option<(NodeId, usize)> {
/// The points the active tool has picked (its `meshPoints` option: the Mesh tool's or Direct
/// Selection's) on the gradient meshes among `ids`, in range, each mesh with its points.
fn picked_mesh_points(s: &Session, ids: &[NodeId]) -> Vec<(NodeId, Vec<usize>)> {
let o = s.tool_options();
let pt = o.get("meshPoint")?;
Some((NodeId(pt.get("id")?.as_u64()?), usize::try_from(pt.get("index")?.as_u64()?).ok()?))
let (Some(picked), Ok(st)) = (o.get("meshPoints").and_then(Value::as_array), s.doc()) else { return vec![] };
let mut out: Vec<(NodeId, Vec<usize>)> = vec![];
for pt in picked {
let (Some(id), Some(index)) = (pt.get("id").and_then(Value::as_u64).map(NodeId), pt.get("index").and_then(Value::as_u64)) else { continue };
let Ok(index) = usize::try_from(index) else { continue };
if !ids.contains(&id) || !matches!(st.doc.node(id).map(|n| &n.kind), Some(NodeKind::Mesh(m)) if index < m.points.len()) {
continue;
}
match out.iter_mut().find(|(m, _)| *m == id) {
Some((_, v)) => v.push(index),
None => out.push((id, vec![index])),
}
}
out
}
/// The focused mesh point (see [`focused_mesh_point`]) with its opacity, when `ids` is just the
/// gradient mesh it belongs to.
pub(crate) fn focused_mesh_point_of(s: &Session, ids: &[NodeId]) -> Option<(NodeId, usize, f32)> {
let (id, index) = focused_mesh_point(s).filter(|(id, _)| ids == [*id])?;
let NodeKind::Mesh(m) = &s.doc().ok()?.doc.node(id)?.kind else { return None };
Some((id, index, m.points.get(index)?.opacity))
/// The picked gradient mesh points (see [`picked_mesh_points`]) when each of `ids` is a gradient
/// mesh with points picked: the Transparency panel's opacity is theirs then.
pub(crate) fn picked_mesh_points_of(s: &Session, ids: &[NodeId]) -> Option<Vec<(NodeId, Vec<usize>)>> {
let v = picked_mesh_points(s, ids);
(!ids.is_empty() && ids.iter().all(|id| v.iter().any(|(m, _)| m == id))).then_some(v)
}
/// A type run's stroke; a painted stroke on a run without one gets 1 pt.
@@ -848,15 +862,16 @@ fn transparency(s: &mut Session, p: &Value) -> Result<Value> {
q.remove("item");
q.remove("id");
q.insert("ids".into(), json!(ids.iter().map(|id| id.0).collect::<Vec<_>>()));
// The gradient mesh point last clicked with the Mesh tool or Direct Selection takes the
// opacity itself, so only the mesh around it fades (#4206).
// The gradient mesh points picked with the Mesh tool or Direct Selection take the opacity
// themselves, so only the mesh around them fades (#4206, #4170, #4208).
if matches!(item, ItemTarget::Top)
&& let Some(o) = q.get("opacity").and_then(Value::as_f64)
&& let Some((id, index, _)) = focused_mesh_point_of(s, &ids)
&& let Some(points) = picked_mesh_points_of(s, &ids)
{
q.remove("opacity");
let opacity = f64::from(super::opacitymask::percent(o));
s.execute("object.mesh.setPointColor", &json!({"id": id.0, "index": index, "opacity": opacity}))?;
let items: Vec<Value> = points.iter().map(|(id, v)| json!({"id": id.0, "points": v})).collect();
s.execute("object.mesh.setPointColor", &json!({"items": items, "opacity": opacity}))?;
if q.keys().all(|k| k == "ids") {
return ok();
}
+75
View File
@@ -233,6 +233,81 @@ fn a_clicked_mesh_point_takes_the_opacity() {
assert!(mesh_opacities(&s, m).iter().all(|a| *a == 1.0));
}
/// Mesh points picked together (Shift-clicks with the Mesh tool, a Direct Selection marquee) all
/// take a solid fill colour and the Transparency panel's opacity, and only they do; the panel
/// shows their opacity, blank when it differs; a drag of one moves them all as one undo step
/// (#4207, #4208, #4170).
#[test]
fn picked_mesh_points_take_the_fill_opacity_and_drag_together() {
let mut s = session();
let m = rect(&mut s, json!("#ff0000"));
s.execute("object.mesh.create", &json!({"rows": 2, "cols": 2})).unwrap();
s.execute("select.set", &json!({"ids": [m.0]})).unwrap();
let v = ViewInfo::default();
let point = |s: &Session, i: usize| {
let NodeKind::Mesh(g) = &s.doc().unwrap().doc.node(m).unwrap().kind else { panic!("not a mesh") };
g.points[i].p
};
let pointer = |s: &mut Session, kind: PointerKind, q: vectorcraft_geom::Point, shift: bool| {
let mut ev = PointerEvent::new(kind, q.x, q.y);
ev.mods.shift = shift;
s.pointer(&ev, v).unwrap();
};
let click = |s: &mut Session, i: usize, shift: bool| {
let q = point(s, i);
pointer(s, PointerKind::Down, q, shift);
pointer(s, PointerKind::Up, q, shift);
};
let check = |s: &Session, picked: &[usize], hex: &str, opacity: f32, what: &str| {
let (c, o) = (mesh_colors(s, m), mesh_opacities(s, m));
for i in 0..c.len() {
let on = picked.contains(&i);
assert_eq!(c[i].to_hex() == hex, on, "{what}: point {i} colour {c:?}");
assert_eq!((o[i] - opacity).abs() < 1e-6, on, "{what}: point {i} opacity {o:?}");
}
assert_eq!(s.doc().unwrap().doc.node(m).unwrap().opacity, 1.0, "{what}: the object stays opaque");
};
// The Mesh tool: a click, then Shift-clicks add points (and a second one takes one away).
s.select_tool("mesh", v).unwrap();
click(&mut s, 1, false);
for i in [4, 8, 8] {
click(&mut s, i, true);
}
s.execute("paint.setFill", &json!({"color": "#0000ff"})).unwrap();
let undo = s.doc().unwrap().history.undo.len();
s.execute("transparency.set", &json!({"opacity": 40})).unwrap();
check(&s, &[1, 4], "#0000ff", 0.4, "mesh tool");
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step for both points");
assert_eq!(s.transparency_info().opacity, Some(40.0), "the panel shows the points' opacity");
s.execute("object.mesh.setPointColor", &json!({"id": m.0, "index": 4, "opacity": 0.9})).unwrap();
assert_eq!(s.transparency_info().opacity, None, "mixed: blank");
// Direct Selection: a marquee over the top-left patch picks its four points.
s.select_tool("directSelection", v).unwrap();
let corner = point(&s, 0);
pointer(&mut s, PointerKind::Down, corner - vectorcraft_geom::Vec2::new(8.0, 8.0), false);
let far = point(&s, 4) + vectorcraft_geom::Vec2::new(5.0, 5.0);
pointer(&mut s, PointerKind::Drag, far, false);
pointer(&mut s, PointerKind::Up, far, false);
assert_eq!(s.doc().unwrap().selection.objects, [m], "the marquee selects the mesh");
s.execute("paint.setFill", &json!({"color": "#00ff00"})).unwrap();
s.execute("transparency.set", &json!({"opacity": 20})).unwrap();
check(&s, &[0, 1, 3, 4], "#00ff00", 0.2, "marquee");
// A drag of one picked point moves all four by the same offset, in one step.
let before: Vec<_> = (0..9).map(|i| point(&s, i)).collect();
let undo = s.doc().unwrap().history.undo.len();
let d = vectorcraft_geom::Vec2::new(4.0, 6.0);
pointer(&mut s, PointerKind::Down, before[4], false);
pointer(&mut s, PointerKind::Drag, before[4] + d, false);
pointer(&mut s, PointerKind::Up, before[4] + d, false);
for (i, was) in before.iter().enumerate() {
let want = if [0, 1, 3, 4].contains(&i) { *was + d } else { *was };
assert!(point(&s, i).distance(want) < 1e-9, "point {i}: {:?} ≠ {want:?}", point(&s, i));
}
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!((0..9).map(|i| point(&s, i)).collect::<Vec<_>>(), before);
}
#[test]
fn blend_moves_a_mesh_to_the_blended_colour() {
let mut s = session();
+57
View File
@@ -851,6 +851,63 @@ fn mesh_point_colour_move_and_undo() {
assert!(s.execute("object.mesh.setPointColor", &json!({"id": a.0, "index": 99, "color": "#000000"})).is_err());
}
/// `object.mesh.movePoints` moves just the points it names (their handles with them), over one or
/// several meshes, as one undo step; `object.mesh.setPointColor` colours and fades several points
/// alike. A bad index, a non-number or a move past the coordinate range fails and changes nothing
/// (#4207, #4208).
#[test]
fn mesh_points_move_and_colour_together() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
let b = rect(&mut s, 200.0, 0.0, 100.0, 100.0);
sel(&mut s, &[a, b]);
s.execute("object.mesh.create", &json!({"rows": 2, "cols": 2})).unwrap();
let (ma, mb) = (mesh_of(&s, a), mesh_of(&s, b));
let n = undo_len(&s);
s.execute("object.mesh.movePoints", &json!({"id": a.0, "points": [0, 4, 4], "dx": 10, "dy": -5})).unwrap();
let m = mesh_of(&s, a);
for (i, (q, was)) in m.points.iter().zip(&ma.points).enumerate() {
let want = if i == 0 || i == 4 { Point::new(was.p.x + 10.0, was.p.y - 5.0) } else { was.p };
assert_eq!((q.p, q.handles), (want, was.handles), "point {i}: moved once if named, its handles with it");
}
assert_eq!(undo_len(&s), n + 1);
s.execute("object.mesh.movePoints", &json!({"items": [{"id": a.0, "points": [1]}, {"id": b.0, "index": 2}], "dx": 1, "dy": 1})).unwrap();
assert_eq!(mesh_of(&s, a).points[1].p, Point::new(ma.points[1].p.x + 1.0, ma.points[1].p.y + 1.0));
assert_eq!(mesh_of(&s, b).points[2].p, Point::new(mb.points[2].p.x + 1.0, mb.points[2].p.y + 1.0));
assert_eq!(undo_len(&s), n + 2, "both meshes in one step");
// A move that would take a point past the coordinate range fails.
assert!(s.execute("object.mesh.movePoints", &json!({"id": a.0, "points": [0], "dx": 1e308, "dy": 0})).is_err());
assert_eq!(undo_len(&s), n + 2);
for _ in 0..2 {
s.execute("edit.undo", &json!({})).unwrap();
}
assert_eq!((mesh_of(&s, a), mesh_of(&s, b)), (ma.clone(), mb.clone()));
// Several points take one colour and opacity, in one step.
s.execute("object.mesh.setPointColor", &json!({"id": a.0, "points": [2, 6], "color": "#0000ff", "opacity": 0.25})).unwrap();
for (i, q) in mesh_of(&s, a).points.iter().enumerate() {
let named = i == 2 || i == 6;
assert_eq!((q.color.to_hex() == "#0000ff", q.opacity == 0.25), (named, named), "point {i}");
}
assert_eq!(undo_len(&s), n + 1);
let n = undo_len(&s);
let before = (mesh_of(&s, a), mesh_of(&s, b));
for p in [
json!({"id": a.0, "points": [0, 99], "dx": 1, "dy": 1}),
json!({"items": [{"id": a.0, "points": [0]}, {"id": b.0, "points": [99]}], "dx": 1, "dy": 1}),
json!({"id": a.0, "points": [-1], "dx": 1, "dy": 1}),
json!({"id": a.0, "points": [1.5], "dx": 1, "dy": 1}),
json!({"id": a.0, "points": ["1"], "dx": 1, "dy": 1}),
json!({"id": a.0, "points": 1, "dx": 1, "dy": 1}),
json!({"id": a.0, "points": [0], "dx": "1", "dy": 1}),
json!({"items": 3, "dx": 1, "dy": 1}),
json!({"points": [0], "dx": 1, "dy": 1}),
] {
assert!(s.execute("object.mesh.movePoints", &p).is_err(), "{p}");
}
assert!(s.execute("object.mesh.setPointColor", &json!({"id": a.0, "points": [0, 9], "color": "#000000"})).is_err());
assert_eq!(((mesh_of(&s, a), mesh_of(&s, b)), undo_len(&s)), (before, n), "nothing changed");
}
#[test]
fn mesh_add_line_and_delete_point() {
let mut s = session();
+14
View File
@@ -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
View File
@@ -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![]));
}
}
+30 -15
View File
@@ -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
View File
@@ -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)]));
}
}
+1
View File
@@ -1860,6 +1860,7 @@ font-technology Variable Променливи
Move in Perspective Преместване в перспектива
Move Locked and Hidden Artwork with Artboard Преместване на заключени и скрити обекти с работната област
Move Mesh Point Преместване на точка от мрежата
Move Mesh Points Преместване на точки от мрежата
Move Plane Преместване на равнина
Move Plane to Match Object Преместване на равнината според обекта
Move Rows Преместване на редове
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2419,6 +2419,7 @@
Move Library Item to Group Mou l’element de la biblioteca al grup
Move Locked and Hidden Artwork with Artboard Mou la il·lustració bloquejada i oculta amb el tauler
Move Mesh Point Mou el punt de malla
Move Mesh Points Mou els punts de malla
Move Panel Mou el panell
Move Plane Mou el pla
Move Plane and Copy Objects Mou el pla i copia els objectes
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3314,6 +3314,7 @@ theme Appearance Motiv
Make Envelope Vytvořit obálku
Move Envelope Mesh Point Přesunout bod sítě obálky
Move Mesh Point Přesunout bod sítě
Move Mesh Points Přesunout body sítě
Move Spine Point Přesunout bod páteře
Reshape Spine Segment Přetvarovat segment páteře
Reset with Mesh Obnovit sítí
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3072,6 +3072,7 @@ theme Appearance Udseende
Make Envelope Opret konvolut
Move Envelope Mesh Point Flyt konvolut-meshpunkt
Move Mesh Point Flyt meshpunkt
Move Mesh Points Flyt meshpunkter
Move Spine Point Flyt rygradpunkt
Reshape Spine Segment Omform rygradsegment
Reset with Mesh Nulstil med mesh
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1915,6 +1915,7 @@ file-filter Templates Vorlagen
Move in Perspective In Perspektive verschieben
Move Locked and Hidden Artwork with Artboard Gesperrte und ausgeblendete Grafik mit Zeichenfläche verschieben
Move Mesh Point Gitterpunkt verschieben
Move Mesh Points Gitterpunkte verschieben
Move Plane Perspektivebene verschieben
Move Plane to Match Object Perspektivebene an Objekt ausrichten
Move Rows Zeilen verschieben
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2675,6 +2675,7 @@ theme Appearance Εμφάνιση
Move in Perspective Μετακίνηση σε προοπτική
Move Locked and Hidden Artwork with Artboard Μετακίνηση κλειδωμένων και κρυφών σχεδίων μαζί με τον πίνακα σχεδίασης
Move Mesh Point Μετακίνηση σημείου πλέγματος
Move Mesh Points Μετακίνηση σημείων πλέγματος
Move Plane Μετακίνηση επιπέδου προοπτικής
Move Plane to Match Object Μετακίνηση επιπέδου προοπτικής ώστε να ταιριάζει στο αντικείμενο
Move Rows Μετακίνηση γραμμών
Can't render this file because it contains an unexpected character in line 164 and column 5.
+1
View File
@@ -1913,6 +1913,7 @@ font-technology Variable Variables
Move in Perspective Mover en perspectiva
Move Locked and Hidden Artwork with Artboard Mover ilustración bloqueada y oculta con la mesa de trabajo
Move Mesh Point Mover punto de malla
Move Mesh Points Mover puntos de malla
Move Plane Mover plano
Move Plane to Match Object Mover plano para que coincida con el objeto
Move Rows Mover filas
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3253,6 +3253,7 @@ theme Appearance Ulkoasu
Make Envelope Tee kuori
Move Envelope Mesh Point Siirrä kuoren verkkopistettä
Move Mesh Point Siirrä verkkopistettä
Move Mesh Points Siirrä verkkopisteitä
Move Spine Point Siirrä selkärangan pistettä
Reshape Spine Segment Muotoile selkärangan segmentti uudelleen
Reset with Mesh Palauta verkolla
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1912,6 +1912,7 @@ font-technology Variable Variables
Move in Perspective Déplacer en perspective
Move Locked and Hidden Artwork with Artboard Déplacer les illustrations verrouillées et masquées avec le plan de travail
Move Mesh Point Déplacer le point de filet
Move Mesh Points Déplacer les points de filet
Move Plane Déplacer le plan
Move Plane to Match Object Déplacer le plan pour l’aligner sur l’objet
Move Rows Déplacer les lignes
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2418,6 +2418,7 @@ file-filter Templates Sablonok
Move Library Item to Group Könyvtárelem áthelyezése csoportba
Move Locked and Hidden Artwork with Artboard Zárolt és rejtett grafika mozgatása a rajztáblával
Move Mesh Point Hálópont áthelyezése
Move Mesh Points Hálópontok áthelyezése
Move Panel Panel áthelyezése
Move Plane Sík áthelyezése
Move Plane to Match Object Sík igazítása az objektumhoz
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3072,6 +3072,7 @@ theme Appearance Tampilan
Make Envelope Buat envelope
Move Envelope Mesh Point Pindahkan titik mesh envelope
Move Mesh Point Pindahkan titik mesh
Move Mesh Points Pindahkan titik-titik mesh
Move Spine Point Pindahkan titik spine
Reshape Spine Segment Bentuk ulang segmen spine
Reset with Mesh Atur ulang dengan mesh
Can't render this file because it contains an unexpected character in line 2169 and column 5.
+1
View File
@@ -1940,6 +1940,7 @@ file-filter Templates Modelli
Move in Perspective Sposta in prospettiva
Move Locked and Hidden Artwork with Artboard Sposta grafica bloccata e nascosta con la tavola da disegno
Move Mesh Point Sposta punto trama
Move Mesh Points Sposta punti trama
Move Plane Sposta piano
Move Plane to Match Object Sposta piano in base all'oggetto
Move Rows Sposta righe
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2082,6 +2082,7 @@ font-technology Variable バリアブル
Move Guide ガイドを移動
Move Locked and Hidden Artwork with Artboard ロックおよび非表示のアートワークをアートボードと一緒に移動
Move Mesh Point メッシュポイントを移動
Move Mesh Points メッシュポイントを移動
Move Plane 面を移動
Move Rows 行を移動
Move Slices スライスを移動
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2885,6 +2885,7 @@ theme Appearance იერსახე
Make Envelope გარსის შექმნა
Move Envelope Mesh Point გარსის ბადის წერტილის გადაადგილება
Move Mesh Point ბადის წერტილის გადაადგილება
Move Mesh Points ბადის წერტილების გადაადგილება
Move Spine Point ხერხემლის წერტილის გადაადგილება
Reshape Spine Segment ხერხემლის სეგმენტის ფორმის შეცვლა
Reset with Mesh ბადით ჩამოყრა
Can't render this file because it is too large.
+1
View File
@@ -2077,6 +2077,7 @@ font-technology Variable 가변
Move Guide 안내선 이동
Move Locked and Hidden Artwork with Artboard 잠기거나 숨겨진 아트워크를 아트보드와 함께 이동
Move Mesh Point 메시 포인트 이동
Move Mesh Points 메시 포인트 이동
Move Plane 평면 이동
Move Rows 행 이동
Move Slices 슬라이스 이동
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3086,6 +3086,7 @@ theme Appearance Vormgeving
Make Envelope Omtrek maken
Move Envelope Mesh Point Maaspunt van omtrek verplaatsen
Move Mesh Point Maaspunt verplaatsen
Move Mesh Points Maaspunten verplaatsen
Move Spine Point Aspunt verplaatsen
Reshape Spine Segment Segment van de as opnieuw vormgeven
Reset with Mesh Opnieuw instellen met maas
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3194,6 +3194,7 @@ theme Appearance Wygląd
Make Envelope Utwórz kopertę
Move Envelope Mesh Point Przesuń punkt siatki koperty
Move Mesh Point Przesuń punkt siatki
Move Mesh Points Przesuń punkty siatki
Move Spine Point Przesuń punkt grzbietu
Reshape Spine Segment Zmień kształt segmentu grzbietu
Reset with Mesh Resetuj z siatką
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1597,6 +1597,7 @@
Move Guide Mover Guia
Move Locked and Hidden Artwork with Artboard Mover Arte Bloqueada e Oculta com a Mesa de Trabalho
Move Mesh Point Mover Ponto de Malha
Move Mesh Points Mover Pontos de Malha
Move Plane Mover Plano
Move Plane to Match Object Mover Plano para Corresponder ao Objeto
Move Slices Mover Fatias
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1938,6 +1938,7 @@ file-filter Templates Шаблоны
Move in Perspective Переместить в перспективе
Move Locked and Hidden Artwork with Artboard Перемещать заблокированный и скрытый рисунок вместе с монтажной областью
Move Mesh Point Переместить точку сетки
Move Mesh Points Переместить точки сетки
Move Plane Переместить плоскость
Move Plane to Match Object Переместить плоскость по объекту
Move Rows Переместить строки
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1860,6 +1860,7 @@ font-technology Variable Variabla
Move in Perspective Flytta i perspektiv
Move Locked and Hidden Artwork with Artboard Flytta låst och dold grafik med ritytan
Move Mesh Point Flytta nätpunkt
Move Mesh Points Flytta nätpunkter
Move Plane Flytta plan
Move Plane to Match Object Flytta plan så att det matchar objekt
Move Rows Flytta rader
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -1757,6 +1757,7 @@ font-technology Variable Değişken
Move in Perspective Perspektifte Hareket Et
Move Locked and Hidden Artwork with Artboard Çalışma Yüzeyi ile Kilitli ve Gizli Resimleri Taşıma
Move Mesh Point Mesh Noktasını Taşı
Move Mesh Points Mesh Noktalarını Taşı
Move Plane Düzlemi Taşı
Move Plane to Match Object Nesneyi Eşleştirecek Düzlemi Taşı
Move Rows Satırları Taşı
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -1934,6 +1934,7 @@ file-filter Templates Шаблони
Move in Perspective Перемістити у перспективі
Move Locked and Hidden Artwork with Artboard Переміщати заблокований та прихований малюнок разом із монтажною областю
Move Mesh Point Перемістити точку сітки
Move Mesh Points Перемістити точки сітки
Move Plane Перемістити площину
Move Plane to Match Object Перемістити площину об’єктом
Move Rows Перемістити рядки
Can't render this file because it contains an unexpected character in line 178 and column 5.
+1
View File
@@ -2227,6 +2227,7 @@ file-filter Templates Mẫu
Move Library Item to Group Di chuyển mục thư viện vào nhóm
Move Locked and Hidden Artwork with Artboard Di chuyển tác phẩm đã khóa và ẩn cùng bảng vẽ
Move Mesh Point Di chuyển điểm lưới
Move Mesh Points Di chuyển các điểm lưới
Move Panel Di chuyển bảng
Move Plane Di chuyển mặt phẳng
Move Plane to Match Object Di chuyển mặt phẳng khớp với đối tượng
Can't render this file because it contains an unexpected character in line 200 and column 5.
+1
View File
@@ -1811,6 +1811,7 @@ file-filter Templates 模板
Move in Perspective 在透视中移动
Move Locked and Hidden Artwork with Artboard 随画板移动锁定和隐藏图稿
Move Mesh Point 移动网格点
Move Mesh Points 移动网格点
Move Plane 移动平面
Move Plane to Match Object 移动平面以符合对象
Move Slices 移动切片
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2729,6 +2729,7 @@ file-filter Templates 範本
Move in Perspective 在透視中移動
Move Locked and Hidden Artwork with Artboard 移動工作區域時一併移動鎖定與隱藏的圖稿
Move Mesh Point 移動網格點
Move Mesh Points 移動網格點
Move Plane 移動平面
Move Plane to Match Object 移動平面以符合物件
Move Rows 移動列
Can't render this file because it contains an unexpected character in line 6 and column 47.
+24 -1
View File
@@ -1249,7 +1249,7 @@ the Transparency panel's values, with `null` where the objects differ. Making a
empty mask and enters mask editing: art drawn then becomes the mask, and the mask commands act on the masked object
until `transparency.stopEditingOpacityMask`. While a mask is edited, `object.exitIsolation` (Esc, the isolation bar's
back arrow, Exit Isolation Mode) leaves mask editing the same way, in one undo step. Saves and exports never include
the editing layer.
the editing layer. With gradient mesh points picked, `opacity` goes to those points (see Picking several mesh points).
```json
{"name":"run_command","arguments":{"command":"transparency.set","params":{"ids":[12],"opacity":40,"blend":"Multiply"}}}
@@ -5599,6 +5599,29 @@ Envelope Options.
{"name":"run_command","arguments":{"command":"object.mesh.movePoint","params":{"id":12,"index":4,"x":220,"y":140,"handle":0}}}
```
## Picking several mesh points
The Mesh tool and Direct Selection pick mesh points on gradient meshes and mesh envelopes. A click picks one point
(its handles show); Shift-click adds a point or takes a picked one away. Direct Selection's marquee picks the points
inside it (Shift toggles them) and selects their meshes; its click on a gradient mesh's line picks the line's two
ends, and a click inside a patch picks the four points around it. The picked points show filled. Dragging one of them
moves them all as one undo step (`object.mesh.movePoints`), a click on one of several picked points picks it alone,
and the arrow keys nudge them by the keyboard increment. The active tool lists them in its options
(`tool.setOption {}` returns them as `meshPoints: [{id, index}…]`). On gradient meshes a solid fill colour (`paint.setFill`, from the
Color panel, Swatches or the Eyedropper) recolours just the picked points, and when every target of
`transparency.set` is a gradient mesh with points picked, its `opacity` fades those points instead of the objects
(`transparency.info` then shows their opacity, `null` when it differs). Both are one undo step.
`object.mesh.movePoints {id, points: [index…], dx, dy}` (or `{items: [{id, points}…], dx, dy}` over several meshes)
moves points by (dx, dy), their handles with them; `object.mesh.setPointColor` takes `points` or `items` the same
way to colour or fade several points at once. An index out of range, an index that is not a whole number ≥ 0, or a
move past the coordinate range fails and changes nothing.
```json
{"name":"run_command","arguments":{"command":"object.mesh.movePoints","params":{"id":12,"points":[0,4],"dx":10,"dy":-5}}}
{"name":"run_command","arguments":{"command":"object.mesh.setPointColor","params":{"id":12,"points":[0,4],"color":"#3366ff","opacity":0.5}}}
```
## Shortcut sets from other apps
`shortcutSet.read {path? | data? | dataBase64?, name?}` reads a shortcut set another app saved as text (a `.kys` file
File diff suppressed because one or more lines are too long