//! Pointer gestures in vector edit mode, one per edit tool. Each gesture is one undo step: it //! starts an interaction and previews its edit against the snapshot until the pointer goes up. //! Positions are read in the snapshot's coordinates (the preview re-fits the vector, which moves //! its origin). Dragged points and handles, and pen clicks, snap (see `point_snap.rs`). use std::sync::Arc; use figcraft_doc::*; use figcraft_geom::{Affine, Point, Rect, Vec2}; use super::point_snap; use super::regions::region_hit; use super::transform::{self, BoxHit, PointBox}; use super::{EditPart, VHit, VTool, VectorEdit, edit, editing, hit, hit_any, local_offset, network, ops, points_of, selected_points, set_state}; use crate::snap::Targets; use crate::tools::{Cursor, Gesture, Handle, PenCursor, PointerEvent, PointerKind}; use crate::{Result, Session}; /// An in-progress edit-mode gesture (cheap to clone: snap targets are shared). #[derive(Clone, Debug)] pub(crate) enum VGesture { /// Drag points (also a segment's ends) of the active layer and `others`' points with them. /// `split` re-adds a point on a segment first; a click without a drag clears the point /// selection when `click_clears` (inside the transform box). Points { start: Point, verts: Vec, others: Arc)>>, split: Option<(usize, f64)>, moved: bool, click_clears: bool, snap: Arc, }, /// Drag a handle (`from`: where it was, relative to its point); the other selected handles /// (`group`, with where they were) move by as much. Handle { seg: usize, start: bool, independent: bool, from: (f64, f64), group: Arc>, targets: Arc, }, /// The transform box: a corner or edge scales the points in it. `pick`: a Shift-press on the /// point at that corner, which a click (no drag) takes out of the selection instead. BoxScale { b: PointBox, handle: Handle, targets: Arc, pick: Option, }, /// The transform box: outside a corner rotates the points about its centre (`angle` so far, /// degrees clockwise on screen). BoxRotate { b: PointBox, start_angle: f64, angle: f64, }, BendSegment { seg: usize, t: f64, }, /// Bend tool on a point: drag pulls out handles, a click on a smooth point sharpens it. BendVertex { v: usize, origin: Point, moved: bool, smooth: bool, }, /// Marquee and lasso: `base` is each edited layer's selection to add to. Marquee { start: Point, base: Vec, }, Lasso { points: Vec, base: Vec, }, Pen { plan: PenPlan, origin: Point, out: Option<(f64, f64)>, }, /// Cut tool: where the press was (page) and what it was on. Cut { start: Point, hit: Option, }, /// Paint bucket: the points dragged over so far (local). Paint { stamps: Vec, }, /// Eraser strokes so far (local points) and the brush radius (local units). Erase { stamps: Vec, radius: f64, }, /// Variable width tool. Width(super::width::WidthGesture), } /// A handle (segment, the one at its `a` end) and where it was, relative to its point. type HandleFrom = ((usize, bool), (f64, f64)); /// Snapping for dragged points: their page positions at the start, the one under the pointer (kept /// on whole pixels) and the targets. #[derive(Clone, Debug)] pub(crate) struct PointSnap { from: Vec, grab: Option, targets: Targets, } /// Eraser brush radius in screen pixels. const ERASER_PX: f64 = 8.0; fn erase_all(net: &mut VectorNetwork, stamps: &[Point], radius: f64) -> bool { stamps.iter().fold(false, |acc, c| ops::erase(net, *c, radius) | acc) } /// What a pen click does (replayed against the snapshot while the handle is dragged). #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) enum PenPlan { /// A new point at `at` (local), connected from the pen's open end. New { at: Point, from: Option }, /// Connect the open end to an existing point (ends the path). Connect { from: usize, to: usize }, /// Add a point on a segment, connected from the open end if any (ends the path). Split { seg: usize, t: f64, from: Option }, } /// Apply a pen plan; `out` is the open end's outgoing handle, `drag` the handle being dragged out /// of the new point. Returns the point the click landed on. fn apply_pen(net: &mut VectorNetwork, plan: PenPlan, out: Option<(f64, f64)>, drag: Option) -> Option { let out = out.unwrap_or((0.0, 0.0)); let back = drag.map(|d| (-d.x, -d.y)).unwrap_or((0.0, 0.0)); match plan { PenPlan::New { at, from } => { net.vertices.push(Vertex::new(at.x, at.y)); let v = net.vertices.len() - 1; if let Some(f) = from { ops::connect(net, f, v, out, back); } if drag.is_some() && let Some(x) = net.vertices.get_mut(v) { x.mirror = Some(HandleMirroring::AngleAndLength); } Some(v) } PenPlan::Connect { from, to } => { let others = net.incident(to); let seg = ops::connect(net, from, to, out, back)?; // Dragging while closing shapes the curve on both sides of the point. if let (Some(d), [(os, ost)]) = (drag, others.as_slice()) { net.set_handle(*os, *ost, (d.x, d.y)); if let Some(x) = net.vertices.get_mut(to) { x.mirror = Some(HandleMirroring::AngleAndLength); } } let _ = seg; Some(to) } PenPlan::Split { seg, t, from } => { let v = ops::split_segment(net, seg, t)?; if let Some(f) = from { ops::connect(net, f, v, out, (0.0, 0.0)); } Some(v) } } } impl Session { /// The transform box to draw around the selected points (page corners tl, tr, br, bl): turned /// with them while a rotation is dragged. pub fn vector_edit_box(&self) -> Option<[Point; 4]> { let ds = self.ds().ok()?; if let Gesture::Vector(VGesture::BoxRotate { b, angle, .. }) = &self.tool.gesture { let m = figcraft_geom::rotate_about(Affine::IDENTITY, b.center(), -angle); return Some(b.corners().map(|c| m * c)); } transform::shown_box(&ds.doc, ds.vector_edit.as_ref()?).map(|b| b.corners()) } /// Feed a pointer event to vector edit mode (the canvas is in edit mode and the main tool is /// Move). Returns false when edit mode ended and the event should go to the normal tool. pub(crate) fn vector_pointer(&mut self, ev: &PointerEvent) -> Result { let Ok(ve) = editing(self) else { return Ok(false) }; match ev.kind { PointerKind::Hover => self.vector_hover(&ve, ev).map(|()| true), PointerKind::Down => self.vector_down(&ve, ev), PointerKind::Move => self.vector_drag(&ve, ev).map(|()| true), PointerKind::Up => self.vector_up(&ve, ev).map(|()| true), PointerKind::DoubleClick => { let tol = 6.0 / ev.zoom.max(1e-6); let doc = self.doc()?.clone(); if ve.anchor.is_some() { super::escape(self)?; } else if hit_any(&doc, &ve, ev.p(), tol).is_none() && ve.tool == VTool::Move { super::end(self)?; } Ok(true) } } } fn vgesture(&mut self, g: VGesture) { self.tool.gesture = Gesture::Vector(g); } /// The document the gesture started from (its preview is applied to it); the current one /// outside gestures. fn gesture_doc(&self) -> Result> { let ds = self.ds()?; Ok(ds.interaction.as_ref().map_or_else(|| ds.doc.clone(), |it| it.doc.clone())) } /// A gesture dragging points `verts` of vector `id` from `start`, and points of other edited /// layers with them (positions and snap targets taken from the document as it is now). fn points_gesture( &self, id: NodeId, start: Point, verts: Vec, others: Vec<(NodeId, Vec)>, split: Option<(usize, f64)>, click_clears: bool, ) -> Result { let ds = self.ds()?; let page_points = |id: NodeId, verts: &[usize]| -> Vec { let w = ds.doc.world_transform(id); network(&ds.doc, id).map(|n| verts.iter().filter_map(|v| n.vertices.get(*v)).map(|v| w * v.point()).collect()).unwrap_or_default() }; let mut from = page_points(id, &verts); for (o, v) in &others { from.extend(page_points(*o, v)); } let grab = from.iter().copied().min_by(|a, b| (*a - start).hypot().total_cmp(&(*b - start).hypot())); let targets = point_snap::targets_moving(&ds.doc, ds.page, Some(id), &verts, &[], &others); Ok(VGesture::Points { start, verts, others: Arc::new(others), split, moved: false, click_clears, snap: Arc::new(PointSnap { from, grab, targets }), }) } /// A gesture dragging handle `start` of segment `seg` (snapping clear of its own point) and the /// handles in `group` with it. fn handle_gesture(&self, id: NodeId, seg: usize, start: bool, independent: bool, group: &[(usize, bool)]) -> Result { let ds = self.ds()?; let net = network(&ds.doc, id); let own: Vec = net.and_then(|n| n.segments.get(seg)).map(|s| vec![if start { s.a } else { s.b }]).unwrap_or_default(); let targets = Arc::new(point_snap::targets(&ds.doc, ds.page, Some(id), &own, &[])); let from = net.and_then(|n| n.handle(seg, start)).unwrap_or_default(); let group: Vec = group.iter().filter(|h| **h != (seg, start)).filter_map(|h| Some((*h, net?.handle(h.0, h.1)?))).collect(); Ok(VGesture::Handle { seg, start, independent, from, group: Arc::new(group), targets }) } /// Every edited layer's selected points (segment ends included) other than the active one's. fn others_moving(doc: &Document, ve: &VectorEdit) -> Vec<(NodeId, Vec)> { ve.others .iter() .filter_map(|o| { let v = points_of(network(doc, o.id)?, &o.vertices, &o.segments); (!v.is_empty()).then_some((o.id, v)) }) .collect() } /// Each edited layer's selection to add to with Shift (empty selections otherwise). fn select_base(ve: &VectorEdit, shift: bool) -> Vec { ve.parts().into_iter().map(|p| EditPart { vertices: if shift { p.vertices } else { vec![] }, segments: vec![], id: p.id }).collect() } /// Select `base` plus the points `found` in each layer (marquee and lasso). fn select_parts(&mut self, base: &[EditPart], found: impl Fn(&Document, NodeId) -> Vec, clear_segments: bool) -> Result<()> { let doc = self.doc()?.clone(); set_state(self, |e| { for part in base { let mut v = part.vertices.clone(); for i in found(&doc, part.id) { if !v.contains(&i) { v.push(i); } } if part.id == e.id { e.vertices = v; if clear_segments { e.segments.clear(); } } else if let Some(o) = e.others.iter_mut().find(|o| o.id == part.id) { o.vertices = v; if clear_segments { o.segments.clear(); } } } }) } /// Start a transform box gesture. fn box_down(&mut self, ve: &VectorEdit, bh: BoxHit, ev: &PointerEvent, pick: Option) -> Result<()> { let ds = self.ds()?; let (doc, page) = (ds.doc.clone(), ds.page); let Some(b) = transform::shown_box(&doc, ve) else { return Ok(()) }; match bh { BoxHit::Inside => { let g = self.points_gesture(ve.id, ev.p(), b.points, vec![], None, true)?; self.begin_interaction("Move points")?; self.vgesture(g); } BoxHit::Handle(Handle::Rotate(_)) => { let start_angle = (ev.p() - b.center()).atan2(); self.begin_interaction("Rotate points")?; self.vgesture(VGesture::BoxRotate { b, start_angle, angle: 0.0 }); } BoxHit::Handle(handle) => { let targets = Arc::new(point_snap::targets(&doc, page, Some(ve.id), &b.points, &[])); self.begin_interaction("Scale points")?; self.vgesture(VGesture::BoxScale { b, handle, targets, pick }); } } Ok(()) } fn vector_hover(&mut self, ve: &VectorEdit, ev: &PointerEvent) -> Result<()> { let doc = self.doc()?.clone(); let tol = 6.0 / ev.zoom.max(1e-6); let width_tool = ve.tool == VTool::Width; let h = if width_tool { None } else { hit(&doc, ve, ev.p(), tol) }; // Other edited layers' parts light up too (a press there makes that layer active). let shown = if width_tool { None } else { h.map(|h| (ve.id, h)).or_else(|| hit_any(&doc, ve, ev.p(), tol)) }; // A pen click on empty canvas lands where it snaps. let pen_at = if ve.tool == VTool::Pen && h.is_none() && !ev.shift { Some(self.snap_pen(ev, Some(ve.id), &[])?) } else { None }; let ov = &mut self.tool.overlay; ov.vector_hover = shown.map(|x| x.1); ov.vector_hover_on = shown.map(|x| x.0); ov.width_hover = None; ov.face.clear(); ov.rubber = None; ov.guides.clear(); ov.brush = (ve.tool == VTool::Eraser).then(|| [ev.x, ev.y, ERASER_PX / ev.zoom.max(1e-6)]); // What a press here would do (see `vector_down`). ov.cursor = match (ve.tool, h) { (VTool::Bend, _) => Cursor::Pen(PenCursor::Bend), (VTool::Move, Some(_)) if ev.cmd => Cursor::Pen(PenCursor::Bend), (VTool::Move, _) if ev.cmd => Cursor::Crosshair, (VTool::Move, Some(VHit::Segment { .. })) if ev.alt => Cursor::Pen(PenCursor::Add), (VTool::Move, _) => Cursor::Default, (VTool::Pen, Some(VHit::Vertex { .. })) => Cursor::Pen(PenCursor::Close), (VTool::Pen, Some(VHit::Segment { .. })) => Cursor::Pen(PenCursor::Add), (VTool::Pen, _) => Cursor::Pen(PenCursor::New), _ => Cursor::Crosshair, }; let w = doc.world_transform(ve.id); // The Move tool hatches the region under the pointer, clear of anything a press would take // first (another layer's parts, Bezier handles, the transform box's handles). Close to a // point or segment but outside the region clearances, the region wins (a press takes it). let mut region = None; match ve.tool { VTool::Move if !ev.cmd => { // Over the transform box's handles: their cursor, and no point highlighted. if let Some(BoxHit::Handle(handle)) = transform::box_hit(&doc, ve, h, ev.p(), ev.zoom) && let Some(b) = transform::shown_box(&doc, ve) { self.tool.overlay.cursor = crate::tools::handle_cursor(&b.corners(), handle); self.tool.overlay.vector_hover = None; } else if shown.is_none_or(|(on, h)| on == ve.id && !matches!(h, VHit::Handle { .. })) { region = Self::region_under(&doc, ve, h, ev); if region.is_some() { self.tool.overlay.vector_hover = None; self.tool.overlay.vector_hover_on = None; } } } VTool::Pen => { let (to, guides) = pen_at.unwrap_or((ev.p(), Vec::new())); if let Some(a) = ve.anchor.and_then(|a| network(&doc, ve.id)?.vertices.get(a).copied()) { let ap = w * a.point(); self.tool.overlay.rubber = Some([ap.x, ap.y, to.x, to.y]); } self.tool.overlay.guides = guides; } VTool::Paint => { // Highlight the closed area the bucket would fill (skipped on huge networks). if let Some(net) = network(&doc, ve.id).filter(|n| n.segments.len() <= 3000) && let Some(face) = net.face_at(w.inverse() * ev.p()) { let mut polys: Vec> = Vec::new(); let mut path = face.path.clone(); path.apply_affine(w); figcraft_geom::kurbo::flatten(path.iter(), 0.25 / ev.zoom.max(1e-6), |el| { use figcraft_geom::kurbo::PathEl; match el { PathEl::MoveTo(p) => polys.push(vec![[p.x, p.y]]), PathEl::LineTo(p) => { if let Some(l) = polys.last_mut() { l.push([p.x, p.y]); } } _ => {} } }); self.tool.overlay.face = polys; } } VTool::Width => self.width_hover(ve, ev)?, _ => {} } let topo = region.and_then(|_| network(&doc, ve.id)).map(VectorNetwork::topology_key); if let Ok(ds) = self.ds_mut() { if let Some(e) = ds.vector_edit.as_mut() && e.region_hover != region { e.region_hover = region; if let Some(k) = topo { e.region_topo = k; } } ds.revision += 1; } Ok(()) } /// The region of the active layer a press at `ev` would take (Move tool, `h` the network's part /// there). Inside the transform box around points selected in their own right, the box keeps /// its drag unless the region is already selected. fn region_under(doc: &Document, ve: &VectorEdit, h: Option, ev: &PointerEvent) -> Option { let r = region_hit(doc, ve, ev.p(), ev.zoom)?; let own = !ve.shown_vertices().is_empty() || !ve.segments.is_empty(); let in_box = !ev.shift && own && transform::box_hit(doc, ve, h, ev.p(), ev.zoom) == Some(BoxHit::Inside); (!in_box || ve.regions.contains(&r)).then_some(r) } /// A press inside region `r` of the active layer (Move tool): select it with its points (Shift /// adds it, or takes it out when it is selected) and drag them. A press on a selected region /// keeps the whole selection and drags it. fn region_down(&mut self, ve: &VectorEdit, doc: &Document, r: usize, ev: &PointerEvent) -> Result<()> { let id = ve.id; let Some(net) = network(doc, id) else { return Ok(()) }; let selected = ve.regions.contains(&r); let shift = ev.shift; if shift && selected { // Its points go too, unless they were selected on their own or another selected // region has them. let kept: Vec = ve.regions.iter().filter(|q| **q != r).flat_map(|q| net.region_points(*q)).collect(); let gone: Vec = net.region_points(r).into_iter().filter(|v| !kept.contains(v) && ve.region_vertices.contains(v)).collect(); return set_state(self, |e| { e.regions.retain(|q| *q != r); e.vertices.retain(|v| !gone.contains(v)); e.region_vertices.retain(|v| !gone.contains(v)); }); } if !selected { let pts = net.region_points(r); let topo = net.topology_key(); set_state(self, |e| { if !shift { e.clear_selection(); } e.handles.clear(); e.regions.push(r); for v in pts { if !e.vertices.contains(&v) { e.vertices.push(v); e.region_vertices.push(v); } } e.region_topo = topo; })?; } let now = editing(self)?; let moving = selected_points(net, &now); let others = if shift || selected { Self::others_moving(doc, &now) } else { vec![] }; let g = self.points_gesture(id, ev.p(), moving, others, None, false)?; self.begin_interaction("Move points")?; self.vgesture(g); Ok(()) } /// Returns false when the click left edit mode (the caller then runs the normal tool). fn vector_down(&mut self, ve: &VectorEdit, ev: &PointerEvent) -> Result { let doc = self.doc()?.clone(); let tol = 6.0 / ev.zoom.max(1e-6); // A press on another edited layer's point, handle or segment (or, with the paint bucket, // area) makes it the active layer; a pen drawing a path keeps to its own layer. let other = match hit_any(&doc, ve, ev.p(), tol) { Some((id, _)) => Some(id), None if ve.tool == VTool::Paint => { let inside = |id: NodeId| network(&doc, id).is_some_and(|n| n.face_at(doc.world_transform(id).inverse() * ev.p()).is_some()); (!inside(ve.id)).then(|| ve.others.iter().map(|o| o.id).find(|id| inside(*id))).flatten() } None => None, }; let switched; let ve = match other { Some(id) if id != ve.id && !(ve.tool == VTool::Pen && ve.anchor.is_some()) => { set_state(self, |e| { e.activate(id); })?; switched = editing(self)?; &switched } _ => ve, }; let h = hit(&doc, ve, ev.p(), tol); self.tool.overlay.vector_hover = None; self.tool.overlay.face.clear(); self.tool.overlay.guides.clear(); let tool = if ev.cmd && ve.tool == VTool::Move { VTool::Bend } else { ve.tool }; let id = ve.id; let w = doc.world_transform(id); // Regions come after Bezier handles (and the transform box's handles), before points and // segments: clear of those, a press inside one selects it. let region = if tool == VTool::Move && !matches!(h, Some(VHit::Handle { .. })) { Self::region_under(&doc, ve, h, ev) } else { None }; // Any press without Shift drops the selected regions, except one on a selected region // (which keeps the selection to drag it); no hover hatch while the button is down. let keep_regions = ev.shift || region.is_some_and(|r| ve.regions.contains(&r)); set_state(self, |e| { e.region_hover = None; if !keep_regions { e.drop_regions(); } })?; // The rest of the press sees the selection without them. let dropped; let ve = if keep_regions || ve.regions.is_empty() { ve } else { dropped = editing(self)?; &dropped }; // The transform box (Shift inside it adds to the selection instead; a Shift-click on the // point at one of its corners takes that point out, a Shift-drag scales in proportion). let pick = match h { Some(VHit::Vertex { index }) if ev.shift => Some(index), _ => None, }; if tool == VTool::Move && let Some(bh) = transform::box_hit(&doc, ve, h, ev.p(), ev.zoom).filter(|bh| !((ev.shift || region.is_some()) && *bh == BoxHit::Inside)) { self.box_down(ve, bh, ev, pick)?; return Ok(true); } if tool == VTool::Move && let Some(r) = region { self.region_down(ve, &doc, r, ev)?; return Ok(true); } match tool { VTool::Move => match h { Some(VHit::Handle { seg, start }) => { // Shift-click adds or takes out a handle; a press on a selected one drags them all. let key = (seg, start); let handles = if ev.shift { let mut hs = ve.handles.clone(); if let Some(i) = hs.iter().position(|x| *x == key) { hs.remove(i); } else { hs.push(key); } hs } else if ve.handles.contains(&key) { ve.handles.clone() } else { vec![key] }; let grabbed = handles.contains(&key); let g = self.handle_gesture(id, seg, start, ev.alt, &handles)?; set_state(self, |e| e.handles = handles)?; if grabbed { self.begin_interaction("Edit handle")?; self.vgesture(g); } } Some(VHit::Vertex { index }) => { // Shift on a point drawn unselected (selected only through a region) selects it // in its own right rather than taking it out. let own = ev.shift && ve.region_vertices.contains(&index); let verts = if own { ve.vertices.clone() } else if ev.shift { let mut v = ve.vertices.clone(); if let Some(i) = v.iter().position(|x| *x == index) { v.remove(i); } else { v.push(index); } v } else if ve.vertices.contains(&index) { ve.vertices.clone() } else { vec![index] }; let keep_segs = ev.shift; // Shift, or a press on a selected point, keeps (and drags) other layers' points. let keep_others = ev.shift || ve.vertices.contains(&index); set_state(self, |e| { if !keep_others { e.clear_selection(); } e.vertices = verts.clone(); e.region_vertices.retain(|v| *v != index); e.handles.clear(); if !keep_segs { e.segments.clear(); } })?; let moving = network(&doc, id) .map(|n| { super::selected_points( n, &VectorEdit { vertices: verts, segments: if keep_segs { ve.segments.clone() } else { vec![] }, ..ve.clone() }, ) }) .unwrap_or_default(); let others = if keep_others { Self::others_moving(&doc, ve) } else { vec![] }; let g = self.points_gesture(id, ev.p(), moving, others, None, false)?; self.begin_interaction("Move points")?; self.vgesture(g); } Some(VHit::Segment { seg, t }) => { if ev.alt { // Add a point on the segment and drag it. self.begin_interaction("Add point")?; let mut v = None; self.preview(|doc, _| { v = edit(doc, id, |net| ops::split_segment(net, seg, t))?; Ok(()) })?; let Some(v) = v else { return Ok(true) }; set_state(self, |e| { e.clear_selection(); e.vertices = vec![v]; })?; let g = self.points_gesture(id, ev.p(), vec![v], vec![], Some((seg, t)), false)?; self.vgesture(g); } else if ev.shift { set_state(self, |e| { if let Some(i) = e.segments.iter().position(|x| *x == seg) { e.segments.remove(i); } else { e.segments.push(seg); } })?; } else { let ends = network(&doc, id).and_then(|n| n.segments.get(seg)).map(|s| vec![s.a, s.b]).unwrap_or_default(); set_state(self, |e| { e.clear_selection(); e.segments = vec![seg]; })?; let g = self.points_gesture(id, ev.p(), ends, vec![], None, false)?; self.begin_interaction("Move segment")?; self.vgesture(g); } } None => { // Outside every edited layer (and not shift): leave edit mode. let inside = ve.parts().iter().any(|p| doc.world_render_bounds(p.id).is_some_and(|b| b.inflate(tol * 4.0, tol * 4.0).contains(ev.p()))); if !inside && !ev.shift { super::end(self)?; return Ok(false); } if !ev.shift { set_state(self, VectorEdit::clear_selection)?; } self.vgesture(VGesture::Marquee { start: ev.p(), base: Self::select_base(ve, ev.shift) }); } }, VTool::Bend => match h { Some(VHit::Handle { seg, start }) => { let g = self.handle_gesture(id, seg, start, true, &[])?; self.begin_interaction("Edit handle")?; self.vgesture(g); } Some(VHit::Vertex { index }) => { let smooth = network(&doc, id).is_some_and(|n| n.incident(index).iter().any(|(s, st)| n.handle(*s, *st).is_some_and(|h| h != (0.0, 0.0)))); let origin = network(&doc, id).and_then(|n| n.vertices.get(index)).map(|v| v.point()).unwrap_or_default(); set_state(self, |e| { e.clear_selection(); e.vertices = vec![index]; })?; self.begin_interaction("Bend")?; self.vgesture(VGesture::BendVertex { v: index, origin, moved: false, smooth }); } Some(VHit::Segment { seg, t }) => { self.begin_interaction("Bend")?; self.vgesture(VGesture::BendSegment { seg, t }); } None => self.vgesture(VGesture::Marquee { start: ev.p(), base: Self::select_base(ve, ev.shift) }), }, VTool::Pen => { let at = if h.is_none() && !ev.shift { self.snap_pen(ev, Some(id), &[])?.0 } else { ev.p() }; let Some(plan) = self.pen_plan(ve, h, w.inverse() * at, ev.shift) else { return Ok(true) }; self.begin_interaction("Pen")?; let out = ve.pen_out; let mut v = None; self.preview(|doc, _| { v = edit(doc, id, |net| apply_pen(net, plan, out, None))?; Ok(()) })?; self.pen_state(plan, v, None)?; self.vgesture(VGesture::Pen { plan, origin: ev.p(), out }); } VTool::Paint => { // A drag paints every area it crosses the way the first one went (one undo step). self.begin_interaction("Paint bucket")?; let stamps = vec![w.inverse() * ev.p()]; let st = stamps.clone(); self.preview(|doc, _| super::paint_all(doc, id, &st).map(|_| ())).ok(); self.vgesture(VGesture::Paint { stamps }); } VTool::Eraser => { let scale = w.determinant().abs().sqrt().max(1e-9); let radius = ERASER_PX / ev.zoom.max(1e-6) / scale; let stamps = vec![w.inverse() * ev.p()]; self.begin_interaction("Eraser")?; let st = stamps.clone(); self.preview(|doc, _| edit(doc, id, |net| erase_all(net, &st, radius)).map(|_| ()))?; set_state(self, |e| { e.vertices.clear(); e.segments.clear(); })?; self.vgesture(VGesture::Erase { stamps, radius }); } VTool::Width => self.width_down(ve, ev)?, VTool::Lasso => { self.tool.overlay.lasso = vec![[ev.x, ev.y]]; self.vgesture(VGesture::Lasso { points: vec![ev.p()], base: Self::select_base(ve, ev.shift) }); } // A click cuts there when the pointer goes up; a drag cuts across (see `cut_up`). VTool::Cut => { self.tool.overlay.rubber = Some([ev.x, ev.y, ev.x, ev.y]); self.vgesture(VGesture::Cut { start: ev.p(), hit: h }); } } Ok(true) } /// What a pen click at local point `lp` does. fn pen_plan(&mut self, ve: &VectorEdit, h: Option, lp: Point, shift: bool) -> Option { let doc = self.doc().ok()?; let net = network(doc, ve.id)?; match h { Some(VHit::Vertex { index }) => match ve.anchor { Some(a) if a != index => Some(PenPlan::Connect { from: a, to: index }), Some(_) => { let _ = super::escape(self); None } None => { // Continue (or branch) from an existing point. let _ = set_state(self, |e| { e.anchor = Some(index); e.pen_out = None; e.vertices = vec![index]; e.segments.clear(); }); None } }, Some(VHit::Segment { seg, t }) => Some(PenPlan::Split { seg, t, from: ve.anchor }), _ => { let mut at = lp; // Shift: 45° steps from the open end. if shift && let Some(a) = ve.anchor.and_then(|a| net.vertices.get(a)) { let v = lp - a.point(); let step = std::f64::consts::FRAC_PI_4; let ang = (v.atan2() / step).round() * step; at = a.point() + Vec2::from_angle(ang) * v.hypot(); } Some(PenPlan::New { at, from: ve.anchor }) } } } /// Edit state after a pen click. fn pen_state(&mut self, plan: PenPlan, v: Option, out: Option<(f64, f64)>) -> Result<()> { set_state(self, |e| { e.segments.clear(); e.vertices = v.into_iter().collect(); match plan { PenPlan::New { .. } => { e.anchor = v; e.pen_out = out; } PenPlan::Connect { .. } | PenPlan::Split { .. } => { e.anchor = None; e.pen_out = None; } } }) } /// A pen click from a command (`drag` = the handle pulled out of the new point, page units). pub(crate) fn pen_click(&mut self, ve: &VectorEdit, p: Point, drag: Vec2, zoom: f64) -> Result> { let doc = self.doc()?.clone(); let tol = 6.0 / zoom.max(1e-6); let w = doc.world_transform(ve.id); let Some(plan) = self.pen_plan(ve, hit(&doc, ve, p, tol), w.inverse() * p, false) else { return Ok(self.ds()?.vector_edit.as_ref().and_then(|e| e.anchor)); }; let inv = w.inverse(); let ld = inv * (Point::ZERO + drag) - inv * Point::ZERO; let drag = (ld.hypot() > 1e-9 && ld.x.is_finite() && ld.y.is_finite()).then_some(ld); let v = edit(self.doc_mut()?, ve.id, |net| apply_pen(net, plan, ve.pen_out, drag))?; self.pen_state(plan, v, drag.map(|d| (d.x, d.y)))?; Ok(v) } fn vector_drag(&mut self, ve: &VectorEdit, ev: &PointerEvent) -> Result<()> { let Gesture::Vector(g) = self.tool.gesture.clone() else { return Ok(()) }; let id = ve.id; let w = self.gesture_doc()?.world_transform(id); let modes = point_snap::modes(self, ev); let inv = w.inverse(); let lp = inv * ev.p(); let to_local = |d: Vec2| inv * (Point::ZERO + d) - inv * Point::ZERO; match g { VGesture::Points { start, verts, others, split, click_clears, snap, .. } => { let mut d = ev.p() - start; if d.hypot() * ev.zoom < 1.0 { return Ok(()); } // Shift: along the axis moved most. let lock = ev.shift.then_some(d.x.abs() > d.y.abs()); match lock { Some(true) => d.y = 0.0, Some(false) => d.x = 0.0, None => {} } let moved: Vec = snap.from.iter().map(|p| *p + d).collect(); let (c, guides) = point_snap::snap(&snap.targets, &moved, snap.grab.map(|g| g + d), ev.zoom, modes, lock); d += c; self.tool.overlay.guides = guides; let ld = to_local(d); self.preview(|doc, _| { edit(doc, id, |net| { if let Some((seg, t)) = split { ops::split_segment(net, seg, t); } ops::move_vertices(net, &verts, ld); })?; for (o, v) in others.iter() { let od = local_offset(doc.world_transform(*o), d); edit(doc, *o, |net| ops::move_vertices(net, v, od))?; } Ok(()) })?; self.vgesture(VGesture::Points { start, verts, others, split, moved: true, click_clears, snap }); } VGesture::Handle { seg, start, independent, from, group, targets } => { let alt = independent || ev.alt; let (c, guides) = point_snap::snap(&targets, &[ev.p()], Some(ev.p()), ev.zoom, modes, None); self.tool.overlay.guides = guides; let lp = inv * (ev.p() + c); self.preview(|doc, _| { edit(doc, id, |net| { let Some(s) = net.segments.get(seg).copied() else { return }; let Some(v) = net.vertices.get(if start { s.a } else { s.b }).copied() else { return }; let rel = (lp.x - v.x, lp.y - v.y); ops::set_handle(net, seg, start, rel, alt); // The other selected handles move as far from where they were. let d = (rel.0 - from.0, rel.1 - from.1); for ((gs, gst), h) in group.iter() { ops::set_handle(net, *gs, *gst, (h.0 + d.0, h.1 + d.1), alt); } }) })?; } VGesture::BoxScale { b, handle, targets, .. } => { // The dragged corner or edge snaps like a resize handle. let (c, guides) = point_snap::snap(&targets, &[ev.p()], None, ev.zoom, (modes.0, false), None); self.tool.overlay.guides = guides; let q = b.frame.inverse() * (ev.p() + c); let (x0, y0, x1, y1) = crate::tools::resized_box(handle, q, b.width, b.height, ev.shift, ev.alt); let m = b.onto(x0, y0, x1, y1); self.preview(|doc, _| transform::transform_points(doc, id, &b.points, m))?; } VGesture::BoxRotate { b, start_angle, .. } => { let c = b.center(); let mut angle = ((ev.p() - c).atan2() - start_angle).to_degrees(); if ev.shift { angle = (angle / 15.0).round() * 15.0; } let m = figcraft_geom::rotate_about(Affine::IDENTITY, c, -angle); self.preview(|doc, _| transform::transform_points(doc, id, &b.points, m))?; self.tool.overlay.cursor = Cursor::Rotate((ev.p() - c).atan2().to_degrees()); self.vgesture(VGesture::BoxRotate { b, start_angle, angle }); } VGesture::BendSegment { seg, t } => { self.preview(|doc, _| edit(doc, id, |net| ops::bend(net, seg, t, lp)))?; } VGesture::BendVertex { v, origin, moved, smooth } => { let d = lp - origin; let page_d = (w * lp - w * origin).hypot() * ev.zoom; if !moved && page_d < 2.0 { return Ok(()); } self.preview(|doc, _| edit(doc, id, |net| ops::make_smooth(net, v, d)))?; self.vgesture(VGesture::BendVertex { v, origin, moved: true, smooth }); } VGesture::Marquee { start, base } => { let r = Rect::from_points(start, ev.p()); self.tool.overlay.marquee = Some([r.x0, r.y0, r.x1, r.y1]); self.select_parts(&base, |doc, id| super::vertices_in(doc, id, r), false)?; } VGesture::Lasso { mut points, base } => { if points.last().is_none_or(|l| (*l - ev.p()).hypot() * ev.zoom >= 2.0) && points.len() < 100_000 { points.push(ev.p()); } self.tool.overlay.lasso = points.iter().map(|p| [p.x, p.y]).collect(); self.select_parts(&base, |doc, id| super::vertices_in_polygon(doc, id, &points), true)?; self.vgesture(VGesture::Lasso { points, base }); } VGesture::Erase { mut stamps, radius } => { self.tool.overlay.brush = Some([ev.x, ev.y, ERASER_PX / ev.zoom.max(1e-6)]); // Stamps a third of the brush apart, filling gaps on fast strokes. let to = lp; let from = stamps.last().copied().unwrap_or(to); let step = (radius / 3.0).max(1e-6); let n = ((to - from).hypot() / step).floor().min(500.0) as usize; for k in 1..=n { stamps.push(from.lerp(to, k as f64 / n as f64)); } if n > 0 && stamps.len() < 20_000 { let st = stamps.clone(); self.preview(|doc, _| edit(doc, id, |net| erase_all(net, &st, radius)).map(|_| ()))?; } self.vgesture(VGesture::Erase { stamps, radius }); } VGesture::Cut { start, .. } => self.tool.overlay.rubber = Some([start.x, start.y, ev.x, ev.y]), VGesture::Paint { mut stamps } => { if stamps.last().is_none_or(|l| (w * *l - ev.p()).hypot() * ev.zoom >= 2.0) && stamps.len() < 10_000 { stamps.push(lp); let st = stamps.clone(); self.preview(|doc, _| super::paint_all(doc, id, &st).map(|_| ())).ok(); } self.vgesture(VGesture::Paint { stamps }); } VGesture::Pen { plan, origin, out, .. } => { let d = to_local(ev.p() - origin); if (ev.p() - origin).hypot() * ev.zoom < 2.0 { return Ok(()); } let mut v = None; self.preview(|doc, _| { v = edit(doc, id, |net| apply_pen(net, plan, out, Some(d)))?; Ok(()) })?; self.pen_state(plan, v, Some((d.x, d.y)))?; self.vgesture(VGesture::Pen { plan, origin, out }); } VGesture::Width(g) => self.width_drag(ve, g, ev)?, } Ok(()) } /// The Cut tool's pointer went up: a click on a point or a segment cuts the path there; a drag /// cuts every path it crosses, parts that come apart moving to layers of their own. fn cut_up(&mut self, ve: &VectorEdit, start: Point, hit: Option, ev: &PointerEvent) -> Result<()> { self.tool.overlay.rubber = None; let id = ve.id; self.begin_interaction("Cut")?; if (ev.p() - start).hypot() * ev.zoom >= 3.0 { return if super::cut_across(self, start, ev.p()).is_ok() { self.commit_interaction() } else { self.cancel_interaction() }; } let mut ends = vec![]; self.preview(|doc, _| { ends = match hit { Some(VHit::Vertex { index }) => edit(doc, id, |net| ops::cut_vertex(net, index))?, Some(VHit::Segment { seg, t }) => edit(doc, id, |net| ops::cut_segment(net, seg, t))?.map(|(a, b)| vec![a, b]).unwrap_or_default(), _ => vec![], }; Ok(()) })?; self.commit_interaction()?; set_state(self, |e| { e.vertices = ends; e.segments.clear(); }) } fn vector_up(&mut self, ve: &VectorEdit, ev: &PointerEvent) -> Result<()> { let g = std::mem::take(&mut self.tool.gesture); self.tool.overlay.marquee = None; self.tool.overlay.lasso.clear(); self.tool.overlay.guides.clear(); let Gesture::Vector(g) = g else { return Ok(()) }; match g { VGesture::Points { verts, others, moved, split, click_clears, .. } => { if moved || split.is_some() { if let ([v], None) = (verts.as_slice(), split) && others.iter().all(|(_, o)| o.is_empty()) { self.merge_dropped_point(ve.id, *v, 6.0 / ev.zoom.max(1e-6))?; } self.commit_interaction()?; } else { self.cancel_interaction()?; if click_clears { set_state(self, |e| { e.vertices.clear(); e.segments.clear(); })?; } } } VGesture::BendVertex { v, moved, smooth, .. } => { if moved { self.commit_interaction()?; } else if smooth { let id = ve.id; self.preview(|doc, _| edit(doc, id, |net| ops::make_sharp(net, v)))?; self.commit_interaction()?; } else { self.cancel_interaction()?; } } VGesture::Handle { .. } | VGesture::BendSegment { .. } | VGesture::Pen { .. } | VGesture::BoxRotate { .. } => { self.commit_interaction()? } // The active layer moves to one with points selected when it has none. VGesture::Marquee { .. } | VGesture::Lasso { .. } => set_state(self, |e| { if e.vertices.is_empty() && let Some(id) = e.others.iter().find(|o| !o.vertices.is_empty()).map(|o| o.id) { e.activate(id); } })?, VGesture::Erase { .. } | VGesture::Paint { .. } => self.commit_interaction()?, VGesture::BoxScale { pick, .. } => { let ds = self.ds()?; let clicked = ds.interaction.as_ref().is_some_and(|it| Arc::ptr_eq(&it.doc, &ds.doc)); match pick.filter(|_| clicked) { Some(v) => { self.cancel_interaction()?; set_state(self, |e| { if let Some(i) = e.vertices.iter().position(|x| *x == v) { e.vertices.remove(i); } else { e.vertices.push(v); } })?; } None => self.commit_interaction()?, } } VGesture::Cut { start, hit } => self.cut_up(ve, start, hit, ev)?, VGesture::Width(g) => self.width_up(g)?, } if let Ok(ds) = self.ds_mut() { ds.revision += 1; } Ok(()) } /// Point `v` of vector `id`, dragged and dropped within `tol` (page units) of another of its /// points, merges into that point (in the same undo step as the move), which stays selected. fn merge_dropped_point(&mut self, id: NodeId, v: usize, tol: f64) -> Result<()> { let moved = self.ds()?.doc.get(id)?.clone(); let w = self.ds()?.doc.world_transform(id); let NodeKind::Vector { network: net } = &moved.kind else { return Ok(()) }; let Some(p) = net.vertices.get(v).map(|x| w * x.point()) else { return Ok(()) }; let Some(keep) = net .vertices .iter() .enumerate() .filter(|(i, _)| *i != v) .map(|(i, x)| (i, (w * x.point() - p).hypot())) .filter(|(_, d)| *d <= tol) .min_by(|a, b| a.1.total_cmp(&b.1)) .map(|(i, _)| i) else { return Ok(()); }; let mut merged = net.clone(); ops::merge_vertices(&mut merged, keep, v); // A merge that would leave no segment (a 2-point line's ends dropped together) only moves. if merged.segments.is_empty() { return Ok(()); } // The preview starts again from the document before the drag: bring the moved layer. self.preview(|doc, _| { *doc.get_mut(id)? = moved; edit(doc, id, |net| *net = merged) })?; set_state(self, |e| { e.vertices = vec![if keep > v { keep - 1 } else { keep }]; e.segments.clear(); }) } }