| | 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; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function useMapZoom(svgRef, enabled = true) { |
| | const zoomRef = useRef(null); |
| |
|
| | useEffect(() => { |
| | if (!enabled || !svgRef.current) return; |
| |
|
| | const svg = d3.select(svgRef.current); |
| |
|
| | |
| | if (svg.select('.viewport-group').empty()) return; |
| |
|
| | |
| | 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); |
| |
|
| | |
| | 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; |
| |
|
| | |
| | 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 }; |
| | } |
| |
|