Explication détaillée du fonctionnement de la gestion des tuiles, déplacements et collisions dans App.tsx
Le fichier App.tsx
gère un système de tuiles déplaçables sur une grille à l'aide de la bibliothèque dnd-kit
. Voici comment les différentes fonctionnalités sont mises en œuvre.
Les tuiles sont stockées sous forme d'un tableau d'objets Tile
dans un état React :
type Tile = {
id: string;
x: number;
y: number;
h: number;
w: number;
};
Chaque tuile possède :
- Un id unique
- Une position x, y dans la grille
- Une largeur (w) et une hauteur (h) en pixels
L'état React gérant ces tuiles est :
const [items, setItems] = useState<Tile[]>([]);
L'application utilise dnd-kit
pour gérer le déplacement des tuiles.
Lorsque l'utilisateur commence à déplacer une tuile :
function handleDragStart({ active }: DragStartEvent) {
setActiveId(active.id.toString());
}
- L'id de la tuile active est enregistré dans l'état
activeId
.
À chaque mouvement de la souris, la fonction handleDragMove
est déclenchée :
function handleDragMove(event: DragMoveEvent) {
const { active, delta } = event;
setItems((oldItems) => {
const idx = oldItems.findIndex((tile) => tile.id === active.id);
const tile = oldItems[idx];
const newX = Math.round((tile.x * TILE_SIZE + delta.x) / TILE_SIZE);
const newY = Math.round((tile.y * TILE_SIZE + delta.y) / TILE_SIZE);
Ce qui se passe ici :
- On récupère la tuile déplacée via
active.id
- On calcule les nouvelles coordonnées
(newX, newY)
en fonction du déplacement de la souris - On vérifie les collisions et ajuste la position si nécessaire :
if (isTilePositionOccupied(newX, newY, oldItems, active.id.toString())) { let collisionX = newX; const collisionY = newY; while (isTilePositionOccupied(collisionX, collisionY, oldItems, active.id.toString())) { collisionX += 1; } setMoveOverlay({ x: collisionX, y: collisionY, w: tile.w, h: tile.h }); } else { setMoveOverlay(null); }
- Si la nouvelle position est occupée, on cherche un emplacement libre vers la droite.
Quand l'utilisateur relâche la tuile :
function handleDragEnd(event: DragEndEvent) {
const { active, delta } = event;
setItems((oldItems) => {
const newTiles = [...oldItems];
const idx = newTiles.findIndex((tile) => tile.id === active.id);
const tile = newTiles[idx];
const newX = Math.round((tile.x * TILE_SIZE + delta.x) / TILE_SIZE);
const newY = Math.round((tile.y * TILE_SIZE + delta.y) / TILE_SIZE);
- On met à jour la position
(newX, newY)
de la tuile déplacée. - On détecte les collisions et on repousse les tuiles affectées vers la droite :
if (isTilePositionOccupied(newX, newY, newTiles, active.id.toString())) { let collisionX = newX; const collisionY = newY; while (isTilePositionOccupied(collisionX, collisionY, newTiles, active.id.toString())) { collisionX += 1; }
- On ajuste la position des autres tuiles si elles se chevauchent :
newTiles.forEach((otherTile, otherIdx) => { if (otherIdx !== idx && isTilePositionOccupied(otherTile.x, otherTile.y, newTiles, otherTile.id)) { let newOtherX = otherTile.x; while (isTilePositionOccupied(newOtherX, otherTile.y, newTiles, otherTile.id)) { newOtherX += 1; } newTiles[otherIdx] = { ...otherTile, x: newOtherX }; } });
- On supprime les informations temporaires de drag :
setActiveId(null); setSkeleton(null); setMoveOverlay(null);
La fonction clé isTilePositionOccupied
détecte si une tuile occupe une position donnée :
function isTilePositionOccupied(x: number, y: number, items: Tile[], ignoreTileId?: string): boolean {
return items.some((item) => {
const rect1 = { x, y, w: TILE_SIZE, h: TILE_SIZE };
const rect2 = { x: item.x, y: item.y, w: item.w, h: item.h };
const isColliding =
rect1.x < rect2.x + rect2.w / TILE_SIZE &&
rect1.x + rect1.w / TILE_SIZE > rect2.x &&
rect1.y < rect2.y + rect2.h / TILE_SIZE &&
rect1.y + rect1.h / TILE_SIZE > rect2.y;
return isColliding && item.id !== ignoreTileId;
});
}
Ce qui est fait ici :
- On compare les coordonnées de la nouvelle position avec celles des autres tuiles.
- Si la tuile est déjà occupée, on retourne
true
, sinonfalse
.
Une tuile peut être agrandie horizontalement, verticalement ou dans les deux directions :
function handleEnlargeTile(id: string, direction: "horizontal" | "vertical" | "both") {
setItems((oldItems) => {
return oldItems.map((tile) => {
if (tile.id === id) {
const newTile = { ...tile };
if (direction === "horizontal") newTile.w += 3 * TILE_SIZE;
if (direction === "vertical") newTile.h += 2 * TILE_SIZE;
return newTile;
}
return tile;
});
});
}
- Cette fonction augmente la largeur et/ou la hauteur de la tuile sélectionnée.
- Elle ajuste aussi les tuiles en conflit en les poussant vers la droite ou vers le bas.
Quand l'utilisateur clique sur une case vide :
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const x = Math.floor((e.clientX - rect.left) / TILE_SIZE);
const y = Math.floor((e.clientY - rect.top) / TILE_SIZE);
if (!isTilePositionOccupied(x, y, items)) {
setItems([...items, { id: generateRandomColorHex(), x, y, h: TILE_SIZE, w: TILE_SIZE }]);
}
};
- On calcule la position de la souris en coordonnées de grille.
- Si l'emplacement est libre, on ajoute une nouvelle tuile.
✔ Déplacement des tuiles via dnd-kit
✔ Détection des collisions pour éviter le chevauchement
✔ Réorganisation automatique en cas de conflit
✔ Agrandissement dynamique des tuiles
✔ Ajout de nouvelles tuiles en cliquant sur une case vide
C'est un système fluide et intelligent, gérant les collisions en temps réel et permettant aux tuiles de s'agrandir et de se réorganiser dynamiquement. 🚀