par , 30/08/2020 à 18h06 (9864 Affichages)
Introduction
Cette étude est divisée en 8 parties (1 billet de blog par partie) :
1. Dessiner sur l'écran avec Dear ImGui
2. La classe canvas (améliorations et tests)
3. La barre d'outils des objets pouvant être dessinés
4. Sélectionner un objet et le modifier
5. Déplacement vertical d'un objet dans le dessin : choix faits
6. Création d'un menu contextuel avec Dear ImGui
7. Déplacement horizontal d'un l'objet dans la fenêtre
8. Utilisation du canvas dans le logiciel miniDart
Comme expliqué dans un précédent billet 2 animations pour Dear ImGui, j'utilise Dear ImGui avec bohneur depuis 4 ans,
ce qui me permet d'assurer la portabilité sous Windows de mon logiciel, tout en écrivant le code sous Linux.
Vous pouvez tester ce canvas avec le logiciel ci-dessous. Un binaire pour Windows est disponible en ligne, et vous le trouverez en suivant le lien donné ci-dessous. Liens:
- Version de développement et code source du logiciel miniDart
Et pour le code lié au sujet de l'article, c'est ici :
- Interface de la classe canvas
- Implémentation
miniDart étant un logigiel d'analyse de la performance sportive (en développement), le besoin d'annoter les vidéos en cours d'analyse a rapidement été formulé par les quelques personnes qui ont l'amabilité de me faire des retours. En fait, les besoins ne sont pas très importants, et je travaille souvent seul.
L'idée, c'est de pouvoir annoter une vidéo, tout en continuant de la visionner : on fait un arrêt sur image,
et hop, on insère un commentaire, dont la durée et quelques paramètres (police, taille, couleur, fond coloré ou pas ...) sont paramétrables.
Remarque: pour l'instant, on ne peut insérer qu'un seul commentaire, mais ça devrait être résolu prochainement.
De plus, quelquefois, on peut avoir besoin d'insérer une flèche (droite ou courbée aussi), ou simplement de dessiner à main levée. D'où l'idée de créér un canvas, activable via un bouton, pour ajouter cette fonctionnalité très utile.
J'insiste: si vous avez une suggestion une idée à soumettre, n'hésitez pas à vous créer un compte et à faire une demande (voir
ici : issues dédiées à miniDart
Pourquoi avoir tout écrit moi-même ?
En fait, j'ai eu beau chercher, je n'ai PAS trouvé de site décrivant un canvas écrit en C++ (n'hésitez pas à faire suivre vos liens si vous en connaissez un).
Ce qui se rapproche le plus, c'est celui proposé par la fondation Mozilla, très bien écrit, et qui fonctionne très bien. Mais c'est un autre langage (Java script ?).
Mais ne soyons pas naïfs, je parie qu'il en existe mais les entreprises ne partagent malheureusement pas leur code ... tout en étant très contentes d'en trouver sur github, ou framagit que j'utilise comme plein de monde.
Alors, je l'ai fait moi-même, même si c'est loin d'être parfait, au moins tout est de moi. Je vous remercie pour votre indulgence, sachant que je ne suis pas un professionnel de la programmation, et que j'ai fait ça sur mon temps libre.
Enfin, je rappelle que toute aide est la bienvenue ... :-)
Comme le développement a demandé pas mal de temps, je vais présenter la progression dans les idées et l'implémentation, dans l'ordre réel, et diviser les articles en plusieurs parties :
Partie 1. dessiner sur l'écran
Exemple fourni par Dear ImGui: il s'appelle "Custom Rendering", cf la copie d'écran ci-dessous
La seconde image présente les primitives (formes de base).
Une nouveauté récente, ce sont les polygones "ngon", et les formes circulaires dont on peut faire varier le nombre de segments. Dès que la pile est vide, on remarquera que les boutons de suppression ont disparu.
voir la page releases de Dear ImGui pour les derniers changements. En résumé, on ne dessine pas grand chose, mais au moins la suggestion est faite !
Choix des objets à dessiner
Dans les choix pour le canvas qui sera présenté, les primitives que nous utiliserons sont :
- rectangle plein ;
- rectangle évidé ;
- cercle plein ;
- cercle évidé ;
- ellipses pleines (voir: ) ;
- ellipses évidées + la possibilité d'effectuer une rotation en maintenant la touche CTRL enfoncée ;
- les traits simples ;
- le tracé à main levée sera obtenu avec une suite de cercles de petits rayons, collés les uns à la suite des autres ;
- le tracé de flèches simples ;
- le tracé de flèches "arrondies". Ces flèches sont obtenues à l'aide d'une courbe de Bezier 4 points (le premier et le dernier, les deux autres étant calculés et pris au 1/3 et le suivant aux 2/3 de la distance
La liste des objets pouvant être dessinés (+ d'autres dont on aura besoin) sont donnés dans le fichier d'en-tête
[**canvas_objects.hpp**](https://framagit.org/ericb/miniDart/...as_objects.hpp)
Noter aussi la liste des chemins des images définies comme des constantes (pour des raisons de sécurité évidentes).
Deux thèmes sont prévus, mais seul le thème DARK sera utilisé pour l'instant.
Réutilisation avec le dessin des flèches
L'algorithme du tracé de la flèche est donné dans la documentation. En voici l'essentiel
Le principe est très simple : l'utilisateur définit 2 points, et les coordonnées des autres sont calculées cf l'équation ci-dessous pour le point C.
Pour le reste, le calcul des coordonnées des autres points, et l'algorithme du tracé complet sont donnés dans le document mentionné ci-dessus.
Rappel : il s'agit de mode immédiat, et tout se passe dans une boucle infinie. À chaque tour, c'est l'état de variables statiques qui définit
ce qui doit être fait et/ou dessiné.
Remarquer l'exemple du gradient (il pourrait devenir utile), et les deux piles possibles permettant de dessiner en avant plan et en
arrière plan, indépendamment de ce qui est déjà dessiné. Comprendre : tout au dessus, ou tout en dessous.
Utilisation : on peut simplement tracer des traits, et les empiler. On peut aussi les dépiler en les supprimant séquentiellement (le dernier,
puis celui d'avant ... etc, jusqu'au premier trait tracé). D'un point de vue logiciel, on crée donc une pile (ou un vecteur en C++) de traits d'une couleur donnée : push / pop pour dépiler
Lien : Algorithme utilisé pour dessiner une flèche avec Dear ImGui (document .pdf)
Tests réalisés
En fait, dessiner quelque chose n'est pas le plus gros problème. En étudiant l'API (détaillée dans ImGui.h), on comprend vite ce qu'il faut pour dessiner quelque chose.
Le vrai problème est ailleurs : quid des clics de souris ?
Méthode "preview"
Pour résoudre le problème, on convient de considérer : les clics droits et/ou gauche de la souris, si le bouton vient d'être appuyé, s'il vient d'être relâché, ou si la souris est déplacée SANS que le bouton ait été relâché.
Le choix fait : clic sans relâcher ET en déplaçant la souris : on est en train de créer un déplacement. Le point origine P1 est donc stocké en mémoire, et dès qu'on relâche la souris, la position finale du curseur servira de point numéro 2.
Les 2 points P1 et P2 permettant ainsi sans ambiguïté de dessiner un trait, un rectangle, un cercle, etc, selon la figure sélectionnée.
Dans le code, ça donne :
1 2 3
|
void md::Canvas::preview(int selectedObject, ImU32 color, int w, float ratio, float outline_thickness)
{ |
Signature de la méthode : on a besoin de la couleur de l'objet à dessiner, du ratio (lié au rapport largeur sur hauteur de l'écran, outline_thickness définit la finesse du tracé
1 2 3 4 5 6 7 8 9 10
|
setMousePosValid(w, ratio);
aDrawnObject.radius_x = 1.0f + ImGui::GetMouseDragDelta().x;
aDrawnObject.radius_y = 1.0f + ImGui::GetMouseDragDelta().y;
if (fabs(aDrawnObject.radius_x) <= 1.0f)
aDrawnObject.radius_x = 1.0f;
aDrawnObject.rotation = ImGui::GetIO().KeyCtrl ? aDrawnObject.radius_y / aDrawnObject.radius_x : 0.0f; |
L'astuce ci-dessus permet, via un appui sur la touche CTRL de faire tourner une ellipse, dans le cas où l'on en dessinerait une.
N.B. : le code de l'ellipse est accessible dans ce patch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
switch(aDrawnObject.anObjectType)
{
case SELECTED_OBJECT:
case EMPTY_RECTANGLE:
case EMPTY_CIRCLE:
case EMPTY_ELLIPSE:
case FILLED_RECTANGLE:
case FILLED_CIRCLE:
case FILLED_ELLIPSE:
case SIMPLE_LINE:
case SIMPLE_ARROW:
{
catchPrimitivesPoints();
}
break; |
Pour les objets définis ci-dessus, une méthode appelée catchPrimitives est appelée. Elle permettra, via certains algorithmes (vus dans une prochaine partie), de savoir si un objet est survolé par le curseur de la souris.
L'objet en train d'être "prévisualisé" est appelé aDrawnObject.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
case RANDOM_LINE:
static bool adding_circle = false;
if (adding_circle)
{
aDrawnObject.anObjectType = selectedObject;
aDrawnObject.objectPoints.push_back(mouse_pos_in_image);
for (int i = 0 ; i < aDrawnObject.objectPoints.size(); i++)
{
ImGui::GetOverlayDrawList()->AddCircleFilled(ImVec2(mp_TextCanvas->image_pos.x + aDrawnObject.objectPoints[i].x,
mp_TextCanvas->image_pos.y + aDrawnObject.objectPoints[i].y),
aDrawnObject.thickness,
aDrawnObject.objBackgroundColor,
8);
}
if (!ImGui::GetIO().MouseDown[0])
{ |
On vient de relâcher le bouton gauche de la souris (souris relâchée !)
1 2 3 4 5
|
adding_circle = false;
if (getIsAnObjectSelected() == false)
currentlyDrawnObjects.push_back(aDrawnObject); |
ci-dessus : on a capturé les caractéristiques de l'objet qui sera ajouté à la pile des objets à dessiner.
On prépare le tour suivant : il faut nettoyer l'objet utilisé pour la prochaine saisie.
1 2 3 4 5 6 7 8 9 10 11 12 13
|
while (!aDrawnObject.objectPoints.empty())
{
aDrawnObject.objectPoints.pop_back();
}
}
}
if (ImGui::IsItemHovered())
{
if ( (ImGui::IsMouseClicked(0)||ImGui::IsMouseClicked(1)) && !ImGui::IsMouseDragging(0) )
{ |
L'utilisateur n'a pas relâché le bouton de la souris, mais il s'est arrêté : on arrête d'ajouter des points.
1 2 3 4 5 6 7 8 9 10
|
adding_circle = false;
}
if ( (ImGui::IsMouseClicked(0)||ImGui::IsMouseClicked(1)) && ImGui::IsMouseDragging(0) )
adding_circle = true;
if ( (!adding_circle && ImGui::IsMouseClicked(0)) )
{ |
ci-dessus, l'utilisateur a cliqué ET il n'était PAS en train de dessiner : on part de ce point
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
|
if (getIsAnObjectSelected() == false)
aDrawnObject.objectPoints.push_back(mouse_pos_in_image);
adding_circle = true;
}
}
break;
case RANDOM_ARROW:
static bool adding_circle2 = false;
if (adding_circle2)
{
aDrawnObject.anObjectType = selectedObject;
arrow_points.push_back(mouse_pos_in_image);
for (int i = 0 ; i < arrow_points.size(); i++)
{
ImGui::GetOverlayDrawList()->AddCircleFilled( ImVec2(mp_TextCanvas->image_pos.x + arrow_points[i].x, mp_TextCanvas->image_pos.y + arrow_points[i].y), aDrawnObject.thickness, aDrawnObject.objBackgroundColor, 8);
}
if (!ImGui::GetIO().MouseDown[0])
{ |
Souris relâchée ! Le second point est maintenant défini, et on va calculer les points (pendant la prévisualisation pour optimiser, et on va ajouter l'objet avec toutes ses caractéristiques dans le vecteur des objets à dessiner. Le fait de pré-calculer certaines caractéristiques permet de tout dessiner très vite (sinon, on aurait à refaire tous ces calculs à chaque tour de boucle !!
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
|
adding_circle2 = false;
aDrawnObject.objectPoints.push_back(arrow_points[0]);
aDrawnObject.objectPoints.push_back(arrow_points [(int)(arrow_points.size()/3.0f)]);
aDrawnObject.objectPoints.push_back(arrow_points[(int)((2*arrow_points.size())/3.0f)]);
aDrawnObject.objectPoints.push_back(arrow_points[arrow_points.size()-1]);
aDrawnObject.P1P4 = sqrtf( (aDrawnObject.objectPoints[1].x - aDrawnObject.objectPoints[0].x)
*(aDrawnObject.objectPoints[1].x - aDrawnObject.objectPoints[0].x)
+ (aDrawnObject.objectPoints[1].y - aDrawnObject.objectPoints[0].y)
*(aDrawnObject.objectPoints[1].y - aDrawnObject.objectPoints[0].y));
if (getIsAnObjectSelected() == false)
currentlyDrawnObjects.push_back(aDrawnObject);
arrow_points.clear();
aDrawnObject.objectPoints.clear();
}
}
if (ImGui::IsItemHovered())
{
if ( (ImGui::IsMouseClicked(0)||ImGui::IsMouseClicked(1)) && !ImGui::IsMouseDragging(0) )
adding_circle2 = false;
if ( (ImGui::IsMouseClicked(0)||ImGui::IsMouseClicked(1)) && ImGui::IsMouseDragging(0) )
adding_circle2 = true;
if ( (!adding_circle2 && ImGui::IsMouseClicked(0)) )
{
if (getIsAnObjectSelected() == false)
arrow_points.push_back(mouse_pos_in_image);
adding_circle2 = true;
}
}
break;
//case TEXT_OBJECT:
case SELECT_CURSOR:
case NOT_A_DRAWN_OBJECT:
{
if (adding_rect)
{
adding_preview1 = true;
zoom_area_points.push_back(mouse_pos_in_image); // catch the second point
if (!ImGui::GetIO().MouseDown[0])
adding_rect = adding_preview1 = false;
}
if (ImGui::IsItemHovered())
{
if ( (((ImGui::IsMouseClicked(0)||ImGui::IsMouseClicked(1) ) && (!zoom_area_points.empty()))) && !ImGui::IsMouseDragging(0) )
{
adding_rect = false;
adding_preview1 = false;
zoom_area_points.pop_back();
zoom_area_points.pop_back();
}
if ( (!adding_rect && ImGui::IsMouseClicked(0)) )
{
zoom_area_points.push_back(mouse_pos_in_image);
adding_rect = true;
}
}
updateSelectedArea(zoom_area_points, color, outline_thickness);
reorder_points(&topLeft, &bottomRight);
if (adding_preview1)
zoom_area_points.pop_back();
}
break;
case TEXT_OBJECT:
default:
break;
}
} |
Pour le code complet , voir la méthode canvas::preview
Suivant : partie2 : la classe canvas