11import matplotlib .pyplot as plt
22import numpy as np
33
4+ from rocketpy .mathutils .vector_matrix import Vector
45from rocketpy .motors import EmptyMotor , HybridMotor , LiquidMotor , SolidMotor
56from rocketpy .rocket .aero_surface import Fin , Fins , NoseCone , Tail
6- from rocketpy .rocket .aero_surface import Fins , NoseCone , Tail
77from rocketpy .rocket .aero_surface .generic_surface import GenericSurface
88
99from .plot_helpers import show_or_save_plot
@@ -178,7 +178,7 @@ def draw(self, vis_args=None, plane="xz", *, filename=None):
178178 and webp (these are the formats supported by matplotlib).
179179 """
180180
181- self .__validate_aerodynamic_surfaces ()
181+ self .__validate_aerodynamic_surfaces (plane )
182182
183183 if vis_args is None :
184184 vis_args = {
@@ -198,9 +198,9 @@ def draw(self, vis_args=None, plane="xz", *, filename=None):
198198
199199 csys = self .rocket ._csys
200200 reverse = csys == 1
201- self .rocket .aerodynamic_surfaces .sort_by_position (reverse = reverse )
201+ surfaces = self .rocket .aerodynamic_surfaces .sort_by_position (reverse = reverse )
202202
203- drawn_surfaces = self ._draw_aerodynamic_surfaces (ax , vis_args , plane )
203+ drawn_surfaces = self ._draw_aerodynamic_surfaces (ax , vis_args , plane , surfaces )
204204 last_radius , last_x = self ._draw_tubes (ax , drawn_surfaces , vis_args )
205205 self ._draw_motor (last_radius , last_x , ax , vis_args )
206206 self ._draw_rail_buttons (ax , vis_args )
@@ -216,13 +216,15 @@ def draw(self, vis_args=None, plane="xz", *, filename=None):
216216 plt .tight_layout ()
217217 show_or_save_plot (filename )
218218
219- def __validate_aerodynamic_surfaces (self ):
219+ def __validate_aerodynamic_surfaces (self , plane ):
220220 if not self .rocket .aerodynamic_surfaces :
221221 raise ValueError (
222222 "The rocket must have at least one aerodynamic surface to be drawn."
223223 )
224+ if plane != "xz" and plane != "yz" :
225+ raise ValueError ("The plane must be 'xz' or 'yz'. The default is 'xz'." )
224226
225- def _draw_aerodynamic_surfaces (self , ax , vis_args , plane ):
227+ def _draw_aerodynamic_surfaces (self , ax , vis_args , plane , surfaces ):
226228 """Draws the aerodynamic surfaces and saves the position of the points
227229 of interest for the tubes."""
228230 # List of drawn surfaces with the position of points of interest
@@ -235,15 +237,17 @@ def _draw_aerodynamic_surfaces(self, ax, vis_args, plane):
235237 # diameter changes. The final point of the last surface is the final
236238 # point of the last tube
237239
238- for surface , position in self . rocket . aerodynamic_surfaces :
240+ for surface , position in surfaces :
239241 if isinstance (surface , NoseCone ):
240242 self ._draw_nose_cone (ax , surface , position .z , drawn_surfaces , vis_args )
241243 elif isinstance (surface , Tail ):
242244 self ._draw_tail (ax , surface , position .z , drawn_surfaces , vis_args )
243245 elif isinstance (surface , Fins ):
244- self ._draw_fins (ax , surface , position .z , drawn_surfaces , vis_args )
246+ self ._draw_fins (
247+ ax , surface , position .z , drawn_surfaces , vis_args , plane
248+ )
245249 elif isinstance (surface , Fin ):
246- self ._draw_fin (ax , surface , position . z , drawn_surfaces , vis_args )
250+ self ._draw_fin (ax , surface , position , drawn_surfaces , vis_args , plane )
247251 elif isinstance (surface , GenericSurface ):
248252 self ._draw_generic_surface (
249253 ax , surface , position , drawn_surfaces , vis_args , plane
@@ -306,13 +310,15 @@ def _draw_tail(self, ax, surface, position, drawn_surfaces, vis_args):
306310 # Add the tail to the list of drawn surfaces
307311 drawn_surfaces .append ((surface , position , surface .bottom_radius , x_tail [- 1 ]))
308312
309- def _draw_fins (self , ax , surface , position , drawn_surfaces , vis_args ):
313+ def _draw_fins (self , ax , surface , position , drawn_surfaces , vis_args , plane ):
310314 """Draws the fins and saves the position of the points of interest
311315 for the tubes."""
312316 num_fins = surface .n
313317 x_fin = - self .rocket ._csys * surface .shape_vec [0 ] + position
314318 y_fin = surface .shape_vec [1 ] + surface .rocket_radius
315- rotation_angles = [2 * np .pi * i / num_fins for i in range (num_fins )]
319+ rotation_angles = np .array ([2 * np .pi * i / num_fins for i in range (num_fins )])
320+ if plane == "xz" :
321+ rotation_angles -= np .pi / 2
316322
317323 for angle in rotation_angles :
318324 # Create a rotation matrix for the current angle around the x-axis
@@ -324,13 +330,6 @@ def _draw_fins(self, ax, surface, position, drawn_surfaces, vis_args):
324330 # Extract x and y coordinates of the rotated points
325331 x_rotated , y_rotated = rotated_points_2d
326332
327- # Project points above the XY plane back into the XY plane (set z-coordinate to 0)
328- x_rotated = np .where (
329- rotated_points_2d [1 ] > 0 , rotated_points_2d [0 ], x_rotated
330- )
331- y_rotated = np .where (
332- rotated_points_2d [1 ] > 0 , rotated_points_2d [1 ], y_rotated
333- )
334333 ax .plot (
335334 x_rotated ,
336335 y_rotated ,
@@ -340,34 +339,55 @@ def _draw_fins(self, ax, surface, position, drawn_surfaces, vis_args):
340339
341340 drawn_surfaces .append ((surface , position , surface .rocket_radius , x_rotated [- 1 ]))
342341
343- def _draw_fin (self , ax , surface , position , drawn_surfaces , vis_args ):
344- """Draws the fins and saves the position of the points of interest
345- for the tubes."""
342+ def _draw_fin (self , ax , surface , position , drawn_surfaces , vis_args , plane ):
343+ """Draws individual fins."""
346344
347- x_fin = - self .rocket ._csys * surface .shape_vec [0 ] + position
348- y_fin = surface .shape_vec [1 ] + surface .rocket_radius
349- angle = surface .angular_position
345+ # Get shape vec
346+ xs = surface .shape_vec [0 ]
347+ ys = surface .shape_vec [1 ]
348+ zs = np .zeros_like (xs )
349+
350+ # Define shape in fin coordinate system
351+ x_fin = - zs
352+ y_fin = ys
353+ z_fin = xs
354+ points = np .column_stack ((x_fin , y_fin , z_fin ))
355+
356+ # Move drawing coordinates to center of fin for cant angle rotation
357+ xd = np .array ([0 , 0 , max (xs ) / 2 ])
358+ points -= xd
350359
351- # Create a rotation matrix for the angle around the x-axis
352- rotation_matrix = np .array ([[1 , 0 ], [0 , np .cos (angle )]])
360+ # Rotate to body coordinate system
361+ for i , p in enumerate (points ):
362+ points [i ] = surface ._rotation_fin_to_body @ Vector (p )
353363
354- # Apply the rotation to the original fin points
355- rotated_points_2d = np .dot ( rotation_matrix , np . vstack (( x_fin , y_fin )) )
364+ rotated_xd = surface . _rotation_fin_to_body @ Vector ( xd )
365+ points + = np .array ( rotated_xd )
356366
357- # Extract x and y coordinates of the rotated points
358- x_rotated , y_rotated = rotated_points_2d
367+ # Back to the drawing system
368+ x_fin_rotated = points [:, 0 ]
369+ y_fin_rotated = points [:, 1 ]
370+ z_fin_rotated = points [:, 2 ]
371+
372+ if plane == "xz" :
373+ x_rotated = self .rocket ._csys * z_fin_rotated + position .z
374+ y_rotated = x_fin_rotated + position .x
375+ elif plane == "yz" :
376+ x_rotated = self .rocket ._csys * z_fin_rotated + position .z
377+ y_rotated = y_fin_rotated + position .y
378+ else : # pragma: no cover
379+ raise ValueError ("Plane must be 'xz' or 'yz'." )
359380
360- # Project points above the XY plane back into the XY plane (set z-coordinate to 0)
361- x_rotated = np .where (rotated_points_2d [1 ] > 0 , rotated_points_2d [0 ], x_rotated )
362- y_rotated = np .where (rotated_points_2d [1 ] > 0 , rotated_points_2d [1 ], y_rotated )
363381 ax .plot (
364382 x_rotated ,
365383 y_rotated ,
366384 color = vis_args ["fins" ],
367385 linewidth = vis_args ["line_width" ],
368386 )
369387
370- drawn_surfaces .append ((surface , position , surface .rocket_radius , x_rotated [- 1 ]))
388+ drawn_surfaces .append (
389+ (surface , position .z , surface .rocket_radius , x_rotated [- 1 ])
390+ )
371391
372392 def _draw_generic_surface (
373393 self ,
@@ -390,8 +410,6 @@ def _draw_generic_surface(
390410 x_pos = position [2 ]
391411 # y position of the surface is the y position in the plot
392412 y_pos = position [1 ]
393- else : # pragma: no cover
394- raise ValueError ("Plane must be 'xz' or 'yz'." )
395413
396414 ax .scatter (
397415 x_pos ,
@@ -470,9 +488,7 @@ def _draw_motor(self, last_radius, last_x, ax, vis_args):
470488
471489 self ._draw_nozzle_tube (last_radius , last_x , nozzle_position , ax , vis_args )
472490
473- def _generate_motor_patches (
474- self , total_csys , ax
475- ): # pylint: disable=unused-argument
491+ def _generate_motor_patches (self , total_csys , ax ):
476492 """Generates motor patches for drawing"""
477493 motor_patches = []
478494
@@ -680,7 +696,7 @@ def all(self):
680696
681697 # Rocket draw
682698 if len (self .rocket .aerodynamic_surfaces ) > 0 :
683- print ("\n Rocket Draw " )
699+ print ("\n Rocket Drawing " )
684700 print ("-" * 40 )
685701 self .draw ()
686702
0 commit comments