tfrere's picture
tfrere HF Staff
feat: FontCLIP pipeline, category colors, and updated How It Works
2fc4361
import { useEffect, useRef, useCallback } from 'react';
import * as d3 from 'd3';
import { useFontMapStore } from '../../../store/fontMapStore';
const SCALE_EXTENT = [0.4, 10.0];
const INITIAL_SCALE = 0.8;
const TRANSITION_DURATION = 750;
/**
* Hook de zoom basé sur DebugUMAP.
* Fonctionne avec un SVG viewBox → antialiasing natif.
*
* Le handler sélectionne .viewport-group dynamiquement pour rester
* compatible avec useMapRenderer qui peut le recréer.
*/
export function useMapZoom(svgRef, enabled = true) {
const zoomRef = useRef(null);
useEffect(() => {
if (!enabled || !svgRef.current) return;
const svg = d3.select(svgRef.current);
// Le viewport-group doit exister (créé par useMapRenderer)
if (svg.select('.viewport-group').empty()) return;
// Nettoyer un éventuel zoom précédent
svg.on('.zoom', null);
const zoom = d3.zoom()
.scaleExtent(SCALE_EXTENT)
.on('zoom', (event) => {
svg.select('.viewport-group').attr('transform', event.transform);
svg.select('.highlight-group').attr('transform', event.transform);
svg.select('.centroids-group').attr('transform', event.transform);
if (window.updateTooltipTransform) window.updateTooltipTransform(event.transform);
if (window.updateTooltipPositions) window.updateTooltipPositions();
});
svg.call(zoom);
// Zoom initial centré à 80 %
const svgRect = svg.node().getBoundingClientRect();
const cx = svgRect.width / 2;
const cy = svgRect.height / 2;
const initialTransform = d3.zoomIdentity
.translate(cx * (1 - INITIAL_SCALE), cy * (1 - INITIAL_SCALE))
.scale(INITIAL_SCALE);
svg.call(zoom.transform, initialTransform);
zoomRef.current = zoom;
// Fonctions globales pour ZoomControls
window.zoomIn = () => svg.transition().duration(200).call(zoom.scaleBy, 1.5);
window.zoomOut = () => svg.transition().duration(200).call(zoom.scaleBy, 1 / 1.5);
window.resetZoom = () => {
const rect = svg.node().getBoundingClientRect();
const rcx = rect.width / 2;
const rcy = rect.height / 2;
const t = d3.zoomIdentity
.translate(rcx * (1 - INITIAL_SCALE), rcy * (1 - INITIAL_SCALE))
.scale(INITIAL_SCALE);
const store = useFontMapStore.getState();
store.setIsTransitioning(true);
store.setHoveredFont(null);
svg.transition().duration(TRANSITION_DURATION).call(zoom.transform, t)
.on('end', () => {
useFontMapStore.getState().setIsTransitioning(false);
});
};
const svgNode = svgRef.current;
return () => {
if (svgNode) d3.select(svgNode).on('.zoom', null);
delete window.zoomIn;
delete window.zoomOut;
delete window.resetZoom;
zoomRef.current = null;
};
}, [enabled, svgRef]);
const centerOnFont = useCallback((font) => {
if (!font || !zoomRef.current || !svgRef.current) return;
const svg = d3.select(svgRef.current);
const glyphGroup = svg.select(`g.glyph-group[data-font-id="${font.id}"]`);
if (glyphGroup.empty()) return;
const transformAttr = glyphGroup.attr('data-original-transform');
if (!transformAttr) return;
const match = transformAttr.match(/translate\(([^,]+),\s*([^)]+)\)/);
if (!match) return;
const fontX = parseFloat(match[1]);
const fontY = parseFloat(match[2]);
const svgNode = svgRef.current;
const width = svgNode.clientWidth || svgNode.getBoundingClientRect().width;
const height = svgNode.clientHeight || svgNode.getBoundingClientRect().height;
const scale = 2.5;
const translateX = width / 2 - fontX * scale;
const translateY = height / 2 - fontY * scale;
const transform = d3.zoomIdentity
.translate(translateX, translateY)
.scale(scale);
const store = useFontMapStore.getState();
store.setIsTransitioning(true);
store.setHoveredFont(null);
svg.transition()
.duration(800)
.ease(d3.easeCubicInOut)
.call(zoomRef.current.transform, transform)
.on('end', () => {
useFontMapStore.getState().setIsTransitioning(false);
});
}, [svgRef]);
const resetZoom = useCallback(() => {
if (window.resetZoom) window.resetZoom();
}, []);
return { centerOnFont, resetZoom };
}