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 theBezier2DGeneratorrenderercurve_manager.constraints—CartesianConstraint2D,PolarConstraint2D,ConstraintManager2Dcurve_manager.curves—CubicBezier2D,QuadraticBezier2D,LineSegment2D,PiecewiseCurve2D, sampling/tangent opscurve_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 againstbezier.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 supportXSortedPoint2DKeeper: Maintains points sorted by x-coordinateUniqueXPoint2DKeeper: Enforces unique x-coordinates with efficient binary searchMonoHandleKeeper: Enforces mono-handle mode for smooth curvesMonotonousSegmentKeeper: 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
Canonical API (recommended)
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 renderinggenerator: 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_handleandright_handleare relative to point location by default
- Note:
remove_point(point_id): Remove a pointmove_point(point_id, tentative_location): Move a point (applies constraints and bounds)get_point(point_id): Get point dataget_all_points(): Get all points in orderadd_constraint(point_id, constraint): Add a constraint to a point, returns constraint IDremove_constraint(point_id, constraint): Remove an unlocked constraintlock_constraint(constraint_id): Lock a constraint to prevent removalunlock_constraint(constraint_id): Unlock a constraint to allow removalis_constraint_locked(constraint_id): Check if a constraint is lockedget_constraint_by_id(constraint_id): Get a constraint by its IDget_constraint_point_id(constraint_id): Get the point ID that a constraint is bound toset_bounds(bounds_x, bounds_y): Update global point boundsget_bounds(): Get current point boundsset_handle_bounds(handle_bounds_x, handle_bounds_y): Update global handle boundsget_handle_bounds(): Get current handle boundsis_within_bounds(x, y): Check if coordinates are within point boundsupdate_left_handle(point_id, left_handle, handle_coords=HandleCoords.RELATIVE): Update left handleupdate_right_handle(point_id, right_handle, handle_coords=HandleCoords.RELATIVE): Update right handleupdate_handles(point_id, left_handle, right_handle, handle_coords=HandleCoords.RELATIVE): Update both handlesadd_points_batch(points_data): Add multiple points efficientlyremove_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 coordinatex_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_limitssupports 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, andConstraintIdare also exported fromcurve_manager.constraints
BezierPoint2D
Data class representing a Bézier control point.
Attributes
location: Tuple[float, float] - The point's (x, y) positionleft_handle: Tuple[float, float] - Relative position of left handleright_handle: Tuple[float, float] - Relative position of right handlehandle_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.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - 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.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| curve_manager-0.4.0.tar.gz | 78.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| curve_manager-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 131.8 kB
Release files / curve_manager-0.4.0.tar.gz
| Download URL | curve_manager-0.4.0.tar.gz |
|---|---|
| Size | 78.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
424c6e2813660aea42aba7502c75704630aa273c60aa0170fe92ab392905b914
|
|
BLAKE2b-256 checksum How to use checksums |
50863b1c429b8e7d13ccc2c1614c64ec8e155e66f0ff76f60d94286ea535d47d
|
| 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.4.0-py3-none-any.whl
| Download URL | curve_manager-0.4.0-py3-none-any.whl |
|---|---|
| Size | 53.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6c8f5b77360b769a1370c15b6208559cfd4055dc6d0b1568ad70b099a92cc891
|
|
BLAKE2b-256 checksum How to use checksums |
01865767cc3206643ebf0af6cd9f667e29acf2d94901e379f5108b8cfe29cced
|
| 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.4.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.