Skip to content

Serialización

Oroya Animate incluye un sistema de serialización JSON que permite guardar y cargar definiciones de escena. Es la base para editores visuales, colaboración y versionado de escenas.


Tabla de contenidos


Serializar una escena

La función serialize convierte todo el scene graph a un string JSON formateado:

import { Scene, Node, createBox, Material, serialize } from '@joroya/core';

const scene = new Scene();

const cube = new Node('hero-cube');
cube.addComponent(createBox(2, 2, 2));
cube.addComponent(new Material({ color: { r: 1, g: 0.5, b: 0 } }));
cube.transform.position = { x: 3, y: 0, z: -1 };
scene.add(cube);

const json = serialize(scene);
console.log(json);

Flujo de serialización

flowchart LR
    S["Scene"] -->|"serialize()"| SR["serializeNode(root)"]
    SR -->|"recursivo"| SN["Para cada nodo:\nid, name, components, children"]
    SN -->|"spread"| SC["Cada component:\n{ type, ...data }"]
    SC --> JSON["JSON.stringify()\ncon indent 2"]
    JSON --> STR["string"]

Deserializar una escena

La función deserialize reconstruye una Scene funcional desde un string JSON:

import { deserialize } from '@joroya/core';

const restoredScene = deserialize(json);

const found = restoredScene.findNodeByName('hero-cube');
console.log(found?.transform.position); // { x: 3, y: 0, z: -1 }

La escena restaurada es completamente funcional — puede montarse en cualquier renderer:

import { ThreeRenderer } from '@joroya/renderer-three';

const renderer = new ThreeRenderer({ canvas, width, height });
renderer.mount(restoredScene);
renderer.render();

Flujo de deserialización

flowchart LR
    STR["string JSON"] -->|"JSON.parse()"| OBJ["SerializableScene"]
    OBJ -->|"deserializeNode()"| RN["Reconstruir nodos\nrecursivamente"]
    RN -->|"switch(type)"| COMP["Recrear componentes:\nTransform, Geometry, Material,\nCamera, Animation"]
    COMP --> SCENE["new Scene()"]
    RN -->|"children"| SCENE

Formato del JSON

Estructura general

graph TD
    SS["SerializableScene"] -->|"root"| SN["SerializableNode"]
    SN -->|"id"| ID["string (UUID)"]
    SN -->|"name"| NAME["string"]
    SN -->|"components"| COMPS["SerializableComponent[]"]
    SN -->|"children"| CHILDREN["SerializableNode[]"]
    CHILDREN -->|"recursivo"| SN
    COMPS --> SC["{ type: ComponentType, ...data }"]

Ejemplo completo de output

{
  "root": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "root",
    "components": [
      {
        "type": "Transform",
        "position": { "x": 0, "y": 0, "z": 0 },
        "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
        "scale": { "x": 1, "y": 1, "z": 1 },
        "localMatrix": [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1],
        "worldMatrix": [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1],
        "isDirty": true
      }
    ],
    "children": [
      {
        "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
        "name": "hero-cube",
        "components": [
          {
            "type": "Transform",
            "position": { "x": 3, "y": 0, "z": -1 },
            "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
            "scale": { "x": 1, "y": 1, "z": 1 },
            "localMatrix": [1,0,0,0, 0,1,0,0, 0,0,1,0, 3,0,-1,1],
            "worldMatrix": [1,0,0,0, 0,1,0,0, 0,0,1,0, 3,0,-1,1],
            "isDirty": false
          },
          {
            "type": "Geometry",
            "definition": {
              "type": "Box",
              "width": 2,
              "height": 2,
              "depth": 2
            }
          },
          {
            "type": "Material",
            "definition": {
              "color": { "r": 1, "g": 0.5, "b": 0 }
            }
          }
        ],
        "children": []
      }
    ]
  }
}

Interfaces internas

InterfaceCamposDescripción
SerializableSceneroot: SerializableNodeContenedor de nivel superior
SerializableNodeid, name, cssClass?, cssId?, components[], children[]Representación plana de un nodo
SerializableComponenttype: ComponentType, + ...dataCada componente con su tipo y datos

Componentes soportados

La serialización conserva solo los datos declarativos soportados. Se excluyen campos del runtime, callbacks, referencias a nodos y objetos del backend, también en datos de eventos. Un Animator enlazado se puede guardar con seguridad.

ComponenteDeserialización
TransformRestaura los campos en un Transform nuevo
Geometry, Material, Camera, Interactive, Light, EnvironmentReconstruye desde definition
RigidBody, Collider, PostProcessing, ParticleSystemReconstruye definiciones; el runtime del backend se crea por separado
Animator, AudioListener, AudioSource, SkinReconstruye definiciones; no guarda una sesión de reproducción ni recursos cargados
AnimationReconstruye desde animations
InstancedMesh, Script, tipos desconocidosSe omiten; necesitan adaptadores de la aplicación

Typed arrays

Float32Array, Uint8Array, Uint16Array y Uint32Array se codifican como { __typedArray, base64 } y recuperan su tipo original. Esto permite guardar tracks de animación y geometrías buffer, pero no restaura el componente InstancedMesh. Otras clases de typed array no están soportadas por esta codificación.

import { typedArrayReplacer, typedArrayReviver } from '@joroya/core';

const encoded = JSON.stringify({ values: new Float32Array([0, 1, 2]) }, typedArrayReplacer);
const decoded = JSON.parse(encoded, typedArrayReviver);
console.log(decoded.values instanceof Float32Array); // true

Datos conservados

Se conservan IDs, nombres, relaciones padre-hijo, transforms, cssClass, cssId y definiciones soportadas de todos los nodos, incluida la raíz. addComponent() reconstruye component.node. Las matrices de mundo se recalculan al renderizar.

La raíz guardada conserva ID, nombre, metadatos CSS, transform y componentes soportados. Scene(root?: Node) acepta una raíz sin padre y mantiene el constructor sin argumentos. El JSON antiguo sin Transform recibe uno por defecto.


Casos de uso

Persistencia en localStorage

function saveScene(scene: Scene): void {
  const json = serialize(scene);
  localStorage.setItem('oroya-scene', json);
}

function loadScene(): Scene | null {
  const json = localStorage.getItem('oroya-scene');
  if (!json) return null;
  return deserialize(json);
}

Exportar como archivo descargable

function downloadScene(scene: Scene, filename: string): void {
  const json = serialize(scene);
  const blob = new Blob([json], { type: 'application/json' });
  const url = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();

  URL.revokeObjectURL(url);
}

downloadScene(scene, 'my-scene.json');

Importar desde archivo

async function importScene(file: File): Promise<Scene> {
  const text = await file.text();
  return deserialize(text);
}

// Con input[type=file]
input.addEventListener('change', async (e) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (file) {
    const scene = await importScene(file);
    renderer.mount(scene);
  }
});

Server-side rendering

// Recibir la escena serializada, renderizar a SVG en el servidor
import { deserialize } from '@joroya/core';
import { renderToSVG } from '@joroya/renderer-svg';

function handleRequest(jsonBody: string): string {
  const scene = deserialize(jsonBody);
  return renderToSVG(scene, { width: 800, height: 600, dt: 0 });
}

Versionado en Git

Guardar escenas como .json permite:

scenes/
├── level-01.json    → Versionado con git
├── level-02.json
└── hub-world.json

Los diffs de Git muestran exactamente qué cambió:

 "name": "hero-cube",
 "components": [
   {
     "type": "Transform",
-    "position": { "x": 3, "y": 0, "z": -1 },
+    "position": { "x": 5, "y": 2, "z": -1 },

Limitaciones

LimitaciónImpacto y alternativa
Callbacks del runtimeonUpdate y los listeners no se guardan; regístralos de nuevo después de cargar
Instancing y componentes personalizadosEl reviver omite InstancedMesh, Script y tipos desconocidos; usa un adaptador
IDs conservados en toda la escenaImportar dos veces puede duplicarlos; crea nodos nuevos para copias independientes (id es readonly)
Sin validación de esquemaJSON o estructuras inválidas pueden lanzar errores; valida las entradas en la aplicación
Recursos externosEl JSON guarda URIs y definiciones, no archivos de texturas, audio, modelos ni el estado físico en ejecución

Los adaptadores internos guardan transforms, definiciones y animaciones SVG. Se puede serializar un Animator enlazado, reproduciendo o pausado; se excluyen mixers, callbacks, referencias a nodos y recursos del backend. Se restauran las definiciones de clips, sin garantizar el instante exacto de reproducción.

1.2

Capabilities, diagnostics and acceptance describe the current rendering and persistence contracts.