Outil (simplifié) de visualisation et d'édition de dessins vectoriels

Table des matières

1 Introduction

L'objectif de cet exercice est de créer un outil permettant de visualiser et éditer des dessins vectoriels. Pour simplifier l'exercice, cet outil ne permet de gérer que des cercles. Il est capable de charger des dessins, stockés dans des fichiers formatés en XML, dont visage.xml est un exemple :

<?xml version="1.0"?>
<!-- Dans cette version de format, les coordonnees (x,y) sont exprimees dans un -->
<!-- repere dont le centre est au centre de l'ecran, les valeurs positives de x -->
<!-- etant vers la droite, les valeurs positives de y etant vers le haut.       --> 
<Drawing>
    <!-- Le noeud suivant cree un cercle dont le centre est en (0,0), -->
    <!-- de rayon 200, de couleur noir, qui a pour etiquette          -->
    <!-- "contourVisage"                                              -->
    <Circle label="contourVisage" x="0" y="0" r="200" color="Black"/>
    <Circle label="nez" x="0" y="0" r="20" color="Red"/>
    <!-- Le noeud suivant cree un groupe "oreilles" positionne en (0,210) -->
    <Group label="oreilles" x="0" y="210">
	<Group label="oreille" x="-210" y="0">
	    <Circle label="c1" x="0" y="0" r="100" color="Black"/>
	    <Circle label="c2" x="0" y="0" r="70" color="Magenta"/>
	</Group>
	<Group label="oreille2" x="210" y="0">
	    <Circle label="c1" x="0" y="0" r="100" color="Black"/>
	    <Circle label="c2" x="0" y="0" r="70" color="Magenta"/>
	</Group>
    </Group>
</Drawing>

Pour réaliser cet exercice, nous allons procéder par étapes, la visualisation graphique n'arrivant qu'à l'étape 8.

NB :

  1. Tout au long de cet exercice, nous supposerons que le fichier XML ou les chaînes de caractère contenant du XML sont correctement formatées.
  2. Cette page contient des liens vers des corrigés. Ces corrigés ne seront mis à disposition qu'au fur et à mesure de l'avancement du groupe CSC4526 dans l'exercice.

2 Création du projet

Cet exercice est fait en TDD (Test Driven Development). Donc, appliquez la procédure cmake de ce document au canevas SampleGoogleTestPugixml.zip (qui contient les références aux environnements GoogleTest et pugixml dont nous nous servirons dans cet exercice).

3 Codage du test unitaire sur la lecture des Circle

  • Dans le répertoire src, créez un fichier Circle.h déclarant une classe Circle et un fichier Circle.cpp la définissant. NB :
    • Codez deux constructeurs
      • Le premier constructeur vous permet d'initialiser chacun des paramètres de Circle avec des valeurs fournies en paramètre.
      • Le deuxième constructeur vous permet de faire vos initialisations à partir d'un seul paramètre de type pugi::xml_node (Pour la lecture de la string XML, nous vous proposons d'utiliser pugixml : il faut donc penser à faire un #include "pugixml.hpp"). Pour l'instant, laissez le corps de ce constructeur vide (vous vous occuperez de son implémentation à la question suivante).
    • Dans votre .h, pensez à ajouter la déclaration bool operator==(const Circle &) const = default; pour vous éviter des comparaisons champ par champ dans les tests unitaires que vous allez écrire.
  • Dans unitTests/unitTests.cpp renommez le test TEST(TestCaseName, TestName) en TEST(TestReadXML, TestCircle)
  • Dans ce test, stockez le XML suivant dans une string :
std::string s = R"(<?xml version = "1.0"?>
		   <Circle label="testCircle" x="0" y="1" r="2" color="Black"/>)";
  • Complétez votre test en y ajoutant :
    • la création d'une instance c de la classe Circle initialisée avec {pugi::xml_node{}} (i.e. un noeud vide).
    • la création d'une instance c_ref de la classe Circle initialisée avec {"testCircle", 0, 1, 2, "Black"}
    • un EXPECT_EQ(c, c_ref);
  • Vérifiez que le test unitaire est KO.
  • La section suivante va affiner ce test pour que l'instance c soit correctement créée à partir de la string au format XML.

Corrigé (à exploiter selon la procédure cmake de ce document).

4 Codage de la lecture des Circle

Dans cette section, nous complétons les différents fichiers pour que le test devienne OK :

  • Dans votre test unitaire, remplacez la ligne définissant c par les lignes suivantes (inspirées du Quickstart pugixml) :
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_string(xml.c_str());
ASSERT_TRUE(result) << result.description(); // Si jamais result est faux, indique que le test est faux *et* affiche la string result.description() (qui contient la raison de l'erreur)
Circle c{doc.child("Circle")};
  • Complétez l'implémentation de votre constructeur Circle(pugi::xml_node node) pour que les données d'initialisation soient extraites de xml_node.
  • Vérifiez que votre test unitaire est désormais OK.

Corrigé (à exploiter selon la procédure cmake de ce document).

5 Codage de la lecture des Group

  • Faites le codage du test unitaire TEST(TestReadXML, TestGroup1) avec la string de test :
<?xml version = "1.0"?>
<Group label="testGroup" x="0" y="1">
     <Circle label="testCircle1" x="2" y="3" r="4" color="Black"/>
     <Circle label="testCircle2" x="5" y="6" r="7" color="Black"/>
</Group>
  • Développez les classes nécessaires pour que le test fonctionne correctement.
  • Faites maintenant le codage du test unitaire TEST(TestReadXML, TestGroup2) avec la string de test ci-dessous.
    • NB : Cela pourrait vous amener à une restructuration de votre code. En particulier, il est probable que vous ne puissiez vous appuyez sur bool operator==(const Group &) const = default (comme cela avait été fait dans le corrigé de l'exercice Analyse d'un fichier XML) et que vous deviez vous contenter de faire des tests d'égalité sur les labels et les coordonnées x et y des différents objets lus.
<?xml version = "1.0"?>
<Group label="testGroup1" x="0" y="1">
     <Circle label="testCircle" x="2" y="3" r="4" color="Black"/>
     <Group label="testGroup2" x="5" y="6"/>
</Group>

Corrigé avec des new (à exploiter selon la procédure cmake de ce document).

Corrigé sans fuites mémoire (avec unique_ptr) (à exploiter selon la procédure cmake de ce document).

5.1 Visualisation des fuites mémoires (optionnel ==> à ne faire que si vous êtes dans les temps)

Ca y est, vos deux tests unitaires passent. Mais, il est possible que vous ayez des fuites mémoire dans le code que vous avez écrit. L'objectif de cette section est de les visualiser, mais pas de les corriger (nous vous montrerons en cours comment les corriger en C++ moderne).

  1. Installez un outil de détection de fuite mémoire en suivant la section "Analyseur de fuites mémoire" de cette page.
  2. Utilisez l'outil pour identifier les fuites mémoire dans le code que vous avez écrit.

6 Calcul des coordonnées absolues des éléments des groupes

Soit un groupe G qui contient un élément E (qui peut-être un cercle ou un groupe). Les coordonnées (x,y) de E sont relatives à G. Or, pour dessiner E, nous avons besoin de ses coordonnées absolues. L'objectif de cette section est de s'occuper du calcul des coordonnées absolues :

  1. Remarquez que les coordonnées absolues n'ont pas besoin d'être stockées dans E. En effet, nous en avons seulement besoin au moment où nous souhaitons afficher E en mode texte ou en mode graphique. Comme les coordonnées absolues de E sont simples à calculer, nous n'avons pas besoin de les stocker dans E pour s'économiser un long temps de calcul au moment de l'affichage.
  2. Il nous faut tout de même vérifier que nous savons correctement calculer les coordonnées absolues : écrivez un test TEST(TestAbsoluteCoord, Test1) dans lequel :
    • Vous créez un groupe G1 de coordonnées (0,1) qui contient un groupe G2 de coordonnées (2,3). G2 contient un cercle C21 de coordonnées (4,5) et un cercle C22 de coordonnées (7,8). NB : pour utiliser une nouvelle méthode de création d'objet, dans votre test, ne créez pas ces éléments avec une chaîne XML, mais directement en appelant des constructeurs de ces éléments.
    • Vous appellez une méthode computeAbsolute (utilisée uniquement pour les tests unitaires) sur G1 qui prend différents paramètres que vous choisirez, mais aussi une structure de données spéciale. Cette structure, remplie par computeAbsolute, contient pour G1 et les éléments dans G1 les différentes coordonnées absolues qui ont été calculées.
    • Vous testez que les coordonnées absolues présentes dans cette structure de données sont correctes.
  3. Modifiez votre code pour que le test soit correct.

Corrigé (à exploiter selon la procédure cmake de ce document).

7 Affichage en mode texte de l'arborescence du dessin

Dans cette section, vous modifiez myMain() (et d'autres parties de votre code) pour que le fichier visage.xml (cf. section 1) soit lu et affiche, en mode texte, l'arborescence d'un dessin, avec un numéro en face de chaque élément, comme par exemple :

#0 : Group "root (member of Drawing)" / absolute=(0,0) / relative=(0,0)
#1 : Circle "contourVisage" / absolute=(0,0) / relative=(0,0) / r=200 / color="Black"
#2 : Circle "nez" / absolute=(0,0) / relative=(0,0) / r=20 / color="Red"
#3 : Group "oreilles" / absolute=(0,210) / relative=(0,210)
#4 : Group "oreille" / absolute=(-210,210) / relative=(-210,0)
#5 : Circle "c1" / absolute=(-210,210) / relative=(0,0) / r=100 / color="Black"
#6 : Circle "c2" / absolute=(-210,210) / relative=(0,0) / r=70 / color="Magenta"
#7 : Group "oreille2" / absolute=(210,210) / relative=(210,0)
#8 : Circle "c1" / absolute=(210,210) / relative=(0,0) / r=100 / color="Black"
#9 : Circle "c2" / absolute=(210,210) / relative=(0,0) / r=70 / color="Magenta"

Avant de vous lancer dans le codage, remarquez que visage.xml est un fichier de ressources. Nous allons donc le stocker dans un endroit adéquat et configurerons notre IDE pour qu'il le mette à disposition de votre exécutable. qui doit être stocké au même endroit que le main() de votre programme. Donc :

  • Créez le répertoires resources (NB : un seul "s" à resources, car mot écrit en anglais), puis le fichier resources/visage.xml et son contenu (cf. section 1).
  • Pour que ce fichier soit recopié au bon endroit par rapport à l'exécutable que générera votre IDE, ajoutez à la fin du fichier mainLauncher\CMakeLists.txt (donc, le fichier CMakeLists.txt du sous-répertoire mainLauncher) les lignes :
add_custom_target(copy-resources ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/resources)
# "COMMAND ${CMAKE_COMMAND} -E copy_directory" triggers a generation error with Visual Studio
# So we need to do  the following solution
file(GLOB RESOURCES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/resources/*.*)
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/resources
                   DEPENDS ${CMAKE_SOURCE_DIR}/resources
                   COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/resources
                   COMMAND ${CMAKE_COMMAND} -E copy_if_different
                           ${RESOURCES}
                           ${CMAKE_CURRENT_BINARY_DIR}/resources)
add_dependencies(mainLauncher copy-resources)
  • Exécutez cmake .. (Rappel : sous Windows, cmake -A x64 ..) dans votre répertoire build
  • Dans myMain.cpp, les lignes suivantes vous permettent de charger le contenu de votre fichier resources/visage.xml dans la variable doc :
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file("resources/visage.xml");
if (!result)
{
        std::cerr << "Could not open file visage.xml because " << result.description() << std::endl;
        return 1;
}
  • Logiquement, pour vos tests unitaires, vous ne devriez pas avoir besoin d'accéder à visage.xml (vous devriez pouvoir vous contenter de travailler avec des string qui contiennent le XML que vous souhaitez analyser). Toutefois, si vous souhaitez y accéder quand même, vous devez ajouter les lignes suivantes à la fin de .\CMakeLists.txt :
add_custom_target(copy-resources-unitTest ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/resources)
# "COMMAND ${CMAKE_COMMAND} -E copy_directory" triggers a generation error with Visual Studio
# So we need to do  the following solution
file(GLOB RESOURCES_UNITTESTS CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/resources/*.*)
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/resources
                   DEPENDS ${CMAKE_SOURCE_DIR}/resources
                   COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/resources
                   COMMAND ${CMAKE_COMMAND} -E copy_if_different
                           ${RESOURCES_UNITTESTS}
                           ${CMAKE_CURRENT_BINARY_DIR}/resources)
add_dependencies(unitTests copy-resources-unitTest)

Maintenant, vous pouvez vous lancer dans le codage de cette nouvelle fonction !

Corrigé (à exploiter selon selon la procédure cmake de ce document).

8 Affichage graphique correspondant au contenu d'un fichier

Dans cette section, vous modifiez myMain() (et d'autres parties de votre code) pour que le fichier visage.xml soit lu et affiche, avec SFML, la figure qu'il contient.

Avant de vous lancer dans le codage, vous devez signifier à votre IDE que vous allez utiliser SFML au cours de cette étape.

  • Dans votre fichier CMakeLists.txt, ajoutez les lignes comprises entre ###### DEBUT lignes a rajouter et ###### FIN lignes a rajouter comme dans le modèle suivant (respectez scrupuleusement l'ordre des lignes, sinon vous aurez des soucis au moment de l'édition des liens) :
.......
include(FetchContent)
project(JIN4 VERSION 1.0.0 LANGUAGES CXX)

set (BUILD_SHARED_LIBS FALSE)

###### DEBUT lignes a rajouter
if(APPLE)
  find_package(SFML 2.5 COMPONENTS system window graphics network audio REQUIRED)
  include_directories(${SFML_INCLUDE_DIRS})
else()
  # Linux or Windows
  FetchContent_Declare(
    sfml
    GIT_REPOSITORY https://github.com/SFML/SFML.git
    GIT_TAG 2.5.1
  )
  FetchContent_MakeAvailable(sfml)
endif()
###### FIN lignes a rajouter

set(CMAKE_CXX_STANDARD 20)

add_subdirectory(mainLauncher)
add_subdirectory(src)
.......
  • Dans mainLauncher\CMakeLists.txt, pour spécifier que votre exécutable mainLauncher utilise sfml-graphics, ajoutez sfml-graphics à la ligne target_link_libraries() :
target_link_libraries(mainLauncher PUBLIC src pugixml sfml-graphics)
  • Dans src\CMakeLists.txt, pour spécifier que vos fichiers sources dans src peuvent référencer les fichiers d'include de SFML, ajoutez sfml-graphics à la ligne target_link_libraries() :
target_link_libraries(src PUBLIC pugixml sfml-graphics)
  • Dans unitTests\CMakeLists.txt, pour spécifier que votre exécutable unitTests utilise sfml-graphics, ajoutez sfml-graphics à la ligne target_link_libraries(unitTests src pugixml) :
target_link_libraries(unitTests src pugixml sfml-graphics)
  • Exécutez cmake dans votre répertoire build

Maintenant, vous pouvez vous lancer dans le codage de cette nouvelle fonction !

Corrigé (à exploiter selon la procédure cmake de de ce document)

9 Interactions avec le dessin

Permettez à l'utilisateur d'agir interactivement sur un dessin en choisissant de changer la couleur d'un élément, le déplacer, le copier, ou (seulement si vous avez le temps) le sauvegarder en XML :

  • L'utilisateur sélectionne l'élément (grâce à son numéro dans l'arborescence affichée grâce à l'affichage issu de la section 7)
  • Il complète la commande (choix de la nouvelle couleur, saisie du vecteur de déplacement de l'élément, copie)

Par exemple, l'outil affiche :

#0 : Group "root (member of Drawing)" / absolute=(0,0) / relative=(0,0)
#1 : Circle "contourVisage" / absolute=(0,0) / relative=(0,0) / r=200 / color="Black"
#2 : Circle "nez" / absolute=(0,0) / relative=(0,0) / r=20 / color="Red"
#3 : Group "oreilles" / absolute=(0,210) / relative=(0,210)
#4 : Group "oreille" / absolute=(-210,210) / relative=(-210,0)
#5 : Circle "c1" / absolute=(-210,210) / relative=(0,0) / r=100 / color="Black"
#6 : Circle "c2" / absolute=(-210,210) / relative=(0,0) / r=70 / color="Magenta"
#7 : Group "oreille2" / absolute=(210,210) / relative=(210,0)
#8 : Circle "c1" / absolute=(210,210) / relative=(0,0) / r=100 / color="Black"
#9 : Circle "c2" / absolute=(210,210) / relative=(0,0) / r=70 / color="Magenta"

Rank of object to modify?

Si l'utilisateur entre 1 au clavier, le système lui affiche les différentes options possibles:

1 : Change color
2 : Copy
3 : Translation by (delta_x, delta_y)
4 : Save

etc.

Pour la mise en place de cette interaction, si vous avez le temps, appuyez-vous sur Dear ImGui, un outil graphique utilisé notamment par Ubisoft (par exemple, dans Ghost Reckon, en parallèle de leur jeu) pour ce genre d'interactions. Utilisez son binding avec SFML.

Corrigé avec interactions au clavier (à exploiter selon la procédure cmake de de ce document).

Corrigé avec interactions avec Dear Imgui (à exploiter selon la procédure cmake de de ce document).

10 Notion de symbole réutilisable

Un outil d'édition de dessins vectoriel comme Adobe Illustrator propose la notion de symbole réutilisable. Leur aide en ligne explique : "Un symbole est un objet artistique que vous pouvez réutiliser dans un document. Par exemple, si vous créez un symbole à partir d'une fleur, vous pouvez ajouter plusieurs instances de ce symbole à l'image sans devoir effectivement ajouter l'objet artistique complexe à maintes reprises."

On aimerait bien que l'oreille soit un symbole réutilisable, c'est-à-dire que, quand l'utilisateur choisit d'affecter un élément de l'oreille, toutes les instances de cette oreille dans l'image sont mises à jour.

Quels changements apporter à la structure de donnée et au format de sauvegarde (donc, dans le fichier XML utilisé en entrée de votre programme et en sortie si vous avez implanté la fonction de sauvegarde à l'étape 10) pour y parvenir ?

Corrigé (à exploiter selon la procédure cmake de de ce document).

Date: 29 avril 2022

Auteur: Michel SIMATIC, Loïc JOLY et Amina GUERMOUCHE

Created: 2022-05-10 mar. 12:51

Validate