mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
Pen: Auto Add/Delete reuses the Add/Delete Anchor Point tools' hit tests (review of #312)
The Pen's Auto Add/Delete had its own copy of the anchor and segment hit tests and of the path.insertAnchor / path.removeAnchor actions the Add and Delete Anchor Point tools build. draw2 now exposes anchor_in / segment_in (the hit tests over a given set of paths) and insert_anchor / remove_anchor, which both tools use. Adds the ROADMAP entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
133390ef7a
commit
7feeb73a81
@@ -12,7 +12,7 @@ use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{NodeId, NodeKind};
|
||||
use vectorcraft_geom::{Anchor, AnchorKind, BezPath, Point, SubPath};
|
||||
|
||||
use crate::{Tool, ToolContext};
|
||||
use crate::{Action, Tool, ToolContext};
|
||||
|
||||
pub use anchor::AnchorTool;
|
||||
pub use curvature::CurvatureTool;
|
||||
@@ -96,8 +96,13 @@ fn candidate_paths(cx: &ToolContext) -> Vec<NodeId> {
|
||||
|
||||
/// Nearest anchor within `tol`: (path, subpath, anchor).
|
||||
pub(crate) fn hit_anchor(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, usize, usize)> {
|
||||
anchor_in(cx, candidate_paths(cx), p, tol)
|
||||
}
|
||||
|
||||
/// Nearest anchor within `tol` on the first of `ids` that has one: (path, subpath, anchor).
|
||||
pub(crate) fn anchor_in(cx: &ToolContext, ids: impl IntoIterator<Item = NodeId>, p: Point, tol: f64) -> Option<(NodeId, usize, usize)> {
|
||||
let mut best: Option<((NodeId, usize, usize), f64)> = None;
|
||||
for id in candidate_paths(cx) {
|
||||
for id in ids {
|
||||
let Some(pd) = cx.doc.node(id).and_then(|n| n.path_data()) else { continue };
|
||||
for (si, ai, a) in pd.anchors() {
|
||||
let d = a.p.distance(p);
|
||||
@@ -114,7 +119,13 @@ pub(crate) fn hit_anchor(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId
|
||||
|
||||
/// Nearest point on a path segment within `tol`: (path, subpath, segment, t).
|
||||
pub(crate) fn hit_segment(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, usize, usize, f64)> {
|
||||
for id in candidate_paths(cx) {
|
||||
segment_in(cx, candidate_paths(cx), p, tol)
|
||||
}
|
||||
|
||||
/// Nearest point within `tol` on a segment of the first of `ids` that passes there: (path,
|
||||
/// subpath, segment, t).
|
||||
pub(crate) fn segment_in(cx: &ToolContext, ids: impl IntoIterator<Item = NodeId>, p: Point, tol: f64) -> Option<(NodeId, usize, usize, f64)> {
|
||||
for id in ids {
|
||||
let Some(pd) = cx.doc.node(id).and_then(|n| n.path_data()) else { continue };
|
||||
if let Some((si, seg, t, _, d)) = pd.nearest(p)
|
||||
&& d <= tol
|
||||
@@ -125,6 +136,16 @@ pub(crate) fn hit_segment(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeI
|
||||
None
|
||||
}
|
||||
|
||||
/// Add an anchor where [`hit_segment`] or [`segment_in`] found a segment.
|
||||
pub(crate) fn insert_anchor((id, si, seg, t): (NodeId, usize, usize, f64)) -> Action {
|
||||
Action::Exec("path.insertAnchor".into(), json!({"id": id.0, "subpath": si, "segment": seg, "t": t}))
|
||||
}
|
||||
|
||||
/// Delete the anchor [`hit_anchor`] or [`anchor_in`] found.
|
||||
pub(crate) fn remove_anchor((id, si, ai): (NodeId, usize, usize)) -> Action {
|
||||
Action::Exec("path.removeAnchor".into(), json!({"id": id.0, "subpath": si, "anchor": ai}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -10,7 +10,7 @@ use serde_json::json;
|
||||
use vectorcraft_doc::NodeId;
|
||||
use vectorcraft_geom::Point;
|
||||
|
||||
use super::{hit_anchor, hit_segment};
|
||||
use super::{hit_anchor, hit_segment, insert_anchor, remove_anchor};
|
||||
use crate::{Action, Cursor, Mods, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -38,17 +38,11 @@ impl AnchorTool {
|
||||
}
|
||||
|
||||
fn add(cx: &ToolContext, p: Point) -> Vec<Action> {
|
||||
match hit_segment(cx, p, cx.tol(4.0)) {
|
||||
Some((id, si, seg, t)) => vec![Action::Exec("path.insertAnchor".into(), json!({"id": id.0, "subpath": si, "segment": seg, "t": t}))],
|
||||
None => vec![],
|
||||
}
|
||||
hit_segment(cx, p, cx.tol(4.0)).map(insert_anchor).into_iter().collect()
|
||||
}
|
||||
|
||||
fn delete(cx: &ToolContext, p: Point) -> Vec<Action> {
|
||||
match hit_anchor(cx, p, cx.tol(4.0)) {
|
||||
Some((id, si, ai)) => vec![Action::Exec("path.removeAnchor".into(), json!({"id": id.0, "subpath": si, "anchor": ai}))],
|
||||
None => vec![],
|
||||
}
|
||||
hit_anchor(cx, p, cx.tol(4.0)).map(remove_anchor).into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-28
@@ -13,7 +13,7 @@
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{NodeId, NodeKind};
|
||||
use vectorcraft_geom::{BezPath, PathData, Point};
|
||||
use vectorcraft_geom::{BezPath, Point};
|
||||
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
@@ -253,32 +253,15 @@ fn auto_add_delete(cx: &ToolContext, p: Point, m: Mods, tol: f64) -> Option<Acti
|
||||
if !cx.auto_add_delete || m.shift {
|
||||
return None;
|
||||
}
|
||||
let paths: Vec<(NodeId, &PathData)> = cx
|
||||
.selection
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|id| cx.doc.is_editable(**id))
|
||||
.filter_map(|&id| match &cx.doc.node(id)?.kind {
|
||||
NodeKind::Path { path, guide: false, .. } => Some((id, path)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let mut best: Option<(NodeId, usize, usize, f64)> = None;
|
||||
for (id, path) in &paths {
|
||||
for (si, ai, a) in path.anchors() {
|
||||
let d = a.p.distance(p);
|
||||
if d <= tol && best.is_none_or(|b| d < b.3) {
|
||||
best = Some((*id, si, ai, d));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((id, si, ai, _)) = best {
|
||||
return Some(Action::Exec("path.removeAnchor".into(), json!({"id": id.0, "subpath": si, "anchor": ai})));
|
||||
}
|
||||
paths.iter().find_map(|(id, path)| {
|
||||
let (si, seg, t, _, d) = path.nearest(p)?;
|
||||
(d <= tol).then(|| Action::Exec("path.insertAnchor".into(), json!({"id": id.0, "subpath": si, "segment": seg, "t": t})))
|
||||
})
|
||||
let paths = || {
|
||||
cx.selection
|
||||
.objects
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&id| cx.doc.is_editable(id) && cx.doc.node(id).is_some_and(|n| matches!(n.kind, NodeKind::Path { guide: false, .. })))
|
||||
};
|
||||
let anchor = crate::draw2::anchor_in(cx, paths(), p, tol).map(crate::draw2::remove_anchor);
|
||||
anchor.or_else(|| crate::draw2::segment_in(cx, paths(), p, tol).map(crate::draw2::insert_anchor))
|
||||
}
|
||||
|
||||
/// A click on a selected blend's spine: delete the point under `p` when no key object sits on it
|
||||
@@ -486,7 +469,7 @@ mod tests {
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let pts = [Point::new(300.0, 400.0), Point::new(350.0, 300.0), Point::new(400.0, 400.0)];
|
||||
let path = PathData::single(vectorcraft_geom::SubPath::polyline(&pts, false));
|
||||
let path = vectorcraft_geom::PathData::single(vectorcraft_geom::SubPath::polyline(&pts, false));
|
||||
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, path, vectorcraft_doc::Appearance::default_art())).unwrap();
|
||||
let mut s = Selection::default();
|
||||
s.set([id]);
|
||||
|
||||
Reference in New Issue
Block a user