Compare commits

...

10 Commits

Author SHA1 Message Date
Miguel 9ba73a9db6 Limpieza general 2025-06-11 18:37:57 +02:00
Miguel aaddfbc3fa Actualización de la interfaz de la calculadora híbrida con ajustes en la geometría de la ventana y el sistema de autocompletado. Se añade una nueva opción para copiar información de depuración al portapapeles, mejorando la accesibilidad de datos. Se optimiza el historial de cálculos y se realizan mejoras en la gestión de variables simbólicas en el motor algebraico. 2025-06-11 11:07:07 +02:00
Miguel 4010de2c12 Implementación de un método para generar contenido LaTeX completo en el panel, mejorando la representación de resultados y asignaciones. Se optimiza el manejo de expresiones y se añaden mejoras en el parseo de ecuaciones. Se actualiza el historial de cálculos eliminando contenido obsoleto. 2025-06-11 10:40:11 +02:00
Miguel 68bed8937a Mejora del panel LaTeX y ajustes en la configuración de la interfaz. Se implementa un sistema para verificar la disponibilidad de MathJax y se optimiza la gestión de ecuaciones pendientes. Se actualizan estilos y se ajusta la geometría de la ventana. Se añade un wrapper para la función 'solve' que convierte cadenas a símbolos automáticamente, mejorando la resolución de ecuaciones. 2025-06-11 10:19:14 +02:00
Miguel b7e35d1ae3 Actualización del motor algebraico para permitir asignaciones duales, integrando ecuaciones en el contexto de SymPy. Se mejora la gestión de variables y se optimiza la lógica de resolución, asegurando que las soluciones se apliquen correctamente al sistema. Se actualiza el historial de cálculos con nuevas expresiones. 2025-06-11 00:01:47 +02:00
Miguel 242d5095af Actualización del historial de cálculos con nuevas expresiones y ajustes en la configuración de la interfaz. Se modifica la geometría de la ventana y se añaden nuevos formatos de texto en la calculadora. Se optimiza el sistema de autocompletado y se corrige la inicialización del popup de autocompletado. 2025-06-10 23:48:10 +02:00
Miguel efbc6a5b52 Version con Pyside basica 2025-06-09 22:07:11 +02:00
Miguel bb82098608 No funcionando el panel Latex 2025-06-09 21:56:16 +02:00
Miguel 0629137956 Mejora del sistema de autocompletado y ajustes en la interfaz de la calculadora. Se implementa un nuevo popup de autocompletado para variables y funciones, optimizando la navegación y selección. Se ajustan estilos y se mejora la gestión de eventos de teclado. Se actualiza el historial de cálculos con nuevas expresiones y se optimiza la lógica de evaluación. 2025-06-08 00:08:26 +02:00
Miguel 0cbf9dbf79 Primera version con pyside 2025-06-07 23:48:14 +02:00
24 changed files with 3345 additions and 5628 deletions

18
.data/history.txt Normal file
View File

@ -0,0 +1,18 @@
ex = (t * 8) / w
var1 = 2
var2 = 4
vatt1 = 4
IP4

View File

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

Binary file not shown.

View File

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

0
app/__init__.py Normal file
View File

2497
app/main_calc_app.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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,32 +582,68 @@ 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
final_value = self._resolve_iteratively(solution_value) free_symbols = solution_value.free_symbols
unresolved_vars = free_symbols - {var_symbol}
# Auto-aplicar la solución al sistema if unresolved_vars:
self._auto_apply_solution(var_symbol, final_value) # Hay variables sin resolver, verificar si tienen valores numéricos
has_numeric_values = True
# Verificar que el resultado no sea problemático for var_sym in unresolved_vars:
if final_value == var_symbol or str(final_value) in ['True', 'False']: var_name = str(var_sym)
return var_symbol if var_name in self.symbol_table:
value = self.symbol_table[var_name]
return Eq(var_symbol, final_value) # Si el valor es simbólico (no numérico), no podemos resolver completamente
if not (hasattr(value, 'is_number') and value.is_number):
has_numeric_values = False
break
else:
# Variable no tiene valor asignado
has_numeric_values = False
break
if has_numeric_values:
# Todas las variables tienen valores numéricos, resolver iterativamente
final_value = self._resolve_iteratively(solution_value)
# Auto-aplicar la solución numérica al sistema
if final_value != var_symbol and not str(final_value) in ['True', 'False']:
self._auto_apply_solution(var_symbol, final_value)
return Eq(var_symbol, final_value)
else:
# Hay variables con valores simbólicos, intentar resolver lo máximo posible
# Aplicar resolución iterativa parcial
resolved_solution = self._resolve_iteratively(solution_value)
result_eq = Eq(var_symbol, resolved_solution)
return result_eq
else:
# No hay variables sin resolver, intentar resolver completamente
final_value = self._resolve_iteratively(solution_value)
# Auto-aplicar la solución al sistema solo si es un valor específico
if final_value != var_symbol and not str(final_value) in ['True', 'False']:
self._auto_apply_solution(var_symbol, final_value)
return Eq(var_symbol, final_value)
else: else:
# Si no hay solución en las ecuaciones, verificar en symbol_table # Si no hay solución en las ecuaciones, verificar en symbol_table
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]
final_value = self._resolve_iteratively(value) # Si el valor en symbol_table es diferente de la variable, devolverlo
return Eq(var_symbol, final_value) if value != var_symbol:
else: final_value = self._resolve_iteratively(value)
return var_symbol return Eq(var_symbol, final_value)
# Si no hay información, devolver la variable tal como está
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.)
return solve(*args, **kwargs) return solve(*args, **kwargs)
@ -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:

View File

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

View File

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

592
app/tl_popup.py Normal file
View File

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

319
calc.py
View File

@ -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():
"""Verifica que todas las dependencias estén instaladas"""
required_modules = [
'PySide6',
'sympy',
'numpy',
'matplotlib'
]
missing = []
for module in required_modules:
try:
__import__(module)
except ImportError:
missing.append(module)
if missing:
print("❌ Faltan las siguientes dependencias:")
for module in missing:
print(f" - {module}")
print("\n💡 Para instalar las dependencias, ejecuta:")
print(" pip install -r requirements.txt")
return False
print("✅ Todas las dependencias están instaladas")
return True
def setup_logging(): def check_pyside6_webengine():
"""Configura el sistema de logging completo""" """Verifica si PySide6 WebEngine está disponible"""
MAX_LOG_FILES = 10 # Límite de archivos de log
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
# Archivo de log con timestamp
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
log_file = log_dir / f"mav_calc_{timestamp}.log"
# Eliminar logs antiguos si se supera el límite
try: try:
existing_logs = sorted( from PySide6.QtWebEngineWidgets import QWebEngineView
[f for f in log_dir.glob("mav_calc_*.log") if f.is_file()], print("✅ PySide6 WebEngine disponible para MathJax")
key=os.path.getmtime return True
) except ImportError:
if len(existing_logs) >= MAX_LOG_FILES: print("⚠️ PySide6 WebEngine no disponible")
logs_to_delete = existing_logs[:len(existing_logs) - MAX_LOG_FILES + 1] print(" Instalando QtWebEngine...")
for old_log in logs_to_delete: try:
old_log.unlink() subprocess.run([sys.executable, '-m', 'pip', 'install', 'PySide6-WebEngine'],
logging.info(f"Eliminado log antiguo: {old_log}") check=True, capture_output=True)
except Exception as e: print("✅ PySide6 WebEngine instalado correctamente")
logging.warning(f"No se pudieron eliminar logs antiguos: {e}") return True
except subprocess.CalledProcessError:
# Configurar logging print("❌ No se pudo instalar PySide6 WebEngine")
logging.basicConfig( return False
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
handlers=[
logging.FileHandler(log_file, encoding='utf-8'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
return logger, log_file
def log_error_with_context(logger, error, context=""):
"""Registra un error con contexto completo"""
logger.error("=" * 50)
logger.error("ERROR DETECTADO")
logger.error("=" * 50)
if context:
logger.error(f"Contexto: {context}")
logger.error(f"Tipo de error: {type(error).__name__}")
logger.error(f"Mensaje: {str(error)}")
logger.error("Traceback completo:")
logger.error(traceback.format_exc())
logger.error("Variables locales en el momento del error:")
# Intentar capturar variables locales del frame donde ocurrió el error
try:
tb = traceback.extract_tb(error.__traceback__)
if tb:
last_frame = tb[-1]
logger.error(f" Archivo: {last_frame.filename}")
logger.error(f" Línea: {last_frame.lineno}")
logger.error(f" Función: {last_frame.name}")
logger.error(f" Código: {last_frame.line}")
except Exception as frame_error:
logger.error(f"No se pudieron obtener detalles del frame: {frame_error}")
logger.error("=" * 50)
def show_error_with_log_info(error, log_file, context=""):
"""Muestra error al usuario con información del log"""
error_msg = f"""Error en Calculadora MAV:
{context}
Error: {type(error).__name__}: {str(error)}
INFORMACIÓN DE DEBUGGING:
Log completo guardado en: {log_file}
Para soporte, enviar el archivo de log
Timestamp: {datetime.datetime.now()}
¿Qué hacer ahora?
1. Revisar el archivo de log para más detalles
2. Intentar reiniciar la aplicación
3. Verificar dependencias con: python launcher.py --setup
4. Ejecutar tests con: python launcher.py --test
"""
try:
root = tk.Tk()
root.withdraw()
# Crear ventana de error personalizada
error_window = tk.Toplevel(root)
error_window.title("Error - Calculadora MAV")
error_window.geometry("600x400")
error_window.configure(bg="#2b2b2b")
# Hacer la ventana modal
error_window.transient(root)
error_window.grab_set()
# Centrar ventana
error_window.update_idletasks()
x = (error_window.winfo_screenwidth() // 2) - (error_window.winfo_width() // 2)
y = (error_window.winfo_screenheight() // 2) - (error_window.winfo_height() // 2)
error_window.geometry(f"+{x}+{y}")
# Contenido
from tkinter import scrolledtext
text_widget = scrolledtext.ScrolledText(
error_window,
font=("Consolas", 10),
bg="#1e1e1e",
fg="#ff6b6b",
wrap=tk.WORD
)
text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
text_widget.insert("1.0", error_msg)
text_widget.config(state="disabled")
# Botones
button_frame = tk.Frame(error_window, bg="#2b2b2b")
button_frame.pack(fill=tk.X, padx=10, pady=5)
def open_log_folder():
try:
if platform.system() == "Windows":
os.startfile(log_file.parent)
elif platform.system() == "Darwin": # macOS
subprocess.run(["open", str(log_file.parent)])
else: # Linux
subprocess.run(["xdg-open", str(log_file.parent)])
except Exception:
pass
def copy_log_path():
error_window.clipboard_clear()
error_window.clipboard_append(str(log_file))
tk.Button(
button_frame,
text="Abrir Carpeta de Logs",
command=open_log_folder,
bg="#4fc3f7",
fg="white"
).pack(side=tk.LEFT, padx=5)
tk.Button(
button_frame,
text="Copiar Ruta del Log",
command=copy_log_path,
bg="#82aaff",
fg="white"
).pack(side=tk.LEFT, padx=5)
tk.Button(
button_frame,
text="Cerrar",
command=error_window.destroy,
bg="#ff6b6b",
fg="white"
).pack(side=tk.RIGHT, padx=5)
# Esperar a que se cierre la ventana
error_window.wait_window()
root.destroy()
except Exception as gui_error:
# Si falla la GUI, mostrar en consola
print("ERROR: No se pudo mostrar ventana de error")
print(error_msg)
print(f"Error adicional: {gui_error}")
# Variable global para el logger
logger = None
log_file = None
def launch_application():
try:
# Importar y ejecutar la aplicación
from main_calc_app import HybridCalculatorApp
root = tk.Tk()
app = HybridCalculatorApp(root)
root.mainloop()
logger.info("Aplicación cerrada normalmente")
except ImportError as e:
logger.error(f"Error de importación: {e}")
log_error_with_context(logger, e, "Importación de módulos de la aplicación")
show_error_with_log_info(e, log_file, "Error importando módulos de la aplicación")
except Exception as e:
logger.error(f"Error durante ejecución de la aplicación: {e}")
log_error_with_context(logger, e, "Ejecución de la aplicación principal")
show_error_with_log_info(e, log_file, "Error durante la ejecución de la aplicación")
def main(): 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 main()
temp_logger = None
temp_log_file = None
# Inicio normal
try:
main()
except Exception as e:
if temp_logger:
log_error_with_context(temp_logger, e, "Error crítico en __main__")
print(f"ERROR CRÍTICO: {e}")
if temp_log_file:
print(f"Ver log completo en: {temp_log_file}")
sys.exit(1)

View File

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

View File

@ -1,6 +0,0 @@
x**2 + y**2 = r**2
r=?
a=r*5+5

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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