app_designer.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. import tkinter as tk
  2. from tkinter import ttk, messagebox, simpledialog
  3. import json
  4. import numpy as np
  5. import math
  6. import matplotlib.pyplot as plt
  7. from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
  8. import os
  9. # ==================== CONSTANTES DE MATERIAL ====================
  10. DENS = 7850 # kg/m3
  11. FY = 355 # MPa
  12. GAMMAS = 1.05 # Factor seguridad acero
  13. EYOUNG = 210000 # MPa
  14. NU = 0.3 # Coef Poisson
  15. FYD = FY * 10**6 / GAMMAS # Pa
  16. EYOUNG_PA = EYOUNG * 10**6 # Pa
  17. G = EYOUNG_PA / (2 * (1 + NU)) # Pa
  18. # ==================== FUNCIONES DE CÁLCULO ====================
  19. def generate_ipe_points(H, b, tf, tw):
  20. """Genera los puntos de una sección IPE a partir de parámetros normalizados"""
  21. # H: altura total, b: ancho flange, tf: espesor flange, tw: espesor alma
  22. h_alma = H - 2*tf
  23. x_alma_ini = (b - tw) / 2
  24. x_alma_fin = (b + tw) / 2
  25. # Puntos de la sección (sentido horario)
  26. points = np.array([
  27. [0, 0], # Esquina inferior izquierda flange
  28. [b, 0], # Esquina inferior derecha flange
  29. [b, tf], # Unión alma-flange derecha
  30. [x_alma_fin, tf], # Esquina superior alma derecha
  31. [x_alma_fin, H - tf], # Esquina inferior alma derecha (arriba)
  32. [b, H - tf], # Unión alma-flange arriba
  33. [b, H], # Esquina superior derecha flange
  34. [0, H], # Esquina superior izquierda flange
  35. [0, H - tf], # Unión alma-flange arriba izquierda
  36. [x_alma_ini, H - tf], # Esquina inferior alma izquierda (arriba)
  37. [x_alma_ini, tf], # Esquina superior alma izquierda
  38. [0, tf], # Unión alma-flange izquierda
  39. [0, 0], # Cerrar la sección
  40. ])
  41. return points
  42. def generate_puente_nuevo_points(h, b, tf, tw, ha, ta, tr, theta):
  43. """Genera los puntos de la sección tipo puente nuevo a partir de parámetros normalizados"""
  44. # h: altura exterior, b: ancho total, tf: espesor ala, tw: espesor alma,
  45. hr = (b-tw) / 2 * math.tan(math.radians(theta))
  46. hl = tr / math.cos(math.radians(theta))
  47. hr_ = (b/2 - ta - tw/2)* math.tan(math.radians(theta))
  48. # Puntos de la sección (sentido horario)
  49. points = np.array([
  50. [0, 0],
  51. [b, 0],
  52. [b, tf + ha],
  53. [(b + tw)/2, tf + ha + hr],
  54. [(b + tw)/2, tf + ha + hr - hl],
  55. [b - ta, tf + ha + hr - hl - hr_],
  56. [b - ta, tf],
  57. [(b + tw)/2, tf],
  58. [(b + tw)/2, h - tf],
  59. [b - ta, h - tf],
  60. [b - ta, h - tf - ha - hr + hl + hr_],
  61. [(b + tw)/2, h - tf - ha - hr + hl],
  62. [(b + tw)/2, h - tf - ha - hr],
  63. [b, h - tf - ha],
  64. [b, h],
  65. [0, h],
  66. [0, h - tf - ha],
  67. [(b - tw)/2, h - tf - ha - hr],
  68. [(b - tw)/2, h - tf - ha - hr + hl],
  69. [ta, h - tf - ha - hr + hl + hr_],
  70. [ta, h - tf],
  71. [(b - tw)/2, h - tf],
  72. [(b - tw)/2, tf],
  73. [ta, tf],
  74. [ta, tf + ha + hr - hl - hr_],
  75. [(b - tw)/2, tf + ha + hr - hl],
  76. [(b - tw)/2, tf + ha + hr],
  77. [0, tf + ha],
  78. [0, 0],
  79. ])
  80. return points
  81. def calculate_section_properties(points):
  82. """Calcula todas las propiedades de la sección - CÓDIGO ORIGINAL SIN MODIFICAR"""
  83. npuntos = len(points)
  84. puntos = points
  85. px = puntos[:, 0]
  86. py = puntos[:, 1]
  87. # ANCHO Y CANTO MÁXIMO
  88. bmax = np.amax(px) - np.amin(px)
  89. hmax = np.amax(py) - np.amin(py)
  90. # PERÍMETRO
  91. long_i = np.zeros(npuntos - 1)
  92. for i in range(npuntos - 1):
  93. long_i[i] = ((puntos[i+1, 0] - puntos[i, 0])**2 + (puntos[i+1, 1] - puntos[i, 1])**2)**(1/2)
  94. perimetro = abs(sum(long_i))
  95. # ÁREA
  96. area_i = np.zeros(npuntos - 1)
  97. for i in range(npuntos - 1):
  98. area_i[i] = (puntos[i+1, 0] - puntos[i, 0]) * (puntos[i+1, 1] + puntos[i, 1]) / 2
  99. area = abs(sum(area_i))
  100. # PESO POR METRO
  101. peso = area * DENS
  102. # CENTRO DE GRAVEDAD
  103. cdg_i = np.zeros([npuntos, 2])
  104. for i in range(npuntos - 1):
  105. h1 = puntos[i, 1]
  106. h2 = puntos[i+1, 1]
  107. b = puntos[i+1, 0] - puntos[i, 0]
  108. d = puntos[i, 0]
  109. if h1 + h2 == 0:
  110. cdg_i[i, 1] = 0
  111. else:
  112. cdg_i[i, 1] = 1/3 * (h1*h1 + h1*h2 + h2*h2) / (h1 + h2)
  113. if h1 + h2 == 0:
  114. cdg_i[i, 0] = d + b/2
  115. else:
  116. cdg_i[i, 0] = d + b/3 * (h1 + 2*h2) / (h1 + h2)
  117. # MOMENTO ESTÁTICO
  118. statico_i = np.zeros([npuntos, 2])
  119. for i in range(npuntos - 1):
  120. statico_i[i, 1] = area_i[i] * cdg_i[i, 1]
  121. statico_i[i, 0] = area_i[i] * cdg_i[i, 0]
  122. cdg = sum(statico_i) / sum(area_i)
  123. xg = cdg[0]
  124. yg = cdg[1]
  125. # FIBRAS MÁS ALEJADAS
  126. v1y = np.amax(py) - yg
  127. v2y = np.amin(py) - yg
  128. v1x = np.amax(px) - xg
  129. v2x = np.amin(px) - xg
  130. # MOMENTOS DE INERCIA
  131. inercia_i = np.zeros([npuntos, 3])
  132. for i in range(npuntos - 1):
  133. h1 = puntos[i, 1]
  134. h2 = puntos[i+1, 1]
  135. b = puntos[i+1, 0] - puntos[i, 0]
  136. d = puntos[i, 0]
  137. xgi = cdg_i[i, 0]
  138. ygi = cdg_i[i, 1]
  139. ai = area_i[i]
  140. # Ixg
  141. if h2 >= h1:
  142. ixcuad_G_local = 1/12 * b * (h1**3) + b * h1 * (h1/2 - ygi)**2
  143. ixtriang_G_loc = 1/36 * b * (h2-h1)**3 + 1/2 * b * (h2-h1) * ((2*h1+h2)/3 - ygi)**2
  144. else:
  145. ixcuad_G_local = 1/12 * b * (h2**3) + b * h2 * (h2/2 - ygi)**2
  146. ixtriang_G_loc = 1/36 * b * (h1-h2)**3 + 1/2 * b * (h1-h2) * ((2*h2+h1)/3 - ygi)**2
  147. inercia_i[i, 0] = ixcuad_G_local + ixtriang_G_loc + ai * (yg - ygi)**2
  148. # Iyg
  149. if h2 >= h1:
  150. iycuad = 1/12 * h1 * b**3 + h1 * b * (b/2 + d - xgi)**2
  151. iytrian = 1/36 * (h2-h1) * b**3 + 1/2 * b * (h2-h1) * (2/3*b + d - xgi)**2
  152. else:
  153. iycuad = 1/12 * h2 * b**3 + h2 * b * (b/2 + d - xgi)**2
  154. iytrian = 1/36 * (h1-h2) * b**3 + 1/2 * b * (h1-h2) * (1/3*b + d - xgi)**2
  155. inercia_i[i, 1] = iycuad + iytrian + ai * (xg - xgi)**2
  156. # Pxyg
  157. if h2 >= h1:
  158. pxygcuadrado = b * h1 * (-h1/2 + ygi) * (-d - b/2 + xgi)
  159. pxytriangulo = b*b * (h2-h1)**2 / 72 + b * (h2-h1) / 2 * (-(h2-h1)/3 - h1 + ygi) * (-d - 2/3*b + xgi)
  160. else:
  161. pxygcuadrado = b * h2 * (-h2/2 + ygi) * (-d - b/2 + xgi)
  162. pxytriangulo = -b*b * (h1-h2)**2 / 72 + b * (h1-h2) / 2 * (-(h1-h2)/3 - h2 + ygi) * (-d - 1/3*b + xgi)
  163. inercia_i[i, 2] = pxygcuadrado + pxytriangulo + ai * (-xg + xgi) * (-yg + ygi)
  164. ig = sum(inercia_i)
  165. ixg = abs(ig[0])
  166. iyg = abs(ig[1])
  167. pxyg = ig[2]
  168. if sum(area_i) >= 0:
  169. pxyg = pxyg
  170. else:
  171. pxyg = -pxyg
  172. # RADIOS DE GIRO
  173. rx = (ixg / area)**0.5
  174. ry = (iyg / area)**0.5
  175. # EJES PRINCIPALES DE INERCIA
  176. ic = (ixg + iyg) / 2
  177. ir = (((ixg - iyg)/2)**2 + pxyg**2)**0.5
  178. imax = ic + ir
  179. imin = ic - ir
  180. rmax = (imax / area)**0.5
  181. rmin = (imin / area)**0.5
  182. # ORIENTACIÓN DE EJES PRINCIPALES
  183. rest = ixg - iyg
  184. if abs(rest) < 10**-12:
  185. tetha = 45
  186. else:
  187. tetha = 0.5 * math.atan((pxyg*2) / (ixg - iyg))
  188. tetha = abs(tetha) * 180 / math.pi
  189. if pxyg > 0:
  190. if ixg > iyg:
  191. tetha = -tetha
  192. else:
  193. tetha = tetha
  194. else:
  195. if ixg > iyg:
  196. tetha = tetha
  197. else:
  198. tetha = -tetha
  199. # MÓDULO RESISTENTE ELÁSTICO
  200. wel1x = abs(ixg / v1y)
  201. wel2x = abs(ixg / v2y)
  202. wel1y = abs(iyg / v1x)
  203. wel2y = abs(iyg / v2x)
  204. # AXIL Y MOMENTO ELÁSTICO
  205. nel = area * FYD
  206. melx = min(wel1x, wel2x) * FYD
  207. mely = min(wel1y, wel2y) * FYD
  208. # CONVERSIÓN A UNIDADES PRÁCTICAS (cm, cm2, cm3, cm4, kN)
  209. pot = 2
  210. bmax_cm = bmax * 10**pot
  211. hmax_cm = hmax * 10**pot
  212. perimetro_cm = perimetro * 10**pot
  213. xg_cm = xg * 10**pot
  214. yg_cm = yg * 10**pot
  215. v1y_cm = v1y * 10**pot
  216. v2y_cm = v2y * 10**pot
  217. v1x_cm = v1x * 10**pot
  218. v2x_cm = v2x * 10**pot
  219. rx_cm = rx * 10**pot
  220. ry_cm = ry * 10**pot
  221. rmax_cm = rmax * 10**pot
  222. rmin_cm = rmin * 10**pot
  223. area_cm2 = area * 10**(pot*2)
  224. wel1x_cm3 = wel1x * 10**(pot*3)
  225. wel2x_cm3 = wel2x * 10**(pot*3)
  226. wel1y_cm3 = wel1y * 10**(pot*3)
  227. wel2y_cm3 = wel2y * 10**(pot*3)
  228. ixg_cm4 = ixg * 10**(pot*4)
  229. iyg_cm4 = iyg * 10**(pot*4)
  230. pxyg_cm4 = pxyg * 10**(pot*4)
  231. imax_cm4 = imax * 10**(pot*4)
  232. imin_cm4 = imin * 10**(pot*4)
  233. nel_kn = nel / 1000
  234. melx_kn = melx / 1000
  235. mely_kn = mely / 1000
  236. return {
  237. 'area': area_cm2,
  238. 'perimetro': perimetro_cm,
  239. 'xg': xg_cm,
  240. 'yg': yg_cm,
  241. 'v1y': v1y_cm,
  242. 'v2y': v2y_cm,
  243. 'v1x': v1x_cm,
  244. 'v2x': v2x_cm,
  245. 'ixg': ixg_cm4,
  246. 'iyg': iyg_cm4,
  247. 'pxyg': pxyg_cm4,
  248. 'imax': imax_cm4,
  249. 'imin': imin_cm4,
  250. 'rx': rx_cm,
  251. 'ry': ry_cm,
  252. 'rmax': rmax_cm,
  253. 'rmin': rmin_cm,
  254. 'wel1x': wel1x_cm3,
  255. 'wel2x': wel2x_cm3,
  256. 'wel1y': wel1y_cm3,
  257. 'wel2y': wel2y_cm3,
  258. 'tetha': tetha,
  259. 'peso': peso,
  260. 'nel': nel_kn,
  261. 'melx': melx_kn,
  262. 'mely': mely_kn,
  263. 'xg_orig': xg,
  264. 'yg_orig': yg,
  265. 'bmax': bmax,
  266. 'hmax': hmax,
  267. 'points': points
  268. }
  269. # ==================== APLICACIÓN TKINTER ====================
  270. class SectionDesignerApp(tk.Tk):
  271. def __init__(self):
  272. super().__init__()
  273. self.title("Diseñador de Secciones de Acero")
  274. self.geometry("1200x800")
  275. self.json_path = r"c:\Users\Daniel.p\Documents\Automatizaciones\Propiedades seccion\secciones_config.json"
  276. self.load_json()
  277. self.current_section_name = None
  278. self.current_points = None
  279. self.current_properties = None
  280. self.create_widgets()
  281. def load_json(self):
  282. """Carga el archivo JSON de configuración"""
  283. if os.path.exists(self.json_path):
  284. with open(self.json_path, 'r') as f:
  285. self.secciones_data = json.load(f)
  286. else:
  287. self.secciones_data = {"secciones": []}
  288. def save_json(self):
  289. """Guarda los cambios al JSON"""
  290. with open(self.json_path, 'w') as f:
  291. json.dump(self.secciones_data, f, indent=2)
  292. def create_widgets(self):
  293. """Crea la interfaz gráfica"""
  294. # ========== PANEL IZQUIERDO (CONTROLES) ==========
  295. left_panel = ttk.Frame(self, width=250)
  296. left_panel.pack(side=tk.LEFT, fill=tk.BOTH, padx=10, pady=10)
  297. # Selector de sección
  298. ttk.Label(left_panel, text="Sección:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  299. self.combo_seccion = ttk.Combobox(
  300. left_panel,
  301. values=[s['nombre'] for s in self.secciones_data['secciones']],
  302. state='readonly',
  303. width=20
  304. )
  305. self.combo_seccion.pack(anchor=tk.W)
  306. self.combo_seccion.bind('<<ComboboxSelected>>', self.on_section_changed)
  307. # Separador
  308. ttk.Separator(left_panel, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
  309. # Selector de tipo de sección
  310. ttk.Label(left_panel, text="Tipo de sección:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  311. self.combo_tipo = ttk.Combobox(
  312. left_panel,
  313. values=["IPE", "Personalizada", "Puente nuevo"],
  314. state='readonly',
  315. width=20
  316. )
  317. self.combo_tipo.pack(anchor=tk.W, pady=(0, 10))
  318. self.combo_tipo.set("IPE")
  319. self.combo_tipo.bind('<<ComboboxSelected>>', self.on_section_type_changed)
  320. # Frame para parámetros IPE
  321. self.frame_ipe = ttk.Frame(left_panel)
  322. self.frame_ipe.pack(fill=tk.BOTH, expand=False, pady=(0, 10))
  323. ttk.Label(self.frame_ipe, text="Parámetros IPE:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  324. # H (Altura)
  325. ttk.Label(self.frame_ipe, text="H (altura, m):").pack(anchor=tk.W)
  326. self.entry_H = ttk.Spinbox(self.frame_ipe, from_=0.0, to=10000.0, increment=0.01, width=15, command=self.on_parameter_change_ipe)
  327. self.entry_H.pack(anchor=tk.W, fill=tk.X)
  328. self.entry_H.set("0.300")
  329. self.entry_H.bind('<FocusOut>', self.on_parameter_change_ipe)
  330. self.entry_H.bind('<Return>', self.on_parameter_change_ipe)
  331. # b (Ancho)
  332. ttk.Label(self.frame_ipe, text="b (ancho flange, m):").pack(anchor=tk.W, pady=(10, 0))
  333. self.entry_b = ttk.Spinbox(self.frame_ipe, from_=0.0, to=10000.0, increment=0.01, width=15, command=self.on_parameter_change_ipe)
  334. self.entry_b.pack(anchor=tk.W, fill=tk.X)
  335. self.entry_b.set("0.150")
  336. self.entry_b.bind('<FocusOut>', self.on_parameter_change_ipe)
  337. self.entry_b.bind('<Return>', self.on_parameter_change_ipe)
  338. # tf (Espesor flange)
  339. ttk.Label(self.frame_ipe, text="tf (espesor flange, m):").pack(anchor=tk.W, pady=(10, 0))
  340. self.entry_tf = ttk.Spinbox(self.frame_ipe, from_=0.0, to=10000.0, increment=0.005, width=15, command=self.on_parameter_change_ipe)
  341. self.entry_tf.pack(anchor=tk.W, fill=tk.X)
  342. self.entry_tf.set("0.0107")
  343. self.entry_tf.bind('<FocusOut>', self.on_parameter_change_ipe)
  344. self.entry_tf.bind('<Return>', self.on_parameter_change_ipe)
  345. # tw (Espesor alma)
  346. ttk.Label(self.frame_ipe, text="tw (espesor alma, m):").pack(anchor=tk.W, pady=(10, 0))
  347. self.entry_tw = ttk.Spinbox(self.frame_ipe, from_=0.0, to=10000.0, increment=0.005, width=15, command=self.on_parameter_change_ipe)
  348. self.entry_tw.pack(anchor=tk.W, fill=tk.X)
  349. self.entry_tw.set("0.0063")
  350. self.entry_tw.bind('<FocusOut>', self.on_parameter_change_ipe)
  351. self.entry_tw.bind('<Return>', self.on_parameter_change_ipe)
  352. # Frame para sección personalizada
  353. self.frame_personalizada = ttk.Frame(left_panel)
  354. ttk.Label(self.frame_personalizada, text="Puntos (JSON):", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  355. ttk.Label(self.frame_personalizada, text="Formato: [[x1,y1], [x2,y2], ...]", font=("Arial", 8)).pack(anchor=tk.W)
  356. self.text_puntos = tk.Text(self.frame_personalizada, height=8, width=30, font=("Courier", 8))
  357. self.text_puntos.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
  358. ttk.Button(self.frame_personalizada, text="Cargar puntos", command=self.load_custom_points).pack(fill=tk.X)
  359. # Frame para puente nuevo
  360. self.frame_puente_nuevo = ttk.Frame(left_panel)
  361. self.frame_puente_nuevo.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  362. ttk.Label(self.frame_puente_nuevo, text="Parámetros puente nuevo:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  363. # H (Altura exterior)
  364. ttk.Label(self.frame_puente_nuevo, text = "h (altura exterior, m):").pack(anchor=tk.W, pady=(10, 0))
  365. self.entry_h_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.01, width=15, command=self.on_parameter_change_puente_nuevo)
  366. self.entry_h_puente.pack(anchor=tk.W, fill=tk.X)
  367. self.entry_h_puente.set("0.300")
  368. self.entry_h_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  369. self.entry_h_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  370. # b (Ancho)
  371. ttk.Label(self.frame_puente_nuevo, text="b (ancho, m):").pack(anchor=tk.W, pady=(10, 0))
  372. self.entry_b_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.005, width=15, command=self.on_parameter_change_puente_nuevo)
  373. self.entry_b_puente.pack(anchor=tk.W, fill=tk.X)
  374. self.entry_b_puente.set("0.150")
  375. self.entry_b_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  376. self.entry_b_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  377. # tf (espesor ala)
  378. ttk.Label(self.frame_puente_nuevo, text="tf (espesor ala, m):").pack(anchor=tk.W, pady=(10, 0))
  379. self.entry_tf_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo)
  380. self.entry_tf_puente.pack(anchor=tk.W, fill=tk.X)
  381. self.entry_tf_puente.set("0.0107")
  382. self.entry_tf_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  383. self.entry_tf_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  384. # tw (espesor alma)
  385. ttk.Label(self.frame_puente_nuevo, text="tw (espesor alma, m):").pack(anchor=tk.W, pady=(10, 0))
  386. self.entry_tw_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo)
  387. self.entry_tw_puente.pack(anchor=tk.W, fill=tk.X)
  388. self.entry_tw_puente.set("0.0063")
  389. self.entry_tw_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  390. self.entry_tw_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  391. # ha (altura refuerzo ala)
  392. ttk.Label(self.frame_puente_nuevo, text="ha (altura refuerzo ala, m):").pack(anchor=tk.W, pady=(10, 0))
  393. self.entry_ha_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo)
  394. self.entry_ha_puente.pack(anchor=tk.W, fill=tk.X)
  395. self.entry_ha_puente.set("0.02")
  396. self.entry_ha_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  397. self.entry_ha_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  398. # ta (espesor refuerzo ala)
  399. ttk.Label(self.frame_puente_nuevo, text="ta (espesor refuerzo ala, m):").pack(anchor=tk.W, pady=(10, 0))
  400. self.entry_ta_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo)
  401. self.entry_ta_puente.pack(anchor=tk.W, fill=tk.X)
  402. self.entry_ta_puente.set("0.01")
  403. self.entry_ta_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  404. self.entry_ta_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  405. # tr (espesor refuerzo alma)
  406. ttk.Label(self.frame_puente_nuevo, text="tr (espesor refuerzo alma, m):").pack(anchor=tk.W, pady=(10, 0))
  407. self.entry_tr_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=10000.0, increment=0.0005, width=15, command=self.on_parameter_change_puente_nuevo)
  408. self.entry_tr_puente.pack(anchor=tk.W, fill=tk.X)
  409. self.entry_tr_puente.set("0.01")
  410. self.entry_tr_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  411. self.entry_tr_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  412. # theta (ángulo refuerzo)
  413. ttk.Label(self.frame_puente_nuevo, text="theta (ángulo refuerzo, grados):").pack(anchor=tk.W, pady=(10, 0))
  414. self.entry_theta_puente = ttk.Spinbox(self.frame_puente_nuevo, from_=0.0, to=360.0, increment=1.0, width=15, command=self.on_parameter_change_puente_nuevo)
  415. self.entry_theta_puente.pack(anchor=tk.W, fill=tk.X)
  416. self.entry_theta_puente.set("45")
  417. self.entry_theta_puente.bind('<FocusOut>', self.on_parameter_change_puente_nuevo)
  418. self.entry_theta_puente.bind('<Return>', self.on_parameter_change_puente_nuevo)
  419. # Separador
  420. ttk.Separator(left_panel, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
  421. # Panel de resultados
  422. ttk.Label(left_panel, text="Propiedades:", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=(10, 5))
  423. self.text_results = tk.Text(left_panel, height=20, width=30, state=tk.DISABLED, font=("Courier", 8))
  424. self.text_results.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  425. # Botón guardar
  426. ttk.Button(left_panel, text="Guardar nueva sección", command=self.save_new_section).pack(fill=tk.X)
  427. # ========== PANEL DERECHO (GRÁFICO) ==========
  428. right_panel = ttk.Frame(self)
  429. right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)
  430. self.figure = plt.Figure(figsize=(6, 8), dpi=100)
  431. self.ax = self.figure.add_subplot(111)
  432. self.canvas = FigureCanvasTkAgg(self.figure, master=right_panel)
  433. self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
  434. # Cargar la primera sección después de que todos los widgets estén creados
  435. if self.secciones_data['secciones']:
  436. self.combo_seccion.current(0)
  437. self.on_section_changed(None)
  438. self.frame_puente_nuevo.pack_forget()
  439. def on_section_type_changed(self, event):
  440. """Cambia entre IPE y Personalizada"""
  441. section_type = self.combo_tipo.get()
  442. if section_type == "IPE":
  443. self.frame_personalizada.pack_forget()
  444. self.frame_puente_nuevo.pack_forget()
  445. self.frame_ipe.pack(fill=tk.BOTH, expand=False, pady=(0, 10))
  446. self.on_parameter_change_ipe(None)
  447. elif section_type == "Puente nuevo":
  448. self.frame_ipe.pack_forget()
  449. self.frame_personalizada.pack_forget()
  450. self.frame_puente_nuevo.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  451. self.on_parameter_change_puente_nuevo(None)
  452. # Aquí podrías cargar valores por defecto o actuales para el puente nuevo
  453. else: # Personalizada
  454. self.frame_ipe.pack_forget()
  455. self.frame_puente_nuevo.pack_forget()
  456. self.frame_personalizada.pack(fill=tk.BOTH, expand=True)
  457. # Mostrar los puntos actuales en el texto
  458. if self.current_points is not None:
  459. import json
  460. puntos_json = json.dumps(self.current_points.tolist(), indent=2)
  461. self.text_puntos.delete(1.0, tk.END)
  462. self.text_puntos.insert(1.0, puntos_json)
  463. def load_custom_points(self):
  464. """Carga puntos personalizados desde el texto"""
  465. try:
  466. import json
  467. text = self.text_puntos.get(1.0, tk.END).strip()
  468. puntos = json.loads(text)
  469. self.current_points = np.array(puntos)
  470. self.current_properties = calculate_section_properties(self.current_points)
  471. self.update_plot()
  472. self.update_results()
  473. messagebox.showinfo("Éxito", "Puntos cargados correctamente")
  474. except json.JSONDecodeError:
  475. messagebox.showerror("Error", "Formato JSON inválido")
  476. except Exception as e:
  477. messagebox.showerror("Error", f"Error al cargar puntos: {str(e)}")
  478. def on_section_changed(self, event):
  479. """Se ejecuta cuando cambia la sección seleccionada"""
  480. idx = self.combo_seccion.current()
  481. if idx >= 0:
  482. seccion = self.secciones_data['secciones'][idx]
  483. self.current_section_name = seccion['nombre']
  484. section_type = seccion.get('tipo', 'otro')
  485. # Actualizar el combo de tipo
  486. if section_type == 'ipe':
  487. self.combo_tipo.set("IPE")
  488. elif section_type == 'puente_nuevo':
  489. self.combo_tipo.set("Puente nuevo")
  490. else:
  491. self.combo_tipo.set("Personalizada")
  492. # Si tiene puntos directos, cargarlos
  493. if 'puntos' in seccion:
  494. self.current_points = np.array(seccion['puntos'])
  495. if section_type == 'ipe':
  496. self.current_properties = calculate_section_properties(self.current_points)
  497. if section_type == 'puente_nuevo':
  498. self.current_properties = calculate_section_properties(self.current_points)
  499. self.update_plot()
  500. self.update_results()
  501. # Si además es IPE, establecer los valores
  502. if section_type == 'ipe' and 'parametros' in seccion:
  503. params = seccion['parametros']
  504. self.entry_H.delete(0, tk.END)
  505. self.entry_H.insert(0, f"{params['H']:.6f}")
  506. self.entry_b.delete(0, tk.END)
  507. self.entry_b.insert(0, f"{params['b']:.6f}")
  508. self.entry_tf.delete(0, tk.END)
  509. self.entry_tf.insert(0, f"{params['tf']:.6f}")
  510. self.entry_tw.delete(0, tk.END)
  511. self.entry_tw.insert(0, f"{params['tw']:.6f}")
  512. # Mostrar frame IPE
  513. self.frame_personalizada.pack_forget()
  514. self.frame_ipe.pack(fill=tk.BOTH, expand=False, pady=(0, 10))
  515. elif section_type == 'puente_nuevo' and 'parametros' in seccion:
  516. params = seccion['parametros']
  517. self.entry_h_puente.delete(0, tk.END)
  518. self.entry_h_puente.insert(0, f"{params['h']:.6f}")
  519. self.entry_b_puente.delete(0, tk.END)
  520. self.entry_b_puente.insert(0, f"{params['b']:.6f}")
  521. self.entry_tf_puente.delete(0, tk.END)
  522. self.entry_tf_puente.insert(0, f"{params['tf']:.6f}")
  523. self.entry_tw_puente.delete(0, tk.END)
  524. self.entry_tw_puente.insert(0, f"{params['tw']:.6f}")
  525. self.entry_ha_puente.delete(0, tk.END)
  526. self.entry_ha_puente.insert(0, f"{params['ha']:.6f}")
  527. self.entry_ta_puente.delete(0, tk.END)
  528. self.entry_ta_puente.insert(0, f"{params['ta']:.6f}")
  529. self.entry_tr_puente.delete(0, tk.END)
  530. self.entry_tr_puente.insert(0, f"{params['tr']:.6f}")
  531. self.entry_theta_puente.delete(0, tk.END)
  532. self.entry_theta_puente.insert(0, f"{params['theta']:.6f}")
  533. # Mostrar frame puente nuevo
  534. self.frame_ipe.pack_forget()
  535. self.frame_personalizada.pack_forget()
  536. self.frame_puente_nuevo.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
  537. else:
  538. # Mostrar frame personalizado
  539. self.frame_ipe.pack_forget()
  540. self.frame_puente_nuevo.pack_forget()
  541. self.frame_personalizada.pack(fill=tk.BOTH, expand=True)
  542. import json
  543. puntos_json = json.dumps(self.current_points.tolist(), indent=2)
  544. self.text_puntos.delete(1.0, tk.END)
  545. self.text_puntos.insert(1.0, puntos_json)
  546. # Si solo tiene parámetros IPE, generar puntos
  547. elif section_type == 'ipe' and 'parametros' in seccion:
  548. params = seccion['parametros']
  549. self.combo_tipo.set("IPE")
  550. self.entry_H.delete(0, tk.END)
  551. self.entry_H.insert(0, f"{params['H']:.6f}")
  552. self.entry_b.delete(0, tk.END)
  553. self.entry_b.insert(0, f"{params['b']:.6f}")
  554. self.entry_tf.delete(0, tk.END)
  555. self.entry_tf.insert(0, f"{params['tf']:.6f}")
  556. self.entry_tw.delete(0, tk.END)
  557. self.entry_tw.insert(0, f"{params['tw']:.6f}")
  558. self.on_parameter_change_ipe(None)
  559. elif section_type == 'puente_nuevo' and 'parametros' in seccion:
  560. params = seccion['parametros']
  561. self.combo_tipo.set("Puente nuevo")
  562. self.entry_h_puente.delete(0, tk.END)
  563. self.entry_h_puente.insert(0, f"{params['h']:.6f}")
  564. self.entry_b_puente.delete(0, tk.END)
  565. self.entry_b_puente.insert(0, f"{params['b']:.6f}")
  566. self.entry_tf_puente.delete(0, tk.END)
  567. self.entry_tf_puente.insert(0, f"{params['tf']:.6f}")
  568. self.entry_tw_puente.delete(0, tk.END)
  569. self.entry_tw_puente.insert(0, f"{params['tw']:.6f}")
  570. self.entry_ha_puente.delete(0, tk.END)
  571. self.entry_ha_puente.insert(0, f"{params['ha']:.6f}")
  572. self.entry_ta_puente.delete(0, tk.END)
  573. self.entry_ta_puente.insert(0, f"{params['ta']:.6f}")
  574. self.entry_tr_puente.delete(0, tk.END)
  575. self.entry_tr_puente.insert(0, f"{params['tr']:.6f}")
  576. self.entry_theta_puente.delete(0, tk.END)
  577. self.entry_theta_puente.insert(0, f"{params['theta']:.6f}")
  578. self.on_parameter_change_puente_nuevo(None)
  579. def on_parameter_change_ipe(self, event=None):
  580. """Se ejecuta cuando cambien los valores de entrada"""
  581. try:
  582. H = float(self.entry_H.get())
  583. b = float(self.entry_b.get())
  584. tf = float(self.entry_tf.get())
  585. tw = float(self.entry_tw.get())
  586. # Validar valores básicos
  587. if H <= 0 or b <= 0 or tf <= 0 or tw <= 0:
  588. return
  589. if tf > H/2 or tw > b:
  590. tf = min(tf, H/2)
  591. tw = min(tw, b)
  592. self.entry_tf.set(f"{tf:.6f}")
  593. self.entry_tw.set(f"{tw:.6f}")
  594. # Generar puntos y calcular
  595. self.current_points = generate_ipe_points(H, b, tf, tw)
  596. self.current_properties = calculate_section_properties(self.current_points)
  597. # Actualizar visualización
  598. self.update_plot()
  599. self.update_results()
  600. except ValueError:
  601. # Ignorar si los valores no son números válidos
  602. pass
  603. def on_parameter_change_puente_nuevo(self, event=None):
  604. """Se ejecuta cuando cambien los valores de entrada"""
  605. try:
  606. h = float(self.entry_h_puente.get())
  607. b = float(self.entry_b_puente.get())
  608. tf = float(self.entry_tf_puente.get())
  609. tw = float(self.entry_tw_puente.get())
  610. ha = float(self.entry_ha_puente.get())
  611. ta = float(self.entry_ta_puente.get())
  612. tr = float(self.entry_tr_puente.get())
  613. theta = float(self.entry_theta_puente.get())
  614. # Validar valores básicos
  615. if h <= 0 or b <= 0 or tf <= 0 or tw <= 0 or ha <= 0 or ta <= 0 or tr <= 0:
  616. return
  617. # Generar puntos y calcular
  618. self.current_points = generate_puente_nuevo_points(h, b, tf, tw, ha, ta, tr, theta)
  619. self.current_properties = calculate_section_properties(self.current_points)
  620. # Actualizar visualización
  621. self.update_plot()
  622. self.update_results()
  623. except ValueError:
  624. # Ignorar si los valores no son números válidos
  625. pass
  626. def update_plot(self):
  627. """Actualiza el gráfico de la sección"""
  628. self.ax.clear()
  629. points = self.current_points
  630. props = self.current_properties
  631. px = points[:, 0]
  632. py = points[:, 1]
  633. xg = props['xg_orig']
  634. yg = props['yg_orig']
  635. bmax = props['bmax']
  636. hmax = props['hmax']
  637. tetha = props['tetha']
  638. # Dibujar sección
  639. self.ax.plot(px, py, 'b-', linewidth=2)
  640. self.ax.fill(px, py, facecolor="lightblue", alpha=0.5)
  641. # Centro de gravedad
  642. self.ax.plot(xg, yg, 'ro', markersize=8, label='CDG')
  643. # Ejes principales
  644. tt = min(bmax, hmax)
  645. self.ax.arrow(xg, yg, tt/3, 0, head_width=tt/30, head_length=tt/30, fc='green', ec='green')
  646. self.ax.arrow(xg, yg, 0, tt/3, head_width=tt/30, head_length=tt/30, fc='green', ec='green')
  647. tet = tetha * (math.pi) / 180
  648. self.ax.arrow(xg, yg, tt/3*math.cos(tet), tt/3*math.sin(tet), head_width=tt/30, head_length=tt/30, fc='red', ec='red')
  649. self.ax.arrow(xg, yg, -tt/3*math.sin(tet), tt/3*math.cos(tet), head_width=tt/30, head_length=tt/30, fc='red', ec='red')
  650. self.ax.set_aspect('equal')
  651. self.ax.grid(True, alpha=0.3)
  652. self.ax.set_xlabel('X (m)', fontweight='bold')
  653. self.ax.set_ylabel('Y (m)', fontweight='bold')
  654. self.ax.set_title(self.current_section_name or 'Sección IPE', fontweight='bold')
  655. self.ax.legend()
  656. self.figure.tight_layout()
  657. self.canvas.draw()
  658. def update_results(self):
  659. """Actualiza el panel de resultados"""
  660. props = self.current_properties
  661. results_text = f"""PROPIEDADES CALCULADAS
  662. GEOMÉTRICAS:
  663. Área: {props['area']:.2f} cm²
  664. Perímetro: {props['perimetro']:.2f} cm
  665. b_max: {props['bmax']*100:.2f} cm
  666. h_max: {props['hmax']*100:.2f} cm
  667. CDG (ref. origen):
  668. X: {props['xg']:.2f} cm
  669. Y: {props['yg']:.2f} cm
  670. INERCIA (eje Xg-Yg):
  671. Ixg: {props['ixg']:.2f} cm⁴
  672. Iyg: {props['iyg']:.2f} cm⁴
  673. Pxyg: {props['pxyg']:.2f} cm⁴
  674. rx: {props['rx']:.2f} cm
  675. ry: {props['ry']:.2f} cm
  676. INERCIA (ejes principales):
  677. Imax: {props['imax']:.2f} cm⁴
  678. Imin: {props['imin']:.2f} cm⁴
  679. θ: {props['tetha']:.2f}°
  680. rmax: {props['rmax']:.2f} cm
  681. rmin: {props['rmin']:.2f} cm
  682. MÓDULO RESISTENTE:
  683. Wel1x: {props['wel1x']:.2f} cm³
  684. Wel2x: {props['wel2x']:.2f} cm³
  685. Wel1y: {props['wel1y']:.2f} cm³
  686. Wel2y: {props['wel2y']:.2f} cm³
  687. MECÁNICAS:
  688. Peso: {props['peso']:.2f} kg/m
  689. Nel: {props['nel']:.2f} kN
  690. Melx: {props['melx']:.2f} kN·m
  691. Mely: {props['mely']:.2f} kN·m
  692. """
  693. self.text_results.config(state=tk.NORMAL)
  694. self.text_results.delete(1.0, tk.END)
  695. self.text_results.insert(1.0, results_text)
  696. self.text_results.config(state=tk.DISABLED)
  697. def save_new_section(self):
  698. """Guarda la sección actual en el JSON"""
  699. if not self.current_section_name or self.current_points is None:
  700. messagebox.showerror("Error", "Selecciona una sección primero")
  701. return
  702. # Preguntar nombre de la nueva sección
  703. new_name = tk.simpledialog.askstring("Guardar sección", "Nombre de la nueva sección:")
  704. if not new_name:
  705. return
  706. # Verificar si ya existe
  707. exists = any(s['nombre'] == new_name for s in self.secciones_data['secciones'])
  708. if exists:
  709. should_overwrite = messagebox.askyesno("Ya existe", f"La sección '{new_name}' ya existe. ¿Sobreescribir?")
  710. if not should_overwrite:
  711. return
  712. # Detectar tipo de sección
  713. section_type = self.combo_tipo.get().lower()
  714. if section_type == "ipe":
  715. section_type = "ipe"
  716. elif section_type == "puente nuevo":
  717. section_type = "puente_nuevo"
  718. else:
  719. section_type = "otro"
  720. # Crear nueva sección con puntos
  721. new_section = {
  722. 'nombre': new_name,
  723. 'tipo': section_type,
  724. 'puntos': self.current_points.tolist() # Guardar los puntos actuales
  725. }
  726. # Si es IPE, guardar también los parámetros
  727. if section_type == "ipe":
  728. try:
  729. H = float(self.entry_H.get())
  730. b = float(self.entry_b.get())
  731. tf = float(self.entry_tf.get())
  732. tw = float(self.entry_tw.get())
  733. if H > 0 and b > 0 and tf > 0 and tw > 0: # Validar que sean mayores a 0
  734. new_section['parametros'] = {
  735. 'H': H,
  736. 'b': b,
  737. 'tf': tf,
  738. 'tw': tw
  739. }
  740. except:
  741. pass # Si no se pueden leer, solo guardamos puntos
  742. elif section_type == "puente_nuevo":
  743. try:
  744. h = float(self.entry_h_puente.get())
  745. b = float(self.entry_b_puente.get())
  746. tf = float(self.entry_tf_puente.get())
  747. tw = float(self.entry_tw_puente.get())
  748. ha = float(self.entry_ha_puente.get())
  749. ta = float(self.entry_ta_puente.get())
  750. tr = float(self.entry_tr_puente.get())
  751. theta = float(self.entry_theta_puente.get())
  752. if all(v > 0 for v in [h, b, tf, tw, ha, ta, tr]) and 0 <= theta <= 360:
  753. new_section['parametros'] = {
  754. 'h': h,
  755. 'b': b,
  756. 'tf': tf,
  757. 'tw': tw,
  758. 'ha': ha,
  759. 'ta': ta,
  760. 'tr': tr,
  761. 'theta': theta
  762. }
  763. except:
  764. pass # Si no se pueden leer, solo guardamos puntos
  765. # Eliminar si existe
  766. self.secciones_data['secciones'] = [s for s in self.secciones_data['secciones'] if s['nombre'] != new_name]
  767. self.secciones_data['secciones'].append(new_section)
  768. self.save_json()
  769. # Actualizar combo
  770. self.combo_seccion['values'] = [s['nombre'] for s in self.secciones_data['secciones']]
  771. self.combo_seccion.set(new_name)
  772. messagebox.showinfo("Éxito", f"Sección '{new_name}' guardada correctamente")
  773. if __name__ == "__main__":
  774. app = SectionDesignerApp()
  775. app.mainloop()