kurbo

repository·main·Indexed 21 days ago

https://github.com/linebender/kurbo

A high-accuracy 2D curves and vector path library for Rust, designed for creative tools, engineering, and scientific applications. It provides data structures and algorithms for Bézier paths (BezPath), 2D affine transformations, and geometric analysis using f64. The library includes support for path flattening, winding numbers, area and perimeter calculations, and a companion crate called polycool for finding roots of low-degree polynomials.

Tokens
17.6K
Snippets
66
Records
85
Agent score
76%

What's inside kurbo

  1. Overview of Kurbo

    main

    Kurbo is a Rust library providing data structures and algorithms for 2D curves and vector paths. It is designed for high accuracy and performance, making it suitable for both creative tools and engineering or scientific applications.

    Key characteristics:

    • Accuracy-focused: Uses analytical solutions where practical (e.g., area calculation via Green's theorem).
    • Approximation control: Many approximate functions include an accuracy parameter.
    • Coordinate type: Uses f64 for all calculations.
    • Early development: The library is in early stages; general curve traits are subject to reorganization.
  2. Find roots of low-degree polynomials with polycool

    main

    polycool is a Rust crate designed for numerically finding roots of low-degree polynomials. It currently implements Yuksel's iterative solver, which finds roots within a specified interval to a target accuracy.

    To use it, create a Poly instance by providing a slice of coefficients. Note that the coefficients should be provided in increasing order of degree (e.g., [constant, x^1, x^2, ...]).

    Use the roots_between method to find all roots within a given range [min, max] at a specific precision epsilon.

    use polycool::Poly;
    
    // The polynomial x^3 - 6x^2 + 11x - 6
    // Coefficients: constant=-6, x=11, x^2=-6, x^3=1
    let p = Poly::new([-6.0, 11.0, -6.0, 1.0]);
    
    // Find roots between -10.0 and 10.0 with a precision of 1e-6
    let roots = p.roots_between(-10.0, 10.0, 1e-6);
    dbg!(roots);
    // [0.9999999999999996, 2.0000000000000018, 2.9999999999999982]
  3. Minimum supported Rust Version (MSRV)

    main

    Kurbo requires Rust 1.85 or later.

    If you encounter compilation errors due to a dependency requiring a higher Rust version and you cannot upgrade your toolchain, you can attempt to downgrade the specific dependency using cargo update:

    # Replace package_name and version with the actual dependency details
    cargo update -p package_name --precise 0.1.1
    cargo update -p package_name --precise 0.1.1
  4. Represent and manipulate Bézier paths with `BezPath`

    main

    A BezPath is a collection of Bézier path elements (PathEl) that can contain multiple subpaths. Each subpath must begin with a MoveTo element.

    BezPath can be used in two primary ways:

    1. Elements (PathEl): Instructions for drawing (e.g., MoveTo, LineTo, QuadTo, CurveTo, ClosePath). This is ideal for drawing APIs.
    2. Segments (PathSeg): Independent geometric shapes (e.g., Line, Quad, Cubic). This is ideal for hit-testing or subdivision.

    Key operations:

    • Construction: Use BezPath::new(), with_capacity(n), or from_vec(Vec<PathEl>). It also implements FromIterator<PathEl>.
    • Modification: Use push(el), move_to(p), line_to(p), quad_to(p1, p2), curve_to(p1, p2, p3), and close_path() to build paths incrementally.
    • Transformation: Apply affine transformations directly using the * operator (e.g., affine * path).
    • Iteration: Use .iter() for elements or .segments() for geometric segments.
    use kurbo::{BezPath, Rect, Shape, Vec2, Point};
    let accuracy = 0.1;
    let rect = Rect::from_origin_size((0., 0.), (10., 10.));
    
    // Create a path from a shape
    let path1 = rect.to_path(accuracy);
    
    // Or collect elements into a path
    let path2: BezPath = rect.path_elements(accuracy).collect();
    
    // Extend a path with another path
    let mut path = rect.to_path(accuracy);
    let shifted_rect = rect + Vec2::new(5.0, 10.0);
    path.extend(shifted_rect.to_path(accuracy));
  5. Use `Vec2` for 2D vector math

    main

    Vec2 is a 2D vector type used for mathematical vectors, translations, and coordinate manipulations. It can be converted to and from [Point] and [Size].

    Key capabilities include:

    • Arithmetic: Supports standard addition, subtraction, multiplication by scalar, and division by scalar.
    • Geometric Operations: Dot products, cross products, magnitude (length), and normalization.
    • Angular Operations: Calculating angles (atan2, angle) and creating unit vectors from angles (from_angle).
    • Transformations: Rotating by 90 degrees (turn_90) or combining rotation and scaling (rotate_scale).
    • Rounding: Various rounding modes including round, ceil, floor, expand (away from zero), and trunc (towards zero).
    use kurbo::Vec2;
    
    let v = Vec2::new(3.0, 4.0);
    let len = v.hypot(); // 5.0
    let unit = v.normalize();
    let dot = v.dot(Vec2::new(1.0, 0.0)); // 3.0
  6. Implement the ParamCurveFit trait for curve fitting

    main

    To use Kurbo's curve fitting algorithms, your source curve must implement the ParamCurveFit trait. This trait allows the fitting engine to sample the curve's position, tangents, and derivatives, and to identify discontinuities like cusps or corners.

    Key methods to implement:

    • sample_pt_tangent(&self, t: f64, sign: f64) -> CurveFitSample: Returns a point and a tangent vector at parameter t. Use the sign parameter to handle discontinuities (e.g., picking a side of a cusp).
    • sample_pt_deriv(&self, t: f64) -> (Point, Vec2): Returns the point and the mathematical derivative at t. This is used for moment integrals.
    • break_cusp(&self, range: Range<f64>) -> Option<f64>: Returns the parameter t of a cusp or corner within the given range. This is critical for preventing the fitter from trying to fit a single smooth curve through a sharp corner.
    • moment_integrals(&self, range: Range<f64>) -> (f64, f64, f64): Computes the integrals of $y dx$, $x y dx$, and $y^2 dx$. A default implementation using Gauss-Legendre quadrature is provided if you implement sample_pt_deriv.
  7. Compose affine transformations

    main

    Transformations can be composed using the multiplication operator (*). Kurbo provides two semantic patterns for composition:

    1. then_* (Post-composition): Applies the new transformation after the current one. self.then_rotate(th) is equivalent to Affine::rotate(th) * self.
    2. pre_* (Pre-composition): Applies the new transformation before the current one. self.pre_rotate(th) is equivalent to self * Affine::rotate(th).

    Note that matrix multiplication is associative: (A * B) * v == A * (B * v).

    use kurbo::Affine;
    
    let mut transform = Affine::IDENTITY;
    
    // Using then_* to chain operations
    transform = transform
        .then_scale(2.0)
        .then_rotate(std::f64::consts::PI / 4.0)
        .then_translate((10.0, 10.0));
  8. How `Insets` work with `Rect`

    main

    An Insets object represents the distances between the edges of a rectangle. It can be used to expand or contract a Rect using arithmetic operators.

    Mental Model

    • Positive insets increase the distance from the center, resulting in a larger rectangle.
    • Negative insets decrease the distance from the center, resulting in a smaller rectangle.
    • Coordinate Space: Insets operate on the absolute rectangle (Rect::abs), meaning they ignore existing negative widths or heights. However, if the inset values are larger than the rectangle's dimensions, the resulting Rect can still have negative width or height.

    Arithmetic Operations

    • rect + insets: Expands the rectangle.
    • rect - insets: Contracts the rectangle.
    • rect - rect = Insets: Subtracting one rectangle from another produces the Insets required to transform the first into the second.

    Example: Expanding a Rect

    use kurbo::{Insets, Rect};
    let rect = Rect::from_origin_size((0., 0.), (10., 10.));
    let insets = Insets::uniform_xy(3., 0.);
    
    let inset_rect = rect + insets;
    assert_eq!(inset_rect.width(), 16.0); // 10.0 + (3.0 * 2)
    assert_eq!(inset_rect.x0, -3.0);
    use kurbo::{Insets, Rect};
    let rect = Rect::from_origin_size((0., 0.), (10., 10.));
    let insets = Insets::uniform_xy(3., 0.);
    
    let inset_rect = rect + insets;
    assert_eq!(inset_rect.width(), 16.0, "10.0 + 3.0 × 2");
    assert_eq!(inset_rect.x0, -3.0);
  9. Use the `Point` struct for 2D coordinates

    main

    The Point struct represents a specific location in 2D space. While it has the same memory layout as Vec2, it is semantically different: Point is a location, whereas Vec2 represents a displacement or vector.

    Important Geometric Constraints: To maintain geometric correctness, kurbo does not implement certain operations for Point:

    • Point + Point is not implemented (adding two locations is undefined).
    • f64 * Point is not implemented (scaling a location is undefined).

    If you need to perform these operations, convert the Point to a Vec2 using to_vec2() first.

    Common Constants:

    • Point::ZERO: The point (0, 0).
    • Point::ORIGIN: The point at the origin (0, 0).
    use kurbo::Point;
    
    let p = Point::new(10.0, 20.0);
    let origin = Point::ORIGIN;
    
    // To perform operations like scaling, convert to Vec2
    // let scaled = p * 2.0; // This would fail
    // let scaled = p.to_vec2() * 2.0;
  10. Use the Shape trait for geometric operations

    main

    The Shape trait is the core abstraction for both open and closed geometric shapes in Kurbo. It allows you to perform common geometric operations like computing the area, bounding_box, perimeter, and winding number of a shape. It also provides mechanisms to convert shapes into Bézier representations.

    Conversion and Iteration

    When working with shapes, you can interact with them in several ways depending on your performance and memory requirements:

    1. Iteration (Efficient): Use path_elements(tolerance) to get an iterator over PathEl (Bézier path elements). This is often zero-allocation. Use path_segments(tolerance) to iterate over PathSegs.
    2. Owned Path (Allocating): Use to_path(tolerance) to create an owned BezPath. This always allocates and is best when you need to retain the path.
    3. Consuming Path (Allocating): Use into_path(tolerance) to convert the shape into a BezPath. This is zero-cost if the shape is already a BezPath.

    Tolerance

    The tolerance parameter controls the accuracy of converting geometric primitives (like circles) into Bézier curves.

    • For UI elements, a value of 0.1 is typically sufficient.
    • For scientific applications, use a smaller value.
    • Note: The number of cubic Bézier segments scales as tolerance ^ (-1/6).
    // Example of converting a shape to a BezPath
    let path = my_shape.to_path(0.1);
    
    // Example of iterating over path elements without allocation
    for element in my_shape.path_elements(0.1) {
        // process element
    }
  11. Use TranslateScale for uniform scaling and translation

    main

    The TranslateScale struct represents a transformation consisting of a uniform scaling followed by a translation. It is less powerful than Affine but is optimized for application to common primitives like Rect.

    Mathematically, it represents the following augmented matrix:

    | s 0 x |
    | 0 s y |
    | 0 0 1 |

    Where s is the scale and (x, y) is the translation.

    Key behaviors:

    • Non-commutative multiplication: The order of operations matters. Scale * Translate results in a different translation than Translate * Scale.
    • Transformation application: You can multiply a TranslateScale by a Point to transform it, or multiply two TranslateScale instances together to compose them.
    • Conversion: It can be converted into a full Affine transformation via Into<Affine>.
    use kurbo::{Point, TranslateScale, Vec2};
    
    let ts = TranslateScale::new(Vec2::new(5.0, 6.0), 2.0);
    let p = Point::new(3.0, 4.0);
    let transformed_p = ts * p; // Result: Point(11.0, 14.0)
  12. Iterate over path elements and segments

    main

    You can traverse a BezPath using two different iterator patterns depending on your goal:

    • iter(): Returns an iterator over PathEl (drawing instructions). Use this when you need to know how the path was constructed (e.g., where the MoveTo commands are).
    • segments(): Returns an iterator over PathSeg (geometric segments). Use this when you need to perform geometric operations like intersection or distance calculation on individual curves/lines.

    If you have a generic iterator of PathEl, you can convert it to a segment iterator using the segments() free function.

    use kurbo::{BezPath, Line, PathEl, PathSeg, Point, Rect, Shape};
    let rect = Rect::from_origin_size((0., 0.), (10., 10.));
    let path = rect.to_path(0.1);
    
    // Accessing elements
    let first_el = path.iter().next(); // Some(PathEl::MoveTo(Point::new(0., 0.)))
    
    // Accessing segments
    let first_seg = path.segments().next(); // Some(PathSeg::Line(Line::new((0., 0.), (10., 0.))))