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:
- Redirige
System.out/erra~/<appdata>/JMathAnim/jmathanim-gui.log(salvo--no-log-file), quitando códigos ANSI. Útil en ejecutables empaquetados sin consola. - Busca un core externo en
<appdata>/JMathAnim/lib/(Launcher.USER_LIB_DIR). Si hay unjmathanim-core-X.Y.Z.jarcompatible (mismo prefijoX.Y) y más nuevo que el empaquetado, construye unURLClassLoaderque lo antepone a todo lo demás. - 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 muevesJMathAnimGUI, hay que actualizar ese literal enLauncher.launchGUI()y el<mainClass>delpom.xmlycrea_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 hiloJMathAnim-Animation, crea el fichero temporal, prepara el directoriomedia/, engancha los listeners de log y progreso del core, detecta errores de Groovy y exponestop()/togglePause()/stepFrame()/shutdown().SceneFactory(interfaz) — decide qué escena ejecuta el script.ScriptExecutor.setSceneFactory(...)es el gancho para cambiar el motor.DefaultSceneFactory— crea unGUIScene2Dnormal.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:
- Procesa
import("...")(processImports) — inserta el contenido de los ficheros importados en línea, con marcadores// === BEGIN IMPORT: … ===. - Procesa
@JMPluginy@Grab— pide aPluginManagerlos 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@Grabvan a un classloader persistente (ver §10). - 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)(hilowarmupen el constructor deJMathAnimGUI). - Compila el script. Si falla, lanza
GroovyScriptErrorcon 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:
LineTrackingCustomizer(core) es unCompilationCustomizerque, en faseSEMANTIC_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).
-
GUIScene2D.setCurrentExecutingLine(int)simplemente guarda el valor en unvolatile int currentExecutingLine. -
La GUI hace polling, no push.
JMathAnimGUI.startLineTracking()arranca unjavax.swing.Timerde 50 ms que leeexecutor.getCurrentScene().getCurrentExecutingLine()y llama atabbedEditor.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 implementesetCurrentExecutingLine/getCurrentExecutingLine(ya están enGUIScene2D) 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): logueaError at line: N, salta a esa línea (goToErrorLine) y restaura el cursor. - Error dentro de un
import(...)(errorFile != null): reportafichero: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í:
- Desde la GUI, menú Plugins (lo habitual). Se recuerda entre sesiones.
- Desde el script, con
@JMPlugin("mi-plugin-1.0.jar"). Es lo que mantiene un script autocontenido para ejecutarlo por línea de comandos conGroovyExecutor, 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
URLClassLoaderpropio del plugin, instancia la clase y lee los snippets que traiga. No llama ainit(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 enplugins.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(...)ySnippetSidebar.refresh()se disparan desde un listener registrado enPluginManager, así que la GUI sigue también a los plugins que carga un script con@JMPluginmientras 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
PluginManagerllama aProjectManager.savePluginsState(), así que cargar o descargar algo desde el menú (o desde un@JMPluginde un script) se escribe en el fichero del proyecto al momento. - Al abrir el proyecto (
onProjectOpened, y también enrestoreProjectal 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.autoloaden la configuración del editor, que son del editor y no del proyecto. En ambos casoscurrentProjectse 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 dePropertiescon todas las claves (KEY_*,WINDOW_*,RECENT_*, …) ystoreGrouped()que escribe el fichero agrupado por secciones y comentado, en UTF-8.app.WindowStateManager— carga/guardajmathanim.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 viejoswitchde 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):
- Crea la escena (idealmente extendiendo
GUIScene2Do al menosJMathAnimScene, y conservandosetCurrentExecutingLine,requestStop,pause/resume/stepOneFramepara que sigan funcionando line-tracking y los controles). - 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);
}
}
- 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
- Añade el valor al enum
config.AppTheme(clave, nombre visible,#nuevo.xml). - Crea el
#nuevo.xmlen 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 delbuildXxxMenu()correspondiente, crea elJMenuItemy engánchalo a un método público deJMathAnimGUI(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(verregisterMenuItems/registerToolbarButtons) en vez de tocarlo a mano; así entra en elsetRunning(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"):
- Añade el método al interfaz
ExecutionListener. - Dispáralo desde
ScriptExecutor.run(...)(o desdeGUIScene2Dvía callback). - 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-filepara ver el log delLauncheren 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 prefijoX.Yque 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:Launchercargacom.jmathanimgui.app.JMathAnimGUIporString. Sincroniza siempre ese literal conpom.xmly 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.ScriptExecutorlos retira enfinallyy enshutdown(). - EDT vs hilo de animación: el hilo
JMathAnim-Animationno debe tocar Swing. Todo lo visual pasa por elExecutionListenerconinvokeLater. - Warmup de compilación: si tocas cómo
GroovyUtilscachea laCompilerConfiguration, revisa quewarmupGuiCompilation(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 enPreScriptBuilder.build()que altere el número de líneas debe reflejarse encountLines(); 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. ```