vue3-sketch-ruler
Version:
> 是一个基于 Vue 3 + TypeScript 的标尺组件库,适用于低代码平台、大屏可视化、做图工具等场景,提供类似 Photoshop 的缩放与标尺辅助线体验。
188 lines (158 loc) • 4.98 kB
text/typescript
/**
* 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 }