iCurve performs robust boolean operations on closed paths made from lines,
quadratic and cubic Bezier curves, and rational elliptic arcs. It uses integer
geometry internally and delegates polygon topology extraction to
iOverlay, while preserving the
curved segment representation in the result.
The crate is currently experimental. The geometry kernel is usable, but the public API may still change before the first stable release.
- Union, intersection, difference, inverse difference, and XOR.
- Even-odd, non-zero, positive, and negative fill rules.
- Lines, quadratic Beziers, cubic Beziers, and rational elliptic arcs.
- Integer kernels for
i16,i32, andi64. - Float input with automatic or explicit fixed-point scaling.
no_stdsupport withalloc.
The integer API is the direct path to the boolean kernel. Each input shape can
contain multiple closed contours. Add shapes as either Subject or Clip,
then select an OverlayRule and a FillRule.
The current API uses point, fill-rule, and overlay-rule types from iOverlay,
so applications should include both crates as dependencies. The capacity
passed to IntCurveOverlay::new is an allocation hint and should approximately
match the total number of input segments.
use i_curve::int::{
bool::overlay::IntCurveOverlay,
curve::{path::CurvePath, segment::CurveSegment, shape::CurveShape},
};
use i_overlay::{
core::{fill_rule::FillRule, overlay::ShapeType, overlay_rule::OverlayRule},
i_shape::int::IntPoint,
};
let subject = CurveShape {
contours: vec![CurvePath {
start: IntPoint::new(0_i32, 0),
segments: vec![
CurveSegment::Line {
to: IntPoint::new(100, 0),
},
CurveSegment::Quad {
ctrl: IntPoint::new(130, 50),
to: IntPoint::new(100, 100),
},
CurveSegment::Line {
to: IntPoint::new(0, 100),
},
CurveSegment::Quad {
ctrl: IntPoint::new(-30, 50),
to: IntPoint::new(0, 0),
},
],
}],
};
let clip = CurveShape {
contours: vec![CurvePath {
start: IntPoint::new(50, -20),
segments: vec![
CurveSegment::Line {
to: IntPoint::new(120, -20),
},
CurveSegment::Line {
to: IntPoint::new(120, 120),
},
CurveSegment::Line {
to: IntPoint::new(50, 120),
},
CurveSegment::Line {
to: IntPoint::new(50, -20),
},
],
}],
};
let mut overlay = IntCurveOverlay::new(8);
overlay.add_shape(subject, ShapeType::Subject);
overlay.add_shape(clip, ShapeType::Clip);
let result = overlay.overlay(OverlayRule::Intersect, FillRule::NonZero);
assert!(!result.is_empty());For difficult inputs, the snapping radius used while splitting curves grows
between topology-refinement rounds according to the selected Precision.
The value is compared with squared endpoint distance, matching iOverlay.
Use with_solver to select a different starting radius or progression:
use i_curve::int::bool::overlay::IntCurveOverlay;
use i_overlay::core::solver::{Precision, Solver};
let solver = Solver::with_precision(Precision::MEDIUM);
let mut overlay = IntCurveOverlay::<i32>::with_solver(8, solver);Precision also accepts custom start and progression exponents. start
remains an absolute squared-radius exponent in the integer grid. Because the
progression presets were originally tuned for an i32 solver, iCurve scales
every non-zero progression by the integer width: i16 uses steps 1/1/2,
i32 uses 1/2/3, and i64 uses 2/4/6 for high/medium/low precision. The
complete solver is forwarded to the final polygon overlay.
IntCurveOverlay::new uses Solver::default().
The result is a Vec<CurveShape<I>>. The first contour of each shape is its
outer boundary; the remaining contours are holes. A contour is closed when the
endpoint of its last segment equals its start point.
The integer curve kernel reserves six bits for growth of polynomial coefficients. Input points should stay inside the following inclusive ranges:
| Integer type | Coordinate bits | Safe coordinate range |
|---|---|---|
i16 |
10 | [-2^10, 2^10] = [-1_024, 1_024] |
i32 |
26 | [-2^26, 2^26] = [-67_108_864, 67_108_864] |
i64 |
58 | [-2^58, 2^58] = [-288_230_376_151_711_744, 288_230_376_151_711_744] |
These limits apply to every endpoint and control point supplied directly to the integer API. The storage types can represent larger values, but such input is not guaranteed to keep intermediate curve coefficients inside their safe range. Translate and uniformly scale larger integer geometry before invoking the kernel.
CurveBuilder creates float paths without quantization. CurveConverter then
chooses a safe integer scale for the complete source shape using the table
above, or validates an explicit scale supplied by the caller against the same
range.
It is important to convert all contours participating in one overlay with the same adapter. The example below builds the subject and clip as two contours, converts them together, and separates them afterwards.
use i_curve::{
float::curve::{
arc::{Ellipse, EllipticArc},
builder::CurveBuilder,
converter::CurveConverter,
},
int::{bool::overlay::IntCurveOverlay, curve::shape::CurveShape},
};
use i_overlay::core::{
fill_rule::FillRule, overlay::ShapeType, overlay_rule::OverlayRule,
};
let ellipse = EllipticArc {
ellipse: Ellipse {
center: [0.0_f64, 0.0],
radius_x: 80.0,
radius_y: 50.0,
rotation: 0.25,
},
start_angle: 0.0,
sweep_angle: core::f64::consts::TAU,
};
let source = CurveBuilder::new()
// Subject contour.
.move_to(ellipse.start_point()).unwrap()
.arc_to(ellipse).unwrap()
.close_contour().unwrap()
// Clip contour.
.move_to([-20.0, -70.0]).unwrap()
.line_to([90.0, -70.0]).unwrap()
.line_to([90.0, 70.0]).unwrap()
.line_to([-20.0, 70.0]).unwrap()
.close_contour().unwrap()
.build().unwrap();
let converter = CurveConverter::<_, i32>::new(source);
let (adapter, int_shape) = converter.into_parts();
let mut contours = int_shape.contours.into_iter();
let subject = CurveShape {
contours: vec![contours.next().unwrap()],
};
let clip = CurveShape {
contours: vec![contours.next().unwrap()],
};
let mut overlay = IntCurveOverlay::new(8);
overlay.add_shape(subject, ShapeType::Subject);
overlay.add_shape(clip, ShapeType::Clip);
let result = overlay.overlay(OverlayRule::Difference, FillRule::NonZero);
// Keep the adapter to map result coordinates back to the float coordinate
// system. Curved segments retain their control points and rational weights.
let first_point = adapter.int_to_float(&result[0].contours[0].start);
println!("first result point: {first_point:?}");For a fixed scale, use
CurveConverter::<_, i32>::try_with_scale(source, scale). A larger scale
preserves smaller details but reduces the available coordinate range.
EllipticArc is a convenient input description: center, radii, rotation,
start angle, and sweep angle. Shapes themselves store only RationalArc.
CurveBuilder::arc_to converts an elliptic arc into connected, XY-monotone
rational quadratic pieces.
use i_curve::float::curve::arc::{Ellipse, EllipticArc};
let arc = EllipticArc {
ellipse: Ellipse {
center: [0.0_f64, 0.0],
radius_x: 100.0,
radius_y: 60.0,
rotation: 0.3,
},
start_angle: 0.0,
sweep_angle: core::f64::consts::PI,
};
let pieces = arc.to_rational_arcs().unwrap();
for pair in pieces.windows(2) {
assert_eq!(pair[0].end_point(), pair[1].start_point());
}
// This succeeds only while the authoritative rational geometry still lies
// on its supporting ellipse within the requested tolerance.
let elliptic = pieces[0].try_to_elliptic_arc(1.0e-10);
assert!(elliptic.is_some());For RationalArc, control_points and weights are the authoritative
geometry. The ellipse and angles are supporting metadata. Intersections and
snapping may move an endpoint away from the supporting ellipse, while the
rational curve remains connected to its neighbouring segments. Consequently,
try_to_elliptic_arc may return None after a boolean operation. This avoids
silently introducing small connector lines or gaps.
Use CurveBuilder::rational_arc_to to add an already materialized rational arc
without reconstructing its endpoints or weights from ellipse metadata.
- Float input is quantized into a shared integer coordinate system.
- Curves are decomposed into simple canonical pieces.
- Intersections split the curves until their chords preserve the same planar topology as the curved geometry.
- The chords are processed by
iOverlay, together with provenance identifying their source curves. - Consecutive output edges with common provenance are recomposed into maximal line, Bezier, or rational-arc runs.
The polygon topology is therefore resolved by the robust integer overlay engine, while the output boundary retains curved segments instead of being flattened into polylines.
- The API is experimental and currently exposes the integer overlay pipeline.
- All input contours must be closed.
CurveBuilderreports unclosed paths. - Features smaller than the selected integer grid can collapse during conversion.
- Conversion from a snapped
RationalArcback toEllipticArcis intentionally fallible.
iCurve is distributed under the MIT License. See LICENSE.