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é :
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 :
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(); |
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; |
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; |
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); |
Code : | Sélectionner tout |
1 2 3 | int draw(); void clean(); |
Code : | Sélectionner tout |
1 2 3 | bool remove(unsigned int); |
Code : | Sélectionner tout |
1 2 | bool moveObjectTo(unsigned int, int); |
Code : | Sélectionner tout |
1 2 3 | void showObjectsStackPopupMenu(unsigned int); |
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>); |
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; } |
Code : | Sélectionner tout |
1 2 3 | ImU32 getBackgroundColor(unsigned int); |
Code : | Sélectionner tout |
1 2 3 4 5 | void catchPrimitivesPoints(void); int show(); |
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]; |
Code : | Sélectionner tout |
1 2 3 | cv::Mat canvasObjectImage[CANVAS_OBJECTS_TYPES_MAX]; |
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; |
Code : | Sélectionner tout |
1 2 3 4 | ImVector <ImVec2> zoom_area_points; DrawnObject aDrawnObject; |
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 { |
Code : | Sélectionner tout |
1 2 | unsigned int anObjectType;// object type, defines properties |
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; |
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 )