Escribir código JMathAnim (guía de trabajo del asistente)

Documento de consulta previa a escribir cualquier script o clase para este proyecto. Recoge lo que no está en el manual, o lo que está repartido y se tarda en encontrar. Complementa a CLASES_GROOVY_PERSONALIZADAS.md, que cubre las clases propias en Groovy.

Todas las referencias apuntan a jmathanim-core salvo que se diga otra cosa.


1. Qué leer antes de escribir

Nunca escribir DSL de memoria: la API se mueve y un nombre de parámetro inventado parece plausible. Todo esto está en el repositorio y se lee rápido:

Fuente Para qué
docs/manual/cheatsheet/main.typ Referencia de sintaxis de todo el DSL, ~2600 líneas. Se busca por palabra clave (== \compound(...)``). La versión markdown se borró en agosto de 2026
.../groovy/dsl/JMAnimBaseScript.groovy Lista canónica de funciones de script. Una por entrada del DSL, cada una delegando en su *DSL.groovy
.../groovy/dsl/<Nombre>DSL.groovy Las claves aceptadas de verdad, en KNOWN_KEYS, y el javadoc de cabecera con ejemplos
src/main/resources/autocomplete/dsl-completions.yaml Parámetros y valores tal como los ve el editor
docs/manual/<NN>_<Tema>/*.md El porqué y ejemplos ejecutables
docs/dev/CLASES_GROOVY_PERSONALIZADAS.md Clases propias: de qué heredar, JMA, trampas de copy()
docs/dev/OBJETOS_COMPUESTOS_POR_SUBCLASE.md Compuesto propio como subclase de StatefulCompound, paso a paso, en Java

Regla práctica: leer el *DSL.groovy correspondiente antes de usar una clave que no se haya visto ya en el cheatsheet. Los KNOWN_KEYS son la verdad; el cheatsheet a veces va un paso por detrás.


2. Cómo quiere el usuario el código

  • DSL siempre que se pueda. Las llamadas directas cortas (scene.add(s), scene.waitSeconds(2)) se quedan como están; todo lo que el DSL cubra se escribe en forma de DSL.
  • Variante "Groovy" de un cookbook = cero DSL. Constructores y métodos directos, para que el lector compare. No mezclar las dos formas.
  • Comentarios cortos y en inglés, y sólo cuando aportan.
  • Nada de recompilar ni ejecutar para "demostrar" un cambio salvo que lo pida. Basta con enumerar lo cambiado.
  • Nunca hacer commit ni push. El control de versiones lo lleva él.

3. Anatomía de un script

  • Se compila con clase base JMAnimBaseScript y un ImportCustomizer que hace star-import de todo com.jmathanim.* (GroovyUtils.java:771). No hacen falta imports para Shape, Vec, AnchorType, MathObject, JMA...
  • El binding trae scene, play, config, camera, fixedCamera, scriptDir, PI, DEGREES.
  • def x = ... en el cuerpo del script es local a run() y no se ve dentro de los métodos que defina el script. Sin def va al binding y sí se ve. Los closures escritos en el cuerpo sí capturan los def anteriores a ellos.
  • import("fichero.groovy") es una directiva de JMathAnim, no el import de Java: inclusión textual, recursiva y sin duplicados (GroovyUtils.java:990). Consecuencia importante: un fichero incluido no puede contener líneas import, porque acabarían en mitad del script principal.
  • r"..." son cadenas crudas para LaTeX, preprocesadas en todo el fichero, también dentro de las clases. r["$a$", "$b$"] hace crudo un bloque entero.
  • @Field está prohibido: el preprocesador borra la línea entera (GroovyUtils.java:554).
  • Las llamadas de config... van antes de crear ningún objeto.

4. Contenedores: group vs compound

  • group es un contenedor lógico: al añadirlo a la escena se añaden sus miembros, y el grupo no dibuja nada.
  • compound es el objeto de escena: entra, se dibuja, se quita y se anima como una unidad, y sus piezas son internas. Es lo que hay que usar para construir un objeto nuevo a partir de partes.

Cosas que se aprenden a base de tropezar:

  • obj: [:] o obj: [] vacíos son error; para un contenedor que se llena después hay que omitir obj (ContainerDSL.groovy:74).
  • Orden del final del build: homogeneize → layout → props comunes (style, transform, stack) → parts → init → addToScene (ContainerDSL.groovy:112). Por eso init: es el único sitio donde una pieza puede colocarse contra otra: los valores del mapa obj: se evalúan antes de que el contenedor exista.
  • methods: se instala en applyCommonProps, antes que parts: e init:, así que init: ya puede llamar a los métodos declarados.
  • Piezas por nombre: c["nombre"] no tiene fallback y siempre da la pieza; c.nombre sí lo tiene, y una propiedad real (center, width, layer, visible, path...) gana. Como un ancla Boxable es un punto usable, el error no falla, sólo coloca mal.
  • Dentro del código: get(String) avisa si no existe, find(String) calla y devuelve null, hasKey pregunta (AbstractCompoundMathObject.java:322-375).
  • addWithKey(clave, obj) añade y nombra; remove(String) quita por nombre; set(int, obj) sustituye conservando el nombre de la ranura (:240, :292, :402).
  • Capas dentro de un compound: las capas de las piezas sólo las ordenan entre ellas (getDrawingOrder, :473), mientras que la capa del compound lo coloca entre los objetos de escena y aplana las de sus piezas. Una pieza añadida después nace en la capa 0: si el resto está en la capa 1, aparecerá debajo. Tras sustituir una pieza, volver a fijar la capa.
  • state: + pose:/rebuild: para geometría que depende de valores con nombre. pose: devuelve las piezas a su posición de construcción antes de cada pasada (nada deriva, cuesta una copia por pieza y frame con cambio); rebuild: trabaja con delta(...) y tiene que ser su propio inverso exacto.

methods: (mapa nombre → closure)

  • Dentro del cuerpo, delegate es el objeto, con DELEGATE_FIRST (MethodsDSL.groovy:129): sus miembros ganan a los del script. Las funciones del DSL (shape, latex, connector, apply, play { }) se resuelven en el script porque el objeto no las tiene.
  • Un closure sin lista de parámetros explícita acepta un it implícito; para un método de cero argumentos hay que escribir { -> ... }.
  • Un nombre que empieza por get se convierte además en propiedad.
  • Los closures viven en la metaclase de esa instancia. Una copia se los repone sola si es un contenedor (hook de copia, MethodsDSL.groovy:75); para lo demás, MethodsDSL.transfer(original, copia) o MethodsDSL.reinstall(copia).

5. Clases propias

Ver CLASES_GROOVY_PERSONALIZADAS.md entero. Lo mínimo:

  • Dentro de una clase no existen ni el binding ni el DSL. Todo va por JMA: JMA.scene, JMA.shape(...), JMA.connector(...), JMA.apply(...), JMA.play { ... } (JMA.java:294 es el bloque, JMA.play a secas es el PlayAnim).
  • Para un objeto de escena de verdad: AbstractCompoundMathObject<MiClase, MathObject<?>>, implementando copy() (lo único abstracto que queda) y usando copyBaseTo(copia) (:618), que lleva estilo, piezas, nombres y metadatos.
  • No guardar los hijos en campos: registrarlos con addWithKey y leerlos del diccionario, porque copy() sí remapea el diccionario y los campos se quedan a null. Los campos de valor propios hay que copiarlos a mano en copy().
  • Los metadatos que apuntan a otras piezas se copian tal cual y quedan apuntando a las piezas del original. Si hay referencias cruzadas, rederivarlas después de copyBaseTo a partir de las claves.
  • El nombre de la clase queda en el paquete por defecto y gana a los star-imports: no llamarla Line, Point, Text, Shape, Group...
  • JMathAnimScene.logger.warn/info, nunca println.

6. Trampas verificadas

Estilos y animación de estilos

  • animStyle(style: [drawAlpha: 0]) falla con NPE. El estilo destino se construye sobre MODrawProperties.makeNullValues() (AnimStyleDSL.groovy:124) y setDrawAlpha/setFillAlpha leen drawColor/fillColor sin comprobar null (MODrawProperties.java:373 y :412). Solución: que los colores viajen con los alphas, MI_ESTILO + [drawAlpha: 0].
  • StyleDSL aplica las claves en el orden en que están escritas (StyleDSL.groovy:93), así que el color tiene que ir antes que su alpha.
  • animStyle interpola sólo las propiedades que menciona el destino: para desvanecer y volver hay que declarar drawAlpha/fillAlpha explícitamente en el estilo de destino, aunque valgan 1.
  • apply(style: [drawAlpha: 0]) sí funciona sobre un objeto real, porque escribe en su mp, que ya tiene color.

Colocación

  • stack: por defecto usa destinyAnchor: CENTER y originAnchor = el reverso del anterior (StackDSL.groovy:31-32), así que stack: [to: otro] centra. Exactamente uno de to, screen o point.
  • Ángulos siempre en radianes (PI/2, 30*DEGREES).
  • Coordenadas: Vec + [x, y] y Vec + Coordinates existen por extensión. Para escribir coordenadas, copyCoordinatesFrom, nunca setX/setY encadenados.

Conectores

  • connectionType: "circular" es el círculo circunscrito a la caja del objeto (radio r·√2 para un círculo de radio r), así que deja un hueco visible; "inscribed" toca el borde exacto. Alias en ConnectorDSL.groovy:154.
  • head/tail por defecto: ARROW2 en la punta y NONE_BUTT en la cola. Para una línea sin puntas, head: "none_butt".
  • Los conectores anclados a objetos se actualizan solos; dentro de un compound, las piezas se actualizan con él.

Nombres de parámetros (agosto 2026)

  • Una región rectangular es siempre rect:; un intervalo de una variable es siempre range: (o xRange, yRange, tRange, axisRange). Antes range: significaba las dos cosas según el builder.
  • Los nombres viejos siguen funcionando y avisan una vez por llamada (BaseDSL.firstOfRenamed): range: como rectángulo en contourPlot, densityPlot, vectorField, layout y animCamera; bounds: en voronoi; objs: y shapes: en combine.
  • Una lista de objetos es obj:. points: (nubes de puntos), anims: (animaciones) y apply: (destinatarios de algo ya construido) nombran cosas distintas y se quedan como están.
  • originAnchor/destinyAnchor (stack, updater) y anchorStart/anchorEnd (conectores) también se quedan: son anclas de alineación frente a puntos de salida, no el mismo concepto.

Varios

  • setMetadata es un put en un mapa, sin cambio de versión (MathObject.java:1165): es barato y no dispara redibujados.
  • getWidth, getHeight, getCenter son defaults de Boxable (Boxable.java:52 y :177), legibles como propiedades: obj.width.
  • Una clase con isX() y getX() a la vez hace que obj.x devuelva el boolean.
  • Parámetros numéricos opcionales: comprobar != null, no la verdad de Groovy, o un 0 o un false legítimos se pierden en silencio.
  • Los bloques play { } / sequence { } juntan lo construido dentro; run: false dentro de un bloque es redundante.

7. Checklist antes de entregar código

  1. ¿He leído el *DSL.groovy de cada builder que uso, o al menos el cheatsheet?
  2. ¿Está todo en DSL lo que el DSL cubre?
  3. ¿Los comentarios son cortos y en inglés?
  4. Si hay clases: ¿JMA en vez del binding, copy() implementado, hijos por addWithKey, sin líneas import si el fichero se incluye?
  5. ¿Alguna animación de alpha sin color acompañándolo?
  6. ¿Alguna pieza añadida o sustituida después de fijar capas?
  7. Sin ejecutar ni compilar para comprobar, salvo petición expresa. Sin commit.