import type { Ball, Point } from './types'; import { canAppend } from './rules'; /** 同一控制器接 PointerEvent 或 Cocos EventTouch;取消不等于松手。 */ export class Gesture { chain: number[] = []; active = false; private previous: Point | null = null; constructor(private readonly balls: () => Ball[], private readonly append = canAppend) {} down(p: Point): void { this.cancel(); this.previous = p; const b = this.hit(p); if (b) { this.active = true; this.chain = [b.id]; } } move(p: Point): void { if (!this.active) return; const from = this.previous || p; // 分段采样避免快速扫过小球时跳号;一次事件最多 120 次。 const steps = Math.min(120, Math.max(1, Math.ceil(Math.hypot(p.x - from.x, p.y - from.y) / 6))); for (let i = 1; i <= steps; i++) { const b = this.hit({ x: from.x + (p.x - from.x) * i / steps, y: from.y + (p.y - from.y) * i / steps }); if (!b || b.id === this.chain[this.chain.length - 1]) continue; if (this.chain.length > 1 && b.id === this.chain[this.chain.length - 2]) { this.chain.pop(); continue; } const balls = this.balls(); if (this.append(this.chain.map(id => balls.find(b => b.id === id)!), b)) this.chain.push(b.id); } this.previous = p; } up(): number[] { const path = [...this.chain]; this.cancel(); return path; } cancel(): void { this.chain = []; this.active = false; this.previous = null; } private hit(p: Point): Ball | undefined { return this.balls().filter(b => Math.hypot(b.x - p.x, b.y - p.y) <= b.r + 4) .sort((a, b) => Math.hypot(a.x - p.x, a.y - p.y) / a.r - Math.hypot(b.x - p.x, b.y - p.y) / b.r)[0]; } }