472 lines
16 KiB
Python
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")
|