Programmation dans Anatomist:
fabriquer un nouvel objet


(mise à jour: 27/07/2001)

Regardez toujours la doc automatique en meme temps... (et les sources des classes que je donne en exemple, ça peut aider aussi)

Objet dérivant de la base, AObject, avec typage dynamique (cad pas connu de la librairie de base libanatomist)
Ex: Hierarchy, objet apporté par le module MyModule:
Déclarer la classe avec le squelette et les fonctions suivantes:
Prenons comme exemple une nouvelle classe d'objet Anatomist: AToto

class AToto : public AObject
{
public:
  friend class MyModule;    // allow access to registerClass() from the module init function
  AToto( params quelconques... );
  virtual ~AToto();
  virtual Tree* optionTree() const;
  static int  classType();
protected:
  static Tree* _optionTree;
private:
  static int registerClass();
  static int _classType;
};


Il y a un certain nombre de fonctions à redéfinir, certaines obligatoires, d'autres fortement conseillées
 

Résumé

Enregistrement du type dynamique
Enregistrement du lecteur
Remplissage du type de l'instance
Enregistrement de l'icone pour la fenetre de controle
Définition des fonctions d'affichage (2D/3D)
Options dans le menu Object Manipulations
 

Enregistrement dynamique du nouveau type d'objet

Le type est demandé et enregistré par un appel à la fonction statique int AObject::registerObjectType( const string & id ).  Le type en question (l'entier retourné par registerObjectType() ) doit être gardé et partagé par toutes les instances de la classe AToto, on le garde donc dans une variable statique: AToto::_classType.
L'ennui est que cette fonction doit etre appelée avant la construction de toute instance de la classe AToto.
La bonne façon de déclarer ce type est désormais de le faire depuis l'initialisation du module auquel appartient l'objet, en redéfinissant la fonction Module::objectsDeclaration().
void MyModule::objectsDeclaration()
{
  AToto::registerClass();
}
//... in AToto definition source:
void AToto::registerClass()
{
  _classType = registerObjectType( "AToto" );
}
A faire dans le constructeur:
Le champ AObject::_type doit etre rempli à la construction. Ce champ est idiot, n'ayons pas peur de le dire, il va être remplacé dans le futur par une fonction virtuelle AObject::type() ou qqchose de ce goût-là: le type n'a pas besoin, et ne DOIT pas être stocké dans chaque instance de la classe. Enfin pour le moment...
Icone dans la fenetre de controle: Les icones des objets sont des QPixmap , la classe qui en est responsable pour le moment est QObjectTree (qui affiche l'arborescence des objets). On peut procéder à l'enregistrement, soit dans une initialisation faite une seule fois (la fonction registerClass() précédente), soit faire cet enregistrement dans le constructeur en vérifiant qu'on ne le refait pas à chaque construction (c'est ce qui est fait dans Hierarchy):
AToto::AToto()
{
  _type = _classType();
  if( QObjectTree::TypeNames.find( _type ) == QObjectTree::TypeNames.end() )
    {
      string      str( theAnatomist->getPath() );
      str += "/icons/list_atoto.pm";
      if( !QObjectTree::TypeIcons[ _type ].load( str.c_str() ) )
        {
          QObjectTree::TypeIcons.erase( _type );
          cerr << "Icon " << str << " not found\n";
        }

      QObjectTree::TypeNames[ _type ] = "AToto";
    }
}
Inconvénient: surcout du test à chaque construction d'une instance de cette classe et de ses dérivées.
Les icônes vont très certainement bientôt "sortir" de la classe QObjectTree qui ne devrait pas être nécessairement connue des modules, ils vont peut-être utiliser IconDictionary (qui est jusqu'ici utilisé uniquement pour les icônes des contrôles, mais rien n'empêche son utilisation pour d'autres besoins).

Menu d'options optionTree():
c'est la liste de ce qui peut etre mis dans le menu "Object manipulations" de la fenetre de controle. Jusqu'ici il ne dépend que du type d'objet, donc peut etre mis en variable statique de chaque classe dérivée de AObject. Mais rien n'empeche de le différentier pour un meme objet, selon son état.

Objets affichables dans les fenetres 2D

Is2DObject() doit retourner true
Redéfinir les fonctions MinX2D(), ..., MaxT2D() (boite englobante de l'objet)
Update2D() effectue l'affichage dans le XImage qui lui est passé en argument.
 

Objets affichables dans les fenetres 3D

Is3DObject() doit retourner true
Redéfinir les fonctions MinX3D(), ..., MaxT3D() (boite englobante de l'objet)
L'affichage d'un objet 3D est demandé par la fenetre par la fonction Update3D( list<GLuint> *, float time, const Point3df & direction ). En partique le paramètre direction ne sert pas, il faudrait peut-etre l'enlever (ou bien il a été prévu pour permettre des fonctionnalités qu'on n'utilise pas pour le moment, et que j'ai oubliées depuis).

2 alternatives:
 

  • Utilisation de la classe de base AGLObject qui encapsule les fonctions OpenGL (création des display lists etc)

  • Exemple: ATriangulated (libanatomist)

    AToto dérive à la fois de AObject (ou une classe dérivée de AObject) et de AGLObject
    Attention à ne pas dériver d'un objet qui est déjà 3D et de AGLObject en plus (héritage en diamant sur AGLObject)
    AGLObject contient volontairement des fonctions qui existent, et qui entrent donc en conflit avec des fonctions de AObject. On est donc forcé de les redéfinir pour les aiguiller, généralement sur la fonction du meme nom de AObject. Le but est de permettre à la partie AGLObject d'accéder à certaines fonctions de AObject qui lui sont nécessaires (les couleurs principalement: matériaux et palettes)
    Ces fonctions sont:

    virtual void clearHasChangedFlags()
    virtual Material& GetMaterial()
    virtual const AObjectPalette* palette() const
    virtual AObjectPalette* getOrCreatePalette()
    AGLObject gère les aspects 3D, les listes OpenGL et les "refresh flags" pour:
    Les noeuds (vertex)
    Les normales
    Les coordonnées de texture 1D et 2D
    L'image de texture 1D et 2D
    Ces aspects tiennent compte du temps
    AGLObject utilise des "vertex arrays" OpenGL, ce qui implique que les listes de vertex, normales, etc. doivent etre passées à la partie AGLObject sous la forme qui va bien: tableaux de flottants généralement. Pour les rendre accessibles, les fonctions virtuelles pures suivanes de AGLObject doivent etre redéfinies:
    virtual unsigned numVertex( float time ) const
    virtual const GLfloat* vertexArray( float time ) const
    virtual unsigned numPolygon( float time ) const
    virtual const GLuint* polygonArray( float time ) const
    Les fonctions suivantes ne sont pas virtuelles pures, mais on les redéfinit si on veut utiliser des normales ou des textures:
    virtual bool hasTexture() const
    virtual bool hasNormals() const
    virtual const GLfloat* normalArray( float time ) const
    virtual unsigned dimTex( float time ) const
    virtual unsigned texCoordSize( float time ) const
    virtual const GLfloat* texCoordArray( float time ) const
    Les fonctions suivantes donnent l'instant effectivement utilisé pour chaque type de composante
    virtual float vertexTime( float time ) const
    virtual float normalTime( float time ) const
    virtual float polygonTime( float time ) const
    virtual float texCoordTime( float time ) const
    virtual float textureTime( float time ) const
  • A la main

  • Redéfinir la fonction Update3D
    Update3D() ajoute à la liste qui lui est donnée des indices de display lists OpenGL. Il faut bien faire attention de remettre à jour les composantes de ces listes qui ont pu changer, et seulement ces composantes-là.
    En principe c'est carrément plus simple d'utiliser AGLObject qui gère déjà tous ces flags de remise à jour (c'est fait pour ça), sauf peut-etre pour des objets vraiment particuliers (je ne sais pas bien lesquels, peut-etre les soucoupes volantes de diffusion, et encore).

    Lecture / écriture disque

    Ecriture:

    C'est simple, il suffit de mettre une fonction d'écriture dans la classe, qui éventuellement crée et utilise un Writer. Comme on utilise l'écriture sur un objet déterminé, il n'y a pas de Writer général qui centralise toutes les écritures, juste une fonction AObject::saveStatic(): cette fonction ouvre une boîte de dialogue demandant un nom de fichier et appelle à son tour une fonction save() (virtuelle ) de l'objet. Il suffit de rendre saveStatic() accessible depuis le menu _optionTree.
     

    Lecture:

    La lecture d'un objet est centralisée dans la fonction ObjectReader::load( const string & filename ) qui elle-meme est appelée par la fonction statique AObject* AObject::load( const char* filename ), et de meme pour les fonctions reload(...). Ces fonctions sont uniques et ne doivent jamais etre modifiées pour des nouveaux objets.
    ObjectReader est une classe singleton qui possède un mécanisme d'enregistrement de nouveaux Readers: elle les centralise.
    On enregistre une nouvelle fonction de lecture en rapport avec un nom d'extension de fichier en appelant la fonction:
    ObjectReader::LoadFunction ObjectReader::registerLoader( const string & extension, LoadFunction newFunc )
    On lui passe donc l'extension et la fonction de lecture, et cette fonction renvoie l'ancienne fonction utilisée pour cette extension (s'il y en avait une, ou un pointeur nul sinon).
    Dans le cas où une extension correspond à plueieurs lecteurs possibles (c'est le cas pour les graphes par exemple), il faut ranger à la main l'ancien pointeur sur le lecteur lors de l'appel à registerLoader, de manière à pouvoir l'appeler si le nouveau lecteur n'arrive pas à lire l'objet. De cette manière les fonctions de lecture s'appellent en chaine jusqu'à ce que l'une d'entre elles arrive effectivement à lire l'objet. On pourrait automatiser ça, mais il faudrait l'automatiser de manière intelligente: appeler les lecteurs dans un ordre précis, du plus spécifique au plus général. A la main, on fait comme on veut: le lecteur de AToto peut appeler l'ancien lecteur avant ou après l'essai du sien propre, comme on veut.
    La fonction de lecture (par ex. une fonction statique de l'objet) doit prendre en entrée le nom de fichier et retourne le nouvel objet, créé et lu, ou un pointeur nul en cas d'échec. Par exemple, de façon à peu près standard:
    static AObject* AToto::load( const string & filename )
    {
      AToto  *toto = 0;
      ATotoReader  tr( filename );
      if( tr )
        {
          try
            {
              toto = new AToto;
              tr >> *toto;
            }
          catch( exception & e )
            {
              cerr << e.what() << endl;
              delete toto;
              toto = 0;
            }
        }
      if( !toto && _oldLoader )          // _oldLoader is the result of ObjectReader::registerLoader()
        return _oldLoader( filename );   // (must be declared in the class definition)
      return toto;
    }
    L'enregistrement du lecteur peut etre fait dans la fonction d'initialisation AToto::registerClass().
    On peut enregistrer plusieurs extensions et les faire pointer sur la meme fonction de lecture (quand il existe plusieurs formats pour un meme type d'objet, par ex .tri et .mesh pour les surfaces), mais dans ce cas il faut prendre soin de conserver les anciens lecteurs de chaque extension et renvoyer sur le bon en cas d'échec de lecture par AToto::load().