mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 04:16:46 -06:00
feat: 为矢量编辑添加线段中点添加点的交互和提示
This commit is contained in:
@@ -270,13 +270,14 @@ fn vector_edit_mode_shows_each_tool_and_modifier() {
|
||||
let mut s = Session::new();
|
||||
let path = id(&ex(&mut s, "vector.create", json!({"points": [[100, 100], [300, 100], [300, 300]], "closed": false})));
|
||||
ex(&mut s, "vector.edit.begin", json!({"id": path}));
|
||||
let (point, segment, empty) = ((300.0, 100.0), (200.0, 100.0), (200.0, 250.0));
|
||||
let (point, segment, midpoint, empty) = ((300.0, 100.0), (150.0, 100.0), (200.0, 100.0), (200.0, 250.0));
|
||||
use Cursor::*;
|
||||
// (vector tool, modifiers, where, cursor, why)
|
||||
type Row = (&'static str, Mods, (f64, f64), Cursor, &'static str);
|
||||
let rows: Vec<Row> = vec![
|
||||
("move", NONE, point, Default, "the arrow over a point"),
|
||||
("move", NONE, segment, Default, "the arrow over a segment"),
|
||||
("move", NONE, midpoint, Pen(PenCursor::Add), "the arrow over a segment's midpoint: adds a point there"),
|
||||
("move", NONE, empty, Default, "the arrow elsewhere"),
|
||||
("move", ALT, segment, Pen(PenCursor::Add), "Alt over a segment adds a point there"),
|
||||
("move", ALT, point, Default, "Alt over a point adds nothing"),
|
||||
|
||||
@@ -285,6 +285,9 @@ pub struct Overlay {
|
||||
pub vector_hover: Option<crate::vectoredit::VHit>,
|
||||
/// The edited layer `vector_hover` is on (several layers can be in edit mode).
|
||||
pub vector_hover_on: Option<NodeId>,
|
||||
/// Vector edit mode: the point a press on the outline would add (the pen's ghost anchor where
|
||||
/// it is over a segment, or the midpoint of the segment under the Move tool).
|
||||
pub vector_add: Option<VectorAdd>,
|
||||
/// Vector edit mode, Variable width tool: the width point, handle or path under the pointer.
|
||||
pub width_hover: Option<crate::vectoredit::WHit>,
|
||||
pub lasso: Vec<[f64; 2]>,
|
||||
@@ -309,6 +312,16 @@ pub struct Overlay {
|
||||
pub shape: Option<ShapeHint>,
|
||||
}
|
||||
|
||||
/// A point a press would add to the edited vector's outline: page position, whether it is a
|
||||
/// segment's midpoint (the Move tool) rather than the pen's place on its segment, and whether the
|
||||
/// pointer is on it (a press adds it; otherwise it just shows where one would go).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
||||
pub struct VectorAdd {
|
||||
pub at: [f64; 2],
|
||||
pub midpoint: bool,
|
||||
pub on: bool,
|
||||
}
|
||||
|
||||
/// The Scale tool's live readout while dragging a handle.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
||||
pub struct ScaleHint {
|
||||
|
||||
@@ -236,7 +236,7 @@ fn selecting_points_and_segments() {
|
||||
match what {
|
||||
"click tl" => click(&mut s, (0.0, 0.0), m),
|
||||
"click tr" => click(&mut s, (100.0, 0.0), m),
|
||||
"click top" => click(&mut s, (50.0, 0.0), m),
|
||||
"click top" => click(&mut s, (30.0, 0.0), m),
|
||||
"marquee top" => drag(&mut s, (-20.0, -20.0), (110.0, 10.0), m),
|
||||
_ => {
|
||||
ex(&mut s, "vector.edit.tool", json!({"tool": "lasso"}));
|
||||
@@ -310,7 +310,7 @@ fn deleting_points_vs_delete_and_heal() {
|
||||
// Deleting a selected segment keeps its points when others use them.
|
||||
let mut s = Session::new();
|
||||
let id = square(&mut s, true);
|
||||
click(&mut s, (50.0, 0.0), NONE);
|
||||
click(&mut s, (30.0, 0.0), NONE);
|
||||
key(&mut s, "Delete", false);
|
||||
let n = net(&s, id);
|
||||
assert_eq!((n.vertices.len(), n.segments.len()), (4, 3), "deleting a segment leaves its points");
|
||||
|
||||
@@ -114,6 +114,16 @@ pub(crate) struct PointSnap {
|
||||
/// Eraser brush radius in screen pixels.
|
||||
const ERASER_PX: f64 = 8.0;
|
||||
|
||||
/// A segment's midpoint is a point a press adds when the pointer is this close (screen pixels).
|
||||
const MIDPOINT_PX: f64 = 6.0;
|
||||
|
||||
/// The page point at `t` (0..1) along segment `seg` of vector `id`.
|
||||
fn segment_point(doc: &Document, id: NodeId, seg: usize, t: f64) -> Option<Point> {
|
||||
use figcraft_geom::kurbo::ParamCurve;
|
||||
let c = network(doc, id)?.cubic(seg)?;
|
||||
Some(doc.world_transform(id) * c.eval(t.clamp(0.0, 1.0)))
|
||||
}
|
||||
|
||||
fn erase_all(net: &mut VectorNetwork, stamps: &[Point], radius: f64) -> bool {
|
||||
stamps.iter().fold(false, |acc, c| ops::erase(net, *c, radius) | acc)
|
||||
}
|
||||
@@ -337,9 +347,22 @@ impl Session {
|
||||
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 };
|
||||
// A point a press on the outline would add: the pen's place under its pointer on a segment
|
||||
// (its ghost anchor), or the midpoint of the segment under the Move tool.
|
||||
let add = match (ve.tool, h) {
|
||||
(VTool::Pen, Some(VHit::Segment { seg, t })) => {
|
||||
segment_point(&doc, ve.id, seg, t).map(|at| crate::tools::VectorAdd { at: [at.x, at.y], midpoint: false, on: true })
|
||||
}
|
||||
(VTool::Move, Some(VHit::Segment { seg, .. })) if !ev.cmd && !ev.alt => segment_point(&doc, ve.id, seg, 0.5).map(|at| {
|
||||
let on = (at - ev.p()).hypot() * ev.zoom.max(1e-6) <= MIDPOINT_PX;
|
||||
crate::tools::VectorAdd { at: [at.x, at.y], midpoint: true, on }
|
||||
}),
|
||||
_ => 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.vector_add = add;
|
||||
ov.width_hover = None;
|
||||
ov.face.clear();
|
||||
ov.rubber = None;
|
||||
@@ -351,6 +374,8 @@ impl Session {
|
||||
(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),
|
||||
// On a segment's midpoint the arrow turns into the pen: a press adds a point there.
|
||||
(VTool::Move, Some(VHit::Segment { .. })) if add.is_some_and(|a| a.on) => 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),
|
||||
@@ -370,11 +395,13 @@ impl Session {
|
||||
{
|
||||
self.tool.overlay.cursor = crate::tools::handle_cursor(&b.corners(), handle);
|
||||
self.tool.overlay.vector_hover = None;
|
||||
self.tool.overlay.vector_add = 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;
|
||||
self.tool.overlay.vector_add = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,6 +537,7 @@ impl Session {
|
||||
};
|
||||
let h = hit(&doc, ve, ev.p(), tol);
|
||||
self.tool.overlay.vector_hover = None;
|
||||
self.tool.overlay.vector_add = 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 };
|
||||
@@ -626,8 +654,14 @@ impl Session {
|
||||
self.vgesture(g);
|
||||
}
|
||||
Some(VHit::Segment { seg, t }) => {
|
||||
if ev.alt {
|
||||
// A press on the midpoint the hover showed adds a point exactly there; Alt adds
|
||||
// one anywhere else on the segment.
|
||||
let midpoint = !ev.alt
|
||||
&& !ev.shift
|
||||
&& segment_point(&doc, id, seg, 0.5).is_some_and(|m| (m - ev.p()).hypot() * ev.zoom.max(1e-6) <= MIDPOINT_PX);
|
||||
if ev.alt || midpoint {
|
||||
// Add a point on the segment and drag it.
|
||||
let t = if midpoint { 0.5 } else { t };
|
||||
self.begin_interaction("Add point")?;
|
||||
let mut v = None;
|
||||
self.preview(|doc, _| {
|
||||
|
||||
@@ -171,7 +171,7 @@ fn lasso_marquee_and_segment_selection() {
|
||||
assert!(s.tool.overlay.lasso.is_empty());
|
||||
// Move tool: clicking a segment selects it; shift adds another.
|
||||
ex(&mut s, "vector.edit.tool", json!({"tool": "move"}));
|
||||
click(&mut s, 50.0, 100.0);
|
||||
click(&mut s, 30.0, 100.0);
|
||||
assert_eq!(ve(&s).segments.len(), 1);
|
||||
assert!(ve(&s).vertices.is_empty());
|
||||
let mut sh = ev(PointerKind::Down, 0.0, 50.0);
|
||||
@@ -371,3 +371,67 @@ fn eraser_strokes_cut_paths_in_one_step() {
|
||||
assert!(key(&mut s, "E", true, false, false).unwrap());
|
||||
assert_eq!(ve(&s).tool, VTool::Eraser);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_segment_shows_its_midpoint_and_a_press_adds_a_point_there() {
|
||||
let mut s = Session::new();
|
||||
let id = rect(&mut s);
|
||||
// Near the top edge but off its midpoint: the hint shows where the point would go, but the
|
||||
// pointer is not on it, so the arrow stays.
|
||||
s.pointer(&ev(PointerKind::Hover, 30.0, 0.0)).unwrap();
|
||||
let add = s.tool.overlay.vector_add.expect("the segment's midpoint");
|
||||
assert!(add.midpoint && !add.on, "{add:?}");
|
||||
assert!((add.at[0] - 50.0).abs() < 1e-9 && add.at[1].abs() < 1e-9, "{add:?}");
|
||||
assert_eq!(s.tool.overlay.cursor, crate::tools::Cursor::Default);
|
||||
// On the midpoint: the pen cursor, and a press adds a point there and selects it.
|
||||
s.pointer(&ev(PointerKind::Hover, 50.0, 0.0)).unwrap();
|
||||
assert!(s.tool.overlay.vector_add.is_some_and(|a| a.on), "the pointer is on the midpoint");
|
||||
assert_eq!(s.tool.overlay.cursor, crate::tools::Cursor::Pen(crate::tools::PenCursor::Add));
|
||||
click(&mut s, 50.0, 0.0);
|
||||
let n = net(&s, id);
|
||||
assert_eq!(n.vertices.len(), 5, "a point was added: {n:?}");
|
||||
assert_eq!(ve(&s).vertices, vec![vertex_at(&n, 50.0, 0.0)], "the new point is selected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_a_midpoint_keeps_new_midpoints_coming_on_each_half() {
|
||||
let mut s = Session::new();
|
||||
let id = rect(&mut s);
|
||||
click(&mut s, 50.0, 0.0);
|
||||
assert_eq!(net(&s, id).vertices.len(), 5);
|
||||
// The left half has its own midpoint, shown and added the same way.
|
||||
s.pointer(&ev(PointerKind::Hover, 25.0, 0.0)).unwrap();
|
||||
let add = s.tool.overlay.vector_add.expect("the new half's midpoint");
|
||||
assert!(add.midpoint && add.on && (add.at[0] - 25.0).abs() < 1e-9, "{add:?}");
|
||||
click(&mut s, 25.0, 0.0);
|
||||
let n = net(&s, id);
|
||||
assert_eq!(n.vertices.len(), 6, "{n:?}");
|
||||
assert_eq!(ve(&s).vertices, vec![vertex_at(&n, 25.0, 0.0)]);
|
||||
// And on it goes: a quarter of the way along.
|
||||
s.pointer(&ev(PointerKind::Hover, 12.5, 0.0)).unwrap();
|
||||
let add = s.tool.overlay.vector_add.expect("another midpoint");
|
||||
assert!((add.at[0] - 12.5).abs() < 1e-9, "{add:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_away_from_the_midpoint_selects_the_segment() {
|
||||
let mut s = Session::new();
|
||||
let id = rect(&mut s);
|
||||
click(&mut s, 30.0, 0.0);
|
||||
assert_eq!(net(&s, id).vertices.len(), 4, "no point is added off the midpoint");
|
||||
assert_eq!(ve(&s).segments.len(), 1, "the segment is selected instead");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pen_shows_its_ghost_anchor_on_the_segment_under_it() {
|
||||
let mut s = Session::new();
|
||||
let id = rect(&mut s);
|
||||
ex(&mut s, "vector.edit.tool", json!({"tool": "pen"}));
|
||||
s.pointer(&ev(PointerKind::Hover, 80.0, 0.0)).unwrap();
|
||||
let add = s.tool.overlay.vector_add.expect("the ghost anchor");
|
||||
assert!(!add.midpoint && add.on, "{add:?}");
|
||||
assert!((add.at[0] - 80.0).abs() < 1e-3 && add.at[1].abs() < 1e-3, "{add:?}");
|
||||
click(&mut s, 80.0, 0.0);
|
||||
assert_eq!(net(&s, id).vertices.len(), 5, "the press added the point");
|
||||
assert_eq!(ve(&s).anchor, None, "the click ended the pen's path");
|
||||
}
|
||||
|
||||
@@ -1021,10 +1021,10 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
|
||||
let hover_on = |id| ov.vector_hover.filter(|_| ov.vector_hover_on.unwrap_or(ve.id) == id);
|
||||
for part in &ve.others {
|
||||
if let Some(po) = figcraft_engine::vectoredit::overlay(&doc, &ve.view(part)) {
|
||||
vector_overlay(app, painter, &po, !width_tool, hover_on(part.id), false);
|
||||
vector_overlay(app, painter, &po, !width_tool, hover_on(part.id), false, None);
|
||||
}
|
||||
}
|
||||
vector_overlay(app, painter, &o, !width_tool, hover_on(ve.id), true);
|
||||
vector_overlay(app, painter, &o, !width_tool, hover_on(ve.id), true, ov.vector_add);
|
||||
if width_tool {
|
||||
width_overlay(app, painter, &doc, &ve, zoom);
|
||||
} else if let Some(c) = app.session.vector_edit_box() {
|
||||
@@ -1327,8 +1327,9 @@ fn bezier_handle(painter: &egui::Painter, v: Pos2, e: Pos2, r: f32, fill: Color3
|
||||
painter.circle_stroke(e, r, stroke(accent(painter), 1.0));
|
||||
}
|
||||
|
||||
/// Vector edit mode: a layer's segments, handles and points (`points`) with its `hover`ed part, and
|
||||
/// for the `active` layer the closed area under the paint bucket and the pen's rubber band.
|
||||
/// Vector edit mode: a layer's segments, handles and points (`points`) with its `hover`ed part, the
|
||||
/// `add` point a press would put on the outline, and for the `active` layer the closed area under
|
||||
/// the paint bucket and the pen's rubber band.
|
||||
fn vector_overlay(
|
||||
app: &FigApp,
|
||||
painter: &egui::Painter,
|
||||
@@ -1336,6 +1337,7 @@ fn vector_overlay(
|
||||
points: bool,
|
||||
hover: Option<figcraft_engine::vectoredit::VHit>,
|
||||
active: bool,
|
||||
add: Option<figcraft_engine::tools::VectorAdd>,
|
||||
) {
|
||||
use figcraft_engine::vectoredit::VHit;
|
||||
let ov = &app.session.tool.overlay;
|
||||
@@ -1383,6 +1385,22 @@ fn vector_overlay(
|
||||
painter.circle_stroke(p, 7.5, stroke(accent(painter), 1.0));
|
||||
}
|
||||
}
|
||||
// The point a press would add to the outline: the pen's ghost anchor where it is over a
|
||||
// segment, or a segment's midpoint under the Move tool (which grows and fills once the pointer
|
||||
// is on it, when a press adds it).
|
||||
if let Some(a) = add {
|
||||
let p = app.to_screen(a.at[0], a.at[1]);
|
||||
let col = accent(painter);
|
||||
if a.midpoint {
|
||||
let (r, fill) = if a.on { (5.5, col) } else { (3.5, Color32::from_white_alpha(190)) };
|
||||
painter.circle_filled(p, r, fill);
|
||||
painter.circle_stroke(p, r, stroke(if a.on { Color32::WHITE } else { col.gamma_multiply(0.7) }, 1.2));
|
||||
} else {
|
||||
let r = Rect::from_center_size(p, vec2(7.0, 7.0));
|
||||
painter.rect_filled(r, CornerRadius::same(5), Color32::from_white_alpha(160));
|
||||
painter.rect_stroke(r, CornerRadius::same(5), stroke(col.gamma_multiply(0.8), 1.0), StrokeKind::Inside);
|
||||
}
|
||||
}
|
||||
if let Some([x0, y0, x1, y1]) = ov.rubber.filter(|_| active) {
|
||||
painter.add(Shape::dashed_line(&[app.to_screen(x0, y0), app.to_screen(x1, y1)], stroke(accent(painter), 1.0), 4.0, 3.0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user