Compare commits
10 Commits
488f78b409
...
9ba73a9db6
Author | SHA1 | Date |
---|---|---|
|
9ba73a9db6 | |
|
aaddfbc3fa | |
|
4010de2c12 | |
|
68bed8937a | |
|
b7e35d1ae3 | |
|
242d5095af | |
|
efbc6a5b52 | |
|
bb82098608 | |
|
0629137956 | |
|
0cbf9dbf79 |
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
ex = (t * 8) / w
|
||||||
|
|
||||||
|
|
||||||
|
var1 = 2
|
||||||
|
var2 = 4
|
||||||
|
vatt1 = 4
|
||||||
|
|
||||||
|
IP4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
## Overview
|
## 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 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 application has this main components:
|
### The application has this main components:
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ 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
|
* 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.
|
* 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.
|
* 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.
|
* 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
|
## Application Architecture
|
||||||
|
|
BIN
MaVCalcv2.lnk
BIN
MaVCalcv2.lnk
Binary file not shown.
|
@ -1,127 +0,0 @@
|
||||||
# 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.
|
|
File diff suppressed because it is too large
Load Diff
|
@ -16,13 +16,13 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_SYMPY_HELPER = False
|
HAS_SYMPY_HELPER = False
|
||||||
|
|
||||||
from type_registry import (
|
from .type_registry import (
|
||||||
get_registered_base_context,
|
get_registered_base_context,
|
||||||
get_registered_tokenization_patterns,
|
get_registered_tokenization_patterns,
|
||||||
discover_and_register_types
|
discover_and_register_types
|
||||||
)
|
)
|
||||||
from tl_bracket_parser import BracketParser
|
from .tl_bracket_parser import BracketParser
|
||||||
from tl_popup import PlotResult
|
from .tl_popup import PlotResult
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -80,7 +80,6 @@ class PureAlgebraicEngine:
|
||||||
'sqrt': sp.sqrt, 'abs': sp.Abs,
|
'sqrt': sp.sqrt, 'abs': sp.Abs,
|
||||||
'pi': sp.pi, 'e': sp.E, 'I': sp.I,
|
'pi': sp.pi, 'e': sp.E, 'I': sp.I,
|
||||||
'oo': sp.oo, 'inf': sp.oo,
|
'oo': sp.oo, 'inf': sp.oo,
|
||||||
'solve': self._smart_solve,
|
|
||||||
'Eq': sp.Eq, 'simplify': sp.simplify,
|
'Eq': sp.Eq, 'simplify': sp.simplify,
|
||||||
'expand': sp.expand, 'factor': sp.factor,
|
'expand': sp.expand, 'factor': sp.factor,
|
||||||
'diff': sp.diff, 'integrate': sp.integrate,
|
'diff': sp.diff, 'integrate': sp.integrate,
|
||||||
|
@ -98,7 +97,55 @@ class PureAlgebraicEngine:
|
||||||
# 2. TIPOS PERSONALIZADOS REGISTRADOS (CLAVE PARA INSTANCIACIÓN)
|
# 2. TIPOS PERSONALIZADOS REGISTRADOS (CLAVE PARA INSTANCIACIÓN)
|
||||||
registered_types = get_registered_base_context()
|
registered_types = get_registered_base_context()
|
||||||
|
|
||||||
# 3. FUNCIONES DE PLOTTING (WRAPPED)
|
# 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)
|
||||||
# Wrappers para capturar llamadas de plot y devolver un objeto PlotResult
|
# Wrappers para capturar llamadas de plot y devolver un objeto PlotResult
|
||||||
def plot_wrapper(*args, **kwargs):
|
def plot_wrapper(*args, **kwargs):
|
||||||
# Intentar extraer la expresión original del primer argumento
|
# Intentar extraer la expresión original del primer argumento
|
||||||
|
@ -127,14 +174,15 @@ class PureAlgebraicEngine:
|
||||||
'plot3d_parametric_line': plot3d_parametric_line_wrapper,
|
'plot3d_parametric_line': plot3d_parametric_line_wrapper,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 4. COMBINAR TODO EN CONTEXTO UNIFICADO
|
# 5. COMBINAR TODO EN CONTEXTO UNIFICADO
|
||||||
self.unified_context = {
|
self.unified_context = {
|
||||||
**sympy_functions,
|
**sympy_functions,
|
||||||
**registered_types, # IP4, FourBytes, IntBase, etc.
|
**registered_types, # IP4, FourBytes, IntBase, etc.
|
||||||
**plotting_functions
|
**plotting_functions,
|
||||||
|
'solve': solve_wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
# 5. VERIFICAR CARGA DE TIPOS PRINCIPALES
|
# 6. VERIFICAR CARGA DE TIPOS PRINCIPALES
|
||||||
required_classes = ['IP4', 'IP4Mask', 'FourBytes', 'IntBase', 'Hex', 'Bin', 'Dec', 'Chr', 'LaTeX']
|
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]
|
missing_classes = [cls for cls in required_classes if cls not in self.unified_context]
|
||||||
|
|
||||||
|
@ -350,7 +398,7 @@ class PureAlgebraicEngine:
|
||||||
return EvaluationResult(line, error_msg, "error", False, str(e), is_solve_query=True)
|
return EvaluationResult(line, error_msg, "error", False, str(e), is_solve_query=True)
|
||||||
|
|
||||||
def _evaluate_assignment(self, line: str) -> EvaluationResult:
|
def _evaluate_assignment(self, line: str) -> EvaluationResult:
|
||||||
"""Evalúa una asignación"""
|
"""Evalúa una asignación CON FUNCIONALIDAD DUAL: asignación al contexto + ecuación a sympy"""
|
||||||
try:
|
try:
|
||||||
var_name, expression_str = line.split('=', 1)
|
var_name, expression_str = line.split('=', 1)
|
||||||
var_name = var_name.strip()
|
var_name = var_name.strip()
|
||||||
|
@ -367,10 +415,30 @@ class PureAlgebraicEngine:
|
||||||
eval_context = self._get_complete_context()
|
eval_context = self._get_complete_context()
|
||||||
result_obj = sympify(expression_str, locals=eval_context)
|
result_obj = sympify(expression_str, locals=eval_context)
|
||||||
|
|
||||||
# Actualizar tabla de símbolos
|
# 1. ASIGNACIÓN AL CONTEXTO (para evaluación directa)
|
||||||
self.symbol_table[var_name] = result_obj
|
self.symbol_table[var_name] = result_obj
|
||||||
self.variables.add(var_name)
|
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}"
|
output = f"{var_name} = {result_obj}"
|
||||||
|
|
||||||
# Devolver el resultado de la asignación
|
# Devolver el resultado de la asignación
|
||||||
|
@ -514,21 +582,54 @@ class PureAlgebraicEngine:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error resolviendo sistema: {e}"
|
return f"Error resolviendo sistema: {e}"
|
||||||
elif len(args) == 1 and hasattr(args[0], 'is_Symbol') and args[0].is_Symbol:
|
elif len(args) == 1 and hasattr(args[0], 'is_Symbol') and args[0].is_Symbol:
|
||||||
# solve(variable) - resolver para una variable específica y auto-aplicar
|
# solve(variable) - resolver para una variable específica
|
||||||
var_symbol = args[0]
|
var_symbol = args[0]
|
||||||
solution_value = self._solve_for_variable(var_symbol)
|
solution_value = self._solve_for_variable(var_symbol)
|
||||||
|
|
||||||
# Si encontramos una solución, auto-aplicar al sistema
|
# Si encontramos una solución diferente de la variable
|
||||||
if solution_value != var_symbol:
|
if solution_value != var_symbol:
|
||||||
# Resolver iterativamente para obtener el valor más simplificado
|
# Verificar si la solución contiene otras variables
|
||||||
|
free_symbols = solution_value.free_symbols
|
||||||
|
unresolved_vars = free_symbols - {var_symbol}
|
||||||
|
|
||||||
|
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)
|
final_value = self._resolve_iteratively(solution_value)
|
||||||
|
|
||||||
# Auto-aplicar la solución al sistema
|
# 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)
|
self._auto_apply_solution(var_symbol, final_value)
|
||||||
|
|
||||||
# Verificar que el resultado no sea problemático
|
return Eq(var_symbol, final_value)
|
||||||
if final_value == var_symbol or str(final_value) in ['True', 'False']:
|
else:
|
||||||
return var_symbol
|
# 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)
|
return Eq(var_symbol, final_value)
|
||||||
else:
|
else:
|
||||||
|
@ -536,9 +637,12 @@ class PureAlgebraicEngine:
|
||||||
var_name = str(var_symbol)
|
var_name = str(var_symbol)
|
||||||
if var_name in self.symbol_table:
|
if var_name in self.symbol_table:
|
||||||
value = self.symbol_table[var_name]
|
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)
|
final_value = self._resolve_iteratively(value)
|
||||||
return Eq(var_symbol, final_value)
|
return Eq(var_symbol, final_value)
|
||||||
else:
|
|
||||||
|
# Si no hay información, devolver la variable tal como está
|
||||||
return var_symbol
|
return var_symbol
|
||||||
else:
|
else:
|
||||||
# solve() con argumentos específicos (múltiples variables, ecuaciones, etc.)
|
# solve() con argumentos específicos (múltiples variables, ecuaciones, etc.)
|
||||||
|
@ -550,13 +654,7 @@ class PureAlgebraicEngine:
|
||||||
return var_symbol
|
return var_symbol
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Buscar si la variable tiene asignación directa en symbol_table
|
# 1. Buscar ecuaciones que contengan esta variable PRIMERO
|
||||||
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]
|
relevant_eqs = [eq for eq in self.equations if var_symbol in eq.free_symbols]
|
||||||
if relevant_eqs:
|
if relevant_eqs:
|
||||||
# Estrategia 1: Buscar asignación directa
|
# Estrategia 1: Buscar asignación directa
|
||||||
|
@ -585,7 +683,8 @@ class PureAlgebraicEngine:
|
||||||
# en términos de otras variables
|
# en términos de otras variables
|
||||||
try:
|
try:
|
||||||
# Obtener todas las variables del sistema excepto la que queremos resolver
|
# Obtener todas las variables del sistema excepto la que queremos resolver
|
||||||
all_vars = list(self.variables)
|
# 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]
|
||||||
other_vars = [v for v in all_vars if v != var_symbol]
|
other_vars = [v for v in all_vars if v != var_symbol]
|
||||||
|
|
||||||
if other_vars:
|
if other_vars:
|
||||||
|
@ -624,7 +723,15 @@ class PureAlgebraicEngine:
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 5. Si nada funciona, devolver la variable tal como está
|
# 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á
|
||||||
return var_symbol
|
return var_symbol
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
|
@ -22,7 +22,7 @@ from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Importar motores de evaluación
|
# Importar motores de evaluación
|
||||||
from main_evaluation_OLD import HybridEvaluationEngine
|
from .main_evaluation import HybridEvaluationEngine
|
||||||
|
|
||||||
|
|
||||||
def run_debug(input_file: str, output_file: str = None, verbose: bool = False):
|
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
|
# Crear motor de evaluación según el módulo especificado
|
||||||
if engine_module == 'main_evaluation_puro':
|
if engine_module == 'main_evaluation_puro':
|
||||||
from main_evaluation_puro import PureAlgebraicEngine
|
from .main_evaluation import PureAlgebraicEngine
|
||||||
engine = PureAlgebraicEngine()
|
engine = PureAlgebraicEngine()
|
||||||
else:
|
else:
|
||||||
# Motor por defecto
|
# Motor por defecto
|
|
@ -5,7 +5,7 @@ import sympy
|
||||||
from sympy import Basic, Symbol, sympify
|
from sympy import Basic, Symbol, sympify
|
||||||
from typing import Any, Optional, Dict
|
from typing import Any, Optional, Dict
|
||||||
import re
|
import re
|
||||||
from class_base import ClassBase
|
from .class_base import ClassBase
|
||||||
|
|
||||||
|
|
||||||
class SympyClassBase(ClassBase, sympy.Basic):
|
class SympyClassBase(ClassBase, sympy.Basic):
|
|
@ -0,0 +1,592 @@
|
||||||
|
"""
|
||||||
|
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
|
305
calc.py
305
calc.py
|
@ -1,270 +1,89 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Launcher principal para Calculadora MAV - CAS Híbrido
|
Launcher para Calculadora MAV - Versión PySide6 con MathJax
|
||||||
Este script maneja la inicialización y ejecución de la aplicación
|
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import messagebox
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
|
||||||
import traceback
|
|
||||||
import platform
|
|
||||||
import platform
|
|
||||||
|
|
||||||
|
def check_dependencies():
|
||||||
def setup_logging():
|
"""Verifica que todas las dependencias estén instaladas"""
|
||||||
"""Configura el sistema de logging completo"""
|
required_modules = [
|
||||||
MAX_LOG_FILES = 10 # Límite de archivos de log
|
'PySide6',
|
||||||
log_dir = Path("logs")
|
'sympy',
|
||||||
log_dir.mkdir(exist_ok=True)
|
'numpy',
|
||||||
|
'matplotlib'
|
||||||
# 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:
|
|
||||||
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__)
|
missing = []
|
||||||
|
for module in required_modules:
|
||||||
|
|
||||||
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:
|
try:
|
||||||
tb = traceback.extract_tb(error.__traceback__)
|
__import__(module)
|
||||||
if tb:
|
except ImportError:
|
||||||
last_frame = tb[-1]
|
missing.append(module)
|
||||||
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)
|
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 show_error_with_log_info(error, log_file, context=""):
|
def check_pyside6_webengine():
|
||||||
"""Muestra error al usuario con información del log"""
|
"""Verifica si PySide6 WebEngine está disponible"""
|
||||||
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:
|
try:
|
||||||
root = tk.Tk()
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
root.withdraw()
|
print("✅ PySide6 WebEngine disponible para MathJax")
|
||||||
|
return True
|
||||||
# Crear ventana de error personalizada
|
except ImportError:
|
||||||
error_window = tk.Toplevel(root)
|
print("⚠️ PySide6 WebEngine no disponible")
|
||||||
error_window.title("Error - Calculadora MAV")
|
print(" Instalando QtWebEngine...")
|
||||||
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:
|
try:
|
||||||
if platform.system() == "Windows":
|
subprocess.run([sys.executable, '-m', 'pip', 'install', 'PySide6-WebEngine'],
|
||||||
os.startfile(log_file.parent)
|
check=True, capture_output=True)
|
||||||
elif platform.system() == "Darwin": # macOS
|
print("✅ PySide6 WebEngine instalado correctamente")
|
||||||
subprocess.run(["open", str(log_file.parent)])
|
return True
|
||||||
else: # Linux
|
except subprocess.CalledProcessError:
|
||||||
subprocess.run(["xdg-open", str(log_file.parent)])
|
print("❌ No se pudo instalar PySide6 WebEngine")
|
||||||
except Exception:
|
return False
|
||||||
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():
|
def main():
|
||||||
"""Función principal del launcher"""
|
"""Función principal del launcher"""
|
||||||
global logger, log_file
|
print("🚀 Iniciando Calculadora MAV - Diseño Minimalista 3 Paneles")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
# Configurar logging al inicio
|
# Verificar dependencias
|
||||||
try:
|
if not check_dependencies():
|
||||||
logger, log_file = setup_logging()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
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)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Verificar WebEngine
|
||||||
|
if not check_pyside6_webengine():
|
||||||
|
print("⚠️ Continuando sin WebEngine (funcionalidad limitada)")
|
||||||
|
|
||||||
|
print("\n🎯 Iniciando aplicación...")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error inesperado: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Configurar logging básico para manejo de argumentos
|
|
||||||
temp_logger = None
|
|
||||||
temp_log_file = None
|
|
||||||
|
|
||||||
# Inicio normal
|
|
||||||
try:
|
|
||||||
main()
|
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
|
Class_LaTeX: Nueva instancia con la expresión parseada
|
||||||
|
|
||||||
Examples:
|
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)
|
# Primero intentar conversión manual mejorada (más confiable para subíndices)
|
||||||
try:
|
try:
|
||||||
|
@ -234,6 +234,7 @@ class Class_LaTeX(SympyClassBase):
|
||||||
(r'\\cdot', '*'), # multiplicación
|
(r'\\cdot', '*'), # multiplicación
|
||||||
(r'\\times', '*'), # multiplicación
|
(r'\\times', '*'), # multiplicación
|
||||||
(r'\\div', '/'), # divisió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
|
(r'\{([^}]*)\}', r'\1'), # eliminar llaves restantes
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -276,19 +277,26 @@ class Class_LaTeX(SympyClassBase):
|
||||||
symbols_dict[name] = sympy.Symbol(name)
|
symbols_dict[name] = sympy.Symbol(name)
|
||||||
|
|
||||||
if symbols_dict:
|
if symbols_dict:
|
||||||
# Si es una ecuación, intentar crearla manualmente
|
# Si es una ecuación, intentar parsearlo lado por lado
|
||||||
if is_equation and '=' in result:
|
if is_equation and '=' in result:
|
||||||
left, right = result.split('=', 1)
|
left, right = result.split('=', 1)
|
||||||
# Crear una ecuación simple usando el primer símbolo encontrado
|
try:
|
||||||
first_symbol = list(symbols_dict.values())[0]
|
# Intentar parsear cada lado individualmente
|
||||||
return sympy.Eq(first_symbol, sum(list(symbols_dict.values())[1:], 0))
|
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")
|
||||||
else:
|
else:
|
||||||
# Para expresiones, devolver el primer símbolo o una suma
|
# Para expresiones, intentar parsear con el contexto de símbolos
|
||||||
symbols_list = list(symbols_dict.values())
|
try:
|
||||||
if len(symbols_list) == 1:
|
return sympy.sympify(result, locals=symbols_dict)
|
||||||
return symbols_list[0]
|
except Exception as e2:
|
||||||
else:
|
print(f"Debug: Error parseando expresión: {e2}")
|
||||||
return sum(symbols_list[1:], symbols_list[0])
|
# Si todo falla, devolver el primer símbolo
|
||||||
|
return list(symbols_dict.values())[0] if symbols_dict else sympy.Symbol('LaTeX_parse_error')
|
||||||
else:
|
else:
|
||||||
return sympy.Symbol('LaTeX_parse_error')
|
return sympy.Symbol('LaTeX_parse_error')
|
||||||
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
x**2 + y**2 = r**2
|
|
||||||
r=?
|
|
||||||
|
|
||||||
a=r*5+5
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
#!/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()
|
|
148
latex_debug.html
148
latex_debug.html
|
@ -1,148 +0,0 @@
|
||||||
|
|
||||||
<!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>
|
|
||||||
|
|
4247
main_calc_app.py
4247
main_calc_app.py
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +1,12 @@
|
||||||
# requirements.txt
|
# requirements.txt
|
||||||
# Calculadora MAV - CAS Híbrido
|
# Calculadora MAV - CAS Híbrido con PySide6 y MathJax
|
||||||
# Dependencias requeridas
|
# Dependencias requeridas
|
||||||
|
|
||||||
# Motor algebraico principal
|
# Motor algebraico principal
|
||||||
sympy>=1.12
|
sympy>=1.12
|
||||||
|
|
||||||
# Interfaz gráfica (generalmente incluido con Python)
|
# Interfaz gráfica moderna
|
||||||
# tkinter - incluido con Python estándar
|
PySide6>=6.6.0
|
||||||
|
|
||||||
# Plotting y visualización
|
# Plotting y visualización
|
||||||
matplotlib>=3.7.0
|
matplotlib>=3.7.0
|
||||||
|
|
37
test.py
37
test.py
|
@ -1,37 +0,0 @@
|
||||||
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()
|
|
669
tl_popup.py
669
tl_popup.py
|
@ -1,669 +0,0 @@
|
||||||
"""
|
|
||||||
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