Skip to content

Commit bc2a188

Browse files
authored
Fix animation start pose being applied twice in Animate.trplot (#238)
Fixes #171. `Animate.trplot` built the frame geometry already placed at the requested start pose. The animation then applied the absolute pose again when drawing, so the frame showed up at the wrong place: asking for a start of `(-1, 0, 2)` displayed it at `(-2, 0, 4)`. The geometry is now built at the identity and the start pose is applied once through `_draw(self.start)`, which matches how the end pose and the intermediate frames are already drawn. Test: added a case to `tests/base/test_transforms3d_plot.py` that checks the drawn frame at the start and end poses for the line, arrow and rviz styles. It fails on `master` and passes with this change. Full suite: 348 passed, 3 skipped. The interactive animation loop itself is not exercised in the headless tests; the endpoint poses are.
1 parent df4cf92 commit bc2a188

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

‎spatialmath/base/animate.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,9 @@ def trplot(
163163
else:
164164
self.start = start
165165

166-
# draw axes at the origin
167-
smb.trplot(self.start, ax=self, **kwargs)
166+
# Store frame geometry at the origin so each absolute pose is applied once.
167+
smb.trplot(np.identity(4), ax=self, **kwargs)
168+
self._draw(self.start)
168169

169170
def set_proj_type(self, proj_type: str):
170171
self.ax.set_proj_type(proj_type)

‎tests/base/test_transforms3d_plot.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,55 @@
2222
from spatialmath.base.transformsNd import isR, t2r, r2t, rt2tr
2323

2424
import matplotlib.pyplot as plt
25+
from spatialmath.base.animate import Animate
2526

2627

2728
class Test3D(unittest.TestCase):
29+
def test_animate_nonidentity_start(self):
30+
# the axis artists of an Animate must sit where a static trplot puts them, both straight after
31+
# trplot(end, start=start) and after _draw(end); losing either half of the fix breaks one of these
32+
def axis_geometry(artists):
33+
geometry = []
34+
for h in artists[:3]: # the three axes of the frame
35+
# arrow style: compare the shaft only, the head wings differ
36+
if hasattr(h, "_segments3d"):
37+
geometry.append(np.asarray(h._segments3d[0]).T)
38+
else:
39+
geometry.append(np.asarray(h.get_data_3d()))
40+
return geometry
41+
42+
def static_geometry(pose, style):
43+
fig = plt.figure()
44+
try:
45+
ax = fig.add_subplot(projection="3d")
46+
trplot(pose, ax=ax, style=style, frame="moving")
47+
kinds = ("Line3DCollection",) if style == "arrow" else ("Line3D",)
48+
return axis_geometry(
49+
[a for a in ax.get_children() if type(a).__name__ in kinds]
50+
)
51+
finally:
52+
plt.close(fig)
53+
54+
start = transl(-1, 0, 2) @ trotz(0.4)
55+
end = transl(1, 2, 1) @ trotx(0.3)
56+
for style in ("line", "arrow", "rviz"):
57+
fig = plt.figure()
58+
try:
59+
anim = Animate(dim=[-4, 4])
60+
anim.trplot(end, start=start, style=style, frame="moving")
61+
artists = [x.h for x in anim.displaylist]
62+
for actual, expected in zip(
63+
axis_geometry(artists), static_geometry(start, style)
64+
):
65+
nt.assert_allclose(actual, expected, atol=1e-12)
66+
anim._draw(end)
67+
for actual, expected in zip(
68+
axis_geometry(artists), static_geometry(end, style)
69+
):
70+
nt.assert_allclose(actual, expected, atol=1e-12)
71+
finally:
72+
plt.close(fig)
73+
2874
@pytest.mark.skipif(
2975
os.environ.get("CI") == "true"
3076
or (sys.platform.startswith("darwin") and sys.version_info < (3, 11)),

0 commit comments

Comments
 (0)