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

ericb2

[Actualité] Écriture d'un canvas en C++, avec Dear ImGui (PARTIE 1 sur 8)

Noter ce billet
par , 30/08/2020 à 18h06 (9482 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


Nom : custom_rendering.png
Affichages : 5197
Taille : 39,9 Ko


La seconde image présente les primitives (formes de base).

Nom : canvas_primitives.png
Affichages : 4702
Taille : 48,8 Ko
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

Nom : principe_calcul.png
Affichages : 4934
Taille : 43,5 Ko

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.

Nom : coordonnees_point_C.png
Affichages : 4988
Taille : 34,8 Ko

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.

Nom : implementation2.png
Affichages : 4461
Taille : 307,8 Ko

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 :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
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é


Code : Sélectionner tout - Visualiser dans une fenêtre à part
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

Code : Sélectionner tout - Visualiser dans une fenêtre à part
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.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 !)

Code : Sélectionner tout - Visualiser dans une fenêtre à part
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.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
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.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 !!

Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
Miniatures attachées Images attachées    

Envoyer le billet « Écriture d'un canvas en C++, avec Dear ImGui    (PARTIE 1 sur 8) » dans le blog Viadeo Envoyer le billet « Écriture d'un canvas en C++, avec Dear ImGui    (PARTIE 1 sur 8) » dans le blog Twitter Envoyer le billet « Écriture d'un canvas en C++, avec Dear ImGui    (PARTIE 1 sur 8) » dans le blog Google Envoyer le billet « Écriture d'un canvas en C++, avec Dear ImGui    (PARTIE 1 sur 8) » dans le blog Facebook Envoyer le billet « Écriture d'un canvas en C++, avec Dear ImGui    (PARTIE 1 sur 8) » dans le blog Digg Envoyer le billet « Écriture d'un canvas en C++, avec Dear ImGui    (PARTIE 1 sur 8) » dans le blog Delicious Envoyer le billet « Écriture d'un canvas en C++, avec Dear ImGui    (PARTIE 1 sur 8) » dans le blog MySpace Envoyer le billet « Écriture d'un canvas en C++, avec Dear ImGui    (PARTIE 1 sur 8) » dans le blog Yahoo

Mis à jour 03/09/2020 à 20h09 par ericb2

Catégories
Programmation , C , C++ , 2D / 3D / Jeux

Commentaires

  1. Avatar de MaximeCh
    • |
    • permalink
    merci
  2. Avatar de ericb2
    • |
    • permalink
    Citation Envoyé par MaximeCh
    merci
    Bonjour,

    C'est moi. Surtout, n'hésitez pas si vous avez des questions, car je ne suis pas certain d'avoir été très clair.

    Par ailleurs, j'attends un peu pour publier les parties suivantes (partie 3 à venir), parce que j'ai un peu l'impression de spammer le site ;-)
  3. Avatar de pamlinux
    • |
    • permalink
    Bonjour,

    Je trouve effectivement le sujet très intéressant et je constate également qu'il est difficile de trouver un canevas écrit en C/C++. Merci pour les 2 premières parties et y aura-t-il les 6 autres annoncée initialement?
  4. Avatar de ericb2
    • |
    • permalink
    Citation Envoyé par pamlinux
    Bonjour,

    Je trouve effectivement le sujet très intéressant et je constate également qu'il est difficile de trouver un canevas écrit en C/C++. Merci pour les 2 premières parties et y aura-t-il les 6 autres annoncée initialement?
    Bonjour,

    Oui, je vais continuer.

    En fait, j'ai publié les précédentes parties trop rapidement. Entre temps, j'ai eu une grosse surcharge de travail (mon vrai boulot), et je n'ai simplement pas trouvé le temps de rédiger la suite.

    À toutes fins utiles, je rappelle, en tant que bénévole qui partage gracieusement ses maigres connaissances sur le sujet, que ce qui est important, c'est de dire ce qui ne va pas, ou qui pourrait être amélioré (de façon constructive, merci).

    À suivre (prochainement)
  5. Avatar de d'Oursse
    • |
    • permalink
    Citation Envoyé par pamlinux
    Bonjour,

    Je trouve effectivement le sujet très intéressant et je constate également qu'il est difficile de trouver un canevas écrit en C/C++. Merci pour les 2 premières parties et y aura-t-il les 6 autres annoncée initialement?
    Il existe un canvas stateful écrit en C, il se nomme Evas et fait partie des EFL