Calc/app/gui_latex.py

505 lines
19 KiB
Python

"""
Sistema de Panel LaTeX para la Calculadora MAV CAS Híbrida
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QTextBrowser
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QFont
from PySide6.QtWebEngineWidgets import QWebEngineView
import logging
from typing import List, Dict, Optional
from .evaluation import EvaluationResult
import sympy
class LatexPanel(QWidget):
"""Panel LaTeX con WebEngine o fallback a HTML"""
def __init__(self, parent=None):
super().__init__(parent)
self.equations = []
self._webview_available = False
self._mathjax_ready = False
self._pending_equations = []
self._parent_calculator = parent
self._webview_initialized = False
self._setup_ui()
# Timer para verificar si MathJax está listo (inicializado bajo demanda)
self._mathjax_check_timer = None
def _setup_ui(self):
"""Configura la UI del panel"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Header
header = QFrame()
header.setFixedHeight(30)
header.setStyleSheet("background-color: #1a1a1a; border-bottom: 1px solid #3c3c3c;")
header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(10, 0, 10, 0)
title = QLabel("📐 Ecuaciones LaTeX")
title.setStyleSheet("color: #80c7f7; font-weight: bold;")
header_layout.addWidget(title)
header_layout.addStretch()
layout.addWidget(header)
# Intentar crear WebEngineView
try:
self.webview = QWebEngineView()
self.webview.setContextMenuPolicy(Qt.NoContextMenu)
self._setup_webview()
layout.addWidget(self.webview)
self._webview_available = True
logging.debug("✅ WebEngineView disponible para LaTeX")
except Exception as e:
logging.warning(f"⚠️ WebEngineView no disponible: {e}")
# Fallback a QTextBrowser
self.text_browser = QTextBrowser()
self.text_browser.setOpenExternalLinks(False)
self._setup_text_browser()
layout.addWidget(self.text_browser)
self._webview_available = False
def _setup_webview(self):
"""Configura WebEngineView con MathJax (inicialización perezosa)"""
# No cargar HTML inmediatamente para optimizar tiempo de inicio
pass
def _setup_text_browser(self):
"""Configura el browser de texto como fallback"""
self.text_browser.setStyleSheet("""
QTextBrowser {
background-color: #1a1a1a;
color: #d4d4d4;
border: none;
font-family: 'Consolas';
font-size: 11px;
padding: 10px;
}
""")
self.text_browser.setHtml("""
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Consolas';
padding: 10px;
}
.equation {
margin: 10px 0;
padding: 10px;
background: #2d2d2d;
border-left: 3px solid #80c7f7;
border-radius: 3px;
}
.comment { border-left-color: #6a9955; }
.assignment { border-left-color: #dcdcaa; }
</style>
<div style="text-align: center; margin-top: 50px; color: #808080;">
📐 Panel de Ecuaciones<br>
<small>Las ecuaciones aparecerán aquí</small>
</div>
""")
def _generate_mathjax_html(self):
"""Genera HTML base con MathJax configurado para SVG"""
return """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<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-svg.js"></script>
<script>
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true
},
svg: {
scale: 0.9,
minScale: 0.5,
fontCache: 'global'
},
startup: {
ready: function () {
MathJax.startup.defaultReady();
// Notificar que MathJax está listo
window.mathJaxReady = true;
console.log('MathJax SVG completamente cargado');
}
}
};
</script>
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 8px;
line-height: 1.2;
}
.equation-block {
background: rgba(45, 45, 48, 0.8);
border-left: 3px solid;
margin: 4px 0;
padding: 6px 12px;
border-radius: 4px;
transition: all 0.1s ease;
}
.equation-block:hover {
background: rgba(45, 45, 48, 0.9);
}
.comment { border-left-color: #6a9955; }
.assignment { border-left-color: #dcdcaa; }
.equation { border-left-color: #c586c0; }
.symbolic { border-left-color: #9cdcfe; }
.math-content {
margin: 2px 0;
font-size: 14px;
}
.comment-text {
font-style: italic;
color: #6a9955;
font-size: 12px;
margin: 0;
}
.info-message {
text-align: center;
padding: 20px;
color: #666;
font-size: 12px;
}
/* Optimizaciones para SVG rendering */
mjx-container[jax="SVG"] {
margin: 2px 0 !important;
}
mjx-container[jax="SVG"] svg {
vertical-align: middle;
}
/* Mejorar contraste del SVG */
mjx-container[jax="SVG"] svg text {
fill: #d4d4d4 !important;
}
</style>
</head>
<body>
<div id="equations-container">
<div class="info-message">
Panel de Ecuaciones LaTeX (SVG)
</div>
</div>
<script>
window.mathJaxReady = false;
function addEquation(type, content) {
var container = document.getElementById('equations-container');
// Limpiar mensaje inicial si existe
var infoMsg = container.querySelector('.info-message');
if (infoMsg) infoMsg.remove();
var equation = document.createElement('div');
equation.className = 'equation-block ' + type;
var mathContent = document.createElement('div');
mathContent.className = 'math-content';
if (type === 'comment') {
mathContent.className = 'comment-text';
mathContent.textContent = content;
} else {
mathContent.innerHTML = '$$' + content + '$$';
}
equation.appendChild(mathContent);
container.appendChild(equation);
// Re-renderizar MathJax solo si está listo
if (window.MathJax && window.mathJaxReady && type !== 'comment') {
MathJax.typesetPromise([equation]).catch(function(err) {
console.error('MathJax SVG error:', err);
});
}
}
function clearEquations() {
var container = document.getElementById('equations-container');
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
}
// Función para notificar que está listo para renderizar
function triggerInitialRender() {
if (window.mathJaxReady) {
// Notificar a Python que MathJax está listo
console.log('MathJax SVG listo para renderizado inicial');
return true;
}
return false;
}
</script>
</body>
</html>"""
def _ensure_webview_initialized(self):
"""Inicializa WebEngine bajo demanda para optimizar tiempo de carga"""
if self._webview_initialized:
return
try:
if hasattr(self, 'webview') and self._webview_available:
html_content = self._generate_mathjax_html()
self.webview.setHtml(html_content)
# Inicializar timer de verificación MathJax
if self._mathjax_check_timer is None:
self._mathjax_check_timer = QTimer()
self._mathjax_check_timer.timeout.connect(self._check_mathjax_ready)
self._mathjax_check_timer.start(500) # Verificar cada 500ms
self._webview_initialized = True
logging.debug("✅ WebEngine LaTeX inicializado bajo demanda")
except Exception as e:
logging.error(f"Error inicializando WebEngine: {e}")
def _check_mathjax_ready(self):
"""Verifica si MathJax está listo y renderiza ecuaciones pendientes"""
if not self._webview_available:
return
# Verificar si MathJax está listo
self.webview.page().runJavaScript(
"window.mathJaxReady || false;",
self._on_mathjax_ready_check
)
def _on_mathjax_ready_check(self, ready):
"""Callback cuando se verifica el estado de MathJax"""
if ready and not self._mathjax_ready:
self._mathjax_ready = True
self._mathjax_check_timer.stop()
logging.debug("✅ MathJax listo, procesando ecuaciones pendientes")
# Renderizar ecuaciones pendientes
for eq in self._pending_equations:
self._add_equation_to_webview(eq['type'], eq['content'])
self._pending_equations.clear()
# Trigger initial render si hay un calculador padre
if self._parent_calculator and hasattr(self._parent_calculator, '_trigger_initial_latex_render'):
self._parent_calculator._trigger_initial_latex_render()
def _add_equation_to_webview(self, eq_type: str, content: str):
"""Añade una ecuación directamente al webview"""
if self._webview_available:
escaped_content = content.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"')
js_code = f"addEquation('{eq_type}', '{escaped_content}');"
self.webview.page().runJavaScript(js_code)
def add_equation(self, eq_type: str, content: str):
"""Añade una ecuación al panel"""
self.equations.append({'type': eq_type, 'content': content})
if self._webview_available:
# Inicializar WebEngine bajo demanda
self._ensure_webview_initialized()
if self._mathjax_ready:
# MathJax está listo, renderizar inmediatamente
self._add_equation_to_webview(eq_type, content)
else:
# MathJax no está listo, guardar para después
self._pending_equations.append({'type': eq_type, 'content': content})
else:
# Actualizar HTML en text browser
self._update_text_browser()
def clear_equations(self):
"""Limpia todas las ecuaciones"""
self.equations.clear()
self._pending_equations.clear()
if self._webview_available and self._webview_initialized:
# Usar JavaScript para limpiar dinámicamente si MathJax está listo
if self._mathjax_ready:
self.webview.page().runJavaScript("clearEquations();")
else:
# Si MathJax no está listo, recargar HTML base limpio
html_content = self._generate_mathjax_html()
self.webview.setHtml(html_content)
elif hasattr(self, 'text_browser'):
# Para fallback o WebEngine no inicializado, solo si text_browser existe
self._setup_text_browser() # Reset al estado inicial
def _update_text_browser(self):
"""Actualiza el contenido del text browser (fallback)"""
# Verificar que text_browser existe antes de usarlo
if not hasattr(self, 'text_browser'):
return
html_parts = ["""
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Consolas';
margin: 0;
padding: 8px;
line-height: 1.2;
}
.equation {
margin: 4px 0;
padding: 6px 12px;
background: rgba(45, 45, 48, 0.8);
border-left: 3px solid #80c7f7;
border-radius: 4px;
font-size: 12px;
}
.comment {
border-left-color: #6a9955;
font-style: italic;
color: #6a9955;
font-size: 11px;
}
.assignment { border-left-color: #dcdcaa; }
.equation-type { border-left-color: #c586c0; }
.symbolic { border-left-color: #9cdcfe; }
code {
font-family: 'Consolas';
font-size: 11px;
color: #d4d4d4;
}
</style>
"""]
for eq in self.equations:
eq_type = eq['type']
content = eq['content']
css_class = eq_type
if eq_type == 'comment':
html_parts.append(f'<div class="equation {css_class}">{content}</div>')
else:
# Para ecuaciones matemáticas, mostrar en formato de código
html_parts.append(f'<div class="equation {css_class}"><code>{content}</code></div>')
self.text_browser.setHtml(''.join(html_parts))
class LatexProcessor:
"""Procesador de contenido LaTeX"""
@staticmethod
def generate_complete_latex_content(result: EvaluationResult) -> str:
"""Genera contenido LaTeX completo incluyendo toda la información del output"""
try:
# Para comentarios, usar el texto directamente
if result.result_type == "comment":
return result.output or ""
latex_parts = []
# PARTE 1: Contenido principal (con LaTeX de SymPy si es posible)
main_content = ""
if result.actual_result_object is not None:
try:
# Verificar si es una ecuación Eq()
if hasattr(result.actual_result_object, 'func') and result.actual_result_object.func.__name__ == 'Equality':
# Es una ecuación, convertir a formato var = valor
lhs = result.actual_result_object.lhs
rhs = result.actual_result_object.rhs
lhs_latex = sympy.latex(lhs)
rhs_latex = sympy.latex(rhs)
main_content = f"{lhs_latex} = {rhs_latex}"
else:
# Intentar generar LaTeX de SymPy para el objeto matemático
main_content = sympy.latex(result.actual_result_object)
# Para asignaciones, el resultado ya viene formateado
if result.is_assignment and not main_content:
# Extraer del output ya formateado
main_content = result.output.split("")[0] if "" in result.output else result.output
except Exception:
# Si falla el LaTeX de SymPy, usar el output textual
main_content = result.output or ""
else:
main_content = result.output or ""
latex_parts.append(main_content)
# PARTE 2: Aproximación numérica (si está disponible en el output)
if result.output and "" in result.output:
approx_parts = result.output.split("", 1)
if len(approx_parts) == 2:
approx_value = approx_parts[1].strip()
# Extraer solo la parte numérica antes del indicador de tipo
if ";" in approx_value:
approx_value = approx_value.split(";")[0].strip()
# La aproximación ya viene en formato var = valor, mantenerla así
latex_parts.append(f"\\approx {approx_value}")
# PARTE 3: Indicador de tipo (si está en el output)
if result.output and "[" in result.output and "]" in result.output:
# Extraer el indicador de tipo (ej: [=], [Equality], etc.)
parts = result.output.split("[")
if len(parts) >= 2:
type_part = "[" + parts[-1] # Tomar el último indicador
if "]" in type_part:
type_indicator = type_part.split("]")[0] + "]"
latex_parts.append(f"\\quad \\text{{{type_indicator}}}")
# Combinar todas las partes
complete_latex = " ".join(latex_parts)
# Limpiar caracteres problemáticos para MathJax
complete_latex = complete_latex.replace("__", "_{").replace("**", "^")
# Agregar llaves de cierre para subíndices
import re
complete_latex = re.sub(r'_\{(\w+)', r'_{\1}', complete_latex)
return complete_latex
except Exception as e:
logging.error(f"Error generando LaTeX completo: {e}")
# Fallback al output original
return result.output or ""
@staticmethod
def should_add_to_latex(result: EvaluationResult) -> tuple[bool, str]:
"""Determina si un resultado debe agregarse al panel LaTeX y qué tipo usar"""
try:
if result.result_type == "comment":
return True, "comment"
elif result.is_assignment:
return True, "assignment"
elif result.is_equation:
return True, "equation"
elif result.success and result.output:
# Agregar si tiene contenido matemático
math_indicators = ['=', '+', '-', '*', '/', '^', 'sqrt', 'sin', 'cos', 'tan', 'log', 'exp']
if any(indicator in result.output for indicator in math_indicators):
return True, "symbolic"
elif result.actual_result_object is not None and isinstance(result.actual_result_object, sympy.Basic):
return True, "symbolic"
return False, ""
except Exception:
return False, ""