UNPKG

vue3-sketch-ruler

Version:

> 是一个基于 Vue 3 + TypeScript 的标尺组件库,适用于低代码平台、大屏可视化、做图工具等场景,提供类似 Photoshop 的缩放与标尺辅助线体验。

188 lines (158 loc) 4.98 kB
/** * useRulerSnap - 智能吸附引擎(M3 W13) * 扩展自 M1 的 useSnapDetection,增加网格吸附、等距分布、自定义规则 */ import { type Ref, computed } from 'vue' import type { GuideLine } from '@sketch-ruler/core' export interface SnapTarget { type: 'tick' | 'guide-line' | 'custom' | 'grid' | 'equidistant' position: number priority: number } export interface SnapResult { position: number target: SnapTarget original: number } export interface SnapRule { id: string priority: number getTargets: (context: SnapContext) => SnapTarget[] } export interface SnapContext { direction: 'h' | 'v' position: number scale: number lines: GuideLine[] viewportSize: { width: number; height: number } } export interface SnapOptions { threshold: Ref<number> scale: Ref<number> tickTargets?: Ref<number[]> guideLineTargets?: Ref<number[]> customTargets?: Ref<number[]> strength?: Ref<number> gridSize?: Ref<number> enableEquidistant?: Ref<boolean> customRules?: Ref<SnapRule[]> lines?: Ref<GuideLine[]> viewportSize?: Ref<{ width: number; height: number }> } export function useRulerSnap(options: SnapOptions) { const { threshold, scale, tickTargets, guideLineTargets, customTargets, strength, gridSize, enableEquidistant, customRules, lines, viewportSize } = options const snapStrength = computed(() => strength?.value ?? 0.5) /** 检测单个数值的吸附 */ function snap(position: number, direction: 'h' | 'v'): SnapResult | null { const s = scale.value if (s <= 0) return null const candidates: SnapTarget[] = [] // 1. 刻度吸附(优先级 1) if (tickTargets?.value) { for (const pos of tickTargets.value) { candidates.push({ type: 'tick', position: pos, priority: 1 }) } } // 2. 参考线吸附(优先级 2) if (guideLineTargets?.value) { for (const pos of guideLineTargets.value) { candidates.push({ type: 'guide-line', position: pos, priority: 2 }) } } // 3. 自定义吸附(优先级 3) if (customTargets?.value) { for (const pos of customTargets.value) { candidates.push({ type: 'custom', position: pos, priority: 3 }) } } // 4. 网格吸附(优先级 4) if (gridSize?.value && gridSize.value > 0) { const gridPos = Math.round(position / gridSize.value) * gridSize.value candidates.push({ type: 'grid', position: gridPos, priority: 4 }) } // 5. 等距分布吸附(优先级 5) if (enableEquidistant?.value && lines?.value) { const dirLines = lines.value .filter((l) => l.orientation === direction && l.visible !== false) .map((l) => l.position) .sort((a, b) => a - b) const equiTargets = computeEquidistantTargets(dirLines) for (const pos of equiTargets) { candidates.push({ type: 'equidistant', position: pos, priority: 5 }) } } // 6. 自定义规则(优先级由规则定义) if (customRules?.value && lines?.value && viewportSize?.value) { const context: SnapContext = { direction, position, scale: s, lines: lines.value, viewportSize: viewportSize.value } for (const rule of customRules.value) { const targets = rule.getTargets(context) for (const t of targets) { candidates.push({ ...t, priority: rule.priority }) } } } if (candidates.length === 0) return null // 转换为屏幕像素距离进行比对 const screenPos = position * s const pixelThreshold = threshold.value let bestTarget: SnapTarget | null = null let bestDistance = Infinity for (const target of candidates) { const targetScreenPos = target.position * s const distance = Math.abs(screenPos - targetScreenPos) if (distance <= pixelThreshold && distance < bestDistance) { bestDistance = distance bestTarget = target } } if (!bestTarget) return null // 软吸附 const str = snapStrength.value const snappedPosition = position * (1 - str) + bestTarget.position * str return { position: snappedPosition, target: bestTarget, original: position } } return { snap } } /** 计算等距分布吸附目标 */ function computeEquidistantTargets(positions: number[]): number[] { if (positions.length < 2) return [] const targets: number[] = [] for (let i = 1; i < positions.length; i++) { const gap = positions[i] - positions[i - 1] // 向前延伸 const forward = positions[i] + gap if (!positions.includes(forward)) { targets.push(forward) } // 向后延伸 const backward = positions[i - 1] - gap if (!positions.includes(backward)) { targets.push(backward) } } return targets } /** 向后兼容导出 */ export { useRulerSnap as useSnapDetection }