import { AnimatePresence, motion } from "motion/react"; import { useEffect, useRef, useState, type RefObject } from "react"; import { Compass, Focus, HelpCircle, LayoutTemplate, Minus, Plus } from "lucide-react"; import { FloatingDock, type FloatingDockEntry } from "@/components/ui/aceternity/floating-dock"; import { aceternityMotion } from "@/lib/aceternity-motion"; import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style"; import { canvasThemes } from "@/lib/canvas-theme"; import { subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport"; import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store"; type CanvasZoomControlsProps = { scale: number; onScaleChange: (scale: number) => void; onFitContent: () => void; onAutoArrange?: () => void; isMiniMapOpen: boolean; onToggleMiniMap: () => void; onOpenShortcuts: () => void; containerRef?: RefObject; }; const QUICK_ZOOM_LEVELS = [0.25, 0.5, 1, 2] as const; export function CanvasZoomControls({ scale, onScaleChange, onFitContent, onAutoArrange, isMiniMapOpen, onToggleMiniMap, onOpenShortcuts, containerRef }: CanvasZoomControlsProps) { const theme = canvasThemes[useActiveTheme()]; const rootRef = useRef(null); const liveScaleRef = useRef(scale); const rangeRef = useRef(null); const dockLabelRef = useRef(null); const panelLabelRef = useRef(null); const [precisionOpen, setPrecisionOpen] = useState(false); useEffect(() => updateScaleDisplay(scale), [scale]); useEffect(() => { const container = containerRef?.current; if (!container) return; return subscribeCanvasViewportPreview(container, (viewport) => updateScaleDisplay(viewport.k)); }, [containerRef]); useEffect(() => { if (!precisionOpen) return; const close = (event: PointerEvent) => { if (event.target instanceof Node && !rootRef.current?.contains(event.target)) setPrecisionOpen(false); }; const closeOnEscape = (event: KeyboardEvent) => { if (event.key === "Escape") setPrecisionOpen(false); }; document.addEventListener("pointerdown", close, true); document.addEventListener("keydown", closeOnEscape); return () => { document.removeEventListener("pointerdown", close, true); document.removeEventListener("keydown", closeOnEscape); }; }, [precisionOpen]); function updateScaleDisplay(nextScale: number) { liveScaleRef.current = nextScale; const percent = String(Math.round(nextScale * 100)); if (rangeRef.current) rangeRef.current.value = percent; if (dockLabelRef.current) dockLabelRef.current.textContent = percent; if (panelLabelRef.current) panelLabelRef.current.textContent = `${percent}%`; } function commitScale(nextScale: number) { const clampedScale = Math.min(2, Math.max(0.05, nextScale)); updateScaleDisplay(clampedScale); onScaleChange(clampedScale); } const items: FloatingDockEntry[] = [ { id: "zoom-minimap", label: isMiniMapOpen ? "关闭小地图" : "打开小地图", icon: , active: isMiniMapOpen, onClick: onToggleMiniMap }, { id: "zoom-fit", label: "适应画布", icon: , onClick: onFitContent }, ...(onAutoArrange ? [{ id: "zoom-auto-arrange", label: "自动整理节点", icon: , onClick: onAutoArrange }] : []), { kind: "separator", id: "zoom-separator" }, { id: "zoom-out", label: "缩小画布", icon: , onClick: () => commitScale(liveScaleRef.current - 0.1) }, { id: "zoom-precision", label: "精确缩放", wide: true, quiet: true, icon: {Math.round(scale * 100)}%, active: precisionOpen, onClick: () => setPrecisionOpen((value) => !value), }, { id: "zoom-in", label: "放大画布", icon: , onClick: () => commitScale(liveScaleRef.current + 0.1) }, { kind: "separator", id: "help-separator" }, { id: "zoom-shortcuts", label: "画布快捷键", icon: , onClick: onOpenShortcuts }, ]; return (
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}> {precisionOpen ? (
画布尺度 精确控制视野密度 {Math.round(scale * 100)}%
commitScale(Number(event.target.value) / 100)} aria-label="精确缩放画布" />
{QUICK_ZOOM_LEVELS.map((level) => ( commitScale(level)} > {Math.round(level * 100)}% ))}
) : null}
); }