Eliminación del archivo de historial de cálculos y ajustes en la configuración de la interfaz. Se mejora la gestión de resultados interactivos, incluyendo la actualización del panel de entrada al editar expresiones. Se optimizan los tags de salida para una mejor visualización de resultados y se implementa un nuevo sistema de redibujo para gráficos interactivos.

This commit is contained in:
Miguel 2025-06-06 16:32:25 +02:00
parent f0bdf1e419
commit 0488122229
6 changed files with 532 additions and 1480 deletions

View File

@ -1,30 +0,0 @@
y = 2*x + 3
x = z * 4
z = 8
y=?
z=?
x=?
# Instanciación via sympify
ip = IP4(120.11.255.2,30)
ip.Nodes()
ip.to_hex()
ip + 1
10.1.1.1
p=Dec(16#FF + 16#FF) / 18
p
t= 2#1010 + 16#f
t.to_base(8)
ip.bit_representation()
ip=ip+20
ip.bit_representation()
ip.mask()
ip.get_prefix_length()
a = b + 5
solve(x)

View File

@ -1,6 +1,6 @@
{ {
"window_geometry": "1020x700+356+1216", "window_geometry": "1020x700+387+1235",
"sash_pos_x": 347, "sash_pos_x": 355,
"symbolic_mode": true, "symbolic_mode": true,
"show_numeric_approximation": true, "show_numeric_approximation": true,
"keep_symbolic_fractions": true, "keep_symbolic_fractions": true,

View File

@ -298,6 +298,8 @@ CARACTERÍSTICAS:
def setup_interactive_manager(self): def setup_interactive_manager(self):
"""Configura el gestor de resultados interactivos""" """Configura el gestor de resultados interactivos"""
self.interactive_manager = InteractiveResultManager(self.root) self.interactive_manager = InteractiveResultManager(self.root)
# Configurar callback para actualizar el panel de entrada cuando se edite una expresión
self.interactive_manager.set_update_callback(self._update_input_expression)
def create_menu(self): def create_menu(self):
"""Crea el menú de la aplicación""" """Crea el menú de la aplicación"""
@ -372,25 +374,38 @@ CARACTERÍSTICAS:
self.output_text.bind("<MouseWheel>", _unified_mouse_wheel) self.output_text.bind("<MouseWheel>", _unified_mouse_wheel)
def setup_output_tags(self): def setup_output_tags(self):
"""Configura tags para coloreado de salida""" """Configura tags de formato para el panel de salida"""
self.output_text.tag_configure("error", foreground="#ff6b6b", font=("Consolas", 11, "bold")) default_font = self._get_input_font()
self.output_text.tag_configure("result", foreground="#abdbe3")
self.output_text.tag_configure("symbolic", foreground="#82aaff")
self.output_text.tag_configure("numeric", foreground="#c3e88d")
self.output_text.tag_configure("equation", foreground="#c792ea")
self.output_text.tag_configure("info", foreground="#ffcb6b")
self.output_text.tag_configure("comment", foreground="#546e7a")
self.output_text.tag_configure("class_hint", foreground="#888888")
self.output_text.tag_configure("type_hint", foreground="#6a6a6a")
# Tags para tipos especializados (genéricos para cualquier tipo) # Crear una fuente específica para errores (bold)
self.output_text.tag_configure("custom_type", foreground="#f9a825") error_font = tkFont.Font(family=default_font.cget("family"), size=default_font.cget("size"), weight="bold")
self.output_text.tag_configure("hex", foreground="#f9a825")
self.output_text.tag_configure("bin", foreground="#4fc3f7") # Tag base
self.output_text.tag_configure("ip", foreground="#fff176") self.output_text.tag_configure("base", font=default_font, foreground="#d4d4d4")
self.output_text.tag_configure("date", foreground="#ff8a80")
self.output_text.tag_configure("chr_type", foreground="#80cbc4") # Tags específicos
self.output_text.tag_configure("helper", foreground="#ffd700", font=("Consolas", 11, "italic")) # Sympy y tipos base
self.output_text.tag_configure("symbolic", foreground="#9cdcfe") # Azul claro (SymPy)
self.output_text.tag_configure("numeric", foreground="#b5cea8") # Verde (Números)
self.output_text.tag_configure("boolean", foreground="#569cd6") # Azul (Booleanos)
self.output_text.tag_configure("string", foreground="#ce9178") # Naranja (Strings)
# Tipos registrados dinámicamente (usar un color base)
self.output_text.tag_configure("custom_type", foreground="#4ec9b0") # Turquesa (Tipos Custom)
# Estado de la aplicación
self.output_text.tag_configure("error", foreground="#f44747", font=error_font) # Rojo
self.output_text.tag_configure("comment", foreground="#6a9955") # Verde Oliva (Comentarios)
self.output_text.tag_configure("assignment", foreground="#dcdcaa") # Amarillo (Asignaciones)
self.output_text.tag_configure("equation", foreground="#c586c0") # Púrpura (Ecuaciones)
self.output_text.tag_configure("plot", foreground="#569cd6", underline=True) # Azul con subrayado (Plots)
# Para el nuevo indicador de tipo algebraico
self.output_text.tag_configure("type_indicator", foreground="#808080") # Gris oscuro
# Configurar tags para tipos específicos si es necesario (ejemplo)
# self.output_text.tag_configure("IP4", foreground="#4ec9b0")
# self.output_text.tag_configure("IntBase", foreground="#4ec9b0")
def on_key_release(self, event=None): def on_key_release(self, event=None):
"""Maneja eventos de teclado""" """Maneja eventos de teclado"""
@ -670,28 +685,99 @@ CARACTERÍSTICAS:
self._display_output(output_data) self._display_output(output_data)
def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]: def _process_evaluation_result(self, result: EvaluationResult) -> List[tuple]:
"""Procesa el resultado de evaluación para display - ADAPTADO AL MOTOR PURO""" """Procesa el resultado de evaluación para display, priorizando resultados interactivos."""
output_parts = [] output_parts = []
indicator_text: Optional[str] = None
if result.result_type == "comment":
output_parts.append(("comment", result.output if result.output is not None else ""))
return output_parts
if not result.success: if not result.success:
# Error # Manejo de errores
ayuda = self.obtener_ayuda(result.input_line) error_prefix = "Error: "
if ayuda: main_error_message = f"{error_prefix}{result.error_message}"
ayuda_linea = ayuda.replace("\n", " ").replace("\r", " ")
if len(ayuda_linea) > 120: # Intentar obtener ayuda contextual para el error
ayuda_text = self.obtener_ayuda(result.input_line) # obtener_ayuda devuelve string o None
if ayuda_text:
ayuda_linea = ayuda_text.replace("\n", " ").replace("\r", " ").strip()
if len(ayuda_linea) > 120: # Acortar si es muy larga
ayuda_linea = ayuda_linea[:117] + "..." ayuda_linea = ayuda_linea[:117] + "..."
output_parts.append(("helper", ayuda_linea))
# Mostrar primero el error principal, luego la sugerencia
output_parts.append(("error", main_error_message))
output_parts.append( ("\n", "\n") ) # Separador para la ayuda
output_parts.append(("helper", f"Sugerencia: {ayuda_linea}"))
else: else:
output_parts.append(("error", f"Error: {result.error_message}")) output_parts.append(("error", main_error_message))
elif result.result_type == "comment": # No se añade type_indicator para errores aquí, el mensaje de error es suficiente.
output_parts.append(("comment", result.input_line))
elif result.result_type == "equation":
output_parts.append(("equation", result.output))
elif result.result_type == "symbolic":
output_parts.append(("symbolic", result.output))
else: else:
# Resultado general # RESULTADO EXITOSO:
output_parts.append(("result", result.output)) # 1. Intentar crear un tag interactivo
interactive_tag_info = self.interactive_manager.create_interactive_tag(
result.actual_result_object,
self.output_text
)
if interactive_tag_info:
tag_name, display_text = interactive_tag_info
output_parts.append((tag_name, display_text))
# Añadir también el indicador de tipo algebraico
if result.algebraic_type:
indicator_text = f"[{result.algebraic_type}]"
output_parts.append((" ", " "))
output_parts.append(("type_indicator", indicator_text))
else:
# 2. Si no es interactivo, usar la lógica de formato de texto anterior
main_output_tag = "base"
if result.is_assignment:
main_output_tag = "assignment"
indicator_text = "[=]"
elif result.is_equation:
main_output_tag = "equation"
indicator_text = "[eq]"
elif result.result_type == "plot":
main_output_tag = "plot"
# Este caso es un fallback si create_interactive_tag no lo manejó
else:
# Lógica para determinar el tag principal y el texto del indicador
if result.algebraic_type:
type_lower = result.algebraic_type.lower()
if type_lower in self.output_text.tag_names():
main_output_tag = type_lower
elif isinstance(result.actual_result_object, sympy.Basic):
main_output_tag = "symbolic"
elif type_lower in ["int", "float", "complex"] or isinstance(result.actual_result_object, (int, float)):
main_output_tag = "numeric"
elif type_lower == "bool" or isinstance(result.actual_result_object, bool):
main_output_tag = "boolean"
elif type_lower == "str" or isinstance(result.actual_result_object, str):
main_output_tag = "string"
elif result.actual_result_object is not None and \
not isinstance(result.actual_result_object, (sympy.Basic, int, float, bool, str, list, dict, tuple, type(None))):
main_output_tag = "custom_type"
else:
main_output_tag = "symbolic"
else:
main_output_tag = "symbolic"
if result.algebraic_type:
is_collection = any(kw in result.algebraic_type.lower() for kw in ["matrix", "list", "dict", "tuple", "vector", "array"])
is_custom_obj_tag = (main_output_tag == "custom_type")
is_non_trivial_sympy = isinstance(result.actual_result_object, sympy.Basic) and \
result.algebraic_type not in ["Symbol", "Expr", "Integer", "Float", "Rational", "BooleanTrue", "BooleanFalse"]
if is_collection or is_custom_obj_tag or is_non_trivial_sympy:
indicator_text = f"[{result.algebraic_type}]"
output_parts.append((main_output_tag, result.output if result.output is not None else ""))
if indicator_text:
output_parts.append((" ", " "))
output_parts.append(("type_indicator", indicator_text))
return output_parts return output_parts
@ -1119,14 +1205,41 @@ programación y análisis numérico.
self.logger.error(f"Error guardando historial: {e}", exc_info=True) self.logger.error(f"Error guardando historial: {e}", exc_info=True)
def on_close(self): def on_close(self):
"""Maneja cierre de la aplicación""" """Maneja cierre de la aplicación de forma completa"""
self.save_history() try:
self._save_settings() # Guardar historial y configuraciones
self.save_history()
self._save_settings()
if self.interactive_manager: # Cerrar todas las ventanas interactivas
self.interactive_manager.close_all_windows() if self.interactive_manager:
self.interactive_manager.close_all_windows()
self.root.destroy() # Detener cualquier job pendiente
if self._debounce_job:
self.root.after_cancel(self._debounce_job)
# Cerrar autocompletado si está abierto
self._close_autocomplete_popup()
# Asegurar que matplotlib libere recursos
try:
import matplotlib.pyplot as plt
plt.close('all')
except:
pass
# Forzar la salida del mainloop
self.root.quit()
except Exception as e:
self.logger.error(f"Error durante el cierre: {e}")
finally:
# Destruir la ventana principal como último recurso
try:
self.root.destroy()
except:
pass
def show_help_window(self): def show_help_window(self):
"""Muestra ventana de ayuda con archivo externo - NUEVO SISTEMA""" """Muestra ventana de ayuda con archivo externo - NUEVO SISTEMA"""
@ -1441,6 +1554,40 @@ Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el
self._evaluate_and_update() self._evaluate_and_update()
self._adjust_input_pane_width() self._adjust_input_pane_width()
def _update_input_expression(self, original_expression, new_expression):
"""Actualiza el panel de entrada reemplazando la expresión original con la nueva"""
try:
# Obtener todo el contenido actual
current_content = self.input_text.get("1.0", tk.END).rstrip('\n')
# Buscar y reemplazar la expresión original
if original_expression in current_content:
updated_content = current_content.replace(original_expression, new_expression, 1)
# Actualizar el panel de entrada
self.input_text.delete("1.0", tk.END)
self.input_text.insert("1.0", updated_content)
# Evaluar automáticamente la nueva expresión
self._evaluate_and_update()
self.logger.info(f"Expresión actualizada: '{original_expression}' -> '{new_expression}'")
else:
# Si no se encuentra la expresión original, agregar la nueva al final
if current_content and not current_content.endswith('\n'):
current_content += '\n'
updated_content = current_content + new_expression
self.input_text.delete("1.0", tk.END)
self.input_text.insert("1.0", updated_content)
self.logger.info(f"Expresión agregada: '{new_expression}'")
except Exception as e:
self.logger.error(f"Error actualizando expresión: {e}")
# Fallback: simplemente insertar la nueva expresión
self.input_text.insert(tk.END, f"\n{new_expression}")
def main(): def main():
"""Función principal""" """Función principal"""

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@ from type_registry import (
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
@dataclass @dataclass
@ -29,11 +30,14 @@ class EvaluationResult:
"""Resultado de evaluación simplificado""" """Resultado de evaluación simplificado"""
input_line: str input_line: str
output: str output: str
result_type: str result_type: str # e.g., "symbolic", "numeric", "error", "comment", "plot"
success: bool success: bool
error_message: Optional[str] = None error_message: Optional[str] = None
is_equation: bool = False is_equation: bool = False
is_solve_query: bool = False is_solve_query: bool = False
algebraic_type: Optional[str] = None # Tipo del objeto Python resultante (e.g., "Matrix", "Integer")
actual_result_object: Any = None # El objeto Python real del resultado
is_assignment: bool = False # True si la línea fue una asignación
class PureAlgebraicEngine: class PureAlgebraicEngine:
@ -47,6 +51,7 @@ class PureAlgebraicEngine:
self.unified_context = {} # Contexto unificado para sympify self.unified_context = {} # Contexto unificado para sympify
self.bracket_parser = BracketParser() self.bracket_parser = BracketParser()
self.tokenization_patterns = [] # Patrones de tokenización self.tokenization_patterns = [] # Patrones de tokenización
self.last_result_object = None # Para la variable 'last'
# Cargar tipos personalizados PRIMERO # Cargar tipos personalizados PRIMERO
self._load_custom_types() self._load_custom_types()
@ -93,18 +98,34 @@ 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 # 3. FUNCIONES DE PLOTTING (WRAPPED)
try: # Wrappers para capturar llamadas de plot y devolver un objeto PlotResult
from sympy.plotting import plot, plot3d, plot_parametric, plot3d_parametric_line def plot_wrapper(*args, **kwargs):
plotting_functions = { # Intentar extraer la expresión original del primer argumento
'plot': plot, original_expr = str(args[0]) if args else ""
'plot3d': plot3d, return PlotResult("plot", args, kwargs, original_expr)
'plot_parametric': plot_parametric,
'plot3d_parametric_line': plot3d_parametric_line, def plot3d_wrapper(*args, **kwargs):
} # Intentar extraer la expresión original del primer argumento
except Exception as e: original_expr = str(args[0]) if args else ""
self.logger.warning(f"Error cargando funciones de plotting: {e}") return PlotResult("plot3d", args, kwargs, original_expr)
plotting_functions = {}
def plot_parametric_wrapper(*args, **kwargs):
# Intentar extraer la expresión original del primer argumento
original_expr = str(args[0]) if args else ""
return PlotResult("plot_parametric", args, kwargs, original_expr)
def plot3d_parametric_line_wrapper(*args, **kwargs):
# Intentar extraer la expresión original del primer argumento
original_expr = str(args[0]) if args else ""
return PlotResult("plot3d_parametric_line", args, kwargs, original_expr)
plotting_functions = {
'plot': plot_wrapper,
'plot3d': plot3d_wrapper,
'plot_parametric': plot_parametric_wrapper,
'plot3d_parametric_line': plot3d_parametric_line_wrapper,
}
# 4. COMBINAR TODO EN CONTEXTO UNIFICADO # 4. COMBINAR TODO EN CONTEXTO UNIFICADO
self.unified_context = { self.unified_context = {
@ -173,16 +194,18 @@ class PureAlgebraicEngine:
return tokenized_line return tokenized_line
def _get_complete_context(self) -> Dict[str, Any]: def _get_complete_context(self) -> Dict[str, Any]:
"""Obtiene contexto completo incluyendo variables del usuario""" """Obtiene contexto completo incluyendo variables del usuario y 'last'"""
complete_context = self.unified_context.copy() complete_context = self.unified_context.copy()
complete_context.update(self.symbol_table) complete_context.update(self.symbol_table)
complete_context['last'] = self.last_result_object # Añadir 'last' al contexto
return complete_context return complete_context
def evaluate_line(self, line: str) -> EvaluationResult: def evaluate_line(self, line: str) -> EvaluationResult:
"""Evalúa una línea de entrada usando sympify unificado""" """Evalúa una línea de entrada usando sympify unificado"""
line = line.strip() line = line.strip()
if not line or line.startswith('#'): if not line or line.startswith('#'):
return EvaluationResult(line, "", "comment", True) # Devolver la línea como output para que se muestre como comentario
return EvaluationResult(line, line, "comment", True)
try: try:
# 1. Aplicar tokenización dinámica # 1. Aplicar tokenización dinámica
@ -222,134 +245,189 @@ class PureAlgebraicEngine:
return any(op in line for op in comparison_ops) return any(op in line for op in comparison_ops)
def _evaluate_solve_shortcut(self, line: str) -> EvaluationResult: def _evaluate_solve_shortcut(self, line: str) -> EvaluationResult:
"""Evalúa atajos de resolución""" """Evalúa atajos de resolución y gestiona 'last'"""
try: try:
if line.startswith('solve('): if line.startswith('solve('):
# Función solve() - manejar casos especiales primero
# Extraer el contenido dentro de solve()
import re import re
match = re.match(r'solve\(([^)]+)\)', line) match = re.match(r'solve\(([^)]+)\)', line)
if match: if match:
var_content = match.group(1).strip() var_content = match.group(1).strip()
# Si es una variable simple, usar nuestra lógica mejorada
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_content): if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_content):
# Crear símbolo directamente sin usar context para evitar sustitución
var_symbol = sp.Symbol(var_content) var_symbol = sp.Symbol(var_content)
solution_result = self._smart_solve(var_symbol) solution_result_obj = self._smart_solve(var_symbol)
output = str(solution_result)
numeric = self._get_numeric_approximation(solution_result)
if numeric and str(solution_result) != str(numeric):
output += f"{numeric}"
return EvaluationResult(line, output, "symbolic", True, is_solve_query=True)
# Para casos más complejos, usar sympify output = str(solution_result_obj)
result = self._evaluate_expression(line) numeric = self._get_numeric_approximation(solution_result_obj)
result.is_solve_query = True if numeric and str(solution_result_obj) != str(numeric):
output += f"{numeric}"
current_algebraic_type = type(solution_result_obj).__name__
# Los resultados de solve() generalmente no son plots
self.last_result_object = solution_result_obj
return EvaluationResult(
input_line=line,
output=output,
result_type="symbolic",
success=True,
is_solve_query=True,
algebraic_type=current_algebraic_type,
actual_result_object=solution_result_obj
)
# Para casos más complejos de solve() o si el regex no coincide,
# usar _evaluate_expression que ya maneja 'last' y los tipos.
# _evaluate_expression se encargará de self.last_result_object.
result = self._evaluate_expression(line) # Llama a la versión ya modificada
result.is_solve_query = True # Mantener esta bandera
return result return result
# Si no es solve(), podría ser otro tipo de atajo (si se añaden más tarde)
# Por ahora, si no empieza con solve(, lo tratamos como una expresión normal.
return self._evaluate_expression(line)
except Exception as e: except Exception as e:
error_msg = f"Error en resolución: {str(e)}" error_msg = f"Error en atajo de resolución '{line}': {type(e).__name__}: {str(e)}"
return EvaluationResult(line, error_msg, "error", False, str(e)) self.logger.error(error_msg)
# No actualizar last_result_object en caso de error
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 manteniendo doble registro""" """Evalúa una asignación"""
try: try:
# Separar variable = expresión var_name, expression_str = line.split('=', 1)
var_name, expr_str = line.split('=', 1)
var_name = var_name.strip() var_name = var_name.strip()
expr_str = expr_str.strip() expression_str = expression_str.strip()
# Validar nombre de variable
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', var_name):
error_msg = f"Error: Nombre de variable inválido '{var_name}'"
self.logger.error(error_msg)
return EvaluationResult(line, error_msg, "error", False, error_msg)
# Evaluar la expresión del lado derecho # Evaluar la expresión del lado derecho
context = self._get_complete_context() # Usar _get_complete_context para incluir 'last' y otras variables
result = sympify(expr_str, locals=context) eval_context = self._get_complete_context()
result_obj = sympify(expression_str, locals=eval_context)
# 1. ASIGNACIÓN DIRECTA (para uso inmediato) # Actualizar tabla de símbolos
self.symbol_table[var_name] = result self.symbol_table[var_name] = result_obj
# Variable asignada correctamente self.variables.add(var_name)
# 2. ECUACIÓN IMPLÍCITA (para solve) output = f"{var_name} = {result_obj}"
var_symbol = sp.Symbol(var_name)
equation = Eq(var_symbol, result)
self.equations.append(equation)
# Añadir símbolo a variables conocidas # Devolver el resultado de la asignación
self.variables.add(var_symbol) return EvaluationResult(
input_line=line,
# Output conciso - mostrar el valor asignado output=output,
output = str(result) result_type="assignment", # o "symbolic" si queremos que se muestre como tal
success=True,
# Añadir aproximación numérica si es útil is_assignment=True, # Indicar que es una asignación
numeric = self._get_numeric_approximation(result) algebraic_type="=", # Marcador para la GUI
if numeric and str(result) != str(numeric): actual_result_object=result_obj # Guardar el objeto asignado
output += f"{numeric}" )
return EvaluationResult(line, output, "assignment", True)
except Exception as e: except Exception as e:
error_msg = f"Error en asignación: {str(e)}" error_msg = f"Error asignando '{line}': {type(e).__name__}: {str(e)}"
return EvaluationResult(line, error_msg, "error", False, str(e)) self.logger.error(error_msg)
# No actualizar last_result_object en caso de error
return EvaluationResult(line, error_msg, "error", False, str(e), is_assignment=True)
def _evaluate_equation(self, line: str) -> EvaluationResult: def _evaluate_equation(self, line: str) -> EvaluationResult:
"""Evalúa una ecuación y la añade al sistema""" """Evalúa una ecuación"""
try: try:
# Separar left = right # Intentar convertir a objeto Ecuación de sympy
left_str, right_str = line.split('=', 1) # Usar _get_complete_context para incluir 'last' y otras variables
left_str = left_str.strip() eval_context = self._get_complete_context()
right_str = right_str.strip() equation_obj = sympify(line, locals=eval_context, parse_function=lambda s: Eq(*s.split('=',1)))
# USAR SYMPIFY UNIFICADO para ambos lados if not isinstance(equation_obj, sp.Equality):
context = self._get_complete_context() # Si no se pudo parsear como Eq(LHS,RHS), tratar como expresión que contiene un igual (posible error o comparación)
left_expr = sympify(left_str, locals=context) # Esto podría ser una comparación booleana que sympify evalúa
right_expr = sympify(right_str, locals=context) if isinstance(equation_obj, sp.logic.boolalg.BooleanFunction) or isinstance(equation_obj, bool):
# Es una comparación, evaluarla como expresión normal
return self._evaluate_expression(line)
else:
raise ValueError("La expresión no es una ecuación válida.")
# Crear ecuación self.equations.append(equation_obj)
equation = Eq(left_expr, right_expr) # Registrar símbolos de la ecuación para posible autocompletado o análisis futuro
for free_symbol in equation_obj.free_symbols:
self.variables.add(str(free_symbol))
# Añadir al sistema output = str(equation_obj)
self.equations.append(equation) # No actualizar last_result_object para ecuaciones
return EvaluationResult(
# Extraer variables input_line=line,
eq_vars = equation.free_symbols output=output,
self.variables.update(eq_vars) result_type="equation",
success=True,
# Output conciso is_equation=True,
output = str(equation) algebraic_type="eq", # Marcador para la GUI
actual_result_object=equation_obj
# Evaluación numérica si es posible )
numeric = self._get_numeric_approximation(equation.rhs)
if numeric and str(equation.rhs) != str(numeric):
output += f"{numeric}"
return EvaluationResult(line, output, "equation", True, is_equation=True)
except Exception as e: except Exception as e:
error_msg = f"Error en ecuación: {str(e)}" error_msg = f"Error en ecuación '{line}': {type(e).__name__}: {str(e)}"
return EvaluationResult(line, error_msg, "error", False, str(e)) self.logger.error(error_msg)
# No actualizar last_result_object en caso de error
return EvaluationResult(line, error_msg, "error", False, str(e), is_equation=True)
def _evaluate_expression(self, line: str) -> EvaluationResult: def _evaluate_expression(self, line: str) -> EvaluationResult:
"""Evalúa una expresión usando sympify unificado ÚNICAMENTE""" """Evalúa una expresión usando sympify unificado y gestiona 'last'"""
try: try:
# USAR SYMPIFY UNIFICADO - Un solo parser # Usar _get_complete_context para incluir 'last' y otras variables
context = self._get_complete_context() eval_context = self._get_complete_context()
result = sympify(line, locals=context) result_obj = sympify(line, locals=eval_context)
# Nota: Las asignaciones ahora se manejan en _evaluate_assignment # Si es un PlotResult, asegurar que tenga la línea original
if isinstance(result_obj, PlotResult) and not result_obj.original_expression:
result_obj.original_expression = line
# Simplificar si es expresión SymPy output = str(result_obj)
if hasattr(result, 'simplify'): result_type_str = "symbolic" # Tipo por defecto
result = simplify(result) current_algebraic_type = type(result_obj).__name__
output = str(result) # Manejo especial para tipos de sympy que pueden necesitar aproximación numérica
if hasattr(result_obj, 'evalf') and not isinstance(result_obj, (sp.Matrix, sp.Basic, sp.Expr)):
# Evitar evalf en matrices directamente o en tipos ya específicos como Integer, Float
pass
# Añadir aproximación numérica numeric_approximation = self._get_numeric_approximation(result_obj)
numeric = self._get_numeric_approximation(result) if numeric_approximation and output != numeric_approximation:
if numeric and str(result) != str(numeric): output += f"{numeric_approximation}"
output += f"{numeric}" # Considerar si esto cambia el result_type a "numeric_approx"
return EvaluationResult(line, output, "symbolic", True) # Determinar si es un objeto de plotting (para no asignarlo a 'last')
is_plot_object = False
if isinstance(result_obj, PlotResult):
is_plot_object = True
result_type_str = "plot" # Marcar como plot para la GUI
# No es necesario cambiar current_algebraic_type, ya será "PlotResult"
# Actualizar last_result_object si no es error y no es plot
if not is_plot_object:
self.last_result_object = result_obj
else:
# Si es un plot, no queremos que 'last' lo referencie para evitar problemas
# si el plot se cierra o se maneja de forma especial.
# Podríamos incluso setear last_result_object a None o al valor previo.
# Por ahora, simplemente no lo actualizamos si es plot.
pass
return EvaluationResult(
input_line=line,
output=output,
result_type=result_type_str,
success=True,
algebraic_type=current_algebraic_type,
actual_result_object=result_obj
)
except Exception as e: except Exception as e:
error_msg = f"Error: {str(e)}" error_msg = f"Error evaluando '{line}': {type(e).__name__}: {str(e)}"
self.logger.error(error_msg)
# No actualizar last_result_object en caso de error
return EvaluationResult(line, error_msg, "error", False, str(e)) return EvaluationResult(line, error_msg, "error", False, str(e))
def _smart_solve(self, *args, **kwargs): def _smart_solve(self, *args, **kwargs):
@ -588,7 +666,7 @@ class PureAlgebraicEngine:
self.equations.clear() self.equations.clear()
self.variables.clear() self.variables.clear()
self.symbol_table.clear() self.symbol_table.clear()
self.logger.info("Contexto limpio")
def get_context_info(self) -> Dict[str, Any]: def get_context_info(self) -> Dict[str, Any]:
"""Información del contexto actual""" """Información del contexto actual"""

View File

@ -13,10 +13,11 @@ import numpy as np
class PlotResult: class PlotResult:
"""Placeholder para resultados de plotting - DEFINICIÓN PRINCIPAL""" """Placeholder para resultados de plotting - DEFINICIÓN PRINCIPAL"""
def __init__(self, plot_type: str, args: tuple, kwargs: dict): def __init__(self, plot_type: str, args: tuple, kwargs: dict, original_expression: str = None):
self.plot_type = plot_type self.plot_type = plot_type
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
self.original_expression = original_expression or ""
def __str__(self): def __str__(self):
return f"📊 Ver {self.plot_type.title()}" return f"📊 Ver {self.plot_type.title()}"
@ -31,8 +32,13 @@ class InteractiveResultManager:
def __init__(self, parent_window: tk.Tk): def __init__(self, parent_window: tk.Tk):
self.parent = parent_window self.parent = parent_window
self.open_windows: Dict[str, Toplevel] = {} self.open_windows: Dict[str, Toplevel] = {}
self.update_input_callback = None # Callback para actualizar el panel de entrada
def create_interactive_tag(self, result: Any, text_widget: tk.Text, index: str) -> Optional[Tuple[str, str]]: 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 Crea un tag interactivo para un resultado si es necesario
@ -135,7 +141,7 @@ class InteractiveResultManager:
print(f"❌ Error abriendo ventana interactiva: {e}") print(f"❌ Error abriendo ventana interactiva: {e}")
def _show_plot_window(self, plot_result: PlotResult, window_key: str): def _show_plot_window(self, plot_result: PlotResult, window_key: str):
"""Muestra ventana con plot matplotlib""" """Muestra ventana con plot matplotlib e interfaz de edición"""
# Asegurar que las dimensiones de la ventana principal estén actualizadas # Asegurar que las dimensiones de la ventana principal estén actualizadas
self.parent.update_idletasks() self.parent.update_idletasks()
parent_x = self.parent.winfo_x() parent_x = self.parent.winfo_x()
@ -144,7 +150,7 @@ class InteractiveResultManager:
parent_height = self.parent.winfo_height() parent_height = self.parent.winfo_height()
# Definir dimensiones y posición para la ventana del plot # Definir dimensiones y posición para la ventana del plot
plot_window_width = 600 # Ancho deseado para la ventana del plot (puedes ajustarlo) plot_window_width = 700 # Aumentado para dar espacio al campo de edición
plot_window_height = parent_height # Misma altura que la ventana principal plot_window_height = parent_height # Misma altura que la ventana principal
# Posicionar la ventana del plot a la derecha de la ventana principal # Posicionar la ventana del plot a la derecha de la ventana principal
@ -163,8 +169,129 @@ class InteractiveResultManager:
) )
self.open_windows[window_key] = window 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: try:
fig, ax = plt.subplots(figsize=(8, 6)) # Tamaño de la figura interna del plot # 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": if plot_result.plot_type == "plot":
self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs) self._create_2d_plot(fig, ax, plot_result.args, plot_result.kwargs)
@ -172,30 +299,28 @@ class InteractiveResultManager:
self._create_3d_plot(fig, plot_result.args, plot_result.kwargs) self._create_3d_plot(fig, plot_result.args, plot_result.kwargs)
# Embed en tkinter # Embed en tkinter
canvas = FigureCanvasTkAgg(fig, window) canvas = FigureCanvasTkAgg(fig, parent_frame)
canvas.draw() canvas.draw()
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Toolbar para interactividad # Toolbar para interactividad
try: try:
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
toolbar = NavigationToolbar2Tk(canvas, window) toolbar = NavigationToolbar2Tk(canvas, parent_frame)
toolbar.update() toolbar.update()
except ImportError: except ImportError:
pass # Si no está disponible, continuar sin toolbar pass # Si no está disponible, continuar sin toolbar
except Exception as e: except Exception as e:
error_label = tk.Label( error_label = tk.Label(
window, parent_frame,
text=f"Error generando plot: {e}", text=f"Error generando plot: {e}",
fg="red", fg="#f44747",
bg="#2b2b2b", bg="#2b2b2b",
font=("Consolas", 12) font=("Consolas", 12)
) )
error_label.pack(pady=20) error_label.pack(pady=20)
print(f"❌ Error en plot: {e}")
def _create_2d_plot(self, fig, ax, args, kwargs): def _create_2d_plot(self, fig, ax, args, kwargs):
"""Crea plot 2D usando SymPy""" """Crea plot 2D usando SymPy"""
if len(args) >= 1: if len(args) >= 1:
@ -518,11 +643,27 @@ class InteractiveResultManager:
return window return window
def close_all_windows(self): def close_all_windows(self):
"""Cierra todas las ventanas interactivas""" """Cierra todas las ventanas interactivas de forma segura"""
for window in list(self.open_windows.values()): windows_to_close = list(self.open_windows.items())
for window_key, window in windows_to_close:
try: try:
if window.winfo_exists(): if window and window.winfo_exists():
window.destroy() # 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: except tk.TclError:
# La ventana ya fue destruida o no es válida
pass pass
except Exception as e:
print(f"Error cerrando ventana {window_key}: {e}")
# Limpiar el diccionario
self.open_windows.clear() self.open_windows.clear()
# Cerrar todas las figuras de matplotlib para liberar memoria
try:
plt.close('all')
except:
pass