Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ env:
# If the compilation fails, then the version specified here needs to be bumped up to reality.
# Be sure to also update the rust-version property in the workspace Cargo.toml file,
# plus all the README.md files of the affected packages.
RUST_MIN_VER: "1.65"
RUST_MIN_VER: "1.82"
# List of packages that will be checked with the minimum supported Rust version.
# This should be limited to packages that are intended for publishing.
RUST_MIN_VER_PKGS: "-p kurbo"
Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ You can find its changes [documented below](#0112-2025-04-28).

## [Unreleased]

This release has an [MSRV][] of 1.65.
This release has an [MSRV][] of 1.82.
It was increased to support floating point math in const functions.

### Changed

- The implementation of stroking is much faster. ([#427][] by [@raphlinus][])

### Fixed

Expand Down Expand Up @@ -125,6 +130,7 @@ Note: A changelog was not kept for or before this release
[#412]: https://github.com/linebender/kurbo/pull/412
[#413]: https://github.com/linebender/kurbo/pull/413
[#418]: https://github.com/linebender/kurbo/pull/418
[#427]: https://github.com/linebender/kurbo/pull/427
[#428]: https://github.com/linebender/kurbo/pull/428
[#429]: https://github.com/linebender/kurbo/pull/429
[#447]: https://github.com/linebender/kurbo/pull/447
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ version = "0.11.2"
edition = "2021"
# Keep in sync with RUST_MIN_VER in .github/workflows/ci.yml, with the relevant README.md files
# and with the MSRV in the `Unreleased` section of CHANGELOG.md.
rust-version = "1.65"
rust-version = "1.82"
license = "Apache-2.0 OR MIT"
repository = "https://github.com/linebender/kurbo"

Expand Down
2 changes: 1 addition & 1 deletion kurbo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ There are traits intended to be useful for general curves (not just Béziers), b

## Minimum supported Rust Version (MSRV)

This version of Kurbo has been verified to compile with **Rust 1.65** and later.
This version of Kurbo has been verified to compile with **Rust 1.82** and later.

Future versions of Kurbo might increase the Rust version requirement.
It will not be treated as a breaking change and as such can even happen with small patch releases.
Expand Down
5 changes: 1 addition & 4 deletions kurbo/examples/arclen_accuracy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@

//! A test program to plot the error of arclength approximation.

// Lots of stuff is commented out or was just something to try.
#![allow(unused)]
#![allow(clippy::unreadable_literal)]
#![allow(clippy::many_single_char_names)]
#![allow(unused, reason = "a bunch of experiments in the code")]

// TODO: make more functionality accessible from command line rather than uncommenting.

Expand Down
10 changes: 5 additions & 5 deletions kurbo/examples/cubic_arclen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@

//! Research testbed for arclengths of cubic Bézier segments.

// Lots of stuff is commented out or was just something to try.
#![allow(unused)]
#![allow(clippy::unreadable_literal)]
#![allow(clippy::many_single_char_names)]
#![allow(unused, reason = "a bunch of experiments in the code")]

use kurbo::common::{GAUSS_LEGENDRE_COEFFS_11, GAUSS_LEGENDRE_COEFFS_7, GAUSS_LEGENDRE_COEFFS_9};
use kurbo::{
Expand Down Expand Up @@ -113,7 +110,10 @@ fn est_gauss11_error_2(c: CubicBez) -> f64 {
.sum::<f64>()
}

#[allow(clippy::neg_cmp_op_on_partial_ord)]
#[expect(
clippy::neg_cmp_op_on_partial_ord,
reason = "probably IEEE semantics nuance"
)]
fn est_max_curvature(c: CubicBez) -> f64 {
let n = 100;
let mut max = 0.0;
Expand Down
2 changes: 2 additions & 0 deletions kurbo/examples/offset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

//! A simple example to show an offset curve of a cubic Bézier segment.

#![expect(deprecated, reason = "example still valid even though deprecated")]

use kurbo::{offset::CubicOffset, CubicBez, Shape};

fn main() {
Expand Down
183 changes: 183 additions & 0 deletions kurbo/examples/stroke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2025 the Kurbo Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Stroke example.
//!
//! This example has been lightly adapted from Vello, which in turn has
//! been adapted from the Skia "tricky stroke" test.

use kurbo::{stroke, Affine, BezPath, Cap, Join, Rect, Shape, Stroke};

fn tricky_strokes() {
const CELL_SIZE: f64 = 200.;
const STROKE_WIDTH: f64 = 30.;
const NUM_COLS: usize = 5;

fn stroke_bounds(pts: &[(f64, f64); 4]) -> Rect {
use kurbo::CubicBez;
CubicBez::new(pts[0], pts[1], pts[2], pts[3])
.bounding_box()
.inflate(STROKE_WIDTH, STROKE_WIDTH)
}

fn map_rect_to_rect(src: &Rect, dst: &Rect) -> (Affine, f64) {
let (scale, x_larger) = {
let sx = dst.width() / src.width();
let sy = dst.height() / src.height();
(sx.min(sy), sx > sy)
};
let tx = dst.x0 - src.x0 * scale;
let ty = dst.y0 - src.y0 * scale;
let (tx, ty) = if x_larger {
(tx + 0.5 * (dst.width() - src.width() * scale), ty)
} else {
(tx, ty + 0.5 * (dst.height() - src.height() * scale))
};
(Affine::new([scale, 0.0, 0.0, scale, tx, ty]), scale)
}

let tricky_cubics = [
[(122., 737.), (348., 553.), (403., 761.), (400., 760.)],
[(244., 520.), (244., 518.), (1141., 634.), (394., 688.)],
[(550., 194.), (138., 130.), (1035., 246.), (288., 300.)],
[(226., 733.), (556., 779.), (-43., 471.), (348., 683.)],
[(268., 204.), (492., 304.), (352., 23.), (433., 412.)],
[(172., 480.), (396., 580.), (256., 299.), (338., 677.)],
[(731., 340.), (318., 252.), (1026., -64.), (367., 265.)],
[(475., 708.), (62., 620.), (770., 304.), (220., 659.)],
[(0., 0.), (128., 128.), (128., 0.), (0., 128.)], // Perfect cusp
[(0., 0.01), (128., 127.999), (128., 0.01), (0., 127.99)], // Near-cusp
[(0., -0.01), (128., 128.001), (128., -0.01), (0., 128.001)], // Near-cusp
[(0., 0.), (0., -10.), (0., -10.), (0., 10.)], // Flat line with 180
[(10., 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s
[(39., -39.), (40., -40.), (40., -40.), (0., 0.)], // Flat diagonal with 180
[(40., 40.), (0., 0.), (200., 200.), (0., 0.)], // Diag w/ an internal 180
[(0., 0.), (1e-2, 0.), (-1e-2, 0.), (0., 0.)], // Circle
// Flat line with no turns:
[
(400.75, 100.05),
(400.75, 100.05),
(100.05, 300.95),
(100.05, 300.95),
],
[(0.5, 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s
[(10., 0.), (0., 0.), (10., 0.), (10., 0.)], // Flat line with a 180
];

// Flat conic with a cusp: (1,1) (2,1) (1,1), weight: 1
let flat_quad = [
// moveTo(1., 1.),
[(2., 1.), (1., 1.)],
];
// Flat conic with a cusp: (1,1) (100,1) (25,1), weight: 0.3
let flat_conic_as_quads = [
// moveTo(1., 1.),
[(2.232486, 1.000000), (3.471740, 1.000000)],
[(4.710995, 1.000000), (5.949262, 1.000000)],
[(7.187530, 1.000000), (8.417061, 1.000000)],
[(9.646591, 1.000000), (10.859690, 1.000000)],
[(12.072789, 1.000000), (13.261865, 1.000000)],
[(14.450940, 1.000000), (15.608549, 1.000000)],
[(16.766161, 1.000000), (17.885059, 1.000000)],
[(19.003958, 1.000000), (20.077141, 1.000000)],
[(21.150328, 1.000000), (22.171083, 1.000000)],
[(23.191839, 1.000000), (24.153776, 1.000000)],
[(25.115715, 1.000000), (26.012812, 1.000000)],
[(26.909912, 1.000000), (27.736557, 1.000000)],
[(28.563202, 1.000000), (29.314220, 1.000000)],
[(30.065239, 1.000000), (30.735928, 1.000000)],
[(31.406620, 1.000000), (31.992788, 1.000000)],
[(32.578957, 1.000000), (33.076927, 1.000000)],
[(33.574905, 1.000000), (33.981567, 1.000000)],
[(34.388233, 1.000000), (34.701038, 1.000000)],
[(35.013851, 1.000000), (35.230850, 1.000000)],
[(35.447845, 1.000000), (35.567669, 1.000000)],
[(35.687500, 1.000000), (35.709404, 1.000000)],
[(35.731312, 1.000000), (35.655155, 1.000000)],
[(35.579006, 1.000000), (35.405273, 1.000000)],
[(35.231541, 1.000000), (34.961311, 1.000000)],
[(34.691086, 1.000000), (34.326057, 1.000000)],
[(33.961029, 1.000000), (33.503479, 1.000000)],
[(33.045937, 1.000000), (32.498734, 1.000000)],
[(31.951530, 1.000000), (31.318098, 1.000000)],
[(30.684669, 1.000000), (29.968971, 1.000000)],
[(29.253277, 1.000000), (28.459791, 1.000000)],
[(27.666309, 1.000000), (26.800005, 1.000000)],
[(25.933704, 1.000000), (25.000000, 1.000000)],
];
// Flat conic with a cusp: (1,1) (100,1) (25,1), weight: 1.5
let bigger_flat_conic_as_quads = [
// moveTo(1., 1.),
[(8.979845, 1.000000), (15.795975, 1.000000)],
[(22.612104, 1.000000), (28.363287, 1.000000)],
[(34.114471, 1.000000), (38.884045, 1.000000)],
[(43.653618, 1.000000), (47.510696, 1.000000)],
[(51.367767, 1.000000), (54.368233, 1.000000)],
[(57.368698, 1.000000), (59.556030, 1.000000)],
[(61.743366, 1.000000), (63.149269, 1.000000)],
[(64.555168, 1.000000), (65.200005, 1.000000)],
[(65.844841, 1.000000), (65.737961, 1.000000)],
[(65.631073, 1.000000), (64.770912, 1.000000)],
[(63.910763, 1.000000), (62.284878, 1.000000)],
[(60.658997, 1.000000), (58.243816, 1.000000)],
[(55.828640, 1.000000), (52.589172, 1.000000)],
[(49.349705, 1.000000), (45.239006, 1.000000)],
[(41.128315, 1.000000), (36.086826, 1.000000)],
[(31.045338, 1.000000), (25.000000, 1.000000)],
];

let mut idx = 0;
let tolerance = 0.25;
for cubic in &tricky_cubics {
let x = (idx % NUM_COLS) as f64 * CELL_SIZE;
let y = (idx / NUM_COLS) as f64 * CELL_SIZE;
let cell = Rect::new(x, y, x + CELL_SIZE, y + CELL_SIZE);
let bounds = stroke_bounds(cubic);
let (t, s) = map_rect_to_rect(&bounds, &cell);
let style = Stroke::new(STROKE_WIDTH / s)
.with_caps(Cap::Butt)
.with_join(Join::Miter);
let mut path = BezPath::new();
path.move_to(cubic[0]);
path.curve_to(cubic[1], cubic[2], cubic[3]);
let stroked = stroke(path, &style, &Default::default(), tolerance);
println!(
" <path d='{}' stroke='#000' fill='#aec' stroke-width='1'/>",
(t * stroked).to_svg()
);
idx += 1;
}

let flat_curves = [
flat_quad.as_slice(),
flat_conic_as_quads.as_slice(),
bigger_flat_conic_as_quads.as_slice(),
];
for quads in flat_curves.iter() {
let mut path = BezPath::new();
path.move_to((1., 1.));
for quad in quads.iter() {
path.quad_to(quad[0], quad[1]);
}
let x = (idx % NUM_COLS) as f64 * CELL_SIZE;
let y = (idx / NUM_COLS) as f64 * CELL_SIZE;
let cell = Rect::new(x, y, x + CELL_SIZE, y + CELL_SIZE);
let bounds = path.bounding_box().inflate(STROKE_WIDTH, STROKE_WIDTH);
let (t, s) = map_rect_to_rect(&bounds, &cell);
let style = Stroke::new(STROKE_WIDTH / s)
.with_caps(Cap::Butt)
.with_join(Join::Miter);
let stroked = stroke(path, &style, &Default::default(), tolerance);
println!(
" <path d='{}' stroke='#000' fill='#aec' stroke-width='1'/>",
(t * stroked).to_svg()
);
idx += 1;
}
}

fn main() {
println!("<svg width='1000' height='1000' xmlns='http://www.w3.org/2000/svg'>");
tricky_strokes();
println!("</svg>");
}
20 changes: 16 additions & 4 deletions kurbo/src/cubicbez.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,10 @@ impl CubicBez {

/// Preprocess a cubic Bézier to ease numerical robustness.
///
/// If the cubic Bézier segment has zero or near-zero derivatives, perturb
/// the control points to make it easier to process (especially offset and
/// stroke), avoiding numerical robustness problems.
pub(crate) fn regularize(&self, dimension: f64) -> CubicBez {
/// If the cubic Bézier segment has zero or near-zero derivatives at the
/// endpoints, perturb the control points to make it easier to process
/// (especially offset and stroke), avoiding numerical robustness problems.
pub(crate) fn regularize_endpoints(&self, dimension: f64) -> CubicBez {
let mut c = *self;
// First step: if control point is too near the endpoint, nudge it away
// along the tangent.
Expand All @@ -411,6 +411,18 @@ impl CubicBez {
return c;
}
}
c
}

/// Preprocess a cubic Bézier to ease numerical robustness.
///
/// If the cubic Bézier segment has zero or near-zero derivatives as an interior
/// cusp, perturb the control points to make curvature finite, avoiding
/// numerical robustness problems in offset and stroke.
pub(crate) fn regularize_cusp(&self, dimension: f64) -> CubicBez {
let mut c = *self;
// First step: if control point is too near the endpoint, nudge it away
// along the tangent.
if let Some(cusp_type) = self.detect_cusp(dimension) {
let d01 = c.p1 - c.p0;
let d01h = d01.hypot();
Expand Down
4 changes: 3 additions & 1 deletion kurbo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@
clippy::match_same_arms,
clippy::partial_pub_fields,
clippy::unseparated_literal_suffix,
clippy::duplicated_attributes
clippy::duplicated_attributes,
clippy::allow_attributes,
clippy::allow_attributes_without_reason
)]

#[cfg(not(any(feature = "std", feature = "libm")))]
Expand Down
Loading