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
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.
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)
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é ?)
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
1 2 3
|
int draw();
void clean(); |
remove() : permet de supprimer un objet en connaissant sa position dans la pile des objets dessinés.
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.
1 2
|
bool moveObjectTo(unsigned int, int); |
Menu contextuel (n'existe que si un objet est sélectionné)
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.
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é)
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é ? )
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.
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.
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()
1 2 3
|
cv::Mat canvasObjectImage[CANVAS_OBJECTS_TYPES_MAX]; |
Ci-dessous : différents pointeurs et variables pour la gestion du Canvas
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
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
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
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.
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..
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
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 )