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")