|
|
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); |
|
|
|
|
|
|
|
|
abortControllerRef.current = new AbortController(); |
|
|
|
|
|
fetch(`/debug-umap/${config.filename}`, { |
|
|
signal: abortControllerRef.current.signal |
|
|
}) |
|
|
.then(res => res.json()) |
|
|
.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); |
|
|
}); |
|
|
} |
|
|
|