Permettre le déplacement individuel des nœuds du graphe #64

Closed
opened 2026-07-21 17:37:36 +02:00 by fy59 · 0 comments
Owner

Permettre le déplacement individuel des nœuds du graphe

Contexte

La vue graphique en lecture seule permet désormais :

  • l’affichage des entités et des relations ;
  • le zoom avec Ctrl + défilement à deux doigts ;
  • le déplacement de l’ensemble du canvas avec Shift + glisser.

Les positions des nœuds sont actuellement recalculées à partir d’une disposition déterministe en grille. Aucun nœud ne possède encore de position modifiable pendant l’utilisation.

La prochaine étape consiste à permettre le déplacement individuel d’un nœud avec un glisser simple.

Objectif

Permettre à l’utilisateur de déplacer une entité dans le canvas en maintenant le bouton principal sur son nœud puis en faisant glisser le pointeur.

Pendant le déplacement :

  • seul le nœud sélectionné doit bouger ;
  • les relations reliées au nœud doivent suivre immédiatement ;
  • le zoom et le déplacement global du canvas doivent continuer de fonctionner ;
  • aucune donnée métier ne doit être modifiée.

Interactions attendues

Déplacement d’un nœud

Glisser simple sur un nœud → déplacer ce nœud

Le geste doit :

  • commencer uniquement lorsque le pointeur se trouve dans les limites d’un nœud ;
  • conserver la position du pointeur dans le nœud afin d’éviter un saut au début du glisser ;
  • déplacer le nœud dans les coordonnées logiques du graphe ;
  • tenir compte du zoom courant ;
  • mettre à jour le rendu à chaque mouvement ;
  • terminer proprement au relâchement.

Déplacement global du canvas

Le comportement existant reste inchangé :

Shift + glisser → déplacer tout le canvas

Un glisser avec Shift ne doit jamais déplacer un nœud individuellement.

Zoom

Le comportement existant reste inchangé :

Ctrl + défilement à deux doigts → zoom

Le déplacement individuel doit rester correct quel que soit le facteur de zoom.

Architecture attendue

Positions privées à la vue

InvestigationGraphModel reste inchangé.

Les positions appartiennent exclusivement à InvestigationGraphView.

La vue doit conserver une structure privée par entité, par exemple :

typedef struct
{
    const EntityRecord *entity_record;

    double x;
    double y;
} InvestigationGraphNodeLayout;

Les positions ne doivent plus être recréées à chaque appel du callback de dessin.

Elles doivent être conservées dans une collection privée de la vue :

GPtrArray *node_layouts;
GHashTable *node_layouts_by_identifier;

Les noms exacts peuvent évoluer, mais les responsabilités doivent rester identiques.

Cycle de vie de la disposition

Lors de investigation_graph_view_set_graph() :

  1. supprimer toute disposition appartenant à l’ancien graphe ;
  2. emprunter le nouveau graphe ;
  3. créer une disposition initiale déterministe ;
  4. créer l’index UUID vers position ;
  5. réinitialiser la navigation ;
  6. demander un nouveau rendu.

Lors de investigation_graph_view_clear() :

  • supprimer toute disposition privée ;
  • détacher le graphe ;
  • annuler un éventuel déplacement en cours ;
  • réinitialiser la navigation.

Lors de investigation_graph_view_free() :

  • libérer la disposition et ses index ;
  • détacher les contrôleurs GTK ;
  • ne jamais libérer les EntityRecord empruntés.

Conversion des coordonnées

Les événements GTK fournissent des coordonnées dans l’espace écran du widget.

Le déplacement des nœuds utilise les coordonnées logiques du graphe.

La conversion attendue est :

logical_x = (screen_x - offset_x) / zoom;
logical_y = (screen_y - offset_y) / zoom;

Le facteur de zoom doit être vérifié avant toute division.

Détection du nœud sous le pointeur

Une fonction privée doit rechercher le nœud contenant une position logique :

static InvestigationGraphNodeLayout *
investigation_graph_view_find_node_at(
    InvestigationGraphView *graph_view,
    double logical_x,
    double logical_y
);

La recherche doit utiliser les dimensions réelles des nœuds.

Lorsque plusieurs nœuds se chevauchent, le nœud dessiné au premier plan doit être sélectionné en priorité. Une recherche depuis la fin du tableau vers le début est suffisante.

État du déplacement

La vue peut conserver au minimum :

InvestigationGraphNodeLayout *dragged_node;

double node_drag_pointer_offset_x;
double node_drag_pointer_offset_y;

gboolean node_dragging;

Le pointeur dragged_node est emprunté à la collection privée de disposition.

Il doit être remis à NULL lors :

  • de la fin du geste ;
  • du changement de graphe ;
  • du nettoyage de la vue ;
  • d’une annulation GTK.

Priorité des gestes

L’ordre de décision doit être clair :

  1. Shift maintenu : déplacement global du canvas ;
  2. glisser simple commencé sur un nœud : déplacement individuel ;
  3. glisser simple commencé dans le vide : aucun déplacement.

Le glisser simple dans le vide ne doit pas déplacer le canvas.

Rendu

Le callback de dessin doit utiliser la disposition conservée par la vue.

Pendant le déplacement :

  • le nœud doit être redessiné à sa nouvelle position ;
  • les relations entrantes et sortantes doivent suivre ;
  • le nœud déplacé peut être dessiné au premier plan ;
  • une différence visuelle légère peut indiquer qu’il est actif.

Aucune animation n’est exigée.

Réinitialisation

Ajouter une API publique pour restaurer la disposition initiale :

void investigation_graph_view_reset_layout(
    InvestigationGraphView *graph_view
);

Cette fonction doit :

  • recalculer la disposition déterministe ;
  • supprimer les déplacements manuels de la session ;
  • conserver le graphe courant ;
  • conserver ou réinitialiser la navigation selon le choix documenté dans l’implémentation.

Pour cette première version, il est recommandé de conserver le zoom et le déplacement global du canvas.

Gestion de la mémoire

  • Les EntityRecord restent empruntés au graphe.
  • Les structures de disposition appartiennent à la vue.
  • La table d’index ne doit pas posséder les EntityRecord.
  • Les clés UUID peuvent être empruntées aux entités tant que le graphe reste valide, ou copiées si cela simplifie le cycle de vie.
  • Aucun objet GTK ne doit être créé pendant un déplacement.
  • Aucun tableau ne doit être reconstruit à chaque mouvement du pointeur.
  • Aucun pointeur vers l’ancien graphe ne doit subsister après set_graph() ou clear().

Gestion des erreurs

En cas d’échec lors de la construction de la disposition :

  • ne pas conserver une disposition partielle ;
  • afficher un message explicite dans la vue ;
  • conserver l’application fonctionnelle ;
  • ne provoquer aucune fuite mémoire ;
  • ne pas laisser les gestes actifs sur un état incohérent.

Hors périmètre

Ce ticket n’inclut pas :

  • persistance des positions dans SQLite ;
  • sélection permanente d’une entité ;
  • ouverture d’une fiche au clic ;
  • déplacement de plusieurs nœuds simultanément ;
  • sélection rectangulaire ;
  • alignement automatique ;
  • magnétisme sur une grille ;
  • annulation/rétablissement ;
  • raccourcis clavier avancés ;
  • création ou modification de relation ;
  • algorithme de placement par forces.

Ces fonctions feront l’objet de tickets séparés.

Critères d’acceptation

  • Un glisser simple sur un nœud déplace uniquement ce nœud.
  • Un glisser simple dans le vide ne déplace rien.
  • Shift + glisser continue de déplacer tout le canvas.
  • Le déplacement individuel fonctionne avec tous les niveaux de zoom autorisés.
  • Le nœud ne saute pas au début du glisser.
  • Les relations suivent le nœud pendant le déplacement.
  • Les autres nœuds restent immobiles.
  • Le changement de graphe détruit l’ancienne disposition.
  • investigation_graph_view_clear() annule tout déplacement actif.
  • investigation_graph_view_reset_layout() restaure la disposition initiale.
  • Aucun composant de vue ne modifie InvestigationGraphModel.
  • Aucun avertissement GTK ou Cairo n’apparaît.
  • make clean && make réussit avec -Werror -Wpedantic.
  • make test réussit.
  • git diff --check ne signale aucune erreur.

Plan d’implémentation conseillé

  1. Déplacer la disposition depuis le callback de dessin vers l’état privé de la vue.
  2. Ajouter les fonctions de création et de destruction de disposition.
  3. Ajouter l’index UUID vers InvestigationGraphNodeLayout.
  4. Adapter le rendu pour utiliser la disposition conservée.
  5. Ajouter la conversion écran vers coordonnées logiques.
  6. Ajouter la détection du nœud sous le pointeur.
  7. Ajouter l’état de déplacement individuel.
  8. Faire cohabiter le glisser simple et Shift + glisser.
  9. Ajouter investigation_graph_view_reset_layout().
  10. Tester le changement de graphe et les erreurs de construction.

Validation manuelle

  1. Ouvrir une enquête contenant plusieurs entités.
  2. Déplacer un nœud avec un glisser simple.
  3. Vérifier que ses relations suivent.
  4. Vérifier que les autres nœuds ne bougent pas.
  5. Zoomer puis déplacer un nœud.
  6. Déplacer le canvas avec Shift + glisser.
  7. Commencer un glisser dans le vide.
  8. Réinitialiser la disposition.
  9. Ouvrir une autre enquête.
  10. Fermer l’application après plusieurs déplacements.
# Permettre le déplacement individuel des nœuds du graphe ## Contexte La vue graphique en lecture seule permet désormais : - l’affichage des entités et des relations ; - le zoom avec `Ctrl` + défilement à deux doigts ; - le déplacement de l’ensemble du canvas avec `Shift` + glisser. Les positions des nœuds sont actuellement recalculées à partir d’une disposition déterministe en grille. Aucun nœud ne possède encore de position modifiable pendant l’utilisation. La prochaine étape consiste à permettre le déplacement individuel d’un nœud avec un glisser simple. ## Objectif Permettre à l’utilisateur de déplacer une entité dans le canvas en maintenant le bouton principal sur son nœud puis en faisant glisser le pointeur. Pendant le déplacement : - seul le nœud sélectionné doit bouger ; - les relations reliées au nœud doivent suivre immédiatement ; - le zoom et le déplacement global du canvas doivent continuer de fonctionner ; - aucune donnée métier ne doit être modifiée. ## Interactions attendues ### Déplacement d’un nœud ```text Glisser simple sur un nœud → déplacer ce nœud ``` Le geste doit : - commencer uniquement lorsque le pointeur se trouve dans les limites d’un nœud ; - conserver la position du pointeur dans le nœud afin d’éviter un saut au début du glisser ; - déplacer le nœud dans les coordonnées logiques du graphe ; - tenir compte du zoom courant ; - mettre à jour le rendu à chaque mouvement ; - terminer proprement au relâchement. ### Déplacement global du canvas Le comportement existant reste inchangé : ```text Shift + glisser → déplacer tout le canvas ``` Un glisser avec `Shift` ne doit jamais déplacer un nœud individuellement. ### Zoom Le comportement existant reste inchangé : ```text Ctrl + défilement à deux doigts → zoom ``` Le déplacement individuel doit rester correct quel que soit le facteur de zoom. ## Architecture attendue ### Positions privées à la vue `InvestigationGraphModel` reste inchangé. Les positions appartiennent exclusivement à `InvestigationGraphView`. La vue doit conserver une structure privée par entité, par exemple : ```c typedef struct { const EntityRecord *entity_record; double x; double y; } InvestigationGraphNodeLayout; ``` Les positions ne doivent plus être recréées à chaque appel du callback de dessin. Elles doivent être conservées dans une collection privée de la vue : ```c GPtrArray *node_layouts; GHashTable *node_layouts_by_identifier; ``` Les noms exacts peuvent évoluer, mais les responsabilités doivent rester identiques. ### Cycle de vie de la disposition Lors de `investigation_graph_view_set_graph()` : 1. supprimer toute disposition appartenant à l’ancien graphe ; 2. emprunter le nouveau graphe ; 3. créer une disposition initiale déterministe ; 4. créer l’index UUID vers position ; 5. réinitialiser la navigation ; 6. demander un nouveau rendu. Lors de `investigation_graph_view_clear()` : - supprimer toute disposition privée ; - détacher le graphe ; - annuler un éventuel déplacement en cours ; - réinitialiser la navigation. Lors de `investigation_graph_view_free()` : - libérer la disposition et ses index ; - détacher les contrôleurs GTK ; - ne jamais libérer les `EntityRecord` empruntés. ## Conversion des coordonnées Les événements GTK fournissent des coordonnées dans l’espace écran du widget. Le déplacement des nœuds utilise les coordonnées logiques du graphe. La conversion attendue est : ```c logical_x = (screen_x - offset_x) / zoom; logical_y = (screen_y - offset_y) / zoom; ``` Le facteur de zoom doit être vérifié avant toute division. ## Détection du nœud sous le pointeur Une fonction privée doit rechercher le nœud contenant une position logique : ```c static InvestigationGraphNodeLayout * investigation_graph_view_find_node_at( InvestigationGraphView *graph_view, double logical_x, double logical_y ); ``` La recherche doit utiliser les dimensions réelles des nœuds. Lorsque plusieurs nœuds se chevauchent, le nœud dessiné au premier plan doit être sélectionné en priorité. Une recherche depuis la fin du tableau vers le début est suffisante. ## État du déplacement La vue peut conserver au minimum : ```c InvestigationGraphNodeLayout *dragged_node; double node_drag_pointer_offset_x; double node_drag_pointer_offset_y; gboolean node_dragging; ``` Le pointeur `dragged_node` est emprunté à la collection privée de disposition. Il doit être remis à `NULL` lors : - de la fin du geste ; - du changement de graphe ; - du nettoyage de la vue ; - d’une annulation GTK. ## Priorité des gestes L’ordre de décision doit être clair : 1. `Shift` maintenu : déplacement global du canvas ; 2. glisser simple commencé sur un nœud : déplacement individuel ; 3. glisser simple commencé dans le vide : aucun déplacement. Le glisser simple dans le vide ne doit pas déplacer le canvas. ## Rendu Le callback de dessin doit utiliser la disposition conservée par la vue. Pendant le déplacement : - le nœud doit être redessiné à sa nouvelle position ; - les relations entrantes et sortantes doivent suivre ; - le nœud déplacé peut être dessiné au premier plan ; - une différence visuelle légère peut indiquer qu’il est actif. Aucune animation n’est exigée. ## Réinitialisation Ajouter une API publique pour restaurer la disposition initiale : ```c void investigation_graph_view_reset_layout( InvestigationGraphView *graph_view ); ``` Cette fonction doit : - recalculer la disposition déterministe ; - supprimer les déplacements manuels de la session ; - conserver le graphe courant ; - conserver ou réinitialiser la navigation selon le choix documenté dans l’implémentation. Pour cette première version, il est recommandé de conserver le zoom et le déplacement global du canvas. ## Gestion de la mémoire - Les `EntityRecord` restent empruntés au graphe. - Les structures de disposition appartiennent à la vue. - La table d’index ne doit pas posséder les `EntityRecord`. - Les clés UUID peuvent être empruntées aux entités tant que le graphe reste valide, ou copiées si cela simplifie le cycle de vie. - Aucun objet GTK ne doit être créé pendant un déplacement. - Aucun tableau ne doit être reconstruit à chaque mouvement du pointeur. - Aucun pointeur vers l’ancien graphe ne doit subsister après `set_graph()` ou `clear()`. ## Gestion des erreurs En cas d’échec lors de la construction de la disposition : - ne pas conserver une disposition partielle ; - afficher un message explicite dans la vue ; - conserver l’application fonctionnelle ; - ne provoquer aucune fuite mémoire ; - ne pas laisser les gestes actifs sur un état incohérent. ## Hors périmètre Ce ticket n’inclut pas : - persistance des positions dans SQLite ; - sélection permanente d’une entité ; - ouverture d’une fiche au clic ; - déplacement de plusieurs nœuds simultanément ; - sélection rectangulaire ; - alignement automatique ; - magnétisme sur une grille ; - annulation/rétablissement ; - raccourcis clavier avancés ; - création ou modification de relation ; - algorithme de placement par forces. Ces fonctions feront l’objet de tickets séparés. ## Critères d’acceptation - [x] Un glisser simple sur un nœud déplace uniquement ce nœud. - [x] Un glisser simple dans le vide ne déplace rien. - [x] `Shift` + glisser continue de déplacer tout le canvas. - [x] Le déplacement individuel fonctionne avec tous les niveaux de zoom autorisés. - [x] Le nœud ne saute pas au début du glisser. - [x] Les relations suivent le nœud pendant le déplacement. - [x] Les autres nœuds restent immobiles. - [x] Le changement de graphe détruit l’ancienne disposition. - [x] `investigation_graph_view_clear()` annule tout déplacement actif. - [x] `investigation_graph_view_reset_layout()` restaure la disposition initiale. - [x] Aucun composant de vue ne modifie `InvestigationGraphModel`. - [x] Aucun avertissement GTK ou Cairo n’apparaît. - [x] `make clean && make` réussit avec `-Werror -Wpedantic`. - [x] `make test` réussit. - [x] `git diff --check` ne signale aucune erreur. ## Plan d’implémentation conseillé 1. Déplacer la disposition depuis le callback de dessin vers l’état privé de la vue. 2. Ajouter les fonctions de création et de destruction de disposition. 3. Ajouter l’index UUID vers `InvestigationGraphNodeLayout`. 4. Adapter le rendu pour utiliser la disposition conservée. 5. Ajouter la conversion écran vers coordonnées logiques. 6. Ajouter la détection du nœud sous le pointeur. 7. Ajouter l’état de déplacement individuel. 8. Faire cohabiter le glisser simple et `Shift` + glisser. 9. Ajouter `investigation_graph_view_reset_layout()`. 10. Tester le changement de graphe et les erreurs de construction. ## Validation manuelle 1. Ouvrir une enquête contenant plusieurs entités. 2. Déplacer un nœud avec un glisser simple. 3. Vérifier que ses relations suivent. 4. Vérifier que les autres nœuds ne bougent pas. 5. Zoomer puis déplacer un nœud. 6. Déplacer le canvas avec `Shift` + glisser. 7. Commencer un glisser dans le vide. 8. Réinitialiser la disposition. 9. Ouvrir une autre enquête. 10. Fermer l’application après plusieurs déplacements.
fy59 closed this issue 2026-07-21 17:54:13 +02:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: fy59/labfy-investigation#64
No description provided.