BezierManager
A comprehensive Python library for managing 2D Bézier curves with advanced constraint handling, efficient point management, and curve generation capabilities.
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 bezier-manager
Or install from source:
git clone https://github.com/Grayjou/BezierManager.git
cd BezierManager
pip install -e .
Quick Start
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
CartesianConstraint2D
CartesianConstraint2D(
fixed_x=None,
fixed_y=None,
x_limits=None,
y_limits=None,
active=True
)
PolarConstraint2D
PolarConstraint2D(
fixed_radius=None,
fixed_angle=None,
radius_limits=None,
angle_limits=None,
center=None,
active=True
)
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 bezier-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 | |
|---|---|---|---|---|
| bezier_manager-0.4.0.tar.gz | 52.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bezier_manager-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 88.9 kB
Release files / bezier_manager-0.4.0.tar.gz
| Download URL | bezier_manager-0.4.0.tar.gz |
|---|---|
| Size | 52.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
026478786c45fc766b534a81ac252de8a47be8d8371b55b74aae3d6fa29e9037
|
|
BLAKE2b-256 checksum How to use checksums |
c6d89fb21e38cc44805145d307df2ccbe8fe1c223099794d23646e8171658672
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Release files / bezier_manager-0.4.0-py3-none-any.whl
| Download URL | bezier_manager-0.4.0-py3-none-any.whl |
|---|---|
| Size | 36.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ceac53518ac1c6964ef8061b7a1991493795153ef50c229da13e69302ce8c8c8
|
|
BLAKE2b-256 checksum How to use checksums |
a878e5a69d344efe88f108ff4b9f03a64aee1bc5648c5b8a23d1059ce5226579
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Security for bezier-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.