mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 08:29:14 +00:00
Mesh: a point on a mesh's outline locates on its edge on every side
A point exactly on a gradient mesh's outline located on the top and left edges but not on the bottom and right ones, where the nonzero winding number is 0. object.mesh.addLine and Create Gradient Mesh at a point there reported the point outside the mesh, and the Eyedropper did not sample it, while the hit test selects the mesh there. locate_within now takes the nearest point of the edge for a point within a millionth of a point of the outline, with any tolerance, and returns nothing for a point that is not finite. A point just outside still needs a tolerance.
This commit is contained in:
@@ -1025,6 +1025,37 @@ mod tests {
|
||||
assert_eq!(m.locate_within(Point::new(30.0, 60.0), 3.0), m.locate(Point::new(30.0, 60.0)));
|
||||
}
|
||||
|
||||
/// A point on a mesh's outline locates on the edge it lies on, on all four sides alike, with no
|
||||
/// tolerance; a point just outside does not, and a tolerance reaches only as far as it says.
|
||||
/// A point that isn't finite locates nowhere.
|
||||
#[test]
|
||||
fn mesh_locate_takes_a_point_on_any_side_of_the_outline() {
|
||||
for (rows, cols) in [(1, 1), (2, 3)] {
|
||||
let m =
|
||||
GradientMesh::for_path(&shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), rows, cols, Color::BLACK, MeshAppearance::Flat, 0.0)
|
||||
.unwrap();
|
||||
let sides = [
|
||||
("top", Point::new(30.0, 0.0), Vec2::new(0.0, -0.01)),
|
||||
("bottom", Point::new(30.0, 100.0), Vec2::new(0.0, 0.01)),
|
||||
("left", Point::new(0.0, 70.0), Vec2::new(-0.01, 0.0)),
|
||||
("right", Point::new(100.0, 70.0), Vec2::new(0.01, 0.0)),
|
||||
];
|
||||
for (side, p, out) in sides {
|
||||
let (r, c, u, v) = m.locate(p).unwrap_or_else(|| panic!("{rows}×{cols} {side}: {p:?} is not located"));
|
||||
assert!(r < m.rows as usize && c < m.cols as usize && (0.0..=1.0).contains(&u) && (0.0..=1.0).contains(&v), "{side}");
|
||||
assert!(m.eval(r, c, u, v).distance(p) < 1e-3, "{rows}×{cols} {side}: {:?}", m.eval(r, c, u, v));
|
||||
let mut e = m.clone();
|
||||
assert!(e.add_lines_at(p, 0.0).is_some(), "{rows}×{cols} {side}: no line at {p:?}");
|
||||
assert_eq!(m.locate(p + out), None, "{rows}×{cols} {side}: just outside");
|
||||
assert_eq!(m.locate_within(p + out * 100.0, 0.5), None, "{rows}×{cols} {side}: past the tolerance");
|
||||
assert!(m.locate_within(p + out * 100.0, 1.5).is_some(), "{rows}×{cols} {side}: within the tolerance");
|
||||
}
|
||||
for p in [Point::new(f64::NAN, 0.0), Point::new(30.0, f64::INFINITY)] {
|
||||
assert_eq!(m.locate_within(p, f64::INFINITY), None, "{p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Corners all in CMYK mix in CMYK (the inks halfway), not in RGB.
|
||||
#[test]
|
||||
fn a_cmyk_mesh_mixes_its_inks() {
|
||||
|
||||
@@ -530,6 +530,10 @@ pub(super) const MAX_BORDER_LINES: usize = 256;
|
||||
/// The most rounds of new lines a mesh made from a path takes to keep its border on the outline.
|
||||
const MAX_BORDER_ROUNDS: usize = 64;
|
||||
|
||||
/// A point this close (in points) to a mesh's outline is on it, for [`GradientMesh::locate_within`]
|
||||
/// with any tolerance.
|
||||
const ON_OUTLINE: f64 = 1e-6;
|
||||
|
||||
/// An anchor whose directions in and out differ by more than this (radians) is a corner: a border
|
||||
/// edge that can't follow the outline takes a mesh line through its sharpest corner first.
|
||||
const SHARP_TURN: f64 = 0.01;
|
||||
@@ -1436,17 +1440,24 @@ impl GradientMesh {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// patch and local (u, v) of the nearest point of the mesh's edge. A point on the outline
|
||||
/// (within [`ON_OUTLINE`]) locates on its edge on every side, with any `tol`.
|
||||
pub fn locate_within(&self, p: Point, tol: f64) -> Option<(usize, usize, f64, f64)> {
|
||||
if !self.is_valid() {
|
||||
if !self.is_valid() || !p.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let bp = self.outline().to_bezpath();
|
||||
if bp.winding(p) == 0 {
|
||||
// A point on the outline takes the nearest point of the edge on every side: the nonzero
|
||||
// winding number counts a point on some edges as outside (a rectangle's bottom and right
|
||||
// edges) and on the others as inside.
|
||||
let edge = self.nearest_edge(p);
|
||||
if let Some((_, r, c, u, v)) = edge.filter(|e| e.0 <= ON_OUTLINE * ON_OUTLINE) {
|
||||
return Some((r, c, u, v));
|
||||
}
|
||||
if self.outline().to_bezpath().winding(p) == 0 {
|
||||
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));
|
||||
return edge.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);
|
||||
|
||||
@@ -290,7 +290,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Add Mesh Line",
|
||||
[],
|
||||
None,
|
||||
"{id, x, y, color? (#rrggbbaa and #rgba are refused), 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}",
|
||||
"{id, x, y, color? (#rrggbbaa and #rgba are refused), keepModel?: bool (as paint.setFill), tolerance?: pt (0)} add a row and a column through (x, y), inside or on the edge, 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
|
||||
),
|
||||
|
||||
@@ -1184,6 +1184,28 @@ fn mesh_add_line_on_the_edge_from_just_outside() {
|
||||
assert!(s.execute("object.mesh.addLine", &json!({"id": a.0, "x": 140, "y": 210, "tolerance": 3})).is_err(), "past the tolerance");
|
||||
}
|
||||
|
||||
/// `object.mesh.addLine` and Create Gradient Mesh `at` a point exactly on the outline add their
|
||||
/// lines there with no tolerance, on the bottom and right edges as on the top and left ones.
|
||||
#[test]
|
||||
fn mesh_add_line_exactly_on_the_edge_needs_no_tolerance() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
sel(&mut s, &[a]);
|
||||
let r = s.execute("object.mesh.create", &json!({"at": [50, 100]})).unwrap();
|
||||
let m = mesh_of(&s, a);
|
||||
assert_eq!((m.rows, m.cols), (1, 2), "a column through the bottom edge");
|
||||
assert!(m.points[r["index"].as_u64().unwrap() as usize].p.distance(Point::new(50.0, 100.0)) < 0.5);
|
||||
for (x, y) in [(100.0, 30.0), (20.0, 0.0), (0.0, 80.0)] {
|
||||
let r = s.execute("object.mesh.addLine", &json!({"id": a.0, "x": x, "y": y})).unwrap();
|
||||
let p = mesh_of(&s, a).points[r["index"].as_u64().unwrap() as usize].p;
|
||||
assert!(p.distance(Point::new(x, y)) < 0.5, "({x}, {y}): {p:?}");
|
||||
}
|
||||
assert_eq!((mesh_of(&s, a).rows, mesh_of(&s, a).cols), (3, 3));
|
||||
let n = undo_len(&s);
|
||||
assert!(s.execute("object.mesh.addLine", &json!({"id": a.0, "x": 60, "y": 100.01})).is_err(), "just outside");
|
||||
assert_eq!(undo_len(&s), n, "nothing changed");
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
+2
-1
@@ -9626,7 +9626,8 @@ right/left/down/up; none stored means the smooth mesh through the points, as old
|
||||
`object.envelope.setMeshPoint {id, index, x, y, handle?}` and `object.mesh.movePoint`, `object.mesh.addLine` and
|
||||
`object.mesh.deletePoint` edit them like a gradient mesh's (one undo step each). On the canvas a selected envelope
|
||||
shows its mesh; the Mesh tool and Direct Selection drag its points and the clicked point's handles, and the Mesh tool
|
||||
adds a row and a column where it clicks inside. `press_key` Escape during a drag of a point or handle of a mesh
|
||||
adds a row and a column where it clicks inside. `object.mesh.addLine`, and `object.mesh.create` with `at`, take a point exactly on
|
||||
the mesh's outline (within a millionth of a point) on any side without `tolerance`; a point outside needs `tolerance`. `press_key` Escape during a drag of a point or handle of a mesh
|
||||
envelope or a gradient mesh cancels it: the point goes back and no undo step is added. The Control bar shows Edit Envelope / Edit Contents, the warp's
|
||||
style, orientation, bend and distortions (or the mesh's rows and columns), Reset (an unbent warp, a flat mesh) and
|
||||
Envelope Options.
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user