Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ 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.

### Fixed

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
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>");
}
12 changes: 9 additions & 3 deletions kurbo/src/cubicbez.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,18 @@ impl CubicBez {
/// 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 {
///
/// The `do_endpoints` parameter controls whether regularization applies to
/// endpoints or only internal cusps. The newer stroke logic should be able
/// to handle zero derivatives at endpoints. In fact, it is more robust when
/// the curve is not perturbed, as the curve matches the robustly computed
/// unit tangents.
pub(crate) fn regularize(&self, dimension: f64, do_endpoints: bool) -> CubicBez {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's worth having separate regularize_endpoints and regularize_cusp methods?

let mut c = *self;
// First step: if control point is too near the endpoint, nudge it away
// along the tangent.
let dim2 = dimension * dimension;
if c.p0.distance_squared(c.p1) < dim2 {
if do_endpoints && c.p0.distance_squared(c.p1) < dim2 {
let d02 = c.p0.distance_squared(c.p2);
if d02 >= dim2 {
// TODO: moderate if this would move closer to p3
Expand All @@ -400,7 +406,7 @@ impl CubicBez {
return c;
}
}
if c.p3.distance_squared(c.p2) < dim2 {
if do_endpoints && c.p3.distance_squared(c.p2) < dim2 {
let d13 = c.p1.distance_squared(c.p2);
if d13 >= dim2 {
// TODO: moderate if this would move closer to p0
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
Loading