22import numpy as np
33
44from rocketpy .motors import EmptyMotor , HybridMotor , LiquidMotor , SolidMotor
5+ from rocketpy .motors .ClusterMotor import ClusterMotor
56from rocketpy .rocket .aero_surface import Fins , NoseCone , Tail
67from rocketpy .rocket .aero_surface .generic_surface import GenericSurface
78
@@ -201,7 +202,7 @@ def draw(self, vis_args=None, plane="xz", *, filename=None):
201202
202203 drawn_surfaces = self ._draw_aerodynamic_surfaces (ax , vis_args , plane )
203204 last_radius , last_x = self ._draw_tubes (ax , drawn_surfaces , vis_args )
204- self ._draw_motor (last_radius , last_x , ax , vis_args )
205+ self ._draw_motor (last_radius , last_x , ax , vis_args , plane )
205206 self ._draw_rail_buttons (ax , vis_args )
206207 self ._draw_center_of_mass_and_pressure (ax )
207208 self ._draw_sensors (ax , self .rocket .sensors , plane )
@@ -410,45 +411,141 @@ def _draw_tubes(self, ax, drawn_surfaces, vis_args):
410411 )
411412 return radius , last_x
412413
413- def _draw_motor (self , last_radius , last_x , ax , vis_args ):
414+ def _draw_motor (self , last_radius , last_x , ax , vis_args , plane = "xz" ):
414415 """Draws the motor from motor patches"""
415- total_csys = self .rocket ._csys * self .rocket .motor ._csys
416- nozzle_position = (
417- self .rocket .motor_position + self .rocket .motor .nozzle_position * total_csys
418- )
416+
417+ # Obtenir les patches (logique cluster/simple est dans la fonction ci-dessous)
418+ motor_patches = self ._generate_motor_patches (ax , plane )
419419
420- # Get motor patches translated to the correct position
421- motor_patches = self ._generate_motor_patches (total_csys , ax )
422-
423- # Draw patches
420+ # Dessiner les patches
424421 if not isinstance (self .rocket .motor , EmptyMotor ):
425- # Add nozzle last so it is in front of the other patches
426- nozzle = self .rocket .motor .plots ._generate_nozzle (
427- translate = (nozzle_position , 0 ), csys = self .rocket ._csys
428- )
429- motor_patches += [nozzle ]
430-
431- outline = self .rocket .motor .plots ._generate_motor_region (
432- list_of_patches = motor_patches
433- )
434- # add outline first so it is behind the other patches
435- ax .add_patch (outline )
436- for patch in motor_patches :
437- ax .add_patch (patch )
422+
423+ if isinstance (self .rocket .motor , ClusterMotor ):
424+ # Logique pour Cluster
425+ for patch in motor_patches :
426+ ax .add_patch (patch )
427+
428+ # Raccorder le tube au début (point z min) du cluster
429+ if self .rocket .motor .positions :
430+ # Trouve le z le plus proche de la coiffe
431+ cluster_z_positions = [pos [2 ] for pos in self .rocket .motor .positions ]
432+ z_connector = min (cluster_z_positions ) if self .rocket ._csys == 1 else max (cluster_z_positions )
433+ self ._draw_nozzle_tube (last_radius , last_x , z_connector , ax , vis_args )
434+
435+ else :
436+ # Logique originale pour Moteur Simple
437+ total_csys = self .rocket ._csys * self .rocket .motor ._csys
438+ nozzle_position = (
439+ self .rocket .motor_position + self .rocket .motor .nozzle_position * total_csys
440+ )
441+
442+ # Ajouter la tuyère (logique originale)
443+ nozzle = self .rocket .motor .plots ._generate_nozzle (
444+ translate = (nozzle_position , 0 ), csys = self .rocket ._csys
445+ )
446+ motor_patches += [nozzle ]
438447
439- self ._draw_nozzle_tube (last_radius , last_x , nozzle_position , ax , vis_args )
448+ outline = self .rocket .motor .plots ._generate_motor_region (
449+ list_of_patches = motor_patches
450+ )
451+ # Ajouter l'outline en premier
452+ ax .add_patch (outline )
453+ for patch in motor_patches :
454+ ax .add_patch (patch )
440455
441- def _generate_motor_patches (self , total_csys , ax ): # pylint: disable=unused-argument
456+ self ._draw_nozzle_tube (last_radius , last_x , nozzle_position , ax , vis_args )
457+ def _generate_motor_patches (self , ax , plane = "xz" ):
442458 """Generates motor patches for drawing"""
443459 motor_patches = []
460+ total_csys = self .rocket ._csys * self .rocket .motor ._csys
444461
445- if isinstance (self .rocket .motor , SolidMotor ):
462+ # ################################################
463+ # ## LOGIQUE D'AFFICHAGE POUR CLUSTER DE MOTEURS ##
464+ # ################################################
465+ if isinstance (self .rocket .motor , ClusterMotor ):
466+ cluster = self .rocket .motor
467+ all_sub_patches = [] # Pour l'outline global
468+
469+ for sub_motor , sub_pos in zip (cluster .motors , cluster .positions ):
470+ # sub_pos est [x, y, z]
471+ motor_longitudinal_pos = sub_pos [2 ] # Position Z du moteur
472+
473+ # Déterminer le décalage (offset)
474+ offset = 0
475+ if plane == "xz" :
476+ offset = sub_pos [0 ] # Coordonnée X
477+ elif plane == "yz" :
478+ offset = sub_pos [1 ] # Coordonnée Y
479+
480+ # `sub_total_csys` convertit un déplacement relatif au moteur
481+ # en un déplacement relatif à la fusée.
482+ sub_total_csys = self .rocket ._csys * sub_motor ._csys
483+
484+ # On réutilise la logique de SolidMotor, mais avec un offset
485+ if isinstance (sub_motor , SolidMotor ):
486+ current_motor_patches = [] # Patches pour CE moteur
487+
488+ # Position Z du centre de masse des grains DANS LE REPERE FUSÉE
489+ grains_cm_pos = (
490+ motor_longitudinal_pos
491+ + sub_motor .grains_center_of_mass_position * sub_total_csys
492+ )
493+ ax .scatter (
494+ grains_cm_pos .real , # Utiliser .real pour éviter ComplexWarning
495+ offset ,
496+ color = "brown" ,
497+ label = f"Grains CM ({ sub_pos [0 ]:.2f} , { sub_pos [1 ]:.2f} )" ,
498+ s = 8 ,
499+ zorder = 10 ,
500+ )
501+
502+ # `translate` prend (pos_z, pos_y_offset)
503+ # Ces fonctions utilisent le _csys interne du sub_motor (qui est 1)
504+ chamber = sub_motor .plots ._generate_combustion_chamber (
505+ translate = (grains_cm_pos .real , offset ), label = None
506+ )
507+ grains = sub_motor .plots ._generate_grains (
508+ translate = (grains_cm_pos .real , offset )
509+ )
510+
511+ # Position Z de la tuyère DANS LE REPERE FUSÉE
512+ nozzle_position = (
513+ motor_longitudinal_pos
514+ + sub_motor .nozzle_position * sub_total_csys
515+ )
516+
517+ # ***** CORRECTION ICI *****
518+ # On ne passe PAS de 'csys' !
519+ # On laisse la fonction _generate_nozzle utiliser son
520+ # propre _csys interne (qui est 1, tail_to_nose),
521+ # car 'nozzle_position' est déjà la coordonnée absolue.
522+ nozzle = sub_motor .plots ._generate_nozzle (
523+ translate = (nozzle_position .real , offset )
524+ )
525+ # **************************
526+
527+ current_motor_patches += [chamber , * grains , nozzle ]
528+ all_sub_patches .extend (current_motor_patches )
529+
530+ # Créer un outline global pour tous les moteurs
531+ if all_sub_patches :
532+ # Utiliser .plots du premier moteur pour la méthode
533+ outline = self .rocket .motor .motors [0 ].plots ._generate_motor_region (
534+ list_of_patches = all_sub_patches
535+ )
536+ motor_patches .append (outline ) # Mettre l'outline en premier
537+ motor_patches .extend (all_sub_patches )
538+
539+ # #####################################
540+ # ## LOGIQUE ORIGINALE (MOTEUR SIMPLE) ##
541+ # #####################################
542+ elif isinstance (self .rocket .motor , SolidMotor ):
446543 grains_cm_position = (
447544 self .rocket .motor_position
448545 + self .rocket .motor .grains_center_of_mass_position * total_csys
449546 )
450547 ax .scatter (
451- grains_cm_position ,
548+ grains_cm_position . real , # .real
452549 0 ,
453550 color = "brown" ,
454551 label = "Grains Center of Mass" ,
@@ -457,10 +554,10 @@ def _generate_motor_patches(self, total_csys, ax): # pylint: disable=unused-arg
457554 )
458555
459556 chamber = self .rocket .motor .plots ._generate_combustion_chamber (
460- translate = (grains_cm_position , 0 ), label = None
557+ translate = (grains_cm_position . real , 0 ), label = None # .real
461558 )
462559 grains = self .rocket .motor .plots ._generate_grains (
463- translate = (grains_cm_position , 0 )
560+ translate = (grains_cm_position . real , 0 ) # .real
464561 )
465562
466563 motor_patches += [chamber , * grains ]
@@ -471,7 +568,7 @@ def _generate_motor_patches(self, total_csys, ax): # pylint: disable=unused-arg
471568 + self .rocket .motor .grains_center_of_mass_position * total_csys
472569 )
473570 ax .scatter (
474- grains_cm_position ,
571+ grains_cm_position . real , # .real
475572 0 ,
476573 color = "brown" ,
477574 label = "Grains Center of Mass" ,
@@ -483,16 +580,16 @@ def _generate_motor_patches(self, total_csys, ax): # pylint: disable=unused-arg
483580 translate = (self .rocket .motor_position , 0 ), csys = total_csys
484581 )
485582 chamber = self .rocket .motor .plots ._generate_combustion_chamber (
486- translate = (grains_cm_position , 0 ), label = None
583+ translate = (grains_cm_position . real , 0 ), label = None # .real
487584 )
488585 grains = self .rocket .motor .plots ._generate_grains (
489- translate = (grains_cm_position , 0 )
586+ translate = (grains_cm_position . real , 0 ) # .real
490587 )
491588 motor_patches += [chamber , * grains ]
492589 for tank , center in tanks_and_centers :
493590 ax .scatter (
494- center [0 ],
495- center [1 ],
591+ center [0 ]. real , # .real
592+ center [1 ]. real , # .real
496593 color = "black" ,
497594 alpha = 0.2 ,
498595 s = 5 ,
@@ -506,8 +603,8 @@ def _generate_motor_patches(self, total_csys, ax): # pylint: disable=unused-arg
506603 )
507604 for tank , center in tanks_and_centers :
508605 ax .scatter (
509- center [0 ],
510- center [1 ],
606+ center [0 ]. real , # .real
607+ center [1 ]. real , # .real
511608 color = "black" ,
512609 alpha = 0.2 ,
513610 s = 4 ,
@@ -576,12 +673,19 @@ def _draw_rail_buttons(self, ax, vis_args):
576673 def _draw_center_of_mass_and_pressure (self , ax ):
577674 """Draws the center of mass and center of pressure of the rocket."""
578675 # Draw center of mass and center of pressure
579- cm = self .rocket .center_of_mass (0 )
580- ax .scatter (cm , 0 , color = "#1565c0" , label = "Center of Mass" , s = 10 )
581-
582- cp = self .rocket .cp_position (0 )
676+
677+ # MODIFICATION 1: Récupérer le vecteur CM et extraire .z
678+ cm_vector = self .rocket .center_of_mass (0 )
679+ # On prend la partie réelle pour éviter les warnings
680+ cm_z = float (cm_vector .z .real )
681+
682+ # On suppose que le CM est sur l'axe pour le dessin 2D
683+ ax .scatter (cm_z , 0 , color = "#1565c0" , label = "Center of Mass" , s = 10 )
684+
685+ # MODIFICATION 2: Prendre la partie réelle du CP aussi
686+ cp_z = float (self .rocket .cp_position (0 ).real )
583687 ax .scatter (
584- cp , 0 , label = "Static Center of Pressure" , color = "red" , s = 10 , zorder = 10
688+ cp_z , 0 , label = "Static Center of Pressure" , color = "red" , s = 10 , zorder = 10
585689 )
586690
587691 def _draw_sensors (self , ax , sensors , plane ):
0 commit comments