Files
vectorcraft/crates/geom/src/lib.rs
T
bflatasticandClaude Opus 5.5 0358c4df95 M3.35: gradient handles snap and honour Constrain Angle; double-clicking a tool button opens its options
Gradient tool handles (a drawn vector's start and end, the end handle and the moved start) snap to anchors, edges
and smart guides, with the guide overlays; Shift constrains to 45° steps from the Constrain Angle preference
(`ToolContext::constrain_angle`, `constrain_angle_from` in geom). Double-clicking a toolbar button runs the new UI
command `tool.options {tool}`: the Gradient tool opens the Gradient panel, the Eyedropper opens Eyedropper Options
(`eyedropper.setOptions {appearance?, transparency?}`, which `appearance.copyFrom` honours and its params override).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:18:34 +07:00

118 lines
3.9 KiB
Rust

//! VectorCraft geometry kernel.
//!
//! The editable path model is anchor based (like every Illustrator tool thinks about paths):
//! a [`PathData`] is a list of [`SubPath`]s, each a list of [`Anchor`]s with absolute in/out
//! handle positions. Rendering and algorithms convert to [`kurbo::BezPath`].
//!
//! Coordinates are document points (1/72 in), y pointing down.
#![forbid(unsafe_code)]
pub mod hit;
pub mod path;
pub mod recognize;
pub mod shapes;
pub mod snap;
pub use kurbo;
pub use kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, Rect, Shape, Size, Vec2};
pub use path::{Anchor, AnchorKind, FillRule, PathData, SubPath};
/// Tolerance used when comparing handle positions to anchor positions.
pub const EPS: f64 = 1e-9;
/// Union of two optional rects.
pub fn union_opt(a: Option<Rect>, b: Option<Rect>) -> Option<Rect> {
match (a, b) {
(Some(a), Some(b)) => Some(a.union(b)),
(a, None) => a,
(None, b) => b,
}
}
/// Normalize an angle in degrees to (-180, 180].
pub fn normalize_deg(mut a: f64) -> f64 {
a %= 360.0;
if a <= -180.0 {
a += 360.0;
} else if a > 180.0 {
a -= 360.0;
}
a
}
/// Snap a vector to the nearest multiple of `step_deg` (used for Shift-constrained drags).
pub fn constrain_angle(v: Vec2, step_deg: f64) -> Vec2 {
constrain_angle_from(v, step_deg, 0.0)
}
/// [`constrain_angle`] in steps counted from `base_deg` (the Constrain Angle preference,
/// counter-clockwise as seen on the y-down page).
pub fn constrain_angle_from(v: Vec2, step_deg: f64, base_deg: f64) -> Vec2 {
let len = v.hypot();
if len < EPS {
return v;
}
let (step, base) = (step_deg.to_radians(), -base_deg.to_radians());
let a = base + ((v.y.atan2(v.x) - base) / step).round() * step;
Vec2::new(a.cos() * len, a.sin() * len)
}
/// Rect from two corner points in any order.
pub fn rect_from_points(a: Point, b: Point) -> Rect {
Rect::from_points(a, b)
}
/// The 9 reference points of a rect (Illustrator's reference-point locator), row-major from top-left.
pub fn reference_point(r: Rect, index: usize) -> Point {
let xs = [r.x0, r.center().x, r.x1];
let ys = [r.y0, r.center().y, r.y1];
Point::new(xs[index % 3], ys[(index / 3).min(2)])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn angle_normalization() {
assert_eq!(normalize_deg(190.0), -170.0);
assert_eq!(normalize_deg(-190.0), 170.0);
assert_eq!(normalize_deg(180.0), 180.0);
assert_eq!(normalize_deg(720.0), 0.0);
}
#[test]
fn constrain_45() {
let v = constrain_angle(Vec2::new(10.0, 1.0), 45.0);
assert!((v.y).abs() < 1e-9);
let v = constrain_angle(Vec2::new(10.0, 9.0), 45.0);
assert!((v.x - v.y).abs() < 1e-9);
}
#[test]
fn constrain_from_a_base_angle() {
// 30° base: 45° steps land on 30°, 75°, … (counter-clockwise on the page, y down).
let deg = |v: Vec2| (-v.y).atan2(v.x).to_degrees();
assert!((deg(constrain_angle_from(Vec2::new(10.0, -4.0), 45.0, 30.0)) - 30.0).abs() < 1e-9);
assert!((deg(constrain_angle_from(Vec2::new(3.0, -10.0), 45.0, 30.0)) - 75.0).abs() < 1e-9);
assert!((deg(constrain_angle_from(Vec2::new(10.0, 1.0), 45.0, 30.0)) + 15.0).abs() < 1e-9);
}
#[test]
fn reference_points() {
let r = Rect::new(0.0, 0.0, 10.0, 20.0);
assert_eq!(reference_point(r, 0), Point::new(0.0, 0.0));
assert_eq!(reference_point(r, 4), Point::new(5.0, 10.0));
assert_eq!(reference_point(r, 8), Point::new(10.0, 20.0));
}
#[test]
fn union_options() {
let a = Rect::new(0.0, 0.0, 1.0, 1.0);
let b = Rect::new(2.0, 2.0, 3.0, 3.0);
assert_eq!(union_opt(Some(a), Some(b)), Some(Rect::new(0.0, 0.0, 3.0, 3.0)));
assert_eq!(union_opt(None, Some(b)), Some(b));
assert_eq!(union_opt(None, None), None);
}
}