import type { EffectInstance } from './types/effects'; function distSqPointToSegment( px: number, py: number, x1: number, y1: number, x2: number, y2: number, ): number { const dx = x2 - x1; const dy = y2 - y1; const len2 = dx * dx + dy * dy; if (len2 < 1e-18) { const ex = px - x1; const ey = py - y1; return ex * ex + ey * ey; } let t = ((px - x1) * dx + (py - y1) * dy) / len2; t = Math.max(0, Math.min(1, t)); const qx = x1 + t * dx; const qy = y1 + t * dy; const ex = px - qx; const ey = py - qy; return ex * ex + ey * ey; } /** Минимальная квадрат дистанции от точки (норм. координаты) до «тела» эффекта — для ластика. */ export function minDistSqEffectToPoint(inst: EffectInstance, p: { x: number; y: number }): number { switch (inst.type) { case 'fog': case 'fire': case 'rain': { let best = Number.POSITIVE_INFINITY; for (const q of inst.points) { const dx = q.x - p.x; const dy = q.y - p.y; best = Math.min(best, dx * dx + dy * dy); } return best; } case 'lightning': return distSqPointToSegment(p.x, p.y, inst.start.x, inst.start.y, inst.end.x, inst.end.y); case 'freeze': { const dx = inst.at.x - p.x; const dy = inst.at.y - p.y; return dx * dx + dy * dy; } case 'scorch': case 'ice': { const dx = inst.at.x - p.x; const dy = inst.at.y - p.y; return dx * dx + dy * dy; } default: return Number.POSITIVE_INFINITY; } } function eraseHitThresholdSq(inst: EffectInstance, toolRadiusN: number): number { if (inst.type === 'scorch' || inst.type === 'ice') { const r = toolRadiusN + inst.radiusN; return r * r; } return toolRadiusN * toolRadiusN; } /** Ближайший эффект в пределах радиуса ластика, иначе `null`. */ export function pickEraseTargetId( instances: readonly EffectInstance[], p: { x: number; y: number }, toolRadiusN: number, ): string | null { let best: { id: string; dd: number } | null = null; for (const inst of instances) { const dd = minDistSqEffectToPoint(inst, p); const th = eraseHitThresholdSq(inst, toolRadiusN); if (dd <= th && (!best || dd < best.dd)) { best = { id: inst.id, dd }; } } return best?.id ?? null; }