commit 93c9e7b91be86ce80499e9c0071edb5870a83ff0 Author: Pedro Jose Romero Gombau Date: Thu Jan 16 16:28:45 2025 +0100 Init diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bec5f46 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.9-slim + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y python3-tk python3-pil python3-pil.imagetk && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY . /app + +RUN pip install --no-cache-dir -r requirements.txt + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/__pycache__/geometry_viewer.cpython-311.pyc b/__pycache__/geometry_viewer.cpython-311.pyc new file mode 100644 index 0000000..2d87062 Binary files /dev/null and b/__pycache__/geometry_viewer.cpython-311.pyc differ diff --git a/__pycache__/tab_drag.cpython-311.pyc b/__pycache__/tab_drag.cpython-311.pyc new file mode 100644 index 0000000..d3d9736 Binary files /dev/null and b/__pycache__/tab_drag.cpython-311.pyc differ diff --git a/__pycache__/tab_search.cpython-311.pyc b/__pycache__/tab_search.cpython-311.pyc new file mode 100644 index 0000000..70765db Binary files /dev/null and b/__pycache__/tab_search.cpython-311.pyc differ diff --git a/__pycache__/tab_simulator.cpython-311.pyc b/__pycache__/tab_simulator.cpython-311.pyc new file mode 100644 index 0000000..ced10a2 Binary files /dev/null and b/__pycache__/tab_simulator.cpython-311.pyc differ diff --git a/geometry_viewer.py b/geometry_viewer.py new file mode 100644 index 0000000..336ddfb --- /dev/null +++ b/geometry_viewer.py @@ -0,0 +1,71 @@ +import matplotlib +matplotlib.use("TkAgg") # Para asegurarnos de usar Tkinter como backend +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D # necesario para 3D +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import numpy as np + +def plot_geometry_in_frame(parent_frame, geom, p1, p2): + """ + Dibuja la geometría (Prisma/Cilindro/Esfera) en un Axes3D dentro + de 'parent_frame' (un Frame de Tkinter). No abre ventana nueva. + (Ejemplo de integración en tu TabDrag) + """ + + fig = plt.Figure(figsize=(4, 3), dpi=100) + ax = fig.add_subplot(111, projection='3d') + ax.set_title(f"{geom}", fontsize=10) + + if geom == "Prisma cuadrado": + lado = p1 + largo = p2 + Xs = [0, lado, lado, 0, 0, lado, lado, 0] + Ys = [0, 0, lado, lado, 0, 0, lado, lado] + Zs = [0, 0, 0, 0, largo, largo, largo, largo] + edges = [(0,1),(1,2),(2,3),(3,0), + (4,5),(5,6),(6,7),(7,4), + (0,4),(1,5),(2,6),(3,7)] + for (i,j) in edges: + ax.plot([Xs[i],Xs[j]], [Ys[i],Ys[j]], [Zs[i],Zs[j]], color='g') + ax.set_xlim(0, max(lado,1)) + ax.set_ylim(0, max(lado,1)) + ax.set_zlim(0, max(largo,1)) + + elif geom == "Cilindro": + r = p1 + h = p2 + theta = np.linspace(0, 2*np.pi, 30) + z = np.linspace(0, h, 30) + theta_grid, z_grid = np.meshgrid(theta, z) + X = r * np.cos(theta_grid) + Y = r * np.sin(theta_grid) + Z = z_grid + ax.plot_surface(X, Y, Z, color='cyan', alpha=0.5) + ax.set_xlim(-r, r) + ax.set_ylim(-r, r) + ax.set_zlim(0, h) + + elif geom == "Esfera": + r = p1 + phi = np.linspace(0, np.pi, 30) + theta = np.linspace(0, 2*np.pi, 30) + phi_grid, theta_grid = np.meshgrid(phi, theta) + X = r*np.sin(phi_grid)*np.cos(theta_grid) + Y = r*np.sin(phi_grid)*np.sin(theta_grid) + Z = r*np.cos(phi_grid) + ax.plot_surface(X, Y, Z, color='yellow', alpha=0.6) + ax.set_xlim(-r, r) + ax.set_ylim(-r, r) + ax.set_zlim(-r, r) + + else: + ax.text2D(0.2, 0.5, "Geometría desconocida", transform=ax.transAxes) + + # Borramos lo anterior en parent_frame y embebemos el nuevo canvas + for child in parent_frame.winfo_children(): + child.destroy() + + canvas = FigureCanvasTkAgg(fig, master=parent_frame) + canvas_widget = canvas.get_tk_widget() + canvas_widget.pack(fill="both", expand=True) + canvas.draw() diff --git a/launcher.bat b/launcher.bat new file mode 100644 index 0000000..7c85d2b --- /dev/null +++ b/launcher.bat @@ -0,0 +1 @@ +docker run -it --rm -e DISPLAY=host.docker.internal:0 launchsim \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..241ab4a --- /dev/null +++ b/main.py @@ -0,0 +1,32 @@ +import tkinter as tk +from tkinter import ttk + +from tab_simulator import TabSimulator +from tab_drag import TabDrag +from tab_search import TabSearch + +class MainApp: + def __init__(self, master): + self.master = master + self.master.title("Obtención de trayectoria y energía") + + self.notebook = ttk.Notebook(master) + self.notebook.pack(fill="both", expand=True) + + # Pestaña 1: Simulador Trayectoria + self.tab_sim = TabSimulator(self.notebook) + self.notebook.add(self.tab_sim.frame, text="Simulador") + + # Pestaña 2: Cálculo Coef. Rozamiento + self.tab_drag = TabDrag(self.notebook, self.tab_sim) + self.notebook.add(self.tab_drag.frame, text="Rozamiento") + + # Pestaña 3: Búsqueda (ángulo que minimiza la velocidad) + self.tab_search = TabSearch(self.notebook, self.tab_sim) + self.notebook.add(self.tab_search.frame, text="Optimización") + + +if __name__ == "__main__": + root = tk.Tk() + app = MainApp(root) + root.mainloop() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4b43f7e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +matplotlib \ No newline at end of file diff --git a/tab_drag.py b/tab_drag.py new file mode 100644 index 0000000..ca99062 --- /dev/null +++ b/tab_drag.py @@ -0,0 +1,200 @@ +import tkinter as tk +from tkinter import ttk +import math + +import matplotlib +matplotlib.use("TkAgg") +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import numpy as np + +class TabDrag: + def __init__(self, notebook, tab_simulator): + """ + Pestaña para calcular un coef. 'b' a partir de la geometría. + Luego se pasa a tab_simulator.set_b_value(...) para usarlo en la simulación. + """ + self.notebook = notebook + self.tab_simulator = tab_simulator + + self.frame = tk.Frame(notebook) + self.frame.pack(fill="both", expand=True) + + # Layout principal + frame_left = tk.Frame(self.frame) + frame_left.pack(side="left", fill="y", padx=5, pady=5) + + frame_right = tk.Frame(self.frame, bd=2, relief="groove") + frame_right.pack(side="right", fill="both", expand=True, padx=5, pady=5) + + self.frame_3d = tk.Frame(frame_right) + self.frame_3d.pack(fill="both", expand=True) + + # Sección Izquierda: geometría + parámetros + tk.Label(frame_left, text="Selecciona geometría:").pack(anchor="w") + self.geometria_var = tk.StringVar(value="Prisma cuadrado") + self.combo_geometrias = ttk.Combobox( + frame_left, textvariable=self.geometria_var, + values=["Prisma cuadrado", "Cilindro", "Esfera"], + state="readonly" + ) + self.combo_geometrias.pack(anchor="w", pady=5) + + self.frame_parametros = tk.Frame(frame_left, bd=1, relief="sunken") + self.frame_parametros.pack(fill="x", pady=5) + + self.label_param1 = tk.Label(self.frame_parametros, text="Parámetro 1:") + self.label_param1.grid(row=0, column=0, sticky="w", padx=5, pady=2) + self.entry_param1 = tk.Entry(self.frame_parametros, width=8) + self.entry_param1.grid(row=0, column=1, padx=5, pady=2) + self.entry_param1.insert(0, "1") + + self.label_param2 = tk.Label(self.frame_parametros, text="Parámetro 2:") + self.label_param2.grid(row=1, column=0, sticky="w", padx=5, pady=2) + self.entry_param2 = tk.Entry(self.frame_parametros, width=8) + self.entry_param2.grid(row=1, column=1, padx=5, pady=2) + self.entry_param2.insert(0, "7") + + self.label_result_b = tk.Label(frame_left, text="Coef (b): N/A", fg="blue") + self.label_result_b.pack() + + btn_calc_b = tk.Button( + frame_left, text="Calcular Coef. Rozamiento", + command=self.calcular_coef_rozamiento + ) + btn_calc_b.pack(pady=5) + + tk.Button( + frame_left, text="Refrescar Vista 3D", + command=self.refrescar_3d + ).pack(pady=10) + + # Creamos la figura 3D + self.fig = plt.Figure(figsize=(4,3), dpi=100) + self.ax = self.fig.add_subplot(111, projection='3d') + self.canvas_3d = FigureCanvasTkAgg(self.fig, master=self.frame_3d) + self.canvas_widget = self.canvas_3d.get_tk_widget() + self.canvas_widget.pack(fill="both", expand=True) + + self.combo_geometrias.bind("<>", self.on_change_geometria) + self.update_param_labels() + self.refrescar_3d() + + def on_change_geometria(self, event=None): + self.update_param_labels() + self.refrescar_3d() + + def update_param_labels(self): + geom = self.geometria_var.get() + self.entry_param2.config(state="normal", fg="black") + if geom=="Prisma cuadrado": + self.label_param1.config(text="Lado base (m):") + self.label_param2.config(text="Longitud (m):") + elif geom=="Cilindro": + self.label_param1.config(text="Radio (m):") + self.label_param2.config(text="Altura (m):") + elif geom=="Esfera": + self.label_param1.config(text="Radio (m):") + self.label_param2.config(text="(no aplica):") + self.entry_param2.delete(0, tk.END) + self.entry_param2.config(state="disabled", fg="gray") + else: + self.label_param1.config(text="Parámetro 1:") + self.label_param2.config(text="Parámetro 2:") + + def refrescar_3d(self): + # Dibuja la geometría en self.ax + geom = self.geometria_var.get() + try: + p1 = float(self.entry_param1.get()) + except ValueError: + p1=1.0 + try: + if self.entry_param2.cget("state")!="disabled": + p2 = float(self.entry_param2.get()) + else: + p2=1.0 + except ValueError: + p2=1.0 + + self.ax.clear() + self.ax.set_axis_off() + + if geom=="Prisma cuadrado": + Xs=[0,p1,p1,0,0,p1,p1,0] + Ys=[0,0,p1,p1,0,0,p1,p1] + Zs=[0,0,0,0,p2,p2,p2,p2] + edges=[(0,1),(1,2),(2,3),(3,0), + (4,5),(5,6),(6,7),(7,4), + (0,4),(1,5),(2,6),(3,7)] + for (i,j) in edges: + self.ax.plot([Xs[i],Xs[j]], [Ys[i],Ys[j]], [Zs[i],Zs[j]], color='orange') + self.ax.set_box_aspect((p1,p1,p2)) + elif geom=="Cilindro": + import numpy as np + import math + r=p1 + h=p2 + theta=np.linspace(0,2*math.pi,30) + z=np.linspace(0,h,30) + T,Z=np.meshgrid(theta,z) + X=r*np.cos(T) + Y=r*np.sin(T) + self.ax.plot_surface(X, Y, Z, color='orange', alpha=0.8) + self.ax.set_box_aspect((2*r,2*r,h)) + elif geom=="Esfera": + import numpy as np + import math + r=p1 + phi=np.linspace(0,math.pi,30) + t=np.linspace(0,2*math.pi,30) + phi_grid,t_grid=np.meshgrid(phi,t) + X=r*np.sin(phi_grid)*np.cos(t_grid) + Y=r*np.sin(phi_grid)*np.sin(t_grid) + Z=r*np.cos(phi_grid) + self.ax.plot_surface(X,Y,Z,color='orange',alpha=0.8) + self.ax.set_box_aspect((2*r,2*r,2*r)) + else: + self.ax.text2D(0.3,0.5,"Geom. desconocida", transform=self.ax.transAxes) + + self.ax.set_title(geom) + self.canvas_3d.draw() + + def calcular_coef_rozamiento(self): + geom = self.geometria_var.get() + try: + p1 = float(self.entry_param1.get()) + except: + self.label_result_b.config(text="Error param1", fg="red") + return + + Cd=1.0 + A=1.0 + if geom=="Prisma cuadrado": + try: + p2=float(self.entry_param2.get()) + except: + self.label_result_b.config(text="Error param2", fg="red") + return + Cd=1.15 + A=p1*p1 + elif geom=="Cilindro": + try: + p2=float(self.entry_param2.get()) + except: + self.label_result_b.config(text="Error param2", fg="red") + return + Cd=0.82 + import math + A=math.pi*(p1**2) + elif geom=="Esfera": + Cd=0.47 + import math + A=math.pi*(p1**2) + + rho=1.225 + b_calc=0.5*rho*Cd*A + + self.label_result_b.config(text=f"Coef (b) ~ {b_calc:.4f}", fg="blue") + self.tab_simulator.set_b_value(b_calc) diff --git a/tab_search.py b/tab_search.py new file mode 100644 index 0000000..20d3b9d --- /dev/null +++ b/tab_search.py @@ -0,0 +1,219 @@ +# 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 diff --git a/tab_simulator.py b/tab_simulator.py new file mode 100644 index 0000000..0002cbd --- /dev/null +++ b/tab_simulator.py @@ -0,0 +1,471 @@ +import tkinter as tk +from tkinter import ttk +import math + +class TabSimulator: + def __init__(self, notebook): + self.frame = tk.Frame(notebook) + + self.m = 0.0 # Masa + self.h0 = 0.0 # Altura inicial (0..2m) + self.trayectoria = [] + self.t_final = 0 + self.proyectil = None + self.vel_text = None + self.current_canvas_width = 1 + self.current_canvas_height = 1 + + # ============ Estructura general ============ + # TOP + self.frame_top = tk.Frame(self.frame) + self.frame_top.pack(side="top", fill="x", padx=5, pady=5) + + # MIDDLE (canvas) + self.frame_middle = tk.Frame(self.frame) + self.frame_middle.pack(side="top", fill="both", expand=True) + + # BOTTOM (slider + energía + log) + self.frame_bottom = tk.Frame(self.frame) + self.frame_bottom.pack(side="bottom", fill="both", expand=False) + + # - Izquierda: slider + energía + self.frame_slider_and_energy = tk.Frame(self.frame_bottom) + self.frame_slider_and_energy.pack(side="left", fill="y", padx=5, pady=5) + + self.frame_slider = tk.Frame(self.frame_slider_and_energy) + self.frame_slider.pack(side="top", fill="x", padx=5, pady=5) + + self.frame_energy = tk.Frame(self.frame_slider_and_energy, bd=2, relief="groove") + self.frame_energy.pack(side="bottom", fill="both", expand=False, padx=5, pady=5) + + # - Derecha: log + self.frame_log = tk.Frame(self.frame_bottom) + self.frame_log.pack(side="right", fill="both", expand=True, padx=5, pady=5) + + # ============ Fila superior de widgets ============ + tk.Label(self.frame_top, text="Parámetro:").grid(row=0, column=0, sticky="w") + self.parametro_var = tk.StringVar() + self.combo_param = ttk.Combobox( + self.frame_top, textvariable=self.parametro_var, + values=["Velocidad (m/s)", "Alcance (m)"], width=15 + ) + self.combo_param.grid(row=0, column=1, padx=5) + self.combo_param.current(0) + + self.entry_v0 = tk.Entry(self.frame_top, width=8) + self.entry_v0.grid(row=0, column=2, padx=5) + + tk.Label(self.frame_top, text="Ángulo (grados):").grid(row=0, column=3, sticky="w") + self.entry_alpha = tk.Entry(self.frame_top, width=8) + self.entry_alpha.grid(row=0, column=4, padx=5) + + tk.Label(self.frame_top, text="Masa (kg):").grid(row=0, column=5, sticky="w") + self.entry_masa = tk.Entry(self.frame_top, width=8) + self.entry_masa.grid(row=0, column=6, padx=5) + + self.check_rozamiento = tk.BooleanVar() + self.check_rozamiento.set(False) + self.chk = tk.Checkbutton( + self.frame_top, text="Incluir rozamiento", + variable=self.check_rozamiento, + command=self.on_toggle_rozamiento + ) + self.chk.grid(row=0, column=7, padx=15) + + tk.Label(self.frame_top, text="Coef. (b):").grid(row=0, column=8, sticky="e") + self.entry_b = tk.Entry(self.frame_top, width=8, state="disabled") + self.entry_b.grid(row=0, column=9, padx=5) + + # Altura inicial (0..2) + tk.Label(self.frame_top, text="Altura (m):").grid(row=0, column=10, sticky="e") + self.entry_h0 = tk.Entry(self.frame_top, width=8) + self.entry_h0.grid(row=0, column=11, padx=5) + self.entry_h0.insert(0, "0.0") # por defecto + + # Botón Calcular + self.button_calcular = tk.Button( + self.frame_top, text="Calcular", + command=self.calcular_trayectoria + ) + self.button_calcular.grid(row=0, column=12, padx=10) + + # Cajitas para pos X e Y + tk.Label(self.frame_top, text="Pos X (m):").grid(row=0, column=13, padx=5) + self.entry_pos_x = tk.Entry(self.frame_top, width=8) + self.entry_pos_x.grid(row=0, column=14, padx=5) + + tk.Label(self.frame_top, text="Pos Y (m):").grid(row=0, column=15, padx=5) + self.entry_pos_y = tk.Entry(self.frame_top, width=8) + self.entry_pos_y.grid(row=0, column=16, padx=5) + + # ============ Canvas ============ + self.canvas = tk.Canvas(self.frame_middle, bg="white") + self.canvas.pack(fill="both", expand=True) + self.canvas.bind("", self.on_resize) + + # ============ Slider tiempo ============ + self.slider_time = tk.Scale( + self.frame_slider, from_=0, to=1, resolution=0.01, + orient=tk.HORIZONTAL, label="Tiempo (s):", + command=self.actualizar_posicion + ) + self.slider_time.pack(fill="x") + + # ============ Cuadro energía ============ + tk.Label(self.frame_energy, text="Energía mecánica", font=("Arial", 10, "bold")).pack() + + self.label_Ec = tk.Label(self.frame_energy, text="Ec: 0.0 J") + self.label_Ec.pack(anchor="w", padx=5) + + self.label_Ep = tk.Label(self.frame_energy, text="Ep: 0.0 J") + self.label_Ep.pack(anchor="w", padx=5) + + self.label_Etot = tk.Label(self.frame_energy, text="E_total: 0.0 J") + self.label_Etot.pack(anchor="w", padx=5) + + self.label_Esobredim = tk.Label(self.frame_energy, text="E_total x1.15: 0.0 J") + self.label_Esobredim.pack(anchor="w", padx=5) + + # Logger + self.text_log = tk.Text(self.frame_log, height=2, state="normal") + self.text_log.pack(fill="both", expand=True) + + def set_log_message(self, mensaje, bg_color="white", fg_color="black"): + self.text_log.config(state="normal", bg=bg_color, fg=fg_color) + self.text_log.delete("1.0", tk.END) + self.text_log.insert(tk.END, mensaje + "\n") + self.text_log.config(state="disabled") + + def set_b_value(self, new_b): + self.entry_b.config(state="normal") + self.entry_b.delete(0, tk.END) + self.entry_b.insert(0, f"{new_b:.4f}") + self.entry_b.config(state="disabled") + + def on_toggle_rozamiento(self): + if self.check_rozamiento.get(): + self.entry_b.config(state="normal") + else: + self.entry_b.config(state="disabled") + + def on_resize(self, event): + self.current_canvas_width = event.width + self.current_canvas_height = event.height + if self.trayectoria: + self.dibujar_trayectoria() + + def calcular_trayectoria(self): + # 1) Lee alpha, masa + try: + alpha_deg = float(self.entry_alpha.get()) + self.m = float(self.entry_masa.get()) + except ValueError: + self.set_log_message("Error: revisa (ángulo, masa).", "red", "white") + return + + # 2) Altura + try: + h0_val = float(self.entry_h0.get()) + except ValueError: + h0_val = 0.0 + if h0_val < 0: h0_val = 0.0 + if h0_val > 2: h0_val = 2.0 + self.h0 = h0_val + + # 3) Validar ángulo + if alpha_deg < 0 or alpha_deg > 90: + self.set_log_message("Introduce un ángulo entre 0 y 90", "red","white") + return + + alpha_rad = math.radians(alpha_deg) + + # 4) Leer param (Vel/Alcance) + modo = self.parametro_var.get() + try: + valor = float(self.entry_v0.get()) + except ValueError: + self.set_log_message("Error en el valor (vel/alcance).", "red","white") + return + + v0 = 0.0 + if modo == "Velocidad (m/s)": + v0 = valor + else: + # Modo alcance + X_final = valor + if not self.check_rozamiento.get(): + v0_min=0.0 + v0_max=1000.0 + for _ in range(100): + guess=0.5*(v0_min+v0_max) + dist_alcanzado = self.simular_dist_sin_rozamiento(guess, alpha_rad, self.h0) + if abs(dist_alcanzado - X_final)<0.1: + v0=guess + break + if dist_alcanzado0", "red","white") + return + + b=0.0 + if self.check_rozamiento.get(): + try: + b = float(self.entry_b.get()) + except: + self.set_log_message("Coef rozamiento no válido", "red","white") + return + + # 5) Integramos la trayectoria + self.canvas.delete("all") + self.trayectoria.clear() + self.proyectil=None + self.vel_text=None + + dt=0.01 + t=0.0 + x=0.0 + y=self.h0 + vx = v0*math.cos(alpha_rad) + vy = v0*math.sin(alpha_rad) + + if not self.check_rozamiento.get(): + # sin rozamiento + while True: + self.trayectoria.append((t,x,y,vx,vy)) + vy_new = vy-9.8*dt + x_new = x+vx*dt + y_new = y+vy*dt + t+=dt + vx= vx + vy= vy_new + x= x_new + y= y_new + if y<=0 and t>0.01: + break + else: + # con rozamiento lineal + while True: + self.trayectoria.append((t,x,y,vx,vy)) + ax=-(b/self.m)*vx + ay=-9.8-(b/self.m)*vy + vx_new = vx+ax*dt + vy_new = vy+ay*dt + x_new = x+vx_new*dt + y_new = y+vy_new*dt + t+=dt + vx= vx_new + vy= vy_new + x= x_new + y= y_new + if y<=0 and t>0.01: + break + + self.t_final = t + self.slider_time.config(from_=0, to=self.t_final) + self.slider_time.set(0) + + self.dibujar_trayectoria() + self.set_log_message(f"Cálculo OK. v0={v0:.2f} m/s", "green","white") + + def simular_dist_sin_rozamiento(self, v0_guess, alpha_rad, h0): + dt=0.01 + x=0.0 + y=h0 + vx=v0_guess*math.cos(alpha_rad) + vy=v0_guess*math.sin(alpha_rad) + t=0.0 + while True: + vy_new= vy-9.8*dt + x_new= x+ vx*dt + y_new= y+ vy*dt + t+=dt + vx= vx + vy= vy_new + x= x_new + y= y_new + if y<=0 and t>0.01: + break + return x + + def simular_dist_con_rozamiento(self, v0_guess, alpha_rad, h0): + dt=0.01 + x=0.0 + y=h0 + vx=v0_guess*math.cos(alpha_rad) + vy=v0_guess*math.sin(alpha_rad) + t=0.0 + try: + b=float(self.entry_b.get()) + except: + b=0.1 + while True: + ax=-(b/self.m)*vx + ay=-9.8-(b/self.m)*vy + vx_new= vx+ax*dt + vy_new= vy+ay*dt + x_new= x+vx_new*dt + y_new= y+vy_new*dt + t+=dt + vx= vx_new + vy= vy_new + x= x_new + y= y_new + if y<=0 and t>0.01: + break + return x + + def dibujar_trayectoria(self): + if not self.trayectoria: + return + + min_x = min(pt[1] for pt in self.trayectoria) + max_x = max(pt[1] for pt in self.trayectoria) + min_y = min(pt[2] for pt in self.trayectoria) + max_y = max(pt[2] for pt in self.trayectoria) + + rx = max_x - min_x + ry = max_y - min_y + if rx<1e-9: rx=1.0 + if ry<1e-9: ry=1.0 + + w = self.current_canvas_width + h = self.current_canvas_height + margen=20 + scale = min((w-2*margen)/rx, (h-2*margen)/ry) + + self.canvas.delete("all") + pts=[] + for (tt,xx,yy,vx_,vy_) in self.trayectoria: + sx = margen + (xx - min_x)*scale + sy = (h - margen) - (yy - min_y)*scale + pts.append((sx, sy)) + + for i in range(len(pts)-1): + x1,y1=pts[i] + x2,y2=pts[i+1] + self.canvas.create_line(x1,y1,x2,y2,fill="blue") + + if pts: + x0,y0 = pts[0] + r=5 + self.proyectil = self.canvas.create_oval(x0-r,y0-r, x0+r,y0+r, fill="red") + self.vel_text = self.canvas.create_text(x0+15,y0, text="v=0.00 m/s", fill="black") + + self.scale=scale + self.margen=margen + self.min_x=min_x + self.min_y=min_y + + self.actualizar_energia(0) + + def actualizar_posicion(self, val): + if not self.trayectoria or not self.proyectil: + return + + t_slider = float(val) + if t_slider<=0: + x_real=self.trayectoria[0][1] + y_real=self.trayectoria[0][2] + vx_real=self.trayectoria[0][3] + vy_real=self.trayectoria[0][4] + elif t_slider>=self.t_final: + x_real=self.trayectoria[-1][1] + y_real=self.trayectoria[-1][2] + vx_real=self.trayectoria[-1][3] + vy_real=self.trayectoria[-1][4] + else: + tiempos=[p[0] for p in self.trayectoria] + idx=0 + while idx=self.t_final: + vx_=self.trayectoria[-1][3] + vy_=self.trayectoria[-1][4] + x_=self.trayectoria[-1][1] + y_=self.trayectoria[-1][2] + else: + tiempos=[p[0] for p in self.trayectoria] + idx=0 + while idx0: + Ep=self.m*9.8*y_ + E_tot=Ec+Ep + E_sobredim=1.15*E_tot + + self.label_Ec.config(text=f"Ec: {Ec:.2f} J") + self.label_Ep.config(text=f"Ep: {Ep:.2f} J") + self.label_Etot.config(text=f"E_total: {E_tot:.2f} J") + self.label_Esobredim.config(text=f"E_total x1.15: {E_sobredim:.2f} J")