This commit is contained in:
Pedro Jose Romero Gombau 2025-01-16 16:28:45 +01:00
commit 93c9e7b91b
12 changed files with 1008 additions and 0 deletions

13
Dockerfile Normal file
View File

@ -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"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

71
geometry_viewer.py Normal file
View File

@ -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()

1
launcher.bat Normal file
View File

@ -0,0 +1 @@
docker run -it --rm -e DISPLAY=host.docker.internal:0 launchsim

32
main.py Normal file
View File

@ -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()

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
matplotlib

200
tab_drag.py Normal file
View File

@ -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("<<ComboboxSelected>>", 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)

219
tab_search.py Normal file
View File

@ -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

471
tab_simulator.py Normal file
View File

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