D: Smooth and Erase tools (path.smooth, path.erase)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-02 20:39:23 -04:00
co-authored by Claude Opus 5.5
parent b10a06f7b8
commit 4e874efbb0
7 changed files with 333 additions and 4 deletions
+2
View File
@@ -262,6 +262,8 @@ pub fn create(id: &str) -> Box<dyn Tool> {
"pen" => Box::new(pen::PenTool::default()),
"gradientSwatch" => Box::new(gradient::GradientTool::default()),
"pencil" => Box::new(pencil::PencilTool::default()),
"smooth" => Box::new(pencil::PathDragTool::new(false)),
"erase" => Box::new(pencil::PathDragTool::new(true)),
"addAnchor" => Box::new(anchors::AnchorTool::new(anchors::Kind::Add)),
"deleteAnchor" => Box::new(anchors::AnchorTool::new(anchors::Kind::Delete)),
"convertDirection" => Box::new(anchors::AnchorTool::new(anchors::Kind::Convert)),
+69
View File
@@ -71,3 +71,72 @@ impl Tool for PencilTool {
self.stroke.is_some()
}
}
/// Smooth and Erase tools (Pencil group): drag along a selected path.
pub struct PathDragTool {
erase: bool,
/// Target path, canvas offset of its spread, and the drag in spread coordinates.
drag: Option<(u64, designcraft_geom::Vec2, Vec<Point>)>,
}
impl PathDragTool {
pub fn new(erase: bool) -> Self {
Self { erase, drag: None }
}
}
impl Tool for PathDragTool {
fn id(&self) -> &'static str {
if self.erase { "erase" } else { "smooth" }
}
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
match ev.kind {
PointerKind::Down => {
// The selected path, or the one under the pointer.
let id = cx.selection.items.first().copied().or_else(|| cx.hit(ev.pos).map(|h| h.1));
let Some(id) = id else { return vec![] };
let Some(loc) = cx.doc.find(id) else { return vec![] };
let off = cx.layout.offset(loc.spread);
self.drag = Some((id.0, off, vec![ev.pos - off]));
vec![]
}
PointerKind::Drag => {
if let Some((_, off, pts)) = &mut self.drag {
pts.push(ev.pos - *off);
}
vec![]
}
PointerKind::Up => {
let Some((id, off, mut pts)) = self.drag.take() else { return vec![] };
pts.push(ev.pos - off);
let points: Vec<_> = pts.iter().map(|p| json!([p.x, p.y])).collect();
let cmd = if self.erase { "path.erase" } else { "path.smooth" };
vec![Action::Exec(cmd.into(), json!({"id": id, "points": points, "tolerance": cx.tol(6.0)}))]
}
_ => vec![],
}
}
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
let Some((_, off, pts)) = &self.drag else { return vec![] };
let mut path = BezPath::new();
for (i, p) in pts.iter().enumerate() {
let p = *p + *off;
if i == 0 {
path.move_to(p);
} else {
path.line_to(p);
}
}
vec![Overlay::Path { path, color: if self.erase { [220, 60, 60] } else { [60, 120, 220] }, dashed: true }]
}
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
Cursor::Pen
}
fn busy(&self) -> bool {
self.drag.is_some()
}
}