Source code for buffalo_wings.airfoil.internal.bezier.bezier_curve_2d

"""Two-dimensional Bezier curves built from scalar Bernstein components."""

from __future__ import annotations

from collections.abc import Sequence

import numpy as np
from buffalo_core.typing import FloatArray, FloatInput, FloatScalar

from .bezier_common import (
    BezierDemotionContinuity,
    bezier_degree,
    demote_bezier_coefficients,
    evaluate_bernstein,
    evaluate_bernstein_u,
    evaluate_bernstein_uu,
    promote_bezier_coefficients,
    validate_bezier_coefficients,
)
from .bezier_curve_1d import BezierCurve1D

NUM_CONTROL_POINT_DIMENSIONS = 2
type ControlPoint2D = tuple[FloatScalar, FloatScalar]
type BezierCurve2DInput = Sequence[ControlPoint2D] | FloatArray


[docs] class BezierCurve2D: """Two-dimensional Bezier curve.""" def __init__( self, *, control_points: BezierCurve2DInput, ) -> None: """ Initialize one two-dimensional Bezier curve. Parameters ---------- control_points : Sequence[tuple[FloatScalar, FloatScalar]] | FloatArray Planar control points ordered from ``u = 0`` to ``u = 1``. Raises ------ ValueError If ``control_points`` is empty or does not have shape ``(n, 2)``. """ control_point_array = validate_bezier_coefficients( control_points, name="BezierCurve2D control_points", ) if control_point_array.size == 0: msg = "BezierCurve2D requires at least one control point." raise ValueError(msg) if ( control_point_array.ndim != NUM_CONTROL_POINT_DIMENSIONS or control_point_array.shape[1] != NUM_CONTROL_POINT_DIMENSIONS ): msg = "BezierCurve2D control_points must have shape (n, 2)." raise ValueError(msg) control_point_array.setflags(write=False) self._control_points = control_point_array
[docs] @classmethod def from_coordinate_curves( cls, *, x_curve: BezierCurve1D, y_curve: BezierCurve1D, ) -> BezierCurve2D: """ Build a two-dimensional Bezier curve from coordinate curves. Parameters ---------- x_curve : BezierCurve1D One-dimensional Bezier curve for the x-coordinate. y_curve : BezierCurve1D One-dimensional Bezier curve for the y-coordinate. Returns ------- BezierCurve2D Two-dimensional Bezier curve composed from the coordinate curves. Raises ------ ValueError If ``x_curve`` and ``y_curve`` do not share the same degree. """ if x_curve.degree != y_curve.degree: msg = "x_curve and y_curve must have the same Bezier degree." raise ValueError(msg) return cls( control_points=np.column_stack(( x_curve.coefficients, y_curve.coefficients, )) )
@property def control_points(self) -> FloatArray: """ Return the stored control points. Returns ------- FloatArray Read-only ``(n, 2)`` array of stored planar control points. """ return self._control_points @property def degree(self) -> int: """ Return the Bezier degree. Returns ------- int Polynomial degree of the two-dimensional Bezier curve. """ return bezier_degree(self.control_points) @property def x_curve(self) -> BezierCurve1D: """ Return the one-dimensional x-coordinate curve. Returns ------- BezierCurve1D One-dimensional Bezier curve for the x-coordinate. """ return BezierCurve1D(coefficients=self.control_points[:, 0]) @property def y_curve(self) -> BezierCurve1D: """ Return the one-dimensional y-coordinate curve. Returns ------- BezierCurve1D One-dimensional Bezier curve for the y-coordinate. """ return BezierCurve1D(coefficients=self.control_points[:, 1])
[docs] def xy(self, u: FloatInput) -> tuple[FloatArray, FloatArray]: """ Evaluate the two-dimensional Bezier curve. Parameters ---------- u : FloatInput Bezier parameter values. Returns ------- FloatArray X-coordinates evaluated at ``u``. FloatArray Y-coordinates evaluated at ``u``. """ points = evaluate_bernstein(self.control_points, u) return points[..., 0], points[..., 1]
[docs] def xy_u(self, u: FloatInput) -> tuple[FloatArray, FloatArray]: """ Evaluate first derivatives of the two-dimensional curve. Parameters ---------- u : FloatInput Bezier parameter values. Returns ------- FloatArray X-derivatives evaluated at ``u``. FloatArray Y-derivatives evaluated at ``u``. """ tangent = evaluate_bernstein_u(self.control_points, u) return tangent[..., 0], tangent[..., 1]
[docs] def xy_uu(self, u: FloatInput) -> tuple[FloatArray, FloatArray]: """ Evaluate second derivatives of the two-dimensional curve. Parameters ---------- u : FloatInput Bezier parameter values. Returns ------- FloatArray Second x-derivatives evaluated at ``u``. FloatArray Second y-derivatives evaluated at ``u``. """ curvature = evaluate_bernstein_uu(self.control_points, u) return curvature[..., 0], curvature[..., 1]
[docs] def promote_degree(self, *, count: int = 1) -> BezierCurve2D: """ Raise the Bezier degree without changing the represented curve. This operation is exact because it applies exact degree promotion independently to the x-coordinate and y-coordinate Bezier curves. Parameters ---------- count : int, default=1 Number of degree-elevation steps to apply. Returns ------- BezierCurve2D Elevated two-dimensional Bezier curve. """ return BezierCurve2D( control_points=promote_bezier_coefficients( self.control_points, count=count, ) )
[docs] def demote_degree( self, *, count: int = 1, continuity: BezierDemotionContinuity = "NOT_CONNECTED", ) -> BezierCurve2D: """ Lower the Bezier degree with constrained least-squares demotion. This operation is intentionally approximate. It applies constrained least-squares degree demotion to the shared control-point array and therefore does not guarantee exact preservation of the original curve, except in the special cases where the curve is exactly reducible to the requested lower degree. Parameters ---------- count : int, default=1 Number of degree-reduction steps to apply. continuity : {"NOT_CONNECTED", "C0", "C1", "C2"}, default="NOT_CONNECTED" Symmetric endpoint continuity to preserve during each demotion step. ``"NOT_CONNECTED"`` leaves the endpoints unconstrained. ``"C0"``, ``"C1"``, and ``"C2"`` preserve endpoint value, value-plus-first-derivative, and value-plus-first-two-derivatives, respectively, when the current degree allows it. Returns ------- BezierCurve2D Reduced-degree two-dimensional Bezier curve produced by a constrained least-squares approximate demotion. """ return BezierCurve2D( control_points=demote_bezier_coefficients( self.control_points, count=count, continuity=continuity, ) )
[docs] def control_polygon(self) -> FloatArray: """ Return the control polygon as a float64 array. Returns ------- FloatArray ``(n, 2)`` array of planar control points. """ return self.control_points.copy()