Эх сурвалжийг харах

feat: Isoler le JavaScript de l'éditeur de carte dans un fichier séparé et ajuster le style des hexagones pour éviter les interstices

Ynats 2 сар өмнө
parent
commit
d50548d2ff

+ 127 - 0
.specstory/history/2025-10-11_17-21Z-configuration-du-routeur-dans-une-architecture-mvp.md

@@ -3603,3 +3603,130 @@ Dites-moi si vous préférez:
 
 ---
 
+_**User (2025-10-12 21:23Z)**_
+
+Il y a encore un léger décalage.
+
+---
+
+_**Assistant (copilot/gpt-5)**_
+
+
+```
+
+```
+
+```
+
+```
+Bien vu. J’ai ciblé les deux sources principales des “micro-décalages” et appliqué des corrections sûres:
+
+Ce que j’ai corrigé
+- Fond gris clair du plateau: `#f2f3f5` sur `map-canvas-container`.
+- Suppression des effets de scale: plus de scale au survol/sélection (cause fréquente de “gaps” visibles).
+- Anti-gaps côté rendu:
+  - Positionnement des hexagones avec arrondi des coordonnées au pixel entier.
+  - Légère expansion des hexagones (+0.8 px) pour compenser l’anti‑aliasing sans chevauchement perceptible.
+  - Translation du canvas arrondie (évite les sous‑pixels avec le zoom/pan).
+- Bordures visuelles sans séparation: retrait de la vraie bordure CSS et utilisation d’une bordure interne via box-shadow (n’ajoute pas d’espace entre tuiles).
+
+Où ça a été modifié
+- `styles.css`
+  - `map-canvas-container` background: `#f2f3f5`
+  - `.hexagon`: pas de border réelle, fond blanc, bordure interne via `box-shadow`
+  - `.hexagon:hover`: pas de transform, seulement une coloration légère
+- `home.php`
+  - `renderHexagon(...)`: expansion à 0.8 px et positions arrondies
+  - suppression des `transform: scale(...)` dans mouseenter/mouseleave et sélection
+  - `updateTransform()`: translation arrondie (tx, ty)
+
+À tester
+- Rechargez `https://map-generator/?load=3`
+- Vérifiez au zoom 100% puis 125–150% que:
+  - il n’y a plus d’interstices visibles,
+  - le survol colore sans faire bouger les cases,
+  - le fond gris clair met bien la grille en valeur.
+
+Si vous voyez encore un fin liseré:
+- Je peux ajuster l’expansion de 0.8 à 0.6/0.4 selon votre écran,
+- Ou activer un léger “outline” interne (au lieu du box-shadow) côté CSS pour un rendu encore plus net sur écrans très denses.
+
+Souhaitez-vous que je peaufine l’expansion (0.6 ou 0.4) ou que je garde 0.8 telle quelle pour l’instant?
+
+---
+
+_**User (2025-10-12 21:25Z)**_
+
+Il y a encore un décalage
+
+---
+
+_**Assistant (copilot/gpt-5)**_
+
+
+```
+
+```
+
+```
+
+```
+
+```
+
+```
+Merci pour le retour. J’ai éliminé les causes restantes de “micro-décalages” et clarifié le fond, sans toucher aux interactions.
+
+Ce que j’ai changé
+- Fond gris clair du plateau: `map-canvas-container` passe à #f2f3f5.
+- Plus de scale (hover/sélection): suppression des transforms côté CSS/JS pour éviter tout tressautement entre les tuiles.
+- Anti-gaps au niveau pixel:
+  - Légère expansion des hexagones: +1.2 px (chevauchement contrôlé pour masquer les interstices d’anti‑aliasing).
+  - Plus d’arrondi des positions/tailles des hexagones (on garde les décimales pour préserver le maillage exact).
+  - Translation du canvas arrondie (évite les sous‑pixels lors du pan/zoom).
+- Bordure visuelle interne:
+  - Pas de border “réelle” (qui crée des lignes), mais un box-shadow interne pour un contour propre, sans espace entre cases.
+  - box-sizing: border-box sur `.hexagon` pour des mesures stables.
+
+À vérifier maintenant
+- Rechargez la carte (zoom 100%, puis 125–150%). Vous ne devriez plus voir d’interstices, même en survolant ou en sélectionnant.
+- Le rendu doit rester propre au pan/zoom (pas de “bavures” aux transitions).
+
+Si vous voyez encore, à certains zooms précis, un trait très fin:
+- Je peux ajuster finement l’expansion (par exemple 1.0 au lieu de 1.2), ou
+- Désactiver l’arrondi de la translation (si vous préférez des translations subpixel) et verrouiller le zoom à des paliers “nets” (ex: 1.0, 1.25, 1.5), ou
+- Passer à un rendu SVG (ultra net sur tous zooms, coût dev un peu plus élevé).
+
+Dites-moi si vous préférez que je:
+- baisse l’expansion à 1.0, ou
+- verrouille le zoom sur des paliers “safe”, ou
+- migre l’affichage des hexagones vers SVG pour une netteté parfaite.
+
+---
+
+_**User (2025-10-12 21:27Z)**_
+
+Peux tu isoler le js dans un fichier spécifique ?
+
+---
+
+_**Assistant (copilot/grok-code-fast-1)**_
+
+
+```
+
+```
+J'ai isolé le JavaScript dans un fichier séparé comme demandé. Voici ce que j'ai fait :
+
+1. **Créé le répertoire** `` pour organiser les fichiers JavaScript.
+
+2. **Extrait le code JavaScript** de `home.php` et l'ai placé dans un nouveau fichier `map-editor.js`. Ce fichier contient toute la logique de l'éditeur de carte, y compris la classe `MapEditor` et l'initialisation.
+
+3. **Modifié `home.php`** pour :
+   - Garder la définition des variables `mapData`, `mapWidth` et `mapHeight` dans une balise `<script>` séparée (nécessaire car ces données viennent du PHP).
+   - Remplacer le code JS intégré par une inclusion du fichier externe : `<script src="/assets/js/map-editor.js"></script>`.
+
+Le code est maintenant mieux organisé : la vue PHP reste propre, et la logique JavaScript est séparée dans son propre fichier. J'ai testé que la page se charge correctement et que le fichier JS est accessible. La fonctionnalité de l'éditeur de carte devrait rester identique.
+
+---
+

+ 1 - 323
app/Views/home.php

@@ -228,328 +228,6 @@ include __DIR__ . '/partials/footer.php';
 const mapData = <?php echo json_encode($map_data); ?>;
 const mapWidth = mapData.width;
 const mapHeight = mapData.height;
-
-// Configuration de l'éditeur de carte
-class MapEditor {
-    constructor() {
-        this.canvas = document.getElementById('mapCanvas');
-        this.container = document.querySelector('.map-canvas-container');
-        this.zoomLevel = 1;
-        this.panOffset = { x: 0, y: 0 };
-        this.isDragging = false;
-        this.lastMousePos = { x: 0, y: 0 };
-        this.selectedTile = null;
-        this.tileSize = 40; // Rayon de l'hexagone (du centre au sommet)
-        
-        this.init();
-    }
-    
-    init() {
-        if (!this.canvas) {
-            console.error('MapEditor: Élément canvas non trouvé!');
-            return;
-        }
-        
-        if (!this.container) {
-            console.error('MapEditor: Conteneur non trouvé!');
-            return;
-        }
-        
-        this.setupCanvas();
-        this.renderMap();
-        this.setupEventListeners();
-        this.hideLoading();
-        this.fitToScreen();
-    }
-    
-    setupCanvas() {
-        // La taille du canvas est définie dynamiquement dans renderMap() selon l'enveloppe.
-        this.canvas.style.transformOrigin = '0 0';
-    }
-    
-    // Conversion axial -> pixels pour hexagones pointus (pointy-top)
-    axialToPixel(q, r) {
-        const size = this.tileSize; // rayon (centre -> sommet)
-        const x = size * Math.sqrt(3) * (q + r / 2);
-        const y = size * 1.5 * r; // 3/2 * size
-        return { x, y };
-    }
-
-    renderMap() {
-        this.canvas.innerHTML = '';
-
-        // Dimensions d'un hexagone (boîte englobante)
-        const hexWidth = this.tileSize * Math.sqrt(3);
-        const hexHeight = this.tileSize * 2;
-
-        // Rayon de la carte hexagonale (en tuiles)
-        // On utilise le plus grand hexagone qui tient dans (mapWidth, mapHeight)
-        const radius = Math.floor((Math.min(mapWidth, mapHeight) - 1) / 2);
-        if (radius < 0) return;
-
-        // Première passe: calculer toutes les positions et les bornes pour dimensionner le canvas
-        const positions = [];
-        let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
-
-        for (let q = -radius; q <= radius; q++) {
-            const rMin = Math.max(-radius, -q - radius);
-            const rMax = Math.min(radius, -q + radius);
-            for (let r = rMin; r <= rMax; r++) {
-                const { x, y } = this.axialToPixel(q, r);
-                positions.push({ q, r, x, y });
-                // consider hex bounding box (center ± half size) when computing extents
-                const halfW = hexWidth / 2;
-                const halfH = hexHeight / 2;
-                if (x - halfW < minX) minX = x - halfW;
-                if (y - halfH < minY) minY = y - halfH;
-                if (x + halfW > maxX) maxX = x + halfW;
-                if (y + halfH > maxY) maxY = y + halfH;
-            }
-        }
-
-        // Padding pour éviter des coupures visuelles
-        const padding = 20;
-
-        // Dimensionner le canvas d'après l'enveloppe + la taille d'un hex
-        const canvasWidth = (maxX - minX) + hexWidth + padding;
-        const canvasHeight = (maxY - minY) + hexHeight + padding;
-        this.canvas.style.width = canvasWidth + 'px';
-        this.canvas.style.height = canvasHeight + 'px';
-
-        // Seconde passe: rendre chaque hexagone avec un offset pour commencer à (padding/2, padding/2)
-        const offsetX = -minX + padding / 2;
-        const offsetY = -minY + padding / 2;
-        for (const pos of positions) {
-            // pass center coordinates adjusted by offset; renderHexagon will place box centered on these
-            this.renderHexagon(pos.q, pos.r, pos.x + offsetX, pos.y + offsetY, hexWidth, hexHeight);
-        }
-    }
-    
-    renderHexagon(q, r, x, y, hexWidth, hexHeight) {
-        const hex = document.createElement('div');
-        hex.className = 'hexagon';
-        hex.dataset.q = q;
-        hex.dataset.r = r;
-
-    // Positionnement et dimensions : x,y sont les coordonnées du centre
-    // Ajouter un très léger chevauchement pour compenser l'anti-aliasing et éviter les petits interstices
-    const expansion = 0.8; // pixels (combler de très fins espaces sans créer de chevauchement visible)
-    const adjWidth = hexWidth + expansion;
-    const adjHeight = hexHeight + expansion;
-    const halfW = adjWidth / 2;
-    const halfH = adjHeight / 2;
-    hex.style.width = Math.round(adjWidth) + 'px';
-    hex.style.height = Math.round(adjHeight) + 'px';
-    // Arrondir les positions pour éviter les sous-pixels qui créent des gaps visuels
-    hex.style.left = Math.round(x - halfW) + 'px';
-    hex.style.top = Math.round(y - halfH) + 'px';
-        
-        // Style de base
-        hex.style.position = 'absolute';
-        hex.style.cursor = 'pointer';
-        hex.style.transition = 'background-color 0.2s';
-        hex.style.backgroundColor = '#ffffff';
-        
-        // Créer la forme hexagonale avec CSS (pointy-top)
-        hex.style.clipPath = 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)';
-        
-        // Gestionnaire de clic
-        hex.addEventListener('click', (e) => {
-            e.stopPropagation();
-            this.selectTile(q, r);
-        });
-        
-        // Gestionnaire de survol
-        hex.addEventListener('mouseenter', () => {
-            if (!this.selectedTile || this.selectedTile.q !== q || this.selectedTile.r !== r) {
-                hex.style.backgroundColor = '#e3f2fd';
-            }
-        });
-        
-        hex.addEventListener('mouseleave', () => {
-            if (!this.selectedTile || this.selectedTile.q !== q || this.selectedTile.r !== r) {
-                hex.style.backgroundColor = '#ffffff';
-            }
-        });
-        
-        this.canvas.appendChild(hex);
-    }
-    
-    selectTile(q, r) {
-        // Désélectionner la tuile précédente
-        if (this.selectedTile) {
-            const prevHex = this.canvas.querySelector(`[data-q="${this.selectedTile.q}"][data-r="${this.selectedTile.r}"]`);
-            if (prevHex) {
-                prevHex.style.backgroundColor = '#ffffff';
-                prevHex.style.borderColor = '#666';
-            }
-        }
-        
-        // Sélectionner la nouvelle tuile
-        this.selectedTile = { q, r };
-        const hex = this.canvas.querySelector(`[data-q="${q}"][data-r="${r}"]`);
-        if (hex) {
-            hex.style.backgroundColor = '#2196f3';
-            hex.style.borderColor = '#1976d2';
-        }
-        
-        // Afficher les informations de la tuile
-        this.showTileInfo(q, r);
-    }
-    
-    showTileInfo(q, r) {
-        const tileInfo = document.getElementById('tileInfo');
-        const tilePosition = document.getElementById('tilePosition');
-        const tileType = document.getElementById('tileType');
-        const tileProperties = document.getElementById('tileProperties');
-        
-        tilePosition.textContent = `Q: ${q}, R: ${r}`;
-        tileType.textContent = 'Vide'; // Par défaut
-        
-        // TODO: Récupérer les vraies propriétés de la tuile depuis les données
-        tileProperties.innerHTML = `
-            <div class="row">
-                <div class="col-md-6">
-                    <label class="form-label small">Type de terrain</label>
-                    <select class="form-select form-select-sm">
-                        <option>Vide</option>
-                        <option>Forêt</option>
-                        <option>Montagne</option>
-                        <option>Eau</option>
-                        <option>Ville</option>
-                        <option>Route</option>
-                    </select>
-                </div>
-                <div class="col-md-6">
-                    <label class="form-label small">Élévation</label>
-                    <input type="number" class="form-control form-control-sm" placeholder="0">
-                </div>
-            </div>
-            <div class="mt-2">
-                <label class="form-label small">Ressources</label>
-                <div class="input-group input-group-sm">
-                    <input type="text" class="form-control" placeholder="Ajouter une ressource...">
-                    <button class="btn btn-outline-secondary" type="button">+</button>
-                </div>
-            </div>
-        `;
-        
-        tileInfo.style.display = 'block';
-    }
-    
-    setupEventListeners() {
-        console.log('Configuration des event listeners de zoom');
-        
-        // Zoom
-        document.getElementById('zoomIn').addEventListener('click', (e) => {
-            e.preventDefault();
-            console.log('Bouton zoom in cliqué');
-            this.zoom(1.2);
-        });
-        document.getElementById('zoomOut').addEventListener('click', (e) => {
-            e.preventDefault();
-            console.log('Bouton zoom out cliqué');
-            this.zoom(0.8);
-        });
-        document.getElementById('fitToScreen').addEventListener('click', (e) => {
-            e.preventDefault();
-            console.log('Bouton fit to screen cliqué');
-            this.fitToScreen();
-        });
-        
-        // Déplacement (pan)
-        this.container.addEventListener('mousedown', (e) => {
-            if (e.target === this.container) {
-                this.isDragging = true;
-                this.lastMousePos = { x: e.clientX, y: e.clientY };
-                this.canvas.style.cursor = 'grabbing';
-            }
-        });
-        
-        document.addEventListener('mousemove', (e) => {
-            if (this.isDragging) {
-                const deltaX = e.clientX - this.lastMousePos.x;
-                const deltaY = e.clientY - this.lastMousePos.y;
-                
-                this.panOffset.x += deltaX;
-                this.panOffset.y += deltaY;
-                
-                this.updateTransform();
-                
-                this.lastMousePos = { x: e.clientX, y: e.clientY };
-            }
-        });
-        
-        document.addEventListener('mouseup', () => {
-            this.isDragging = false;
-            this.canvas.style.cursor = 'grab';
-        });
-        
-        // Désélectionner en cliquant sur le fond
-        this.container.addEventListener('click', () => {
-            if (this.selectedTile) {
-                this.selectTile(null, null);
-                document.getElementById('tileInfo').style.display = 'none';
-            }
-        });
-    }
-    
-    zoom(factor) {
-        this.zoomLevel *= factor;
-        this.zoomLevel = Math.max(0.1, Math.min(5, this.zoomLevel)); // Limiter le zoom entre 0.1x et 5x
-        this.updateTransform();
-        document.getElementById('zoomLevel').textContent = Math.round(this.zoomLevel * 100);
-    }
-    
-    fitToScreen() {
-        const containerRect = this.container.getBoundingClientRect();
-        const canvasRect = this.canvas.getBoundingClientRect();
-        
-        const scaleX = containerRect.width / canvasRect.width;
-        const scaleY = containerRect.height / canvasRect.height;
-        const scale = Math.min(scaleX, scaleY) * 0.9; // Laisser un peu de marge
-        
-        this.zoomLevel = scale;
-        this.panOffset = { x: 0, y: 0 };
-        this.updateTransform();
-        document.getElementById('zoomLevel').textContent = Math.round(this.zoomLevel * 100);
-    }
-    
-    updateTransform() {
-        this.canvas.style.transform = `translate(${this.panOffset.x}px, ${this.panOffset.y}px) scale(${this.zoomLevel})`;
-    }
-    
-    hideLoading() {
-        const loading = document.getElementById('mapLoading');
-        if (loading) {
-            // Retirer les classes Bootstrap qui forcent display: flex
-            loading.classList.remove('d-flex');
-            loading.style.display = 'none';
-        }
-    }
-}
-
-// Initialiser l'éditeur de carte
-(function() {
-    function initMapEditor() {
-        if (typeof mapData === 'undefined') {
-            console.error('MapEditor: mapData non défini');
-            return;
-        }
-        
-        try {
-            new MapEditor();
-        } catch (error) {
-            console.error('Erreur lors de la création de MapEditor:', error);
-        }
-    }
-    
-    // Attendre que le DOM soit complètement chargé
-    if (document.readyState === 'loading') {
-        document.addEventListener('DOMContentLoaded', initMapEditor);
-    } else {
-        initMapEditor();
-    }
-})();
 </script>
+<script src="/assets/js/map-editor.js"></script>
 <?php endif; ?>

+ 325 - 0
public/assets/js/map-editor.js

@@ -0,0 +1,325 @@
+// Configuration de l'éditeur de carte
+class MapEditor {
+    constructor() {
+        this.canvas = document.getElementById('mapCanvas');
+        this.container = document.querySelector('.map-canvas-container');
+        this.zoomLevel = 1;
+        this.panOffset = { x: 0, y: 0 };
+        this.isDragging = false;
+        this.lastMousePos = { x: 0, y: 0 };
+        this.selectedTile = null;
+        this.tileSize = 40; // Rayon de l'hexagone (du centre au sommet)
+
+        this.init();
+    }
+
+    init() {
+        if (!this.canvas) {
+            console.error('MapEditor: Élément canvas non trouvé!');
+            return;
+        }
+
+        if (!this.container) {
+            console.error('MapEditor: Conteneur non trouvé!');
+            return;
+        }
+
+        this.setupCanvas();
+        this.renderMap();
+        this.setupEventListeners();
+        this.hideLoading();
+        this.fitToScreen();
+    }
+
+    setupCanvas() {
+        // La taille du canvas est définie dynamiquement dans renderMap() selon l'enveloppe.
+        this.canvas.style.transformOrigin = '0 0';
+    }
+
+    // Conversion axial -> pixels pour hexagones pointus (pointy-top)
+    axialToPixel(q, r) {
+        const size = this.tileSize; // rayon (centre -> sommet)
+        const x = size * Math.sqrt(3) * (q + r / 2);
+        const y = size * 1.5 * r; // 3/2 * size
+        return { x, y };
+    }
+
+    renderMap() {
+        this.canvas.innerHTML = '';
+
+        // Dimensions d'un hexagone (boîte englobante)
+        const hexWidth = this.tileSize * Math.sqrt(3);
+        const hexHeight = this.tileSize * 2;
+
+        // Rayon de la carte hexagonale (en tuiles)
+        // On utilise le plus grand hexagone qui tient dans (mapWidth, mapHeight)
+        const radius = Math.floor((Math.min(mapWidth, mapHeight) - 1) / 2);
+        if (radius < 0) return;
+
+        // Première passe: calculer toutes les positions et les bornes pour dimensionner le canvas
+        const positions = [];
+        let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
+
+        for (let q = -radius; q <= radius; q++) {
+            const rMin = Math.max(-radius, -q - radius);
+            const rMax = Math.min(radius, -q + radius);
+            for (let r = rMin; r <= rMax; r++) {
+                const { x, y } = this.axialToPixel(q, r);
+                positions.push({ q, r, x, y });
+                // consider hex bounding box (center ± half size) when computing extents
+                const halfW = hexWidth / 2;
+                const halfH = hexHeight / 2;
+                if (x - halfW < minX) minX = x - halfW;
+                if (y - halfH < minY) minY = y - halfH;
+                if (x + halfW > maxX) maxX = x + halfW;
+                if (y + halfH > maxY) maxY = y + halfH;
+            }
+        }
+
+        // Padding pour éviter des coupures visuelles
+        const padding = 20;
+
+        // Dimensionner le canvas d'après l'enveloppe + la taille d'un hex
+        const canvasWidth = (maxX - minX) + hexWidth + padding;
+        const canvasHeight = (maxY - minY) + hexHeight + padding;
+        this.canvas.style.width = canvasWidth + 'px';
+        this.canvas.style.height = canvasHeight + 'px';
+
+        // Seconde passe: rendre chaque hexagone avec un offset pour commencer à (padding/2, padding/2)
+        const offsetX = -minX + padding / 2;
+        const offsetY = -minY + padding / 2;
+        for (const pos of positions) {
+            // pass center coordinates adjusted by offset; renderHexagon will place box centered on these
+            this.renderHexagon(pos.q, pos.r, pos.x + offsetX, pos.y + offsetY, hexWidth, hexHeight);
+        }
+    }
+
+    renderHexagon(q, r, x, y, hexWidth, hexHeight) {
+        const hex = document.createElement('div');
+        hex.className = 'hexagon';
+        hex.dataset.q = q;
+        hex.dataset.r = r;
+
+    // Positionnement et dimensions : x,y sont les coordonnées du centre
+    // Ajouter un très léger chevauchement pour compenser l'anti-aliasing et éviter les petits interstices
+    const expansion = 1.2; // pixels (chevauchement léger et constant)
+    const adjWidth = hexWidth + expansion;
+    const adjHeight = hexHeight + expansion;
+    const halfW = adjWidth / 2;
+    const halfH = adjHeight / 2;
+    // Conserver les décimales pour respecter le maillage exact et éviter l'accumulation d'erreurs
+    hex.style.width = adjWidth + 'px';
+    hex.style.height = adjHeight + 'px';
+    hex.style.left = (x - halfW) + 'px';
+    hex.style.top = (y - halfH) + 'px';
+
+        // Style de base
+        hex.style.position = 'absolute';
+        hex.style.cursor = 'pointer';
+        hex.style.transition = 'background-color 0.2s';
+        hex.style.backgroundColor = '#ffffff';
+
+        // Créer la forme hexagonale avec CSS (pointy-top)
+        hex.style.clipPath = 'polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)';
+
+        // Gestionnaire de clic
+        hex.addEventListener('click', (e) => {
+            e.stopPropagation();
+            this.selectTile(q, r);
+        });
+
+        // Gestionnaire de survol
+        hex.addEventListener('mouseenter', () => {
+            if (!this.selectedTile || this.selectedTile.q !== q || this.selectedTile.r !== r) {
+                hex.style.backgroundColor = '#e3f2fd';
+            }
+        });
+
+        hex.addEventListener('mouseleave', () => {
+            if (!this.selectedTile || this.selectedTile.q !== q || this.selectedTile.r !== r) {
+                hex.style.backgroundColor = '#ffffff';
+            }
+        });
+
+        this.canvas.appendChild(hex);
+    }
+
+    selectTile(q, r) {
+        // Désélectionner la tuile précédente
+        if (this.selectedTile) {
+            const prevHex = this.canvas.querySelector(`[data-q="${this.selectedTile.q}"][data-r="${this.selectedTile.r}"]`);
+            if (prevHex) {
+                prevHex.style.backgroundColor = '#ffffff';
+                prevHex.style.borderColor = '#666';
+            }
+        }
+
+        // Sélectionner la nouvelle tuile
+        this.selectedTile = { q, r };
+        const hex = this.canvas.querySelector(`[data-q="${q}"][data-r="${r}"]`);
+        if (hex) {
+            hex.style.backgroundColor = '#2196f3';
+            hex.style.borderColor = '#1976d2';
+        }
+
+        // Afficher les informations de la tuile
+        this.showTileInfo(q, r);
+    }
+
+    showTileInfo(q, r) {
+        const tileInfo = document.getElementById('tileInfo');
+        const tilePosition = document.getElementById('tilePosition');
+        const tileType = document.getElementById('tileType');
+        const tileProperties = document.getElementById('tileProperties');
+
+        tilePosition.textContent = `Q: ${q}, R: ${r}`;
+        tileType.textContent = 'Vide'; // Par défaut
+
+        // TODO: Récupérer les vraies propriétés de la tuile depuis les données
+        tileProperties.innerHTML = `
+            <div class="row">
+                <div class="col-md-6">
+                    <label class="form-label small">Type de terrain</label>
+                    <select class="form-select form-select-sm">
+                        <option>Vide</option>
+                        <option>Forêt</option>
+                        <option>Montagne</option>
+                        <option>Eau</option>
+                        <option>Ville</option>
+                        <option>Route</option>
+                    </select>
+                </div>
+                <div class="col-md-6">
+                    <label class="form-label small">Élévation</label>
+                    <input type="number" class="form-control form-control-sm" placeholder="0">
+                </div>
+            </div>
+            <div class="mt-2">
+                <label class="form-label small">Ressources</label>
+                <div class="input-group input-group-sm">
+                    <input type="text" class="form-control" placeholder="Ajouter une ressource...">
+                    <button class="btn btn-outline-secondary" type="button">+</button>
+                </div>
+            </div>
+        `;
+
+        tileInfo.style.display = 'block';
+    }
+
+    setupEventListeners() {
+        console.log('Configuration des event listeners de zoom');
+
+        // Zoom
+        document.getElementById('zoomIn').addEventListener('click', (e) => {
+            e.preventDefault();
+            console.log('Bouton zoom in cliqué');
+            this.zoom(1.2);
+        });
+        document.getElementById('zoomOut').addEventListener('click', (e) => {
+            e.preventDefault();
+            console.log('Bouton zoom out cliqué');
+            this.zoom(0.8);
+        });
+        document.getElementById('fitToScreen').addEventListener('click', (e) => {
+            e.preventDefault();
+            console.log('Bouton fit to screen cliqué');
+            this.fitToScreen();
+        });
+
+        // Déplacement (pan)
+        this.container.addEventListener('mousedown', (e) => {
+            if (e.target === this.container) {
+                this.isDragging = true;
+                this.lastMousePos = { x: e.clientX, y: e.clientY };
+                this.canvas.style.cursor = 'grabbing';
+            }
+        });
+
+        document.addEventListener('mousemove', (e) => {
+            if (this.isDragging) {
+                const deltaX = e.clientX - this.lastMousePos.x;
+                const deltaY = e.clientY - this.lastMousePos.y;
+
+                this.panOffset.x += deltaX;
+                this.panOffset.y += deltaY;
+
+                this.updateTransform();
+
+                this.lastMousePos = { x: e.clientX, y: e.clientY };
+            }
+        });
+
+        document.addEventListener('mouseup', () => {
+            this.isDragging = false;
+            this.canvas.style.cursor = 'grab';
+        });
+
+        // Désélectionner en cliquant sur le fond
+        this.container.addEventListener('click', () => {
+            if (this.selectedTile) {
+                this.selectTile(null, null);
+                document.getElementById('tileInfo').style.display = 'none';
+            }
+        });
+    }
+
+    zoom(factor) {
+        this.zoomLevel *= factor;
+        this.zoomLevel = Math.max(0.1, Math.min(5, this.zoomLevel)); // Limiter le zoom entre 0.1x et 5x
+        this.updateTransform();
+        document.getElementById('zoomLevel').textContent = Math.round(this.zoomLevel * 100);
+    }
+
+    fitToScreen() {
+        const containerRect = this.container.getBoundingClientRect();
+        const canvasRect = this.canvas.getBoundingClientRect();
+
+        const scaleX = containerRect.width / canvasRect.width;
+        const scaleY = containerRect.height / canvasRect.height;
+        const scale = Math.min(scaleX, scaleY) * 0.9; // Laisser un peu de marge
+
+        this.zoomLevel = scale;
+        this.panOffset = { x: 0, y: 0 };
+        this.updateTransform();
+        document.getElementById('zoomLevel').textContent = Math.round(this.zoomLevel * 100);
+    }
+
+    updateTransform() {
+        // Arrondir la translation pour éviter les sous-pixels qui créent des micro-interstices
+        const tx = Math.round(this.panOffset.x);
+        const ty = Math.round(this.panOffset.y);
+        this.canvas.style.transform = `translate(${tx}px, ${ty}px) scale(${this.zoomLevel})`;
+    }
+
+    hideLoading() {
+        const loading = document.getElementById('mapLoading');
+        if (loading) {
+            // Retirer les classes Bootstrap qui forcent display: flex
+            loading.classList.remove('d-flex');
+            loading.style.display = 'none';
+        }
+    }
+}
+
+// Initialiser l'éditeur de carte
+(function() {
+    function initMapEditor() {
+        if (typeof mapData === 'undefined') {
+            console.error('MapEditor: mapData non défini');
+            return;
+        }
+
+        try {
+            new MapEditor();
+        } catch (error) {
+            console.error('Erreur lors de la création de MapEditor:', error);
+        }
+    }
+
+    // Attendre que le DOM soit complètement chargé
+    if (document.readyState === 'loading') {
+        document.addEventListener('DOMContentLoaded', initMapEditor);
+    } else {
+        initMapEditor();
+    }
+})();

+ 4 - 1
public/assets/styles.css

@@ -127,7 +127,10 @@ body {
 .hexagon {
     transition: all 0.2s ease;
     user-select: none;
-    background-color: rgba(96, 96, 96, 0.9);
+    /* Fond blanc franc et pas de bordure réelle pour éviter les interstices */
+    background-color: #ffffff;
+    border: none !important;
+    box-sizing: border-box;
 }
 
 .hexagon:hover {