import type { SelectionBox, ViewportTransform } from "@/types/canvas"; export const CANVAS_VIEWPORT_PREVIEW_EVENT = "canvas:viewport-preview"; export const CANVAS_GRAPHICS_VIEWPORT_PREVIEW_EVENT = "canvas:graphics-viewport-preview"; export const CANVAS_SELECTION_PREVIEW_EVENT = "canvas:selection-preview"; // 空间网格点模式的点半径(像素单位)。远距(缩放 < 0.12)时用更小半径避免糊成一团。 // 同时被 (infinite-canvas)与滚动同步(applyCanvasLiveViewport)共享,避免两处漂移。 export function canvasDotPx(scale: number): string { return scale < 0.12 ? "1.2px" : "2px"; } export function applyCanvasLiveViewport(container: HTMLDivElement | null, viewport: ViewportTransform, notify = true) { if (!container) return; const gridSize = 48 * viewport.k; const committedScale = Number(container.style.getPropertyValue("--canvas-committed-scale")) || viewport.k; container.style.setProperty("--canvas-live-x", `${viewport.x}px`); container.style.setProperty("--canvas-live-y", `${viewport.y}px`); container.style.setProperty("--canvas-live-scale", String(viewport.k)); // 外置节点标题用同一帧逆倍率抵消世界层缩放,避免等待 React 提交后再校正尺寸。 container.style.setProperty("--canvas-live-inverse-scale", String(1 / Math.max(viewport.k, 0.05))); container.style.setProperty("--canvas-live-scale-ratio", String(viewport.k / committedScale)); container.style.setProperty("--canvas-grid-size", `${gridSize}px`); container.style.setProperty("--canvas-grid-x", `${viewport.x % gridSize}px`); container.style.setProperty("--canvas-grid-y", `${viewport.y % gridSize}px`); container.style.setProperty("--canvas-dot-size", canvasDotPx(viewport.k)); // 图形层必须逐帧跟随 DOM 世界层;浮层和滚动通知仍可按原频率节流。 container.dispatchEvent(new CustomEvent(CANVAS_GRAPHICS_VIEWPORT_PREVIEW_EVENT, { detail: viewport })); if (notify) { container.dispatchEvent(new CustomEvent(CANVAS_VIEWPORT_PREVIEW_EVENT, { detail: viewport })); // Ant Design overlays watch scrollable ancestors, but CSS transforms do not emit layout events. container.dispatchEvent(new Event("scroll")); } } export function subscribeCanvasGraphicsViewportPreview(container: HTMLDivElement, listener: (viewport: ViewportTransform) => void) { const handlePreview = (event: Event) => listener((event as CustomEvent).detail); container.addEventListener(CANVAS_GRAPHICS_VIEWPORT_PREVIEW_EVENT, handlePreview); return () => container.removeEventListener(CANVAS_GRAPHICS_VIEWPORT_PREVIEW_EVENT, handlePreview); } export function subscribeCanvasViewportPreview(container: HTMLDivElement, listener: (viewport: ViewportTransform) => void) { const handlePreview = (event: Event) => listener((event as CustomEvent).detail); container.addEventListener(CANVAS_VIEWPORT_PREVIEW_EVENT, handlePreview); return () => container.removeEventListener(CANVAS_VIEWPORT_PREVIEW_EVENT, handlePreview); } export function applyCanvasSelectionPreview(container: HTMLDivElement | null, selection: SelectionBox) { container?.dispatchEvent(new CustomEvent(CANVAS_SELECTION_PREVIEW_EVENT, { detail: selection })); } export function subscribeCanvasSelectionPreview(container: HTMLDivElement, listener: (selection: SelectionBox) => void) { const handlePreview = (event: Event) => listener((event as CustomEvent).detail); container.addEventListener(CANVAS_SELECTION_PREVIEW_EVENT, handlePreview); return () => container.removeEventListener(CANVAS_SELECTION_PREVIEW_EVENT, handlePreview); }