from manim import *
from typing import Optional
from manim_pymunk.constraints import VConstraint
from pymunk.constraints import RotaryLimitJoint
from pymunk import Space
import numpy as np
[docs]
class VRotaryLimitJoint(VConstraint):
"""Initializes a Rotary Limit Joint constraint between two Mobjects.
This joint constrains the relative rotation between two bodies to stay
within a specific angular range. It acts as a physical stop when the
bodies reach the minimum or maximum angle limits.
Parameters
----------
a_mob
The first Mobject (the reference body).
b_mob
The second Mobject (the constrained body).
min_angle
The minimum allowed relative angle (in radians).
max_angle
The maximum allowed relative angle (in radians).
arc_indicator_class
The Manim class used to visualize the angular limits (typically Arc).
Set to None to hide the visual representation.
arc_indicator_config
Configuration dictionary for the styling of the visual arc.
Examples
--------
.. manim:: VRotaryLimitJointExample
from manim_pymunk import *
class VRotaryLimitJointExample(SpaceScene):
def construct(self):
static_dot = Dot(ORIGIN)
square = Square().move_to(static_dot)
square2 = Square().move_to(static_dot.get_center() + UP * 2).scale(0.5)
constraints = [
VPinJoint(static_dot, square),
VPinJoint(
square,
square2,
anchor_a_local=square.get_corner(UR) - square.get_center(),
distance=2,
connect_line_class=Line,
),
VRotaryLimitJoint(
static_dot,
square2,
min_angle=-PI / 6,
max_angle=PI / 6,
),
]
self.add_static_body(static_dot)
self.add_dynamic_body(square, square2, angular_velocity=PI * 2)
self.add_shapes_filter(static_dot, square, square2, group=2)
self.add_constraints(*constraints)
self.wait(3)
"""
def __init__(
self,
a_mob: Mobject,
b_mob: Mobject,
min_angle: float = -PI / 4,
max_angle: float = PI / 4,
arc_indicator_class: Optional[Arc] = Arc,
arc_indicator_config: dict = {
"radius": 0.5,
"color": YELLOW,
"stroke_width": 2,
},
**kwargs,
):
super().__init__(**kwargs)
self.a_mob = a_mob
self.b_mob = b_mob
# 物理限制参数
self.min_angle = min_angle
self.max_angle = max_angle
self.arc_indicator_class = arc_indicator_class
self.arc_indicator_config = arc_indicator_config
self.arc_indicator_a: Optional[VMobject] = None
self.arc_indicator_b: Optional[VMobject] = None
self.constraint: Optional[RotaryLimitJoint] = None
def __check_data(self):
"""Verify the validity of constraint parameters."""
pass
[docs]
def install(self, space: Space):
a_body = getattr(self.a_mob, "body", None)
b_body = getattr(self.b_mob, "body", None)
if not a_body or not b_body:
raise ValueError("VRotaryLimitJoint 连接的物体必须先执行 add_dynamic_body")
self.constraint = RotaryLimitJoint(
a_body, b_body, self.min_angle, self.max_angle
)
if self.arc_indicator_class:
self.arc_indicator_a = self.arc_indicator_class(
angle=0, **self.arc_indicator_config
)
self.arc_indicator_b = self.arc_indicator_class(
angle=0, **self.arc_indicator_config
)
self.add(self.arc_indicator_a, self.arc_indicator_b)
space.add(self.constraint)
self.add_updater(self.mob_updater)
[docs]
def mob_updater(self, mob, dt):
"""Visual control updater"""
if not self.constraint:
return
a_body = self.constraint.a
b_body = self.constraint.b
wa = a_body.position
wb = b_body.position
p1 = np.array([wa.x, wa.y, 0])
p2 = np.array([wb.x, wb.y, 0])
diff = p2 - p1
dist = np.linalg.norm(diff)
if dist < 0.001:
unit_vec = np.array([1, 0, 0])
else:
unit_vec = diff / dist
rel_angle = b_body.angle - a_body.angle
display_angle = rel_angle if abs(rel_angle) > 0.005 else 0.005
buff = 0.3
line_angle = np.arctan2(unit_vec[1], unit_vec[0]) # 连线的绝对角度
if self.arc_indicator_a:
new_arc_a = self.arc_indicator_class(
angle=display_angle, **self.arc_indicator_config
)
target_pos_a = p1 - unit_vec * (self.a_mob.get_width() / 2 + buff)
new_arc_a.move_to(target_pos_a)
new_arc_a.rotate(
line_angle - display_angle / 2 + PI, about_point=target_pos_a
)
self.arc_indicator_a.become(new_arc_a)
if self.arc_indicator_b:
new_arc_b = self.arc_indicator_class(
angle=-display_angle, **self.arc_indicator_config
)
target_pos_b = p2 + unit_vec * (self.b_mob.get_width() / 2 + buff)
new_arc_b.move_to(target_pos_b)
new_arc_b.rotate(
line_angle - (-display_angle) / 2, about_point=target_pos_b
)
self.arc_indicator_b.become(new_arc_b)