File size: 4,488 Bytes
b3f0c1a
 
 
 
 
c2b0fe1
 
b3f0c1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2b0fe1
 
 
 
 
b3f0c1a
 
c2b0fe1
b3f0c1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4319071
b3f0c1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useEffect, useRef, useCallback } from 'react';
import * as d3 from 'd3';
import { useFontMapStore } from '../../../store/fontMapStore';

const INITIAL_SCALE = 0.8;
// Min scale = reset scale — user can't zoom out past the initial framing.
const SCALE_EXTENT = [INITIAL_SCALE, 10.0];
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);

    // Compute viewport size so we can cap pan to the canvas bounds.
    const svgRect = svg.node().getBoundingClientRect();
    const cx = svgRect.width / 2;
    const cy = svgRect.height / 2;

    const zoom = d3.zoom()
      .scaleExtent(SCALE_EXTENT)
      .translateExtent([[0, 0], [svgRect.width, svgRect.height]])
      .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);
    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 = 4.0;
    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 };
}