import { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import * as d3 from 'd3'; // Import des hooks spécialisés import { usePositioning } from './usePositioning'; import { useVisualState } from './useVisualState'; import { useZoom } from './useZoom'; import { useGlyphRenderer } from './useGlyphRenderer'; import { useViewportCulling } from './useViewportCulling'; import { useOpacityCache } from './useOpacityCache'; import { useDebouncedUpdates } from './useDebouncedUpdates'; import { calculatePositions } from '../utils/voronoiDilation'; /** * Hook refactorisé pour la visualisation D3 * Utilise des hooks spécialisés pour une meilleure séparation des responsabilités */ export const useD3Visualization = ( fonts, filter, searchTerm, darkMode, loading, dilationFactor = 0.8, characterSize = 1.0, onFontSelect = null, selectedFont = null, hoveredFont = null, zoomLevel = 0.9, variantSizeImpact = false, canNavigate = false ) => { const svgRef = useRef(); const [debugMode, setDebugMode] = useState(false); const [useCSSTransform, setUseCSSTransform] = useState(false); const [, setIsTransitioning] = useState(false); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const isInitializedRef = useRef(false); const isUpdatingRef = useRef(false); const previousPositionsRef = useRef(null); const lastSizeRef = useRef(null); // Hooks spécialisés - utiliser useRef pour éviter les re-calculs constants const positionsRef = useRef([]); const hasPositionsRef = useRef(false); // Recalculer les positions seulement quand nécessaire useMemo(() => { if (fonts.length && dimensions.width > 0 && dimensions.height > 0) { positionsRef.current = calculatePositions(fonts, dimensions.width, dimensions.height, dilationFactor); hasPositionsRef.current = positionsRef.current.length > 0; } }, [fonts, dimensions.width, dimensions.height, dilationFactor]); const positions = positionsRef.current; const hasPositions = hasPositionsRef.current; // Hooks d'optimisation const viewportBoundsRef = useRef(null); const { visiblePositions, getViewportBounds, totalCount, visibleCount } = useViewportCulling(positions, viewportBoundsRef.current); const { getOpacity, invalidateCache } = useOpacityCache(); const { debouncedUpdate, flushUpdate } = useDebouncedUpdates(16); // 60fps const { visualStateRef, updateVisualStates, updateGlyphSizes, updateGlyphOpacity, updateGlyphColors } = useVisualState(); const { setupZoom, setupGlobalZoomFunctions, centerOnFont, createZoomIndicator } = useZoom(svgRef, darkMode, useCSSTransform); const { createGlyphs } = useGlyphRenderer(); // Mémoriser la configuration des couleurs const colorScale = useMemo(() => { const scale = d3.scaleOrdinal( darkMode ? ['#ffffff', '#cccccc', '#999999', '#666666', '#333333'] : ['#000000', '#333333', '#666666', '#999999', '#cccccc'] ); const families = [...new Set(fonts.map(d => d.family))]; families.forEach(family => scale(family)); return scale; }, [fonts, darkMode]); // Fonction pour initialiser la visualisation const initializeVisualization = useCallback(() => { if (loading || !fonts.length || !hasPositions || dimensions.width <= 0 || dimensions.height <= 0) { return; } const svg = d3.select(svgRef.current); // Optimisation du rendu pour qualité vectorielle svg.style('shape-rendering', 'geometricPrecision') .style('text-rendering', 'geometricPrecision') .style('image-rendering', 'crisp-edges') .style('vector-effect', 'non-scaling-stroke'); // Nettoyer le SVG si c'est la première initialisation if (!isInitializedRef.current) { svg.selectAll('*').remove(); isInitializedRef.current = true; } // Créer les groupes principaux let uiGroup = svg.select('.ui-group'); let viewportGroup = svg.select('.viewport-group'); if (uiGroup.empty()) { uiGroup = svg.append('g').attr('class', 'ui-group'); } if (viewportGroup.empty()) { viewportGroup = svg.append('g').attr('class', 'viewport-group'); // Force le rendu vectoriel sur le viewport group viewportGroup.style('shape-rendering', 'geometricPrecision') .style('text-rendering', 'geometricPrecision') .style('image-rendering', 'crisp-edges'); } // Configurer le zoom setupZoom(svg, viewportGroup, uiGroup, dimensions.width, dimensions.height); // Configurer les fonctions de zoom globales setupGlobalZoomFunctions(svg); // Créer l'indicateur de zoom et de navigation createZoomIndicator(uiGroup, dimensions.width, dimensions.height, canNavigate); // Créer les glyphes createGlyphs( viewportGroup, positions, darkMode, characterSize, filter, searchTerm, colorScale, debugMode, onFontSelect, selectedFont, visualStateRef ); // Gérer le redimensionnement const handleResize = () => { const container = svgRef.current?.parentElement; if (container) { const newWidth = container.clientWidth; const newHeight = container.clientHeight; setDimensions({ width: newWidth, height: newHeight }); svg.attr('width', newWidth).attr('height', newHeight); } }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, [ loading, fonts.length, hasPositions, dimensions, darkMode, characterSize, filter, searchTerm, colorScale, debugMode, onFontSelect, selectedFont, setupZoom, setupGlobalZoomFunctions, createZoomIndicator, createGlyphs ]); // Effet pour l'initialisation (une seule fois) useEffect(() => { if (loading || !fonts.length || !hasPositions) return; const timer = setTimeout(() => { const svg = d3.select(svgRef.current); // Vérifier si déjà initialisé if (svg.select('.viewport-group').empty()) { initializeVisualization(); } }, 100); return () => clearTimeout(timer); }, [loading, fonts.length, hasPositions, dimensions.width, dimensions.height, darkMode, useCSSTransform]); // Effet unifié optimisé pour toutes les mises à jour visuelles useEffect(() => { if (loading || !fonts.length || !hasPositions) return; const updateFn = () => { const svg = d3.select(svgRef.current); const viewportGroup = svg.select('.viewport-group'); if (viewportGroup.empty()) return; // Mise à jour unique et optimisée de tous les états visuels updateVisualStates(svg, viewportGroup, selectedFont, hoveredFont, darkMode); updateGlyphSizes(viewportGroup, selectedFont, characterSize); updateGlyphOpacity(viewportGroup, positions, filter, searchTerm, selectedFont); updateGlyphColors(viewportGroup, fonts, darkMode); }; // Debouncer les mises à jour pour éviter les re-renders excessifs debouncedUpdate(updateFn); }, [selectedFont, hoveredFont, darkMode, characterSize, filter, searchTerm, fonts, loading, hasPositions, updateVisualStates, updateGlyphSizes, updateGlyphOpacity, updateGlyphColors, debouncedUpdate]); // Fonction pour compenser le zoom lors des changements de positions const compensateZoomForPositionChange = useCallback((svg, viewportGroup, previousPositions, newPositions) => { if (!previousPositions || !newPositions || previousPositions.length === 0 || newPositions.length === 0) return; // Calculer le centre des positions précédentes const prevCenterX = d3.mean(previousPositions, d => d.x); const prevCenterY = d3.mean(previousPositions, d => d.y); // Calculer le centre des nouvelles positions const newCenterX = d3.mean(newPositions, d => d.x); const newCenterY = d3.mean(newPositions, d => d.y); // Calculer le décalage const offsetX = prevCenterX - newCenterX; const offsetY = prevCenterY - newCenterY; // Obtenir la transformation actuelle const currentTransform = d3.zoomTransform(svg.node()); // Appliquer la compensation immédiatement const compensatedTransform = currentTransform.translate(offsetX, offsetY); svg.call(d3.zoom().transform, compensatedTransform); }, []); // Effet optimisé pour les changements de propriétés (dilatation, taille, filtre, mode sombre) // NE PAS inclure positions dans les dépendances pour éviter les re-renders constants useEffect(() => { if (loading || !fonts.length || !hasPositions) return; if (isUpdatingRef.current) return; if (visualStateRef.current.isTransitioning) return; isUpdatingRef.current = true; const svg = d3.select(svgRef.current); const viewportGroup = svg.select('.viewport-group'); if (viewportGroup.empty()) { isUpdatingRef.current = false; return; } // Compenser le zoom seulement si les positions ont vraiment changé (pas juste le zoom/pan) if (previousPositionsRef.current && previousPositionsRef.current.length > 0 && previousPositionsRef.current.length === positions.length && Math.abs(previousPositionsRef.current[0]?.x - positions[0]?.x) > 1) { compensateZoomForPositionChange(svg, viewportGroup, previousPositionsRef.current, positions); } // Mise à jour optimisée des glyphes - seulement les positions si elles ont changé const glyphGroups = viewportGroup.selectAll('.font-glyph-group'); const fontGlyphs = viewportGroup.selectAll('.font-glyph'); // Mettre à jour les bounds du viewport viewportBoundsRef.current = getViewportBounds(svg, viewportGroup); const baseSize = 16; let currentSize = baseSize * characterSize; // Ajuster la taille en fonction du nombre de variantes si activé if (variantSizeImpact) { // Calculer un multiplicateur basé sur le nombre de variantes const variantMultiplier = Math.max(0.5, Math.min(2.0, 1 + (positions.length / fonts.length) * 0.5)); currentSize *= variantMultiplier; } const offset = currentSize / 2; // Vérifier si la taille a vraiment changé pour éviter le flicker const sizeChanged = !lastSizeRef.current || Math.abs(lastSizeRef.current - currentSize) > 0.1; // Mise à jour des positions uniquement si nécessaire (pas à chaque mouvement de souris) if (previousPositionsRef.current?.length !== positions.length || (previousPositionsRef.current?.[0]?.x !== positions[0]?.x && Math.abs(previousPositionsRef.current?.[0]?.x - positions[0]?.x) > 1)) { glyphGroups .data(visiblePositions) // Utiliser seulement les positions visibles .attr('transform', d => `translate(${d.x}, ${d.y})`); } // Mise à jour de l'opacité avec cache glyphGroups .style('opacity', d => getOpacity(d, filter, searchTerm, selectedFont)); // Mise à jour de la taille seulement si elle a changé if (sizeChanged) { fontGlyphs .transition() .duration(200) // Transition douce pour éviter le flicker .ease(d3.easeCubicOut) .attr('width', currentSize) .attr('height', currentSize) .attr('x', -offset) .attr('y', -offset); lastSizeRef.current = currentSize; } isUpdatingRef.current = false; previousPositionsRef.current = positions; }, [dilationFactor, characterSize, filter, searchTerm, darkMode, fonts, loading, hasPositions, selectedFont, visualStateRef, compensateZoomForPositionChange, variantSizeImpact]); // Effet pour centrer sur une police sélectionnée useEffect(() => { if (loading || !fonts.length || !hasPositions || !selectedFont) return; centerOnFont(selectedFont, fonts, dimensions.width, dimensions.height, dilationFactor, visualStateRef, setIsTransitioning); }, [selectedFont, fonts, loading, hasPositions, dimensions, dilationFactor, centerOnFont, visualStateRef]); // Effet pour mettre à jour l'indicateur de navigation useEffect(() => { if (loading || !fonts.length || !hasPositions) return; const svg = d3.select(svgRef.current); const uiGroup = svg.select('.ui-group'); if (!uiGroup.empty()) { createZoomIndicator(uiGroup, dimensions.width, dimensions.height, canNavigate); } }, [canNavigate, loading, fonts.length, hasPositions, dimensions, createZoomIndicator]); // Gestion du mode debug avec la touche 'd' et basculement CSS transform avec 't' useEffect(() => { const handleKeyPress = (event) => { if (event.key === 'd' || event.key === 'D') { setDebugMode(prev => !prev); } if (event.key === 't' || event.key === 'T') { setUseCSSTransform(prev => !prev); console.log('CSS Transform mode:', !useCSSTransform); } }; window.addEventListener('keydown', handleKeyPress); return () => window.removeEventListener('keydown', handleKeyPress); }, [useCSSTransform]); // Initialisation des dimensions useEffect(() => { const container = svgRef.current?.parentElement; if (container) { const width = container.clientWidth; const height = container.clientHeight; setDimensions({ width, height }); } }, []); // Effet pour pré-calculer les positions dilatées au démarrage useEffect(() => { if (loading || !fonts.length) return; const container = svgRef.current?.parentElement; if (!container) return; const width = container.clientWidth; const height = container.clientHeight; if (width === 0 || height === 0) return; // Plus de pré-calcul nécessaire avec la nouvelle approche simple }, [fonts, loading]); return svgRef; };