Créer un objet FeaturePython partie II

From FreeCAD Documentation
Revision as of 11:22, 10 January 2020 by Mario52 (talk | contribs)
Other languages:

App::FeaturePython vs. Part::FeaturePython

Jusqu'à présent, nous nous sommes concentrés sur les aspects internes d'une classe Python construite autour d'un objet FeaturePython - en particulier, un objet App::FeaturePython object.
Nous avons créé l'objet, défini certaines propriétés et ajouté des rappels d'événements au niveau du document qui permettent à notre objet de répondre à un recalcul de document avec la méthode execute().

Mais nous n'avons toujours pas de boîte.

Cependant, nous pouvons facilement en créer une en ajoutant seulement deux lignes.

Tout d'abord, ajoutez une nouvelle importation en haut du fichier: import Part

Ensuite, dans execute(), ajoutez la ligne suivante (vous pouvez supprimer l'instruction print()):

def execute():
    """
    Called on document recompute
    """

    Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height)

Ces commandes exécutent des scripts python fournis avec FreeCAD par défaut.

  • La méthode makeBox() génère une nouvelle forme de boîte.
  • L'appel englobant à Part.show() ajoute la forme à l'arborescence du document et la rend visible.

Si FreeCAD est ouvert, rechargez le module boîte et créez un nouvel objet boîte en utilisant box.create() (supprimez tous les objets boîte existants, juste pour garder les choses propres).

Remarquez comment une boîte apparaît immédiatement à l'écran: la méthode execute() est appelée immédiatement après la création de la boîte et nous forçons le document à recalculer à la fin de box.create().


Mais il y a un problème.

Cela devrait être assez évident. La boîte elle-même est représentée par un objet entièrement différent de notre objet FeaturePython. La raison en est que Part.show() crée un objet boîte séparé et l'ajoute au document. En fait, si vous accédez à votre objet FeaturePython et modifiez les dimensions, vous verrez une autre forme de boîte créée et l'ancienne laissée en place. Ce n'est pas bon! De plus, si la vue du rapport est ouverte, vous pouvez remarquer une erreur indiquant 'Les recalculs imbriqués d'un document ne sont pas autorisés'. Cela a à voir avec l'utilisation de la méthode Part.show() dans un objet FeaturePython. Nous voulons éviter de faire cela.

Astuce

Le problème est que nous nous appuyons sur les méthodes Part.make*(), qui ne génèrent que des objets Part::Feature non paramétriques (formes simples et stupides), tout comme vous obtiendriez si vous copiez un objet paramétrique à l'aide de Part Copie simple.

Ce que nous voulons, bien sûr, est un objet boîte paramétrique qui redimensionne la boîte existante lorsque nous modifions ses propriétés. Nous pourrions supprimer l'objet Part::Feature précédent et le régénérer à chaque fois que nous modifions une propriété, mais nous avons encore deux objets à gérer - notre objet FeaturePython personnalisé et l'objet Part::Feature qu'il génère.

Alors, comment pouvons-nous résoudre ce problème?

Tout d'abord, nous devons utiliser le bon type d'objet.

Jusqu'à présent, nous avons utilisé des objets App::FeaturePython. Ils sont pas mal mais ils ne sont pas destinés à être utilisés comme objets de géométrie. Ils sont plutôt mieux utilisés comme objets de document qui ne nécessitent pas de représentation visuelle dans la vue 3D. Nous devons donc utiliser un objet Part::FeaturePython à la place.


Dans create(), modifiez la ligne suivante:

obj = App.ActiveDocument.addObject('App::FeaturePython', obj_name)

pour lire:

obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)

Pour terminer les modifications dont nous avons besoin, la ligne suivante de la méthode execute() doit être modifiée:

Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height))

en:

obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)

Votre code devrait ressembler à ceci:

import FreeCAD as App
import Part

def create(obj_name):
  """
  Object creation method
  """

  obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)

  fpo = box(obj)

  App.ActiveDocument.recompute()

  return fpo

class box():

  def __init__(self, obj):
      """
      Default Constructor
      """

      self.Type = 'box'

      obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = ""
      obj.addProperty('App::PropertyLength', 'Length', 'Dimensions', 'Box length').Length = 10.0
      obj.addProperty('App::PropertyLength', 'Width', 'Dimensions', 'Box width').Width = '10 mm'
      obj.addProperty('App::PropertyLength', 'Height', 'Dimensions', 'Box height').Height = '1 cm'

      obj.Proxy = self
      self.Object = obj

  def execute(self, obj):
      """
      Called on document recompute
      """

      obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)

Maintenant, enregistrez vos modifications et revenez à FreeCAD. Supprimez tous les objets existants, rechargez le module de boîte et créez un nouvel objet de boîte.

Les résultats peuvent sembler un peu mitigés. L'icône dans l'arborescence est différente - c'est une boîte, maintenant. Mais il n'y a pas de cube. Et l'icône est toujours grise!

Qu'est-il arrivé? Bien que nous ayons correctement créé notre forme de boîte et l'avons affectée à un objet Part::FeaturePython, avant de pouvoir l'afficher dans notre vue 3D, nous devons attribuer un ViewProvider.

Écrire un ViewProvider

Un fournisseur de vues est le composant d'un objet qui lui permet d'avoir une représentation visuelle dans l'interface graphique - en particulier dans la vue 3D. FreeCAD utilise une structure d'application appelée 'vue du modèle' qui est conçue pour séparer les données (le 'modèle') de sa représentation visuelle (la «vue»). Si vous avez passé du temps à travailler avec FreeCAD en Python, vous en serez probablement déjà conscient grâce à l'utilisation de deux modules Python principaux: FreeCAD et FreeCADGui (souvent aliasés respectivement «App» et «Gui»).

Ainsi, notre implémentation de FeaturePython Box nécessite également ces éléments. Jusqu'à présent, nous nous sommes concentrés uniquement sur la partie «modèle», il est donc temps d'écrire la «vue». Heureusement, la plupart des implémentations de vues sont simples et nécessitent peu d'efforts pour écrire, au moins pour commencer. Voici un exemple de ViewProvider, emprunté et légèrement modifié de [1]

class ViewProviderBox:
   def __init__(self, obj):
       """
       Set this object to the proxy object of the actual view provider
       """
       obj.Proxy = self

   def attach(self, obj):
       """
       Setup the scene sub-graph of the view provider, this method is mandatory
       """
       return

   def updateData(self, fp, prop):
       """
       If a property of the handled feature has changed we have the chance to handle this here
       """
       return

   def getDisplayModes(self,obj):
       """
       Return a list of display modes.
       """
       return None

   def getDefaultDisplayMode(self):
       """
       Return the name of the default display mode. It must be defined in getDisplayModes.
       """
       return "Shaded"

   def setDisplayMode(self,mode):
       """
       Map the display mode defined in attach with those defined in getDisplayModes.
       Since they have the same names nothing needs to be done.
       This method is optional.
       """
       return mode

   def onChanged(self, vp, prop):
       """
       Print the name of the property that has changed
       """

       App.Console.PrintMessage("Change property: " + str(prop) + "\n")

   def getIcon(self):
       """
       Return the icon in XMP format which will appear in the tree view. This method is optional and if not defined a default icon is shown.
       """

       return """
           /* XPM */
           static const char * ViewProviderBox_xpm[] = {
           "16 16 6 1",
           " 	c None",
           ".	c #141010",
           "+	c #615BD2",
           "@	c #C39D55",
           "#	c #000000",
           "$	c #57C355",
           "        ........",
           "   ......++..+..",
           "   .@@@@.++..++.",
           "   .@@@@.++..++.",
           "   .@@  .++++++.",
           "  ..@@  .++..++.",
           "###@@@@ .++..++.",
           "##$.@@$#.++++++.",
           "#$#$.$$$........",
           "#$$#######      ",
           "#$$#$$$$$#      ",
           "#$$#$$$$$#      ",
           "#$$#$$$$$#      ",
           " #$#$$$$$#      ",
           "  ##$$$$$#      ",
           "   #######      "};
           """

   def __getstate__(self):
       """
       Called during document saving.
       """
       return None

   def __setstate__(self,state):
       """"
       Called during document restore.
       """"
       return None

Remarquez dans le code ci-dessus, nous définissons également une icône XMP pour cet objet. La conception d'icônes dépasse le cadre de ce didacticiel, mais les conceptions d'icônes de base peuvent être gérées à l'aide d'outils open source tels que GIMP, Krita et Inkscape. La méthode getIcon est également facultative. S'il n'est pas fourni, FreeCAD fournira une icône par défaut.


Sans un ViewProvider défini, nous devons maintenant l'utiliser pour donner à notre objet la possibilité de la visualisation.

Revenez à la méthode create() dans votre code et ajoutez ce qui suit vers la fin:

ViewProviderBox(obj.ViewObject)

Cette instance de la classe ViewProvider personnalisée lui transmet le ViewObject intégré de FeaturePython. Le ViewObject ne fera rien sans notre implémentation de la classe personnalisée donc, quand la classe ViewProvider s'initialise, elle enregistre une référence à elle-même dans l'attribut ViewObject.Proxy de FeaturePython. De cette façon, lorsque FreeCAD a besoin de rendre notre Box visuellement, il peut trouver la classe ViewProvider pour le faire.

Maintenant, enregistrez les modifications et revenez à FreeCAD. Importez ou rechargez le module Box et appelez box.create().

Nous ne voyons toujours rien, mais notez ce qui est arrivé à l'icône à côté de l'objet boîte. C'est en couleur! Et ça a une forme! C'est un indice que notre ViewProvider fonctionne comme prévu.

Alors maintenant, il est temps de réellement *ajouter* une boîte.

Revenez au code et ajoutez la ligne suivante à la méthode execute():

obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)

Maintenant, enregistrez, rechargez le module de boîte dans FreeCAD et créez une nouvelle boîte.

Vous devriez voir une boîte apparaître à l'écran! Si elle est trop grande (ou ne s'affiche pas du tout), cliquez sur l'un des boutons 'ViewFit' pour l'adapter à la vue 3D.

Notez ce que nous avons fait ici qui diffère de la façon dont il a été implémenté pour App::FeaturePython ci-dessus. Dans la méthode execute(), nous avons appelé la macro Part.makeBox() et lui avons transmis les propriétés de la boîte comme précédemment. Mais plutôt que d'appeler Part.show() sur l'objet résultant (qui a créé un nouvel objet boîte séparé), nous l'avons simplement affecté à la propriété Shape de notre Part::FeaturePython objet à la place.

Vous pouvez même modifier les dimensions de la boîte en modifiant les valeurs dans le panneau de propriétés FreeCAD. Essayez!

Piégeage d'événements

Jusqu'à présent, nous n'avons pas abordé explicitement le piégeage d'événements. Presque toutes les méthodes d'une classe FeaturePython servent de rappel accessible à l'objet FeaturePython (qui obtient l'accès à notre instance de classe via l'attribut Proxy, si vous vous en souvenez).

Voici une liste des rappels pouvant être implémentés dans l'objet FeaturePython de base:

FeaturePython basic callbacks
execute(self, obj) Appelé pendant le recalcul du document N'appelez pas recompute() à partir de cette méthode (ou de toute méthode appelée à partir de execute()) car cela provoque un recalcul imbriqué.
onBeforeChanged(self, obj, prop) Appelé avant qu'une valeur de propriété ne soit modifiée prop est le nom de la propriété à modifier, pas l'objet de propriété lui-même. Les modifications de propriété ne peuvent pas être annulées. Les valeurs de propriété précédente / suivante ne sont pas disponibles simultanément pour la comparaison.
onChanged(self, obj, prop) Appelé après la modification d'une propriété prop est le nom de la propriété à modifier, pas l'objet de propriété lui-même.
onDocumentRestored(self, obj) Appelé après la restauration d'un document ou la copie / la duplication d'un objet FeaturePython. Parfois, les références à l'objet FeaturePython de la classe ou à la classe de l'objet FeaturePython peuvent être rompues, car la méthode class __init__() n'est pas appelée lorsque l'objet est reconstruit. L'ajout de {{{1}}} ou {{{1}}} résout souvent ces problèmes.

En outre, il existe deux rappels dans la classe ViewProvider qui peuvent parfois s'avérer utiles:

ViewProvider basic callbacks
updateData(self, obj, prop) Appelé après la modification d'une propriété de données (modèle) obj est une référence à l'instance de classe FeaturePython, pas à l'instance de ViewProvider. prop est le nom de la propriété à modifier, pas l'objet de propriété lui-même.
onChanged(self, vobj, prop) Appelé après la modification d'une propriété de vue vobj est une référence à l'instance de ViewProvider. prop est le nom de la propriété view qui a été modifiée.


Tip
Il n'est pas rare de rencontrer une situation où les rappels Python ne sont pas déclenchés comme ils le devraient. Les débutants dans ce domaine doivent être assurés que le système de rappel FeaturePython n'est pas fragile ou cassé. Invariablement, lorsque les rappels ne s'exécutent pas, c'est parce qu'une référence est perdue ou indéfinie dans le code sous-jacent. Si, cependant, les rappels semblent rompre sans explication, la fourniture de références d'objet / proxy dans le rappel onDocumentRestored() (comme indiqué dans le premier tableau ci-dessus) peut atténuer ces problèmes. Jusqu'à ce que vous soyez à l'aise avec le système de rappel, il peut être utile d'ajouter des instructions d'impression dans chaque rappel pour imprimer des messages sur la console comme diagnostic pendant le développement.

Le code

Le code complet de cet exemple:

import FreeCAD as App
import Part

def create(obj_name):
   """
   Object creation method
   """

   obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)

   fpo = box(obj)

   ViewProviderBox(obj.ViewObject)

   App.ActiveDocument.recompute()

   return fpo


class box():

   def __init__(self, obj):
       """
       Default Constructor
       """

       self.Type = 'box'
       self.ratios = None

       obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = 'Hello World!'
       obj.addProperty('App::PropertyLength', 'Length', 'Dimensions', 'Box length').Length = 10.0
       obj.addProperty('App::PropertyLength', 'Width', 'Dimensions', 'Box width'). Width = '10 mm'
       obj.addProperty('App::PropertyLength', 'Height', 'Dimensions', 'Box Height').Height = '1 cm'
       obj.addProperty('App::PropertyBool', 'Aspect Ratio', 'Dimensions', 'Lock the box aspect ratio').Aspect_Ratio = False

       obj.Proxy = self
       self.Object = obj

   def __getstate__(self):
       return self.Type

   def __setstate__(self, state):
       if state:
           self.Type = state
 
   def execute(self, obj):
       """
       Called on document recompute
       """

       obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)


class ViewProviderBox:
   def __init__(self, obj):
       """
       Set this object to the proxy object of the actual view provider
       """
       obj.Proxy = self

   def attach(self, obj):
       """
       Setup the scene sub-graph of the view provider, this method is mandatory
       """
       return

   def updateData(self, fp, prop):
       """
       If a property of the handled feature has changed we have the chance to handle this here
       """

       return

   def getDisplayModes(self,obj):
       """
       Return a list of display modes.
       """
       return None

   def getDefaultDisplayMode(self):
       """
       Return the name of the default display mode. It must be defined in getDisplayModes.
       """
       return "Shaded"

   def setDisplayMode(self,mode):
       """
       Map the display mode defined in attach with those defined in getDisplayModes.
       Since they have the same names nothing needs to be done.
       This method is optional.
       """
       return mode

   def onChanged(self, vobj, prop):
       """
       Print the name of the property that has changed
       """

       App.Console.PrintMessage("Change property: " + str(prop) + "\n")

   def getIcon(self):
       """
       Return the icon in XMP format which will appear in the tree view. This method is optional and if not defined a default icon is shown.
       """

       return """
           /* XPM */
           static const char * ViewProviderBox_xpm[] = {
           "16 16 6 1",
           " 	c None",
           ".	c #141010",
           "+	c #615BD2",
           "@	c #C39D55",
           "#	c #000000",
           "$	c #57C355",
           "        ........",
           "   ......++..+..",
           "   .@@@@.++..++.",
           "   .@@@@.++..++.",
           "   .@@  .++++++.",
           "  ..@@  .++..++.",
           "###@@@@ .++..++.",
           "##$.@@$#.++++++.",
           "#$#$.$$$........",
           "#$$#######      ",
           "#$$#$$$$$#      ",
           "#$$#$$$$$#      ",
           "#$$#$$$$$#      ",
           " #$#$$$$$#      ",
           "  ##$$$$$#      ",
           "   #######      "};
           """

   def __getstate__(self):
       return None

   def __setstate__(self,state):
       return None