# tab_search.py import tkinter as tk from tkinter import ttk import math import matplotlib matplotlib.use("TkAgg") import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg class TabSearch: def __init__(self, notebook, tab_simulator): """ Tercera pestaña: 'Búsqueda de ángulo'. Recibe 'tab_simulator' para leer la config en la pestaña 1: - masa (m) - coef de rozamiento (b) - altura h0 - si hay drag o no - alcance (si el modo es "Alcance (m)") """ self.notebook = notebook self.tab_sim = tab_simulator # referencia a la pestaña de simulación self.frame = tk.Frame(notebook, width=280) self.frame.pack(side="left", fill="both", expand=True) # Fijamos ancho y desactivamos propagación para la columna self.frame.pack_propagate(False) # -- Layout principal: izquierda (botones + info), derecha (gráfico) -- self.frame_left = tk.Frame(self.frame, width=240) self.frame_left.pack(side="left", fill="y", padx=5, pady=5) self.frame_left.pack_propagate(False) self.frame_right = tk.Frame(self.frame) self.frame_right.pack(side="right", fill="both", expand=True, padx=5, pady=5) # 1) Botones e info en la parte izquierda tk.Button(self.frame_left, text="Importar config", command=self.import_config).pack(pady=5) tk.Button(self.frame_left, text="Ejecutar búsqueda", command=self.run_search).pack(pady=5) # Etiquetas para mostrar la config importada self.label_m = tk.Label(self.frame_left, text="Masa: ??? kg", bg="white") self.label_m.pack(pady=2, anchor="w") self.label_b = tk.Label(self.frame_left, text="b: ???", bg="white") self.label_b.pack(pady=2, anchor="w") self.label_h0 = tk.Label(self.frame_left, text="h0: ??? m", bg="white") self.label_h0.pack(pady=2, anchor="w") self.label_drag = tk.Label(self.frame_left, text="Drag: ???", bg="white") self.label_drag.pack(pady=2, anchor="w") self.label_x_target = tk.Label(self.frame_left, text="X_target: ???", bg="white") self.label_x_target.pack(pady=2, anchor="w") # Label para mostrar el ángulo/vel. hallados tras la búsqueda self.label_result = tk.Label(self.frame_left, text="Resultado:\n...", fg="green", bg="white") self.label_result.pack(pady=10, fill="x") # 2) Creamos figure + canvas a la derecha (para dibujar la curva) self.fig = plt.Figure(figsize=(4, 3), dpi=100) self.ax = self.fig.add_subplot(111) self.ax.set_xlabel("Ángulo (grados)") self.ax.set_ylabel("Vel. requerida (m/s)") self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame_right) self.canvas_widget = self.canvas.get_tk_widget() self.canvas_widget.pack(fill="both", expand=True) # Variables donde guardaremos la config importada self.m = 1.0 # masa self.b = 0.0 # coef rozamiento self.h0 = 0.0 # altura self.has_drag = False self.x_target = None # Alcance importado, si está en modo "Alcance (m)" def import_config(self): """ Lee la config actual de la pestaña 1 (simulador): - masa - b (si hay rozamiento) - h0 - check_rozamiento => has_drag - y si param es "Alcance (m)", tomamos ese v0 como x_target """ try: # Masa self.m = float(self.tab_sim.entry_masa.get()) # Rozamiento if self.tab_sim.check_rozamiento.get(): self.has_drag = True self.b = float(self.tab_sim.entry_b.get()) else: self.has_drag = False self.b = 0.0 # Altura self.h0 = float(self.tab_sim.entry_h0.get()) # Alcance si param== "Alcance (m)" if self.tab_sim.parametro_var.get() == "Alcance (m)": self.x_target = float(self.tab_sim.entry_v0.get()) else: self.x_target = None # Actualizamos en pantalla self.label_m.config(text=f"Masa: {self.m:.2f} kg") self.label_b.config(text=f"b: {self.b:.4f}") self.label_h0.config(text=f"h0: {self.h0:.2f} m") self.label_drag.config(text=f"Drag: {self.has_drag}") if self.x_target is not None: self.label_x_target.config(text=f"X_target: {self.x_target:.2f} m") else: self.label_x_target.config(text="X_target: (No hay)") self.label_result.config(text="Config importada OK", bg="lightgreen", fg="black") except ValueError: self.label_result.config(text="Error al leer config", bg="red", fg="white") self.x_target = None def run_search(self): """ Barre ángulos de 0..90 y busca la velocidad mínima para alcanzar la distancia x_target (si existe). Si no hay x_target (porque el modo era Vel(m/s)), mostramos error. """ if self.x_target is None: # Si el usuario está en modo "Velocidad (m/s)" en la pestaña 1, # no tenemos 'x_target' válido. Lógica: self.label_result.config( text="Error: en la pestaña 1 no se eligió 'Alcance (m)'", bg="red", fg="white" ) return X_target = self.x_target # usar la que importamos angulos = [] velocidades = [] best_angle = None best_v = 1e9 # Recorremos ángulos de 0..90 for angle_deg in range(0, 91): v0_min = 0.0 v0_max = 300.0 final_v = 0.0 # Bisección for _ in range(100): guess = 0.5*(v0_min + v0_max) dist = self.simular_dist(guess, angle_deg, self.has_drag) if abs(dist - X_target) < 0.1: final_v = guess break if dist < X_target: v0_min = guess else: v0_max = guess final_v = guess angulos.append(angle_deg) velocidades.append(final_v) if final_v < best_v: best_v = final_v best_angle = angle_deg # Dibujamos self.ax.clear() self.ax.set_xlabel("Ángulo (grados)") self.ax.set_ylabel("Vel. requerida (m/s)") self.ax.plot(angulos, velocidades, color="red") # Marcamos el punto mínimo self.ax.plot([best_angle], [best_v], marker="o", markersize=8, color="green") self.canvas.draw() self.label_result.config( text=f"Mín. en {best_angle}°, v0={best_v:.2f} m/s", bg="white", fg="green" ) def simular_dist(self, v0_guess, angle_deg, drag): """ Integra la distancia horizontal partiendo de (0,h0) hasta y<=0. Devuelve x final. Usa self.m, self.b, self.h0, drag (bool). """ dt = 0.01 x = 0.0 y = self.h0 alpha = math.radians(angle_deg) vx = v0_guess * math.cos(alpha) vy = v0_guess * math.sin(alpha) while True: if drag: ax = -(self.b / self.m) * vx ay = -9.8 - (self.b / self.m) * vy else: ax = 0.0 ay = -9.8 vx += ax*dt vy += ay*dt x += vx*dt y += vy*dt if y <= 0: break return x