Skip to content

Commit ce26585

Browse files
committed
Fix all(?) issues with Space.remove from separate callbacks #247
1 parent 9ed6c72 commit ce26585

File tree

4 files changed

+56
-84
lines changed

4 files changed

+56
-84
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ Changelog
33
=========
44

55
.. NEXT
6-
- Experimentally Build wheels for iOS
6+
- Experimentally Build wheels for iOS (Issue #276)
7+
- Fix all(?) issues calling remove in separate callbacks (Issue #247)
78
89
910

pymunk/_callbacks.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,18 +231,16 @@ def ext_cpCollisionSeparateFunc(
231231
_arb: ffi.CData, _space: ffi.CData, data: ffi.CData
232232
) -> None:
233233
handler = ffi.from_handle(data)
234-
235-
orig_locked = handler._space._locked
236-
handler._space._locked = True
234+
space = handler._space
235+
orig_locked = space._locked
236+
space._locked = True
237237
try:
238238
# this try is needed since a separate callback will be called
239239
# if a colliding object is removed, regardless if its in a
240240
# step or not. Meaning the unlock must succeed
241-
handler._separate(
242-
Arbiter(_arb, handler._space), handler._space, handler.data["separate"]
243-
)
241+
handler._separate(Arbiter(_arb, space), space, handler.data["separate"])
244242
finally:
245-
handler._space._locked = orig_locked
243+
space._locked = orig_locked
246244

247245

248246
# body.py

pymunk/space.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def spacefree(cp_space: ffi.CData) -> None:
133133
self._locked = False
134134

135135
self._add_later: set[_AddableObjects] = set()
136-
self._remove_later: set[_AddableObjects] = set()
136+
self._remove_later: dict[_AddableObjects, None] = dict()
137137
self._bodies_to_check: set[Body] = set()
138138

139139
@property
@@ -387,9 +387,20 @@ def remove(self, *objs: _AddableObjects) -> None:
387387
body, remove the joints and shapes attached to it.
388388
"""
389389
if self._locked:
390-
self._remove_later.update(objs)
390+
for o in objs:
391+
self._remove_later[o] = None
391392
return
392393

394+
self._remove(*objs)
395+
removed = set()
396+
while self._remove_later:
397+
to_remove, _ = self._remove_later.popitem()
398+
if to_remove not in removed:
399+
self._remove(to_remove)
400+
removed.add(to_remove)
401+
402+
def _remove(self, *objs: _AddableObjects) -> None:
403+
"""Unsafe internal remove, will not check space is unlocked."""
393404
for o in objs:
394405
if isinstance(o, Body):
395406
self._remove_body(o)
@@ -577,9 +588,13 @@ def step(self, dt: float) -> None:
577588
self._locked = False
578589
self.add(*self._add_later)
579590
self._add_later.clear()
580-
for obj in self._remove_later:
581-
self.remove(obj)
582-
self._remove_later.clear()
591+
592+
removed = set()
593+
while self._remove_later:
594+
to_remove, _ = self._remove_later.popitem()
595+
if to_remove not in removed:
596+
removed.add(to_remove)
597+
self._remove(to_remove)
583598

584599
for key in self._post_step_callbacks:
585600
self._post_step_callbacks[key](self)

pymunk/tests/test_space.py

Lines changed: 29 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -601,23 +601,6 @@ def separate(arb: p.Arbiter, space: p.Space, data: Any) -> None:
601601

602602
self.assertTrue(self.separated)
603603

604-
def testCollisionHandlerRemoveSeparateAdd(self) -> None:
605-
s = p.Space()
606-
b1 = p.Body(1, 10)
607-
c1 = p.Circle(b1, 10)
608-
c2 = p.Circle(s.static_body, 5)
609-
610-
s.add(b1, c1, c2)
611-
612-
def separate(*_: Any) -> None:
613-
s.add(p.Circle(s.static_body, 2))
614-
s.remove(c1)
615-
616-
s.on_collision(separate=separate)
617-
618-
s.step(1)
619-
s.remove(c1)
620-
621604
def testCollisionHandlerDefaultCallbacks(self) -> None:
622605
s = p.Space()
623606

@@ -644,79 +627,66 @@ def testCollisionHandlerDefaultCallbacks(self) -> None:
644627
for _ in range(10):
645628
s.step(0.1)
646629

647-
@unittest.skip("Existing bug in Pymunk. TODO: Fix bug and enable test")
648-
def testRemoveInSeparate(self) -> None:
630+
def testRemoveInSeparateWithinStep(self) -> None:
649631
s = p.Space()
650-
print("\n XXX start")
651632

652633
shape1 = p.Circle(s.static_body, 1)
653634
shape1.collision_type = 1
654635

655-
body2 = p.Body()
636+
body2 = p.Body(1, 1)
656637
shape2 = p.Circle(body2, 1)
657-
shape2.density = 1
658-
# shape1.collision_type = 2
659638

660-
body3 = p.Body()
639+
body3 = p.Body(1, 1)
661640
shape3 = p.Circle(body3, 1)
662-
shape3.density = 1
663-
# shape1.collision_type = 3
664641

665642
s.add(shape1, shape2, body2, shape3, body3)
666643

667644
def remove1(*_: Any) -> None:
668-
print("remove1")
669645
s.remove(shape1)
670646

671647
def remove2(*_: Any) -> None:
672-
print("remove2")
673648
s.remove(shape2)
674649

675-
# s.on_collision(1, 0).separate = remove2
676-
s.on_collision(1, 0, separate=remove1)
677-
678-
s.step(0.001)
650+
def remove3(*_: Any) -> None:
651+
s.remove(shape3)
679652

680-
# trigger separate with shape2 and shape3, shape1 will be removed 2x
681-
s.remove(shape1)
682-
683-
s.on_collision(1, 0, separate=remove2)
684-
s.add(shape1)
653+
s.on_collision(separate=remove1)
654+
s.on_collision(1, separate=remove2)
655+
s.on_collision(0, 1, separate=remove3)
685656

657+
for _ in range(10):
686658
s.step(1)
687-
# trigger separate with shape2 and shape3, shape1 is removed, and shape2 will be removed
688-
s.remove(shape1)
689-
690-
# self.assertNotIn(shape1, s.shapes)
691-
# s.remove(shape1)
692-
# s.step(1)
693-
694-
print(" XXX end")
659+
self.assertEqual(len(s.shapes), 0)
695660

696-
@unittest.skip("Existing bug in Pymunk. TODO: Fix bug and enable test")
697661
def testRemoveInSeparateWithoutStep(self) -> None:
698662
s = p.Space()
699-
print("\n XXX start")
700663

701664
shape1 = p.Circle(s.static_body, 1)
665+
shape1.collision_type = 1
702666

703-
body2 = p.Body()
667+
body2 = p.Body(1, 1)
704668
shape2 = p.Circle(body2, 1)
705-
shape2.density = 1
706669

707-
s.add(shape1, shape2, body2)
670+
body3 = p.Body(1, 1)
671+
shape3 = p.Circle(body3, 1)
672+
673+
s.add(shape1, shape2, body2, shape3, body3)
708674

709-
def separate(*_: Any) -> None:
710-
pass
675+
def remove2(*_: Any) -> None:
676+
s.remove(shape2, body2)
711677

712-
s.step(1)
713-
s.on_collision(0, separate=separate)
678+
def remove3(*_: Any) -> None:
679+
s.remove(shape3, body3)
714680

715-
s.remove(shape1)
681+
s.on_collision(0, separate=remove2)
682+
s.on_collision(1, separate=remove3)
716683

717-
print(" XXX end")
684+
s.step(0.1)
685+
686+
self.assertEqual(len(s.shapes), 3)
687+
s.remove(shape1)
688+
self.assertEqual(len(s.shapes), 0)
718689

719-
@unittest.skip("Existing bug in Pymunk. TODO: Fix bug and enable test")
720690
def testCollisionHandlerRemoveAfterSeparate(self) -> None:
721691
# In this test the separate must happen before post_solve in the same step()
722692
print()
@@ -735,35 +705,24 @@ def testCollisionHandlerRemoveAfterSeparate(self) -> None:
735705
shape3.collision_type = 3
736706

737707
space.add(shape1, body2, shape2, shape3, body3)
738-
print("START", shape1, shape2, shape3)
739708

740709
def separate(arbiter: p.Arbiter, space: p.Space, data: Any) -> None:
741-
print("SEP", arbiter.shapes)
742710
self.separate_occurred = True
743711

744712
def post_solve(arbiter: p.Arbiter, space: p.Space, data: Any) -> None:
745-
print("POST", arbiter.shapes)
746713
if self.separate_occurred:
747-
print("POST REMOVE", arbiter.shapes)
748714
space.remove(*arbiter.shapes)
749715

750716
space.on_collision(1, 2, post_solve=post_solve)
751717
space.on_collision(3, 2, separate=separate)
752718

753-
print(1)
754719
self.separate_occurred = False
755720
space.step(1)
756-
print(2)
757721
body3.position = 10, 0
758-
# self.assertEqual(len(space.shapes), 3)
759722

760723
self.separate_occurred = False
761724
space.step(1)
762-
print(3)
763725
space.remove(shape3)
764-
# self.assertEqual(len(space.shapes), 1)
765-
print(4)
766-
# space.remove(shape3)
767726

768727
def testCollisionHandlerAddRemoveInStep(self) -> None:
769728
s = p.Space()
@@ -791,14 +750,13 @@ def pre_solve_remove(arb: p.Arbiter, space: p.Space, data: Any) -> None:
791750
self.assertTrue(b in s.bodies)
792751
self.assertTrue(c in s.shapes)
793752

794-
s.on_collision(0, 0, pre_solve=pre_solve_add)
753+
s.on_collision(pre_solve=pre_solve_add)
795754

796755
s.step(0.1)
797-
return
798756
self.assertTrue(b in s.bodies)
799757
self.assertTrue(c in s.shapes)
800758

801-
s.on_collision(0, 0).pre_solve = pre_solve_remove
759+
s.on_collision(pre_solve=pre_solve_remove)
802760

803761
s.step(0.1)
804762

0 commit comments

Comments
 (0)