Skip to content

Commit 9a18464

Browse files
committed
Added callback functions for DampedSpring and DampedRotarySpring to allow customized force/torque calculations
1 parent ee6b278 commit 9a18464

7 files changed

+137
-1
lines changed

CHANGELOG.rst

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55
..
66
Fix for 2 static bodies that are changed to dynamic and are attached to constraints
77
Updated the fork of Chipmunk2D used by Pymunk, fixing a number of issues, including maxForce on Spring constaints.
8+
Added callback functions for DampedSpring and DampedRotarySpring to allow customized force/torque calculations.
89
910
Pymunk 6.7.0 (2024-05-01)
1011
-------------------------

pymunk/_callbacks.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,22 @@ def ext_cpConstraintPostSolveFunc(
292292
constraint._post_solve_func(constraint, constraint.a.space)
293293

294294

295+
@ffi.def_extern()
296+
def ext_cpDampedSpringForceFunc(cp_constraint: ffi.CData, dist: float) -> float:
297+
constraint = ffi.from_handle(lib.cpConstraintGetUserData(cp_constraint))
298+
return constraint._force_func(constraint, dist)
299+
300+
301+
@ffi.def_extern()
302+
def ext_cpDampedRotarySpringTorqueFunc(
303+
cp_constraint: ffi.CData, relative_angle: float
304+
) -> float:
305+
constraint = ffi.from_handle(lib.cpConstraintGetUserData(cp_constraint))
306+
return constraint._torque_func(constraint, relative_angle)
307+
295308

296309
# Pickle of Arbiters
297310
@ffi.def_extern()
298311
def ext_cpArbiterIteratorFunc(_arbiter, data): # type: ignore
299312
arbiters = ffi.from_handle(data)
300-
arbiters.append(_arbiter)
313+
arbiters.append(_arbiter)

pymunk/constraints.py

+66
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282

8383
_logger = logging.getLogger(__name__)
8484

85+
_TorqueFunc = Callable[["DampedRotarySpring", float], float]
86+
_ForceFunc = Callable[["DampedSpring", float], float]
87+
8588

8689
class Constraint(PickleMixin, TypingAttrMixing, object):
8790
"""Base class of all constraints.
@@ -582,6 +585,8 @@ class DampedSpring(Constraint):
582585
"damping",
583586
]
584587

588+
_pickle_attrs_skip = Constraint._pickle_attrs_skip + ["_force_func"]
589+
585590
def __init__(
586591
self,
587592
a: "Body",
@@ -672,6 +677,34 @@ def _set_damping(self, damping: float) -> None:
672677
doc="""How soft to make the damping of the spring.""",
673678
)
674679

680+
@staticmethod
681+
def spring_force(spring: "DampedSpring", dist: float) -> float:
682+
"""Default damped spring force function."""
683+
return lib.defaultSpringForce(spring._constraint, dist)
684+
685+
def _set_force_func(self, func: _ForceFunc) -> None:
686+
if func == DampedSpring.spring_force:
687+
lib.cpDampedSpringSetSpringForceFunc(
688+
self._constraint,
689+
ffi.cast(
690+
"cpDampedSpringForceFunc", ffi.addressof(lib, "defaultSpringForce")
691+
),
692+
)
693+
else:
694+
self._force_func = func
695+
lib.cpDampedSpringSetSpringForceFunc(
696+
self._constraint, lib.ext_cpDampedSpringForceFunc
697+
)
698+
699+
force_func = property(
700+
fset=_set_force_func,
701+
doc="""The force callback function.
702+
703+
The force callback function is called each time step and is used to
704+
calculate the force of the spring (exclusing any damping).
705+
""",
706+
)
707+
675708

676709
class DampedRotarySpring(Constraint):
677710
"""DampedRotarySpring works like the DammpedSpring but in a angular fashion."""
@@ -682,6 +715,10 @@ class DampedRotarySpring(Constraint):
682715
"damping",
683716
]
684717

718+
_pickle_attrs_skip = Constraint._pickle_attrs_skip + ["_torque_func"]
719+
720+
_torque_func: Optional[_TorqueFunc] = None
721+
685722
def __init__(
686723
self, a: "Body", b: "Body", rest_angle: float, stiffness: float, damping: float
687724
) -> None:
@@ -733,6 +770,35 @@ def _set_damping(self, damping: float) -> None:
733770
doc="""How soft to make the damping of the spring.""",
734771
)
735772

773+
@staticmethod
774+
def spring_torque(spring: "DampedRotarySpring", relative_angle: float) -> float:
775+
"""Default damped rotary spring torque function."""
776+
return lib.defaultSpringTorque(spring._constraint, relative_angle)
777+
778+
def _set_torque_func(self, func: _TorqueFunc) -> None:
779+
if func == DampedRotarySpring.spring_torque:
780+
lib.cpDampedRotarySpringSetSpringTorqueFunc(
781+
self._constraint,
782+
ffi.cast(
783+
"cpDampedRotarySpringTorqueFunc",
784+
ffi.addressof(lib, "defaultSpringTorque"),
785+
),
786+
)
787+
else:
788+
self._torque_func = func
789+
lib.cpDampedRotarySpringSetSpringTorqueFunc(
790+
self._constraint, lib.ext_cpDampedRotarySpringTorqueFunc
791+
)
792+
793+
torque_func = property(
794+
fset=_set_torque_func,
795+
doc="""The torque callback function.
796+
797+
The torque callback function is called each time step and is used to
798+
calculate the torque of the spring (exclusing any damping).
799+
""",
800+
)
801+
736802

737803
class RotaryLimitJoint(Constraint):
738804
"""RotaryLimitJoint constrains the relative rotations of two bodies."""

pymunk/tests/test_constraint.py

+42
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,27 @@ def testDamping(self) -> None:
338338
j.damping = 2
339339
self.assertEqual(j.damping, 2)
340340

341+
def testForceFunc(self) -> None:
342+
s = p.Space()
343+
a, b = p.Body(10, 10), p.Body(20, 20)
344+
b.position = 0, 100
345+
j = DampedSpring(a, b, (0, 0), (0, 0), 0, 1, 0)
346+
s.add(a, b, j)
347+
348+
def f(spring: p.DampedSpring, dist: float) -> float:
349+
self.assertEqual(spring, j)
350+
self.assertEqual(dist, 100)
351+
352+
return 1
353+
354+
j.force_func = f
355+
s.step(1)
356+
self.assertEqual(j.impulse, 1)
357+
358+
j.force_func = DampedSpring.spring_force
359+
s.step(1)
360+
self.assertAlmostEqual(j.impulse, -100.15)
361+
341362
def testPickle(self) -> None:
342363
a, b = p.Body(10, 10), p.Body(20, 20)
343364
j = DampedSpring(a, b, (1, 2), (3, 4), 5, 6, 7)
@@ -376,6 +397,27 @@ def testDamping(self) -> None:
376397
j.damping = 2
377398
self.assertEqual(j.damping, 2)
378399

400+
def testTorqueFunc(self) -> None:
401+
s = p.Space()
402+
a, b = p.Body(10, 10), p.Body(20, 20)
403+
b.angle = 2
404+
j = DampedRotarySpring(a, b, 0, 10, 0)
405+
s.add(a, b, j)
406+
407+
def f(spring: p.DampedRotarySpring, relative_angle: float) -> float:
408+
self.assertEqual(spring, j)
409+
self.assertEqual(relative_angle, -2)
410+
411+
return 1
412+
413+
j.torque_func = f
414+
s.step(1)
415+
self.assertEqual(j.impulse, 1)
416+
417+
j.torque_func = DampedRotarySpring.spring_torque
418+
s.step(1)
419+
self.assertAlmostEqual(j.impulse, -21.5)
420+
379421
def testPickle(self) -> None:
380422
a, b = p.Body(10, 10), p.Body(20, 20)
381423
j = DampedRotarySpring(a, b, 1, 2, 3)

pymunk_cffi/callbacks_cdef.h

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ extern "Python" {
33
// cpConstraint.h
44
void ext_cpConstraintPreSolveFunc(cpConstraint *constraint, cpSpace *space);
55
void ext_cpConstraintPostSolveFunc(cpConstraint *constraint, cpSpace *space);
6+
cpFloat ext_cpDampedSpringForceFunc(cpConstraint *constraint, cpFloat dist);
7+
cpFloat ext_cpDampedRotarySpringTorqueFunc(cpConstraint *constraint, cpFloat relative_angle);
68

79

810
// cpBody.h

pymunk_cffi/extensions.c

+8
Original file line numberDiff line numberDiff line change
@@ -447,4 +447,12 @@ cpArbiter *cpArbiterNew(cpShape *a, cpShape *b)
447447
cpContact *cpContactArrAlloc(int count)
448448
{
449449
return (cpContact *)cpcalloc(count, sizeof(struct cpContact));
450+
}
451+
452+
cpFloat defaultSpringForce(cpDampedSpring *spring, cpFloat dist){
453+
return (spring->restLength - dist)*spring->stiffness;
454+
}
455+
456+
cpFloat defaultSpringTorque(cpDampedRotarySpring *spring, cpFloat relativeAngle){
457+
return (relativeAngle - spring->restAngle)*spring->stiffness;
450458
}

pymunk_cffi/extensions_cdef.h

+4
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,7 @@ cpArbiter *cpArbiterNew(cpShape *a, cpShape *b);
9999
typedef struct cpContact cpContact;
100100

101101
cpContact *cpContactArrAlloc(int count);
102+
103+
cpFloat defaultSpringForce(cpDampedSpring *spring, cpFloat dist);
104+
105+
cpFloat defaultSpringTorque(cpDampedRotarySpring *spring, cpFloat relativeAngle);

0 commit comments

Comments
 (0)