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

Michel SIMATIC, Loïc JOLY et Amina GUERMOUCHE

5 avril 2023

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="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 :

  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 Etape 0 : Mise en place de la base de code de cet exercice (0h30)

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 :

        Group g{ doc.child("Group") };`
        #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).

3 Etape 1 : Codage de la lecture des groupes hybrides (2h30)

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="testCircle" x="2" y="3" r="4" color="Black"/>
             <Group label="testGroup" x="5" y="6"/>
        </Group>
Group "testGroupHybrid", x: 0, y: 1, children: [
| Circle "testCircle", x: 2, y: 3, r: 4, color: "Black"
| Group "testGroup", x: 5, y: 6, children: [
| ]
]

Modifiez votre code jusqu’à ce que tous vos tests soient OK.

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

4 Etape 2 : Codage de la lecture des groupes hybrides sans fuites mémoire (1h30)

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).

5 Etape 3 : Affichage en mode texte de l’arborescence du dessin (1h30)

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 :

        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)
        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;
        }

Maintenant, vous pouvez coder l’affichage en mode texte de l’arborescence du dessin.

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

6 Etape 4 : Affichage graphique correspondant au contenu d’un fichier (2h00)

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.

        .......
        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)
        .......
        target_link_libraries(mainLauncher PUBLIC src pugixml sfml-graphics)
        target_link_libraries(src PUBLIC pugixml sfml-graphics)
        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).

7 Etape 5 : Interactions avec le dessin (2h00)

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).

8 Etape 6 : Notion de symbole réutilisable (2h00)

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).