File size: 7,754 Bytes
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import * as d3 from 'd3';
import { forceSimulation, forceManyBody, forceCollide } from 'd3-force';

// Cache pour éviter de recréer les simulations
let simulationCache = new Map();
let lastPositionsHash = null;

/**
 * Calcule la bounding box d'un groupe de positions
 * @param {Array} positions - Positions des polices
 * @returns {Object} Bounding box {minX, maxX, minY, maxY, width, height, centerX, centerY}
 */
const calculateBoundingBox = (positions) => {
  if (!positions || positions.length === 0) {
    return { minX: 0, maxX: 0, minY: 0, maxY: 0, width: 0, height: 0, centerX: 0, centerY: 0 };
  }
  
  const xs = positions.map(p => p.x);
  const ys = positions.map(p => p.y);
  
  const minX = Math.min(...xs);
  const maxX = Math.max(...xs);
  const minY = Math.min(...ys);
  const maxY = Math.max(...ys);
  
  return {
    minX,
    maxX,
    minY,
    maxY,
    width: maxX - minX,
    height: maxY - minY,
    centerX: (minX + maxX) / 2,
    centerY: (minY + maxY) / 2
  };
};

/**
 * Génère un hash simple pour les positions (pour le cache)
 * @param {Array} positions - Positions des polices
 * @returns {string} Hash des positions
 */
const generatePositionsHash = (positions) => {
  return positions.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join('|');
};

/**
 * Applique une dilatation optimisée avec cache et interpolation
 * @param {Array} positions - Positions initiales des polices
 * @param {number} sliderValue - Valeur du slider (0-1)
 * @returns {Array} Positions après simulation
 */
export const applySimpleDilation = (positions, sliderValue) => {
  if (!positions || positions.length === 0 || sliderValue === 0) {
    return positions;
  }

  // Configuration basée sur la valeur du slider (0-1 → équivalent 0-0.35)
  const mappedValue = sliderValue * 0.35;
  
  // Points de cache pour interpolation
  const cachePoints = [0, 0.1, 0.2, 0.3, 0.35];
  const cacheKey = Math.min(...cachePoints.filter(p => p >= mappedValue));
  
  // Vérifier si on a déjà calculé cette valeur
  const positionsHash = generatePositionsHash(positions);
  const cacheEntryKey = `${positionsHash}-${cacheKey}`;
  
  if (simulationCache.has(cacheEntryKey) && lastPositionsHash === positionsHash) {
    const cachedResult = simulationCache.get(cacheEntryKey);
    
    // Si c'est exactement la valeur cachée, la retourner directement
    if (Math.abs(mappedValue - cacheKey) < 0.001) {
      return cachedResult;
    }
    
    // Sinon, interpoler avec la valeur précédente
    const prevCacheKey = cachePoints[cachePoints.indexOf(cacheKey) - 1];
    if (prevCacheKey !== undefined) {
      const prevCacheEntryKey = `${positionsHash}-${prevCacheKey}`;
      if (simulationCache.has(prevCacheEntryKey)) {
        const prevResult = simulationCache.get(prevCacheEntryKey);
        const interpolationFactor = (mappedValue - prevCacheKey) / (cacheKey - prevCacheKey);
        
        return interpolatePositions(prevResult, cachedResult, interpolationFactor);
      }
    }
  }
  
  // Calculer la nouvelle simulation si pas en cache
  console.log('🎯 Computing new simulation for slider:', sliderValue, 'mapped:', mappedValue);
  
  const result = computeSimulation(positions, mappedValue);
  
  // Mettre en cache
  simulationCache.set(cacheEntryKey, result);
  lastPositionsHash = positionsHash;
  
  // Limiter la taille du cache
  if (simulationCache.size > 10) {
    const firstKey = simulationCache.keys().next().value;
    simulationCache.delete(firstKey);
  }
  
  return result;
};

/**
 * Calcule une simulation D3 pour une valeur donnée
 * @param {Array} positions - Positions initiales
 * @param {number} mappedValue - Valeur mappée (0-0.35)
 * @returns {Array} Positions après simulation
 */
const computeSimulation = (positions, mappedValue) => {
  // Calculer la bounding box initiale
  const initialBBox = calculateBoundingBox(positions);
  
  // Créer une copie des positions pour la simulation
  const simulationData = positions.map(pos => ({ 
    x: pos.x, 
    y: pos.y, 
    name: pos.name,
    originalX: pos.x,
    originalY: pos.y
  }));
  
  const maxSteps = 100;
  const simulationSteps = Math.round(mappedValue * maxSteps);
  const forceStrength = -mappedValue * 500;
  const collideRadius = 15 + (mappedValue * 25);
  
  // Créer la simulation D3
  const simulation = forceSimulation(simulationData)
    .force("charge", forceManyBody().strength(forceStrength))
    .force("collide", forceCollide(collideRadius))
    .stop();
  
  // Exécuter la simulation
  for (let i = 0; i < simulationSteps; i++) {
    simulation.tick();
  }
  
  // Calculer la bounding box après simulation
  const finalBBox = calculateBoundingBox(simulationData);
  
  // Calculer le facteur de réduction
  const widthRatio = initialBBox.width / finalBBox.width;
  const heightRatio = initialBBox.height / finalBBox.height;
  const reductionFactor = Math.min(widthRatio, heightRatio);
  
  // Appliquer la réduction
  const reducedPositions = simulationData.map(pos => ({
    x: initialBBox.centerX + (pos.x - finalBBox.centerX) * reductionFactor,
    y: initialBBox.centerY + (pos.y - finalBBox.centerY) * reductionFactor
  }));
  
  // Retourner les positions mises à jour
  return reducedPositions.map((simPos, index) => ({
    ...positions[index],
    x: simPos.x,
    y: simPos.y
  }));
};

/**
 * Interpole entre deux ensembles de positions
 * @param {Array} positionsA - Positions A
 * @param {Array} positionsB - Positions B
 * @param {number} factor - Facteur d'interpolation (0-1)
 * @returns {Array} Positions interpolées
 */
const interpolatePositions = (positionsA, positionsB, factor) => {
  return positionsA.map((posA, index) => {
    const posB = positionsB[index];
    if (!posB || posA.name !== posB.name) return posA;
    
    return {
      ...posA,
      x: posA.x + (posB.x - posA.x) * factor,
      y: posA.y + (posB.y - posA.y) * factor
    };
  });
};

/**
 * Vide le cache des simulations (utile quand les données changent)
 */
export const clearSimulationCache = () => {
  simulationCache.clear();
  lastPositionsHash = null;
  console.log('🧹 Simulation cache cleared');
};

/**
 * Obtient les statistiques du cache
 * @returns {Object} Statistiques du cache
 */
export const getCacheStats = () => {
  return {
    cacheSize: simulationCache.size,
    lastPositionsHash: lastPositionsHash ? lastPositionsHash.substring(0, 20) + '...' : null,
    cacheKeys: Array.from(simulationCache.keys())
  };
};

/**
 * Calcule les positions avec simulation simple
 * @param {Array} fonts - Données des polices avec coordonnées UMAP
 * @param {number} width - Largeur du conteneur
 * @param {number} height - Hauteur du conteneur
 * @param {number} sliderValue - Valeur du slider (0-1)
 * @returns {Array} Positions calculées pour l'affichage
 */
export const calculatePositions = (fonts, width, height, sliderValue) => {
  if (!fonts || fonts.length === 0) {
    return [];
  }

  // Créer les échelles pour convertir les coordonnées UMAP en coordonnées d'écran
  const xExtent = d3.extent(fonts, d => d.x);
  const yExtent = d3.extent(fonts, d => d.y);
  
  const xScale = d3.scaleLinear()
    .domain(xExtent)
    .range([50, width - 50]);

  const yScale = d3.scaleLinear()
    .domain(yExtent)
    .range([height - 50, 50]);

  // Convertir les positions UMAP en coordonnées d'écran (positions de base)
  const basePositions = fonts.map(font => ({
    ...font,
    originalX: font.x,
    originalY: font.y,
    x: xScale(font.x),
    y: yScale(font.y)
  }));

  // Si slider à 0, retourner les positions de base
  if (sliderValue === 0) {
    return basePositions;
  }

  // Appliquer la dilatation optimisée
  return applySimpleDilation(basePositions, sliderValue);
};