I was hoping to figure out how to use affine transforms to generate the 2-D isometric points based on the 3-D coordinates of the court, ball and opponent. But simply couldn’t manage to get it to work. My mathematical skills have lain ununsed for too many decades. But, because my inability to do so is somewhat infuriating, I may yet waste some significant hours trying to make it work. If I do, I will cover my effort in a later post.

Once I got that working I was going to look at augmenting the isometric projection with a side view. I felt that would enable those looking at the isometric image to better understand what/who was where. The what is the ball and the resulting 2-D triangular plane in the 3-D space of the squash court. The who is, of course, the opponent. Figured matplotlib’s subplots would handle this pretty nicely.

So, let’s see what we can manage.

Subplots

I am going to have two plots split across the width of the image. The leftmost will contain the isometric projection. The right subplot will show the side view (i.e. viewer has there back to one of the side walls).

I am not too sure how big to make the figure, but I will start at 11.2 x 4.8 inches. And I think I will start with 6 and 5.2 inch subplots. The larger one for the isometric view as it is more complex. But if that looks too silly, I will play with the sizes.

Let’s set up the plotting environment.

fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, gridspec_kw={'width_ratios': [sp1_wr, sp2_wr]},
  figsize=(fig_wd, fig_ht), frameon=False)
for ax in (ax1, ax2):
  for spine in ax.spines.values():
    spine.set_visible(False)
  ax.tick_params(bottom=False, labelbottom=False, left=False, labelleft=False)

Isometric View

If I had a more recent version of matplotlib in my environment, I could have replaced gridspec_kw={'width_ratios': [sp1_wr, sp2_wr]} with width_ratios=[sp1_wr, sp2_wr]. A wee nicety. Will look at upgrading.

After refactoring the above code, I changed all the existing occurences of ax. to ax1.. Then checked to make sure I got the output I expected. And, getting the following image, I figure we’re halfway there.

figure with two subplots on one row with isometric image on the left, nothing on the right
Half the sub-plots working!

Side View

I got somewhat lazy here. Didn’t use values from the court dimension array I had set up previously. Well mostly not.

I started by drawing the side wall of the court, no red lines. Oh yes, I also modified the aspect ratio for the subplot. If I didn’t the court wall came out as more square than rectangular.

ax2.set_aspect('equal')
ax2.plot([0, 45], [0, 0], c='k')
ax2.plot([0, 0], [0, 24], c='k')
ax2.plot([45, 45], [0, 24], c='k')
ax2.plot([0, 45], [24, 24], c='k')
figure with two subplots on one row with isometric image on the left, blank side wall on the right
Add the side wall in second subplot.

Didn’t like that gap between the two images, so added , layout='tight' to the parameters of plt.subplots().

fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, gridspec_kw={'width_ratios': [sp1_wr, sp2_wr]},
  figsize=(fig_wd, fig_ht), frameon=False, layout='tight')

And, I think the following looks a bit better.

figure with two subplots on one row with isometric image on the left, blank side wall on the right
Add the side wall in second subplot.

Now lets get the various lines and tin in place.

ax2.plot([0, 31], [20, 20], c='r')
ax2.plot([31, 45], [15, 15], c='r')
ax2.plot([31, 31], [15, 20], c='r')
ax2.plot([0, 1/6], [17/12, 15/12], c='r')
ax2.plot([1/6, 1/6], [15/12, 0], c='r')
figure with two subplots on one row with isometric image on the left, side wall with red playing lines, including tin, on the right
Add top red line on side wall, and an impression of the tin at the front.

And, now the ball and the base of the interference plane. Ball is at tee line, 8 feet from the left side wall, and 8.5 feet above the floor.

# different perspective, (0, 0) is at bottom left, so ball at length court - p_ball[1]
b_pos = crt_points[3][1] - p_ball[1]
ax2.scatter(b_pos, p_ball[2], c='b')
ax2.plot([0, b_pos], [18/12, p_ball[2]], c='b')
add ball and base of interfernce plane to side view subplot
Add ball and base of interfernce plane.

Finally, let’s toss in the opponent. I am assuming the opponent is about 1 foot thick at the thickest. He is 6 feet 1 inch tall, 6 feet from the left side wall and 24 feet from the back wall. Opponent is ~3 inches below the triangular plane indicating where flight interference would occur. That’s a pretty small space for the side view image.

o_pos = crt_points[3][1] - p_oppo[1]
# assume oppo 1 foot thick, centered on above position
# if interference colour portion of opponent above triangular plane in red
if in_pyr and pyr_z <= 1:
  ax2.plot([o_pos - 0.5, o_pos + 0.5], [0, 0], c='g')
  ax2.plot([o_pos - 0.5, o_pos + 0.5], [p_ht, p_ht], c='r')
  ax2.plot([o_pos - 0.5, o_pos - 0.5], [0, p_ht], c='g')
  ax2.plot([o_pos + 0.5, o_pos + 0.5], [0, p_ht], c='g')
  ax2.plot([o_pos - 0.5, o_pos - 0.5], [p_ht * pyr_z, p_ht], c='r')
  ax2.plot([o_pos + 0.5, o_pos + 0.5], [p_ht * pyr_z, p_ht], c='r')
else:
  ax2.plot([o_pos - 0.5, o_pos + 0.5], [0, 0], c='g')
  ax2.plot([o_pos - 0.5, o_pos + 0.5], [p_ht, p_ht], c='g')
  ax2.plot([o_pos - 0.5, o_pos - 0.5], [0, p_ht * pyr_z], c='g')
  ax2.plot([o_pos + 0.5, o_pos + 0.5], [0, p_ht * pyr_z], c='g')
add opponet to side view subplot
Add opponent.

I eventually decided to try showing a player who has squatted down to avoid being in the plane of interference. So some new variables and code. I took the opportunity to refactor some of my other code as well to use the new variables. Let’s start with the variables. I am assuming a player is roughly 1 foot thick and 2 feet wide when standing. I change the thickness to roughly 32 inches when squatted. And a squatted height of 38 inches. Rough measurements of me in a squatted attitude. All converted to feet as I am using feet to plot things in my images.

# ball, opponent and pyramid
p_ball = [8, 5, 8.5]
p_oppo = [6, 24, 0]
p_ht = 6 + 1/12
p_i_wd = 2
p_i_thck = 1
p_s_thck = 1
# crouching: 38 inches height, 32 inches thick
p_crouch = False
if p_crouch:
  p_ht = 38 / 12
  p_i_thck = 32 / 12
  p_s_thck = 32 / 12

In the isometric code, wherever I had previously used hard coded values or a default of 1, I replaced those with the appropriate variable. For example:

# this
pe1 = patches.Arc((o_x_1, o_y_1), 2*iso_fs, iso_fs, color='g')
# became this
pe1 = patches.Arc((o_x_1, o_y_1), p_i_wd*iso_fs, p_i_thck*iso_fs, color='g')

Similarly for the code above plotting the opponent in the side view. Hard coded/assumed values replaced as appropriate.

o_pos = crt_points[3][1] - p_oppo[1]
p_hlf_t = p_s_thck / 2
if in_pyr and pyr_z <= 1:
  ax2.plot([o_pos - p_hlf_t, o_pos + p_hlf_t], [0, 0], c='g')
  ax2.plot([o_pos - p_hlf_t, o_pos + p_hlf_t], [p_ht, p_ht], c='r')
  ax2.plot([o_pos - p_hlf_t, o_pos - p_hlf_t], [0, p_ht], c='g')
  ax2.plot([o_pos + p_hlf_t, o_pos + p_hlf_t], [0, p_ht], c='g')
  ax2.plot([o_pos - p_hlf_t, o_pos - p_hlf_t], [p_ht * pyr_z, p_ht], c='r')
  ax2.plot([o_pos + p_hlf_t, o_pos + p_hlf_t], [p_ht * pyr_z, p_ht], c='r')
else:
  ax2.plot([o_pos - p_hlf_t, o_pos + p_hlf_t], [0, 0], c='g')
  ax2.plot([o_pos - p_hlf_t, o_pos + p_hlf_t], [p_ht, p_ht], c='g')
  ax2.plot([o_pos - p_hlf_t, o_pos - p_hlf_t], [0, p_ht], c='g')
  ax2.plot([o_pos + p_hlf_t, o_pos + p_hlf_t], [0, p_ht], c='g')

Examples

Let’s lower the ball by a foot, to 7 ½ feet.

lower striking position of ball by 1 foot, interference now present
Lower striking position of ball to 7.5 feet above the floor.

Now, we get interference if opponent standing upright. If they were to crouch down, there would not be any interference. Let’s have a look at that situation.

Okay, just needed to flip the logical variable p_crouch to true to cover the situation where player squats to avoid being in the plane of interference. Should likely have added a command line parameter to control that value. But this is pretty much a one off bit of coding.

with the lower striking position of ball but opponent has crouched down
No change to striking point, but opponent has crouched down.

How about a ground stroke being struck roughly 28 inches above the floor.

ball at a ground stroke striking height with opponent crouched down
Striking point at ground stoke height, opponent has crouched down.

One last one. The ball will be back at 8 ½ feet above the floor, 8 feet from the left side wall, but now 5 feet from the back wall. And the opponent again standing upright.

ball 8.5 feet above floor, 8 feet from left side wall, 5 feet from back wall, opponent upright
Striking point at volley height, but closer to back wall.

Done

Think that’s it for this one. More fun. Still not sure of the real world value.

That said, until next time, I hope you also have some fun coding or plotting or…

Resources

  • matplotlib.pyplot.subplots — do note the version when reading the documentation
  • matplotlib.figure
  • matplotlib.axes.Axes.set_aspect