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-coresalvo 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
JMAnimBaseScripty unImportCustomizerque hace star-import de todocom.jmathanim.*(GroovyUtils.java:771). No hacen falta imports paraShape,Vec,AnchorType,MathObject,JMA... - El binding trae
scene,play,config,camera,fixedCamera,scriptDir,PI,DEGREES. def x = ...en el cuerpo del script es local arun()y no se ve dentro de los métodos que defina el script. Sindefva al binding y sí se ve. Los closures escritos en el cuerpo sí capturan losdefanteriores 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íneasimport, 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.@Fieldestá 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
groupes un contenedor lógico: al añadirlo a la escena se añaden sus miembros, y el grupo no dibuja nada.compoundes 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: [:]oobj: []vacíos son error; para un contenedor que se llena después hay que omitirobj(ContainerDSL.groovy:74).- Orden del final del build:
homogeneize→layout→ props comunes (style,transform,stack) →parts→init→addToScene(ContainerDSL.groovy:112). Por esoinit:es el único sitio donde una pieza puede colocarse contra otra: los valores del mapaobj:se evalúan antes de que el contenedor exista. methods:se instala enapplyCommonProps, antes queparts:einit:, así queinit:ya puede llamar a los métodos declarados.- Piezas por nombre:
c["nombre"]no tiene fallback y siempre da la pieza;c.nombresí lo tiene, y una propiedad real (center,width,layer,visible,path...) gana. Como un anclaBoxablees 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,hasKeypregunta (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 condelta(...)y tiene que ser su propio inverso exacto.
methods: (mapa nombre → closure)
- Dentro del cuerpo,
delegatees el objeto, conDELEGATE_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
itimplícito; para un método de cero argumentos hay que escribir{ -> ... }. - Un nombre que empieza por
getse 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)oMethodsDSL.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:294es el bloque,JMA.playa secas es elPlayAnim). - Para un objeto de escena de verdad:
AbstractCompoundMathObject<MiClase, MathObject<?>>, implementandocopy()(lo único abstracto que queda) y usandocopyBaseTo(copia)(:618), que lleva estilo, piezas, nombres y metadatos. - No guardar los hijos en campos: registrarlos con
addWithKeyy leerlos del diccionario, porquecopy()sí remapea el diccionario y los campos se quedan a null. Los campos de valor propios hay que copiarlos a mano encopy(). - 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
copyBaseToa 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, nuncaprintln.
6. Trampas verificadas
Estilos y animación de estilos
animStyle(style: [drawAlpha: 0])falla con NPE. El estilo destino se construye sobreMODrawProperties.makeNullValues()(AnimStyleDSL.groovy:124) ysetDrawAlpha/setFillAlphaleendrawColor/fillColorsin comprobar null (MODrawProperties.java:373y:412). Solución: que los colores viajen con los alphas,MI_ESTILO + [drawAlpha: 0].StyleDSLaplica las claves en el orden en que están escritas (StyleDSL.groovy:93), así que el color tiene que ir antes que su alpha.animStyleinterpola sólo las propiedades que menciona el destino: para desvanecer y volver hay que declarardrawAlpha/fillAlphaexplícitamente en el estilo de destino, aunque valgan 1.apply(style: [drawAlpha: 0])sí funciona sobre un objeto real, porque escribe en sump, que ya tiene color.
Colocación
stack:por defecto usadestinyAnchor: CENTERyoriginAnchor= el reverso del anterior (StackDSL.groovy:31-32), así questack: [to: otro]centra. Exactamente uno deto,screenopoint.- Ángulos siempre en radianes (
PI/2,30*DEGREES). - Coordenadas:
Vec + [x, y]yVec + Coordinatesexisten por extensión. Para escribir coordenadas,copyCoordinatesFrom, nuncasetX/setYencadenados.
Conectores
connectionType: "circular"es el círculo circunscrito a la caja del objeto (radior·√2para un círculo de radior), así que deja un hueco visible;"inscribed"toca el borde exacto. Alias enConnectorDSL.groovy:154.head/tailpor defecto:ARROW2en la punta yNONE_BUTTen 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 siemprerange:(oxRange,yRange,tRange,axisRange). Antesrange: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 encontourPlot,densityPlot,vectorField,layoutyanimCamera;bounds:envoronoi;objs:yshapes:encombine. - Una lista de objetos es
obj:.points:(nubes de puntos),anims:(animaciones) yapply:(destinatarios de algo ya construido) nombran cosas distintas y se quedan como están. originAnchor/destinyAnchor(stack, updater) yanchorStart/anchorEnd(conectores) también se quedan: son anclas de alineación frente a puntos de salida, no el mismo concepto.
Varios
setMetadataes unputen un mapa, sin cambio de versión (MathObject.java:1165): es barato y no dispara redibujados.getWidth,getHeight,getCenterson defaults deBoxable(Boxable.java:52y:177), legibles como propiedades:obj.width.- Una clase con
isX()ygetX()a la vez hace queobj.xdevuelva el boolean. - Parámetros numéricos opcionales: comprobar
!= null, no la verdad de Groovy, o un0o unfalselegítimos se pierden en silencio. - Los bloques
play { }/sequence { }juntan lo construido dentro;run: falsedentro de un bloque es redundante.
7. Checklist antes de entregar código
- ¿He leído el
*DSL.groovyde cada builder que uso, o al menos el cheatsheet? - ¿Está todo en DSL lo que el DSL cubre?
- ¿Los comentarios son cortos y en inglés?
- Si hay clases: ¿
JMAen vez del binding,copy()implementado, hijos poraddWithKey, sin líneasimportsi el fichero se incluye? - ¿Alguna animación de alpha sin color acompañándolo?
- ¿Alguna pieza añadida o sustituida después de fijar capas?
- Sin ejecutar ni compilar para comprobar, salvo petición expresa. Sin commit.