mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -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)?);
|
||||
|
||||
Reference in New Issue
Block a user