mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 12:47:13 -06:00
Gradient Mesh: select several mesh points; move, color and opacity reach them all (#4207, #4208, #4170)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)?);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user