Sistema gráfico de JMathAnim (subproyecto jmathanim-gui)

Documento de referencia para desarrollo y mantenimiento del editor gráfico. Se centra en cómo dialogan la GUI y el core, y en los puntos donde normalmente tendrás que tocar código en el futuro.

Estado: refactor de julio 2026 (paquetes app / config / editor / execution / ui / dsl / latex / autocomplete).


1. Visión de conjunto

JMathAnim son dos artefactos separados:

  • jmathanim-core — la librería de animación (com.jmathanim.*). No sabe nada de la GUI. Compila y ejecuta scripts Groovy, renderiza, codifica vídeo, parsea configuración, etc.
  • jmathanim-gui — el editor Swing (com.jmathanimgui.*). Embebe un panel JavaFX para el preview y orquesta al core.

La GUI depende del core, nunca al revés. Todo el contacto ocurre a través de un puñado de clases del core que la GUI conoce por nombre. Si algún día partes el core en otro repositorio, esa "superficie de contacto" es lo único que hay que mantener estable.

flowchart TB
    subgraph GUI["jmathanim-gui (Swing + JavaFX)"]
        Launcher["app.Launcher\n(arranque + classloader)"]
        MainWin["app.JMathAnimGUI\n(ventana + orquestación)"]
        Exec["execution.ScriptExecutor\n(hilo de ejecución)"]
        Scene["execution.GUIScene2D\n(escena de preview)"]
        Renderer["execution.GUIJavaFXRenderer"]
        LogP["ui.LogPanel"]
    end
    subgraph CORE["jmathanim-core (com.jmathanim.*)"]
        GU["groovy.GroovyUtils\n(compila + ejecuta script)"]
        LTC["groovy.LineTrackingCustomizer"]
        GSE["groovy.GroovyScriptError"]
        Logger["core.JMathAnimLogger"]
        JScene["core.JMathAnimScene"]
        JRend["renderers.JavaFXRenderer"]
    end

    Launcher -->|reflexión| MainWin
    MainWin --> Exec --> Scene
    Scene -->|extends| JScene
    Scene --> GU
    GU --> LTC
    GU -.->|lanza| GSE
    Scene --> Renderer -->|extends| JRend
    Logger -->|listeners| LogP

Superficie de contacto GUI ↔ core (memorízala)

Punto de contacto Clase del core Cómo lo usa la GUI
Arranque / versión com.jmathanim.core.VersionInfo Launcher lee la versión del core empaquetado
Ejecutar un script com.jmathanim.core.groovy.GroovyUtils GUIScene2D lo instancia y llama runSetup()/runSketch()
Escena base com.jmathanim.core.JMathAnimScene GUIScene2D extends JMathAnimScene
Renderer base com.jmathanim.renderers.FXRenderer.JavaFXRenderer GUIJavaFXRenderer extends JavaFXRenderer
Errores de script com.jmathanim.core.groovy.GroovyScriptError se captura y se re-lanza como GUIScene2D.GroovyScriptException
Log y progreso com.jmathanim.core.JMathAnimLogger listeners setLogListener / setProgressBarListener
Seguimiento de líneas com.jmathanim.core.groovy.LineTrackingCustomizer inyecta scene.setCurrentExecutingLine(n) en el AST
Plugins GroovyUtils.getLoadedPluginNames() / unloadAllPlugins() menú Tools → Unload Plugins
Config de animación com.jmathanim.core.JMathAnimConfig flags (FPS, sonido), tamaño de medio, etc.

2. Arranque: app.Launcher y la carga del core

Launcher.main() es el mainClass real del JAR (ver jmathanim-gui/pom.xml). Hace tres cosas antes de arrancar la GUI:

  1. Redirige System.out/err a ~/<appdata>/JMathAnim/jmathanim-gui.log (salvo --no-log-file), quitando códigos ANSI. Útil en ejecutables empaquetados sin consola.
  2. Busca un core externo en <appdata>/JMathAnim/lib/ (Launcher.USER_LIB_DIR). Si hay un jmathanim-core-X.Y.Z.jar compatible (mismo prefijo X.Y) y más nuevo que el empaquetado, construye un URLClassLoader que lo antepone a todo lo demás.
  3. Lanza la GUI por reflexión: classLoader.loadClass("com.jmathanimgui.app.JMathAnimGUI").getMethod("main", …).

⚠️ La GUI se carga por reflexión con un String. Si renombras o mueves JMathAnimGUI, hay que actualizar ese literal en Launcher.launchGUI() y el <mainClass> del pom.xml y crea_jpackage_macincloud.sh.

Por qué un classloader "platform-first"

El truco clave está en buildClassLoader(): usa ClassLoader.getPlatformClassLoader() como padre (no el del sistema). Como jpackage pone todos los JARs en el classpath del sistema, usar el del sistema como padre provocaría parent-first delegation y ganaría el core empaquetado. Con el platform loader como padre, el URLClassLoader que montamos es el que resuelve todas las clases de aplicación, así que el core externo gana.

Directorio de datos por plataforma (Launcher.getAppDataDir())

SO Ruta
Windows %APPDATA%\JMathAnim
macOS ~/Library/Application Support/JMathAnim
Linux ~/.config/jmathanim

Ahí viven: jmathanim.config (config de la GUI), lib/ (cores externos) y jmathanim-gui.log.


3. Mapa de paquetes tras el refactor

com.jmathanimgui
├── app          Arranque, ventana principal y estado global
│   ├── Launcher                 punto de entrada + classloader del core
│   ├── JMathAnimGUI             ventana principal (orquestador, ~1200 líneas)
│   ├── SplashScreen
│   ├── UpdateChecker            comprobación de actualizaciones GUI/core
│   ├── WindowStateManager       config file + geometría de ventana + divisores
│   ├── ProjectManager           ciclo de vida de proyectos + main script
│   ├── RecentItemsManager       listas de recientes (ficheros y proyectos)
│   └── ApiDocsNavigator         abrir la API doc del core en el navegador
│
├── config       Configuración y valores
│   ├── GUIProperties            claves + persistencia agrupada del config
│   ├── ProjectProperties        fichero .jmproj de proyecto
│   └── AppTheme                 enum de temas de animación (key/nombre/xml)
│
├── editor       Editor de texto (RSyntaxTextArea)
│   ├── TabbedEditorPane         pestañas + API pública del editor
│   ├── EditorTab                una pestaña (editar líneas, comentar, folding…)
│   ├── StringLiteralUtils       parseo/escape de literales de cadena (estático)
│   ├── ResourceLinkNavigator    navegación Ctrl+click a import/recursos
│   ├── FindReplaceDialog
│   └── GroovyCodeFormatter
│
├── execution    Motor de ejecución + render (la parte GUI↔core)
│   ├── ScriptExecutor           hilo de ejecución, ciclo run/stop/pause/step
│   ├── SceneFactory (interfaz)  QUÉ clase ejecuta los scripts  ← punto flexible
│   ├── DefaultSceneFactory      implementación por defecto (GUIScene2D)
│   ├── ExecutionListener        callbacks de ejecución hacia la UI
│   ├── PreScriptBuilder         construye el prescript inyectado
│   ├── GUIScene2D               escena que corre el script y pinta el preview
│   ├── GUIJavaFXRenderer        renderer JavaFX embebido
│   ├── PreviewOverlayManager    overlay de interacción sobre el preview
│   ├── CoordinateDisplayFeature muestra coords / "moveTo" al hacer clic
│   ├── SvgExporter              exportar la vista actual a SVG
│   ├── GroovyErrorFormatter     formatea el mensaje de error para el log
│   └── DebugLatexRect
│
├── ui           Composición de UI
│   ├── MainMenuBar / MainToolBar
│   ├── AnimationControls        habilita/deshabilita controles run/stop/…
│   ├── LogPanel                 panel de log (ANSI + progreso)
│   ├── AboutDialog / ExamplesMenuBuilder
│   ├── SettingsDialog / SnippetSidebar / ToolBarIcons
│   ├── SnippetCatalog           construye el catálogo de snippets desde el core
│
├── dsl          DSL de snippets
│   ├── DslSnippetInserter       inserta/edita bloques DSL en el editor
│   ├── AttrSpec                 (key, value, quoted)
│   ├── CoreDefinitions          lee los ficheros de definiciones del core
│   ├── DSLParser / DslEditing   (preexistentes)
│
├── latex        Editores/preview LaTeX y editor de color
└── autocomplete Autocompletado, token maker, fold parser

Regla de dependencias sanas: app conoce a todos; ui/editor/execution no deberían conocer a app salvo por callbacks explícitos (hoy MainMenuBar y MainToolBar reciben la JMathAnimGUI como colaborador — es un patrón Swing aceptable, pero no lo generalices a más clases).


3.bis Las definiciones del DSL viven en el core

Nada de lo que el editor sabe del DSL está escrito en este módulo: bloques, parámetros, snippets y capacidades salen de ficheros YAML en los recursos del core, bajo autocomplete/:

Fichero Qué define Lo carga
dsl-completions.yaml claves y valores de cada bloque DSL; también resaltado y plegado DslCompletionLoader, JMathAnimTokenMaker
general-completions.yaml completado plano, shorthands y plantillas del editor GeneralCompletionLoader
editor-completions.yaml tablas de valores de los tipos (AnchorType, LayoutType…), completado por método, listados de ficheros EditorCompletionLoader
snippets.yaml catálogo de la barra de snippets y de la paleta rápida SnippetCatalog
dsl-editing.yaml qué bloques aceptan style:, transform:, effects: DslEditing

Se leen por classloader, así que describen el core que está en uso: cuando el Launcher pone delante un jmathanim-core-X.Y.Z.jar externo, un core que añade un bloque DSL trae consigo sus definiciones y el GUI no necesita actualizarse. CoreDefinitions.loadAll(), invocado al principio de initializeComponents(), los lee antes de construir nada que los use.

Para añadir un bloque DSL nuevo basta con tocar el core: la FooDSL.groovy, su registro en JMAnimBaseScript y sus entradas en estos ficheros.


4. El motor de ejecución (execution)

Antes toda la lógica de ejecución vivía dentro de JMathAnimGUI. Ahora está en ScriptExecutor, que no tiene dependencias de Swing: se comunica con la UI sólo a través de ExecutionListener. Esto permite reutilizarlo o testearlo sin ventana.

4.1. Piezas

  • ScriptExecutor — lanza el hilo JMathAnim-Animation, crea el fichero temporal, prepara el directorio media/, engancha los listeners de log y progreso del core, detecta errores de Groovy y expone stop() / togglePause() / stepFrame() / shutdown().
  • SceneFactory (interfaz) — decide qué escena ejecuta el script. ScriptExecutor.setSceneFactory(...) es el gancho para cambiar el motor.
  • DefaultSceneFactory — crea un GUIScene2D normal.
  • ExecutionListener — callbacks: onLog, onProgress, onSetupComplete, onPauseStateChanged, onDslMoveToInsert, onCompleted, onStopped, onScriptError, onError, onFinished.
  • PreScriptBuilder — construye el "prescript": el bloque de código que se antepone al script del usuario (nivel de log, config preview/producción, tamaño de medio, tema, resourcesDir, flags FPS/sonido).

4.2. Flujo completo de un "Run"

sequenceDiagram
    participant U as Usuario (F5)
    participant GUI as JMathAnimGUI
    participant PB as PreScriptBuilder
    participant EX as ScriptExecutor
    participant SF as SceneFactory
    participant SC as GUIScene2D
    participant GU as GroovyUtils (core)
    participant L as JMathAnimLogger (core)

    U->>GUI: runAnimationPreviewStyle()
    GUI->>GUI: guarda pestaña(s)
    GUI->>PB: build(config, "#preview.xml", forceSize, w, h, resDir)
    PB-->>GUI: prescript (texto)
    GUI->>GUI: prescript + código usuario  →  ExecutionRequest
    GUI->>EX: run(request, listener, reviser)
    EX->>EX: crea temp .groovy + media/
    EX->>L: setLogListener / setProgressBarListener
    EX->>SF: createScene(path, pane, dir, prescriptLines)
    SF-->>EX: GUIScene2D
    EX->>SC: execute()
    SC->>GU: new GroovyUtils(...)  (compila)
    SC->>GU: runSetup() / runSketch()
    GU-->>L: log / progreso  --(listener)-->  LogPanel
    GU-->>SC: setCurrentExecutingLine(n)  (inyectado)
    SC-->>EX: fin normal / excepción
    EX-->>GUI: onCompleted / onScriptError / onStopped / onFinished

4.3. ExecutionRequest y RequestReviser

run() recibe un ExecutionRequest inmutable (contenido completo, nº de líneas de prescript, directorio, nombre de salida) y un RequestReviser. El reviser es el fallback para el caso "no se pudo crear media/ porque el script nunca se ha guardado": el executor pide a la GUI que muestre el diálogo de guardar (en el EDT) y reconstruye la petición con la nueva ruta. Toda la interacción Swing vive en el reviser/listener, no en el executor.


5. Interacción script ↔ core (GroovyUtils)

GUIScene2D no compila nada por su cuenta: delega en com.jmathanim.core.groovy.GroovyUtils. La secuencia dentro del constructor de GUIScene2D:

this.groovyUtils = new GroovyUtils(
        groovyScriptFileName, this, scriptDirectory, prescriptLines, /*isGUI*/ true);

Con isGUI = true, GroovyUtils:

  1. Procesa import("...") (processImports) — inserta el contenido de los ficheros importados en línea, con marcadores // === BEGIN IMPORT: … ===.
  2. Procesa @JMPlugin y @Grab — pide a PluginManager los JARs de plugin que declare el script, recoge los paquetes de todo lo que haya cargado (venga del script o del menú Plugins) e inicializa los plugins con la escena; las dependencias @Grab van a un classloader persistente (ver §10).
  3. Construye la config del compilador reutilizando una configuración compartida y cacheada (star-imports + clase base + LineTrackingCustomizer), lo que evita repagar el ~800 ms de resolución de imports en cada run. Ese warmup se dispara al arrancar la GUI: GroovyUtils.warmupGuiCompilation(GUIScene2D.class) (hilo warmup en el constructor de JMathAnimGUI).
  4. Compila el script. Si falla, lanza GroovyScriptError con línea y fichero ya ajustados.

Luego execute() (heredado de JMathAnimScene) llama a setupSketch() y runSketch(), que en GUIScene2D delegan en groovyUtils.runSetup() / groovyUtils.runSketch().

El prescript y el mapeo de líneas

El script que realmente se compila es prescript + código del usuario. El prescript desplaza la numeración, por eso PreScriptBuilder.countLines() calcula cuántas líneas ocupa y ese número (prescriptLines) viaja hasta GroovyUtils, que construye un mapa de líneas (buildLineMap / SourceLocation[]) para traducir la línea "cruda" del AST a la línea real del fichero del usuario (o <= 0 si pertenece al prescript o a un import — en cuyo caso no se rastrea).


6. Seguimiento de línea en ejecución (line tracking)

Es el resaltado de la línea que se está ejecutando en tiempo real. Mecánica:

  1. LineTrackingCustomizer (core) es un CompilationCustomizer que, en fase SEMANTIC_ANALYSIS, recorre el AST y antes de cada sentencia inyecta:
scene.setCurrentExecutingLine(<línea del usuario>)

usando el mapa de líneas (sólo para líneas del script principal).

  1. GUIScene2D.setCurrentExecutingLine(int) simplemente guarda el valor en un volatile int currentExecutingLine.

  2. La GUI hace polling, no push. JMathAnimGUI.startLineTracking() arranca un javax.swing.Timer de 50 ms que lee executor.getCurrentScene().getCurrentExecutingLine() y llama a tabbedEditor.highlightExecutingLine(line). Al terminar, stopLineTracking() lo para y limpia el resaltado.

flowchart LR
    subgraph core
        LTC["LineTrackingCustomizer\ninyecta en AST"] --> SC["GUIScene2D\ncurrentExecutingLine (volatile)"]
    end
    subgraph gui
        T["Swing Timer 50ms"] -->|lee| SC
        T --> H["TabbedEditorPane.highlightExecutingLine"]
    end

Si en el futuro un motor alternativo no usa GroovyUtils, para conservar el line tracking basta con que su escena también implemente setCurrentExecutingLine/getCurrentExecutingLine (ya están en GUIScene2D) y que su compilación inyecte esas llamadas.


7. Errores detallados

Cadena de propagación de un error de script:

GroovyUtils  --lanza-->  GroovyScriptError(mensaje, fichero, fase, línea, errorFile)
     │  (línea ya ajustada por el mapa de líneas)
     ▼
GUIScene2D   --captura y re-lanza-->  GUIScene2D.GroovyScriptException(mensaje, línea, errorFile)
     ▼
ScriptExecutor  --> listener.onScriptError(e)
     ▼
JMathAnimGUI.handleGroovyScriptError(e, …)   (en el EDT)

handleGroovyScriptError distingue dos casos:

  • Error en el script principal (errorFile == null): loguea Error at line: N, salta a esa línea (goToErrorLine) y restaura el cursor.
  • Error dentro de un import(...) (errorFile != null): reporta fichero:línea, abre esa pestaña y salta a la línea; no restaura el cursor (te lleva deliberadamente al sitio del error).

Además hay una vía secundaria: ScriptExecutor.checkForGroovyError() escanea los mensajes de log en busca de patrones de error (GROOVY_ERROR_PATTERN) por si un error se reporta sólo por log y no por excepción, para no marcar el run como "completado" erróneamente.

El mensaje se embellece con GroovyErrorFormatter.formatGroovyError(...) antes de pintarse en el LogPanel.


8. Render y preview

GUIScene2D crea su renderer sobreescribiendo createRenderer():

guiRenderer = new GUIJavaFXRenderer(this, renderPane);  // renderPane = StackPane del preview
guiRenderer.initialize();

GUIJavaFXRenderer extends JavaFXRenderer (core): reutiliza toda la maquinaria de dibujo JavaFX del core pero pinta en el StackPane embebido en el JFXPanel de la ventana, en lugar de en una ventana propia.

Cuando setupSketch() termina y el tamaño de medio ya es definitivo, el core dispara onSetupCompleteCallback, que la GUI usa (ExecutionListener.onSetupComplete → updatePreviewAfterSetup) para ajustar el tamaño del StackPane y del JFXPanel.

Overlay de interacción

PreviewOverlayManager monta un Pane transparente encima del preview y distribuye eventos de ratón/teclado a OverlayFeatures. La feature por defecto es CoordinateDisplayFeature, que muestra las coordenadas del mundo bajo el cursor y, con su menú contextual "DSL moveTo", inserta moveTo:[x, y] en el editor (a través del callback dslMoveToInserter → ExecutionListener.onDslMoveToInsert → tabbedEditor.setDslTransformSnippet).

Para previews embebidos que gestionan su propio ratón (p. ej. el de LaTeX), GUIScene2D.setOverlayEnabled(false) desactiva el overlay.


9. Logging y barra de progreso

El core emite todo por JMathAnimLogger (nunca System.out directamente — ver la nota de memoria "usa el logger, no stdio"). La GUI se engancha con dos listeners estáticos que ScriptExecutor instala al empezar un run y retira en finally:

JMathAnimLogger.setLogListener(msg -> { if (!closing) { listener.onLog(msg); checkForGroovyError(msg); }});
JMathAnimLogger.setProgressBarListener(p -> { if (!closing) listener.onProgress(p); });
// …
JMathAnimLogger.removeGUILogListener();
JMathAnimLogger.removeProgressBarListener();

onLog → LogPanel.appendLog (parsea códigos ANSI y colorea). onProgress → LogPanel.updateProgressBar (reescribe en sitio la última línea de barra).

Como los listeners son estáticos y globales, sólo puede haber un run "escuchando" a la vez. Por eso el executor se asegura de pararlos siempre.


10. Plugins y @Grab

Un plugin es un JAR que declara en su MANIFEST.MF una clase que implementa com.jmathanim.core.groovy.JMAnimPlugin:

JMAnimPlugin-Class:          com.example.MyPlugin   (obligatorio)
JMAnimPlugin-Name:           My Plugin
JMAnimPlugin-Version:        1.0.0
JMAnimPlugin-Description:    Qué añade
JMAnimPlugin-MinCoreVersion: 1.4.8

Todos los métodos de JMAnimPlugin tienen implementación por defecto, así que un plugin mínimo sólo sobreescribe getPackages() (los paquetes que se star-importan en los scripts mientras el plugin esté cargado).

Quien carga y descarga es core.groovy.PluginManager, el único sitio que toca el registro. Hay dos caminos de entrada, y ambos acaban ahí:

  1. Desde la GUI, menú Plugins (lo habitual). Se recuerda entre sesiones.
  2. Desde el script, con @JMPlugin("mi-plugin-1.0.jar"). Es lo que mantiene un script autocontenido para ejecutarlo por línea de comandos con GroovyExecutor, donde no hay GUI que gestione nada.

Un plugin que ya esté cargado por un camino lo reutiliza el otro: declarar @JMPlugin de algo que la GUI ya cargó no lo carga dos veces.

Resolución del JAR de @JMPlugin (PluginManager.resolvePluginJar):

  • ruta absoluta → tal cual;
  • ruta con directorio (libs/x.jar) → relativa al directorio del script;
  • nombre pelado → se busca en <scriptDir>/resources/plugins/, luego en <scriptDir>/, y por último entre los plugins ya cargados (por nombre de fichero), para que un script que nombra un plugin que la GUI cargó de otro sitio siga funcionando.

Ciclo de vida

  • Cargar lee el manifest, comprueba la versión del core, abre un URLClassLoader propio del plugin, instancia la clase y lee los snippets que traiga. No llama a init(scene): un plugin cargado desde el menú todavía no tiene escena.
  • PluginManager.initForScene(scene) se llama antes de cada compilación e inicializa con la escena que va a ejecutarse todo lo que esté cargado.
  • Descargar llama a dispose(scene) (sólo si llegó a inicializarse) y cierra el classloader del plugin, con lo que se libera el JAR y puedes reemplazarlo sin reiniciar.

Un classloader por plugin es lo que permite descargar uno solo. Como un classloader tiene un único padre y el script tiene que ver los de todos a la vez, entre el classloader de dependencias y el del script se intercala un PluginsClassLoader que reparte la búsqueda entre los classloaders de los plugins cargados. Consecuencia: los plugins están aislados entre sí; uno que necesite clases de otro tiene que empaquetarlas.

El classloader compartido de @Grab (getSharedDependencyClassLoader()) ya no contiene los plugins y ya no se cierra al descargarlos: las descargas y el warm-up del JIT sobreviven a un ciclo de carga/descarga.

Snippets de plugin

Un plugin puede traer su propio catálogo de snippets, en el mismo formato que autocomplete/snippets.yaml del core, en un fichero de su JAR (por defecto jmathanim-snippets.yaml en la raíz; getSnippetsResource() lo cambia). Se lee del JAR con JarFile, no por classloader, para que un fichero que se llame igual que uno del core no devuelva el del core.

El texto YAML viaja en PluginInfo.getSnippetsYaml(). La GUI lo pasa a SnippetCatalog.setPluginSnippets(...), que construye una categoría de primer nivel Plugins con una subcategoría por plugin, de modo que los snippets aparecen en Plugins / <nombre> / … y desaparecen al descargarlo.

La GUI

  • plugins.PluginRegistry — la lista de JARs conocidos (los que el usuario añadió, guardados en plugins.known, más los que haya en <appData>/plugins/) y cuáles se cargan al arrancar (plugins.autoload).
  • plugins.PluginManagerDialog — la tabla de gestión: añadir, cargar, descargar, marcar para el arranque y olvidar.
  • MainMenuBar.refreshPluginsMenu(...) y SnippetSidebar.refresh() se disparan desde un listener registrado en PluginManager, así que la GUI sigue también a los plugins que carga un script con @JMPlugin mientras está abierta.

Cargar o descargar se rechaza mientras haya una animación en marcha: los classloaders que se van a cerrar podrían estar en uso.

Plugins de proyecto

Con un proyecto abierto, el project.jmproj manda: la clave plugins guarda los JARs cargados, relativos al directorio del proyecto cuando están dentro de él (lo normal, resources/plugins/x.jar), absolutos si no.

  • El listener de PluginManager llama a ProjectManager.savePluginsState(), así que cargar o descargar algo desde el menú (o desde un @JMPlugin de un script) se escribe en el fichero del proyecto al momento.
  • Al abrir el proyecto (onProjectOpened, y también en restoreProject al arrancar) se cargan sus plugins; lo que falle se reporta y se salta, el proyecto se abre igual.
  • Al cerrarlo o cambiar de proyecto se descargan, salvo los marcados como plugins.autoload en la configuración del editor, que son del editor y no del proyecto. En ambos casos currentProject se pone a null antes de descargar, para que el listener no reescriba el fichero del proyecto que se está cerrando.

El selector de "Load Plugin…" y el "Add JAR…" del gestor arrancan en getPluginBrowseDir(): resources/plugins del proyecto (o del script actual si no hay proyecto), y si no existe, el propio directorio base.


11. Configuración y persistencia

  • config.GUIProperties — subclase de Properties con todas las claves (KEY_*, WINDOW_*, RECENT_*, …) y storeGrouped() que escribe el fichero agrupado por secciones y comentado, en UTF-8.
  • app.WindowStateManager — carga/guarda jmathanim.config, aplica y captura la geometría de ventana y las posiciones de los divisores.
  • app.ProjectManager — proyectos (.jmproj), main script, y el stash de ficheros abiertos sin proyecto (se restauran al cerrar el proyecto).
  • app.RecentItemsManager — una sola clase para "recientes" de ficheros (con aceleradores Ctrl+1..9) y de proyectos.
  • config.AppTheme — enum que sustituye al viejo switch de temas repetido; cada valor conoce su clave, su nombre visible y su #tema.xml.

12. Recetas de mantenimiento (casos de uso)

12.1. Cambiar la clase/motor que ejecuta los scripts

Éste es el gancho principal de flexibilidad del refactor. Para ejecutar los scripts con otra escena/renderer (p. ej. un futuro GUIOpenGLScene):

  1. Crea la escena (idealmente extendiendo GUIScene2D o al menos JMathAnimScene, y conservando setCurrentExecutingLine, requestStop, pause/resume/stepOneFrame para que sigan funcionando line-tracking y los controles).
  2. Implementa una SceneFactory:
public class OpenGLSceneFactory implements SceneFactory {
    @Override
    public GUIScene2D createScene(String scriptPath, StackPane pane,
                                  String scriptDir, int prescriptLines) {
        return new GUIOpenGLScene(scriptPath, pane, scriptDir, prescriptLines);
    }
}
  1. Instálala en el executor (una sola línea, en JMathAnimGUI.initializeComponents() o desde un menú/preferencia):
executor.setSceneFactory(new OpenGLSceneFactory());

No hay que tocar ScriptExecutor, ni el prescript, ni el listener: todo lo demás sigue igual. Si el nuevo motor no deriva de GUIScene2D, habría que generalizar el tipo de retorno de SceneFactory a un interfaz común; hoy es GUIScene2D a propósito, para no romper el resto de la API.

12.2. Añadir un tema de animación

  1. Añade el valor al enum config.AppTheme (clave, nombre visible, #nuevo.xml).
  2. Crea el #nuevo.xml en los recursos del core (donde viven los demás #dark.xml, #light.xml…).

Menú, combo del toolbar, SettingsDialog y PreScriptBuilder se actualizan solos, porque todos derivan de AppTheme.values().

12.3. Añadir una entrada de menú o botón de toolbar

  • Menú: en ui.MainMenuBar, dentro del buildXxxMenu() correspondiente, crea el JMenuItem y engánchalo a un método público de JMathAnimGUI (e -> gui.miAccion()).
  • Toolbar: igual en ui.MainToolBar.
  • Si el control debe habilitarse/deshabilitarse según el estado de la animación, regístralo en AnimationControls (ver registerMenuItems / registerToolbarButtons) en vez de tocarlo a mano; así entra en el setRunning(boolean) / setPaused(boolean) centralizado.

12.4. Añadir un color/estilo de log o un código ANSI

Todo el coloreado vive en ui.LogPanel: añade el estilo en setupLogStyles(), un valor al enum LogStyle si quieres exponerlo, y el case correspondiente en getStyleForAnsiCode().

12.5. Añadir una feature de overlay sobre el preview

Implementa PreviewOverlayManager.OverlayFeature y regístrala donde se crea el overlay, en GUIScene2D.createRenderer():

overlayManager.addFeature(new MiFeature());

Recibirás initialize(Pane), eventos de ratón/teclado y onDetach(). Mira CoordinateDisplayFeature como plantilla.

12.6. Interacción script/motor: enganchar a un nuevo evento

Si necesitas reaccionar a algo nuevo del run (p. ej. "primer frame renderizado"):

  1. Añade el método al interfaz ExecutionListener.
  2. Dispáralo desde ScriptExecutor.run(...) (o desde GUIScene2D vía callback).
  3. Impleméntalo en el listener anónimo de JMathAnimGUI.buildExecutionListener(...).

Mantén la regla: el executor no toca Swing; cualquier acceso a la UI va dentro del listener con SwingUtilities.invokeLater.

12.7. Depurar el arranque / core externo

  • Ejecuta con --no-log-file para ver el log del Launcher en consola (imprime qué core selecciona, el classloader, los JARs incluidos…).
  • El core externo debe llamarse jmathanim-core-X.Y.Z.jar, estar en <appdata>/JMathAnim/lib/, tener el mismo prefijo X.Y que el empaquetado y ser más nuevo; si no, se ignora (el log lo dice explícitamente).
  • Si la GUI "no ve" tus cambios del core, comprueba que no estás cargando por error un JAR viejo de lib/.

13. Chuleta: "¿dónde toco para…?"

Quiero… Fichero(s)
Cambiar el motor que corre los scripts execution.SceneFactory + ScriptExecutor.setSceneFactory
Cambiar el prescript (config inyectada) execution.PreScriptBuilder
Tocar el ciclo run/stop/pause/step execution.ScriptExecutor
Reaccionar a eventos del run execution.ExecutionListener + JMathAnimGUI.buildExecutionListener
Render / preview JavaFX execution.GUIJavaFXRenderer, execution.GUIScene2D
Overlay del preview execution.PreviewOverlayManager, CoordinateDisplayFeature
Menús / toolbar ui.MainMenuBar, ui.MainToolBar, ui.AnimationControls
Log / colores / progreso ui.LogPanel
Temas de animación config.AppTheme (+ xml en el core)
Persistencia / config / ventana config.GUIProperties, app.WindowStateManager
Proyectos / main script app.ProjectManager
Recientes (ficheros/proyectos) app.RecentItemsManager
Editor de texto (comandos, folding…) editor.TabbedEditorPane, editor.EditorTab
Snippets DSL dsl.DslSnippetInserter, dsl.AttrSpec
Arranque / carga del core externo app.Launcher (+ pom.xml mainClass)
Errores de script GUIScene2D.GroovyScriptException, JMathAnimGUI.handleGroovyScriptError, execution.GroovyErrorFormatter
Plugins GroovyUtils (core) + JMathAnimGUI.unloadPlugins

14. Trampas conocidas / invariantes a respetar

  • Reflexión del main: Launcher carga com.jmathanimgui.app.JMathAnimGUI por String. Sincroniza siempre ese literal con pom.xml y los scripts de empaquetado si mueves la clase.
  • Listeners de log globales: son estáticos en JMathAnimLogger; sólo un run activo a la vez. ScriptExecutor los retira en finally y en shutdown().
  • EDT vs hilo de animación: el hilo JMathAnim-Animation no debe tocar Swing. Todo lo visual pasa por el ExecutionListener con invokeLater.
  • Warmup de compilación: si tocas cómo GroovyUtils cachea la CompilerConfiguration, revisa que warmupGuiCompilation(GUIScene2D.class) sigue calentando la misma config que usan los runs reales (si no, vuelve el ~800 ms de la primera ejecución).
  • prescriptLines: cualquier cambio en PreScriptBuilder.build() que altere el número de líneas debe reflejarse en countLines(); si no, las líneas de error y el resaltado se desalinean.
  • Classloader de plugins persistente: mantiene los JAR abiertos; para reemplazar un plugin en caliente hay que unloadAllPlugins() antes. ```