Untitled

 avatar
unknown
plain_text
5 months ago
26 kB
5
Indexable
import customtkinter as ctk
from tkinter import messagebox, StringVar
import tkinter as tk
from math import sin, cos, tan, pi, asin, acos, atan
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import re
from fractions import Fraction

class CircleGeneratorApp:
    def __init__(self, root, parent_root=None):
        self.root = root
        self.parent_root = parent_root
        self.root.title("Генератор Тригонометрической Окружности")
        self.root.geometry("1200x800")

        self.button_color = "#53acf8"

        # Верхняя панель с кнопкой "Назад"
        top_frame = ctk.CTkFrame(self.root)
        top_frame.pack(side="top", fill="x")

        back_button = ctk.CTkButton(top_frame, text="Назад", fg_color=self.button_color, corner_radius=15,
                                    command=self.go_back)
        back_button.pack(side="left", padx=10, pady=10)

        # Основные фреймы
        self.left_frame = ctk.CTkFrame(self.root, width=400)
        self.left_frame.pack(side="left", fill="y", padx=10, pady=10)

        self.right_frame = ctk.CTkFrame(self.root)
        self.right_frame.pack(side="right", fill="both", expand=True, padx=10, pady=10)

        # Инициализация переменных
        self.equations = []
        self.lower_bound = StringVar()
        self.upper_bound = StringVar()
        self.latex_output = None
        self.preview_canvas = None

        # Настройки окружности
        self.setup_circle_settings()

    def go_back(self):
        self.root.destroy()
        if self.parent_root:
            self.parent_root.deiconify()

    def setup_circle_settings(self):
        # Заголовок настроек
        ctk.CTkLabel(self.left_frame, text="Настройки окружности", font=("Arial", 18)).pack(pady=10)

        # Поле для ввода интервала
        interval_frame = ctk.CTkFrame(self.left_frame)
        interval_frame.pack(pady=5)

        ctk.CTkLabel(interval_frame, text="Нижняя граница:").grid(row=0, column=0, padx=5, pady=5)
        ctk.CTkEntry(interval_frame, textvariable=self.lower_bound).grid(row=0, column=1, padx=5, pady=5)

        ctk.CTkLabel(interval_frame, text="Верхняя граница:").grid(row=1, column=0, padx=5, pady=5)
        ctk.CTkEntry(interval_frame, textvariable=self.upper_bound).grid(row=1, column=1, padx=5, pady=5)

        # Кнопка для добавления уравнений
        add_eq_button = ctk.CTkButton(self.left_frame, text="Добавить уравнение", fg_color=self.button_color,
                                      corner_radius=15, command=self.add_equation)
        add_eq_button.pack(pady=10)

        # Фрейм для списка уравнений
        self.equations_frame = ctk.CTkFrame(self.left_frame)
        self.equations_frame.pack(pady=5)

        # Добавляем одно уравнение по умолчанию
        self.add_equation()

        # Кнопки для генерации кода и предпросмотра
        action_frame = ctk.CTkFrame(self.left_frame)
        action_frame.pack(pady=10)

        generate_button = ctk.CTkButton(action_frame, text="Сгенерировать код LaTeX", fg_color=self.button_color,
                                        corner_radius=15, command=self.generate_latex_code)
        generate_button.pack(pady=5)

        copy_button = ctk.CTkButton(action_frame, text="Скопировать код в буфер", fg_color=self.button_color,
                                    corner_radius=15, command=self.copy_to_clipboard)
        copy_button.pack(pady=5)

        preview_button = ctk.CTkButton(action_frame, text="Показать предпросмотр", fg_color=self.button_color,
                                       corner_radius=15, command=self.show_preview)
        preview_button.pack(pady=5)

    def add_equation(self):
        eq_var = StringVar()
        eq_frame = ctk.CTkFrame(self.equations_frame)
        eq_frame.pack(fill="x", pady=5)

        ctk.CTkEntry(eq_frame, textvariable=eq_var, width=200).pack(side="left", padx=5)
        remove_button = ctk.CTkButton(eq_frame, text="Удалить", fg_color="red", corner_radius=15,
                                      command=lambda: self.remove_equation(eq_frame, eq_var))
        remove_button.pack(side="left", padx=5)

        self.equations.append(eq_var)

    def remove_equation(self, frame, eq_var):
        frame.destroy()
        self.equations.remove(eq_var)

    def generate_latex_code(self):
        try:
            # Парсинг интервала
            lower_bound = self.parse_expression(self.lower_bound.get())
            upper_bound = self.parse_expression(self.upper_bound.get())

            if lower_bound >= upper_bound:
                messagebox.showerror("Ошибка", "Нижняя граница должна быть меньше верхней границы.")
                return

            # Преобразование границ в градусы
            lower_deg = np.degrees(lower_bound)
            upper_deg = np.degrees(upper_bound)

            # Форматируем границы для вывода
            lower_bound_tex = self.format_angle_tex(lower_bound)
            upper_bound_tex = self.format_angle_tex(upper_bound)

            # Подготовка LaTeX кода
            latex_code = ""

            # Шаблон для текста
            latex_code += f"Отберем корни на тригонометрической окружности. Для этого отметим на ней дугу, соответствующую отрезку $\\otr{{{lower_bound_tex}; {upper_bound_tex}}},$ концы этой дуги и лежащие на ней точки серий решений из пункта а).\n\n"

            # Начало окружности
            latex_code += "\\begin{center}\n"
            latex_code += "    \\begin{tikzpicture}[> = stealth, scale=0.7]\n"
            latex_code += "        \\draw[thick, ->, black] (-4, 0) -- (4, 0);\n"
            latex_code += "        \\draw[thick, ->, black] (0, -4) -- (0, 4);\n"
            latex_code += "        \\draw (0, 0) circle (3);\n"

            # Отрисовка дуги интервала
            latex_code += f"        \\draw[shkblue, line width=2pt] ({lower_deg}:{3}) arc ({lower_deg}:{upper_deg}:{3});\n"

            # Отметка границ интервала
            latex_code += f"        \\fill[shkblue] ({lower_deg}:{3}) node[above left] {{$ {lower_bound_tex} $}} circle (3pt);\n"
            latex_code += f"        \\fill[shkblue] ({upper_deg}:{3}) node[above right] {{$ {upper_bound_tex} $}} circle (3pt);\n"

            # Обработка уравнений
            solutions_within = []
            solutions_outside = []
            for eq_var in self.equations:
                eq = eq_var.get()
                general_solutions = self.solve_equation(eq)
                specific_solutions = self.get_solutions_in_interval(general_solutions, lower_bound, upper_bound)
                for sol in specific_solutions:
                    solutions_within.append(sol)
                # Добавляем решения вне интервала
                outside_solutions = self.get_solutions_outside_interval(general_solutions, lower_bound, upper_bound)
                for sol in outside_solutions:
                    solutions_outside.append(sol)

            # Удаляем дубликаты
            solutions_within = list(set(solutions_within))
            solutions_outside = list(set(solutions_outside))

            # Сортируем решения
            solutions_within.sort()
            solutions_outside.sort()

            # Подготовка набора координат решений внутри интервала
            solutions_within_coords = set()
            for angle in solutions_within:
                x = round(np.cos(angle), 8)
                y = round(np.sin(angle), 8)
                solutions_within_coords.add((x, y))

            # Фильтрация решений вне интервала
            filtered_solutions_outside = []
            for angle in solutions_outside:
                x = round(np.cos(angle), 8)
                y = round(np.sin(angle), 8)
                if (x, y) not in solutions_within_coords:
                    filtered_solutions_outside.append(angle)
            solutions_outside = filtered_solutions_outside

            # Отрисовка решений внутри интервала
            for angle in solutions_within:
                deg = np.degrees(angle)
                deg = round(deg, 5)  # Округляем для избежания ошибок
                position = self.get_label_position(deg)
                angle_tex = self.format_angle_tex(angle)
                latex_code += f"        \\fill[black] ({deg}:{3}) node[{position}] {{$ {angle_tex} $}} circle (3pt);\n"

            # Отрисовка решений вне интервала (без подписей)
            for angle in solutions_outside:
                deg = np.degrees(angle)
                deg = round(deg, 5)
                latex_code += f"        \\draw[black, fill = white] ({deg}:{3}) circle (3pt);\n"

            # Добавление пунктирных линий между парами решений (для sin и cos)
            for eq_var in self.equations:
                eq = eq_var.get().replace(' ', '')
                if eq.startswith('sinx=') or eq.startswith('cosx='):
                    # Найдем все пары решений внутри интервала
                    pairs = self.get_solution_pairs(eq, lower_bound, upper_bound)
                    for angle1, angle2 in pairs:
                        deg1 = np.degrees(angle1)
                        deg2 = np.degrees(angle2)
                        latex_code += f"        \\draw[black, dashed] ({deg1}:{3}) -- ({deg2}:{3});\n"

            # Конец окружности
            latex_code += "    \\end{tikzpicture}\n"
            latex_code += "\\end{center}\n\n"

            # Вывод точек, которые лежат внутри интервала
            if len(solutions_within) > 1:
                points_within = " ".join([f"${self.format_angle_tex(a)};$" for a in solutions_within[:-1]]) + " " + f"${self.format_angle_tex(solutions_within[-1])}.$"
            else:
                points_within = f"${self.format_angle_tex(solutions_within[0])}$"

            latex_code += f"Следовательно, на отрезке $\\otr{{{lower_bound_tex}; {upper_bound_tex}}}$ лежат точки {points_within}"

            # Выводим код в текстовое поле
            if self.latex_output:
                self.latex_output.destroy()

            self.latex_output = ctk.CTkTextbox(self.right_frame, width=800, height=300)
            self.latex_output.pack(pady=10)
            self.latex_output.insert("1.0", latex_code)

        except Exception as e:
            messagebox.showerror("Ошибка", f"Произошла ошибка при генерации кода: {e}")

    def copy_to_clipboard(self):
        if self.latex_output:
            self.root.clipboard_clear()
            self.root.clipboard_append(self.latex_output.get("1.0", "end"))
            self.root.update()
            messagebox.showinfo("Копирование", "Код LaTeX скопирован в буфер обмена.")
        else:
            messagebox.showwarning("Внимание", "Сначала сгенерируйте код LaTeX.")

    def show_preview(self):
        if self.preview_canvas:
            self.preview_canvas.get_tk_widget().destroy()
            self.preview_canvas = None

        try:
            # Генерируем данные для предварительного просмотра
            fig, ax = plt.subplots(figsize=(6,6))
            ax.set_aspect('equal')
            ax.axis('off')

            # Рисуем окружность
            circle = plt.Circle((0,0), 1, fill=False)
            ax.add_artist(circle)

            # Рисуем координатные оси
            ax.plot([-1.2, 1.2], [0, 0], color='black', lw=1)
            ax.plot([0, 0], [-1.2, 1.2], color='black', lw=1)

            # Парсинг интервала
            lower_bound = self.parse_expression(self.lower_bound.get())
            upper_bound = self.parse_expression(self.upper_bound.get())

            # Отрисовка дуги интервала
            theta = np.linspace(lower_bound, upper_bound, 100)
            x = np.cos(theta)
            y = np.sin(theta)
            ax.plot(x, y, color='blue', lw=3)

            # Отметка границ интервала
            x_lb = np.cos(lower_bound)
            y_lb = np.sin(lower_bound)
            x_ub = np.cos(upper_bound)
            y_ub = np.sin(upper_bound)
            ax.plot([x_lb], [y_lb], 'o', color='blue')
            ax.plot([x_ub], [y_ub], 'o', color='blue')

            ax.text(x_lb, y_lb, f"${self.format_angle_tex(lower_bound)}$", ha='right', va='bottom')
            ax.text(x_ub, y_ub, f"${self.format_angle_tex(upper_bound)}$", ha='left', va='bottom')

            # Обработка уравнений
            solutions_within = []
            solutions_outside = []
            for eq_var in self.equations:
                eq = eq_var.get()
                general_solutions = self.solve_equation(eq)
                specific_solutions = self.get_solutions_in_interval(general_solutions, lower_bound, upper_bound)
                for sol in specific_solutions:
                    solutions_within.append(sol)
                # Добавляем решения вне интервала
                outside_solutions = self.get_solutions_outside_interval(general_solutions, lower_bound, upper_bound)
                for sol in outside_solutions:
                    solutions_outside.append(sol)

            # Удаляем дубликаты
            solutions_within = list(set(solutions_within))
            solutions_outside = list(set(solutions_outside))

            # Подготовка набора координат решений внутри интервала
            solutions_within_coords = set()
            for angle in solutions_within:
                x_coord = round(np.cos(angle), 8)
                y_coord = round(np.sin(angle), 8)
                solutions_within_coords.add((x_coord, y_coord))

            # Фильтрация решений вне интервала
            filtered_solutions_outside = []
            for angle in solutions_outside:
                x_coord = round(np.cos(angle), 8)
                y_coord = round(np.sin(angle), 8)
                if (x_coord, y_coord) not in solutions_within_coords:
                    filtered_solutions_outside.append(angle)
            solutions_outside = filtered_solutions_outside

            # Отрисовка решений внутри интервала
            for angle in solutions_within:
                x = np.cos(angle)
                y = np.sin(angle)
                ax.plot([x], [y], 'o', color='black')
                pos = self.get_label_position_plot(angle)
                angle_tex = self.format_angle_tex(angle)
                ax.text(x, y, f"${angle_tex}$", ha=pos[0], va=pos[1])

            # Отрисовка решений вне интервала (без подписей)
            for angle in solutions_outside:
                x = np.cos(angle)
                y = np.sin(angle)
                ax.plot([x], [y], 'o', color='white', markeredgecolor='black')

            self.preview_canvas = FigureCanvasTkAgg(fig, master=self.right_frame)
            self.preview_canvas.draw()
            self.preview_canvas.get_tk_widget().pack(pady=10)

        except Exception as e:
            messagebox.showerror("Ошибка", f"Произошла ошибка при генерации предпросмотра: {e}")

    def parse_expression(self, expr):
        # Заменяем 'pi' на 'np.pi'
        expr = expr.replace('pi', 'np.pi')
        # Вставляем '*' между цифрой и буквой или открывающей скобкой, избегая случаев с '-' перед цифрой
        expr = re.sub(r'(?<![\d\.])(\d)([a-zA-Z\(])', r'\1*\2', expr)
        expr = re.sub(r'(\d\.)([a-zA-Z\(])', r'\1*\2', expr)  # Для чисел с плавающей точкой
        expr = expr.replace(')(', ')*(')  # Между скобками
        try:
            # Определяем безопасный словарь для eval
            allowed_names = {
                'np': np,
                'sin': np.sin,
                'cos': np.cos,
                'tan': np.tan,
                'asin': np.arcsin,
                'acos': np.arccos,
                'atan': np.arctan,
                'sqrt': np.sqrt,
                '__builtins__': {}
            }
            value = eval(expr, {"__builtins__": None}, allowed_names)
            return value
        except Exception as e:
            raise ValueError(f"Ошибка при разборе выражения '{expr}': {e}")

    def solve_equation(self, eq):
        # Удаляем пробелы
        eq = eq.replace(' ', '')
        # Регулярное выражение для парсинга
        pattern = r'(sin|cos|tan|cot)x=(.+)'
        match = re.match(pattern, eq)
        if not match:
            raise ValueError(f"Некорректный формат уравнения: {eq}")
        func = match.group(1)
        value_str = match.group(2)
        # Парсим значение
        value = self.parse_expression(value_str)
        general_solutions = []
        if func == 'sin':
            if abs(value) > 1:
                return []  # Нет решений
            angle = asin(value)
            general_solutions.append(('sin', angle))
        elif func == 'cos':
            if abs(value) > 1:
                return []  # Нет решений
            angle = acos(value)
            general_solutions.append(('cos', angle))
        elif func == 'tan':
            angle = atan(value)
            general_solutions.append(('tan', angle))
        elif func == 'cot':
            angle = atan(1 / value)
            general_solutions.append(('cot', angle))
        else:
            raise ValueError(f"Неизвестная функция: {func}")
        return general_solutions

    def get_solutions_in_interval(self, general_solutions, lower_bound, upper_bound):
        specific_solutions = []
        for sol_type, angle in general_solutions:
            if sol_type in ['sin', 'cos']:
                period = 2 * np.pi
                k_min = int(np.floor((lower_bound - angle) / period))
                k_max = int(np.ceil((upper_bound - angle) / period))
                for k in range(k_min, k_max + 1):
                    x = angle + period * k
                    if lower_bound <= x <= upper_bound:
                        specific_solutions.append(x)
                    # Второе решение для sin и cos
                    if sol_type == 'sin':
                        x2 = np.pi - angle + period * k
                    else:  # 'cos'
                        x2 = -angle + period * k
                    if lower_bound <= x2 <= upper_bound:
                        specific_solutions.append(x2)
            elif sol_type in ['tan', 'cot']:
                period = np.pi
                k_min = int(np.floor((lower_bound - angle) / period))
                k_max = int(np.ceil((upper_bound - angle) / period))
                for k in range(k_min, k_max + 1):
                    x = angle + period * k
                    if lower_bound <= x <= upper_bound:
                        specific_solutions.append(x)
            else:
                pass  # Для других функций
        return specific_solutions

    def get_solutions_outside_interval(self, general_solutions, lower_bound, upper_bound):
        specific_solutions = []
        for sol_type, angle in general_solutions:
            if sol_type in ['sin', 'cos']:
                period = 2 * np.pi
                # Проверяем решения рядом с границами интервала
                k_values = range(-2, 3)
                for k in k_values:
                    x = angle + period * k
                    if x < lower_bound or x > upper_bound:
                        specific_solutions.append(x)
                    # Второе решение для sin и cos
                    if sol_type == 'sin':
                        x2 = np.pi - angle + period * k
                    else:
                        x2 = -angle + period * k
                    if x2 < lower_bound or x2 > upper_bound:
                        specific_solutions.append(x2)
            elif sol_type in ['tan', 'cot']:
                period = np.pi
                k_values = range(-2, 3)
                for k in k_values:
                    x = angle + period * k
                    if x < lower_bound or x > upper_bound:
                        specific_solutions.append(x)
            else:
                pass  # Для других функций
        return specific_solutions

    def format_angle_tex(self, angle):
        # Преобразуем угол в кратные pi
        frac = Fraction(angle / np.pi).limit_denominator()
        numerator = frac.numerator
        denominator = frac.denominator
        if denominator == 1:
            if numerator == 0:
                return '0'
            elif numerator == 1:
                return '\\pi'
            elif numerator == -1:
                return '-\\pi'
            else:
                return f'{numerator}\\pi'
        else:
            if numerator < 0:
                num_str = -numerator  # Делаем числитель положительным
                sign = '-'
            else:
                num_str = numerator
                sign = ''
            if num_str == 1:
                num_str = ''
            return f'{sign}\\dfrac{{{num_str}\\pi}}{{{denominator}}}'

    def get_label_position(self, deg):
        # Определяем положение метки на основе квадранта
        if 0 < deg < 90 or -360 < deg < -270:
            return 'above right'
        elif 90 < deg < 180 or -270 < deg < -180:
            return 'above left'
        elif 180 < deg < 270 or -180 < deg < -90:
            return 'below left'
        elif 270 < deg < 360 or -90 < deg < 0:
            return 'below right'
        elif deg == 0 or deg == 360 or deg == -360 or deg == -720:
            return 'above right'  # Ось X положительная
        elif deg == 90 or deg == -270 or deg == -630 or deg == -990:
            return 'above left'  # Ось Y положительная
        elif deg == 180 or deg == -180 or deg == -540 or deg == -900 :
            return 'above left'  # Ось X отрицательная
        elif deg == 270 or deg == -90 or deg == -450 or deg == -810:
            return 'below left'  # Ось Y отрицательная
        else:
            return 'above left'  # По умолчанию

    def get_label_position_plot(self, angle):
        # Для matplotlib, аналогично методу get_label_position
        deg = np.degrees(angle)
        if 0 < deg < 90 or -360 < deg < -270:
            return ('right', 'top')
        elif 90 < deg < 180 or -270 < deg < -180:
            return ('left', 'top')
        elif 180 < deg < 270 or -180 < deg < -90:
            return ('left', 'bottom')
        elif 270 < deg < 360 or -90 < deg < 0:
            return ('right', 'bottom')
        elif deg == 0 or deg == 360 or deg == -360 or deg == -720:
            return ('right', 'top')
        elif deg == 90 or deg == -270 or deg == -630 or deg == -990:
            return ('left', 'top')
        elif deg == 180 or deg == -180 or deg == -540 or deg == -900 :
            return ('left', 'top')
        elif deg == 270 or deg == -90 or deg == -450 or deg == -810:
            return ('left', 'bottom')
        else:
            return ('center', 'center')

    def get_solution_pairs(self, eq, lower_bound, upper_bound):
        # Находим пары решений для sin и cos внутри интервала
        pairs = []
        general_solutions = self.solve_equation(eq)
        for sol_type, angle in general_solutions:
            if sol_type in ['sin', 'cos']:
                period = 2 * np.pi
                k_min = int(np.floor((lower_bound - angle) / period))
                k_max = int(np.ceil((upper_bound - angle) / period))
                for k in range(k_min, k_max + 1):
                    x1 = angle + period * k
                    if sol_type == 'sin':
                        x2 = np.pi - angle + period * k
                    else:
                        x2 = -angle + period * k
                    if lower_bound <= x1 <= upper_bound and lower_bound <= x2 <= upper_bound:
                        pairs.append((x1, x2))
            else:
                pass
        return pairs
Editor is loading...
Leave a Comment