IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Écriture d'un canvas en C++ avec Dear ImGui,
Partie 2 sur 8, un billet blog par ericb2

Le , par ericb2

0PARTAGES

Partie 2. La classe canvas (amélioration et tests)

Le contexte

La classe Canvas a été créée pour fonctionner avec le logiciel miniDart, mais on devrait pouvoir l'adapter à un autre logiciel sans problème. Celui-ci est basé sur Dear ImGui, et fonctionne selon le paradigme du mode immédiat

Historiquement, et pour ceux qui l'ont vue, le mode immédiat, c'est la vidéo de Casey Muratori:

.

Mais il y a eu d'autres présentations et définitions, comme par exemple celle de Jari Komppa que je trouve d'une merveilleuse simplicité. À propos de Jari Komppa, allez aussi voir le projet SoLoud qui est assez incroyable lui aussi.

Plus simplement, le logiciel miniDart fonctionne comme un moteur de jeu : on exécute une boucle infinie de type "évènements, mise à jour des états logiques
puis calcul du rendu graphique et affichage", et on recommence indéfiniment dans que la condition de sortie de la boucle n'est pas réalisée.

Important: on n'effectue l'étape du rendu+affichage qu'une fois dans la boucle. Chaque nouvel objet est ajouté dans une pile qui n'est vidée qu'une seule fois par tour lors du rendu, afin d'éviter de surcharger la machine (cf le fonctionnement de Dear ImGui).

Dans miniDart, pour éviter de passer d'une fenêtre à une autre, on utilise des onglets. Un seul onglet est actif à la fois, le reste ne sera ni évalué, ni calculé dans la boucle (on ne dépensera pas de ressources pour "afficher" les objets contenus dans les onglets inactifs, mais les instances des objets créés sont toujours en mémoire). Cela signifie aussi que l'on pourra avoir plusieurs instances de ce Canvas fonctionnant en même temps, mais ce sera forcément une unique instance par onglet.

En ce qui concerne le Canvas, il NE devra être exécuté et accessible à l'utilisateur QUE SI :

- l'onglet dans lequel une instance existe est actif;
- cette instance a correctement été initialisée ;
- la barre d'outils est active. Validation : on voit les icônes des objets pouvant être dessinés ;



Canvas inactif : on ne peut pas dessiner




Canvas actif : on peut sélectionner un objet, et le dessiner dans la zone juste au dessus (celle contenant l'image)


- les objets peuvent être dessinés seulement la zone dans laquelle des images sont affichées, y compris sans qu'on visualise quelque chose (pas de source vidéo active)

- le curseur de la souris survole une certaine partie de l'écran. Validation : l'objet dessiné change de couleur lorsqu'il est survolé par le curseur de la souris

Objet non survolé :

Remarque : noter la couleur grise de l'objet survolé par le curseur de la souris ci-dessous (TODO : trouver une plus belle couleur :-) )

Objet survolé :

- [TODO, à venir ultérieurement] les objets dessinés devront pouvoir être intégrés dans les images pendant l'enregistrement

Remarque : actuellement, OpenCV affiche des images dans une vue openGL, et OpenGL dessine par dessus, et seul la zone de texte, qui utilise OpenGL + FreeType + Harfbuzz est pour l'instant "ajoutée" aux images enregistrées.

Les besoins

On souhaite pouvoir créer et utiliser une instance d'un objet canvas : il faudra donc un onglet actif (sinon une fenêtre active si pas d'onglets). On supposera cette condition réalisée en tant que pré-requis.

On suppose de plus qu'une instance du Canvas existe dans l'onglet actif, et que le curseur de la souris survole la zone "dessinable", qui n'est autre que celle de l'image en cours de visualisation (par exemple une vidéo en cours de lecture)

À chaque tour de boucle principale, si l'onglet contenant l'instance est actif, on doit pouvoir :

  • créer un nouvel objet avec la souris. Méthode: clic gauche+faire glisser sans relâcher: le curseur dessine l'objet dont l'icône est active ;
  • afficher dynamiquement l'objet en train d'être dessiné ;

Conditions : un objet pouvant être dessiné doit être sélectionné dans la barre d'outils (par un clic gauche)
Action réalisée : on ne dessinait pas avant le clic gauche. Une fois le bouton gauche enfoncé, sans relâcher, on fait glisser le curseur de la souris.
Effet attendu: l'objet est dessiné progressivement. Si on revient en arrière, la modification est visualisée en temps réel

  • ne pas mettre le processeur à genoux pendant la manipulation (actuellement : limitation à 60 fps environ => ~ 12% d'un coeur).

Action réalisée : en ralentissant la boucle principale, ne pas dépasser une certaine charge par coeur // Optimisation

  • dessiner de nouveaux objets ;
  • sélectionner le type d'objet à dessiner ;
  • interagir avec les objets dessinés ;
  • effacer des objets ;
  • sélectionner un objet (dans ce cas, sa couleur est modifiée);
  • visualiser quand un objet est survolé ;
  • déplacer les objets ;
  • modifier l'ordre d'empilement d'un objet : monter ou descendre d'un niveau,placer un objet à l'avant ou à l'arrière :
  • supprimer le dernier objet créé ;
  • supprimer tous les objets dessinés.


Exemple pour illustrer le déplacement vertical des objets dans la pile des objets dessinés (cf ci-dessous) : l'objet jaune était sous l'objet rouge. Mais on ne veut pas toucher le bleu.

AVANT :

APRÈS :





Tous ces besoins ont permis de définir l'interface du canvas


Commentaires sur l'interface

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11


namespace md

{
    class Canvas
    {
        public:
            Canvas();
            ~Canvas();
Tous les booléens ci-dessous servent à déterminer ce qu'est en train de faire l'utilisateur, et si on peut dessiner, stocker les informations ou autre chose.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
            bool           init();
            void           update(ImVec2);

            bool           addObject();
            bool           adding_circle;
            bool           adding_circle2;
            bool           adding_preview1;
            bool           adding_preview2;
            bool           adding_rect;
            bool           adding_rect2;
bcol contient la couleur de l'objet dessiné (flat color pour l'instant)

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9

            ImVec4  bcol;
            // future use
            // ImVec4 ocol;

            int            iconWidth;
            int            iconHeight;
            int            frame_padding;
setMouseValid() : confirme que le curseur de la souris est bien dans la zone image, lorsqu'on dessine le cadre qui sera contenu dans la loupe.

preview() : contient l'indice de l'objet sélectionné, ainsi que la couleur qui sera retournée (sélectionné ou simplement survolé ?)


Code : Sélectionner tout
1
2
3
4
5
6
7

            void           setMousePosValid(int, float);
            void           preview(int selectedObject, ImU32, int, float, float);
            void           updateSelectedArea(ImVector <ImVec2> points, ImU32, float);
            // FIXME, usefull
            //void           setSelectedAreaPoints(ImVec2, ImVec2);
draw() : ajoute la pile d'objet dans la pile graphique

Code : Sélectionner tout
1
2
3
  
          int            draw();
            void           clean();
remove() : permet de supprimer un objet en connaissant sa position dans la pile des objets dessinés.

Code : Sélectionner tout
1
2
3

            bool           remove(unsigned int);
moveObjectTo() permet de déplacer verticalement un objet dans la pile des objets dessinés : monter / descendre d'un niveau, placer à l'arrière ou à l'avant, ou encore supprimer.
Code : Sélectionner tout
1
2
            bool           moveObjectTo(unsigned int, int);
Menu contextuel (n'existe que si un objet est sélectionné)

Code : Sélectionner tout
1
2
3

            void           showObjectsStackPopupMenu(unsigned int);
Les méthodes ci-dessous retournent vrai si on survole le type d'objet (donné par leur nom). Les algorithmes correspondants seront présentés dans une prochaine partie.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

            bool           insideCircle(ImVec2, ImVec2, float);
            bool           intersectEmptyCircle(ImVec2, ImVec2, float, float);

            //             intersection when : (x == A) OR (x === B)  OR ((vec A)*(vec B) < EPSILON  AND  ( x bettwen xB and xC) AND ( y  between yB and yC))
            bool           intersectSegment(ImVec2 /* mousePos */, ImVec2 /* Point_A */ , ImVec2  /* Point_B */);
            bool           mousePosIsPoint(ImVec2 /* mousePos */, ImVec2 /* aGivenPoint */);

            bool           insideSimpleArrow(ImVec2, ImVector<ImVec2>, ImVector<ImVec2>);
            bool           insidePolygon(ImVec2, ImVector<ImVec2>);

            //             only horizontal rectangle are drawn
            bool           insideFilledRectangle(ImVec2, ImVector<ImVec2>);
            bool           intersectEmptyRectangle(ImVec2, ImVector<ImVec2>, ImVector<ImVec2>);

            bool           insideEllipse(ImVec2, float, ImVec2, ImVec2); // includes empty ellipse
            bool           intersectEmptyEllipse(ImVec2, float, ImVec2, ImVec2, float /* thickness */);

            bool           insideCurve(ImVec2, ImVector<ImVec2>);
            bool           insideArrow(ImVec2, ImVector<ImVec2>);
Les méthodes ci-dessous permettent de sélectionner un objet, de retrouver l'indice de celui qui est actuellement actif (s'il existe), de retourner la valeur du booléen permettant de savoir si un objet est actuellement dans l'état actif (sélectionné)

Code : Sélectionner tout
1
2
3
4
5
6
7
8

   
         inline void    setSelected(unsigned int selectedObject) { currentActiveDrawnObjectIndex = selectedObject;}
            inline unsigned int   getCurrentActiveDrawnObjectIndex(void) { return currentActiveDrawnObjectIndex ; }

            inline bool    getIsAnObjectSelected (void) { return anObjectIsCurrentlySelected; }
            inline void    setObjectCurrentlySelected (bool bValue) { anObjectIsCurrentlySelected = bValue; }
La méthode ci-dessous retourne la couleur de l'objet (sélectionné ou simplement survolé ? )
Code : Sélectionner tout
1
2
3

            ImU32          getBackgroundColor(unsigned int);
La méthode ci-dessous retrouve les paramètres des objets dans la pile des objets dessinés, en fonction du type d'objet.
Code : Sélectionner tout
1
2
3
4
5


            void           catchPrimitivesPoints(void);
            int            show();
Les fonctions -initialisation des textures OpenGL- ci-dessous permettent de dessiner les icônes des objets dans la barre d'outils.

Code : Sélectionner tout
1
2
3
4
5
6
7

            void           loadCanvasObjectsIcons(void);
            void           createCanvasObjectsImagesTexIds(void);
            void           cleanCanvasObjectsImagesTexIds(void);

            GLuint         canvasObjectImageTexId[CANVAS_OBJECTS_TYPES_MAX];
chaque icône d'objet pouvant être dessiné est convertie en objet matriciel OpenCV de type cv::Mat()

Code : Sélectionner tout
1
2
3

            cv::Mat        canvasObjectImage[CANVAS_OBJECTS_TYPES_MAX];
Ci-dessous : différents pointeurs et variables pour la gestion du Canvas
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10

            md::TextCanvas * mp_TextCanvas;

            ImVec2           topLeft;
            ImVec2           bottomRight;

            ImDrawList *     p_drawList;
            ImVec2           mouse_pos_in_image;
            ImVector <ImVec2> arrow_points;
Ci-dessous zoom_area points et un vecteur de 2 points (ayant chacun 2 composantes), qui définissent la position de la partie à zoomer avec la loupe
Code : Sélectionner tout
1
2
3
4

            ImVector <ImVec2> zoom_area_points;
            DrawnObject      aDrawnObject;
L'objet en cours d'élaboration est un objet de type DrawnObject, dont la définition est donnée dans canvas_objects.hpp

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10

            std::vector <DrawnObject> currentlyDrawnObjects;

        private:
            unsigned int    currentActiveDrawnObjectIndex;
            bool            anObjectIsCurrentlySelected;
    };

} /* namespace md */

Principes de fonctionnement

Les méthodes essentielles utilisées dans la boucle principale sont canvas::preview(), canvas::draw() et canvas::update(). La méthode canvas::moveTo() est appelée dans le menu popup de canvas::update().

Autour de la ligne 1444, dans Sources/src/Application/miniDart.cpp :

On retrouve l'adresse de la liste des objets à dessiner par Dear ImGui (fenêtres, widgets, etc), à laquelle on va ajouter notre pile d'objets, puis les coordonnées du curseur de la souris.

À l'étape suivante, on appelle canvas::preview() avec les coordonnées du curseur de la souris dans la zone image, dans le cas où un type d'objet pouvant être dessiné serait défini, l'utilisateur réunissant les conditions pour que quelque chose soit dessiné.

Puis on appelle canvas::draw() pour dessiner tout ce qui doit l'être, y compris l'éventuel nouvel objet ajouté dans la pile.

Enfin, on appelle canvas::update qui passe en revue tous le vecteur des objets à dessiner, et détecte pour chacun d'entre eux s'il est survolé par le curseur de la souris. Si un objet est survolé, son paramètre hovered passe à vrai, et la couleur de cet objet changera. S'il est sélectionné, c'est une autre couleur qui sera affichée, afin de pouvoir faire la distinction entre survolé et sélectionné (on peut modifier certains de ces paramètres, le déplacer etc).

Ainsi :
- si l'objet n'est pas sélectionné, mais simplement survolé, sa couleur change ;
- si plusieurs objets ont une zone commune, ce sont tous les objets survolés simultanément qui changent de couleur ;
- si l'objet est sélectionné par un clic gauche sans relâchement, il pourra être déplacé ;
- s'il est sélectionné, avec un clic droit on pourra le déplacer verticalement, changer sa couleur, ou même encore le supprimer.

Remarque : les algorithmes utilisés permettant de savoir si le curseur de la souris est à l'intérieur de la forme de l'objet (ou pas) seront présentés dans une prochaine partie.

Dessiner ou pas

Pour cela on capture la position du curseur de la souris sur l'écran, dans une fenêtre en utilisant la méthode ImGui::IsItemHovered()
Si la zone image, à l'intérieur de la fenêtre racine est survolée : on peut dessiner
Sinon : on ne peut pas dessiner.

Prévisualisation d'un objet à dessiner

Concerne la création d'un nouvel objet, c'est à dire qu'avant le clic gauche sans relâchement, on n'était pas en train de dessiner. On doit donc capturer 2 positions -distinctes et suffisamment éloignées- du curseur à l'écran, détecter l'appui sur un bouton, si on se déplace sans relâcher, ou le relâchement simple d'un bouton, car il faut "comprendre" ce que fait l'utilisateur, et traduire ses actions. Tout ça à environ 60 images par seconde ! (parce que je limite, sinon, le rafraîchissement atteint entre 200 et 400 images par seconde sur un i7@1,8 GHz, mais le processeur est à 100% de sa charge dans ce cas.

Lorsqu'on relâche le bouton de la souris :
- si les dimensions de l'objet sont inférieures à quelque chose de détectable : on ne fait rien
- si les dimensions sont supérieures à une certaine limite, on stocke le type, les paramètres essentiels de l'objet dans la pile

Dans tous les cas on vide l'objet à prévisualiser.

Pour des raisons d'OPTIMISATION, certains paramètres essentiels au dessin sont calculés PENDANT la pré-visualisation.

Méthode : canvas::preview()

Dessin d'un objet

On dessine, dans l'ordre correspondant à la création, tous les objets stockés dans la pile de type ImGui:: DrawList(). Si la pile est vide, on ne dessine rien. La couleur de l'objet sera calculée en temps réel, et dépendra de l'état de l'objet : survolé et/ou sélectionné

Stockage des données

Pour chaque objet, des données sont stockées lorsque l'utilisateur "relâche" le bouton gauche de la souris, après avoir dessiné un objet.

Pour stocker proprement les paramètres d'un objet, la structure DrawnObject a été créée, et son interface est définie dans le fichier d'en-tête canvas_objects.hpp


Code : Sélectionner tout
1
2
3
typedef struct DrawnObject
{
anObjectype est un entier qui contient le type d'objet, qui n'est pas forcément un objet à dessiner. Exemple : SELECT_CURSOR.

Code : Sélectionner tout
1
2
    unsigned int anObjectType;// object type, defines properties
thickness: contient l'épaisseur du trait
P1P4 : contient la distance entre 2 points, ou un rayon. Si la valeur est inférieure à une valeur seuil, l'objet dessiné avec preview n'est pas ajouté à la pile d'objets à dessiner.

Les commentaires permettent de comprendre l'utilité de chacun des paramètres stockés avec chaque objet présent dans la pile des objets à dessiner..

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    float        thickness;
    float        P1P4;        // line length
    float        R2_in;       // (intern radius)^2
    float        R2_out;      // (extern radius)^2

    // ellipse properties must be calculated just after preview
    ImVec2       F1;          // ellipse focus point 1
    ImVec2       F2;          //         focus point 2
    float        long_axis;    // ellipse long axis
    float        radius_x;    // ellipse x radius
    float        radius_y;    // ellipse y radius
    float        rotation;    // rotation angle (CTRL key + MouseDrag)

    float        arrowLength;
    float        arrowWidth;

    bool         selected;
    bool         hovered;
    bool         record;
    bool         has_outline;

    ImVector <ImVec2>  arrowPolygon; // inside helpers
    ImVector <ImVec2>  Rect_ext; // inside helpers
    ImVector <ImVec2>  Rect_int; // inside helpers
    ImVector <ImVec2>  hullPoints; // inside helpers
    ImVector <ImVec2>  objectPoints;  // depends on the case
    ImU32  objBackgroundColor;
TODO : mettre la bordure en surbrillance quand un objet est sélectionné, plutôt que modifier la couleur du fond

Code : Sélectionner tout
1
2
3
4

    ImU32  objOutlineColor;
} DrawnObject;

Merci d'avance pour tout retour constructif :-) Et si vous avez des questions, ou si vous trouvez une erreur ou une imprécision, n'hésitez surtout pas, à commenter ou à me contacter.

Eric Bachard



Précédent : dessiner sur l'écran (partie 1)
À SUIVRE (partie3 : la barre d'outils )

Une erreur dans cette actualité ? Signalez-nous-la !