Compare commits
No commits in common. "9ba73a9db63317bb8df043075d5b173628bed757" and "488f78b409c00f287f8f2e5179484c18763190af" have entirely different histories.
9ba73a9db6
...
488f78b409
|
@ -1,18 +0,0 @@
|
|||
|
||||
ex = (t * 8) / w
|
||||
|
||||
|
||||
var1 = 2
|
||||
var2 = 4
|
||||
vatt1 = 4
|
||||
|
||||
IP4
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
## Overview
|
||||
|
||||
The MAV Calculator is a hybrid Computer Algebra System (CAS) built around a pure algebraic engine that treats all assignments as equations. The application follows a clean pipeline from user input to result display, with sophisticated tokenization and symbolic computation capabilities. The GUI design must be minimalist and dark themed.
|
||||
The MAV Calculator is a hybrid Computer Algebra System (CAS) built around a pure algebraic engine that treats all assignments as equations. The application follows a clean pipeline from user input to result display, with sophisticated tokenization and symbolic computation capabilities.
|
||||
|
||||
### The application has this main components:
|
||||
|
||||
|
@ -21,7 +21,6 @@ The MAV Calculator is a hybrid Computer Algebra System (CAS) built around a pure
|
|||
* Cycle: every time the user change the input panel start a cycle that ends when all lines are evaluated and produced an output
|
||||
* Input panel: tk text area where the user can write or modify text.
|
||||
* Output panel: read only tk area text correlated 1:1 to every line on the input panel . Every input line must correspond to only 1 line on the output. The output lines can have colors and binds to click for opening the plots or to show matrix or lists.
|
||||
* Mathjax panel: read only panel atached to the output panel on the right with the comments, equations and asignments. The formulas must be rendered using mathjax. This pannel must be collapsable.
|
||||
* Persistence: The app maintain the state of all configurated setup and dimension of the window and all the text on the input panel.
|
||||
|
||||
## Application Architecture
|
||||
|
|
BIN
MaVCalcv2.lnk
BIN
MaVCalcv2.lnk
Binary file not shown.
|
@ -0,0 +1,127 @@
|
|||
# Sistema de Renderizado LaTeX Simplificado
|
||||
|
||||
## Problema Original
|
||||
El panel LaTeX no se renderizaba correctamente porque faltaba el motor de renderizado matemático (MathJax) y las fuentes matemáticas apropiadas.
|
||||
|
||||
## Solución Implementada
|
||||
|
||||
### 🌐 **Sistema Único: tkinterweb + MathJax 3**
|
||||
- **Dependencia requerida**: tkinterweb
|
||||
- **Motor de renderizado**: MathJax 3 optimizado
|
||||
- **Fuentes matemáticas**: STIX Two Math, Computer Modern Serif
|
||||
- **Configuración automática** de delimitadores LaTeX (`$$...$$`, `$...$`)
|
||||
- **Re-renderizado dinámico** cuando se agregan nuevas ecuaciones
|
||||
|
||||
```javascript
|
||||
// MathJax configurado automáticamente
|
||||
window.MathJax = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$']],
|
||||
displayMath: [['$$', '$$']],
|
||||
processEscapes: true,
|
||||
processEnvironments: true,
|
||||
packages: {'[+]': ['ams', 'newcommand', 'configmacros']}
|
||||
},
|
||||
startup: {
|
||||
ready: function () {
|
||||
MathJax.startup.defaultReady();
|
||||
console.log('✅ MathJax 3 listo');
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Instalación
|
||||
|
||||
### Dependencia Requerida
|
||||
```bash
|
||||
pip install tkinterweb
|
||||
```
|
||||
|
||||
**Nota**: Si no tienes tkinterweb instalado, la aplicación se cerrará automáticamente con un mensaje de error claro.
|
||||
|
||||
## Características Técnicas
|
||||
|
||||
### ✅ **Renderizado Optimizado**
|
||||
- **MathJax 3** para ecuaciones LaTeX profesionales
|
||||
- **Fuentes matemáticas mejoradas** (STIX Two Math, Computer Modern Serif)
|
||||
- **Re-renderizado automático** cuando se agregan ecuaciones
|
||||
- **Indicador de estado** que muestra si MathJax está activo
|
||||
|
||||
### ✅ **Integración Perfecta**
|
||||
- **Panel expandible** en el lado derecho de la calculadora
|
||||
- **Sincronización automática** con las evaluaciones
|
||||
- **Detección inteligente** de ecuaciones, asignaciones y comentarios
|
||||
- **Indicador visual** cuando hay contenido disponible
|
||||
|
||||
### ✅ **Tipos de Contenido Soportados**
|
||||
- **Ecuaciones**: `x**2 + 2*x = 8`
|
||||
- **Asignaciones**: `a = 5`
|
||||
- **Expresiones simbólicas**: `sin(x) + cos(x)`
|
||||
- **Comentarios**: `# Esto es un comentario`
|
||||
|
||||
## Uso
|
||||
|
||||
1. **Abrir panel LaTeX**: Click en el botón 📐 en el borde derecho
|
||||
2. **Escribir ecuaciones**: Las ecuaciones se detectan automáticamente
|
||||
3. **Ver renderizado**: MathJax renderiza las ecuaciones en tiempo real
|
||||
4. **Cerrar panel**: Click en ✕ o en el botón 📐 nuevamente
|
||||
|
||||
## Solución de Problemas
|
||||
|
||||
### Si ves "Cargando MathJax..."
|
||||
- **Causa**: MathJax no se ha cargado completamente
|
||||
- **Solución**: Espera unos segundos, MathJax se carga desde CDN
|
||||
- **Verificación**: El indicador cambiará a "✅ MathJax activo"
|
||||
|
||||
### Si la aplicación se cierra al inicio
|
||||
- **Causa**: tkinterweb no está instalado
|
||||
- **Solución**: `pip install tkinterweb`
|
||||
- **Verificación**: Reinicia la aplicación
|
||||
|
||||
### Si las ecuaciones no se renderizan
|
||||
- **Causa**: Error en la sintaxis LaTeX
|
||||
- **Solución**: Verifica que la sintaxis LaTeX sea correcta
|
||||
- **Verificación**: Revisa la consola del navegador (F12 en tkinterweb)
|
||||
|
||||
## Ventajas del Sistema Simplificado
|
||||
|
||||
### 🎯 **Confiabilidad**
|
||||
- **Un solo sistema** = menos puntos de falla
|
||||
- **Dependencia mínima** = fácil instalación
|
||||
- **Error claro** si falta la dependencia
|
||||
|
||||
### 🚀 **Rendimiento**
|
||||
- **MathJax 3** optimizado para velocidad
|
||||
- **Sin fallbacks** = código más limpio
|
||||
- **Carga asíncrona** = no bloquea la UI
|
||||
|
||||
### 🔧 **Mantenimiento**
|
||||
- **Código simplificado** = fácil de mantener
|
||||
- **Sin lógica compleja** de detección de sistemas
|
||||
- **Comportamiento predecible**
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
Calculadora MAV
|
||||
├── Panel de Entrada (izquierda)
|
||||
├── Panel de Salida (centro)
|
||||
└── Panel LaTeX (derecha, expandible)
|
||||
└── tkinterweb.HtmlFrame
|
||||
└── MathJax 3 + HTML optimizado
|
||||
├── Ecuaciones renderizadas
|
||||
├── Fuentes matemáticas
|
||||
└── Indicador de estado
|
||||
```
|
||||
|
||||
## Conclusión
|
||||
|
||||
El sistema simplificado con **tkinterweb + MathJax 3** proporciona:
|
||||
- ✅ Renderizado LaTeX profesional
|
||||
- ✅ Instalación simple (una dependencia)
|
||||
- ✅ Comportamiento predecible
|
||||
- ✅ Mantenimiento fácil
|
||||
- ✅ Experiencia de usuario consistente
|
||||
|
||||
**Resultado**: Panel LaTeX que funciona perfectamente sin fallbacks ni complejidad innecesaria.
|
2497
app/main_calc_app.py
2497
app/main_calc_app.py
File diff suppressed because it is too large
Load Diff
592
app/tl_popup.py
592
app/tl_popup.py
|
@ -1,592 +0,0 @@
|
|||
"""
|
||||
Sistema de resultados interactivos con tags clickeables - VERSIÓN PYSIDE6
|
||||
"""
|
||||
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QTextEdit,
|
||||
QLabel, QLineEdit, QPushButton, QFrame, QScrollArea)
|
||||
from PySide6.QtCore import Qt, QTimer, Signal
|
||||
from PySide6.QtGui import QFont, QTextCursor, QTextCharFormat, QColor
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
import sympy
|
||||
from typing import Any, Optional, Dict, List, Tuple
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
|
||||
import numpy as np
|
||||
|
||||
|
||||
class PlotResult:
|
||||
"""Placeholder para resultados de plotting - DEFINICIÓN PRINCIPAL"""
|
||||
|
||||
def __init__(self, plot_type: str, args: tuple, kwargs: dict, original_expression: str = None):
|
||||
self.plot_type = plot_type
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.original_expression = original_expression or ""
|
||||
|
||||
def __str__(self):
|
||||
return f"📊 Ver {self.plot_type.title()}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"PlotResult('{self.plot_type}', {self.args}, {self.kwargs})"
|
||||
|
||||
|
||||
class InteractiveResultManager(QWidget):
|
||||
"""Maneja resultados interactivos con ventanas emergentes - PySide6"""
|
||||
|
||||
plot_requested = Signal(object) # Señal para solicitar mostrar plot en MathJax
|
||||
|
||||
def __init__(self, parent_window=None):
|
||||
super().__init__()
|
||||
self.parent = parent_window
|
||||
self.open_windows: Dict[str, QWidget] = {}
|
||||
self.update_input_callback = None
|
||||
self._setup_styles()
|
||||
|
||||
def _setup_styles(self):
|
||||
"""Configurar estilos para ventanas emergentes"""
|
||||
self.window_style = """
|
||||
QWidget {
|
||||
background-color: #2b2b2b;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
QTextEdit {
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border: 1px solid #3c3c3c;
|
||||
font-family: 'Consolas';
|
||||
font-size: 11px;
|
||||
}
|
||||
QLineEdit {
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border: 1px solid #3c3c3c;
|
||||
padding: 5px;
|
||||
font-family: 'Consolas';
|
||||
font-size: 11px;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #4fc3f7;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
font-family: 'Consolas';
|
||||
font-size: 9px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #29b6f6;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #0288d1;
|
||||
}
|
||||
QLabel {
|
||||
color: #d4d4d4;
|
||||
font-family: 'Consolas';
|
||||
font-size: 10px;
|
||||
}
|
||||
"""
|
||||
|
||||
def set_update_callback(self, callback):
|
||||
"""Establece el callback para actualizar el panel de entrada"""
|
||||
self.update_input_callback = callback
|
||||
|
||||
def create_interactive_link(self, result: Any) -> Optional[Tuple[str, str, Any]]:
|
||||
"""
|
||||
Crea un link interactivo para un resultado si es necesario
|
||||
|
||||
Returns:
|
||||
(link_id, display_text, result_object) si se creó link, None si no es necesario
|
||||
"""
|
||||
link_id = None
|
||||
display_text = ""
|
||||
|
||||
if isinstance(result, PlotResult):
|
||||
link_id = f"plot_{id(result)}"
|
||||
display_text = f"📊 Ver {result.plot_type.title()}"
|
||||
|
||||
elif isinstance(result, sympy.Matrix):
|
||||
link_id = f"matrix_{id(result)}"
|
||||
rows, cols = result.shape
|
||||
display_text = f"📋 Ver Matriz {rows}×{cols}"
|
||||
|
||||
elif isinstance(result, list) and len(result) > 5:
|
||||
link_id = f"list_{id(result)}"
|
||||
display_text = f"📋 Ver Lista ({len(result)} elementos)"
|
||||
|
||||
elif isinstance(result, dict) and len(result) > 3:
|
||||
link_id = f"dict_{id(result)}"
|
||||
display_text = f"🔍 Ver Diccionario ({len(result)} entradas)"
|
||||
|
||||
elif hasattr(result, '__dict__') and len(str(result)) > 100:
|
||||
link_id = f"object_{id(result)}"
|
||||
display_text = f"🔍 Ver Detalles ({type(result).__name__})"
|
||||
|
||||
if link_id and display_text:
|
||||
return (link_id, display_text, result)
|
||||
|
||||
return None
|
||||
|
||||
def handle_interactive_click(self, result: Any, is_mathjax_click: bool = False):
|
||||
"""Maneja clicks en elementos interactivos"""
|
||||
if isinstance(result, PlotResult):
|
||||
if not is_mathjax_click:
|
||||
# Primera vez: mostrar en MathJax
|
||||
self.plot_requested.emit(result)
|
||||
else:
|
||||
# Click en MathJax: abrir ventana emergente
|
||||
self._show_plot_window(result)
|
||||
else:
|
||||
# Otros tipos siempre abren ventana
|
||||
self._handle_other_interactive_click(result)
|
||||
|
||||
def _handle_other_interactive_click(self, result: Any):
|
||||
"""Maneja clicks en elementos no-plot"""
|
||||
window_key = f"{type(result).__name__}_{id(result)}"
|
||||
|
||||
# Si ya existe la ventana, enfocarla
|
||||
if window_key in self.open_windows:
|
||||
window = self.open_windows[window_key]
|
||||
if window and not window.isHidden():
|
||||
window.raise_()
|
||||
window.activateWindow()
|
||||
return
|
||||
else:
|
||||
del self.open_windows[window_key]
|
||||
|
||||
# Crear nueva ventana
|
||||
try:
|
||||
if isinstance(result, sympy.Matrix):
|
||||
self._show_matrix_window(result, window_key)
|
||||
elif isinstance(result, list):
|
||||
self._show_list_window(result, window_key)
|
||||
elif isinstance(result, dict):
|
||||
self._show_dict_window(result, window_key)
|
||||
else:
|
||||
self._show_object_window(result, window_key)
|
||||
except Exception as e:
|
||||
print(f"❌ Error abriendo ventana interactiva: {e}")
|
||||
|
||||
def _show_plot_window(self, plot_result: PlotResult):
|
||||
"""Muestra ventana con plot matplotlib e interfaz de edición"""
|
||||
window_key = f"plot_{id(plot_result)}"
|
||||
|
||||
# Si ya existe la ventana, enfocarla
|
||||
if window_key in self.open_windows:
|
||||
window = self.open_windows[window_key]
|
||||
if window and not window.isHidden():
|
||||
window.raise_()
|
||||
window.activateWindow()
|
||||
return
|
||||
else:
|
||||
del self.open_windows[window_key]
|
||||
|
||||
# Obtener posición de la ventana principal
|
||||
if self.parent:
|
||||
parent_pos = self.parent.pos()
|
||||
parent_size = self.parent.size()
|
||||
plot_window_x = parent_pos.x() + parent_size.width()
|
||||
plot_window_y = parent_pos.y()
|
||||
else:
|
||||
plot_window_x = 100
|
||||
plot_window_y = 100
|
||||
|
||||
# Crear ventana
|
||||
window = QWidget()
|
||||
window.setWindowTitle(f"Plot - {plot_result.plot_type}")
|
||||
window.resize(700, 600)
|
||||
window.move(plot_window_x, plot_window_y)
|
||||
window.setStyleSheet(self.window_style)
|
||||
|
||||
# Layout principal
|
||||
layout = QVBoxLayout(window)
|
||||
|
||||
# Frame superior para edición
|
||||
edit_frame = QFrame()
|
||||
edit_layout = QHBoxLayout(edit_frame)
|
||||
|
||||
# Label y campo de edición
|
||||
label = QLabel("Expresión:")
|
||||
expression_edit = QLineEdit(plot_result.original_expression)
|
||||
redraw_btn = QPushButton("Redibujar")
|
||||
|
||||
edit_layout.addWidget(label)
|
||||
edit_layout.addWidget(expression_edit)
|
||||
edit_layout.addWidget(redraw_btn)
|
||||
|
||||
# Frame para el canvas del plot
|
||||
canvas_frame = QFrame()
|
||||
canvas_layout = QVBoxLayout(canvas_frame)
|
||||
|
||||
layout.addWidget(edit_frame)
|
||||
layout.addWidget(canvas_frame)
|
||||
|
||||
# Crear plot inicial
|
||||
self._create_plot_in_frame(plot_result, canvas_frame)
|
||||
|
||||
# Conectar botón de redibujar
|
||||
def redraw():
|
||||
self._redraw_plot(plot_result, canvas_frame, expression_edit.text())
|
||||
|
||||
redraw_btn.clicked.connect(redraw)
|
||||
|
||||
# Configurar cierre
|
||||
def on_close():
|
||||
edited_expression = expression_edit.text().strip()
|
||||
original_expression = plot_result.original_expression.strip()
|
||||
|
||||
if edited_expression != original_expression and self.update_input_callback:
|
||||
self.update_input_callback(original_expression, edited_expression)
|
||||
|
||||
if window_key in self.open_windows:
|
||||
del self.open_windows[window_key]
|
||||
|
||||
window.closeEvent = lambda event: (on_close(), event.accept())
|
||||
|
||||
# Registrar y mostrar ventana
|
||||
self.open_windows[window_key] = window
|
||||
window.show()
|
||||
|
||||
# Focus en el campo de edición
|
||||
expression_edit.setFocus()
|
||||
expression_edit.selectAll()
|
||||
|
||||
def _redraw_plot(self, plot_result: PlotResult, canvas_frame: QFrame, new_expression: str):
|
||||
"""Redibuja el plot con una nueva expresión"""
|
||||
try:
|
||||
# Limpiar el frame actual
|
||||
layout = canvas_frame.layout()
|
||||
if layout:
|
||||
while layout.count():
|
||||
child = layout.takeAt(0)
|
||||
if child.widget():
|
||||
child.widget().deleteLater()
|
||||
|
||||
# Evaluar la nueva expresión
|
||||
import sympy as sp
|
||||
|
||||
# Crear contexto básico para evaluación
|
||||
eval_context = {
|
||||
'sin': sp.sin, 'cos': sp.cos, 'tan': sp.tan,
|
||||
'exp': sp.exp, 'log': sp.log, 'sqrt': sp.sqrt,
|
||||
'pi': sp.pi, 'e': sp.E, 'x': sp.Symbol('x'), 'y': sp.Symbol('y'),
|
||||
'z': sp.Symbol('z'), 't': sp.Symbol('t')
|
||||
}
|
||||
|
||||
# Evaluar la expresión
|
||||
new_expr = sp.sympify(new_expression, locals=eval_context)
|
||||
|
||||
# Crear nuevo PlotResult con la expresión actualizada
|
||||
new_plot_result = PlotResult(
|
||||
plot_result.plot_type,
|
||||
(new_expr,) + plot_result.args[1:],
|
||||
plot_result.kwargs,
|
||||
new_expression
|
||||
)
|
||||
|
||||
# Actualizar la expresión original en el objeto
|
||||
plot_result.original_expression = new_expression
|
||||
|
||||
# Redibujar
|
||||
self._create_plot_in_frame(new_plot_result, canvas_frame)
|
||||
|
||||
except Exception as e:
|
||||
# Mostrar error en el frame
|
||||
if not canvas_frame.layout():
|
||||
canvas_frame.setLayout(QVBoxLayout())
|
||||
|
||||
error_label = QLabel(f"Error en expresión: {e}")
|
||||
error_label.setStyleSheet("color: #f44747; font-size: 11px;")
|
||||
error_label.setWordWrap(True)
|
||||
canvas_frame.layout().addWidget(error_label)
|
||||
|
||||
def _create_plot_in_frame(self, plot_result: PlotResult, parent_frame: QFrame):
|
||||
"""Crea el plot dentro del frame especificado"""
|
||||
try:
|
||||
if not parent_frame.layout():
|
||||
parent_frame.setLayout(QVBoxLayout())
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 6))
|
||||
|
||||
if plot_result.plot_type == "plot":
|
||||
self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs)
|
||||
elif plot_result.plot_type == "plot3d":
|
||||
self._create_3d_plot(fig, plot_result.args, plot_result.kwargs)
|
||||
|
||||
# Embed en PySide6
|
||||
canvas = FigureCanvasQTAgg(fig)
|
||||
parent_frame.layout().addWidget(canvas)
|
||||
|
||||
except Exception as e:
|
||||
if not parent_frame.layout():
|
||||
parent_frame.setLayout(QVBoxLayout())
|
||||
|
||||
error_label = QLabel(f"Error generando plot: {e}")
|
||||
error_label.setStyleSheet("color: #f44747; font-size: 12px;")
|
||||
error_label.setWordWrap(True)
|
||||
parent_frame.layout().addWidget(error_label)
|
||||
|
||||
def _create_2d_plot(self, fig, ax, args, kwargs):
|
||||
"""Crea plot 2D usando SymPy"""
|
||||
if len(args) >= 1:
|
||||
expr = args[0]
|
||||
|
||||
try:
|
||||
if len(args) >= 2:
|
||||
# Rango especificado: (variable, start, end)
|
||||
var_range = args[1]
|
||||
if isinstance(var_range, tuple) and len(var_range) == 3:
|
||||
var, start, end = var_range
|
||||
x_vals = np.linspace(float(start), float(end), 1000)
|
||||
|
||||
# Evaluar expresión
|
||||
f = sympy.lambdify(var, expr, 'numpy')
|
||||
y_vals = f(x_vals)
|
||||
|
||||
ax.plot(x_vals, y_vals, **kwargs)
|
||||
ax.set_xlabel(str(var))
|
||||
ax.set_ylabel(str(expr))
|
||||
ax.grid(True)
|
||||
ax.set_title(f"Plot: {expr}")
|
||||
else:
|
||||
# Rango por defecto
|
||||
free_symbols = list(expr.free_symbols)
|
||||
if free_symbols:
|
||||
var = free_symbols[0]
|
||||
x_vals = np.linspace(-10, 10, 1000)
|
||||
f = sympy.lambdify(var, expr, 'numpy')
|
||||
y_vals = f(x_vals)
|
||||
|
||||
ax.plot(x_vals, y_vals, **kwargs)
|
||||
ax.set_xlabel(str(var))
|
||||
ax.set_ylabel(str(expr))
|
||||
ax.grid(True)
|
||||
ax.set_title(f"Plot: {expr}")
|
||||
|
||||
except Exception as e:
|
||||
ax.text(0.5, 0.5, f"Error: {e}",
|
||||
transform=ax.transAxes, ha='center', va='center')
|
||||
ax.set_title("Error en Plot")
|
||||
|
||||
def _create_3d_plot(self, fig, args, kwargs):
|
||||
"""Crea plot 3D"""
|
||||
try:
|
||||
ax = fig.add_subplot(111, projection='3d')
|
||||
|
||||
if len(args) >= 3:
|
||||
expr = args[0]
|
||||
x_range = args[1] # (x, x_start, x_end)
|
||||
y_range = args[2] # (y, y_start, y_end)
|
||||
|
||||
if isinstance(x_range, tuple) and isinstance(y_range, tuple):
|
||||
x_var, x_start, x_end = x_range
|
||||
y_var, y_start, y_end = y_range
|
||||
|
||||
x_vals = np.linspace(float(x_start), float(x_end), 50)
|
||||
y_vals = np.linspace(float(y_start), float(y_end), 50)
|
||||
X, Y = np.meshgrid(x_vals, y_vals)
|
||||
|
||||
f = sympy.lambdify([x_var, y_var], expr, 'numpy')
|
||||
Z = f(X, Y)
|
||||
|
||||
ax.plot_surface(X, Y, Z, **kwargs)
|
||||
ax.set_xlabel(str(x_var))
|
||||
ax.set_ylabel(str(y_var))
|
||||
ax.set_zlabel(str(expr))
|
||||
ax.set_title(f"3D Plot: {expr}")
|
||||
|
||||
except Exception as e:
|
||||
ax.text2D(0.5, 0.5, f"Error: {e}", transform=ax.transAxes)
|
||||
|
||||
def _show_matrix_window(self, matrix: sympy.Matrix, window_key: str):
|
||||
"""Muestra ventana con matriz formateada"""
|
||||
rows, cols = matrix.shape
|
||||
window_title = f"Matriz {rows}×{cols}"
|
||||
|
||||
window = self._create_base_window(window_title, 600, 500)
|
||||
self.open_windows[window_key] = window
|
||||
|
||||
layout = QVBoxLayout(window)
|
||||
|
||||
# Widget de texto con scroll
|
||||
text_widget = QTextEdit()
|
||||
text_widget.setFont(QFont("Courier New", 12))
|
||||
text_widget.setReadOnly(True)
|
||||
|
||||
# Formatear matriz
|
||||
matrix_str = self._format_matrix(matrix)
|
||||
text_widget.setPlainText(matrix_str)
|
||||
|
||||
layout.addWidget(text_widget)
|
||||
|
||||
# Botones de utilidad
|
||||
button_frame = QFrame()
|
||||
button_layout = QHBoxLayout(button_frame)
|
||||
|
||||
if matrix.is_square:
|
||||
try:
|
||||
det_btn = QPushButton("Determinante")
|
||||
det_btn.clicked.connect(
|
||||
lambda: self._show_matrix_property(matrix, "determinante", matrix.det())
|
||||
)
|
||||
button_layout.addWidget(det_btn)
|
||||
|
||||
inv_btn = QPushButton("Inversa")
|
||||
inv_btn.clicked.connect(
|
||||
lambda: self._show_matrix_property(matrix, "inversa", matrix.inv())
|
||||
)
|
||||
button_layout.addWidget(inv_btn)
|
||||
except:
|
||||
pass
|
||||
|
||||
layout.addWidget(button_frame)
|
||||
window.show()
|
||||
|
||||
def _format_matrix(self, matrix: sympy.Matrix) -> str:
|
||||
"""Formatea una matriz para display"""
|
||||
rows, cols = matrix.shape
|
||||
|
||||
# Calcular ancho máximo de elementos
|
||||
max_width = 0
|
||||
for i in range(rows):
|
||||
for j in range(cols):
|
||||
element_str = str(matrix[i, j])
|
||||
max_width = max(max_width, len(element_str))
|
||||
|
||||
max_width = max(max_width, 8) # Mínimo 8 caracteres
|
||||
|
||||
# Construir representación
|
||||
lines = []
|
||||
lines.append("┌" + " " * (max_width * cols + cols - 1) + "┐")
|
||||
|
||||
for i in range(rows):
|
||||
line = "│"
|
||||
for j in range(cols):
|
||||
element_str = str(matrix[i, j])
|
||||
padded = element_str.center(max_width)
|
||||
line += padded
|
||||
if j < cols - 1:
|
||||
line += " "
|
||||
line += "│"
|
||||
lines.append(line)
|
||||
|
||||
lines.append("└" + " " * (max_width * cols + cols - 1) + "┘")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _show_matrix_property(self, matrix: sympy.Matrix, prop_name: str, prop_value: Any):
|
||||
"""Muestra propiedad de matriz en ventana separada"""
|
||||
window = self._create_base_window(f"Matriz - {prop_name.title()}", 400, 300)
|
||||
|
||||
layout = QVBoxLayout(window)
|
||||
text_widget = QTextEdit()
|
||||
text_widget.setFont(QFont("Courier New", 12))
|
||||
text_widget.setReadOnly(True)
|
||||
|
||||
if isinstance(prop_value, sympy.Matrix):
|
||||
content = f"{prop_name.title()}:\n\n{self._format_matrix(prop_value)}"
|
||||
else:
|
||||
content = f"{prop_name.title()}: {prop_value}"
|
||||
|
||||
text_widget.setPlainText(content)
|
||||
layout.addWidget(text_widget)
|
||||
|
||||
window.show()
|
||||
|
||||
def _show_list_window(self, lst: list, window_key: str):
|
||||
"""Muestra ventana con lista expandida"""
|
||||
window_title = f"Lista ({len(lst)} elementos)"
|
||||
window = self._create_base_window(window_title, 500, 400)
|
||||
self.open_windows[window_key] = window
|
||||
|
||||
layout = QVBoxLayout(window)
|
||||
text_widget = QTextEdit()
|
||||
text_widget.setFont(QFont("Consolas", 11))
|
||||
text_widget.setReadOnly(True)
|
||||
|
||||
content = "Elementos de la lista:\n\n"
|
||||
for i, item in enumerate(lst):
|
||||
content += f"[{i}] {item}\n"
|
||||
|
||||
text_widget.setPlainText(content)
|
||||
layout.addWidget(text_widget)
|
||||
|
||||
window.show()
|
||||
|
||||
def _show_dict_window(self, dct: dict, window_key: str):
|
||||
"""Muestra ventana con diccionario expandido"""
|
||||
window = self._create_base_window(f"Diccionario ({len(dct)} entradas)", 500, 400)
|
||||
self.open_windows[window_key] = window
|
||||
|
||||
layout = QVBoxLayout(window)
|
||||
text_widget = QTextEdit()
|
||||
text_widget.setFont(QFont("Consolas", 11))
|
||||
text_widget.setReadOnly(True)
|
||||
|
||||
content = "Entradas del diccionario:\n\n"
|
||||
for key, value in dct.items():
|
||||
content += f"{key}: {value}\n"
|
||||
|
||||
text_widget.setPlainText(content)
|
||||
layout.addWidget(text_widget)
|
||||
|
||||
window.show()
|
||||
|
||||
def _show_object_window(self, obj: Any, window_key: str):
|
||||
"""Muestra ventana con detalles de objeto"""
|
||||
window = self._create_base_window(f"Objeto - {type(obj).__name__}", 600, 500)
|
||||
self.open_windows[window_key] = window
|
||||
|
||||
layout = QVBoxLayout(window)
|
||||
text_widget = QTextEdit()
|
||||
text_widget.setFont(QFont("Consolas", 11))
|
||||
text_widget.setReadOnly(True)
|
||||
|
||||
content = f"Objeto: {type(obj).__name__}\n\n"
|
||||
content += f"Valor: {obj}\n\n"
|
||||
content += f"Representación: {repr(obj)}\n\n"
|
||||
|
||||
if hasattr(obj, '__dict__'):
|
||||
content += "Atributos:\n"
|
||||
for attr, value in obj.__dict__.items():
|
||||
content += f" {attr}: {value}\n"
|
||||
|
||||
content += "\nMétodos disponibles:\n"
|
||||
for attr in dir(obj):
|
||||
if not attr.startswith('_') and callable(getattr(obj, attr, None)):
|
||||
content += f" {attr}()\n"
|
||||
|
||||
text_widget.setPlainText(content)
|
||||
layout.addWidget(text_widget)
|
||||
|
||||
window.show()
|
||||
|
||||
def _create_base_window(self, title: str, width: int = 500, height: int = 400) -> QWidget:
|
||||
"""Crea ventana base con estilo consistente"""
|
||||
window = QWidget()
|
||||
window.setWindowTitle(title)
|
||||
window.resize(width, height)
|
||||
window.setStyleSheet(self.window_style)
|
||||
|
||||
# Centrar ventana
|
||||
if self.parent:
|
||||
parent_pos = self.parent.pos()
|
||||
parent_size = self.parent.size()
|
||||
x = parent_pos.x() + (parent_size.width() - width) // 2
|
||||
y = parent_pos.y() + (parent_size.height() - height) // 2
|
||||
window.move(x, y)
|
||||
|
||||
return window
|
||||
|
||||
def close_all_windows(self):
|
||||
"""Cierra todas las ventanas interactivas"""
|
||||
windows_to_close = list(self.open_windows.values())
|
||||
|
||||
for window in windows_to_close:
|
||||
if window and not window.isHidden():
|
||||
window.close()
|
||||
|
||||
self.open_windows.clear()
|
||||
|
||||
# Cerrar todas las figuras de matplotlib
|
||||
try:
|
||||
plt.close('all')
|
||||
except:
|
||||
pass
|
317
calc.py
317
calc.py
|
@ -1,89 +1,270 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Launcher para Calculadora MAV - Versión PySide6 con MathJax
|
||||
Launcher principal para Calculadora MAV - CAS Híbrido
|
||||
Este script maneja la inicialización y ejecución de la aplicación
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import logging
|
||||
import datetime
|
||||
import traceback
|
||||
import platform
|
||||
import platform
|
||||
|
||||
def check_dependencies():
|
||||
"""Verifica que todas las dependencias estén instaladas"""
|
||||
required_modules = [
|
||||
'PySide6',
|
||||
'sympy',
|
||||
'numpy',
|
||||
'matplotlib'
|
||||
]
|
||||
|
||||
missing = []
|
||||
for module in required_modules:
|
||||
try:
|
||||
__import__(module)
|
||||
except ImportError:
|
||||
missing.append(module)
|
||||
|
||||
if missing:
|
||||
print("❌ Faltan las siguientes dependencias:")
|
||||
for module in missing:
|
||||
print(f" - {module}")
|
||||
print("\n💡 Para instalar las dependencias, ejecuta:")
|
||||
print(" pip install -r requirements.txt")
|
||||
return False
|
||||
|
||||
print("✅ Todas las dependencias están instaladas")
|
||||
return True
|
||||
|
||||
def check_pyside6_webengine():
|
||||
"""Verifica si PySide6 WebEngine está disponible"""
|
||||
def setup_logging():
|
||||
"""Configura el sistema de logging completo"""
|
||||
MAX_LOG_FILES = 10 # Límite de archivos de log
|
||||
log_dir = Path("logs")
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Archivo de log con timestamp
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
log_file = log_dir / f"mav_calc_{timestamp}.log"
|
||||
|
||||
# Eliminar logs antiguos si se supera el límite
|
||||
try:
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
print("✅ PySide6 WebEngine disponible para MathJax")
|
||||
return True
|
||||
except ImportError:
|
||||
print("⚠️ PySide6 WebEngine no disponible")
|
||||
print(" Instalando QtWebEngine...")
|
||||
try:
|
||||
subprocess.run([sys.executable, '-m', 'pip', 'install', 'PySide6-WebEngine'],
|
||||
check=True, capture_output=True)
|
||||
print("✅ PySide6 WebEngine instalado correctamente")
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
print("❌ No se pudo instalar PySide6 WebEngine")
|
||||
return False
|
||||
existing_logs = sorted(
|
||||
[f for f in log_dir.glob("mav_calc_*.log") if f.is_file()],
|
||||
key=os.path.getmtime
|
||||
)
|
||||
if len(existing_logs) >= MAX_LOG_FILES:
|
||||
logs_to_delete = existing_logs[:len(existing_logs) - MAX_LOG_FILES + 1]
|
||||
for old_log in logs_to_delete:
|
||||
old_log.unlink()
|
||||
logging.info(f"Eliminado log antiguo: {old_log}")
|
||||
except Exception as e:
|
||||
logging.warning(f"No se pudieron eliminar logs antiguos: {e}")
|
||||
|
||||
# Configurar logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
return logger, log_file
|
||||
|
||||
|
||||
|
||||
def log_error_with_context(logger, error, context=""):
|
||||
"""Registra un error con contexto completo"""
|
||||
logger.error("=" * 50)
|
||||
logger.error("ERROR DETECTADO")
|
||||
logger.error("=" * 50)
|
||||
if context:
|
||||
logger.error(f"Contexto: {context}")
|
||||
logger.error(f"Tipo de error: {type(error).__name__}")
|
||||
logger.error(f"Mensaje: {str(error)}")
|
||||
logger.error("Traceback completo:")
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error("Variables locales en el momento del error:")
|
||||
|
||||
# Intentar capturar variables locales del frame donde ocurrió el error
|
||||
try:
|
||||
tb = traceback.extract_tb(error.__traceback__)
|
||||
if tb:
|
||||
last_frame = tb[-1]
|
||||
logger.error(f" Archivo: {last_frame.filename}")
|
||||
logger.error(f" Línea: {last_frame.lineno}")
|
||||
logger.error(f" Función: {last_frame.name}")
|
||||
logger.error(f" Código: {last_frame.line}")
|
||||
except Exception as frame_error:
|
||||
logger.error(f"No se pudieron obtener detalles del frame: {frame_error}")
|
||||
|
||||
logger.error("=" * 50)
|
||||
|
||||
|
||||
def show_error_with_log_info(error, log_file, context=""):
|
||||
"""Muestra error al usuario con información del log"""
|
||||
error_msg = f"""Error en Calculadora MAV:
|
||||
|
||||
{context}
|
||||
|
||||
Error: {type(error).__name__}: {str(error)}
|
||||
|
||||
INFORMACIÓN DE DEBUGGING:
|
||||
• Log completo guardado en: {log_file}
|
||||
• Para soporte, enviar el archivo de log
|
||||
• Timestamp: {datetime.datetime.now()}
|
||||
|
||||
¿Qué hacer ahora?
|
||||
1. Revisar el archivo de log para más detalles
|
||||
2. Intentar reiniciar la aplicación
|
||||
3. Verificar dependencias con: python launcher.py --setup
|
||||
4. Ejecutar tests con: python launcher.py --test
|
||||
"""
|
||||
|
||||
try:
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
# Crear ventana de error personalizada
|
||||
error_window = tk.Toplevel(root)
|
||||
error_window.title("Error - Calculadora MAV")
|
||||
error_window.geometry("600x400")
|
||||
error_window.configure(bg="#2b2b2b")
|
||||
|
||||
# Hacer la ventana modal
|
||||
error_window.transient(root)
|
||||
error_window.grab_set()
|
||||
|
||||
# Centrar ventana
|
||||
error_window.update_idletasks()
|
||||
x = (error_window.winfo_screenwidth() // 2) - (error_window.winfo_width() // 2)
|
||||
y = (error_window.winfo_screenheight() // 2) - (error_window.winfo_height() // 2)
|
||||
error_window.geometry(f"+{x}+{y}")
|
||||
|
||||
# Contenido
|
||||
from tkinter import scrolledtext
|
||||
|
||||
text_widget = scrolledtext.ScrolledText(
|
||||
error_window,
|
||||
font=("Consolas", 10),
|
||||
bg="#1e1e1e",
|
||||
fg="#ff6b6b",
|
||||
wrap=tk.WORD
|
||||
)
|
||||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
text_widget.insert("1.0", error_msg)
|
||||
text_widget.config(state="disabled")
|
||||
|
||||
# Botones
|
||||
button_frame = tk.Frame(error_window, bg="#2b2b2b")
|
||||
button_frame.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
def open_log_folder():
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
os.startfile(log_file.parent)
|
||||
elif platform.system() == "Darwin": # macOS
|
||||
subprocess.run(["open", str(log_file.parent)])
|
||||
else: # Linux
|
||||
subprocess.run(["xdg-open", str(log_file.parent)])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def copy_log_path():
|
||||
error_window.clipboard_clear()
|
||||
error_window.clipboard_append(str(log_file))
|
||||
|
||||
tk.Button(
|
||||
button_frame,
|
||||
text="Abrir Carpeta de Logs",
|
||||
command=open_log_folder,
|
||||
bg="#4fc3f7",
|
||||
fg="white"
|
||||
).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
tk.Button(
|
||||
button_frame,
|
||||
text="Copiar Ruta del Log",
|
||||
command=copy_log_path,
|
||||
bg="#82aaff",
|
||||
fg="white"
|
||||
).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
tk.Button(
|
||||
button_frame,
|
||||
text="Cerrar",
|
||||
command=error_window.destroy,
|
||||
bg="#ff6b6b",
|
||||
fg="white"
|
||||
).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
# Esperar a que se cierre la ventana
|
||||
error_window.wait_window()
|
||||
root.destroy()
|
||||
|
||||
except Exception as gui_error:
|
||||
# Si falla la GUI, mostrar en consola
|
||||
print("ERROR: No se pudo mostrar ventana de error")
|
||||
print(error_msg)
|
||||
print(f"Error adicional: {gui_error}")
|
||||
|
||||
|
||||
# Variable global para el logger
|
||||
logger = None
|
||||
log_file = None
|
||||
|
||||
def launch_application():
|
||||
|
||||
try:
|
||||
# Importar y ejecutar la aplicación
|
||||
from main_calc_app import HybridCalculatorApp
|
||||
root = tk.Tk()
|
||||
|
||||
app = HybridCalculatorApp(root)
|
||||
root.mainloop()
|
||||
|
||||
logger.info("Aplicación cerrada normalmente")
|
||||
|
||||
except ImportError as e:
|
||||
logger.error(f"Error de importación: {e}")
|
||||
log_error_with_context(logger, e, "Importación de módulos de la aplicación")
|
||||
show_error_with_log_info(e, log_file, "Error importando módulos de la aplicación")
|
||||
except Exception as e:
|
||||
logger.error(f"Error durante ejecución de la aplicación: {e}")
|
||||
log_error_with_context(logger, e, "Ejecución de la aplicación principal")
|
||||
show_error_with_log_info(e, log_file, "Error durante la ejecución de la aplicación")
|
||||
|
||||
|
||||
def main():
|
||||
"""Función principal del launcher"""
|
||||
print("🚀 Iniciando Calculadora MAV - Diseño Minimalista 3 Paneles")
|
||||
print("=" * 60)
|
||||
global logger, log_file
|
||||
|
||||
# Verificar dependencias
|
||||
if not check_dependencies():
|
||||
sys.exit(1)
|
||||
|
||||
# Verificar WebEngine
|
||||
if not check_pyside6_webengine():
|
||||
print("⚠️ Continuando sin WebEngine (funcionalidad limitada)")
|
||||
|
||||
print("\n🎯 Iniciando aplicación...")
|
||||
|
||||
# Configurar logging al inicio
|
||||
try:
|
||||
# Importar y ejecutar la aplicación
|
||||
from app.main_calc_app import main as run_app
|
||||
run_app()
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Error de importación: {e}")
|
||||
print(" Verifica que todos los archivos del proyecto estén presentes")
|
||||
sys.exit(1)
|
||||
|
||||
logger, log_file = setup_logging()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error inesperado: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"ERROR: No se pudo configurar logging: {e}")
|
||||
# Continuar sin logging si falla
|
||||
logger = None
|
||||
log_file = None
|
||||
|
||||
logger.info("Iniciando verificaciones del sistema...")
|
||||
|
||||
try:
|
||||
|
||||
launch_application()
|
||||
|
||||
logger.info("Aplicación cerrada - fin de sesión")
|
||||
logger.info("=" * 60)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Aplicación interrumpida por el usuario (Ctrl+C)")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logger.error("Error crítico en main():")
|
||||
log_error_with_context(logger, e, "Función principal del launcher")
|
||||
show_error_with_log_info(e, log_file, "Error crítico durante el inicio")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
# Configurar logging básico para manejo de argumentos
|
||||
temp_logger = None
|
||||
temp_log_file = None
|
||||
|
||||
# Inicio normal
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
if temp_logger:
|
||||
log_error_with_context(temp_logger, e, "Error crítico en __main__")
|
||||
print(f"ERROR CRÍTICO: {e}")
|
||||
if temp_log_file:
|
||||
print(f"Ver log completo en: {temp_log_file}")
|
||||
sys.exit(1)
|
||||
|
|
|
@ -69,7 +69,7 @@ class Class_LaTeX(SympyClassBase):
|
|||
Class_LaTeX: Nueva instancia con la expresión parseada
|
||||
|
||||
Examples:
|
||||
LaTeX.parse_latex(r"$$Brix_{Bev} = \\frac{Brix_{syr} + Brix_{H_2O} \\cdot R_M}{R_M + 1}$$")
|
||||
LaTeX.parse_latex(r"$$Brix_{Bev} = \frac{Brix_{syr} + Brix_{H_2O} \cdot R_M}{R_M + 1}$$")
|
||||
"""
|
||||
# Primero intentar conversión manual mejorada (más confiable para subíndices)
|
||||
try:
|
||||
|
@ -234,7 +234,6 @@ class Class_LaTeX(SympyClassBase):
|
|||
(r'\\cdot', '*'), # multiplicación
|
||||
(r'\\times', '*'), # multiplicación
|
||||
(r'\\div', '/'), # división
|
||||
(r'\\([a-zA-Z]+)', r'\1'), # convertir \delta a delta, \alpha a alpha, etc.
|
||||
(r'\{([^}]*)\}', r'\1'), # eliminar llaves restantes
|
||||
]
|
||||
|
||||
|
@ -277,26 +276,19 @@ class Class_LaTeX(SympyClassBase):
|
|||
symbols_dict[name] = sympy.Symbol(name)
|
||||
|
||||
if symbols_dict:
|
||||
# Si es una ecuación, intentar parsearlo lado por lado
|
||||
# Si es una ecuación, intentar crearla manualmente
|
||||
if is_equation and '=' in result:
|
||||
left, right = result.split('=', 1)
|
||||
try:
|
||||
# Intentar parsear cada lado individualmente
|
||||
left_expr = sympy.sympify(left.strip(), locals=symbols_dict)
|
||||
right_expr = sympy.sympify(right.strip(), locals=symbols_dict)
|
||||
return sympy.Eq(left_expr, right_expr)
|
||||
except Exception as e2:
|
||||
print(f"Debug: Error parseando lados de ecuación: {e2}")
|
||||
# Como último recurso, devolver un símbolo con la expresión completa
|
||||
return sympy.Symbol(f"LaTeX_parse_error_{len(symbols_dict)}_symbols")
|
||||
# Crear una ecuación simple usando el primer símbolo encontrado
|
||||
first_symbol = list(symbols_dict.values())[0]
|
||||
return sympy.Eq(first_symbol, sum(list(symbols_dict.values())[1:], 0))
|
||||
else:
|
||||
# Para expresiones, intentar parsear con el contexto de símbolos
|
||||
try:
|
||||
return sympy.sympify(result, locals=symbols_dict)
|
||||
except Exception as e2:
|
||||
print(f"Debug: Error parseando expresión: {e2}")
|
||||
# Si todo falla, devolver el primer símbolo
|
||||
return list(symbols_dict.values())[0] if symbols_dict else sympy.Symbol('LaTeX_parse_error')
|
||||
# Para expresiones, devolver el primer símbolo o una suma
|
||||
symbols_list = list(symbols_dict.values())
|
||||
if len(symbols_list) == 1:
|
||||
return symbols_list[0]
|
||||
else:
|
||||
return sum(symbols_list[1:], symbols_list[0])
|
||||
else:
|
||||
return sympy.Symbol('LaTeX_parse_error')
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
|
||||
x**2 + y**2 = r**2
|
||||
r=?
|
||||
|
||||
a=r*5+5
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"window_geometry": "1272x700+331+1194",
|
||||
"sash_pos_x": 440,
|
||||
"symbolic_mode": true,
|
||||
"show_numeric_approximation": true,
|
||||
"keep_symbolic_fractions": true,
|
||||
"auto_simplify": false,
|
||||
"latex_panel_visible": true
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script para instalar tkinterweb con soporte JavaScript completo
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
def check_installed(package_name):
|
||||
"""Verifica si un paquete está instalado"""
|
||||
try:
|
||||
importlib.import_module(package_name)
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def install_package(package_spec):
|
||||
"""Instala un paquete usando pip"""
|
||||
try:
|
||||
print(f"🔄 Instalando {package_spec}...")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", package_spec])
|
||||
print(f"✅ {package_spec} instalado correctamente")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Error instalando {package_spec}: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Función principal"""
|
||||
print("🚀 Instalador de tkinterweb con JavaScript")
|
||||
print("=" * 50)
|
||||
|
||||
# Verificar estado actual
|
||||
print("\n📊 Estado actual:")
|
||||
tkinterweb_available = check_installed("tkinterweb")
|
||||
pythonmonkey_available = check_installed("pythonmonkey")
|
||||
|
||||
print(f" • tkinterweb: {'✅ Instalado' if tkinterweb_available else '❌ No instalado'}")
|
||||
print(f" • pythonmonkey: {'✅ Instalado' if pythonmonkey_available else '❌ No instalado'}")
|
||||
|
||||
if tkinterweb_available and pythonmonkey_available:
|
||||
print("\n🎉 ¡Todo está instalado correctamente!")
|
||||
print(" La calculadora debería funcionar con JavaScript completo.")
|
||||
return
|
||||
|
||||
print("\n🔧 Procediendo con la instalación...")
|
||||
|
||||
# Instalar tkinterweb con JavaScript
|
||||
if not tkinterweb_available or not pythonmonkey_available:
|
||||
print("\n1️⃣ Instalando tkinterweb[javascript]...")
|
||||
success = install_package("tkinterweb[javascript]")
|
||||
|
||||
if not success:
|
||||
print("\n⚠️ La instalación automática falló.")
|
||||
print(" Intenta manualmente:")
|
||||
print(" pip install tkinterweb[javascript]")
|
||||
print("\n Si hay problemas con PythonMonkey:")
|
||||
print(" 1. Asegúrate de tener Python 3.8+")
|
||||
print(" 2. En Windows, puede requerir Visual Studio Build Tools")
|
||||
print(" 3. En Linux, puede requerir build-essential")
|
||||
return
|
||||
|
||||
# Verificar instalación final
|
||||
print("\n🔍 Verificando instalación...")
|
||||
tkinterweb_final = check_installed("tkinterweb")
|
||||
pythonmonkey_final = check_installed("pythonmonkey")
|
||||
|
||||
print(f" • tkinterweb: {'✅' if tkinterweb_final else '❌'}")
|
||||
print(f" • pythonmonkey: {'✅' if pythonmonkey_final else '❌'}")
|
||||
|
||||
if tkinterweb_final and pythonmonkey_final:
|
||||
print("\n🎉 ¡Instalación completada exitosamente!")
|
||||
print(" La calculadora MAV ahora puede usar JavaScript completo.")
|
||||
print(" Reinicia la aplicación para activar las funciones avanzadas.")
|
||||
else:
|
||||
print("\n⚠️ Instalación parcial.")
|
||||
print(" La calculadora funcionará en modo fallback.")
|
||||
print(" Consulta la documentación para resolver problemas de instalación.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,148 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ecuaciones LaTeX - PyWebView</title>
|
||||
|
||||
<!-- MathJax 3 configuración optimizada para pywebview -->
|
||||
<script>
|
||||
window.MathJax = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$']],
|
||||
displayMath: [['$$', '$$']],
|
||||
processEscapes: true
|
||||
},
|
||||
options: {
|
||||
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
|
||||
},
|
||||
startup: {
|
||||
ready: function () {
|
||||
console.log('🚀 [pywebview] Iniciando MathJax...');
|
||||
MathJax.startup.defaultReady();
|
||||
console.log('✅ [pywebview] MathJax listo');
|
||||
|
||||
// Auto-renderizar después de carga
|
||||
setTimeout(function() {
|
||||
if (MathJax.typesetPromise) {
|
||||
MathJax.typesetPromise().then(function() {
|
||||
console.log('🎉 [pywebview] Renderizado automático completado');
|
||||
}).catch(function(err) {
|
||||
console.log('❌ [pywebview] Error en renderizado:', err);
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- MathJax 3 CDN -->
|
||||
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #1a1a1a;
|
||||
color: #d4d4d4;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.equation-block {
|
||||
margin: 4px 0;
|
||||
padding: 8px 10px;
|
||||
background-color: #2d2d2d;
|
||||
border-left: 3px solid #80c7f7;
|
||||
border-radius: 4px;
|
||||
word-wrap: break-word;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.equation-block:hover {
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
|
||||
.equation-content {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.math-display {
|
||||
font-size: 15px;
|
||||
text-align: left;
|
||||
margin: 2px 0;
|
||||
padding: 4px;
|
||||
background-color: #252525;
|
||||
border-radius: 3px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
/* Tipos de ecuaciones */
|
||||
.assignment { border-left-color: #dcdcaa; }
|
||||
.equation { border-left-color: #c586c0; }
|
||||
.comment { border-left-color: #6a9955; font-style: italic; }
|
||||
.symbolic { border-left-color: #9cdcfe; }
|
||||
|
||||
.info-message {
|
||||
text-align: center;
|
||||
color: #80c7f7;
|
||||
font-style: italic;
|
||||
margin: 20px;
|
||||
padding: 15px;
|
||||
border: 1px dashed #80c7f7;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 11px;
|
||||
color: #808080;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* Animación suave */
|
||||
.equation-block {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="equations-container">\n
|
||||
<div class="equation-block equation">
|
||||
<div class="equation-content">
|
||||
<div class="math-display">$$x^{2} + y^{2} = r^{2}$$</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="equation-block symbolic">
|
||||
<div class="equation-content">
|
||||
<div class="math-display">$$r = - \sqrt{x^{2} + y^{2}}$$</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="equation-block assignment">
|
||||
<div class="equation-content">
|
||||
<div class="math-display">$$5 - 5 \sqrt{x^{2} + y^{2}}$$</div>
|
||||
</div>
|
||||
</div>\n</div>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status">
|
||||
✓ PyWebView activo - MathJax cargándose...
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -16,13 +16,13 @@ try:
|
|||
except ImportError:
|
||||
HAS_SYMPY_HELPER = False
|
||||
|
||||
from .type_registry import (
|
||||
from type_registry import (
|
||||
get_registered_base_context,
|
||||
get_registered_tokenization_patterns,
|
||||
discover_and_register_types
|
||||
)
|
||||
from .tl_bracket_parser import BracketParser
|
||||
from .tl_popup import PlotResult
|
||||
from tl_bracket_parser import BracketParser
|
||||
from tl_popup import PlotResult
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -80,6 +80,7 @@ class PureAlgebraicEngine:
|
|||
'sqrt': sp.sqrt, 'abs': sp.Abs,
|
||||
'pi': sp.pi, 'e': sp.E, 'I': sp.I,
|
||||
'oo': sp.oo, 'inf': sp.oo,
|
||||
'solve': self._smart_solve,
|
||||
'Eq': sp.Eq, 'simplify': sp.simplify,
|
||||
'expand': sp.expand, 'factor': sp.factor,
|
||||
'diff': sp.diff, 'integrate': sp.integrate,
|
||||
|
@ -97,55 +98,7 @@ class PureAlgebraicEngine:
|
|||
# 2. TIPOS PERSONALIZADOS REGISTRADOS (CLAVE PARA INSTANCIACIÓN)
|
||||
registered_types = get_registered_base_context()
|
||||
|
||||
# 3. FUNCIÓN SOLVE ESPECIAL que maneja cadenas como símbolos
|
||||
def solve_wrapper(*args, **kwargs):
|
||||
"""Wrapper para solve que convierte cadenas a símbolos automáticamente"""
|
||||
processed_args = []
|
||||
for arg in args:
|
||||
if isinstance(arg, str):
|
||||
# Si es una cadena, convertir a símbolo
|
||||
processed_args.append(sp.Symbol(arg))
|
||||
elif hasattr(arg, 'is_number') and arg.is_number:
|
||||
# Si es un valor numérico, buscar el símbolo correspondiente
|
||||
# en symbol_table y verificar contexto
|
||||
for var_name, var_value in self.symbol_table.items():
|
||||
if var_value == arg:
|
||||
# Encontramos una variable con este valor, usar el símbolo
|
||||
processed_args.append(sp.Symbol(var_name))
|
||||
break
|
||||
else:
|
||||
# Si no se encuentra, intentar usar _smart_solve directo
|
||||
# que puede manejar valores numéricos
|
||||
processed_args.append(arg)
|
||||
else:
|
||||
processed_args.append(arg)
|
||||
|
||||
result = self._smart_solve(*processed_args, **kwargs)
|
||||
|
||||
# Si el resultado está vacío, intentar un enfoque alternativo
|
||||
if hasattr(result, '__len__') and len(result) == 0:
|
||||
# Caso especial: si solve() devuelve lista vacía,
|
||||
# intentar resolver como si no hubiera valores asignados
|
||||
alt_args = []
|
||||
for arg in args:
|
||||
if hasattr(arg, 'is_number') and arg.is_number:
|
||||
# Buscar la variable que tiene este valor
|
||||
for var_name, var_value in self.symbol_table.items():
|
||||
if var_value == arg:
|
||||
alt_args.append(sp.Symbol(var_name))
|
||||
break
|
||||
else:
|
||||
alt_args.append(arg)
|
||||
else:
|
||||
alt_args.append(arg)
|
||||
|
||||
if alt_args != processed_args:
|
||||
# Si hay diferencia, reintentar
|
||||
return self._smart_solve(*alt_args, **kwargs)
|
||||
|
||||
return result
|
||||
|
||||
# 4. FUNCIONES DE PLOTTING (WRAPPED)
|
||||
# 3. FUNCIONES DE PLOTTING (WRAPPED)
|
||||
# Wrappers para capturar llamadas de plot y devolver un objeto PlotResult
|
||||
def plot_wrapper(*args, **kwargs):
|
||||
# Intentar extraer la expresión original del primer argumento
|
||||
|
@ -174,15 +127,14 @@ class PureAlgebraicEngine:
|
|||
'plot3d_parametric_line': plot3d_parametric_line_wrapper,
|
||||
}
|
||||
|
||||
# 5. COMBINAR TODO EN CONTEXTO UNIFICADO
|
||||
# 4. COMBINAR TODO EN CONTEXTO UNIFICADO
|
||||
self.unified_context = {
|
||||
**sympy_functions,
|
||||
**registered_types, # IP4, FourBytes, IntBase, etc.
|
||||
**plotting_functions,
|
||||
'solve': solve_wrapper
|
||||
**plotting_functions
|
||||
}
|
||||
|
||||
# 6. VERIFICAR CARGA DE TIPOS PRINCIPALES
|
||||
# 5. VERIFICAR CARGA DE TIPOS PRINCIPALES
|
||||
required_classes = ['IP4', 'IP4Mask', 'FourBytes', 'IntBase', 'Hex', 'Bin', 'Dec', 'Chr', 'LaTeX']
|
||||
missing_classes = [cls for cls in required_classes if cls not in self.unified_context]
|
||||
|
||||
|
@ -398,7 +350,7 @@ class PureAlgebraicEngine:
|
|||
return EvaluationResult(line, error_msg, "error", False, str(e), is_solve_query=True)
|
||||
|
||||
def _evaluate_assignment(self, line: str) -> EvaluationResult:
|
||||
"""Evalúa una asignación CON FUNCIONALIDAD DUAL: asignación al contexto + ecuación a sympy"""
|
||||
"""Evalúa una asignación"""
|
||||
try:
|
||||
var_name, expression_str = line.split('=', 1)
|
||||
var_name = var_name.strip()
|
||||
|
@ -415,30 +367,10 @@ class PureAlgebraicEngine:
|
|||
eval_context = self._get_complete_context()
|
||||
result_obj = sympify(expression_str, locals=eval_context)
|
||||
|
||||
# 1. ASIGNACIÓN AL CONTEXTO (para evaluación directa)
|
||||
# Actualizar tabla de símbolos
|
||||
self.symbol_table[var_name] = result_obj
|
||||
self.variables.add(var_name)
|
||||
|
||||
# 2. AGREGAR COMO ECUACIÓN A SYMPY (para resolución algebraica)
|
||||
try:
|
||||
# Crear el símbolo de la variable y la ecuación simbólica
|
||||
var_symbol = sp.Symbol(var_name)
|
||||
right_expr = sympify(expression_str, locals=eval_context)
|
||||
equation_obj = Eq(var_symbol, right_expr)
|
||||
|
||||
# Agregar a la lista de ecuaciones para resolución
|
||||
self.equations.append(equation_obj)
|
||||
|
||||
# Registrar símbolos de la ecuación
|
||||
for free_symbol in equation_obj.free_symbols:
|
||||
self.variables.add(str(free_symbol))
|
||||
|
||||
self.logger.debug(f"Asignación dual agregada: {var_name} = {result_obj} (como ecuación: {equation_obj})")
|
||||
|
||||
except Exception as eq_error:
|
||||
# Si falla como ecuación, continuar solo con la asignación
|
||||
self.logger.warning(f"No se pudo agregar '{line}' como ecuación: {eq_error}")
|
||||
|
||||
output = f"{var_name} = {result_obj}"
|
||||
|
||||
# Devolver el resultado de la asignación
|
||||
|
@ -582,68 +514,32 @@ class PureAlgebraicEngine:
|
|||
except Exception as e:
|
||||
return f"Error resolviendo sistema: {e}"
|
||||
elif len(args) == 1 and hasattr(args[0], 'is_Symbol') and args[0].is_Symbol:
|
||||
# solve(variable) - resolver para una variable específica
|
||||
# solve(variable) - resolver para una variable específica y auto-aplicar
|
||||
var_symbol = args[0]
|
||||
solution_value = self._solve_for_variable(var_symbol)
|
||||
|
||||
# Si encontramos una solución diferente de la variable
|
||||
# Si encontramos una solución, auto-aplicar al sistema
|
||||
if solution_value != var_symbol:
|
||||
# Verificar si la solución contiene otras variables
|
||||
free_symbols = solution_value.free_symbols
|
||||
unresolved_vars = free_symbols - {var_symbol}
|
||||
# Resolver iterativamente para obtener el valor más simplificado
|
||||
final_value = self._resolve_iteratively(solution_value)
|
||||
|
||||
if unresolved_vars:
|
||||
# Hay variables sin resolver, verificar si tienen valores numéricos
|
||||
has_numeric_values = True
|
||||
for var_sym in unresolved_vars:
|
||||
var_name = str(var_sym)
|
||||
if var_name in self.symbol_table:
|
||||
value = self.symbol_table[var_name]
|
||||
# Si el valor es simbólico (no numérico), no podemos resolver completamente
|
||||
if not (hasattr(value, 'is_number') and value.is_number):
|
||||
has_numeric_values = False
|
||||
break
|
||||
else:
|
||||
# Variable no tiene valor asignado
|
||||
has_numeric_values = False
|
||||
break
|
||||
|
||||
if has_numeric_values:
|
||||
# Todas las variables tienen valores numéricos, resolver iterativamente
|
||||
final_value = self._resolve_iteratively(solution_value)
|
||||
|
||||
# Auto-aplicar la solución numérica al sistema
|
||||
if final_value != var_symbol and not str(final_value) in ['True', 'False']:
|
||||
self._auto_apply_solution(var_symbol, final_value)
|
||||
|
||||
return Eq(var_symbol, final_value)
|
||||
else:
|
||||
# Hay variables con valores simbólicos, intentar resolver lo máximo posible
|
||||
# Aplicar resolución iterativa parcial
|
||||
resolved_solution = self._resolve_iteratively(solution_value)
|
||||
result_eq = Eq(var_symbol, resolved_solution)
|
||||
return result_eq
|
||||
else:
|
||||
# No hay variables sin resolver, intentar resolver completamente
|
||||
final_value = self._resolve_iteratively(solution_value)
|
||||
|
||||
# Auto-aplicar la solución al sistema solo si es un valor específico
|
||||
if final_value != var_symbol and not str(final_value) in ['True', 'False']:
|
||||
self._auto_apply_solution(var_symbol, final_value)
|
||||
|
||||
return Eq(var_symbol, final_value)
|
||||
# Auto-aplicar la solución al sistema
|
||||
self._auto_apply_solution(var_symbol, final_value)
|
||||
|
||||
# Verificar que el resultado no sea problemático
|
||||
if final_value == var_symbol or str(final_value) in ['True', 'False']:
|
||||
return var_symbol
|
||||
|
||||
return Eq(var_symbol, final_value)
|
||||
else:
|
||||
# Si no hay solución en las ecuaciones, verificar en symbol_table
|
||||
var_name = str(var_symbol)
|
||||
if var_name in self.symbol_table:
|
||||
value = self.symbol_table[var_name]
|
||||
# Si el valor en symbol_table es diferente de la variable, devolverlo
|
||||
if value != var_symbol:
|
||||
final_value = self._resolve_iteratively(value)
|
||||
return Eq(var_symbol, final_value)
|
||||
|
||||
# Si no hay información, devolver la variable tal como está
|
||||
return var_symbol
|
||||
final_value = self._resolve_iteratively(value)
|
||||
return Eq(var_symbol, final_value)
|
||||
else:
|
||||
return var_symbol
|
||||
else:
|
||||
# solve() con argumentos específicos (múltiples variables, ecuaciones, etc.)
|
||||
return solve(*args, **kwargs)
|
||||
|
@ -654,7 +550,13 @@ class PureAlgebraicEngine:
|
|||
return var_symbol
|
||||
|
||||
try:
|
||||
# 1. Buscar ecuaciones que contengan esta variable PRIMERO
|
||||
# 1. Buscar si la variable tiene asignación directa en symbol_table
|
||||
var_name = str(var_symbol)
|
||||
if var_name in self.symbol_table:
|
||||
# Devolver el valor de la asignación directa
|
||||
return self.symbol_table[var_name]
|
||||
|
||||
# 2. Buscar ecuaciones que contengan esta variable
|
||||
relevant_eqs = [eq for eq in self.equations if var_symbol in eq.free_symbols]
|
||||
if relevant_eqs:
|
||||
# Estrategia 1: Buscar asignación directa
|
||||
|
@ -683,8 +585,7 @@ class PureAlgebraicEngine:
|
|||
# en términos de otras variables
|
||||
try:
|
||||
# Obtener todas las variables del sistema excepto la que queremos resolver
|
||||
# Convertir todas las variables a símbolos para sympy
|
||||
all_vars = [sp.Symbol(v) if isinstance(v, str) else v for v in self.variables]
|
||||
all_vars = list(self.variables)
|
||||
other_vars = [v for v in all_vars if v != var_symbol]
|
||||
|
||||
if other_vars:
|
||||
|
@ -723,15 +624,7 @@ class PureAlgebraicEngine:
|
|||
except:
|
||||
pass
|
||||
|
||||
# 5. Si no hay ecuaciones relevantes, buscar en symbol_table como último recurso
|
||||
var_name = str(var_symbol)
|
||||
if var_name in self.symbol_table:
|
||||
value = self.symbol_table[var_name]
|
||||
# Solo devolver si el valor no es la misma variable (evitar bucles)
|
||||
if value != var_symbol:
|
||||
return value
|
||||
|
||||
# 6. Si nada funciona, devolver la variable tal como está
|
||||
# 5. Si nada funciona, devolver la variable tal como está
|
||||
return var_symbol
|
||||
|
||||
except Exception as e:
|
|
@ -1,12 +1,12 @@
|
|||
# requirements.txt
|
||||
# Calculadora MAV - CAS Híbrido con PySide6 y MathJax
|
||||
# Calculadora MAV - CAS Híbrido
|
||||
# Dependencias requeridas
|
||||
|
||||
# Motor algebraico principal
|
||||
sympy>=1.12
|
||||
|
||||
# Interfaz gráfica moderna
|
||||
PySide6>=6.6.0
|
||||
# Interfaz gráfica (generalmente incluido con Python)
|
||||
# tkinter - incluido con Python estándar
|
||||
|
||||
# Plotting y visualización
|
||||
matplotlib>=3.7.0
|
||||
|
|
|
@ -22,7 +22,7 @@ from datetime import datetime
|
|||
from pathlib import Path
|
||||
|
||||
# Importar motores de evaluación
|
||||
from .main_evaluation import HybridEvaluationEngine
|
||||
from main_evaluation_OLD import HybridEvaluationEngine
|
||||
|
||||
|
||||
def run_debug(input_file: str, output_file: str = None, verbose: bool = False):
|
||||
|
@ -59,7 +59,7 @@ def run_debug(input_file: str, output_file: str = None, verbose: bool = False):
|
|||
|
||||
# Crear motor de evaluación según el módulo especificado
|
||||
if engine_module == 'main_evaluation_puro':
|
||||
from .main_evaluation import PureAlgebraicEngine
|
||||
from main_evaluation_puro import PureAlgebraicEngine
|
||||
engine = PureAlgebraicEngine()
|
||||
else:
|
||||
# Motor por defecto
|
|
@ -5,7 +5,7 @@ import sympy
|
|||
from sympy import Basic, Symbol, sympify
|
||||
from typing import Any, Optional, Dict
|
||||
import re
|
||||
from .class_base import ClassBase
|
||||
from class_base import ClassBase
|
||||
|
||||
|
||||
class SympyClassBase(ClassBase, sympy.Basic):
|
|
@ -0,0 +1,37 @@
|
|||
import webview
|
||||
|
||||
def create_webview_app():
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
||||
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
|
||||
<script>
|
||||
MathJax = {
|
||||
tex: { inlineMath: [['$', '$']], displayMath: [['$$', '$$']] }
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.demo { background: #f0f0f0; padding: 10px; margin: 10px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>MathJax con webview</h1>
|
||||
<div class="demo">
|
||||
<p>Ecuación simple: $x^2 + y^2 = z^2$</p>
|
||||
<p>Ecuación compleja:</p>
|
||||
$$\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}$$
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
webview.create_window('MathJax App', html=html_content, width=800, height=600)
|
||||
webview.start()
|
||||
|
||||
|
||||
# Función principal para elegir método
|
||||
if __name__ == "__main__":
|
||||
create_webview_app()
|
|
@ -0,0 +1,669 @@
|
|||
"""
|
||||
Sistema de resultados interactivos con tags clickeables - VERSIÓN CORREGIDA
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import Toplevel, scrolledtext
|
||||
import sympy
|
||||
from typing import Any, Optional, Dict, List, Tuple
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
import numpy as np
|
||||
|
||||
|
||||
class PlotResult:
|
||||
"""Placeholder para resultados de plotting - DEFINICIÓN PRINCIPAL"""
|
||||
|
||||
def __init__(self, plot_type: str, args: tuple, kwargs: dict, original_expression: str = None):
|
||||
self.plot_type = plot_type
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.original_expression = original_expression or ""
|
||||
|
||||
def __str__(self):
|
||||
return f"📊 Ver {self.plot_type.title()}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"PlotResult('{self.plot_type}', {self.args}, {self.kwargs})"
|
||||
|
||||
|
||||
class InteractiveResultManager:
|
||||
"""Maneja resultados interactivos con ventanas emergentes"""
|
||||
|
||||
def __init__(self, parent_window: tk.Tk):
|
||||
self.parent = parent_window
|
||||
self.open_windows: Dict[str, Toplevel] = {}
|
||||
self.update_input_callback = None # Callback para actualizar el panel de entrada
|
||||
|
||||
def set_update_callback(self, callback):
|
||||
"""Establece el callback para actualizar el panel de entrada"""
|
||||
self.update_input_callback = callback
|
||||
|
||||
def create_interactive_tag(self, result: Any, text_widget: tk.Text) -> Optional[Tuple[str, str]]:
|
||||
"""
|
||||
Crea un tag interactivo para un resultado si es necesario
|
||||
|
||||
Returns:
|
||||
(tag_name, display_text) si se creó tag, None si no es necesario
|
||||
"""
|
||||
tag_name = None
|
||||
display_text = ""
|
||||
|
||||
# 🔧 CORRECCIÓN: Verificar con isinstance correcto
|
||||
if isinstance(result, PlotResult):
|
||||
tag_name = f"plot_{id(result)}"
|
||||
display_text = f"📊 Ver {result.plot_type.title()}"
|
||||
|
||||
elif isinstance(result, sympy.Matrix):
|
||||
tag_name = f"matrix_{id(result)}"
|
||||
rows, cols = result.shape
|
||||
display_text = f"📋 Ver Matriz {rows}×{cols}"
|
||||
|
||||
elif isinstance(result, list) and len(result) > 5:
|
||||
tag_name = f"list_{id(result)}"
|
||||
display_text = f"📋 Ver Lista ({len(result)} elementos)"
|
||||
|
||||
elif isinstance(result, dict) and len(result) > 3:
|
||||
tag_name = f"dict_{id(result)}"
|
||||
display_text = f"🔍 Ver Diccionario ({len(result)} entradas)"
|
||||
|
||||
elif hasattr(result, '__dict__') and len(str(result)) > 100:
|
||||
tag_name = f"object_{id(result)}"
|
||||
display_text = f"🔍 Ver Detalles ({type(result).__name__})"
|
||||
|
||||
# 🔧 CORRECCIÓN: Solo crear tag si se encontró un tipo interactivo
|
||||
if tag_name and display_text:
|
||||
try:
|
||||
# Configurar tag
|
||||
text_widget.tag_configure(
|
||||
tag_name,
|
||||
foreground="#4fc3f7",
|
||||
underline=True,
|
||||
font=("Consolas", 11, "underline")
|
||||
)
|
||||
|
||||
# Bind click event
|
||||
text_widget.tag_bind(
|
||||
tag_name,
|
||||
"<Button-1>",
|
||||
lambda e, r=result: self._handle_interactive_click(r)
|
||||
)
|
||||
|
||||
text_widget.tag_bind(
|
||||
tag_name,
|
||||
"<Enter>",
|
||||
lambda e: text_widget.config(cursor="hand2")
|
||||
)
|
||||
|
||||
text_widget.tag_bind(
|
||||
tag_name,
|
||||
"<Leave>",
|
||||
lambda e: text_widget.config(cursor="")
|
||||
)
|
||||
|
||||
return (tag_name, display_text)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error creando tag interactivo: {e}")
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def _handle_interactive_click(self, result: Any):
|
||||
"""Maneja clicks en elementos interactivos"""
|
||||
window_key = f"{type(result).__name__}_{id(result)}"
|
||||
|
||||
# Si ya existe la ventana, enfocarla
|
||||
if window_key in self.open_windows:
|
||||
window = self.open_windows[window_key]
|
||||
try:
|
||||
if window.winfo_exists():
|
||||
window.lift()
|
||||
window.focus_set()
|
||||
return
|
||||
else:
|
||||
del self.open_windows[window_key]
|
||||
except tk.TclError:
|
||||
del self.open_windows[window_key]
|
||||
|
||||
# Crear nueva ventana
|
||||
try:
|
||||
if isinstance(result, PlotResult):
|
||||
self._show_plot_window(result, window_key)
|
||||
elif isinstance(result, sympy.Matrix):
|
||||
self._show_matrix_window(result, window_key)
|
||||
elif isinstance(result, list):
|
||||
self._show_list_window(result, window_key)
|
||||
elif isinstance(result, dict):
|
||||
self._show_dict_window(result, window_key)
|
||||
else:
|
||||
self._show_object_window(result, window_key)
|
||||
except Exception as e:
|
||||
print(f"❌ Error abriendo ventana interactiva: {e}")
|
||||
|
||||
def _show_plot_window(self, plot_result: PlotResult, window_key: str):
|
||||
"""Muestra ventana con plot matplotlib e interfaz de edición"""
|
||||
# Asegurar que las dimensiones de la ventana principal estén actualizadas
|
||||
self.parent.update_idletasks()
|
||||
parent_x = self.parent.winfo_x()
|
||||
parent_y = self.parent.winfo_y()
|
||||
parent_width = self.parent.winfo_width()
|
||||
parent_height = self.parent.winfo_height()
|
||||
|
||||
# Definir dimensiones y posición para la ventana del plot
|
||||
plot_window_width = 700 # Aumentado para dar espacio al campo de edición
|
||||
plot_window_height = parent_height # Misma altura que la ventana principal
|
||||
|
||||
# Posicionar la ventana del plot a la derecha de la ventana principal
|
||||
plot_window_x = parent_x + parent_width
|
||||
plot_window_y = parent_y # Misma posición Y que la ventana principal
|
||||
|
||||
window_title = f"Plot - {plot_result.plot_type}"
|
||||
|
||||
# Crear la ventana base especificando la posición
|
||||
window = self._create_base_window(
|
||||
window_title,
|
||||
width=plot_window_width,
|
||||
height=plot_window_height,
|
||||
pos_x=plot_window_x,
|
||||
pos_y=plot_window_y
|
||||
)
|
||||
self.open_windows[window_key] = window
|
||||
|
||||
# Frame principal para organizar la ventana
|
||||
main_frame = tk.Frame(window, bg="#2b2b2b")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Frame superior para el campo de edición
|
||||
edit_frame = tk.Frame(main_frame, bg="#2b2b2b")
|
||||
edit_frame.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
# Label para el campo de edición
|
||||
tk.Label(
|
||||
edit_frame,
|
||||
text="Expresión:",
|
||||
bg="#2b2b2b",
|
||||
fg="#d4d4d4",
|
||||
font=("Consolas", 10)
|
||||
).pack(side=tk.LEFT)
|
||||
|
||||
# Campo de entrada para editar la expresión
|
||||
self.current_expression = tk.StringVar()
|
||||
self.current_expression.set(plot_result.original_expression)
|
||||
|
||||
expression_entry = tk.Entry(
|
||||
edit_frame,
|
||||
textvariable=self.current_expression,
|
||||
bg="#1e1e1e",
|
||||
fg="#d4d4d4",
|
||||
font=("Consolas", 11),
|
||||
insertbackground="#ffffff"
|
||||
)
|
||||
expression_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 5))
|
||||
|
||||
# Botón para redibujar
|
||||
redraw_btn = tk.Button(
|
||||
edit_frame,
|
||||
text="Redibujar",
|
||||
command=lambda: self._redraw_plot(plot_result, canvas_frame, expression_entry.get()),
|
||||
bg="#4fc3f7",
|
||||
fg="white",
|
||||
font=("Consolas", 9),
|
||||
relief=tk.FLAT
|
||||
)
|
||||
redraw_btn.pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
# Frame para el canvas del plot
|
||||
canvas_frame = tk.Frame(main_frame, bg="#2b2b2b")
|
||||
canvas_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||
|
||||
# Configurar el protocolo de cierre para guardar la expresión editada
|
||||
def on_window_close():
|
||||
edited_expression = expression_entry.get().strip()
|
||||
original_expression = plot_result.original_expression.strip()
|
||||
|
||||
# Si la expresión cambió y tenemos un callback, actualizar el panel de entrada
|
||||
if edited_expression != original_expression and self.update_input_callback:
|
||||
self.update_input_callback(original_expression, edited_expression)
|
||||
|
||||
# Limpiar la ventana del registro
|
||||
if window_key in self.open_windows:
|
||||
del self.open_windows[window_key]
|
||||
|
||||
# Cerrar la ventana
|
||||
window.destroy()
|
||||
|
||||
window.protocol("WM_DELETE_WINDOW", on_window_close)
|
||||
|
||||
# Crear el plot inicial
|
||||
self._create_plot_in_frame(plot_result, canvas_frame)
|
||||
|
||||
# Hacer focus en el campo de entrada para edición inmediata
|
||||
expression_entry.focus_set()
|
||||
expression_entry.select_range(0, tk.END)
|
||||
|
||||
def _redraw_plot(self, plot_result: PlotResult, canvas_frame: tk.Frame, new_expression: str):
|
||||
"""Redibuja el plot con una nueva expresión"""
|
||||
try:
|
||||
# Limpiar el frame actual
|
||||
for widget in canvas_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
# Evaluar la nueva expresión
|
||||
import sympy as sp
|
||||
|
||||
# Crear contexto básico para evaluación
|
||||
eval_context = {
|
||||
'sin': sp.sin, 'cos': sp.cos, 'tan': sp.tan,
|
||||
'exp': sp.exp, 'log': sp.log, 'sqrt': sp.sqrt,
|
||||
'pi': sp.pi, 'e': sp.E, 'x': sp.Symbol('x'), 'y': sp.Symbol('y'),
|
||||
'z': sp.Symbol('z'), 't': sp.Symbol('t')
|
||||
}
|
||||
|
||||
# Evaluar la expresión
|
||||
new_expr = sp.sympify(new_expression, locals=eval_context)
|
||||
|
||||
# Crear nuevo PlotResult con la expresión actualizada
|
||||
new_plot_result = PlotResult(
|
||||
plot_result.plot_type,
|
||||
(new_expr,) + plot_result.args[1:], # Mantener argumentos adicionales
|
||||
plot_result.kwargs,
|
||||
new_expression
|
||||
)
|
||||
|
||||
# Actualizar la expresión original en el objeto
|
||||
plot_result.original_expression = new_expression
|
||||
|
||||
# Redibujar
|
||||
self._create_plot_in_frame(new_plot_result, canvas_frame)
|
||||
|
||||
except Exception as e:
|
||||
# Mostrar error en el frame
|
||||
error_label = tk.Label(
|
||||
canvas_frame,
|
||||
text=f"Error en expresión: {e}",
|
||||
fg="#f44747",
|
||||
bg="#2b2b2b",
|
||||
font=("Consolas", 11),
|
||||
wraplength=600
|
||||
)
|
||||
error_label.pack(pady=20)
|
||||
|
||||
def _create_plot_in_frame(self, plot_result: PlotResult, parent_frame: tk.Frame):
|
||||
"""Crea el plot dentro del frame especificado"""
|
||||
try:
|
||||
fig, ax = plt.subplots(figsize=(8, 6))
|
||||
|
||||
if plot_result.plot_type == "plot":
|
||||
self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs)
|
||||
elif plot_result.plot_type == "plot3d":
|
||||
self._create_3d_plot(fig, plot_result.args, plot_result.kwargs)
|
||||
|
||||
# Embed en tkinter
|
||||
canvas = FigureCanvasTkAgg(fig, parent_frame)
|
||||
canvas.draw()
|
||||
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Toolbar para interactividad
|
||||
try:
|
||||
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
|
||||
toolbar = NavigationToolbar2Tk(canvas, parent_frame)
|
||||
toolbar.update()
|
||||
except ImportError:
|
||||
pass # Si no está disponible, continuar sin toolbar
|
||||
|
||||
except Exception as e:
|
||||
error_label = tk.Label(
|
||||
parent_frame,
|
||||
text=f"Error generando plot: {e}",
|
||||
fg="#f44747",
|
||||
bg="#2b2b2b",
|
||||
font=("Consolas", 12)
|
||||
)
|
||||
error_label.pack(pady=20)
|
||||
|
||||
def _create_2d_plot(self, fig, ax, args, kwargs):
|
||||
"""Crea plot 2D usando SymPy"""
|
||||
if len(args) >= 1:
|
||||
expr = args[0]
|
||||
|
||||
try:
|
||||
if len(args) >= 2:
|
||||
# Rango especificado: (variable, start, end)
|
||||
var_range = args[1]
|
||||
if isinstance(var_range, tuple) and len(var_range) == 3:
|
||||
var, start, end = var_range
|
||||
x_vals = np.linspace(float(start), float(end), 1000)
|
||||
|
||||
# Evaluar expresión
|
||||
f = sympy.lambdify(var, expr, 'numpy')
|
||||
y_vals = f(x_vals)
|
||||
|
||||
ax.plot(x_vals, y_vals, **kwargs)
|
||||
ax.set_xlabel(str(var))
|
||||
ax.set_ylabel(str(expr))
|
||||
ax.grid(True)
|
||||
ax.set_title(f"Plot: {expr}")
|
||||
else:
|
||||
# Rango por defecto
|
||||
free_symbols = list(expr.free_symbols)
|
||||
if free_symbols:
|
||||
var = free_symbols[0]
|
||||
x_vals = np.linspace(-10, 10, 1000)
|
||||
f = sympy.lambdify(var, expr, 'numpy')
|
||||
y_vals = f(x_vals)
|
||||
|
||||
ax.plot(x_vals, y_vals, **kwargs)
|
||||
ax.set_xlabel(str(var))
|
||||
ax.set_ylabel(str(expr))
|
||||
ax.grid(True)
|
||||
ax.set_title(f"Plot: {expr}")
|
||||
|
||||
except Exception as e:
|
||||
ax.text(0.5, 0.5, f"Error: {e}",
|
||||
transform=ax.transAxes, ha='center', va='center')
|
||||
ax.set_title("Error en Plot")
|
||||
|
||||
def _create_3d_plot(self, fig, args, kwargs):
|
||||
"""Crea plot 3D"""
|
||||
try:
|
||||
ax = fig.add_subplot(111, projection='3d')
|
||||
|
||||
if len(args) >= 3:
|
||||
expr = args[0]
|
||||
x_range = args[1] # (x, x_start, x_end)
|
||||
y_range = args[2] # (y, y_start, y_end)
|
||||
|
||||
if isinstance(x_range, tuple) and isinstance(y_range, tuple):
|
||||
x_var, x_start, x_end = x_range
|
||||
y_var, y_start, y_end = y_range
|
||||
|
||||
x_vals = np.linspace(float(x_start), float(x_end), 50)
|
||||
y_vals = np.linspace(float(y_start), float(y_end), 50)
|
||||
X, Y = np.meshgrid(x_vals, y_vals)
|
||||
|
||||
f = sympy.lambdify([x_var, y_var], expr, 'numpy')
|
||||
Z = f(X, Y)
|
||||
|
||||
ax.plot_surface(X, Y, Z, **kwargs)
|
||||
ax.set_xlabel(str(x_var))
|
||||
ax.set_ylabel(str(y_var))
|
||||
ax.set_zlabel(str(expr))
|
||||
ax.set_title(f"3D Plot: {expr}")
|
||||
|
||||
except Exception as e:
|
||||
ax.text2D(0.5, 0.5, f"Error: {e}", transform=ax.transAxes)
|
||||
|
||||
def _show_matrix_window(self, matrix: sympy.Matrix, window_key: str):
|
||||
"""Muestra ventana con matriz formateada"""
|
||||
rows, cols = matrix.shape
|
||||
window_title = f"Matriz {rows}×{cols}"
|
||||
|
||||
# Asegurar que las dimensiones de la ventana principal estén actualizadas
|
||||
self.parent.update_idletasks()
|
||||
parent_x = self.parent.winfo_x()
|
||||
parent_y = self.parent.winfo_y()
|
||||
parent_width = self.parent.winfo_width()
|
||||
parent_height = self.parent.winfo_height()
|
||||
|
||||
# Definir dimensiones y posición para la ventana de la matriz
|
||||
matrix_window_width = 600 # Ancho deseado
|
||||
matrix_window_height = parent_height # Misma altura que la ventana principal
|
||||
matrix_window_x = parent_x + parent_width # A la derecha
|
||||
matrix_window_y = parent_y # Misma posición Y
|
||||
|
||||
window = self._create_base_window(window_title, width=matrix_window_width, height=matrix_window_height,
|
||||
pos_x=matrix_window_x, pos_y=matrix_window_y)
|
||||
self.open_windows[window_key] = window
|
||||
|
||||
# Crear frame con scroll
|
||||
frame = tk.Frame(window, bg="#2b2b2b")
|
||||
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
text_widget = scrolledtext.ScrolledText(
|
||||
frame,
|
||||
font=("Courier New", 12),
|
||||
bg="#1e1e1e",
|
||||
fg="#d4d4d4",
|
||||
insertbackground="#ffffff",
|
||||
wrap=tk.NONE
|
||||
)
|
||||
text_widget.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Formatear matriz
|
||||
matrix_str = self._format_matrix(matrix)
|
||||
text_widget.insert("1.0", matrix_str)
|
||||
text_widget.config(state="disabled")
|
||||
|
||||
# Botones de utilidad
|
||||
button_frame = tk.Frame(window, bg="#2b2b2b")
|
||||
button_frame.pack(fill=tk.X, padx=10, pady=5)
|
||||
|
||||
try:
|
||||
det_btn = tk.Button(
|
||||
button_frame,
|
||||
text="Determinante",
|
||||
command=lambda: self._show_matrix_property(matrix, "determinante", matrix.det()),
|
||||
bg="#3c3c3c",
|
||||
fg="white"
|
||||
)
|
||||
det_btn.pack(side=tk.LEFT, padx=5)
|
||||
except:
|
||||
pass # Skip si la matriz no es cuadrada
|
||||
|
||||
if matrix.is_square:
|
||||
try:
|
||||
inv_btn = tk.Button(
|
||||
button_frame,
|
||||
text="Inversa",
|
||||
command=lambda: self._show_matrix_property(matrix, "inversa", matrix.inv()),
|
||||
bg="#3c3c3c",
|
||||
fg="white"
|
||||
)
|
||||
inv_btn.pack(side=tk.LEFT, padx=5)
|
||||
except:
|
||||
pass # Skip si no es invertible
|
||||
|
||||
def _format_matrix(self, matrix: sympy.Matrix) -> str:
|
||||
"""Formatea una matriz para display"""
|
||||
rows, cols = matrix.shape
|
||||
|
||||
# Calcular ancho máximo de elementos
|
||||
max_width = 0
|
||||
for i in range(rows):
|
||||
for j in range(cols):
|
||||
element_str = str(matrix[i, j])
|
||||
max_width = max(max_width, len(element_str))
|
||||
|
||||
max_width = max(max_width, 8) # Mínimo 8 caracteres
|
||||
|
||||
# Construir representación
|
||||
lines = []
|
||||
lines.append("┌" + " " * (max_width * cols + cols - 1) + "┐")
|
||||
|
||||
for i in range(rows):
|
||||
line = "│"
|
||||
for j in range(cols):
|
||||
element_str = str(matrix[i, j])
|
||||
padded = element_str.center(max_width)
|
||||
line += padded
|
||||
if j < cols - 1:
|
||||
line += " "
|
||||
line += "│"
|
||||
lines.append(line)
|
||||
|
||||
lines.append("└" + " " * (max_width * cols + cols - 1) + "┘")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _show_matrix_property(self, matrix: sympy.Matrix, prop_name: str, prop_value: Any):
|
||||
"""Muestra propiedad de matriz en ventana separada"""
|
||||
prop_window = self._create_base_window(f"Matriz - {prop_name.title()}", width=400, height=300)
|
||||
|
||||
text_widget = scrolledtext.ScrolledText(
|
||||
prop_window,
|
||||
font=("Courier New", 12),
|
||||
bg="#1e1e1e",
|
||||
fg="#d4d4d4",
|
||||
insertbackground="#ffffff"
|
||||
)
|
||||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
if isinstance(prop_value, sympy.Matrix):
|
||||
content = f"{prop_name.title()}:\n\n{self._format_matrix(prop_value)}"
|
||||
else:
|
||||
content = f"{prop_name.title()}: {prop_value}"
|
||||
|
||||
text_widget.insert("1.0", content)
|
||||
text_widget.config(state="disabled")
|
||||
|
||||
def _show_list_window(self, lst: list, window_key: str):
|
||||
"""Muestra ventana con lista expandida"""
|
||||
window_title = f"Lista ({len(lst)} elementos)"
|
||||
|
||||
# Asegurar que las dimensiones de la ventana principal estén actualizadas
|
||||
self.parent.update_idletasks()
|
||||
parent_x = self.parent.winfo_x()
|
||||
parent_y = self.parent.winfo_y()
|
||||
parent_width = self.parent.winfo_width()
|
||||
parent_height = self.parent.winfo_height()
|
||||
|
||||
# Definir dimensiones y posición para la ventana de la lista
|
||||
list_window_width = 500 # Ancho deseado
|
||||
list_window_height = parent_height # Misma altura que la ventana principal
|
||||
list_window_x = parent_x + parent_width # A la derecha
|
||||
list_window_y = parent_y # Misma posición Y
|
||||
|
||||
window = self._create_base_window(window_title, width=list_window_width, height=list_window_height,
|
||||
pos_x=list_window_x, pos_y=list_window_y)
|
||||
self.open_windows[window_key] = window
|
||||
|
||||
text_widget = scrolledtext.ScrolledText(
|
||||
window,
|
||||
font=("Consolas", 11),
|
||||
bg="#1e1e1e",
|
||||
fg="#d4d4d4",
|
||||
insertbackground="#ffffff"
|
||||
)
|
||||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
content = "Elementos de la lista:\n\n"
|
||||
for i, item in enumerate(lst):
|
||||
content += f"[{i}] {item}\n"
|
||||
|
||||
text_widget.insert("1.0", content)
|
||||
text_widget.config(state="disabled")
|
||||
|
||||
def _show_dict_window(self, dct: dict, window_key: str):
|
||||
"""Muestra ventana con diccionario expandido"""
|
||||
window = self._create_base_window(f"Diccionario ({len(dct)} entradas)", width=500, height=400)
|
||||
self.open_windows[window_key] = window
|
||||
|
||||
text_widget = scrolledtext.ScrolledText(
|
||||
window,
|
||||
font=("Consolas", 11),
|
||||
bg="#1e1e1e",
|
||||
fg="#d4d4d4",
|
||||
insertbackground="#ffffff"
|
||||
)
|
||||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
content = "Entradas del diccionario:\n\n"
|
||||
for key, value in dct.items():
|
||||
content += f"{key}: {value}\n"
|
||||
|
||||
text_widget.insert("1.0", content)
|
||||
text_widget.config(state="disabled")
|
||||
|
||||
def _show_object_window(self, obj: Any, window_key: str):
|
||||
"""Muestra ventana con detalles de objeto"""
|
||||
window = self._create_base_window(f"Objeto - {type(obj).__name__}", width=600, height=500)
|
||||
self.open_windows[window_key] = window
|
||||
|
||||
text_widget = scrolledtext.ScrolledText(
|
||||
window,
|
||||
font=("Consolas", 11),
|
||||
bg="#1e1e1e",
|
||||
fg="#d4d4d4",
|
||||
insertbackground="#ffffff"
|
||||
)
|
||||
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
content = f"Objeto: {type(obj).__name__}\n\n"
|
||||
content += f"Valor: {obj}\n\n"
|
||||
content += f"Representación: {repr(obj)}\n\n"
|
||||
|
||||
if hasattr(obj, '__dict__'):
|
||||
content += "Atributos:\n"
|
||||
for attr, value in obj.__dict__.items():
|
||||
content += f" {attr}: {value}\n"
|
||||
|
||||
content += "\nMétodos disponibles:\n"
|
||||
for attr in dir(obj):
|
||||
if not attr.startswith('_') and callable(getattr(obj, attr, None)):
|
||||
content += f" {attr}()\n"
|
||||
|
||||
text_widget.insert("1.0", content)
|
||||
text_widget.config(state="disabled")
|
||||
|
||||
def _create_base_window(self,
|
||||
title: str,
|
||||
width: int = 500,
|
||||
height: int = 400,
|
||||
pos_x: Optional[int] = None,
|
||||
pos_y: Optional[int] = None) -> Toplevel:
|
||||
"""Crea ventana base con estilo consistente y posición opcional"""
|
||||
window = Toplevel(self.parent)
|
||||
window.title(title)
|
||||
window.configure(bg="#2b2b2b")
|
||||
window.transient(self.parent) # Hace que la ventana aparezca encima del padre
|
||||
|
||||
# Construir la cadena de geometría completa WxH+X+Y
|
||||
geometry_str = f"{width}x{height}"
|
||||
|
||||
if pos_x is not None and pos_y is not None:
|
||||
# Usar posición provista, asegurándose de que no sea negativa
|
||||
final_x = max(0, pos_x)
|
||||
final_y = max(0, pos_y)
|
||||
geometry_str += f"+{final_x}+{final_y}"
|
||||
else:
|
||||
# Centrar ventana si no se especifica posición
|
||||
# Para centrar, necesitamos las dimensiones de la pantalla
|
||||
# y las dimensiones de la ventana (width, height ya las tenemos)
|
||||
screen_width = window.winfo_screenwidth()
|
||||
screen_height = window.winfo_screenheight()
|
||||
|
||||
center_x = (screen_width // 2) - (width // 2)
|
||||
center_y = (screen_height // 2) - (height // 2)
|
||||
final_x = max(0, center_x)
|
||||
final_y = max(0, center_y)
|
||||
geometry_str += f"+{final_x}+{final_y}"
|
||||
|
||||
window.geometry(geometry_str) # Aplicar tamaño y posición de una sola vez
|
||||
|
||||
return window
|
||||
|
||||
def close_all_windows(self):
|
||||
"""Cierra todas las ventanas interactivas de forma segura"""
|
||||
windows_to_close = list(self.open_windows.items())
|
||||
|
||||
for window_key, window in windows_to_close:
|
||||
try:
|
||||
if window and window.winfo_exists():
|
||||
# Forzar el cierre del protocolo de cierre si existe
|
||||
window.protocol("WM_DELETE_WINDOW", window.destroy)
|
||||
window.quit() # Detener el mainloop de la ventana si lo tiene
|
||||
window.destroy() # Destruir la ventana
|
||||
except tk.TclError:
|
||||
# La ventana ya fue destruida o no es válida
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Error cerrando ventana {window_key}: {e}")
|
||||
|
||||
# Limpiar el diccionario
|
||||
self.open_windows.clear()
|
||||
|
||||
# Cerrar todas las figuras de matplotlib para liberar memoria
|
||||
try:
|
||||
plt.close('all')
|
||||
except:
|
||||
pass
|
Loading…
Reference in New Issue