M3.97: freeform points drag out to delete, spread ring drags, Lines mode draws lines

With the Gradient tool on a freeform gradient the selected point shows its
spread as a dashed ring with a handle; dragging either sets the spread. A
point dragged out of the object is removed on release, and double-clicking a
point opens the gradientStop popover, which now edits the selected freeform
point (colour, opacity, spread). In Lines mode successive clicks draw one
smooth line (with a rubber band to the pointer); clicking an existing point
continues the line from it, and Escape or a click off the art ends it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 04:43:43 +07:00
co-authored by Claude Opus 5.5
parent 37acaebf95
commit b8793eeaef
12 changed files with 533 additions and 94 deletions
+1 -8
View File
@@ -5,7 +5,7 @@
use serde_json::{Value, json};
use vectorcraft_color::freeform::spread_scale;
use vectorcraft_color::{Freeform, FreeformMode, FreeformPoint, GradientKind, Paint};
use vectorcraft_doc::{Node, NodeKind};
use vectorcraft_doc::NodeKind;
use vectorcraft_geom::{Affine, Rect};
use super::appearance::{edit_items, edits_stroke, index_param, item_target};
@@ -323,10 +323,3 @@ fn get(s: &mut Session, p: &Value) -> Result<Value> {
v["selected"] = json!(selected);
Ok(v)
}
/// Whether `n`'s shape contains `p` (its filled regions; true for objects without them, such as
/// type, whose boxes stand in).
pub(crate) fn inside_fn(n: &Node) -> impl Fn(vectorcraft_geom::Point) -> bool + use<> {
let shapes = n.clip_shapes(None);
move |p| shapes.is_empty() || shapes.iter().any(|(bp, rule)| vectorcraft_geom::hit::fill_contains(bp, *rule, p))
}
+1 -1
View File
@@ -357,7 +357,7 @@ fn edit_gradient(s: &mut Session, p: &Value) -> Result<Value> {
}
let b = item_paint_bounds(n, index, !stroke);
// Only a gradient turning freeform needs the shape (its first points go inside it).
let inside = (freeform && b.is_some()).then(|| super::freeform::inside_fn(n));
let inside = (freeform && b.is_some()).then(|| n.contains_fn());
let inside: &dyn Fn(Point) -> bool = match &inside {
Some(f) => f,
None => &|_| true,
+1 -1
View File
@@ -61,7 +61,7 @@ fn the_first_application_places_points_inside_the_shape() {
let g = fill_gradient(&s, id);
assert_eq!(g.gradient.kind, GradientKind::Freeform);
let f = g.freeform.as_ref().unwrap();
let inside = cmd::freeform::inside_fn(node(&s, id));
let inside = node(&s, id).contains_fn();
assert!(f.points.len() >= 4 && f.points.iter().all(|p| inside(p.at)), "{:?}", f.points);
// The stops follow the points' colours (swatch chips and exports show them).
assert_eq!(g.gradient.stops.iter().map(|st| st.color).collect::<Vec<_>>(), f.points.iter().map(|p| p.color).collect::<Vec<_>>());