24 mai 2023
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="oreille1" x="-210" y="0">
<Circle label="c11" x="0" y="0" r="100" color="Black"/>
<Circle label="c12" x="0" y="0" r="70" color="Magenta"/>
</Group>
<Group label="oreille2" x="210" y="0">
<Circle label="c21" x="0" y="0" r="100" color="Black"/>
<Circle label="c22" 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 4.
NB :
Reprenez le code que vous avez obtenu à la fin de l’exercice Analyse d’un fichier XML ou bien utilisez directement son corrigé.
Modifiez le code de la manière suivante :
class
au lieu du mot-clé struct
. Ainsi, désormais, par défaut, les déclarations d’attribut ou de méthode sont privées. Il vous faut donc ajouter le mot-clé public:
pour rendre les déclarations visibles de l’extérieur (et, si besoin, mentionner private:
pour celles que vous souhaitez garder privées).Circle(const pugi::xml_node& node);
et un constructeur Group(const pugi::xml_node& node);
myMain.cpp
et dans unitTests.cpp
, par exemple, en créant un groupe simplement avec :"Group") };` Group g{ doc.child(
myMain.cpp
en ne gardant que :#include "myMain.h"
int myMain()
{return 0;
}
Enfin, vérifiez que vos tests unitaires s’exécutent correctement et que, par conséquent, votre code est toujours en mesure de lire correctement les XML suivants :
<?xml version = "1.0"?>
<Circle label="testCircle" x="0" y="1" r="2" color="Black" />)";
et
<?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>
Corrigé (à exploiter selon la procédure cmake
de ce document).
Dans une démarche TDD (Test Driven Development), commencez par coder le test unitaire TEST(TestReadXML, TestGroupHybrid)
qui :
<?xml version = "1.0"?>
<Group label="testGroupHybrid" x="0" y="1">
<Circle label="testCircle1" x="2" y="3" r="4" color="Black"/>
<Group label="testGroup" x="5" y="6">
<Circle label="testCircle2" x="7" y="8" r="9" color="Black"/>
</Group>
<Circle label="testCircle3" x="10" y="11" r="12" color="Black"/>
</Group>
g
correspondant à cet XMLg.dump()
est égal à une string c_dump_ref
que vous initialisez avec le contenu du dump que vous vous attendez à obtenir, par exemple :Group "testGroupHybrid", x: 0, y: 1, children: [
| Circle "testCircle1", x: 2, y: 3, r: 4, color: "Black"
| Group "testGroup", x: 5, y: 6, children: [
| | Circle "testCircle2", x: 7, y: 8, r: 9, color: "Black"
| ]
| Circle "testCircle3", x: 10, y: 11, r: 12, color: "Black"
]
NB : L’ordre d’apparition des éléments doit être respecté : En particulier, pour ce test, votre code doit afficher Circle "testCircle1"
, puis Group "testGroup"
contenant le Circle "testCircle2"
, et enfin Circle "testCircle3"
. Votre code ne doit pas afficher Circle "testCircle1"
, Circle "testCircle3"
, puis Group "testGroup"
contenant le Circle "testCircle2"
.
Modifiez votre code jusqu’à ce que tous vos tests soient OK.
Corrigé (à exploiter selon la procédure cmake
de ce document).
Ca y est, tous vos tests unitaires de l’étape 1 passent.
Mais, il est possible que vous ayez des fuites mémoire dans le code que vous avez écrit. Vérifiez-le en appliquant la section “Analyse de fuites mémoire” de cette page.
Après le point de cours sur la gestion des fuites mémoire en C++ moderne grâce aux pointeurs “intelligents”, faites évoluer votre code pour éradiquer vos fuites mémoire.
NB : Si vous rencontrez (ou non !) des soucis à convertir vos pointeurs nus en unique_ptr
, consultez ce mini guide de survie avec les unique_ptr
pour en savoir plus.
Corrigé (à exploiter selon la procédure cmake
de ce document).
Dans cette section, vous modifiez myMain()
(et d’autres parties de votre code) pour que le fichier visage.xml
(cf. section “Introduction” de cet énoncé) soit lu et affiche, grâce à un dump()
, l’arborescence d’un dessin.
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 :
resources
(NB : un seul “s” à resources
, car mot écrit en anglais), puis le fichier resources/visage.xml
et son contenu (cf. section “Introduction”).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)
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)
resources/visage.xml
dans la variable doc
:
pugi::xml_document doc;if (auto result = doc.load_file("resources/visage.xml"); !result)
{std::cerr << "Could not open file visage.xml because " << result.description() << std::endl;
return 1;
}
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 à la fin de unitTests\CMakeLists.txt
les même lignes que celles ajoutées pour mainLauncher\CMakeLists.txt
(cf. ci-dessus).Maintenant, vous pouvez coder l’affichage en mode texte de l’arborescence du dessin.
Corrigé (à exploiter selon la procédure cmake
de ce document).
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.
.\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) :
.......set (BUILD_SHARED_LIBS FALSE)
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) # This cmake_policy avoids warning by cmake when we use pugixml
cmake_policy(SET CMP0135 NEW) # This cmake_policy avoids warning by cmake when we fetch contents based on URL
###### 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)
.......
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)
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)
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)
Maintenant, vous pouvez vous lancer dans le codage de cette nouvelle fonction avec un dernier conseil pour la route : Pour dessiner, vous aurez besoin de coordonnées absolues ; calculez-les dynamiquement (ne les stockez pas dans les objets, car cela serait incompatible avec les questions suivantes).
Corrigé (à exploiter selon la procédure cmake
de ce document).
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 :
Par exemple, l’outil affiche :
Drawing [
| Circle "contourVisage", x: 0, y: 0, r: 200, color: "Black"
| Circle "nez", x: 0, y: 0, r: 20, color: "Red"
| Group "oreilles", x: 0, y: 210, children: [
| | Group "oreille1", x: -210, y: 0, children: [
| | | Circle "c11", x: 0, y: 0, r: 100, color: "Black"
| | | Circle "c12", x: 0, y: 0, r: 70, color: "Magenta"
| | ]
| | Group "oreille2", x: 210, y: 0, children: [
| | | Circle "c21", x: 0, y: 0, r: 100, color: "Black"
| | | Circle "c22", x: 0, y: 0, r: 70, color: "Magenta"
| | ]
| ]
]
Label of object to modify (or SAVE to save in save.xml file or EXIT to exit)?
Si l’utilisateur entre contourVisage
au clavier, le système lui affiche les différentes options possibles:
1 : Change color
2 : Copy
3 : Translation by (delta_x, delta_y)
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 ce document).
Corrigé avec interactions avec Dear Imgui (à exploiter selon la procédure cmake
de ce document).
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ées 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) pour y parvenir ?
Corrigé avec shared_ptr
(à exploiter selon la procédure cmake
de ce document).