source/tab_simulator.py
Pedro Jose Romero Gombau 93c9e7b91b Init
2025-01-16 16:28:45 +01:00

472 lines
16 KiB
Python

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("<Configure>", 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_alcanzado<X_final:
v0_min=guess
else:
v0_max=guess
v0=guess
else:
v0_min=0.0
v0_max=1000.0
for _ in range(100):
guess=0.5*(v0_min+v0_max)
dist_alcanzado = self.simular_dist_con_rozamiento(guess, alpha_rad, self.h0)
if abs(dist_alcanzado - X_final)<0.1:
v0=guess
break
if dist_alcanzado<X_final:
v0_min=guess
else:
v0_max=guess
v0=guess
if v0<=0:
self.set_log_message("No se pudo determinar v0>0", "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<len(self.trayectoria)-1 and tiempos[idx+1]<t_slider:
idx+=1
x_real=self.trayectoria[idx][1]
y_real=self.trayectoria[idx][2]
vx_real=self.trayectoria[idx][3]
vy_real=self.trayectoria[idx][4]
# Pos en canvas
w = self.current_canvas_width
h = self.current_canvas_height
sx = self.margen + (x_real - self.min_x)*self.scale
sy = (h - self.margen) - (y_real - self.min_y)*self.scale
coords_act = self.canvas.coords(self.proyectil)
x_c=(coords_act[0]+coords_act[2])/2
y_c=(coords_act[1]+coords_act[3])/2
dx=sx-x_c
dy=sy-y_c
self.canvas.move(self.proyectil, dx, dy)
vel = math.sqrt(vx_real**2 + vy_real**2)
if self.vel_text:
coords_text=self.canvas.coords(self.vel_text)
xt=coords_text[0]
yt=coords_text[1]
dx_t=sx-xt
dy_t=(sy+15)-yt
self.canvas.move(self.vel_text, dx_t, dy_t)
self.canvas.itemconfig(self.vel_text, text=f"v={vel:.2f} m/s")
# Pos X e Y (en metros, no en pixeles)
self.entry_pos_x.delete(0, tk.END)
self.entry_pos_x.insert(0, f"{x_real:.2f}")
self.entry_pos_y.delete(0, tk.END)
self.entry_pos_y.insert(0, f"{y_real:.2f}")
self.actualizar_energia(t_slider)
def actualizar_energia(self, t_slider):
if not self.trayectoria:
return
if t_slider<=0:
vx_=self.trayectoria[0][3]
vy_=self.trayectoria[0][4]
x_=self.trayectoria[0][1]
y_=self.trayectoria[0][2]
elif t_slider>=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 idx<len(self.trayectoria)-1 and tiempos[idx+1]<t_slider:
idx+=1
vx_=self.trayectoria[idx][3]
vy_=self.trayectoria[idx][4]
x_=self.trayectoria[idx][1]
y_=self.trayectoria[idx][2]
Ec = 0.5*self.m*(vx_**2 + vy_**2)
Ep=0.0
if y_>0:
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")