El formato .ggb y el importador de JMathAnim

Referencia del formato de archivo de GeoGebra, obtenida leyendo el generador de XML del propio GeoGebra, y estado del importador de JMathAnim frente a él.

Existe para no volver a hacer ingeniería inversa: cada afirmación de aquí lleva la clase y la línea del código de GeoGebra que la produce.

Código de JMathAnim implicado: jmathanim-core/src/main/java/com/jmathanim/mathobjects/constructible/GeogebraLoader.java y GeogebraCommandParser.java, con tests en jmathanim-core/src/test/java/com/jmathanim/Geogebra*Test.java y .../mathobjects/constructible/GeogebraExpressionParserTest.java.


0. Dónde está la documentación real

No hace falta adivinar el formato. Hay dos fuentes:

El esquema XSD. Cada .ggb lo declara en su propio tag raíz:

<geogebra format="5.0" version="5.0.478.0" app="classic" platform="d" id="..."
          xsi:noNamespaceSchemaLocation="https://www.geogebra.org/apps/xsd/ggb.xsd"
          xmlns="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

La URL se construye en MyXMLio.addGeoGebraHeader (MyXMLio.java:268): ggb.xsd para archivos normales y ggt.xsd para herramientas. El XSD no está en el repositorio de GeoGebra, solo publicado en esa URL.

El código fuente. Es la fuente autoritativa y la que se ha usado para este documento. Las rutas de abajo son relativas a:

<repo-geogebra>/source/shared/common/src/main/java/org/geogebra/common/
Rol Clase Para qué sirve
Escribe un objeto kernel/geos/GeoElement.java:4418 (getXML) Orden de los tags de un <element>
Escribe el estilo kernel/geos/XMLBuilder.java:52 (getXMLVisualTags) Qué tags de estilo se emiten y cuándo
Escribe un comando kernel/algos/AlgoElement.java:1451 (getCmdXML) Estructura de <command>
Lee todo io/ConsElementXMLHandler.java:2213-2492 Catálogo completo de tags de <element>
Lee vista y GUI io/MyXMLHandler.java Todo lo que hay fuera de <construction>
Vista euclídea euclidian/EuclidianView.java:4631 (startXML) Cómo se serializa la cámara

Ese switch de ConsElementXMLHandler (unos 105 casos) es literalmente la lista cerrada de tags que puede llevar un <element>. Es el checklist de referencia.


1. El contenedor

Un .ggb es un ZIP. Nombres de entrada en io/MyXMLio.java:51-76:

Entrada Contenido
geogebra.xml La construcción y todos los ajustes. Lo único que lee hoy JMathAnim
geogebra_defaults2d.xml Estilos por defecto de cada tipo de objeto en 2D
geogebra_defaults3d.xml Ídem en 3D
geogebra_macro.xml Herramientas propias del usuario (solo si las hay)
geogebra_javascript.js Scripts globales
geogebra_thumbnail.png Miniatura
(varias) Las imágenes insertadas, con el nombre que declara <file name="...">

format="5.0" es constante desde hace años (GeoGebraConstants.java:176); el atributo version sí cambia y es el que distingue archivos antiguos.


2. Estructura de geogebra.xml

<geogebra format="5.0" version="..." app="classic" platform="d" id="...">
  <gui>          ...  <font size="16"/>  </gui>
  <euclidianView> ... </euclidianView>          <!-- puede haber varias -->
  <euclidianView3D> ... </euclidianView3D>
  <algebraView> ... </algebraView>
  <kernel> ... </kernel>
  <tableview .../>
  <scripting .../>
  <construction title="" author="" date="">
     <worksheetText above="" below=""/>
     ... elementos, comandos y expresiones, en orden de construcción ...
     <group .../>
  </construction>
</geogebra>

Construction.getConstructionXML (kernel/Construction.java:1278) escribe la cabecera, el texto de hoja de trabajo, los elementos (:1318) y los grupos.


3. Reglas de serialización que hay que conocer

3.1 <element>, <command> y <expression>

Para cada elemento de la construcción, en orden:

  • Objeto independiente → <expression> opcional (GeoElement.java:4431) seguido de su <element>.
  • Objeto dependiente → <command> seguido de un <element> por cada salida etiquetada (AlgoElement.java:1318).
  • Objeto definido por fórmula (los AlgoDependent*) → el nombre de comando es "Expression" y entonces no se escribe <command> sino <expression> (AlgoElement.java:1345 y getExpXML en :1410):
<expression label="m" exp="a + 7 b"/>
<expression label="P" exp="A + (1,2)" type="point"/>

El atributo type solo aparece para point, vector, line, plane, conic, quadric, implicitpoly, surfacecartesian y list.

Consecuencia práctica: un objeto puede llegar sin <command> sin que sea un objeto libre. Hay que mirar también las <expression>.

Un <element> siempre viene después del <command> o la <expression> que lo define, así que un parser de una sola pasada en orden de documento es correcto.

3.2 Los argumentos de <command> no siempre son nombres

<command name="Circle">
  <input a0="A" a1="B"/>
  <output a0="c"/>
</command>

Los atributos son a0, a1, a2... y su valor es geo.getLabel(xmlTemplate). Cuando el argumento no tiene etiqueta, ese método devuelve su definición, no un nombre (AlgoElement.java:1487). De ahí salen valores como:

  • (1,2) — punto anónimo, siempre en cartesianas (ver 3.5)
  • Vector[(4,-3)] — GeoGebra envuelve a propósito los vectores sin etiqueta para que sigan siendo vectores al recargar (AlgoElement.java:1480)
  • Rotate[(1,2),(3,4)] — un comando entero anidado
  • 30° — un ángulo en grados

3.3 Las salidas pueden ser cadena vacía

getCmdOutputXML (AlgoElement.java:1584) escribe a_i="" para las salidas sin etiqueta, no las omite. Hay que saltarlas, no tratarlas como objetos sin nombre.

3.4 Los tags que coinciden con el valor por defecto no se escriben

Por eso el ZIP incluye geogebra_defaults2d.xml, con entradas <element type="..." default="N"> dentro de un <defaults>. N es el identificador de kernel/ConstructionDefaults.java:81-155:

N Constante N Constante
10 DEFAULT_POINT_FREE 40 DEFAULT_CONIC
11 DEFAULT_POINT_DEPENDENT 41 DEFAULT_CONIC_SECTOR
12 DEFAULT_POINT_ON_PATH 50 DEFAULT_NUMBER
13 DEFAULT_POINT_IN_REGION 52 DEFAULT_ANGLE
20 DEFAULT_LINE 60 DEFAULT_FUNCTION
21 DEFAULT_SEGMENT 70 DEFAULT_POLYGON
25 DEFAULT_RAY 71 DEFAULT_POLYLINE
30 DEFAULT_VECTOR 100 DEFAULT_TEXT

El mapeo objeto → N está en ConstructionDefaults.getDefaultType(GeoElement), y depende de si el objeto es independiente, está sobre un camino o dentro de una región.

Consecuencia: reconstruir el estilo exacto exige leer geogebra_defaults2d.xml. Sin él, la ausencia de un tag no se puede interpretar.

3.5 Formato de los números

StringTemplate.xmlTemplate (kernel/StringTemplate.java:308-328):

  • forceSF = true con 15 cifras significativas → puede aparecer notación científica (1.234E-5). Double.parseDouble lo acepta.
  • localizeCmds = false → los nombres de comando dentro de expresiones son siempre los internos en inglés, independientemente del idioma del usuario.
  • getCoordStyle() devuelve COORD_STYLE_DEFAULT → los puntos en XML son siempre cartesianos (x, y), nunca polares. Esto simplifica el parseo de argumentos.

4. Valores de type= en <element>

Salen de GeoClass.xmlName (plugin/GeoClass.java). Los 2D relevantes:

point  vector  line  ray  segment  polygon  polyline  conic  conicpart  angle
numeric  boolean  text  formula  image  function  functionNVar  curvecartesian
implicitpoly  list  locus  penstroke  button  textfield  inlinetext  table
piechart  stadium  casCell  audio  video  embed

Los 3D añaden sufijo: point3d, line3d, conic3d, plane3d, quadric, polygon3d...

GeoClass guarda además una prioridad de dibujo por tipo, usada por kernel/geos/DefaultGeoPriorityComparator.java:24. El orden real de pintado es:

layer  →  prioridad de tipo  →  índice de construcción  →  id

Prioridades: polígono 50, polilínea 51, cónica 70, número/ángulo 80, función 90, recta 100, segmento/rayo 110, vector 120, locus 130, punto 140, texto 150. Por eso en GeoGebra los puntos quedan siempre encima de los polígonos aunque compartan capa. Existe además un tag <ordering val="..."/> para orden explícito.


5. Dónde está la geometría ya evaluada

Esto es lo más útil para importar: el <element> siempre lleva el resultado que GeoGebra calculó, aunque el <command> que lo generó no se sepa interpretar.

type Tag Semántica
point, line, ray, segment, vector <coords x y z> Punto: homogéneas, dividir por z. Recta: x·X + y·Y + z = 0. Vector: componentes, con z=0. (kernel/geos/GeoVec3D.java:518)
vector <startPoint> De dónde se dibuja; sin él, del origen (kernel/geos/GeoVector.java:599)
conic, conicpart <matrix A0..A5> A0·x² + A1·y² + A2 + 2·A3·xy + 2·A4·x + 2·A5·y = 0 (kernel/kernelND/GeoConicND.java:3370, índices en kernelND/ConicMatrix.java)
conic <eigenvectors x0 y0 z0 x1 y1 z1> Direcciones de los ejes, ya calculadas. Se escribe antes de <matrix> a propósito
numeric, angle <value val> Valor. En un ángulo, en radianes (kernel/geos/GeoAngle.java:457)
text <expression exp> El contenido, incluidas las comillas
image <file name> + <startPoint number=...> Ver 6.3
function <expression exp> La fórmula; no hay forma tabulada

Un punto al infinito tiene z="0" en <coords>; entonces x,y son la dirección.

Lo que NO está guardado. Un segment y un ray solo llevan la recta que los soporta, nunca sus extremos (GeoSegment.getStyleXML solo añade outlyingIntersections y keepTypeOnTransform). Un polygon no lleva sus vértices (GeoPolygon.java:1683). Un conicpart no lleva los parámetros del arco: no hay ningún tag para ellos en ConsElementXMLHandler. Esos tipos solo se pueden reconstruir a través de su <command>.


6. Detalles que muerden

6.1 <coordSystem> tiene dos formas excluyentes

EuclidianView.startXML (euclidian/EuclidianView.java:4653):

if (!isZoomable() && !asPreference) {
    sbxml.attr("xMin", ...).attr("xMax", ...).attr("yMin", ...).attr("yMax", ...);
} else {
    sbxml.attr("xZero", ...).attr("yZero", ...).attr("scale", ...).attr("yscale", ...);
}
  • Forma zoomable: xZero/yZero es la posición del origen en píxeles y scale/yscale los píxeles por unidad. Necesita <size width height> para convertirse en límites:
xmin = -xZero / scale          xmax = (width - xZero) / scale
ymin = (yZero - height)/yscale  ymax = yZero / yscale
  • Forma fijada: el usuario ha anclado los límites y se guardan tal cual, en coordenadas matemáticas. No hay xZero ni scale. Además sus valores son etiquetas de GeoNumeric, que suelen ser números literales pero pueden nombrar un elemento de la construcción.

yscale es opcional; si falta, ambos ejes comparten escala.

<size> solo se escribe si width > 50 && height > 50 (EuclidianView.java:4646), así que puede faltar.

6.2 Cuál es la vista principal

EuclidianViewCompanion.getXMLid (euclidian/EuclidianViewCompanion.java:211) escribe el hijo <viewNumber viewNo="N"/> solo si evNo >= 2. La regla correcta es:

La vista principal es la <euclidianView> sin hijo <viewNumber>, o con viewNo="1".

Quedarse con la primera en orden de documento es una heurística que falla en archivos donde la vista secundaria se escribe antes. (Archivos antiguos, como los de 2018, sí escriben viewNo="1" explícitamente; por eso hay que aceptar ambos casos.)

6.3 Las esquinas de una imagen

XMLBuilder.getCornerPointXML (kernel/geos/XMLBuilder.java:303):

<startPoint number="0" exp="A"/>          <!-- esquina ligada a un punto -->
<startPoint number="2" x="3" y="1" z="1"/><!-- esquina en coordenadas absolutas -->
  • number: 0 = inferior izquierda, 1 = inferior derecha, 2 = superior izquierda, 3 = centro (GeoImage.CENTER_INDEX, kernel/geos/GeoImage.java:55). Si el atributo falta, GeoGebra asume 0 (ConsElementXMLHandler.java:1272).
  • Las esquinas nulas no se escriben, así que la posición en el documento no coincide con el número de esquina.
  • Con exp la esquina está ligada a un punto etiquetado; con x/y/z es absoluta. En archivos antiguos el nombre del punto viene en label en vez de en exp. El caso habitual (imagen pegada sin puntos) es el absoluto.
  • <centered val="true"/> indica que la esquina 3 es el centro.
  • Con <absoluteScreenLocation x y> la imagen está anclada a la pantalla, no al mundo.

Cómo completa GeoGebra las esquinas que faltan. En GeoImage.getInternalCornerPointCoords (kernel/geos/GeoImage.java:845) y getCornerAx/getCornerAy (:821), con A el ancla, B la esquina 1, D la esquina 2:

A  = centrado ? centro - (anchoPx/2/xScale, altoPx/2/yScale) : esquina0

B  = esquina1                                     si existe
   = A + (anchoPx/xScale, 0)                      si tampoco hay esquina2
   = A + (anchoPx/altoPx)·(D.y - A.y, A.x - D.x)  si hay esquina2

D  = esquina2                                     si existe
   = A + (0, altoPx/yScale)                       si tampoco hay esquina1
   = A + (altoPx/anchoPx)·(A.y - B.y, B.x - A.x)  si hay esquina1

O sea que el tamaño de una imagen sin esquinas explícitas depende de dos datos que no están en el <element>: el tamaño en píxeles del archivo de imagen y la escala de la vista.

6.4 El tamaño de fuente de los textos

GeoText.appendFontTag (kernel/geos/GeoText.java:1294):

sb.attr("serif", serifFont);
sb.attr("sizeM", fontSizeD);                        // el valor real: un multiplicador
double oldFontSize = app.getFontSize()*fontSizeD - app.getFontSize();
sb.attr("size", (int) oldFontSize);                 // delta legado, "for ggb40 compatibility"
sb.attr("style", fontStyle);

size es un incremento respecto al tamaño de la GUI y puede ser 0 o negativo. El tamaño real es sizeM × <gui><font size="16"/> (main/App.java:2494). El tag <font> solo se escribe si algo es no-default, así que su ausencia significa sizeM=1, style=0, serif=false.

6.5 Intersect con tercer argumento

kernel/commands/CmdIntersect.java:385 (intersect3). El tercer argumento puede ser:

  • un número: índice del punto de intersección (recta-cónica, cónica-cónica, polinomio-recta)
  • un punto: sugerencia de rama para la búsqueda numérica

Y con cuatro argumentos, los dos últimos son xMin, xMax de un intervalo, no índices.

6.6 Grosores en píxeles

  • Línea: el ancho del trazo es lineThickness / 2.0 píxeles (euclidian/Drawable.java:629).
  • Punto: pointSize es el radio en píxeles; el diámetro es 2·pointSize (euclidian/draw/DrawPoint.java:196).

Ninguna de las dos depende del zoom: son medidas en píxeles de pantalla. Sí dependen del tamaño de la vista, porque una línea de 2.5 px pesa el doble en una vista de 500 px que en una de 1000.

Del lado de JMathAnim, thickness es una fracción del ancho de la imagen. La conversión sale de Renderer.ThicknessToMathWidth, que devuelve thickness · anchoVistaMat / 4000: el cociente entre el grosor y el ancho de la vista es thickness/4000, así que

thickness / 4000 = píxelesDeTrazo / anchoVistaGgbPx

Para el punto vale lo mismo: AbstractPoint.generateDotShape escala el círculo unidad por .5 · ThicknessToMathWidth(this), o sea que el diámetro del punto es exactamente ThicknessToMathWidth(thickness).

Además, <evSettings lineThicknessScaled="true"/> (la app Notes) hace que el trazo sí siga al zoom: Drawable.updateStrokes multiplica el grosor por xScale / 50 (EuclidianView.SCALE_STANDARD).


7. Tags de estilo

Escritos por XMLBuilder.getXMLVisualTags (kernel/geos/XMLBuilder.java:52) y GeoElement.getLineStyleXML (kernel/geos/GeoElement.java:4722).

<show object="true" label="true" ev="2"/>
<objColor r="77" g="77" b="255" alpha="0.0"/>
<layer val="0"/>
<labelMode val="0"/>
<lineStyle thickness="5" type="0" typeHidden="1" opacity="178"/>
<pointSize val="5"/>
<pointStyle val="0"/>

<show ev="N"> es una máscara de bits: bit 0 = oculto en Graphics 1, bit 1 = visible en Graphics 2, bits 2-5 para 3D. Sin el atributo, el objeto se ve en Graphics 1 y no en Graphics 2.

<objColor alpha> va en [0,1] y es la opacidad del relleno.

<lineStyle opacity> va en [0,255] y es la opacidad del trazo. El valor por defecto de polígonos, cónicas, funciones y polilíneas es 178, o sea que casi todos los trazos de GeoGebra se dibujan al 70%.

lineStyle type (plugin/EuclidianStyleConstants.java:24-34):

Valor Estilo
-1 POINTWISE
0 continuo
10 discontinuo corto
15 discontinuo largo
20 punteado
30 raya-punto

pointStyle (plugin/EuclidianStyleConstants.java:173-184):

Valor Estilo Valor Estilo
-1 usar el default 5 rombo hueco
0 punto relleno 6-9 triángulo N/S/E/O
1 aspa 10 sin borde
2 círculo hueco
3 cruz
4 rombo relleno

decoration type (kernel/kernelND/GeoElementND.java:112-147): en segmentos 1-3 son una, dos o tres marcas y 4-6 una, dos o tres flechas; en ángulos 1-2 arcos, 3-5 marcas, 6-7 flechas.

labelMode val: 0 nombre, 1 nombre+valor, 2 valor, 3 título, 4 título+valor.

Otros tags de presentación que existen y conviene tener localizados: <caption val>, <condition showObject> (visibilidad condicional), <dynamicCaption>, <arcSize>, <angleStyle>, <emphasizeRightAngle>, <startStyle>/<endStyle> (puntas de flecha), <fixed>, <auxiliary>, <trace>, <bgColor>, <objColor dynamicr/dynamicg/dynamicb>.


8. Comandos

Los nombres son los internos de kernel/commands/Commands.java, siempre en inglés. Tabla de geometría y de cónicas:

Line  Ray  Segment  AngularBisector  OrthogonalLine  LineBisector  Tangent  Polar
Point  PointIn  Midpoint  Intersect  IntersectPath  IntersectRegion  ClosestPoint  Vertex
Distance  Length  Radius  Area  Circumference  Perimeter  Slope  Angle  InteriorAngles
CircleArc  Arc  Sector  CircleSector  CircumcircleArc  CircumcircleSector
Polygon  RigidPolygon  PolyLine  PenStroke  Locus  Centroid  TriangleCenter  Barycenter
Circle  Incircle  Semicircle  Ellipse  Hyperbola  Parabola  Conic
Center  Focus  Directrix  Axes  FirstAxis  SecondAxis  Diameter  Eccentricity  Parameter
Mirror  Rotate  Translate  Dilate  Shear  Stretch

Mirror cubre simetría axial, central e inversión respecto a una circunferencia según el tipo del segundo argumento.

Cuidado con los arcos, que son seis comandos distintos y no uno con variantes:

Comando Argumentos Procesador
CircleArc, CircleSector centro, A, B CmdCircleArcSector
Arc, Sector una cónica ya existente más dos números o dos puntos de ella CmdArcSector
CircumcircleArc, CircumcircleSector A, B, C, los tres de paso CmdCircumcircleArc

Arc no generaliza a CircleArc: recorta una cónica que ya está en la construcción, así que sirve también para elipses.

De los seis, el importador cubre CircleArc, CircleSector y CircumcircleArc (ver 9.6).

Casos especiales: <command name="AlgoNonCommand"> cuando el algoritmo no tiene comando asociado (AlgoElement.java:1456), y <command name="MiHerramienta"> para las macros definidas en geogebra_macro.xml.


9. Estado del importador

Resuelto

  • Dos formas de <coordSystem> y ausencia de <size>: GeogebraLoader.parseEuclidianView. Los límites fijados que sean etiquetas se resuelven contra la construcción, por lo que la construcción se parsea antes que la vista.
  • Selección de la vista principal por <viewNumber>: GeogebraLoader.isMainEuclidianView.
  • Regex de argumentos: GeogebraCommandParser.PATTERN_POINT y PATTERN_COMMAND, anclados con matches() y probando comando antes que punto. Vector[...] se desenvuelve. Un argumento que no se entiende devuelve null y el objeto no se importa, en vez de convertirse en un punto en (0,0) sin aviso.
  • Fallback estático: GeogebraCommandParser.processStaticFallback. Ver 9.1.
  • Esquinas de imagen: GeogebraCommandParser.imageCorners. Ver 9.2.
  • Tamaño de fuente: GeogebraCommandParser.latexScale. Ver 9.3.
  • Opacidad de trazo y pointStyle: GeogebraCommandParser.parseStylingOptions y parsePointStyle. Ver 9.4.
  • Grosores relativos al tamaño de la vista: GeogebraCommandParser.scaledThickness y lineThickness. Ver 9.5.
  • Traductor de fórmulas: GeogebraExpressionParser, que hace vivos los numéricos, los puntos y las funciones definidos por una fórmula. Ver 9.7.
  • Parabola y Hyperbola: GeogebraCommandParser.processParabola (foco y directriz) y processHyperbola (dos focos y un punto), más los fallbacks estáticos staticParabola y staticHyperbola. Los objetos son CTParabola y CTHyperbola, que recortan lo que dibujan a la vista de la cámara igual que Line hace con sus puntos de borde. Con esto ya entran las cuatro cónicas.
  • Objetos derivados de una cónica (Center, Focus, Vertex, Directrix, Axes, FirstAxis, SecondAxis, Asymptote) y Tangent e Intersect contra cualquier cónica. Ver 9.8.

Cubierto por GeogebraLoaderImportTest, GeogebraStaticFallbackTest y GeogebraStyleTest. Lo de 9.2 y 9.3 no tiene tests: construir un JMImage exige el renderer de JavaFX, que los tests no tienen (JMImage.<init> castea el renderer a JavaFXRenderer), y el tamaño de un CTLatex tampoco es observable con DummyRenderer.

9.1 El fallback estático

Cuando un objeto no se ha podido construir desde su <command> —porque el comando no está implementado, o porque es un objeto independiente de un tipo que el parser no monta por su cuenta— GeogebraLoader.parseGeogebraElement llama a processStaticFallback, que lo reconstruye a partir de la geometría que GeoGebra ya dejó evaluada en el propio <element> (sección 5).

El objeto resultante se construye sobre Vec anónimos: está en el sitio correcto pero no sigue a sus padres. Si la animación mueve el objeto del que dependía en GeoGebra, este se queda quieto.

type Reconstruido como A partir de
angle Scalar <value val> (radianes)
line CTLine por dos puntos <coords> → punto más cercano al origen y dirección (-y, x)
vector CTVector <coords> + <startPoint>
conic (elipse) CTEllipse <matrix> → centro, autovalores y autovector del eje mayor
conic (circunferencia) CTCircle Ídem, cuando los focos coinciden
conic (parábola) CTParabola <matrix> → autovector del autovalor nulo, foco y directriz
conic (hipérbola) CTHyperbola <matrix> → centro, autovalores y autovector del eje transverso

point y numeric ya se construían desde su <element> antes de esto.

No se reconstruyen: segment, ray, polygon, polyline ni conicpart (no guardan los datos, ver sección 5). Se avisa y se saltan; nunca se inventa una geometría aproximada. Las cuatro cónicas no degeneradas sí se reconstruyen.

Detalles de implementación que importan:

  • El tipo de cónica se decide por el signo de A0·A1 - A3²: > 0 elipse, = 0 parábola, < 0 hipérbola. El cero se compara relativo al cuadrado del tamaño de los coeficientes (1e-9), porque una parábola lo tiene exactamente nulo y lo que queda es ruido de redondeo; una elipse necesitaría ejes que difirieran en un factor 30000 para colarse.
  • La parábola no tiene centro, así que no sirve el camino de la elipse. El eje es el autovector del autovalor nulo (se prueban (A3, -A0) y (A1, -A3) y se usa el más largo). En ese marco la ecuación queda L·Y² + 2·Au·X + 2·Av·Y + A2 = 0 con L = A0+A1 el otro autovalor, de donde salen el parámetro focal -Au/L, el vértice completando cuadrados, y el foco y la directriz a medio parámetro focal a cada lado. La apertura va hacia +u cuando L·Au < 0.
  • El centro sale de anular las dos derivadas parciales; el valor de la forma cuadrática en el centro es A4·cx + A5·cy + A2 y, en la elipse, tiene que ser negativo para que la cónica tenga puntos reales. En la hipérbola vale cualquier signo distinto de cero; si es cero, la cónica degenera en dos rectas que se cortan.
  • Los semiejes son sqrt(-f(c)/λ) con λ los autovalores de la parte cuadrática; el menor corresponde al eje mayor. En la hipérbola los dos autovalores tienen signos opuestos, así que el eje transverso (el que corta a la curva) es el del autovalor que hace positivo ese cociente, y el semieje conjugado sale de sqrt(f(c)/λ) con el otro.
  • El autovector se calcula, no se lee de <eigenvectors>, para no depender del convenio de ordenación de GeoGebra. Se prueban las dos expresiones equivalentes (A3, λ-A0) y (λ-A1, A3) y se usa la más larga, porque cada una degenera en un caso distinto.
  • El umbral para decidir que una elipse es una circunferencia es 1e-6 relativo al semieje mayor, no algo del orden del épsilon de máquina: una circunferencia cuyos coeficientes arrastran ruido de redondeo de orden 1e-16 produce focos separados unos 1e-8.
  • El aviso por comando no soportado es de nivel debug; lo que se registra como warn es el resultado por objeto y un resumen al final del archivo. getStaticallyImportedLabels() devuelve las etiquetas afectadas.

9.2 Esquinas de imagen

GeogebraCommandParser.imageCorners reproduce el algoritmo de la sección 6.3. Devuelve las dos esquinas que necesita CTImage (inferior izquierda e inferior derecha) o null si no puede determinarlas. Cubre los cuatro casos: dos esquinas declaradas, ancla + esquina superior izquierda, solo ancla, y centrado.

  • Las esquinas se emparejan por su atributo number, no por su orden en el documento.
  • Se aceptan tanto exp (o label, en archivos antiguos) como coordenadas absolutas.
  • Una esquina que nombra un punto se devuelve como el punto importado, no como una copia de sus coordenadas, para que la imagen lo siga.
  • Cuando una esquina hay que calcularla y el ancla es un punto vivo, el resultado es un CTTranslatedPoint sobre el ancla, no un Vec fijo. Así una imagen anclada a un punto viaja con él en lugar de deformarse al moverlo.
  • El tamaño en píxeles se lee de la cabecera del archivo con ImageIO, sin decodificar la imagen entera. Un formato que la plataforma no sabe medir (SVG) devuelve null: entonces solo se puede importar si el archivo declara las dos esquinas inferiores.
  • La escala de la vista la aporta GeogebraLoader.applyViewScale, que corre antes de la construcción. La forma zoomable de <coordSystem> la da directa; la forma fijada la deriva de <size> y los límites, y se queda sin ella si esos límites son etiquetas.

Aparte: construir el JMImage va ahora dentro de un catch (RuntimeException). Sin él, un formato que el renderer no soporta lanzaba una excepción que escapaba de parse() y tiraba todo el import, no solo la imagen.

9.3 Tamaño de fuente

GeogebraCommandParser.latexScale sustituye la lectura del atributo size:

escala = BASE_LATEX_SCALE · sizeM · (tamaño de fuente del <gui>) / 16
  • sizeM es el multiplicador real. Si el <font> no existe, es 1: el tag solo se escribe cuando algo en él no es el valor por defecto.
  • Si hay size pero no sizeM (archivos anteriores a que existiera), se reconstruye el multiplicador como (guiFontSize + size) / guiFontSize, que es lo que size significa.
  • BASE_LATEX_SCALE = 5/36 es empírico y se conserva del código anterior, así que un archivo con los valores por defecto se importa exactamente igual que antes. Lo que cambia es que ahora el tamaño es proporcional al de GeoGebra; antes era proporcional a la diferencia contra el tamaño de la interfaz, de modo que sizeM=1.5 daba 0.222 y sizeM=2 daba 0.444, mientras que sizeM=1 caía en una rama especial con 0.139.

La escala resultante pasa por scaledToViewWidth, la misma de 9.5: GeoGebra mide el texto en puntos de su fuente de interfaz, así que el mismo texto ocupa más parte de una vista estrecha que de una ancha, exactamente igual que un trazo. Un archivo guardado en una vista de 1000 px se importa igual que antes.

Aviso, y es gordo: ctLatex.scale(...) no hace nada. Un Constructible es rígido, como dice el manual, y ese .scale(size) llevaba ahí desde siempre calculando un tamaño que luego se tiraba. Hay que escalar el objeto interno, getMathObject().scale(...), que va por la modelMatrix y por tanto sobrevive a la recompilación que hace changeInnerLaTeX.

9.4 Las dos opacidades y el estilo de punto

Todo dentro de parseStylingOptions.

Opacidad. GeoGebra guarda las dos por separado y el importador ya las lee así:

Qué Dónde Rango Ausente significa
Relleno <objColor alpha> 0..1 opaco
Trazo <lineStyle opacity> 0..255 opaco

El atributo opacity solo se escribe cuando el trazo no es opaco (GeoElement.getLineStyleXML, kernel/geos/GeoElement.java:4731), así que su ausencia se lee como 255. Pero opaco no es el caso normal: polígonos, cónicas, funciones y polilíneas llevan opacity="178" por defecto, o sea que casi todos los trazos de GeoGebra van al 70%. Antes de esto el importador los ponía opacos.

Como el tag <lineStyle> se lee ahora antes que <objColor>, la opacidad entra directamente en el alfa del color de trazo, y el de relleno conserva el suyo.

pointStyle. parsePointStyle mapea los once códigos de plugin/EuclidianStyleConstants.java:173-184:

GeoGebra JMathAnim GeoGebra JMathAnim
0 DOT CIRCLE 6 TRIANGLE_NORTH TRIANGLE_UP_FILLED
1 CROSS CROSS 7 TRIANGLE_SOUTH TRIANGLE_DOWN_FILLED
2 CIRCLE (hueco) RING 8 TRIANGLE_EAST TRIANGLE_UP_FILLED
3 PLUS PLUS 9 TRIANGLE_WEST TRIANGLE_UP_FILLED
4 FILLED_DIAMOND CIRCLE 10 NO_OUTLINE CIRCLE
5 EMPTY_DIAMOND RING

Tres no tienen equivalente y caen a la forma más parecida: los dos rombos y los triángulos laterales. El resto es exacto. Lo importante del cambio es el 2: antes iba a CIRCLE, un punto relleno, cuando en GeoGebra es un círculo hueco.

El valor -1 significa «usa el default de este tipo de objeto», y GeoGebra directamente no escribe el tag en ese caso (XMLBuilder.appendPointProperties, kernel/geos/XMLBuilder.java:359). El importador no fuerza nada entonces; los dos defaults coinciden de todas formas, punto relleno en ambos lados.

El mapeo vivía duplicado en processPoint y ahora está solo en parseStylingOptions, que corre para todos los elementos justo después. <pointSize>, en cambio, sí se escribe siempre, así que no hay riesgo en haber quitado el valor por defecto que processPoint aplicaba.

9.5 Grosores relativos al tamaño de la vista

El problema no era la calibración absoluta sino que thickness · 2 y pointSize · 6 ignoran el tamaño de la vista. Como GeoGebra mide en píxeles y JMathAnim en fracción de imagen, esos factores constantes equivalían a asumir que todo archivo se guardó en una vista de un ancho concreto. scaledThickness corrige eso:

thickness = valorGgb · factor · REFERENCE_VIEW_WIDTH / anchoVistaGgbPx

con REFERENCE_VIEW_WIDTH = 1000 y los dos factores de siempre. Consecuencias:

  • Un archivo guardado en una vista de 1000 px se importa exactamente igual que antes. El cambio es una generalización, no un recalibrado.
  • Los otros dejan de depender del ancho de la ventana con que se guardaron.
  • Si falta <size>, se usa la anchura de referencia, o sea el comportamiento anterior.

Para las líneas ese factor 2 no es solo empírico: sale de la física de 6.6. Con píxeles = thickness/2 y thickness_jma = (píxeles / ancho) · 4000, una vista de 1000 px da thickness_jma = 2 · thickness, que es justo SCALING_FACTOR_THICKNESS_LINE.

Para los puntos no cuadra igual: la misma cuenta con diámetro 2 · pointSize daría un factor 8, no 6, en la vista de referencia. Se ha dejado en 6 para no cambiar el aspecto de lo que ya funciona; si alguna vez se comparan lado a lado un archivo y su import, ahí está el candidato a ajustar.

lineThickness aplica además el multiplicador xScale / 50 cuando la vista declara lineThicknessScaled, que es lo que hace la app Notes.

9.6 CircumcircleArc, y por qué necesitó una clase nueva

CircumcircleArc(A, B, C) es el arco de la circunferencia que pasa por los tres, de A a C pasando por B. Lo obvio es componerlo con lo que ya hay:

CTCircle circumcircle = CTCircle.make3Points(A, B, C);
CTCircleArc.make(circumcircle.getCircleCenter(), A, C);   // NO funciona

No funciona, y el test lo demuestra: esa CTCircle no está en la escena y nadie depende de ella, así que nunca vuelve a ejecutar computeCircleCenterRadius(). El arco lee un centro congelado en el del primer fotograma y se queda quieto al mover los puntos.

Hay además un segundo problema, este de geometría: CTCircleArc barre siempre en sentido antihorario desde su punto inicial, o sea que empezar en A o en C depende de cómo esté orientado el triángulo. GeoGebra recalcula ese signo en cada compute():

det = (Bx - Ax)*(Cy - Ay) - (By - Ay)*(Cx - Ax);
setParameters(alpha, beta, det > 0);   // AlgoConicPartCircumcircleND.computeCircle

Decidirlo una sola vez al importar deja el arco saltando al complementario en cuanto una animación cruza B al otro lado de la recta AC.

Por eso hay una clase, CTCircumcircleArc, que depende de A, B y C —no de un centro derivado— y en cada rebuildShape() recalcula centro, radio y orientación. Usa una CTCircle interna solo para la fórmula del circuncentro, invocándola ella misma en vez de confiar en el orden del grafo.

Es la primera vez en todo esto que hizo falta tocar la librería y no solo el importador.

Después se añadió también CTCircumcircleSector, con la misma estructura y el trozo de CTCircleSector que cierra el arco sobre el centro. Las dos clases están expuestas en el DSL como segunda forma de ctCircleArc y ctCircleSector:

ctCircleArc(A: P, B: Q, C: R)
ctCircleSector(A: P, B: Q, C: R)

O sea que el comando CircumcircleSector del importador ya solo necesita su case en parseGeogebraCommand, igual que processCircumcircleArc.

9.7 El traductor de fórmulas

GeogebraExpressionParser traduce lo que hay en los <expression> a mXparser y lo ata a los objetos de la construcción que nombra. Es lo que hace que un deslizador de GeoGebra mueva algo al importarlo.

Dos propiedades de xmlTemplate (3.5) lo hacen viable: la multiplicación siempre va escrita con *, así que no hay producto implícito que adivinar, y los nombres de función son los internos en inglés.

Las etiquetas se renombran a ggb0, ggb1…, obligatoriamente. Comprobado contra mXparser 6.1: un argumento llamado pi, o con una letra griega, hace que la expresión entera evalúe a NaN sin ningún error. Como las etiquetas de GeoGebra son a menudo griegas, usarlas tal cual daría números silenciosamente equivocados.

Qué se traduce:

Aritmética + - * / ^, paréntesis, notación científica
Funciones trigonométricas e hiperbólicas y sus inversas, abs sgn sqrt cbrt exp floor ceil min max gamma
Logaritmos ln, lg→log10, ld→log2, log(b,x), y log(x) de una sola rama = natural, que solo escriben los archivos anteriores a GeoGebra 5
Ajustes de aridad round(x)→round(x,0) y nroot(x,n)→root(n,x), que mXparser escribe al revés
Constantes pi, π, ℯ, y el sufijo °
Referencias cualquier escalar de la construcción, y x(A) / y(A)

Qué no, y se rechaza en vez de adivinarse: listas, condicionales, intervalos, complejos, derivadas, funciones definidas por el usuario y lo que solo entiende el CAS. Un número equivocado es mucho peor que un objeto que falta, así que ante la duda no se traduce y se cae al valor que GeoGebra ya había calculado.

Una función trae su propio lado izquierdo. Lo que hay en el tag no es el cuerpo sino la definición entera:

<expression label="f" exp="f(x) = (cos(x))^(3) + (sin(x))^(2)" type="function"/>

Sale de AlgoDependentFunction.toExpString, que devuelve getLabel() + "(" + getVarString() + ") = " + cuerpo. Hay que quitar ese prefijo antes de traducir nada, o el propio nombre de la función parece una llamada a una función desconocida. De paso da el nombre real de la variable, que no siempre es x.

Con qué se construye cada cosa:

  • Numéricos y ángulos → Scalar.makeFrom(supplier, fuentes), o sea vivos.
  • Puntos → CTExpressionPoint (clase nueva), traduciendo la fórmula dos veces, una por coordenada: un punto de la fórmula aporta solo esa coordenada y un literal (3, 4) colapsa al número que toca, de modo que A + (3, 4) da x(A) + 3 y y(A) + 4.
  • Funciones → CTFunctionGraph. Solo admite un escalar como parámetro, así que si la fórmula lee varios se combinan en un escalar detector de cambios, con pesos irracionales distintos para que ninguna combinación pase desapercibida. La lambda ignora el valor que recibe y lee cada escalar por su cuenta.

Los textos. Un texto de GeoGebra es una cadena de concatenaciones, "Área = " + a + " cm", y su + es el mismo operador vaya lo que vaya a los lados, aplicado de izquierda a derecha. O sea que cortar por los + de primer nivel y mostrar cada trozo es exactamente lo que muestra GeoGebra ("x" + 2 + 3 se lee x23). Los trozos entrecomillados son literales; los demás se traducen y pasan a ser los marcadores {#0}…{#9} que AbstractLatexMathObject ya sabía sustituir.

Los decimales salen de <kernel><decimals val="2"/>, y el valor escrito en cada marcador va redondeado a esos decimales: recompilar LaTeX es caro y Scalar.setValue solo propaga si el valor cambia, así que redondear es lo que evita recompilar en cada fotograma mientras los dígitos en pantalla son los mismos.

Una fórmula que no lee nada no se deriva. <expression exp="5"/> deja un Scalar normal y modificable, no uno de solo lectura: un deslizador libre puede estar guardado así, y es justo lo primero que uno quiere animar.

Aviso de licencia: mXparser 6 imprime por consola un aviso pidiendo confirmar el tipo de uso (License.iConfirmNonCommercialUse(...)). Calcula bien sin confirmarlo. Es una decisión del dueño del proyecto, así que el importador no la toma.

9.8 La ecuación implícita, y todo lo que cuelga de ella

Las cuatro cónicas implementan HasConicEquation, que devuelve los coeficientes de A0·x² + A1·y² + A2 + 2·A3·xy + 2·A4·x + 2·A5·y = 0: la misma forma y el mismo orden que el tag <matrix>, aunque solo hasta un factor común, así que nunca se comparan uno a uno contra los del fichero. Cada cónica sabe describirse a su manera (centro y semiejes, o vértice y parámetro focal) y HasConicEquation.coefficientsOf desarrolla eso alrededor del origen.

Con esa única función salen las dos operaciones que antes había que escribir por tipo:

  • Recta ∩ cónica: sustituir la recta en la ecuación deja una cuadrática en el parámetro de la recta. Es HasConicEquation.intersectWithLine, y CTIntersectionPoint la usa para su tipo LINE_CONIC. Vale igual para elipse, parábola e hipérbola; la circunferencia conserva su camino antiguo. El parámetro que devuelve es el mismo lambda con el que validateSolutionForLines recorta segmentos y semirrectas, así que eso sigue funcionando.
  • Tangente desde un punto: la polar del punto, que es la matriz de la cónica aplicada a él. Si el punto está en la curva la polar es la tangente; si está fuera, es la cuerda de contacto, y las dos tangentes van del punto a donde la polar corta a la cónica. Un solo cálculo cubre los dos casos que GeoGebra distingue. Es CTTangentPointConic.

Dos tolerancias relativas, y no son opcionales. Ambos casos interesantes caen justo encima de un cero:

  • El término cuadrático de la sustitución se anula exactamente cuando la recta es paralela a una asíntota o al eje de una parábola. Como los coeficientes de la cónica vienen a su vez de un cálculo, ahí queda un residuo de orden 1e-14 y la fórmula devuelve un punto en 1e15 en vez de decir que solo hay una intersección.
  • El discriminante se anula exactamente cuando la recta es tangente. O sea que el caso que CTTangentPointConic construye siempre es justo el que el redondeo puede tirar al lado negativo, y sin la tolerancia la tangente sale NaN la mitad de las veces.

Ambas usan HasConicEquation.EPSILON_RELATIVE (1e-12) contra el tamaño de los propios términos, no contra un absoluto.

Objetos derivados. Casi ninguno necesitó clase nueva, porque salen de lo que la cónica ya tiene: el centro es el punto medio de los focos (CTMidPoint), el eje que los lleva es la recta que pasa por ellos (CTLine), el otro eje es su perpendicular por el centro (CTLineOrthogonal), el eje de una parábola es la perpendicular a la directriz por el foco, y la directriz es la propia recta con la que se construyó. Los vértices y los focos son CTLinkedPoint sobre coordenadas que la cónica mantiene vivas.

Las dos excepciones: CTHyperbolaAsymptote, porque su pendiente es el cociente de los semiejes y no se puede escribir con los focos, y CTEllipseVertexPoint, que ya existía.

Numeración. Focus y Vertex sobre una hipérbola siguen el mismo convenio que la elipse: se lee <eigenvectors> y se comparan con la dirección que el objeto toma como positiva (firstAxisRunsForward), que es del centro hacia el segundo foco. Las asíntotas no se numeran así: el fichero no dice cuál es cuál, y una construcción que nombre las dos puede recibirlas al revés.

Lo que sigue sin hacerse: cónica ∩ cónica. Son hasta cuatro puntos, o sea una cuártica, y la sustitución de arriba no sirve. CTIntersectionPoint lo detecta y avisa (CONIC_CONIC). El camino clásico es el haz C1 + λ·C2, buscar la λ que lo degenera y partir esa cónica degenerada en dos rectas, pero eso pide aritmética compleja para el caso de rectas imaginarias.


Pendiente, por orden de rendimiento

  1. Leer geogebra_defaults2d.xml (3.4) para que la ausencia de un tag signifique algo.
  2. Orden de dibujo por prioridad de tipo, no solo por layer (sección 4).
  3. Intersect con punto como tercer argumento (6.5).
  4. Máscara ev de <show> (sección 7): hoy se importa como visible un objeto que solo se ve en Graphics 2.
  5. Salidas vacías (3.3): saltarlas en getArgumentLabels en vez de avisar de un objeto sin nombre.
  6. Macros: detectar geogebra_macro.xml para dar un mensaje útil.
  7. Cónica ∩ cónica (9.8): hasta cuatro puntos, una cuártica. Es la única parte de Intersect que queda fuera.
  8. Comandos que faltan (sección 8). Son ahora los que más aportan: al no poder reconstruirse desde el <element>, el fallback no los cubre. Por orden de uso probable: Dilate, Diameter, Eccentricity, Parameter, Polar, y Arc/Sector, que no son el caso general de CircleArc/CircleSector sino un recorte de una cónica que ya existe, entre dos parámetros o entre dos de sus puntos (CmdArcSector frente a CmdCircleArcSector); sobre una elipse harían falta más que CTCircleArc.

10. Archivo de ejemplo mínimo

Extracto real de circles.ggb (GeoGebra 5.0.478), útil como referencia rápida:

<euclidianView>
    <viewNumber viewNo="1"/>
    <size  width="568" height="423"/>
    <coordSystem xZero="205.0" yZero="343.0" scale="50.0" yscale="50.0"/>
    <evSettings axes="false" grid="false" pointCapturing="3" gridType="3"/>
</euclidianView>
<construction title="" author="" date="">
<element type="point" label="A">
    <show object="true" label="true"/>
    <objColor r="77" g="77" b="255" alpha="0.0"/>
    <layer val="0"/>
    <labelMode val="0"/>
    <animation step="1" speed="1" type="1" playing="false"/>
    <coords x="1.54" y="1.78" z="1.0"/>
    <pointSize val="5"/>
    <pointStyle val="0"/>
</element>
<command name="Circle">
    <input a0="A" a1="B"/>
    <output a0="c"/>
</command>
<element type="conic" label="c">
    <show object="true" label="true"/>
    <objColor r="0" g="0" b="0" alpha="0.0"/>
    <layer val="0"/>
    <labelMode val="0"/>
    <lineStyle thickness="5" type="0" typeHidden="1" opacity="178"/>
    <eigenvectors  x0="1.0" y0="0.0" z0="1.0" x1="-0.0" y1="1.0" z1="1.0"/>
    <matrix A0="1.0" A1="1.0" A2="0.3352" A3="0.0" A4="-1.54" A5="-1.78"/>
    <eqnStyle style="specific"/>
</element>
<command name="Intersect">
    <input a0="c" a1="d" a2="2"/>
    <output a0="C"/>
</command>
</construction>