File size: 6,061 Bytes
b700c24
eebc40f
6bda4a6
b700c24
 
 
eebc40f
 
b700c24
 
eebc40f
b700c24
 
eebc40f
b700c24
eebc40f
b700c24
eebc40f
b700c24
 
 
 
eebc40f
b700c24
 
 
 
eebc40f
b700c24
 
eebc40f
b700c24
 
 
eebc40f
 
 
 
b700c24
eebc40f
b700c24
 
6bda4a6
b700c24
6bda4a6
b700c24
6bda4a6
 
b700c24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eebc40f
b700c24
 
 
eebc40f
b700c24
 
 
 
eebc40f
b700c24
 
 
eebc40f
b700c24
 
 
eebc40f
b700c24
 
 
 
 
 
eebc40f
b700c24
 
eebc40f
b700c24
 
 
 
 
 
 
 
eebc40f
b700c24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eebc40f
b700c24
 
eebc40f
b700c24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eebc40f
b700c24
eebc40f
b700c24
eebc40f
 
b700c24
eebc40f
b700c24
eebc40f
b700c24
 
 
eebc40f
b700c24
 
 
 
 
 
eebc40f
 
b700c24
 
 
 
 
 
 
 
 
 
 
 
eebc40f
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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;
};