mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Added proportion_from_point method to VMobject, which uses some new bezier curve utilities (#1469)
* Add function to get the bezier curve parameter given a point and the bezier's control points. Add function to determine if a point lies on a bezier curve. * Add method to get_nth_curve_length Add method to get the proportion along the path of the VMobject a particular point is. * Added type hints and docstrings to added bezier functions. Added better docstrings to added VMobject methods. Ran black on both files. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply some of @friedkeenan 's suggestions * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix an incorrect import. * Changed root filter to be more readable. Removed CLOSED_THRESHOLD as it was unused everywhere else in the library Added round_to parameter to replace CLOSED_THRESHOLD Set default value of round_to to 1e-6, which is the same as VMobject.tolerance_for_point_equality * Simplify polynomial generating method. * Make bezier_params_from_point work for bezier curves of any degree. * More descriptive names for k and l * Apply suggestions from @friedkeenan's code review Formatting changes; code cleanliness. Made `point_lies_on_bezier_curve` pass `round_to` to `bezier_params_from_point` Co-authored-by: friedkeenan <friedkeenan@protonmail.com> * Formatting changes as suggested by @friedkeenan. * Rename bezier_params_from_point to proportions_along_bezier_curve_for_point Use proportions_along_bezier_curve_for_point alone and not point_lies_on_bezier in proportion_from_point * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use proper typings for proportions_along_bezier_curve_for_point Co-authored-by: friedkeenan <friedkeenan@protonmail.com> * Add a comment paragraph explaining how proportion_from_point works. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix a bug where certain points are wrongly identified as being on the curve. Thanks @Skaft ! * Moved around and added explanation comments. Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: friedkeenan <friedkeenan@protonmail.com>
This commit is contained in:
parent
446c42d8d3
commit
c11b649e9a
3 changed files with 202 additions and 2 deletions
|
|
@ -1917,6 +1917,9 @@ class Mobject:
|
|||
def point_from_proportion(self, alpha):
|
||||
raise NotImplementedError("Please override in a child class.")
|
||||
|
||||
def proportion_from_point(self, point):
|
||||
raise NotImplementedError("Please override in a child class.")
|
||||
|
||||
def get_pieces(self, n_pieces):
|
||||
template = self.copy()
|
||||
template.submobjects = []
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ from ...utils.bezier import (
|
|||
integer_interpolate,
|
||||
interpolate,
|
||||
partial_bezier_points,
|
||||
point_lies_on_bezier,
|
||||
proportions_along_bezier_curve_for_point,
|
||||
)
|
||||
from ...utils.color import BLACK, WHITE, color_to_rgba
|
||||
from ...utils.deprecation import deprecated, deprecated_params
|
||||
|
|
@ -1030,6 +1032,28 @@ class VMobject(Mobject):
|
|||
"""
|
||||
return bezier(self.get_nth_curve_points(n))
|
||||
|
||||
def get_nth_curve_length(
|
||||
self, n: int, sample_points: Optional[int] = None
|
||||
) -> float:
|
||||
"""Returns the (approximate) length of the nth curve.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n
|
||||
The index of the desired curve.
|
||||
sample_points
|
||||
The number of points to sample to find the length.
|
||||
|
||||
Returns
|
||||
-------
|
||||
length : :class:`float`
|
||||
The length of the nth curve.
|
||||
"""
|
||||
|
||||
_, length = self.get_nth_curve_function_with_length(n, sample_points)
|
||||
|
||||
return length
|
||||
|
||||
def get_nth_curve_function_with_length(
|
||||
self, n: int, sample_points: Optional[int] = None
|
||||
) -> typing.Tuple[typing.Callable[[float], np.ndarray], float]:
|
||||
|
|
@ -1155,6 +1179,61 @@ class VMobject(Mobject):
|
|||
|
||||
current_length += length
|
||||
|
||||
def proportion_from_point(
|
||||
self,
|
||||
point: typing.Iterable[typing.Union[float, int]],
|
||||
) -> float:
|
||||
"""Returns the proportion along the path of the :class:`VMobject`
|
||||
a particular given point is at.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
point
|
||||
The Cartesian coordinates of the point which may or may not lie on the :class:`VMobject`
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The proportion along the path of the :class:`VMobject`.
|
||||
|
||||
Raises
|
||||
------
|
||||
:exc:`ValueError`
|
||||
If ``point`` does not lie on the curve.
|
||||
:exc:`Exception`
|
||||
If the :class:`VMobject` has no points.
|
||||
"""
|
||||
self.throw_error_if_no_points()
|
||||
|
||||
# Iterate over each bezier curve that the ``VMobject`` is composed of, checking
|
||||
# if the point lies on that curve. If it does not lie on that curve, add
|
||||
# the whole length of the curve to ``target_length`` and move onto the next
|
||||
# curve. If the point does lie on the curve, add how far along the curve
|
||||
# the point is to ``target_length``.
|
||||
# Then, divide ``target_length`` by the total arc length of the shape to get
|
||||
# the proportion along the ``VMobject`` the point is at.
|
||||
|
||||
num_curves = self.get_num_curves()
|
||||
total_length = self.get_arc_length()
|
||||
target_length = 0
|
||||
for n in range(num_curves):
|
||||
control_points = self.get_nth_curve_points(n)
|
||||
length = self.get_nth_curve_length(n)
|
||||
proportions_along_bezier = proportions_along_bezier_curve_for_point(
|
||||
point, control_points
|
||||
)
|
||||
if len(proportions_along_bezier) > 0:
|
||||
proportion_along_nth_curve = max(proportions_along_bezier)
|
||||
target_length += length * proportion_along_nth_curve
|
||||
break
|
||||
target_length += length
|
||||
else:
|
||||
raise ValueError(f"Point {point} does not lie on this curve.")
|
||||
|
||||
alpha = target_length / total_length
|
||||
|
||||
return alpha
|
||||
|
||||
def get_anchors_and_handles(self) -> typing.Iterable[np.ndarray]:
|
||||
"""Returns anchors1, handles1, handles2, anchors2,
|
||||
where (anchors1[i], handles1[i], handles2[i], anchors2[i])
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ __all__ = [
|
|||
"get_smooth_cubic_bezier_handle_points",
|
||||
"diag_to_matrix",
|
||||
"is_closed",
|
||||
"proportions_along_bezier_curve_for_point",
|
||||
"point_lies_on_bezier",
|
||||
]
|
||||
|
||||
|
||||
import typing
|
||||
from functools import reduce
|
||||
|
||||
import numpy as np
|
||||
from scipy import linalg
|
||||
|
|
@ -24,8 +27,6 @@ from scipy import linalg
|
|||
from ..utils.simple_functions import choose
|
||||
from ..utils.space_ops import cross2d, find_intersection
|
||||
|
||||
CLOSED_THRESHOLD: float = 0.001
|
||||
|
||||
|
||||
def bezier(
|
||||
points: np.ndarray,
|
||||
|
|
@ -367,3 +368,120 @@ def get_quadratic_approximation_of_cubic(a0, h0, h1, a1):
|
|||
|
||||
def is_closed(points: typing.Tuple[np.ndarray, np.ndarray]) -> bool:
|
||||
return np.allclose(points[0], points[-1])
|
||||
|
||||
|
||||
def proportions_along_bezier_curve_for_point(
|
||||
point: typing.Iterable[typing.Union[float, int]],
|
||||
control_points: typing.Iterable[typing.Iterable[typing.Union[float, int]]],
|
||||
round_to: typing.Optional[typing.Union[float, int]] = 1e-6,
|
||||
) -> np.ndarray:
|
||||
"""Obtains the proportion along the bezier curve corresponding to a given point
|
||||
given the bezier curve's control points.
|
||||
|
||||
The bezier polynomial is constructed using the coordinates of the given point
|
||||
as well as the bezier curve's control points. On solving the polynomial for each dimension,
|
||||
if there are roots common to every dimension, those roots give the proportion along the
|
||||
curve the point is at. If there are no real roots, the point does not lie on the curve.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
point
|
||||
The Cartesian Coordinates of the point whose parameter
|
||||
should be obtained.
|
||||
control_points
|
||||
The Cartesian Coordinates of the ordered control
|
||||
points of the bezier curve on which the point may
|
||||
or may not lie.
|
||||
round_to
|
||||
A float whose number of decimal places all values
|
||||
such as coordinates of points will be rounded.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray[float]
|
||||
List containing possible parameters (the proportions along the bezier curve)
|
||||
for the given point on the given bezier curve.
|
||||
This usually only contains one or zero elements, but if the
|
||||
point is, say, at the beginning/end of a closed loop, may return
|
||||
a list with more than 1 value, corresponding to the beginning and
|
||||
end etc. of the loop.
|
||||
|
||||
Raises
|
||||
------
|
||||
:class:`ValueError`
|
||||
When ``point`` and the control points have different shapes.
|
||||
"""
|
||||
# Method taken from
|
||||
# http://polymathprogrammer.com/2012/04/03/does-point-lie-on-bezier-curve/
|
||||
|
||||
if not all(np.shape(point) == np.shape(c_p) for c_p in control_points):
|
||||
raise ValueError(
|
||||
f"Point {point} and Control Points {control_points} have different shapes."
|
||||
)
|
||||
|
||||
control_points = np.array(control_points)
|
||||
n = len(control_points) - 1
|
||||
|
||||
roots = []
|
||||
for dim, coord in enumerate(point):
|
||||
control_coords = control_points[:, dim]
|
||||
terms = []
|
||||
for term_power in range(n, -1, -1):
|
||||
outercoeff = choose(n, term_power)
|
||||
term = []
|
||||
sign = 1
|
||||
for subterm_num in range(term_power, -1, -1):
|
||||
innercoeff = choose(term_power, subterm_num) * sign
|
||||
subterm = innercoeff * control_coords[subterm_num]
|
||||
if term_power == 0:
|
||||
subterm -= coord
|
||||
term.append(subterm)
|
||||
sign *= -1
|
||||
terms.append(outercoeff * sum(np.array(term)))
|
||||
if all(term == 0 for term in terms):
|
||||
# Then both Bezier curve and Point lie on the same plane.
|
||||
# Roots will be none, but in this specific instance, we don't need to consider that.
|
||||
continue
|
||||
bezier_polynom = np.polynomial.Polynomial(terms[::-1])
|
||||
polynom_roots = bezier_polynom.roots()
|
||||
if len(polynom_roots) > 0:
|
||||
polynom_roots = np.around(polynom_roots, int(np.log10(1 / round_to)))
|
||||
roots.append(polynom_roots)
|
||||
|
||||
roots = [[root for root in rootlist if root.imag == 0] for rootlist in roots]
|
||||
roots = reduce(np.intersect1d, roots) # Get common roots.
|
||||
roots = np.array([r.real for r in roots])
|
||||
return roots
|
||||
|
||||
|
||||
def point_lies_on_bezier(
|
||||
point: typing.Iterable[typing.Union[float, int]],
|
||||
control_points: typing.Iterable[typing.Iterable[typing.Union[float, int]]],
|
||||
round_to: typing.Optional[typing.Union[float, int]] = 1e-6,
|
||||
) -> bool:
|
||||
"""Checks if a given point lies on the bezier curves with the given control points.
|
||||
|
||||
This is done by solving the bezier polynomial with the point as the constant term; if
|
||||
any real roots exist, the point lies on the bezier curve.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
point
|
||||
The Cartesian Coordinates of the point to check.
|
||||
control_points
|
||||
The Cartesian Coordinates of the ordered control
|
||||
points of the bezier curve on which the point may
|
||||
or may not lie.
|
||||
round_to
|
||||
A float whose number of decimal places all values
|
||||
such as coordinates of points will be rounded.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the point lies on the curve.
|
||||
"""
|
||||
|
||||
roots = proportions_along_bezier_curve_for_point(point, control_points, round_to)
|
||||
|
||||
return len(roots) > 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue