2017-05-20 1 views
0

Modifier finalInterdisez collision d'un QGraphicsItem mobile avec d'autres articles

je me suis dit sur la question et a réalisé une solution comme une réponse ci-dessous. Si vous avez trébuché ici de google cherchant un moyen décent de déplacer QGraphicsItems/QGraphicsObjects via l'indicateur ItemIsMovable tout en évitant la collision avec d'autres nœuds, j'ai inclus une méthode de travail itemChange à la toute fin de ma réponse.

Mon projet d'origine traitait des nœuds d'accrochage à une grille arbitraire, mais il est facile à supprimer et pas du tout nécessaire pour que cette solution fonctionne. Du point de vue de l'efficacité, ce n'est pas l'utilisation la plus intelligente des structures de données ou des algorithmes, donc si vous vous retrouvez avec beaucoup de nœuds sur l'écran, vous voudrez peut-être essayer quelque chose de plus intelligent.


Original Question

J'ai un QGraphicsItem sous-classé, le nœud, qui définit les drapeaux pour ItemIsMovable et ItemSendsGeometryChanges. Ces objets Node ont des fonctions shape() et boundingRect() surchargées qui leur donnent un rectangle de base pour se déplacer.

Le déplacement d'un nœud avec la souris appelle la fonction itemChange() comme prévu, et je peux appeler une fonction snapPoint() qui force le nœud déplacé à toujours s'accrocher à une grille. (Ceci est une fonction simple qui retourne un nouveau QPointF basé sur un paramètre donné, où le nouveau point est restreint à la coordonnée la plus proche sur la grille d'alignement réelle). Cela fonctionne parfaitement comme c'est maintenant.

Mon gros problème est que je voudrais éviter d'enclencher dans une position qui entre en collision avec d'autres nœuds. C'est-à-dire, si je déplace un nœud dans un emplacement résultant (accroché à la grille), j'aimerais que la liste collidingItems() soit complètement vide.

Je peux détecter avec précision le collidingItems après que le drapeau ItemPositionHasChanged soit levé dans itemChange(), mais malheureusement, cela se produit après que le nœud a déjà été déplacé. Le nœud est revenu du itemChange() initial (ce qui est mauvais puisque le nœud a déjà été déplacé dans un emplacement invalide qui chevauche certains autres nœuds).

est ici la fonction itemChange() j'ai jusqu'à présent:

QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value) 
{ 
    if (change == ItemPositionChange && scene()) 
    { 
     // Snap the value to the grid 
     QPointF pt = value.toPointF(); 
     QPointF snapped = snapPoint(pt); 

     // How much we've moved by 
     qreal delX = snapped.x() - pos().x(); 
     qreal delY = snapped.y() - pos().y(); 

     // We didn't move enough to justify a new snapped position 
     // Visually, this results in no changes and no collision testing 
     // is required, so we can quit early 
     if (delX == 0 && delY == 0) 
      return snapped; 

     // If we're here, then we know the Node's position has actually 
     // updated, and we know exactly how far it's moved based on delX 
     // and delY (which is constrained to the snap grid dimensions) 
     // for example: delX may be -16 and delY may be 0 if we've moved 
     // left on a grid of 16px size squares 

     // Ideally, this is the location where I'd like to check for 
     // collisions before returning the snapped point. If I can detect 
     // if there are any nodes colliding with this one now, I can avoid 
     // moving it at all and avoid the ItemPositionHasChanged checks 
     // below 

     return snapped; 
    } 
    else if (change == ItemPositionHasChanged && scene()) 
    { 
     // This gets fired after the Node actually gets moved (once return 
     // snapped gets called above) 

     // The collidingItems list is now validly set and can be compared 
     // but unfortunately its too late, as we've already moved into a 
     // potentially offending position that overlaps other Nodes 

     if (collidingItems().empty()) 
     { 
      // This is okay 
     } 
     else 
     { 
      // This is bad! This is what we wanted to avoid 
     } 
    } 

    return QGraphicsItem::itemChange(change, value); 
} 

Ce que j'ai essayé

J'ai essayé stocker les delX et delY changements, et si nous sommes entrés dans le dernier bloc d'autre ci-dessus, j'ai essayé d'inverser le précédent mouvement avec moveBy(-lastDelX, -lastDelY). Cela a eu des résultats horribles, car cela est appelé alors que la souris est toujours pressée et déplace le nœud lui-même (qui finit par déplacer le nœud plusieurs fois en essayant de contourner une collision, entraînant souvent le nœud voler sur le côté de l'écran très rapidement). J'ai également essayé de stocker les points précédents (connus) et d'appeler un setX et setY sur la position pour revenir en arrière si une collision a été trouvée, mais cela a également eu des problèmes similaires à l'approche ci-dessus.

Conclusion

Si je pouvais trouver un moyen de détecter la collidingItems() avec précision avant le déménagement arrive même, je serais en mesure d'éviter le bloc ItemPositionHasChanged tout à fait, en vous assurant que le ItemPositionChange ne retourne que les points qui sont valide, évitant ainsi toute logique pour passer à une position précédemment valide.

Merci de votre temps, et s'il vous plaît laissez-moi savoir si vous avez besoin de plus de code ou d'exemples pour illustrer le problème mieux.

Edit 1:

Je pense que je suis tombé sur une technique qui va probablement servir de base à la solution réelle, mais je encore besoin de l'aide car il est pas tout à fait travailler.

Dans la fonction itemChange() avant la dernière return snapped;, j'ai ajouté le code suivant:

// Make a copy of the shape and translate it correctly 
// This should be equivalent to the shape formed after a successful 
// move (i.e. after return snapped;) 
QPainterPath path = shape(); 
path.translate(delX, delY); 

// Examine all the other items to see if they collide with the desired 
// shape.... 
for (QGraphicsItem* other : canvas->items()) 
{ 
    if (other == this) // don't care about this node, obviously 
     continue; 

    if (other->collidesWithPath(path)) 
    { 
     // We should hopefully only reach this if another 
     // item collides with the desired path. If that happens, 
     // we don't want to move this node at all. Unfortunately, 
     // we always seem to enter this block even when the nodes 
     // shouldn't actually collide. 
     qDebug() << "Found collision?"; 
     return pos(); 
    } 
} 

// Should only reach this if no collisions found, so we're okay to move 
return snapped; 

... mais cela ne fonctionne pas non plus. Tout nouveau noeud entre toujours dans le bloc collidesWithPath, même s'ils ne sont pas proches l'un de l'autre. L'objet canvas dans le code ci-dessus est juste QGraphicsView qui contient tous ces objets Node, au cas où cela ne serait pas clair non plus.

Des idées pour faire fonctionner le collidesWithPath correctement? Le chemin qui a copié le shape() est-il erroné? Je ne suis pas sûr de ce qui ne va pas ici, car la comparaison des deux formes semble bien fonctionner une fois le déménagement terminé (c'est-à-dire que je peux détecter les collisions avec précision après). Je peux télécharger le code entier si cela peut vous aider.

Répondre

0

Je l'ai compris. J'étais en effet sur la bonne voie avec la modification d'appel shape(). Au lieu de tester réellement l'objet de forme complète, qui est un objet trajectoire dont les tests de collision peuvent être plus complexes et inefficaces, j'ai écrit une fonction supplémentaire basée sur this thread qui compare deux QRectF (qui est essentiellement ce que l'appel de forme renvoie quand même):

bool rectsCollide(QRectF a, QRectF b) 
{ 
    qreal ax1, ax2, ay1, ay2; 
    a.getCoords(&ax1, &ay1, &ax2, &ay2); 

    qreal bx1, bx2, by1, by2; 
    b.getCoords(&bx1, &by1, &bx2, &by2); 

    return (ax1 < bx2 && 
      ax2 > bx1 && 
      ay1 < by2 && 
      ay2 > by1); 
} 

Puis, dans la même zone que la modification du message original:

QRectF myTranslatedRect = getShapeRect(); 
myTranslatedRect.translate(delX, delY); 

for (QGraphicsItem* other : canvas->items()) 
{ 
    if (other == this) 
     continue; 

    Node* n = (Node*)other; 
    if (rectsCollide(myTranslatedRect, n->getShapeRect())) 
     return pos(); 
} 

return snapped; 

Et qui fonctionne avec une petite fonction d'aide getShapeRect() qui revient fondamentalement juste le même rectangle utilisé dans la overriden shape() fonction , mais dans QRectF forme au lieu d'un QPainterPath.

Cette solution semble fonctionner plutôt bien. Les nœuds peuvent être déplacés librement (tout en restant contraints à la grille d'accrochage), sauf dans le cas où ils pourraient chevaucher un autre nœud. Dans ce cas, aucun mouvement n'est affiché.

Edit 1:

J'ai passé plus de temps à travailler sur ce pour obtenir un résultat un peu mieux, et je me suis dit que je partagerais depuis que je me sens comme les noeuds se déplacer avec collision est quelque chose qui peut être assez commun.Dans le code ci-dessus, "glisser" un nœud contre un long nœud/ensemble de nœuds qui entrerait en collision avec le nœud actuellement déplacé a des effets indésirables; à savoir, puisque nous venons par défaut de renvoyer pos() sur toute collision, nous ne bougeons jamais du tout - même s'il était possible de se déplacer dans la direction X ou Y avec succès. Cela se produit sur une diagonale glisser contre un collisionneur (sorte de difficile à expliquer, mais si vous construisez un projet similaire avec le code ci-dessus, vous pouvez le voir en action lorsque vous essayez de faire glisser un nœud à côté d'un autre). Nous pouvons facilement détecter cette condition avec quelques modifications mineures:

Au lieu de vérifier rectsCollide() par rapport à un seul rectangle traduit, nous pouvons avoir deux renvois séparés; celui qui est traduit dans la direction X et celui qui est traduit dans le Y. La boucle for vérifie maintenant pour s'assurer que tout mouvement X est valide indépendamment du mouvement Y. Après la boucle for, nous pouvons simplement vérifier les cas pour déterminer le mouvement final.

est ici la méthode itemChange() finale que vous pouvez utiliser librement dans votre code:

QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value) 
{ 
    if (change == ItemPositionChange && scene()) 
    { 
     // Figure out the new point 
     QPointF snapped = snapPoint(value.toPointF()); 

     // deltas to store amount of change 
     qreal delX = snapped.x() - pos().x(); 
     qreal delY = snapped.y() - pos().y(); 

     // No changes 
     if (delX == 0 && delY == 0) 
      return snapped; 

     // Check against all the other items for collision 
     QRectF translateX = getShapeRect(); 
     QRectF translateY = getShapeRect(); 
     translateX.translate(delX, 0); 
     translateY.translate(0, delY); 

     bool xOkay = true; 
     bool yOkay = true; 

     for (QGraphicsItem* other : canvas->items()) 
     { 
      if (other == this) 
       continue; 

      if (rectsCollide(translateX, ((Node*)other)->getShapeRect())) 
       xOkay = false; 
      if (rectsCollide(translateY, ((Node*)other)->getShapeRect())) 
       yOkay = false; 
     } 

     if (xOkay && yOkay) // Both valid, so move to the snapped location 
      return snapped; 
     else if (xOkay) // Move only in the X direction 
     { 
      QPointF r = pos(); 
      r.setX(snapped.x()); 
      return r; 
     } 
     else if (yOkay) // Move only in the Y direction 
     { 
      QPointF r = pos(); 
      r.setY(snapped.y()); 
      return r; 
     } 
     else // Don't move at all 
      return pos(); 
    } 

    return QGraphicsItem::itemChange(change, value); 
} 

Pour le code ci-dessus, il est en fait assez trivial pour éviter tout genre de comportement claquant, si vous voulez la libre circulation non contraint à une grille. Le correctif pour cela est juste pour changer la ligne qui appelle la fonction snapPoint() et utilisez simplement la valeur d'origine à la place. N'hésitez pas à commenter si vous avez des questions.