Skip to main content

A comprehensive 2D Bézier curve management library with constraints and advanced point handling

Project description

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 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 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 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

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) 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bezier_manager-0.3.0.tar.gz (52.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bezier_manager-0.3.0-py3-none-any.whl (35.4 kB view details)

Uploaded Python 3

File details

Details for the file bezier_manager-0.3.0.tar.gz.

File metadata

  • Download URL: bezier_manager-0.3.0.tar.gz
  • Upload date:
  • Size: 52.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for bezier_manager-0.3.0.tar.gz
Algorithm Hash digest
SHA256 1ec670160d673d08b03aeb6345629ff4d4ca7f6b1adec4cb73f3877f7b26f3f2
MD5 4e674e58807b9d1f15a954153c8ec0d2
BLAKE2b-256 52218755e9adaaf5052ef2223589f44161d2b0bc213848b01f42ad1b9569da1f

See more details on using hashes here.

File details

Details for the file bezier_manager-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: bezier_manager-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 35.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for bezier_manager-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 80fedbf8165fd6997ccac9b9f14d3ed7c27cfa4c6b41554a0ae5a39944c2e5d5
MD5 8e4950ce03d123658a11937c3b83d83d
BLAKE2b-256 5cd6bb4eb072ed2812575b623888bd33132a440d08354a9d825268c22e19cc14

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page