Primera version con pyside
This commit is contained in:
parent
488f78b409
commit
0cbf9dbf79
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
# Calculadora MAV - Versión PySide6 con Diseño Minimalista
|
||||||
|
|
||||||
|
## 🎯 Implementación Minimalista de 3 Paneles
|
||||||
|
|
||||||
|
Esta es la implementación de la Calculadora MAV usando **PySide6** con diseño minimalista de 3 paneles y renderizado **MathJax** para ecuaciones LaTeX, manteniendo correspondencia 1:1 línea por línea entre entrada y salida.
|
||||||
|
|
||||||
|
## ✨ Características Principales
|
||||||
|
|
||||||
|
### 🔧 Interfaz Moderna
|
||||||
|
- **PySide6**: Framework Qt moderno y nativo
|
||||||
|
- **Tema Oscuro**: Diseño elegante y profesional
|
||||||
|
- **Resaltado de Sintaxis**: Coloreado inteligente de expresiones matemáticas
|
||||||
|
- **Interfaz Responsive**: Se adapta al tamaño de la ventana
|
||||||
|
|
||||||
|
### 🧮 Motor de Cálculo
|
||||||
|
- **SymPy**: Motor algebraico simbólico completo
|
||||||
|
- **Evaluación Asíncrona**: Cálculos en threads separados
|
||||||
|
- **Tipos Personalizados**: Sistema de tipos extensible
|
||||||
|
- **Historial Automático**: Guarda y restaura sesiones
|
||||||
|
|
||||||
|
### 📐 Renderizado LaTeX
|
||||||
|
- **MathJax**: Renderizado web profesional de ecuaciones
|
||||||
|
- **Tiempo Real**: Actualización automática del panel LaTeX
|
||||||
|
- **Múltiples Tipos**: Ecuaciones, asignaciones y expresiones simbólicas
|
||||||
|
- **Interactivo**: Panel redimensionable y ocultable
|
||||||
|
|
||||||
|
## 🚀 Instalación y Uso
|
||||||
|
|
||||||
|
### Requisitos Previos
|
||||||
|
```bash
|
||||||
|
Python 3.8 o superior
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instalar Dependencias
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ejecutar la Aplicación
|
||||||
|
```bash
|
||||||
|
# Opción 1: Usar el launcher (recomendado)
|
||||||
|
python launch_pyside6.py
|
||||||
|
|
||||||
|
# Opción 2: Ejecutar directamente
|
||||||
|
python main_calc_app_pyside6.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎮 Guía de Uso
|
||||||
|
|
||||||
|
### Interfaz Principal - 3 Paneles Minimalistas
|
||||||
|
- **Panel 1 (Izquierda)**: Entrada de expresiones matemáticas
|
||||||
|
- **Panel 2 (Centro)**: Resultados con correspondencia 1:1 línea por línea
|
||||||
|
- **Panel 3 (Derecha)**: Renderizado LaTeX/MathJax de ecuaciones y comentarios (colapsable)
|
||||||
|
|
||||||
|
### Atajos de Teclado
|
||||||
|
| Atajo | Función |
|
||||||
|
|-------|---------|
|
||||||
|
| `Ctrl+Enter` | Evaluar expresión |
|
||||||
|
| `Shift+Enter` | Evaluar expresión |
|
||||||
|
| `F12` | Mostrar/ocultar panel LaTeX |
|
||||||
|
| `Ctrl+N` | Nueva sesión |
|
||||||
|
| `Ctrl+O` | Abrir archivo |
|
||||||
|
| `Ctrl+S` | Guardar archivo |
|
||||||
|
|
||||||
|
### Ejemplos de Uso
|
||||||
|
```python
|
||||||
|
# Ecuaciones básicas
|
||||||
|
x**2 + y**2 = r**2
|
||||||
|
|
||||||
|
# Resolver ecuaciones
|
||||||
|
solve(x**2 - 4, x)
|
||||||
|
|
||||||
|
# Cálculo diferencial
|
||||||
|
diff(x**3 + 2*x**2 + x, x)
|
||||||
|
|
||||||
|
# Cálculo integral
|
||||||
|
integrate(x**2, x)
|
||||||
|
|
||||||
|
# Álgebra lineal
|
||||||
|
Matrix([[1, 2], [3, 4]])
|
||||||
|
|
||||||
|
# Asignaciones
|
||||||
|
a = x**2 + 5
|
||||||
|
b = solve(a - 10, x)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Características de la Interfaz
|
||||||
|
|
||||||
|
### Resaltado de Sintaxis
|
||||||
|
- **Números**: Color azul claro (`#89ddff`)
|
||||||
|
- **Funciones**: Color azul (`#82aaff`) y negrita
|
||||||
|
- **Variables**: Color verde claro (`#c3e88d`)
|
||||||
|
- **Operadores**: Color rojo (`#ff6b6b`) y negrita
|
||||||
|
- **Paréntesis**: Color naranja (`#f78c6c`) y negrita
|
||||||
|
|
||||||
|
### Panel LaTeX
|
||||||
|
- **Ecuaciones**: Borde azul (`#4fc3f7`)
|
||||||
|
- **Asignaciones**: Borde verde (`#c3e88d`)
|
||||||
|
- **Expresiones Simbólicas**: Borde naranja (`#f78c6c`)
|
||||||
|
- **Hover Effects**: Cambios de color al pasar el ratón
|
||||||
|
|
||||||
|
## 🔧 Arquitectura Técnica
|
||||||
|
|
||||||
|
### Componentes Principales
|
||||||
|
```
|
||||||
|
HybridCalculatorPySide6 (Ventana Principal)
|
||||||
|
├── MathInputHighlighter (Resaltado de sintaxis)
|
||||||
|
├── CalculatorWorker (Evaluación asíncrona)
|
||||||
|
├── MathJaxPanel (Renderizado LaTeX)
|
||||||
|
└── PureAlgebraicEngine (Motor de cálculo)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Threading
|
||||||
|
- **UI Thread**: Interfaz de usuario principal
|
||||||
|
- **Worker Thread**: Evaluación matemática asíncrona
|
||||||
|
- **Signals/Slots**: Comunicación segura entre threads
|
||||||
|
|
||||||
|
### WebEngine
|
||||||
|
- **QWebEngineView**: Para renderizado MathJax
|
||||||
|
- **JavaScript Integration**: Comunicación bidireccional
|
||||||
|
- **HTML5**: Soporte completo para MathJax 3.x
|
||||||
|
|
||||||
|
## 🆚 Comparación con Versión Original
|
||||||
|
|
||||||
|
| Característica | Tkinter Original | PySide6 Nueva |
|
||||||
|
|----------------|------------------|---------------|
|
||||||
|
| **Framework** | Tkinter | PySide6/Qt |
|
||||||
|
| **Renderizado LaTeX** | pywebview/tkinterweb | MathJax nativo |
|
||||||
|
| **Tema** | Básico | Moderno oscuro |
|
||||||
|
| **Resaltado** | No | Sintaxis completa |
|
||||||
|
| **Threading** | Básico | Asíncrono avanzado |
|
||||||
|
| **Responsive** | Limitado | Completo |
|
||||||
|
| **Cross-platform** | Básico | Nativo Qt |
|
||||||
|
|
||||||
|
## 🔍 Solución de Problemas
|
||||||
|
|
||||||
|
### Error: "No module named 'PySide6'"
|
||||||
|
```bash
|
||||||
|
pip install PySide6
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: "No module named 'PySide6.QtWebEngineWidgets'"
|
||||||
|
```bash
|
||||||
|
pip install PySide6-WebEngine
|
||||||
|
```
|
||||||
|
|
||||||
|
### Panel LaTeX no funciona
|
||||||
|
1. Verificar conexión a internet (MathJax CDN)
|
||||||
|
2. Comprobar que WebEngine esté instalado
|
||||||
|
3. Ver logs en consola para errores JavaScript
|
||||||
|
|
||||||
|
### Rendimiento lento
|
||||||
|
1. Cerrar otras aplicaciones que usen Qt/WebEngine
|
||||||
|
2. Reducir el número de ecuaciones en el panel LaTeX
|
||||||
|
3. Verificar que no hay bucles infinitos en expresiones
|
||||||
|
|
||||||
|
## 🚧 Desarrollo y Contribución
|
||||||
|
|
||||||
|
### Estructura del Código
|
||||||
|
```
|
||||||
|
main_calc_app_pyside6.py # Aplicación principal
|
||||||
|
launch_pyside6.py # Launcher con verificaciones
|
||||||
|
requirements.txt # Dependencias actualizadas
|
||||||
|
README_PYSIDE6.md # Esta documentación
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extensiones Futuras
|
||||||
|
- [ ] Autocompletado inteligente
|
||||||
|
- [ ] Gráficos integrados con matplotlib
|
||||||
|
- [ ] Exportación de ecuaciones LaTeX
|
||||||
|
- [ ] Temas personalizables
|
||||||
|
- [ ] Plugin system
|
||||||
|
- [ ] Colaboración en tiempo real
|
||||||
|
|
||||||
|
## 📄 Licencia
|
||||||
|
|
||||||
|
Mismo sistema de licencia que el proyecto original.
|
||||||
|
|
||||||
|
## 💝 Agradecimientos
|
||||||
|
|
||||||
|
- **Qt/PySide6**: Framework de interfaz moderna
|
||||||
|
- **MathJax**: Renderizado matemático profesional
|
||||||
|
- **SymPy**: Motor algebraico potente
|
||||||
|
- **Comunidad Python**: Soporte y documentación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**¡Disfruta calculando con la nueva interfaz moderna! 🎉**
|
|
@ -0,0 +1,87 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Demostración del diseño minimalista de 3 paneles
|
||||||
|
Calculadora MAV - PySide6
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
def show_demo_instructions():
|
||||||
|
"""Muestra las instrucciones de la demostración"""
|
||||||
|
print("🎯 DEMOSTRACIÓN: Calculadora MAV - Diseño Minimalista")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print("📋 DISEÑO DE 3 PANELES:")
|
||||||
|
print(" ┌─────────────┬─────────────┬─────────────┐")
|
||||||
|
print(" │ PANEL 1 │ PANEL 2 │ PANEL 3 │")
|
||||||
|
print(" │ ENTRADA │ RESULTADOS │ LATEX │")
|
||||||
|
print(" │ │ (1:1) │ (colapsable)│")
|
||||||
|
print(" └─────────────┴─────────────┴─────────────┘")
|
||||||
|
print()
|
||||||
|
print("✨ CARACTERÍSTICAS PRINCIPALES:")
|
||||||
|
print(" • Correspondencia 1:1 entre líneas de entrada y salida")
|
||||||
|
print(" • Tema oscuro minimalista")
|
||||||
|
print(" • Resaltado de sintaxis en tiempo real")
|
||||||
|
print(" • Panel LaTeX con MathJax para ecuaciones")
|
||||||
|
print(" • Motor algebraico SymPy sin cambios")
|
||||||
|
print()
|
||||||
|
print("🎮 EJEMPLOS PARA PROBAR:")
|
||||||
|
print(" # Comentario - aparece en panel LaTeX")
|
||||||
|
print(" x**2 + y**2 = r**2")
|
||||||
|
print(" solve(x**2 - 4, x)")
|
||||||
|
print(" a = 5*x + 3")
|
||||||
|
print(" diff(x**3, x)")
|
||||||
|
print(" Matrix([[1, 2], [3, 4]])")
|
||||||
|
print()
|
||||||
|
print("⌨️ ATAJOS DE TECLADO:")
|
||||||
|
print(" • Ctrl+Enter / Shift+Enter: Evaluar")
|
||||||
|
print(" • F12: Mostrar/ocultar panel LaTeX")
|
||||||
|
print(" • Ctrl+N: Nueva sesión")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def run_demo():
|
||||||
|
"""Ejecuta la demostración"""
|
||||||
|
show_demo_instructions()
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = input("¿Ejecutar la aplicación? (s/N): ").strip().lower()
|
||||||
|
if response in ['s', 'sí', 'si', 'y', 'yes']:
|
||||||
|
print("\n🚀 Iniciando Calculadora MAV...")
|
||||||
|
|
||||||
|
# Verificar dependencias primero
|
||||||
|
try:
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
print("✅ PySide6 disponible")
|
||||||
|
except ImportError:
|
||||||
|
print("❌ PySide6 no está instalado")
|
||||||
|
print(" Ejecuta: pip install -r requirements.txt")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Iniciar aplicación
|
||||||
|
from main_calc_app_pyside6 import main as run_app
|
||||||
|
|
||||||
|
# Precarga con contenido de demostración
|
||||||
|
print("📝 Precargando contenido de demostración...")
|
||||||
|
demo_content = """# Demostración Calculadora MAV
|
||||||
|
x**2 + y**2 = r**2
|
||||||
|
solve(x**2 - 4, x)
|
||||||
|
a = 5*x + 3
|
||||||
|
diff(a, x)"""
|
||||||
|
|
||||||
|
# Ejecutar aplicación
|
||||||
|
run_app()
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("👋 ¡Hasta luego!")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n🚪 Demostración cancelada")
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Error: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(run_demo())
|
|
@ -1,6 +1,8 @@
|
||||||
|
|
||||||
|
|
||||||
x**2 + y**2 = r**2
|
x**2 + y**2 = r**2
|
||||||
|
|
||||||
r=?
|
r=?
|
||||||
|
|
||||||
a=r*5+5
|
a=r*5+5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
{
|
{
|
||||||
"window_geometry": "1272x700+331+1194",
|
"window_geometry": "1400x800",
|
||||||
"sash_pos_x": 440,
|
"debug_mode": false,
|
||||||
"symbolic_mode": true,
|
"latex_panel_visible": true
|
||||||
"show_numeric_approximation": true,
|
|
||||||
"keep_symbolic_fractions": true,
|
|
||||||
"auto_simplify": false,
|
|
||||||
"latex_panel_visible": true
|
|
||||||
}
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Launcher para Calculadora MAV - Versión PySide6 con MathJax
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
import importlib.util
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def check_dependencies():
|
||||||
|
"""Verifica que todas las dependencias estén instaladas"""
|
||||||
|
required_modules = [
|
||||||
|
'PySide6',
|
||||||
|
'sympy',
|
||||||
|
'numpy',
|
||||||
|
'matplotlib'
|
||||||
|
]
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
for module in required_modules:
|
||||||
|
try:
|
||||||
|
__import__(module)
|
||||||
|
except ImportError:
|
||||||
|
missing.append(module)
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print("❌ Faltan las siguientes dependencias:")
|
||||||
|
for module in missing:
|
||||||
|
print(f" - {module}")
|
||||||
|
print("\n💡 Para instalar las dependencias, ejecuta:")
|
||||||
|
print(" pip install -r requirements.txt")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ Todas las dependencias están instaladas")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_pyside6_webengine():
|
||||||
|
"""Verifica si PySide6 WebEngine está disponible"""
|
||||||
|
try:
|
||||||
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
|
print("✅ PySide6 WebEngine disponible para MathJax")
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
print("⚠️ PySide6 WebEngine no disponible")
|
||||||
|
print(" Instalando QtWebEngine...")
|
||||||
|
try:
|
||||||
|
subprocess.run([sys.executable, '-m', 'pip', 'install', 'PySide6-WebEngine'],
|
||||||
|
check=True, capture_output=True)
|
||||||
|
print("✅ PySide6 WebEngine instalado correctamente")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
print("❌ No se pudo instalar PySide6 WebEngine")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Función principal del launcher"""
|
||||||
|
print("🚀 Iniciando Calculadora MAV - Diseño Minimalista 3 Paneles")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Verificar dependencias
|
||||||
|
if not check_dependencies():
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Verificar WebEngine
|
||||||
|
if not check_pyside6_webengine():
|
||||||
|
print("⚠️ Continuando sin WebEngine (funcionalidad limitada)")
|
||||||
|
|
||||||
|
print("\n🎯 Iniciando aplicación...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Importar y ejecutar la aplicación
|
||||||
|
from main_calc_app_pyside6 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__":
|
||||||
|
main()
|
|
@ -0,0 +1,789 @@
|
||||||
|
"""
|
||||||
|
Calculadora MAV CAS Híbrida - Aplicación PySide6 con MathJax
|
||||||
|
Diseño minimalista de 3 paneles con correspondencia 1:1 línea por línea
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QTextEdit, QPlainTextEdit, QSplitter, QPushButton, QLabel,
|
||||||
|
QFrame, QMenuBar, QStatusBar, QMessageBox, QFileDialog,
|
||||||
|
QScrollArea, QSizePolicy
|
||||||
|
)
|
||||||
|
from PySide6.QtCore import (
|
||||||
|
Qt, QTimer, QThread, QObject, Signal, QUrl, QSize
|
||||||
|
)
|
||||||
|
from PySide6.QtGui import (
|
||||||
|
QFont, QTextCursor, QTextCharFormat, QColor, QIcon,
|
||||||
|
QTextDocument, QSyntaxHighlighter, QTextFormat,
|
||||||
|
QKeySequence, QShortcut, QFontMetrics
|
||||||
|
)
|
||||||
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
|
from PySide6.QtWebEngineCore import QWebEngineSettings
|
||||||
|
|
||||||
|
# Importar componentes del CAS híbrido
|
||||||
|
from main_evaluation_puro import PureAlgebraicEngine, EvaluationResult
|
||||||
|
from tl_popup import InteractiveResultManager, PlotResult
|
||||||
|
from type_registry import get_registered_helper_functions, get_registered_base_context
|
||||||
|
import sympy
|
||||||
|
from sympy_helper import SympyTools as SympyHelper
|
||||||
|
|
||||||
|
|
||||||
|
class MathInputHighlighter(QSyntaxHighlighter):
|
||||||
|
"""Resaltador de sintaxis para expresiones matemáticas"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setup_highlighting_rules()
|
||||||
|
|
||||||
|
def setup_highlighting_rules(self):
|
||||||
|
"""Configura las reglas de resaltado"""
|
||||||
|
self.highlighting_rules = []
|
||||||
|
|
||||||
|
# Números
|
||||||
|
number_format = QTextCharFormat()
|
||||||
|
number_format.setForeground(QColor("#89ddff"))
|
||||||
|
self.highlighting_rules.append((r'\b\d+\.?\d*\b', number_format))
|
||||||
|
|
||||||
|
# Funciones matemáticas
|
||||||
|
function_format = QTextCharFormat()
|
||||||
|
function_format.setForeground(QColor("#82aaff"))
|
||||||
|
function_format.setFontWeight(QFont.Bold)
|
||||||
|
functions = [
|
||||||
|
'sin', 'cos', 'tan', 'log', 'ln', 'exp', 'sqrt', 'abs',
|
||||||
|
'solve', 'diff', 'integrate', 'limit', 'series', 'factor',
|
||||||
|
'expand', 'simplify', 'Matrix', 'det', 'inv'
|
||||||
|
]
|
||||||
|
for func in functions:
|
||||||
|
pattern = rf'\b{func}\b'
|
||||||
|
self.highlighting_rules.append((pattern, function_format))
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
variable_format = QTextCharFormat()
|
||||||
|
variable_format.setForeground(QColor("#c3e88d"))
|
||||||
|
self.highlighting_rules.append((r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', variable_format))
|
||||||
|
|
||||||
|
# Operadores
|
||||||
|
operator_format = QTextCharFormat()
|
||||||
|
operator_format.setForeground(QColor("#ff6b6b"))
|
||||||
|
operator_format.setFontWeight(QFont.Bold)
|
||||||
|
self.highlighting_rules.append((r'[+\-*/=<>!&|^]', operator_format))
|
||||||
|
|
||||||
|
# Paréntesis y corchetes
|
||||||
|
bracket_format = QTextCharFormat()
|
||||||
|
bracket_format.setForeground(QColor("#f78c6c"))
|
||||||
|
bracket_format.setFontWeight(QFont.Bold)
|
||||||
|
self.highlighting_rules.append((r'[\[\](){}]', bracket_format))
|
||||||
|
|
||||||
|
def highlightBlock(self, text):
|
||||||
|
"""Aplica el resaltado al bloque de texto"""
|
||||||
|
for pattern, format_obj in self.highlighting_rules:
|
||||||
|
expression = re.compile(pattern)
|
||||||
|
for match in expression.finditer(text):
|
||||||
|
start, end = match.span()
|
||||||
|
self.setFormat(start, end - start, format_obj)
|
||||||
|
|
||||||
|
|
||||||
|
class SynchronizedTextEdit(QTextEdit):
|
||||||
|
"""Editor de texto que mantiene sincronización línea por línea"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setLineWrapMode(QTextEdit.NoWrap)
|
||||||
|
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
|
||||||
|
def sync_scroll_with(self, other_widget):
|
||||||
|
"""Sincroniza el scroll con otro widget"""
|
||||||
|
self.verticalScrollBar().valueChanged.connect(
|
||||||
|
other_widget.verticalScrollBar().setValue
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LineNumberedPlainTextEdit(QPlainTextEdit):
|
||||||
|
"""Editor de texto plano con numeración de líneas implícita"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setLineWrapMode(QPlainTextEdit.NoWrap)
|
||||||
|
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||||
|
|
||||||
|
|
||||||
|
class MathJaxPanel(QWebEngineView):
|
||||||
|
"""Panel web para renderizado de LaTeX con MathJax - Panel colapsable a la derecha"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.equations = []
|
||||||
|
self.setup_webview()
|
||||||
|
self.load_mathjax_base()
|
||||||
|
|
||||||
|
def setup_webview(self):
|
||||||
|
"""Configura el webview"""
|
||||||
|
self.setMinimumWidth(300)
|
||||||
|
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
|
||||||
|
self.settings().setAttribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
|
||||||
|
self.settings().setAttribute(QWebEngineSettings.LocalContentCanAccessFileUrls, True)
|
||||||
|
|
||||||
|
def load_mathjax_base(self):
|
||||||
|
"""Carga el HTML base con MathJax"""
|
||||||
|
html_content = self.generate_base_html()
|
||||||
|
self.setHtml(html_content)
|
||||||
|
|
||||||
|
def generate_base_html(self):
|
||||||
|
"""Genera el HTML base con MathJax configurado"""
|
||||||
|
return """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>MathJax Panel</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
margin: 8px;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.equation {
|
||||||
|
margin: 6px 0;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #2d2d30;
|
||||||
|
border-left: 3px solid #4fc3f7;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.equation:hover {
|
||||||
|
background-color: #363636;
|
||||||
|
border-left-color: #82aaff;
|
||||||
|
}
|
||||||
|
.equation-type {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: #4fc3f7;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.math-content {
|
||||||
|
font-size: 1.0em;
|
||||||
|
}
|
||||||
|
.assignment { border-left-color: #c3e88d; }
|
||||||
|
.assignment .equation-type { color: #c3e88d; }
|
||||||
|
.symbolic { border-left-color: #f78c6c; }
|
||||||
|
.symbolic .equation-type { color: #f78c6c; }
|
||||||
|
.comment {
|
||||||
|
border-left-color: #696969;
|
||||||
|
background-color: #252526;
|
||||||
|
}
|
||||||
|
.comment .equation-type { color: #696969; }
|
||||||
|
</style>
|
||||||
|
<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-mml-chtml.js"></script>
|
||||||
|
<script>
|
||||||
|
window.MathJax = {
|
||||||
|
tex: {
|
||||||
|
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
||||||
|
displayMath: [['$$', '$$'], ['\\[', '\\]']],
|
||||||
|
processEscapes: true
|
||||||
|
},
|
||||||
|
chtml: {
|
||||||
|
scale: 1.0,
|
||||||
|
minScale: 0.7
|
||||||
|
},
|
||||||
|
startup: {
|
||||||
|
ready: function () {
|
||||||
|
MathJax.startup.defaultReady();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function addEquation(type, content) {
|
||||||
|
const container = document.getElementById('equations-container');
|
||||||
|
if (!container) {
|
||||||
|
document.body.innerHTML = '<div id="equations-container"></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const equationDiv = document.createElement('div');
|
||||||
|
equationDiv.className = 'equation ' + type;
|
||||||
|
equationDiv.innerHTML = `
|
||||||
|
<div class="equation-type">${type}</div>
|
||||||
|
<div class="math-content">$$${content}$$</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById('equations-container').appendChild(equationDiv);
|
||||||
|
|
||||||
|
if (window.MathJax && MathJax.typesetPromise) {
|
||||||
|
MathJax.typesetPromise([equationDiv]).catch(function (err) {
|
||||||
|
console.log('MathJax error:', err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearEquations() {
|
||||||
|
const container = document.getElementById('equations-container');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="equations-container"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_equation(self, equation_type: str, latex_content: str):
|
||||||
|
"""Añade una ecuación al panel"""
|
||||||
|
self.equations.append({'type': equation_type, 'content': latex_content})
|
||||||
|
# Escapar backticks en el contenido LaTeX
|
||||||
|
escaped_content = latex_content.replace('`', '\\`')
|
||||||
|
js_code = f"addEquation('{equation_type}', `{escaped_content}`);"
|
||||||
|
self.page().runJavaScript(js_code)
|
||||||
|
|
||||||
|
def clear_equations(self):
|
||||||
|
"""Limpia todas las ecuaciones"""
|
||||||
|
self.equations.clear()
|
||||||
|
self.page().runJavaScript("clearEquations();")
|
||||||
|
|
||||||
|
|
||||||
|
class HybridCalculatorPySide6(QMainWindow):
|
||||||
|
"""Aplicación principal del CAS híbrido - Diseño minimalista de 3 paneles"""
|
||||||
|
|
||||||
|
SETTINGS_FILE = "hybrid_calc_settings.json"
|
||||||
|
HISTORY_FILE = "hybrid_calc_history.txt"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# Configurar logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ========== USAR EL MOTOR ORIGINAL SIN CAMBIOS ==========
|
||||||
|
self.engine = PureAlgebraicEngine()
|
||||||
|
self.interactive_manager = None
|
||||||
|
|
||||||
|
# Variables de configuración
|
||||||
|
self.settings = self.load_settings()
|
||||||
|
self.debug = self.settings.get("debug_mode", False)
|
||||||
|
|
||||||
|
# Variables de estado UI
|
||||||
|
self.latex_panel_visible = True
|
||||||
|
self._debounce_timer = QTimer()
|
||||||
|
self._debounce_timer.setSingleShot(True)
|
||||||
|
self._debounce_timer.timeout.connect(self._evaluate_and_update)
|
||||||
|
|
||||||
|
# ========== CONFIGURAR HELPERS DINÁMICOS (COMO EN ORIGINAL) ==========
|
||||||
|
self._setup_dynamic_helpers()
|
||||||
|
|
||||||
|
# Configurar interfaz
|
||||||
|
self.setup_ui()
|
||||||
|
self.setup_shortcuts()
|
||||||
|
self.load_history()
|
||||||
|
|
||||||
|
# Configurar manager interactivo
|
||||||
|
self.setup_interactive_manager()
|
||||||
|
|
||||||
|
self.logger.info("✅ Calculadora MAV PySide6 inicializada")
|
||||||
|
|
||||||
|
def _setup_dynamic_helpers(self):
|
||||||
|
"""Configura helpers dinámicamente desde el registro de tipos - COMO EN ORIGINAL"""
|
||||||
|
try:
|
||||||
|
self.HELPERS = get_registered_helper_functions()
|
||||||
|
self.HELPERS.append(SympyHelper.Helper)
|
||||||
|
self.logger.info(f"Helpers dinámicos cargados: {len(self.HELPERS)}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error cargando helpers dinámicos: {e}")
|
||||||
|
self.HELPERS = [SympyHelper.Helper]
|
||||||
|
|
||||||
|
def setup_interactive_manager(self):
|
||||||
|
"""Configura el manager de contenido interactivo - COMO EN ORIGINAL"""
|
||||||
|
try:
|
||||||
|
self.interactive_manager = InteractiveResultManager(self)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error configurando interactive manager: {e}")
|
||||||
|
self.interactive_manager = None
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Configura la interfaz de usuario - DISEÑO MINIMALISTA DE 3 PANELES"""
|
||||||
|
self.setWindowTitle("Calculadora MAV - CAS Híbrido")
|
||||||
|
self.setGeometry(100, 100, 1400, 800)
|
||||||
|
self.setStyleSheet(self.get_minimal_dark_theme())
|
||||||
|
|
||||||
|
# Widget central
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
# Layout principal horizontal
|
||||||
|
main_layout = QHBoxLayout(central_widget)
|
||||||
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
main_layout.setSpacing(1)
|
||||||
|
|
||||||
|
# ========== PANEL 1: ENTRADA ==========
|
||||||
|
self.input_text = LineNumberedPlainTextEdit()
|
||||||
|
self.input_text.setFont(QFont("Consolas", 11))
|
||||||
|
self.input_text.setPlaceholderText("Introduce expresiones matemáticas...")
|
||||||
|
self.input_text.textChanged.connect(self._on_input_changed)
|
||||||
|
|
||||||
|
# Configurar highlighter
|
||||||
|
self.highlighter = MathInputHighlighter(self.input_text.document())
|
||||||
|
|
||||||
|
# ========== PANEL 2: SALIDA (SINCRONIZADO 1:1) ==========
|
||||||
|
self.output_text = SynchronizedTextEdit()
|
||||||
|
self.output_text.setFont(QFont("Consolas", 11))
|
||||||
|
self.output_text.setReadOnly(True)
|
||||||
|
|
||||||
|
# Sincronizar scroll entre entrada y salida
|
||||||
|
self.input_text.verticalScrollBar().valueChanged.connect(
|
||||||
|
self.output_text.verticalScrollBar().setValue
|
||||||
|
)
|
||||||
|
self.output_text.verticalScrollBar().valueChanged.connect(
|
||||||
|
self.input_text.verticalScrollBar().setValue
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========== PANEL 3: MATHJAX (COLAPSABLE) ==========
|
||||||
|
self.latex_panel = MathJaxPanel()
|
||||||
|
|
||||||
|
# ========== LAYOUT DE 3 PANELES ==========
|
||||||
|
# Paneles 1 y 2 tienen mismo ancho, panel 3 es más estrecho
|
||||||
|
main_layout.addWidget(self.input_text, 1)
|
||||||
|
main_layout.addWidget(self.output_text, 1)
|
||||||
|
main_layout.addWidget(self.latex_panel, 0)
|
||||||
|
|
||||||
|
# ========== CONFIGURAR TAGS DE SALIDA ==========
|
||||||
|
self.setup_output_tags()
|
||||||
|
|
||||||
|
# ========== MENÚ Y BARRA DE ESTADO ==========
|
||||||
|
self.create_menu_bar()
|
||||||
|
self.create_status_bar()
|
||||||
|
|
||||||
|
def setup_output_tags(self):
|
||||||
|
"""Configura los tags de formato para la salida - COMO EN ORIGINAL"""
|
||||||
|
doc = self.output_text.document()
|
||||||
|
|
||||||
|
# Crear formatos de texto
|
||||||
|
self.tag_formats = {}
|
||||||
|
|
||||||
|
# Error
|
||||||
|
error_format = QTextCharFormat()
|
||||||
|
error_format.setForeground(QColor("#ff6b6b"))
|
||||||
|
self.tag_formats['error'] = error_format
|
||||||
|
|
||||||
|
# Resultado simbólico
|
||||||
|
symbolic_format = QTextCharFormat()
|
||||||
|
symbolic_format.setForeground(QColor("#c3e88d"))
|
||||||
|
self.tag_formats['symbolic'] = symbolic_format
|
||||||
|
|
||||||
|
# Resultado numérico
|
||||||
|
numeric_format = QTextCharFormat()
|
||||||
|
numeric_format.setForeground(QColor("#89ddff"))
|
||||||
|
self.tag_formats['numeric'] = numeric_format
|
||||||
|
|
||||||
|
# Tipo personalizado
|
||||||
|
custom_format = QTextCharFormat()
|
||||||
|
custom_format.setForeground(QColor("#f78c6c"))
|
||||||
|
self.tag_formats['custom'] = custom_format
|
||||||
|
|
||||||
|
def _on_input_changed(self):
|
||||||
|
"""Maneja cambios en la entrada con debounce - COMO EN ORIGINAL"""
|
||||||
|
self._debounce_timer.start(300) # 300ms delay
|
||||||
|
|
||||||
|
def _evaluate_and_update(self):
|
||||||
|
"""Evalúa la entrada y actualiza la salida - USANDO MOTOR ORIGINAL"""
|
||||||
|
try:
|
||||||
|
# Obtener líneas de entrada
|
||||||
|
input_text = self.input_text.toPlainText()
|
||||||
|
lines = input_text.split('\n')
|
||||||
|
|
||||||
|
# Evaluar usando el motor original
|
||||||
|
self._evaluate_lines(lines)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error en evaluación: {e}")
|
||||||
|
self._show_error(str(e))
|
||||||
|
|
||||||
|
def _evaluate_lines(self, lines: List[str]):
|
||||||
|
"""Evalúa líneas usando el motor original - SIN CAMBIOS EN LÓGICA"""
|
||||||
|
try:
|
||||||
|
# Limpiar panel LaTeX
|
||||||
|
self.latex_panel.clear_equations()
|
||||||
|
|
||||||
|
# Limpiar salida
|
||||||
|
self.output_text.clear()
|
||||||
|
|
||||||
|
# Evaluar cada línea
|
||||||
|
output_lines = []
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
# Línea vacía o comentario
|
||||||
|
output_lines.append("")
|
||||||
|
if line.startswith('#'):
|
||||||
|
# Añadir comentario al panel LaTeX
|
||||||
|
comment_text = line[1:].strip()
|
||||||
|
if comment_text:
|
||||||
|
self.latex_panel.add_equation("comment", comment_text)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# Evaluar usando el motor original
|
||||||
|
result = self.engine.evaluate_line(line)
|
||||||
|
|
||||||
|
# Procesar resultado
|
||||||
|
output_data = self._process_evaluation_result(result)
|
||||||
|
if output_data:
|
||||||
|
output_lines.append(output_data[0][1]) # Tomar el texto del resultado
|
||||||
|
|
||||||
|
# Añadir al panel LaTeX si es aplicable
|
||||||
|
self._add_to_latex_panel_if_applicable(result)
|
||||||
|
else:
|
||||||
|
output_lines.append("")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"❌ {str(e)}"
|
||||||
|
output_lines.append(error_msg)
|
||||||
|
self.logger.error(f"Error evaluando línea '{line}': {e}")
|
||||||
|
|
||||||
|
# Mostrar salida manteniendo correspondencia 1:1
|
||||||
|
self._display_output_lines(output_lines)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error en _evaluate_lines: {e}")
|
||||||
|
self._show_error(str(e))
|
||||||
|
|
||||||
|
def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]:
|
||||||
|
"""Procesa resultado de evaluación - USANDO ESTRUCTURA ORIGINAL"""
|
||||||
|
if not result.success:
|
||||||
|
return [("error", f"❌ {result.error_message}")]
|
||||||
|
|
||||||
|
output_data = []
|
||||||
|
|
||||||
|
# El resultado principal está en result.output
|
||||||
|
if result.output:
|
||||||
|
# Determinar el tipo de formato basado en result_type
|
||||||
|
if result.result_type == "error":
|
||||||
|
output_data.append(("error", f"❌ {result.output}"))
|
||||||
|
elif result.result_type == "plot":
|
||||||
|
output_data.append(("plot", f"📈 {result.output}"))
|
||||||
|
elif result.result_type == "symbolic":
|
||||||
|
output_data.append(("symbolic", f"📊 {result.output}"))
|
||||||
|
elif result.result_type == "numeric":
|
||||||
|
output_data.append(("numeric", f"🔢 {result.output}"))
|
||||||
|
else:
|
||||||
|
# Por defecto, mostrar como simbólico
|
||||||
|
output_data.append(("symbolic", result.output))
|
||||||
|
|
||||||
|
# Si no hay resultados, mostrar algo
|
||||||
|
if not output_data:
|
||||||
|
output_data.append(("symbolic", "✓"))
|
||||||
|
|
||||||
|
return output_data
|
||||||
|
|
||||||
|
def _display_output_lines(self, output_lines: List[str]):
|
||||||
|
"""Muestra líneas de salida manteniendo correspondencia 1:1"""
|
||||||
|
self.output_text.clear()
|
||||||
|
|
||||||
|
for i, line in enumerate(output_lines):
|
||||||
|
if i > 0:
|
||||||
|
self.output_text.append("") # Nueva línea
|
||||||
|
|
||||||
|
# Determinar formato basado en contenido
|
||||||
|
if line.startswith("❌"):
|
||||||
|
self._append_formatted_text(line, self.tag_formats['error'])
|
||||||
|
elif line.startswith("📊"):
|
||||||
|
self._append_formatted_text(line, self.tag_formats['symbolic'])
|
||||||
|
elif line.startswith("🔢"):
|
||||||
|
self._append_formatted_text(line, self.tag_formats['numeric'])
|
||||||
|
else:
|
||||||
|
self._append_formatted_text(line, self.tag_formats.get('custom', None))
|
||||||
|
|
||||||
|
def _append_formatted_text(self, text: str, format_obj: QTextCharFormat = None):
|
||||||
|
"""Añade texto formateado al panel de salida"""
|
||||||
|
cursor = self.output_text.textCursor()
|
||||||
|
cursor.movePosition(QTextCursor.End)
|
||||||
|
|
||||||
|
if format_obj:
|
||||||
|
cursor.insertText(text, format_obj)
|
||||||
|
else:
|
||||||
|
cursor.insertText(text)
|
||||||
|
|
||||||
|
def _add_to_latex_panel_if_applicable(self, result: EvaluationResult):
|
||||||
|
"""Añade resultado al panel LaTeX - USANDO ESTRUCTURA ORIGINAL"""
|
||||||
|
if not result.success:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determinar tipo de ecuación basado en flags del result
|
||||||
|
if result.is_assignment:
|
||||||
|
equation_type = "assignment"
|
||||||
|
elif result.is_equation:
|
||||||
|
equation_type = "equation"
|
||||||
|
else:
|
||||||
|
equation_type = "symbolic"
|
||||||
|
|
||||||
|
# Generar LaTeX del objeto resultado actual
|
||||||
|
if result.actual_result_object is not None:
|
||||||
|
latex_content = self._sympy_to_latex(result.actual_result_object)
|
||||||
|
if latex_content:
|
||||||
|
self.latex_panel.add_equation(equation_type, latex_content)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error añadiendo al panel LaTeX: {e}")
|
||||||
|
|
||||||
|
def _sympy_to_latex(self, sympy_obj) -> str:
|
||||||
|
"""Convierte objeto SymPy a LaTeX - COMO EN ORIGINAL"""
|
||||||
|
try:
|
||||||
|
if hasattr(sympy_obj, 'latex'):
|
||||||
|
return sympy_obj.latex()
|
||||||
|
elif hasattr(sympy, 'latex'):
|
||||||
|
return sympy.latex(sympy_obj)
|
||||||
|
else:
|
||||||
|
return str(sympy_obj)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error convirtiendo a LaTeX: {e}")
|
||||||
|
return str(sympy_obj)
|
||||||
|
|
||||||
|
def _show_error(self, error_msg: str):
|
||||||
|
"""Muestra mensaje de error"""
|
||||||
|
self.output_text.append(f"❌ Error: {error_msg}")
|
||||||
|
self.statusBar().showMessage(f"❌ {error_msg}", 5000)
|
||||||
|
|
||||||
|
def get_minimal_dark_theme(self):
|
||||||
|
"""Tema oscuro minimalista"""
|
||||||
|
return """
|
||||||
|
QMainWindow {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
QPlainTextEdit, QTextEdit {
|
||||||
|
background-color: #252526;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
selection-background-color: #264f78;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
QMenuBar {
|
||||||
|
background-color: #2d2d30;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
QMenuBar::item:selected {
|
||||||
|
background-color: #094771;
|
||||||
|
}
|
||||||
|
QMenu {
|
||||||
|
background-color: #2d2d30;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
}
|
||||||
|
QMenu::item:selected {
|
||||||
|
background-color: #094771;
|
||||||
|
}
|
||||||
|
QStatusBar {
|
||||||
|
background-color: #007acc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup_shortcuts(self):
|
||||||
|
"""Configura atajos de teclado"""
|
||||||
|
# Evaluar manual (forzar evaluación inmediata)
|
||||||
|
eval_shortcut = QShortcut(QKeySequence("Ctrl+Return"), self)
|
||||||
|
eval_shortcut.activated.connect(self._evaluate_and_update)
|
||||||
|
|
||||||
|
eval_shortcut2 = QShortcut(QKeySequence("Shift+Return"), self)
|
||||||
|
eval_shortcut2.activated.connect(self._evaluate_and_update)
|
||||||
|
|
||||||
|
# Toggle LaTeX panel
|
||||||
|
latex_shortcut = QShortcut(QKeySequence("F12"), self)
|
||||||
|
latex_shortcut.activated.connect(self.toggle_latex_panel)
|
||||||
|
|
||||||
|
def create_menu_bar(self):
|
||||||
|
"""Crea la barra de menú minimalista"""
|
||||||
|
menubar = self.menuBar()
|
||||||
|
|
||||||
|
# Menú Archivo
|
||||||
|
file_menu = menubar.addMenu("Archivo")
|
||||||
|
|
||||||
|
new_action = file_menu.addAction("Nuevo")
|
||||||
|
new_action.setShortcut(QKeySequence.New)
|
||||||
|
new_action.triggered.connect(self.new_session)
|
||||||
|
|
||||||
|
open_action = file_menu.addAction("Abrir...")
|
||||||
|
open_action.setShortcut(QKeySequence.Open)
|
||||||
|
open_action.triggered.connect(self.load_file)
|
||||||
|
|
||||||
|
save_action = file_menu.addAction("Guardar...")
|
||||||
|
save_action.setShortcut(QKeySequence.Save)
|
||||||
|
save_action.triggered.connect(self.save_file)
|
||||||
|
|
||||||
|
file_menu.addSeparator()
|
||||||
|
exit_action = file_menu.addAction("Salir")
|
||||||
|
exit_action.triggered.connect(self.close)
|
||||||
|
|
||||||
|
# Menú Ver
|
||||||
|
view_menu = menubar.addMenu("Ver")
|
||||||
|
toggle_latex_action = view_menu.addAction("Mostrar/Ocultar LaTeX")
|
||||||
|
toggle_latex_action.setShortcut(QKeySequence("F12"))
|
||||||
|
toggle_latex_action.triggered.connect(self.toggle_latex_panel)
|
||||||
|
|
||||||
|
# Menú Ayuda
|
||||||
|
help_menu = menubar.addMenu("Ayuda")
|
||||||
|
about_action = help_menu.addAction("Acerca de...")
|
||||||
|
about_action.triggered.connect(self.show_about)
|
||||||
|
|
||||||
|
def create_status_bar(self):
|
||||||
|
"""Crea la barra de estado"""
|
||||||
|
self.statusBar().showMessage("🔢 Calculadora MAV - Sistema Algebraico Híbrido")
|
||||||
|
|
||||||
|
def toggle_latex_panel(self):
|
||||||
|
"""Muestra/oculta el panel LaTeX"""
|
||||||
|
self.latex_panel_visible = not self.latex_panel_visible
|
||||||
|
|
||||||
|
if self.latex_panel_visible:
|
||||||
|
self.latex_panel.show()
|
||||||
|
else:
|
||||||
|
self.latex_panel.hide()
|
||||||
|
|
||||||
|
def new_session(self):
|
||||||
|
"""Inicia una nueva sesión"""
|
||||||
|
self.input_text.clear()
|
||||||
|
self.output_text.clear()
|
||||||
|
self.latex_panel.clear_equations()
|
||||||
|
self.statusBar().showMessage("✨ Nueva sesión iniciada", 2000)
|
||||||
|
|
||||||
|
def load_file(self):
|
||||||
|
"""Carga un archivo"""
|
||||||
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self, "Abrir archivo", "", "Archivos de texto (*.txt);;Todos los archivos (*)"
|
||||||
|
)
|
||||||
|
if file_path:
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
self.input_text.setPlainText(content)
|
||||||
|
self.statusBar().showMessage(f"📁 Archivo cargado: {Path(file_path).name}", 3000)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(self, "Error", f"No se pudo cargar el archivo:\n{e}")
|
||||||
|
|
||||||
|
def save_file(self):
|
||||||
|
"""Guarda el contenido actual"""
|
||||||
|
file_path, _ = QFileDialog.getSaveFileName(
|
||||||
|
self, "Guardar archivo", "", "Archivos de texto (*.txt);;Todos los archivos (*)"
|
||||||
|
)
|
||||||
|
if file_path:
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(self.input_text.toPlainText())
|
||||||
|
self.statusBar().showMessage(f"💾 Archivo guardado: {Path(file_path).name}", 3000)
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.warning(self, "Error", f"No se pudo guardar el archivo:\n{e}")
|
||||||
|
|
||||||
|
def show_about(self):
|
||||||
|
"""Muestra información sobre la aplicación"""
|
||||||
|
QMessageBox.about(
|
||||||
|
self,
|
||||||
|
"Acerca de Calculadora MAV",
|
||||||
|
"""
|
||||||
|
<h2>Calculadora MAV - CAS Híbrido</h2>
|
||||||
|
<p><b>Versión PySide6 con diseño minimalista</b></p>
|
||||||
|
<p>Sistema algebraico computacional híbrido con 3 paneles:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Panel de entrada (izquierda)</li>
|
||||||
|
<li>Panel de resultados 1:1 (centro)</li>
|
||||||
|
<li>Panel LaTeX/MathJax (derecha)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Motor algebraico: <b>SymPy</b> con tipos personalizados</p>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_settings(self) -> Dict[str, Any]:
|
||||||
|
"""Carga configuración desde archivo"""
|
||||||
|
try:
|
||||||
|
if Path(self.SETTINGS_FILE).exists():
|
||||||
|
with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"No se pudo cargar configuración: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"window_geometry": "1400x800",
|
||||||
|
"debug_mode": False,
|
||||||
|
"latex_panel_visible": True
|
||||||
|
}
|
||||||
|
|
||||||
|
def save_settings(self):
|
||||||
|
"""Guarda configuración a archivo"""
|
||||||
|
try:
|
||||||
|
settings = {
|
||||||
|
"window_geometry": f"{self.width()}x{self.height()}",
|
||||||
|
"debug_mode": self.debug,
|
||||||
|
"latex_panel_visible": self.latex_panel_visible
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(settings, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error guardando configuración: {e}")
|
||||||
|
|
||||||
|
def load_history(self):
|
||||||
|
"""Carga historial desde archivo"""
|
||||||
|
try:
|
||||||
|
if Path(self.HISTORY_FILE).exists():
|
||||||
|
with open(self.HISTORY_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
history = f.read().strip()
|
||||||
|
if history:
|
||||||
|
self.input_text.setPlainText(history)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"No se pudo cargar historial: {e}")
|
||||||
|
|
||||||
|
def save_history(self):
|
||||||
|
"""Guarda historial a archivo"""
|
||||||
|
try:
|
||||||
|
with open(self.HISTORY_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(self.input_text.toPlainText())
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error guardando historial: {e}")
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Maneja el evento de cierre de la aplicación"""
|
||||||
|
self.save_settings()
|
||||||
|
self.save_history()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Función principal"""
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationName("Calculadora MAV")
|
||||||
|
app.setApplicationVersion("2.0.0")
|
||||||
|
|
||||||
|
# Configurar estilo
|
||||||
|
app.setStyle('Fusion')
|
||||||
|
|
||||||
|
# Crear y mostrar ventana principal
|
||||||
|
window = HybridCalculatorPySide6()
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
# Ejecutar aplicación
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -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
|
||||||
|
|
359
test.py
359
test.py
|
@ -1,37 +1,332 @@
|
||||||
import webview
|
# OPCIÓN 1: PySide6 (RECOMENDADA)
|
||||||
|
# pip install PySide6
|
||||||
|
|
||||||
def create_webview_app():
|
import sys
|
||||||
html_content = """
|
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QHBoxLayout
|
||||||
<!DOCTYPE html>
|
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||||
<html>
|
from PySide6.QtCore import QUrl
|
||||||
<head>
|
|
||||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
class MathJaxPySide6(QMainWindow):
|
||||||
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
|
def __init__(self):
|
||||||
<script>
|
super().__init__()
|
||||||
MathJax = {
|
self.setWindowTitle("🧮 MathJax con PySide6")
|
||||||
tex: { inlineMath: [['$', '$']], displayMath: [['$$', '$$']] }
|
self.setGeometry(100, 100, 1200, 800)
|
||||||
};
|
|
||||||
</script>
|
# Widget central
|
||||||
<style>
|
central_widget = QWidget()
|
||||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
self.setCentralWidget(central_widget)
|
||||||
.demo { background: #f0f0f0; padding: 10px; margin: 10px 0; }
|
|
||||||
</style>
|
# Layout principal
|
||||||
</head>
|
layout = QVBoxLayout(central_widget)
|
||||||
<body>
|
|
||||||
<h1>MathJax con webview</h1>
|
# Botones de control
|
||||||
<div class="demo">
|
button_layout = QHBoxLayout()
|
||||||
<p>Ecuación simple: $x^2 + y^2 = z^2$</p>
|
|
||||||
<p>Ecuación compleja:</p>
|
btn_reload = QPushButton("🔄 Recargar")
|
||||||
$$\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}$$
|
btn_zoom_in = QPushButton("🔍+ Zoom In")
|
||||||
</div>
|
btn_zoom_out = QPushButton("🔍- Zoom Out")
|
||||||
</body>
|
btn_fullscreen = QPushButton("📺 Pantalla Completa")
|
||||||
</html>
|
|
||||||
"""
|
btn_reload.clicked.connect(self.reload_page)
|
||||||
|
btn_zoom_in.clicked.connect(self.zoom_in)
|
||||||
|
btn_zoom_out.clicked.connect(self.zoom_out)
|
||||||
|
btn_fullscreen.clicked.connect(self.toggle_fullscreen)
|
||||||
|
|
||||||
|
button_layout.addWidget(btn_reload)
|
||||||
|
button_layout.addWidget(btn_zoom_in)
|
||||||
|
button_layout.addWidget(btn_zoom_out)
|
||||||
|
button_layout.addWidget(btn_fullscreen)
|
||||||
|
button_layout.addStretch()
|
||||||
|
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
# Vista web
|
||||||
|
self.web_view = QWebEngineView()
|
||||||
|
layout.addWidget(self.web_view)
|
||||||
|
|
||||||
|
# HTML con MathJax avanzado
|
||||||
|
self.html_content = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>MathJax Avanzado</title>
|
||||||
|
<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: [['$$', '$$'], ['\\\\[', '\\\\]']],
|
||||||
|
packages: {'[+]': ['ams', 'color', 'physics']}
|
||||||
|
},
|
||||||
|
chtml: {
|
||||||
|
scale: 1.3,
|
||||||
|
minScale: 0.5,
|
||||||
|
matchFontHeight: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
margin: 30px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 { color: #2c3e50; text-align: center; margin-bottom: 40px; }
|
||||||
|
.section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-left: 5px solid #007bff;
|
||||||
|
padding: 25px;
|
||||||
|
margin: 25px 0;
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
.section h3 {
|
||||||
|
color: #495057;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.interactive {
|
||||||
|
background: #e8f5e8;
|
||||||
|
border-left-color: #28a745;
|
||||||
|
}
|
||||||
|
.physics {
|
||||||
|
background: #fff3cd;
|
||||||
|
border-left-color: #ffc107;
|
||||||
|
}
|
||||||
|
.advanced {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-left-color: #dc3545;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
#dynamic-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🚀 Laboratorio Matemático Avanzado</h1>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>📊 Álgebra Lineal</h3>
|
||||||
|
<p>Determinante de matriz 3×3:</p>
|
||||||
|
$$\\det(A) = \\begin{vmatrix}
|
||||||
|
a_{11} & a_{12} & a_{13} \\\\
|
||||||
|
a_{21} & a_{22} & a_{23} \\\\
|
||||||
|
a_{31} & a_{32} & a_{33}
|
||||||
|
\\end{vmatrix}$$
|
||||||
|
|
||||||
|
<p>Eigenvalores: $\\det(A - \\lambda I) = 0$</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section physics">
|
||||||
|
<h3>⚛️ Física Cuántica</h3>
|
||||||
|
<p>Operador Hamiltoniano:</p>
|
||||||
|
$$\\hat{H} = \\frac{-\\hbar^2}{2m}\\nabla^2 + V(\\mathbf{r})$$
|
||||||
|
|
||||||
|
<p>Función de onda normalizada:</p>
|
||||||
|
$$\\int_{-\\infty}^{\\infty} |\\Psi(x,t)|^2 dx = 1$$
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section advanced">
|
||||||
|
<h3>🔬 Matemáticas Avanzadas</h3>
|
||||||
|
<p>Transformada de Fourier:</p>
|
||||||
|
$$\\mathcal{F}[f(t)] = \\int_{-\\infty}^{\\infty} f(t) e^{-2\\pi i \\xi t} dt$$
|
||||||
|
|
||||||
|
<p>Función Gamma:</p>
|
||||||
|
$$\\Gamma(z) = \\int_0^\\infty t^{z-1} e^{-t} dt$$
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section interactive">
|
||||||
|
<h3>🎮 Generador Interactivo</h3>
|
||||||
|
<button onclick="addCalculusExample()">📈 Cálculo</button>
|
||||||
|
<button onclick="addStatisticsExample()">📊 Estadística</button>
|
||||||
|
<button onclick="addGeometryExample()">📐 Geometría</button>
|
||||||
|
<button onclick="addPhysicsExample()">⚛️ Física</button>
|
||||||
|
<button onclick="clearContent()">🧹 Limpiar</button>
|
||||||
|
|
||||||
|
<div id="dynamic-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const examples = {
|
||||||
|
calculus: [
|
||||||
|
{
|
||||||
|
title: "Regla de L'Hôpital",
|
||||||
|
eq: "$$\\\\lim_{x \\\\to c} \\\\frac{f(x)}{g(x)} = \\\\lim_{x \\\\to c} \\\\frac{f'(x)}{g'(x)}$$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Integración por partes",
|
||||||
|
eq: "$$\\\\int u \\\\, dv = uv - \\\\int v \\\\, du$$"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
statistics: [
|
||||||
|
{
|
||||||
|
title: "Distribución Normal",
|
||||||
|
eq: "$$f(x) = \\\\frac{1}{\\\\sigma\\\\sqrt{2\\\\pi}} e^{-\\\\frac{(x-\\\\mu)^2}{2\\\\sigma^2}}$$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Teorema Central del Límite",
|
||||||
|
eq: "$$\\\\bar{X}_n \\\\xrightarrow{d} \\\\mathcal{N}\\\\left(\\\\mu, \\\\frac{\\\\sigma^2}{n}\\\\right)$$"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
geometry: [
|
||||||
|
{
|
||||||
|
title: "Volumen de esfera",
|
||||||
|
eq: "$$V = \\\\frac{4}{3}\\\\pi r^3$$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Ley de cosenos",
|
||||||
|
eq: "$$c^2 = a^2 + b^2 - 2ab\\\\cos(C)$$"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
physics: [
|
||||||
|
{
|
||||||
|
title: "Ecuaciones de Maxwell",
|
||||||
|
eq: "$$\\\\nabla \\\\cdot \\\\mathbf{E} = \\\\frac{\\\\rho}{\\\\epsilon_0}$$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Relatividad Especial",
|
||||||
|
eq: "$$t' = \\\\gamma \\\\left(t - \\\\frac{vx}{c^2}\\\\right)$$"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function addExample(category) {
|
||||||
|
const categoryExamples = examples[category];
|
||||||
|
const randomExample = categoryExamples[Math.floor(Math.random() * categoryExamples.length)];
|
||||||
|
|
||||||
|
const content = document.getElementById('dynamic-content');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'section';
|
||||||
|
div.innerHTML = `
|
||||||
|
<h4>➕ ${randomExample.title}</h4>
|
||||||
|
${randomExample.eq}
|
||||||
|
`;
|
||||||
|
content.appendChild(div);
|
||||||
|
|
||||||
|
MathJax.typesetPromise([div]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCalculusExample() { addExample('calculus'); }
|
||||||
|
function addStatisticsExample() { addExample('statistics'); }
|
||||||
|
function addGeometryExample() { addExample('geometry'); }
|
||||||
|
function addPhysicsExample() { addExample('physics'); }
|
||||||
|
|
||||||
|
function clearContent() {
|
||||||
|
document.getElementById('dynamic-content').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
MathJax.startup.promise.then(() => {
|
||||||
|
console.log('✅ MathJax cargado en PySide6!');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Cargar contenido
|
||||||
|
self.web_view.setHtml(self.html_content)
|
||||||
|
|
||||||
webview.create_window('MathJax App', html=html_content, width=800, height=600)
|
def reload_page(self):
|
||||||
webview.start()
|
self.web_view.setHtml(self.html_content)
|
||||||
|
|
||||||
|
def zoom_in(self):
|
||||||
|
current_zoom = self.web_view.zoomFactor()
|
||||||
|
self.web_view.setZoomFactor(current_zoom * 1.2)
|
||||||
|
|
||||||
|
def zoom_out(self):
|
||||||
|
current_zoom = self.web_view.zoomFactor()
|
||||||
|
self.web_view.setZoomFactor(current_zoom / 1.2)
|
||||||
|
|
||||||
|
def toggle_fullscreen(self):
|
||||||
|
if self.isFullScreen():
|
||||||
|
self.showNormal()
|
||||||
|
else:
|
||||||
|
self.showFullScreen()
|
||||||
|
|
||||||
|
# OPCIÓN 2: PyQt6 (Alternativa)
|
||||||
|
# pip install PyQt6 PyQt6-WebEngine
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
|
||||||
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
|
|
||||||
|
class MathJaxPyQt6(QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("MathJax con PyQt6")
|
||||||
|
self.setGeometry(100, 100, 1000, 700)
|
||||||
|
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
layout = QVBoxLayout(central_widget)
|
||||||
|
|
||||||
|
self.web_view = QWebEngineView()
|
||||||
|
layout.addWidget(self.web_view)
|
||||||
|
|
||||||
|
# Mismo HTML que PySide6
|
||||||
|
html_content = "[MISMO HTML DE ARRIBA]"
|
||||||
|
self.web_view.setHtml(html_content)
|
||||||
|
|
||||||
|
def run_pyqt6():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = MathJaxPyQt6()
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run_pyside6():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = MathJaxPySide6()
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
# Función principal para elegir método
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
create_webview_app()
|
print("🧮 Iniciando aplicación MathJax con PySide6...")
|
||||||
|
print("✨ Funciones disponibles:")
|
||||||
|
print(" - Zoom in/out")
|
||||||
|
print(" - Pantalla completa")
|
||||||
|
print(" - Generador interactivo de ecuaciones")
|
||||||
|
print(" - MathJax completamente funcional")
|
||||||
|
|
||||||
|
run_pyside6()
|
||||||
|
|
||||||
|
# RESUMEN DE INSTALACIÓN:
|
||||||
|
#
|
||||||
|
# Para PySide6 (RECOMENDADO):
|
||||||
|
# pip install PySide6
|
||||||
|
#
|
||||||
|
# Para PyQt6:
|
||||||
|
# pip install PyQt6 PyQt6-WebEngine
|
||||||
|
#
|
||||||
|
# Ambos funcionan igual de bien para MathJax,
|
||||||
|
# pero PySide6 tiene licencia más permisiva.
|
|
@ -0,0 +1,212 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script de prueba para la versión PySide6 de Calculadora MAV
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def test_imports():
|
||||||
|
"""Prueba todas las importaciones necesarias"""
|
||||||
|
print("🧪 Probando importaciones...")
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
("PySide6.QtWidgets", "QApplication"),
|
||||||
|
("PySide6.QtCore", "QThread"),
|
||||||
|
("PySide6.QtGui", "QFont"),
|
||||||
|
("PySide6.QtWebEngineWidgets", "QWebEngineView"),
|
||||||
|
("sympy", None),
|
||||||
|
("numpy", None),
|
||||||
|
("matplotlib", None),
|
||||||
|
]
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
for module, attr in tests:
|
||||||
|
try:
|
||||||
|
if attr:
|
||||||
|
mod = __import__(module, fromlist=[attr])
|
||||||
|
getattr(mod, attr)
|
||||||
|
else:
|
||||||
|
__import__(module)
|
||||||
|
print(f" ✅ {module}" + (f".{attr}" if attr else ""))
|
||||||
|
success_count += 1
|
||||||
|
except ImportError as e:
|
||||||
|
print(f" ❌ {module}" + (f".{attr}" if attr else "") + f" - {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ {module}" + (f".{attr}" if attr else "") + f" - {e}")
|
||||||
|
|
||||||
|
print(f"\n📊 Resultado: {success_count}/{len(tests)} módulos disponibles")
|
||||||
|
return success_count == len(tests)
|
||||||
|
|
||||||
|
def test_application_creation():
|
||||||
|
"""Prueba la creación básica de la aplicación"""
|
||||||
|
print("\n🏗️ Probando creación de aplicación...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
from PySide6.QtCore import QCoreApplication
|
||||||
|
|
||||||
|
# Crear aplicación mínima
|
||||||
|
if not QCoreApplication.instance():
|
||||||
|
app = QApplication([])
|
||||||
|
else:
|
||||||
|
app = QCoreApplication.instance()
|
||||||
|
|
||||||
|
print(" ✅ QApplication creada correctamente")
|
||||||
|
|
||||||
|
# Probar importación de nuestra aplicación
|
||||||
|
from main_calc_app_pyside6 import HybridCalculatorPySide6
|
||||||
|
print(" ✅ Clase HybridCalculatorPySide6 importada")
|
||||||
|
|
||||||
|
# Probar creación de ventana (sin mostrar)
|
||||||
|
window = HybridCalculatorPySide6()
|
||||||
|
print(" ✅ Ventana principal creada")
|
||||||
|
|
||||||
|
# Verificar componentes principales
|
||||||
|
assert hasattr(window, 'input_text'), "input_text no encontrado"
|
||||||
|
assert hasattr(window, 'output_text'), "output_text no encontrado"
|
||||||
|
assert hasattr(window, 'latex_panel'), "latex_panel no encontrado"
|
||||||
|
assert hasattr(window, 'engine'), "engine no encontrado"
|
||||||
|
print(" ✅ Componentes principales verificados")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_engine_functionality():
|
||||||
|
"""Prueba la funcionalidad del motor de cálculo"""
|
||||||
|
print("\n⚙️ Probando motor de cálculo...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from main_evaluation_puro import PureAlgebraicEngine
|
||||||
|
|
||||||
|
engine = PureAlgebraicEngine()
|
||||||
|
print(" ✅ Motor PureAlgebraicEngine creado")
|
||||||
|
|
||||||
|
# Prueba básica
|
||||||
|
result = engine.evaluate_line("2 + 2")
|
||||||
|
print(f" ✅ Evaluación básica: 2 + 2 = {result.output if result.success else result.error_message}")
|
||||||
|
|
||||||
|
# Prueba simbólica
|
||||||
|
result = engine.evaluate_line("x**2 + y**2")
|
||||||
|
print(f" ✅ Evaluación simbólica: x**2 + y**2 = {result.output if result.success else result.error_message}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_mathjax_html():
|
||||||
|
"""Prueba la generación de HTML MathJax"""
|
||||||
|
print("\n📐 Probando generación MathJax...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from main_calc_app_pyside6 import MathJaxPanel
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
from PySide6.QtCore import QCoreApplication
|
||||||
|
|
||||||
|
if not QCoreApplication.instance():
|
||||||
|
app = QApplication([])
|
||||||
|
|
||||||
|
panel = MathJaxPanel()
|
||||||
|
print(" ✅ Panel MathJax creado")
|
||||||
|
|
||||||
|
# Verificar HTML base
|
||||||
|
html = panel.generate_base_html()
|
||||||
|
assert "MathJax" in html, "MathJax no encontrado en HTML"
|
||||||
|
assert "equation" in html, "Estilos de ecuación no encontrados"
|
||||||
|
print(" ✅ HTML base generado correctamente")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_minimal_app():
|
||||||
|
"""Ejecuta una versión mínima de la aplicación para prueba visual"""
|
||||||
|
print("\n🖥️ Iniciando prueba visual (cierra la ventana para continuar)...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
from main_calc_app_pyside6 import HybridCalculatorPySide6
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = HybridCalculatorPySide6()
|
||||||
|
|
||||||
|
# Precargar algunos datos de prueba
|
||||||
|
window.input_text.setPlainText("# Prueba PySide6\nx**2 + y**2 = r**2\nsolve(x**2 - 4, x)")
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
print(" ✅ Aplicación mostrada - cierra la ventana para continuar")
|
||||||
|
|
||||||
|
# No ejecutar el loop principal, solo mostrar
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Función principal de pruebas"""
|
||||||
|
print("🚀 Iniciando pruebas de Calculadora MAV PySide6")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
tests = [
|
||||||
|
("Importaciones", test_imports),
|
||||||
|
("Creación de aplicación", test_application_creation),
|
||||||
|
("Motor de cálculo", test_engine_functionality),
|
||||||
|
("HTML MathJax", test_mathjax_html),
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for test_name, test_func in tests:
|
||||||
|
try:
|
||||||
|
result = test_func()
|
||||||
|
results.append((test_name, result))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"💥 Error inesperado en {test_name}: {e}")
|
||||||
|
results.append((test_name, False))
|
||||||
|
|
||||||
|
# Resumen de resultados
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("📋 RESUMEN DE PRUEBAS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
total = len(results)
|
||||||
|
|
||||||
|
for test_name, success in results:
|
||||||
|
status = "✅ PASÓ" if success else "❌ FALLÓ"
|
||||||
|
print(f" {status:<10} {test_name}")
|
||||||
|
if success:
|
||||||
|
passed += 1
|
||||||
|
|
||||||
|
print(f"\n📊 Resultado final: {passed}/{total} pruebas pasaron")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("🎉 ¡Todas las pruebas pasaron! La aplicación está lista.")
|
||||||
|
|
||||||
|
# Ofrecer ejecutar prueba visual
|
||||||
|
try:
|
||||||
|
response = input("\n¿Quieres ejecutar una prueba visual? (s/N): ").strip().lower()
|
||||||
|
if response in ['s', 'sí', 'si', 'y', 'yes']:
|
||||||
|
run_minimal_app()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🚪 Prueba cancelada por el usuario")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("⚠️ Algunas pruebas fallaron. Revisa los errores antes de ejecutar la aplicación.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Prueba simple del motor PureAlgebraicEngine para verificar la API
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def test_engine():
|
||||||
|
"""Prueba básica del motor"""
|
||||||
|
print("🧪 Probando PureAlgebraicEngine...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Importar el motor
|
||||||
|
from main_evaluation_puro import PureAlgebraicEngine
|
||||||
|
print(" ✅ Import exitoso")
|
||||||
|
|
||||||
|
# Crear instancia
|
||||||
|
engine = PureAlgebraicEngine()
|
||||||
|
print(" ✅ Instancia creada")
|
||||||
|
|
||||||
|
# Pruebas básicas
|
||||||
|
test_cases = [
|
||||||
|
"2 + 2",
|
||||||
|
"x**2 + y**2",
|
||||||
|
"solve(x**2 - 4, x)",
|
||||||
|
"# Esto es un comentario",
|
||||||
|
"a = 5*x + 3",
|
||||||
|
"x**2 + y**2 = r**2"
|
||||||
|
]
|
||||||
|
|
||||||
|
print("\n📝 Ejecutando casos de prueba:")
|
||||||
|
for i, test_case in enumerate(test_cases, 1):
|
||||||
|
try:
|
||||||
|
result = engine.evaluate_line(test_case)
|
||||||
|
status = "✅" if result.success else "❌"
|
||||||
|
output = result.output if result.success else result.error_message
|
||||||
|
print(f" {i}. {status} '{test_case}' → {output}")
|
||||||
|
|
||||||
|
# Información adicional del resultado
|
||||||
|
if result.success:
|
||||||
|
print(f" Tipo: {result.result_type}, Asignación: {result.is_assignment}, Ecuación: {result.is_equation}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {i}. ❌ '{test_case}' → ERROR: {e}")
|
||||||
|
|
||||||
|
print("\n✅ Pruebas del motor completadas")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error en prueba del motor: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Función principal"""
|
||||||
|
print("🚀 Prueba Simple del Motor Algebraico")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
success = test_engine()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n🎉 ¡Motor funcionando correctamente!")
|
||||||
|
print(" Puedes proceder a usar la aplicación PySide6")
|
||||||
|
else:
|
||||||
|
print("\n⚠️ Problemas detectados en el motor")
|
||||||
|
print(" Revisa los errores antes de usar la aplicación")
|
||||||
|
|
||||||
|
return 0 if success else 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
Loading…
Reference in New Issue