| | import { useEffect, useRef } from 'react'; |
| | import * as d3 from 'd3'; |
| | import { calculateMappingDimensions, createGlyphTransform } from '../utils/mappingUtils.js'; |
| | import { applyColorsToGlyphGroup } from '../utils/colorUtils.js'; |
| | import { getConfig } from '../config/mapConfig.js'; |
| | import { useDebugUMAPStore } from '../store'; |
| |
|
| | |
| | |
| | |
| | export function useGlyphRenderer({ svgRef, enabled = true }) { |
| | const configs = useDebugUMAPStore((state) => state.configs); |
| | const currentConfigIndex = useDebugUMAPStore((state) => state.currentConfigIndex); |
| | const baseGlyphSize = useDebugUMAPStore((state) => state.baseGlyphSize); |
| | const useCategoryColors = useDebugUMAPStore((state) => state.useCategoryColors); |
| | const darkMode = useDebugUMAPStore((state) => state.darkMode); |
| | const showCentroids = useDebugUMAPStore((state) => state.showCentroids); |
| | const setCurrentFonts = useDebugUMAPStore((state) => state.setCurrentFonts); |
| | const setMappingFunctions = useDebugUMAPStore((state) => state.setMappingFunctions); |
| | const setGlyphsLoaded = useDebugUMAPStore((state) => state.setGlyphsLoaded); |
| |
|
| | |
| | const abortControllerRef = useRef(null); |
| | const timeoutRefs = useRef([]); |
| |
|
| | console.log('useGlyphRenderer: Hook appelé avec:', { |
| | configsLength: configs.length, |
| | currentConfigIndex, |
| | baseGlyphSize, |
| | svgRef: !!svgRef.current |
| | }); |
| | |
| | useEffect(() => { |
| | console.log('useGlyphRenderer: useEffect déclenché, enabled:', enabled, 'configs.length:', configs.length); |
| | |
| | |
| | if (abortControllerRef.current) { |
| | abortControllerRef.current.abort(); |
| | } |
| | timeoutRefs.current.forEach(timeout => clearTimeout(timeout)); |
| | timeoutRefs.current = []; |
| | |
| | if (!enabled || configs.length === 0) { |
| | console.log('useGlyphRenderer: Pas activé ou pas de configurations, sortie'); |
| | return; |
| | } |
| |
|
| | const config = configs[currentConfigIndex]; |
| | if (!config) { |
| | console.log('useGlyphRenderer: Pas de config pour l\'index', currentConfigIndex); |
| | return; |
| | } |
| |
|
| | console.log('useGlyphRenderer: Chargement de la config:', config.filename || 'live'); |
| |
|
| | |
| | abortControllerRef.current = new AbortController(); |
| |
|
| | |
| | const dataPromise = config.fonts |
| | ? Promise.resolve(config) |
| | : fetch(`/debug-umap/${config.filename}`, { |
| | signal: abortControllerRef.current.signal |
| | }).then(res => res.json()); |
| |
|
| | dataPromise |
| | .then(data => { |
| | const svg = svgRef.current; |
| | if (!svg) return; |
| |
|
| | |
| | let viewportGroup = d3.select(svg).select('.viewport-group'); |
| | if (viewportGroup.empty()) { |
| | viewportGroup = d3.select(svg).append('g').attr('class', 'viewport-group'); |
| | } else { |
| | |
| | viewportGroup.selectAll('g.glyph-group').remove(); |
| | viewportGroup.selectAll('.centroid-label').remove(); |
| | } |
| |
|
| | |
| | const { mapX, mapY } = calculateMappingDimensions(data.fonts); |
| | |
| | |
| | setCurrentFonts(data.fonts); |
| | setMappingFunctions({ mapX, mapY }); |
| | setGlyphsLoaded(false); |
| |
|
| | |
| | const batchSize = getConfig('glyph.batchLoading.batchSize', 40); |
| | const fonts = data.fonts; |
| | |
| | const loadBatch = (startIndex) => { |
| | const endIndex = Math.min(startIndex + batchSize, fonts.length); |
| | const batch = fonts.slice(startIndex, endIndex); |
| | |
| | |
| | const promises = batch.map(font => |
| | fetch(`/data/char/${font.id}_a.svg`, { |
| | signal: abortControllerRef.current.signal |
| | }) |
| | .then(res => res.text()) |
| | .then(svgContent => { |
| | |
| | if (!svgRef.current || abortControllerRef.current.signal.aborted) { |
| | return; |
| | } |
| | renderGlyph(viewportGroup, svgContent, font, mapX, mapY, baseGlyphSize, useCategoryColors, darkMode); |
| | }) |
| | .catch((err) => { |
| | if (err.name !== 'AbortError') { |
| | console.warn('Erreur lors du chargement du glyphe:', font.id, err); |
| | } |
| | }) |
| | ); |
| | |
| | |
| | Promise.all(promises).then(() => { |
| | |
| | if (!svgRef.current || abortControllerRef.current.signal.aborted) { |
| | return; |
| | } |
| | |
| | if (endIndex < fonts.length) { |
| | |
| | const delay = getConfig('glyph.batchLoading.delay', 10); |
| | const timeout = setTimeout(() => loadBatch(endIndex), delay); |
| | timeoutRefs.current.push(timeout); |
| | } else { |
| | |
| | setGlyphsLoaded(true); |
| | |
| | if (showCentroids) { |
| | const centroidTimeout = setTimeout(() => { |
| | if (!abortControllerRef.current.signal.aborted) { |
| | createCentroids(data.fonts, mapX, mapY, viewportGroup, darkMode, useCategoryColors); |
| | } |
| | }, 50); |
| | timeoutRefs.current.push(centroidTimeout); |
| | } |
| | } |
| | }); |
| | }; |
| | |
| | |
| | loadBatch(0); |
| | }) |
| | .catch(err => { |
| | if (err.name !== 'AbortError') { |
| | console.error('Erreur:', err); |
| | } |
| | }); |
| |
|
| | |
| | return () => { |
| | if (abortControllerRef.current) { |
| | abortControllerRef.current.abort(); |
| | } |
| | timeoutRefs.current.forEach(timeout => clearTimeout(timeout)); |
| | timeoutRefs.current = []; |
| | }; |
| | }, [enabled, configs, currentConfigIndex]); |
| |
|
| | |
| | useEffect(() => { |
| | if (!svgRef.current) return; |
| |
|
| | const svg = svgRef.current; |
| | const viewportGroup = svg.querySelector('.viewport-group'); |
| | if (!viewportGroup) return; |
| | |
| | const glyphGroups = viewportGroup.querySelectorAll('g.glyph-group'); |
| | |
| | glyphGroups.forEach(group => { |
| | const category = group.getAttribute('data-category'); |
| | applyColorsToGlyphGroup(group, category, useCategoryColors, darkMode); |
| | }); |
| | |
| | |
| | const centroidLabels = viewportGroup.querySelectorAll('.centroid-label'); |
| | const strokeColors = getConfig('color.centroid.stroke', { light: '#ffffff', dark: '#000000' }); |
| | const strokeColor = darkMode ? strokeColors.dark : strokeColors.light; |
| | |
| | const categoryColors = getConfig('color.categories', {}); |
| | const fallbackColor = getConfig('color.centroid.fallback', '#95a5a6'); |
| | const defaultColors = getConfig('color.defaults', { light: '#333333', dark: '#ffffff' }); |
| | |
| | centroidLabels.forEach(label => { |
| | const category = label.textContent; |
| | const fillColor = useCategoryColors |
| | ? (categoryColors[category] || fallbackColor) |
| | : (darkMode ? defaultColors.dark : defaultColors.light); |
| | |
| | label.setAttribute('fill', fillColor); |
| | label.setAttribute('stroke', strokeColor); |
| | }); |
| | }, [useCategoryColors, darkMode]); |
| |
|
| | |
| | useEffect(() => { |
| | if (!svgRef.current) return; |
| |
|
| | const svg = svgRef.current; |
| | const viewportGroup = svg.querySelector('.viewport-group'); |
| | if (!viewportGroup) return; |
| | |
| | const centroidLabels = viewportGroup.querySelectorAll('.centroid-label'); |
| | centroidLabels.forEach(label => { |
| | label.style.display = showCentroids ? 'block' : 'none'; |
| | }); |
| | }, [showCentroids]); |
| |
|
| | |
| | useEffect(() => { |
| | if (!svgRef.current || !enabled) return; |
| |
|
| | const svg = svgRef.current; |
| | const viewportGroup = svg.querySelector('.viewport-group'); |
| | if (!viewportGroup) return; |
| | |
| | const glyphGroups = viewportGroup.querySelectorAll('g.glyph-group'); |
| | glyphGroups.forEach(group => { |
| | const originalTransform = group.getAttribute('data-original-transform'); |
| | if (originalTransform) { |
| | |
| | const match = originalTransform.match(/translate\(([^,]+),\s*([^)]+)\)/); |
| | if (match) { |
| | const x = parseFloat(match[1]); |
| | const y = parseFloat(match[2]); |
| | |
| | |
| | |
| | const newTransform = `translate(${x}, ${y}) scale(${baseGlyphSize})`; |
| | group.setAttribute('transform', newTransform); |
| | } |
| | } |
| | }); |
| | }, [enabled, baseGlyphSize]); |
| |
|
| | |
| | return {}; |
| | } |
| |
|
| | |
| | |
| | |
| | function renderGlyph(viewportGroup, svgContent, font, mapX, mapY, baseGlyphSize, useCategoryColors, darkMode) { |
| | |
| | const group = document.createElementNS('http://www.w3.org/2000/svg', 'g'); |
| | const originalTransform = createGlyphTransform( |
| | mapX(font.x), |
| | mapY(font.y), |
| | baseGlyphSize |
| | ); |
| | |
| | group.setAttribute('transform', originalTransform); |
| | group.setAttribute('data-original-transform', originalTransform); |
| | group.setAttribute('data-category', font.family); |
| | group.setAttribute('class', 'glyph-group'); |
| | |
| | |
| | const parser = new DOMParser(); |
| | const svgDoc = parser.parseFromString(svgContent, 'image/svg+xml'); |
| | const svgElement = svgDoc.querySelector('svg'); |
| | |
| | if (svgElement) { |
| | |
| | while (svgElement.firstChild) { |
| | const child = svgElement.firstChild; |
| | group.appendChild(child); |
| | } |
| | } |
| | |
| | |
| | viewportGroup.node().appendChild(group); |
| | |
| | |
| | applyColorsToGlyphGroup(group, font.family, useCategoryColors, darkMode); |
| | } |
| |
|
| | |
| | |
| | |
| | function createCentroids(fonts, mapX, mapY, viewportGroup, darkMode, useCategoryColors = true) { |
| | |
| | const centroids = {}; |
| | const categoryCounts = {}; |
| |
|
| | fonts.forEach(font => { |
| | const category = font.family; |
| | if (!centroids[category]) { |
| | centroids[category] = { x: 0, y: 0, count: 0 }; |
| | categoryCounts[category] = 0; |
| | } |
| | |
| | centroids[category].x += font.x; |
| | centroids[category].y += font.y; |
| | centroids[category].count += 1; |
| | categoryCounts[category] += 1; |
| | }); |
| |
|
| | |
| | Object.keys(centroids).forEach(category => { |
| | centroids[category].x /= centroids[category].count; |
| | centroids[category].y /= centroids[category].count; |
| | }); |
| |
|
| | |
| | viewportGroup.selectAll('.centroid-label').remove(); |
| |
|
| | |
| | Object.entries(centroids).forEach(([category, centroid]) => { |
| | const x = mapX(centroid.x); |
| | const y = mapY(centroid.y); |
| | |
| | |
| | const categoryColors = getConfig('color.categories', {}); |
| | const fallbackColor = getConfig('color.centroid.fallback', '#95a5a6'); |
| | const defaultColors = getConfig('color.defaults', { light: '#333333', dark: '#ffffff' }); |
| | |
| | const color = useCategoryColors |
| | ? (categoryColors[category] || fallbackColor) |
| | : (darkMode ? defaultColors.dark : defaultColors.light); |
| |
|
| | |
| | const strokeColors = getConfig('color.centroid.stroke', { light: '#ffffff', dark: '#000000' }); |
| | const strokeColor = darkMode ? strokeColors.dark : strokeColors.light; |
| | |
| | const textConfig = getConfig('centroid.text', { |
| | fontSize: 16, |
| | fontWeight: 'bold', |
| | fontFamily: 'Arial, Helvetica, sans-serif', |
| | strokeWidth: 4 |
| | }); |
| | |
| | viewportGroup.append('text') |
| | .attr('x', x) |
| | .attr('y', y) |
| | .attr('text-anchor', 'middle') |
| | .attr('font-size', `${textConfig.fontSize}px`) |
| | .attr('font-weight', textConfig.fontWeight) |
| | .attr('font-family', textConfig.fontFamily) |
| | .attr('fill', color) |
| | .attr('stroke', strokeColor) |
| | .attr('stroke-width', `${textConfig.strokeWidth}px`) |
| | .attr('stroke-linejoin', 'round') |
| | .attr('stroke-linecap', 'round') |
| | .attr('paint-order', 'stroke fill') |
| | .attr('class', 'centroid-label') |
| | .text(category); |
| | }); |
| | } |
| |
|