import { useRef, useEffect, useState } from 'react'; import * as d3 from 'd3'; import { useFontMapStore } from '../../../store/fontMapStore'; import { useGlyphRenderer } from './useGlyphRenderer'; import { useZoom } from './useZoom'; import { useVisualState } from './useVisualState'; /** * Hook principal pour la visualisation D3 * Orchestre le rendu, le zoom et les interactions */ export const useD3Visualization = (fonts, glyphPaths, filter, searchTerm, darkMode, loading) => { const svgRef = useRef(null); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const scaledPositionsRef = useRef([]); // Pour stocker les positions mises à l'échelle // Store global const { selectedFont, setSelectedFont, setHoveredFont } = useFontMapStore(); // Hooks spécialisés const { createGlyphs, updateGlyphPositions, updateGlyphDimensions } = useGlyphRenderer(); const { setupZoom, setupGlobalZoomFunctions, centerOnFont, createZoomIndicator } = useZoom(svgRef, darkMode); const { visualStateRef, updateVisualStates, updateGlyphSizes, updateGlyphOpacity, updateGlyphColors } = useVisualState(); // Initialisation et redimensionnement useEffect(() => { const handleResize = () => { if (svgRef.current && svgRef.current.parentElement) { const { clientWidth, clientHeight } = svgRef.current.parentElement; setDimensions({ width: clientWidth, height: clientHeight }); } }; window.addEventListener('resize', handleResize); handleResize(); return () => window.removeEventListener('resize', handleResize); }, []); // Rendu principal D3 useEffect(() => { if (loading || !fonts || fonts.length === 0 || !svgRef.current || dimensions.width === 0) return; const svg = d3.select(svgRef.current); svg.selectAll('*').remove(); // Nettoyage complet const width = dimensions.width; const height = dimensions.height; svg .attr('width', width) .attr('height', height) .style('background-color', darkMode ? '#1a1a1a' : '#ffffff'); // Groupes principaux const containerGroup = svg.append('g').attr('class', 'container-group'); const viewportGroup = containerGroup.append('g').attr('class', 'viewport-group'); const uiGroup = svg.append('g').attr('class', 'ui-group'); // Injecter le sprite SVG directement dans le SVG // Créer les symboles à partir des glyphPaths if (glyphPaths && Object.keys(glyphPaths).length > 0) { const defs = svg.append('defs'); Object.entries(glyphPaths).forEach(([id, pathData]) => { defs.append('symbol') .attr('id', id) .attr('viewBox', '0 0 80 80') .append('path') .attr('d', pathData) .attr('fill', 'currentColor'); }); console.log(`✅ Injected ${Object.keys(glyphPaths).length} symbols into SVG`); } else { console.warn('⚠️ No glyphPaths available for sprite injection'); } // Configuration du zoom const zoom = setupZoom(svg, viewportGroup, uiGroup, width, height); setupGlobalZoomFunctions(svg); // Calculer les échelles pour mapper les coordonnées UMAP vers l'espace SVG const padding = 50; // Padding autour de la visualisation const xExtent = d3.extent(fonts, d => d.x); const yExtent = d3.extent(fonts, d => d.y); const xScale = d3.scaleLinear() .domain(xExtent) .range([padding, width - padding]); const yScale = d3.scaleLinear() .domain(yExtent) .range([padding, height - padding]); // Créer les positions mises à l'échelle const scaledPositions = fonts.map(font => ({ ...font, x: xScale(font.x), y: yScale(font.y) })); // Stocker pour les effets réactifs scaledPositionsRef.current = scaledPositions; // Échelle de couleurs const colorScale = 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 => colorScale(family)); // Création des glyphes avec les positions mises à l'échelle createGlyphs( viewportGroup, scaledPositions, // Utiliser les positions mises à l'échelle ! glyphPaths, // Paths extraits pour rendu direct darkMode, 1.0, // characterSize par défaut filter, searchTerm, colorScale, false, // debugMode setSelectedFont, selectedFont, visualStateRef ); // Indicateurs UI createZoomIndicator(uiGroup, width, height, true); }, [ loading, fonts, glyphPaths, // Needed for sprite injection dimensions.width, dimensions.height, darkMode, setupZoom, createGlyphs, createZoomIndicator, setupGlobalZoomFunctions, visualStateRef, setSelectedFont, selectedFont // Pour l'état initial ]); // Mises à jour réactives (sans redessiner tout) useEffect(() => { if (!svgRef.current || loading || scaledPositionsRef.current.length === 0) return; const svg = d3.select(svgRef.current); const viewportGroup = svg.select('.viewport-group'); if (viewportGroup.empty()) return; // Mise à jour des états visuels updateVisualStates(svg, viewportGroup, selectedFont, null, darkMode); updateGlyphOpacity(viewportGroup, scaledPositionsRef.current, filter, searchTerm, selectedFont); // Si une police est sélectionnée, on centre dessus if (selectedFont && !visualStateRef.current.isTransitioning) { // Utiliser les positions mises à l'échelle pour le centrage centerOnFont(selectedFont, scaledPositionsRef.current, visualStateRef, (val) => { visualStateRef.current.isTransitioning = val; }); } }, [ selectedFont, filter, searchTerm, darkMode, fonts, loading, updateVisualStates, updateGlyphOpacity, centerOnFont, visualStateRef ]); return svgRef; };