Skip to main content

BezierManager (curve_manager)

A Python library for managing and editing 2D Bézier curves: control-point keepers, a constraint system (Cartesian/polar), constraint locking, bounds, curve geometry, and smoothing.

Two import namespaces are provided:

  • curve_manager — the canonical package:
    • curve_manager.editing — point/handle model, keepers, segment cache and the Bezier2DGenerator renderer
    • curve_manager.constraints — CartesianConstraint2D, PolarConstraint2D, ConstraintManager2D
    • curve_manager.curves — CubicBezier2D, QuadraticBezier2D, LineSegment2D, PiecewiseCurve2D, sampling/tangent ops
    • curve_manager.smoothing — midpoint-quadratic polyline smoothing
  • bezier.two_d — the legacy namespace, retained as silent compatibility shims that re-export the same canonical classes. Code written against bezier.two_d.* keeps working unchanged.

Features

Core Functionality

  • Bézier Point Management: Create, update, and manage 2D Bézier control points with handles
  • Multiple Keeper Types:
    • Point2DKeeper: Basic point management with constraint support
    • XSortedPoint2DKeeper: Maintains points sorted by x-coordinate
    • UniqueXPoint2DKeeper: Enforces unique x-coordinates with efficient binary search
    • MonoHandleKeeper: Enforces mono-handle mode for smooth curves
    • MonotonousSegmentKeeper: Enforces monotonous Y-coordinates in segments
  • Constraint System:
    • Cartesian constraints (fixed coordinates, axis limits)
    • Polar constraints (fixed radius/angle, range limits)
    • Custom center points for polar constraints
    • Multiple constraints per point
    • Constraint locking: Lock constraints to prevent removal
    • Constraint IDs: Track and manage constraints by unique IDs
  • Bounds Support:
    • Global point bounds (bounds_x, bounds_y) for canvas-like behavior
    • Handle bounds: Optional bounds for handle positions (handle_bounds_x, handle_bounds_y)
    • Independent control of point and handle bounds
  • Batch Operations: Efficient bulk point addition and removal
  • Curve Generation: Render smooth Bézier curves from control points

Advanced Features

  • Efficient X-Coordinate Tracking: O(log n) lookups using binary search
  • Handle Modes: Support for FREE, MIRRORED, and ALIGNED handle modes
  • Point Reordering: Relocate points in sequences while maintaining integrity
  • Range Queries: Find points within x-coordinate ranges
  • Protected Method Architecture: Extensible design with protected methods for subclass customization

Installation

pip install curve_manager

Or install from source:

git clone https://github.com/Grayjou/curve_manager.git
cd curve_manager
pip install -e .

Quick Start

The canonical import path is curve_manager — including the constraint system that was migrated here:

from curve_manager.constraints import (
    CartesianConstraint2D,
    PolarConstraint2D,
    ConstraintManager2D,
)
from curve_manager.editing.handles import HandleCoords

# Constraints are plain value objects you can compose per point.
cart = CartesianConstraint2D(x_limits=(50, 150))
polar = PolarConstraint2D(fixed_radius=100.0, center=(200, 200))

# The manager tracks constraints by ID and supports locking.
manager = ConstraintManager2D()
cid = manager.add_constraint(0, cart)
manager.lock_constraint(cid)
manager.remove_constraint(0, cart)      # locked -> silently ignored
x, y = manager.apply_constraints(0, 80, 120)
print(x, y)                             # (80.0, 120.0)

Curve geometry and smoothing are also canonical:

from curve_manager.curves import CubicBezier2D, sample
from curve_manager.smoothing import midpoint_quadratic_curve

curve = CubicBezier2D((0, 0), (1, 2), (2, -1), (3, 0))
pts = sample(curve, count=50)

smoothed = midpoint_quadratic_curve([(0, 0), (2, 0), (2, 2), (5, 2)])

So is the entire editor/keeper layer, including the render cache:

from curve_manager.editing import (
    MonoHandleKeeper,
    Point2DKeeper,
)
from curve_manager.editing.rendering import Bezier2DGenerator
from curve_manager.editing.segment_cache import SegmentCache

# A renderer (or a SegmentCache) can be injected into any keeper.
renderer = Bezier2DGenerator(samples_per_segment=30)
keeper = Point2DKeeper(bounds_x=(0, 800), bounds_y=(0, 600), generator=renderer)
keeper.add_point((100, 200), (-30, 0), (30, 0))
keeper.add_point((400, 300), (-50, 20), (50, -20))
curve = keeper.render_curve()

# The cache invalidation mirror is first-class and interchangeable:
cache = SegmentCache(samples_per_segment=30, generator=renderer)
assert keeper.segment_cache.samples_per_segment == cache.samples_per_segment

All bezier.two_d.* examples below keep working through compatibility shims that re-export the same canonical classes.

Basic Usage

from bezier.two_d.keeper import Point2DKeeper

# Create a keeper with global bounds (like a canvas)
keeper = Point2DKeeper(bounds_x=(0, 800), bounds_y=(0, 600))

# Add control points
p1 = keeper.add_point(
    location=(100, 200),
    left_handle=(-30, 0),
    right_handle=(30, 0)
)

p2 = keeper.add_point(
    location=(400, 300),
    left_handle=(-50, 20),
    right_handle=(50, -20)
)

# Move a point (respects bounds)
keeper.move_point(p1, (150, 250))

# Get all points
points = keeper.get_all_points()

Using Constraints

from bezier.two_d.keeper import Point2DKeeper
from bezier.two_d.constraints import CartesianConstraint2D, PolarConstraint2D

keeper = Point2DKeeper()

# Add a point
pid = keeper.add_point(location=(100, 100), left_handle=(0, 0), right_handle=(0, 0))

# Add Cartesian constraint (limit x to range)
constraint = CartesianConstraint2D(x_limits=(50, 150))
cid = keeper.add_constraint(pid, constraint)  # Returns constraint ID

# Add polar constraint (fixed radius from center)
polar = PolarConstraint2D(fixed_radius=100.0, center=(200, 200))
polar_cid = keeper.add_constraint(pid, polar)

# Lock a constraint to prevent removal
keeper.lock_constraint(cid)

# Try to remove - will be silently ignored because it's locked
keeper.remove_constraint(pid, constraint)

# Unlock to allow removal
keeper.unlock_constraint(cid)
keeper.remove_constraint(pid, constraint)  # Now it can be removed

# Move point - constraints are automatically applied
keeper.move_point(pid, (80, 120))

Handle Bounds

from bezier.two_d.keeper import Point2DKeeper
from bezier.two_d.point import HandleCoords

# Create keeper with bounds for both points and handles
keeper = Point2DKeeper(
    bounds_x=(0, 800),        # Point positions limited to 0-800
    bounds_y=(0, 600),        # Point positions limited to 0-600
    handle_bounds_x=(-100, 900),  # Handles can extend slightly beyond
    handle_bounds_y=(-100, 700)
)

# Add point with handles (handles are relative by default)
pid = keeper.add_point(
    location=(400, 300),
    left_handle=(-50, -30),   # Relative to point location
    right_handle=(50, 30),
    handle_coords=HandleCoords.RELATIVE  # Explicit (this is the default)
)

# Update handle with absolute coordinates
keeper.update_right_handle(
    pid,
    (500, 350),  # Absolute position
    handle_coords=HandleCoords.ABSOLUTE
)

# Handles exceeding bounds will be clamped automatically

X-Sorted Points

from bezier.two_d.x_sorted_keeper import XSortedPoint2DKeeper

# Points are automatically sorted by x-coordinate
keeper = XSortedPoint2DKeeper(bounds_x=(0, 1000), bounds_y=(0, 1000))

keeper.add_point((300, 100), (0, 0), (0, 0))
keeper.add_point((100, 200), (0, 0), (0, 0))  # Automatically inserted before first point
keeper.add_point((500, 150), (0, 0), (0, 0))  # Automatically inserted at end

# Points are always sorted by x
points = keeper.get_all_points()
assert points[0].location[0] < points[1].location[0] < points[2].location[0]

# Query points in x range
points_in_range = keeper.get_points_in_x_range(150, 400)

Unique X-Coordinates

from bezier.two_d.unique_x_keeper import UniqueXPoint2DKeeper

# Enforces unique x-coordinates
keeper = UniqueXPoint2DKeeper(tolerance=1e-9)

keeper.add_point((100, 100), (0, 0), (0, 0))
keeper.add_point((100, 200), (0, 0), (0, 0))  # X automatically adjusted

points = keeper.get_all_points()
# Second point's x will be slightly different (100 + tolerance * 2)

Generating Curves

from bezier.two_d.bezier_gen import Bezier2DGenerator
from bezier.two_d.point import BezierPoint2D

# Create control points
p0 = BezierPoint2D(location=(0, 0), left_handle=(0, 0), right_handle=(1, 0))
p1 = BezierPoint2D(location=(3, 3), left_handle=(-1, 0), right_handle=(1, 0))
p2 = BezierPoint2D(location=(6, 0), left_handle=(-1, 0), right_handle=(0, 0))

points = [p0, p1, p2]

# Generate smooth curve
generator = Bezier2DGenerator()
curve_points = generator.render_bezier_curve(points, samples_per_segment=20)

# curve_points is a list of (x, y) tuples along the curve
for x, y in curve_points:
    print(f"Point: ({x}, {y})")

Batch Operations

from bezier.two_d.keeper import Point2DKeeper

keeper = Point2DKeeper()

# Add multiple points efficiently
points_data = [
    ((100, 100), (-10, 0), (10, 0)),
    ((200, 150), (-15, 5), (15, -5)),
    ((300, 120), (-12, 3), (12, -3)),
]
point_ids = keeper.add_points_batch(points_data)

# Remove multiple points efficiently
keeper.remove_points_batch([point_ids[0], point_ids[2]])

API Reference

Point2DKeeper

Main class for managing Bézier points with constraints.

Constructor

Point2DKeeper(
    bounds_x=None, 
    bounds_y=None,
    handle_bounds_x=None,
    handle_bounds_y=None,
    samples_per_segment=20,
    generator=None
)
  • bounds_x: Optional tuple (min_x, max_x) for global x-axis bounds (point positions)
  • bounds_y: Optional tuple (min_y, max_y) for global y-axis bounds (point positions)
  • handle_bounds_x: Optional tuple (min_x, max_x) for handle x-axis bounds (absolute handle positions)
  • handle_bounds_y: Optional tuple (min_y, max_y) for handle y-axis bounds (absolute handle positions)
  • samples_per_segment: Number of samples per curve segment for rendering
  • generator: Optional custom Bezier2DGenerator instance

Methods

  • add_point(location, left_handle, right_handle, handle_mode=None, handle_coords=HandleCoords.RELATIVE): Add a new point
    • Note: left_handle and right_handle are relative to point location by default
  • remove_point(point_id): Remove a point
  • move_point(point_id, tentative_location): Move a point (applies constraints and bounds)
  • get_point(point_id): Get point data
  • get_all_points(): Get all points in order
  • add_constraint(point_id, constraint): Add a constraint to a point, returns constraint ID
  • remove_constraint(point_id, constraint): Remove an unlocked constraint
  • lock_constraint(constraint_id): Lock a constraint to prevent removal
  • unlock_constraint(constraint_id): Unlock a constraint to allow removal
  • is_constraint_locked(constraint_id): Check if a constraint is locked
  • get_constraint_by_id(constraint_id): Get a constraint by its ID
  • get_constraint_point_id(constraint_id): Get the point ID that a constraint is bound to
  • set_bounds(bounds_x, bounds_y): Update global point bounds
  • get_bounds(): Get current point bounds
  • set_handle_bounds(handle_bounds_x, handle_bounds_y): Update global handle bounds
  • get_handle_bounds(): Get current handle bounds
  • is_within_bounds(x, y): Check if coordinates are within point bounds
  • update_left_handle(point_id, left_handle, handle_coords=HandleCoords.RELATIVE): Update left handle
  • update_right_handle(point_id, right_handle, handle_coords=HandleCoords.RELATIVE): Update right handle
  • update_handles(point_id, left_handle, right_handle, handle_coords=HandleCoords.RELATIVE): Update both handles
  • add_points_batch(points_data): Add multiple points efficiently
  • remove_points_batch(point_ids): Remove multiple points efficiently

Constraints

The constraint system lives canonically in curve_manager.constraints (legacy bezier.two_d.constraints / bezier.two_d.constraint_manager re-export the same classes).

CartesianConstraint2D

CartesianConstraint2D(
    fixed_x=None,
    fixed_y=None,
    x_limits=None,
    y_limits=None,
    active=True
)
  • fixed_x / fixed_y: Override the corresponding coordinate
  • x_limits / y_limits: Clamp the coordinate to (lo, hi)
  • Fixed values take priority over limits

PolarConstraint2D

PolarConstraint2D(
    fixed_radius=None,
    fixed_angle=None,
    radius_limits=None,
    angle_limits=None,
    center=None,
    active=True
)
  • Constraints are applied in polar coordinates relative to center (origin by default)
  • angle_limits supports wrapping intervals (e.g. (170°, -170°))
  • Angles are normalized to [-pi, pi]; radii are clamped to be non-negative

ConstraintManager2D

manager = ConstraintManager2D()
cid = manager.add_constraint(point_id, constraint)   # returns unique ID
manager.lock_constraint(cid)                          # prevent removal
manager.unlock_constraint(cid)
manager.get_constraint_by_id(cid)
manager.get_constraint_point_id(cid)
x, y = manager.apply_constraints(point_id, x, y)
  • Multiple constraints per point; same constraint object reused across points keeps one ID
  • Locked constraints are silently ignored on removal
  • Constraint2D, Constraint2DProtocol, PointId, and ConstraintId are also exported from curve_manager.constraints

BezierPoint2D

Data class representing a Bézier control point.

Attributes

  • location: Tuple[float, float] - The point's (x, y) position
  • left_handle: Tuple[float, float] - Relative position of left handle
  • right_handle: Tuple[float, float] - Relative position of right handle
  • handle_mode: HandleMode - FREE, MIRRORED, or ALIGNED

Development

Running Tests

# Install dev dependencies
pip install -e ".[dev]"

# Run all tests
pytest

# Run with coverage
pytest --cov=bezier --cov-report=html

# Run specific test file
pytest tests/test_cartesian_constraints.py

Type Checking

mypy bezier

Code Quality

# Run linter
ruff check bezier

# Format code
ruff format bezier

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Changelog

See CHANGELOG.md for a list of changes in each version.

Acknowledgments

  • Inspired by Bézier curve editing in graphics applications
  • Built with efficiency and scalability in mind
  • Uses NumPy for efficient curve generation

Metadata

Release files for curve-manager 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for curve-manager 0.5.0
File Size Uploaded Provenance
curve_manager-0.5.0.tar.gz 89.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for curve-manager 0.5.0
File Interpreter ABI Platform
curve_manager-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 150.8 kB

Release files / curve_manager-0.5.0.tar.gz

Download URL curve_manager-0.5.0.tar.gz
Size 89.1 kB
Tags Source
SHA-256 checksum
How to use checksums
a4234ca92cbf6de18976e56846fc51b77651a0048e2ea48cfd48a2a38b213866
BLAKE2b-256 checksum
How to use checksums
81beaa7d356a5d95a72cb3b977c323c205617242e98f413105498b2914489be8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.0

Release files / curve_manager-0.5.0-py3-none-any.whl

Download URL curve_manager-0.5.0-py3-none-any.whl
Size 61.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d94de13010d1e213acc38117603148203dd5237eabb7926fa35b0ddc4e1a4c62
BLAKE2b-256 checksum
How to use checksums
a3cfca606579f151a9804af8efdf49e310fa0551b4a081a5b17bc8b10981b1f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.0

Security for curve-manager 0.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page