mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge pull request #4954 from dotCooCoo/fix/mesh-locate-edge
Mesh tool: a click on a mesh's edge from just outside adds the lines there
This commit is contained in:
+16
-1
@@ -237,13 +237,28 @@ mod tests {
|
||||
fn mesh_insert_and_remove_lines() {
|
||||
let mut m =
|
||||
GradientMesh::for_path(&shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), 1, 1, Color::BLACK, MeshAppearance::Flat, 0.0).unwrap();
|
||||
let i = m.add_lines_at(Point::new(30.0, 60.0)).unwrap();
|
||||
let i = m.add_lines_at(Point::new(30.0, 60.0), 0.0).unwrap();
|
||||
assert_eq!((m.rows, m.cols), (2, 2));
|
||||
assert!(m.points[i].p.distance(Point::new(30.0, 60.0)) < 0.5, "{:?}", m.points[i].p);
|
||||
assert!(m.remove_point_lines(i));
|
||||
assert_eq!((m.rows, m.cols), (1, 1));
|
||||
}
|
||||
|
||||
/// A point outside the mesh within the tolerance locates the nearest point of its edge.
|
||||
#[test]
|
||||
fn mesh_locate_within_reaches_the_edge() {
|
||||
let m = GradientMesh::for_path(&shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), 2, 2, Color::BLACK, MeshAppearance::Flat, 0.0).unwrap();
|
||||
let p = Point::new(102.0, 30.0);
|
||||
assert_eq!(m.locate(p), None);
|
||||
assert_eq!(m.locate_within(p, 1.0), None);
|
||||
let (r, c, u, v) = m.locate_within(p, 3.0).unwrap();
|
||||
assert_eq!((r, c), (0, 1));
|
||||
assert_eq!(u, 1.0, "on the right edge");
|
||||
let q = m.eval(r, c, u, v);
|
||||
assert!(q.distance(Point::new(100.0, 30.0)) < 1e-6, "{q:?}");
|
||||
assert_eq!(m.locate_within(Point::new(30.0, 60.0), 3.0), m.locate(Point::new(30.0, 60.0)));
|
||||
}
|
||||
|
||||
/// Corners all in CMYK mix in CMYK (the inks halfway), not in RGB.
|
||||
#[test]
|
||||
fn a_cmyk_mesh_mixes_its_inks() {
|
||||
|
||||
@@ -592,12 +592,21 @@ impl GradientMesh {
|
||||
|
||||
/// Which patch and local (u, v) contains `p` (sampled search; `None` outside the mesh).
|
||||
pub fn locate(&self, p: Point) -> Option<(usize, usize, f64, f64)> {
|
||||
self.locate_within(p, 0.0)
|
||||
}
|
||||
|
||||
/// As [`Self::locate`], and for a point outside the mesh within `tol` of its outline, the
|
||||
/// patch and local (u, v) of the nearest point of the mesh's edge.
|
||||
pub fn locate_within(&self, p: Point, tol: f64) -> Option<(usize, usize, f64, f64)> {
|
||||
if !self.is_valid() {
|
||||
return None;
|
||||
}
|
||||
let bp = self.outline().to_bezpath();
|
||||
if bp.winding(p) == 0 {
|
||||
return None;
|
||||
if tol.is_nan() || tol <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
return self.nearest_edge(p).filter(|e| e.0 <= tol * tol).map(|(_, r, c, u, v)| (r, c, u, v));
|
||||
}
|
||||
const N: usize = 16;
|
||||
let mut best = (f64::INFINITY, 0, 0, 0.0, 0.0);
|
||||
@@ -634,6 +643,30 @@ impl GradientMesh {
|
||||
Some((r, c, u, v))
|
||||
}
|
||||
|
||||
/// The point of the mesh's outer edge nearest `p`: its squared distance from `p`, and its
|
||||
/// boundary patch and local (u, v).
|
||||
fn nearest_edge(&self, p: Point) -> Option<(f64, usize, usize, f64, f64)> {
|
||||
use vectorcraft_geom::kurbo::ParamCurveNearest;
|
||||
let (rows, cols) = (self.rows as usize, self.cols as usize);
|
||||
let mut best: Option<(f64, usize, usize, f64, f64)> = None;
|
||||
let mut take = |e: CubicBez, at: (usize, usize, Option<f64>, Option<f64>)| {
|
||||
let n = e.nearest(p, 1e-9);
|
||||
if best.is_none_or(|b| n.distance_sq < b.0) {
|
||||
let (r, c, u, v) = at;
|
||||
best = Some((n.distance_sq, r, c, u.unwrap_or(n.t), v.unwrap_or(n.t)));
|
||||
}
|
||||
};
|
||||
for c in 0..cols {
|
||||
take(self.edge_u(0, c), (0, c, None, Some(0.0)));
|
||||
take(self.edge_u(rows, c), (rows - 1, c, None, Some(1.0)));
|
||||
}
|
||||
for r in 0..rows {
|
||||
take(self.edge_v(r, 0), (r, 0, Some(0.0), None));
|
||||
take(self.edge_v(r, cols), (r, cols - 1, Some(1.0), None));
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// Index of the mesh point within `tol` of `p`.
|
||||
pub fn point_near(&self, p: Point, tol: f64) -> Option<usize> {
|
||||
self.points
|
||||
@@ -721,9 +754,10 @@ impl GradientMesh {
|
||||
self.cols += 1;
|
||||
}
|
||||
|
||||
/// Mesh tool click: add a row and a column through `p` (inside the mesh). Returns the new point index.
|
||||
pub fn add_lines_at(&mut self, p: Point) -> Option<usize> {
|
||||
let (r, c, u, v) = self.locate(p)?;
|
||||
/// Mesh tool click: add a row and a column through `p` (inside the mesh, or outside within
|
||||
/// `tol` of its edge, through the nearest point of the edge). Returns the new point index.
|
||||
pub fn add_lines_at(&mut self, p: Point, tol: f64) -> Option<usize> {
|
||||
let (r, c, u, v) = self.locate_within(p, tol)?;
|
||||
let mut rr = r;
|
||||
let mut cc = c;
|
||||
let row_new = v > 0.02 && v < 0.98;
|
||||
|
||||
@@ -236,7 +236,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Create Gradient Mesh…",
|
||||
["Object"],
|
||||
None,
|
||||
"{ids?, rows?: n (4), cols?: n (4), appearance?: flat|center|edge, highlight?: % (100), at?: [x,y], columns?: as cols} convert filled paths into gradient meshes in their fill colour (a gradient fill: the colour it paints at each point), lightened per `appearance` (with `at`: 1×1 mesh plus lines through that point); an embedded image becomes a mesh over its rectangle (its transform kept) whose points take the average colour and opacity of the picture around them (a linked image is refused: embed it first) → {ids, index?}",
|
||||
"{ids?, rows?: n (4), cols?: n (4), appearance?: flat|center|edge, highlight?: % (100), at?: [x,y], tolerance?: pt (0), columns?: as cols} convert filled paths into gradient meshes in their fill colour (a gradient fill: the colour it paints at each point), lightened per `appearance` (with `at`: 1×1 mesh plus lines through that point, or through the nearest point of the edge when `at` is outside within `tolerance`); an embedded image becomes a mesh over its rectangle (its transform kept) whose points take the average colour and opacity of the picture around them (a linked image is refused: embed it first) → {ids, index?}",
|
||||
has_selection,
|
||||
mesh_create
|
||||
),
|
||||
@@ -272,7 +272,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add Mesh Line",
|
||||
[],
|
||||
None,
|
||||
"{id, x, y, color?, keepModel?: bool (as paint.setFill)} add a row and a column through (x, y) to a gradient mesh (new point in `color`) or a mesh envelope → {index}",
|
||||
"{id, x, y, color?, keepModel?: bool (as paint.setFill), tolerance?: pt (0)} add a row and a column through (x, y) to a gradient mesh (new point in `color`) or a mesh envelope; a point outside within `tolerance` of the edge adds them through the nearest point of the edge → {index}",
|
||||
has_doc,
|
||||
mesh_add_line
|
||||
),
|
||||
|
||||
@@ -16,8 +16,9 @@ pub(super) fn mesh_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(a) => MeshAppearance::parse(a).ok_or_else(|| bad(C, "appearance must be flat|center|edge"))?,
|
||||
None => MeshAppearance::Flat,
|
||||
};
|
||||
check_numbers(p, &["highlight"], C)?;
|
||||
check_numbers(p, &["highlight", "tolerance"], C)?;
|
||||
let highlight = f64_or(p, "highlight", 100.0).clamp(0.0, 100.0);
|
||||
let tol = f64_or(p, "tolerance", 0.0).max(0.0);
|
||||
let at = match p.get("at") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(finite_numbers(v).map(|[x, y]| Point::new(x, y)).ok_or_else(|| bad(C, "`at` must be [x, y]"))?),
|
||||
@@ -58,7 +59,7 @@ pub(super) fn mesh_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let Some(mut m) = m else { continue };
|
||||
if let Some(q) = at {
|
||||
index = m.add_lines_at(q);
|
||||
index = m.add_lines_at(q, tol);
|
||||
}
|
||||
let mut mn = Node::new(n.id, NodeKind::Mesh(m));
|
||||
mn.name = n.name.clone();
|
||||
@@ -273,9 +274,11 @@ 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)?);
|
||||
crate::cmd::paint::check_keep_model(p, C)?;
|
||||
check_numbers(p, &["tolerance"], C)?;
|
||||
let tol = f64_or(p, "tolerance", 0.0).max(0.0);
|
||||
let color = p.get("color").and_then(color_value).map(|c| crate::cmd::paint::new_color(s, p, c));
|
||||
let index = with_mesh(s, p, C, "Add Mesh Line", true, |m| {
|
||||
let i = m.add_lines_at(q).ok_or_else(|| bad(C, "point is outside the mesh"))?;
|
||||
let i = m.add_lines_at(q, tol).ok_or_else(|| bad(C, "point is outside the mesh"))?;
|
||||
if let Some(c) = color {
|
||||
m.points[i].color = c;
|
||||
}
|
||||
|
||||
@@ -927,6 +927,32 @@ fn mesh_add_line_and_delete_point() {
|
||||
assert!(s.execute("object.mesh.deletePoint", &json!({"id": a.0, "index": 0})).is_err(), "corner lines can't go");
|
||||
}
|
||||
|
||||
/// A Mesh tool click that hits a mesh's edge from just outside it adds a mesh line at the
|
||||
/// nearest point of the edge; `tolerance` gives `object.mesh.addLine` the same reach.
|
||||
#[test]
|
||||
fn mesh_add_line_on_the_edge_from_just_outside() {
|
||||
use crate::tooling::ViewInfo;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0, 100.0, 100.0);
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("object.mesh.create", &json!({"rows": 1, "cols": 1})).unwrap();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("mesh", v).unwrap();
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 99.0, 160.0), v).unwrap();
|
||||
}
|
||||
let m = mesh_of(&s, a);
|
||||
assert_eq!((m.rows, m.cols), (2, 1), "a row through the left edge");
|
||||
assert!(m.points.iter().any(|q| q.p.distance(Point::new(100.0, 160.0)) < 0.5), "{:?}", m.points.iter().map(|q| q.p).collect::<Vec<_>>());
|
||||
let r = s.execute("object.mesh.addLine", &json!({"id": a.0, "x": 140, "y": 202, "tolerance": 3})).unwrap();
|
||||
let m = mesh_of(&s, a);
|
||||
assert_eq!((m.rows, m.cols), (2, 2), "a column through the bottom edge");
|
||||
assert!(m.points[r["index"].as_u64().unwrap() as usize].p.distance(Point::new(140.0, 200.0)) < 0.5);
|
||||
assert!(s.execute("object.mesh.addLine", &json!({"id": a.0, "x": 140, "y": 202})).is_err(), "no tolerance: outside");
|
||||
assert!(s.execute("object.mesh.addLine", &json!({"id": a.0, "x": 140, "y": 210, "tolerance": 3})).is_err(), "past the tolerance");
|
||||
}
|
||||
|
||||
/// A mesh point `index`, and `rows`, `cols` or `columns` for Create Gradient Mesh, that is not a
|
||||
/// whole number is an error that changes nothing; 4.0 names point 4.
|
||||
#[test]
|
||||
|
||||
@@ -232,10 +232,12 @@ impl Tool for MeshTool {
|
||||
(Paint::Solid { color, .. }, false) => Some(crate::params::color_json(color)),
|
||||
_ => None,
|
||||
};
|
||||
// A click that hits an edge from just outside adds the lines at the edge.
|
||||
let tol = cx.hit_options().tol;
|
||||
for id in [leaf, top] {
|
||||
match cx.doc.node(id).map(|n| &n.kind) {
|
||||
Some(NodeKind::Mesh(_)) => {
|
||||
let mut params = json!({"id": id.0, "x": p.x, "y": p.y});
|
||||
let mut params = json!({"id": id.0, "x": p.x, "y": p.y, "tolerance": tol});
|
||||
if let Some(c) = &color {
|
||||
params["color"] = c.clone();
|
||||
params["keepModel"] = json!(true);
|
||||
@@ -243,10 +245,10 @@ impl Tool for MeshTool {
|
||||
return vec![Action::Exec("object.mesh.addLine".into(), params)];
|
||||
}
|
||||
Some(NodeKind::Envelope { .. }) if cx.doc.node(id).and_then(grid_of).is_some() => {
|
||||
return vec![Action::Exec("object.mesh.addLine".into(), json!({"id": id.0, "x": p.x, "y": p.y}))];
|
||||
return vec![Action::Exec("object.mesh.addLine".into(), json!({"id": id.0, "x": p.x, "y": p.y, "tolerance": tol}))];
|
||||
}
|
||||
Some(NodeKind::Path { guide: false, .. }) | Some(NodeKind::Compound { .. }) => {
|
||||
return vec![Action::Exec("object.mesh.create".into(), json!({"ids": [id.0], "at": [p.x, p.y]}))];
|
||||
return vec![Action::Exec("object.mesh.create".into(), json!({"ids": [id.0], "at": [p.x, p.y], "tolerance": tol}))];
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -490,6 +492,9 @@ mod tests {
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = create("mesh").unwrap();
|
||||
let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 160.0));
|
||||
assert_eq!(acts, vec![Action::Exec("object.mesh.create".into(), json!({"ids": [a.0], "at": [150.0, 160.0]}))]);
|
||||
assert_eq!(
|
||||
acts,
|
||||
vec![Action::Exec("object.mesh.create".into(), json!({"ids": [a.0], "at": [150.0, 160.0], "tolerance": c.hit_options().tol}))]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ mod tests {
|
||||
// Inside the envelope: a new row and column.
|
||||
assert_eq!(
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 160.0)),
|
||||
vec![Action::Exec("object.mesh.addLine".into(), json!({"id": e.0, "x": 150.0, "y": 160.0}))]
|
||||
vec![Action::Exec("object.mesh.addLine".into(), json!({"id": e.0, "x": 150.0, "y": 160.0, "tolerance": c.hit_options().tol}))]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user