-
Notifications
You must be signed in to change notification settings - Fork 29
Lenguaje y Bloques: Diseño viejo
El objetivo de esta página es documentar mínimamente el uso de Blockly en Pilas Bloques, hasta Septiembre de 2016.
Este diseño fue mutando porque inició como forma de tunear el lenguaje JavaScript que venía ya definido en Blockly.
Sin embargo éste diseño debería cambiar bastante, porque deberíamos agregar un nuevo lenguaje PilasBloquesProgramBuilder (o algo así) en lugar de estar sobreescribiendo características del lenguaje JavaScript ya implementado en Blockly.
Para poder decidir un buen diseño, hay que primero documentar el existente.
Al utilizar Blockly, es necesario pensar en estas partes:
- La definición visual de cada bloque.
- El toolbox (que define categorías y dónde van a estar los bloques creados)
- El lenguaje (que especifica, para cada bloque del toolbox, cómo se armará el texto final que representa lo que programamos)
El toolbox se define a través de un XML.
En actividad.js puede verse desde dónde se define el toolbox (nótese que durante el artículo usaré link estático a un commit específico, para que si se actualiza el archivo la desscripción actual no cambie)
Tiene la siguiente forma:
Blockly.inject(contenedor, {
// Varias opciones más....
toolbox: Blockly.Xml.textToDom(this.obtenerLenguaje()),
});
Ese this.obtenerLenguaje()
devuelve un string XML que representa la definición del toolbox usando la especificación de Blockly
Básicamente En Pilas Bloques hay tres tipos de objetos: Lenguaje, Bloque, Categoría.
Nos creamos nuestros propios objetos para poderles pedir a cada uno que escupa el XML
Objeto | Método | Qué hace |
---|---|---|
Lenguaje | build() | A partir de todos los bloques previamente cargados, devuelve el xml completo para el toolbox, incluyendo categorías y bloques. |
Categoria | generarXML(bloques) | A partir de muchos bloques, devuelve el xml correspondiente a la categoría, que luego irá en el toolbox (y tendrá todos esos bloques dentro) |
Bloque | build() | Devuelve el xml correspondiente a ese bloque |
Nota: Estaría bueno hacer un refactor que haga que todos esos métodos se llamen igual: xml(). Así se entiende que devuelven el xml, muy simple.
Nota: Otro refactor bueno sería cambiar el nombre de la clase "Lenguaje" por "Toolbox", ya que no es el lenguaje de posta. El lenguaje de posta debería ser el conjunto de métodos
block_javascript()
(ver abajo).
La razón por la que las categorías se obtienen a través de Lenguaje.ordenCategorias()
es para que el toolbox se cree con las categorías en ese orden. La razón por la que los bloques conocen a las categorías y no al revés, es para tener la facilidad de definir, en cada bloque, a qué categoría pertenece, y no tener que andar saltando a otra clase para hacerlo (se define en el mismo bloque).
- Para escupir el XML del toolbox es necesario "llenar" al lenguaje con las clases de bloques que se desea, y luego al mandarle el mensaje
build()
se obtiene el XML para el toolbox. - Cuando hacemos
Blockly.inject(...
Allí el mensajeActividad.obtenerLenguaje()
toma los bloques de la actividad, se los carga al lenguaje, y llama al métodoLenguaje.build()
. - Esto es independiente del lenguaje. Podríamos decir que, si quisiéramos generar el lenguaje "Gobstones", siempre vamos a necesitar hacer lo mismo: definir un toolbox, con categorías y un orden, y armar el xml.
Pilas Bloques utiliza la API Javascript de Blockly, en lugar del JSON. Esto permite usar la jerarquía de objetos Bloque de Pilas Bloques para hacer la creación más flexible. (En la doc oficial elegir la solapa "Javascript" para ver los ejemplos con esa API)
Todos los Bloques que heredan de la clase Bloque implementan el método block_init()
. Ese es el mensaje que se ejecuta cuando se desea construir visualmente un bloque:
El mensaje block_init()
se wrappea en Bloque.registrarVista()
como el método init()
que llama Blockly para crearlo.
En otras palabras, en block_init()
se escribe el código de la API Javascript de Blockly:
var EstructuraDeControl = Bloque.extend({
block_init(block) {
this._super(block);
block.setColour(Blockly.Blocks.loops.COLOUR);
block.setInputsInline(true);
block.setPreviousStatement(true);
block.setNextStatement(true);
}
});
- Documentación oficial de Blockly acá.
- Cada bloque registra una función en
Blockly.NombreDelLenguaje[idDelBloque]
que es la función que escupe el código a generar. (ver abajo, Registro de Bloques). - Pilas Bloques toma su clase Bloque (de Bloques.js) y lo que registra en Blockly es el método
block_javascript(block)
. - Entonces, el código generado se encuentra viendo los métodos
block_javascript(block)
en bloques.js . - En estos métodos hay mucha magia negra, y merecen refactor, que hay que hacer leyendo la documentación de Blockly. OJO. Acá es donde están resueltos los problemas de evaluación recursiva de expresiones, y de llamados a métodos, y definición de procedimientos... ¡Leer la doc oficial!
Para que todo lo anterior funcione, antes de inyectar el toolbox hay que registrar ambas partes de un bloque de Blockly: la parte viscual (en Blockly.Blocks
) y la parte del lenguaje (en Blockly.NombreDelLenguaje
).
Es por eso que cada llamada a Lenguaje.agregar_bloque(claseBloque)
(al construir el toolbox) lo que hace es registrar ambas cosas:
registrar_en_blockly() {
this.registrarVista();
this.registrarGeneracionJS();
},
registrarVista(){
var myThis = this;
Blockly.Blocks[this.get('id')] = {
init() {
myThis.block_init(this);
}
};
},
registrarGeneracionJS(){
var myThis = this;
Blockly.JavaScript[this.get('id')] = function(block) {
return myThis.block_javascript(block);
};
},
Además, es interesante ver que la clase CambioJSDeBlockly
está allí para sobreescribir el método registrar_en_blockly()
. La idea es que los bloques que hereden de CambioJSDeBlockly no registren su vista, ya que la vista es la que viene definida por defecto en Blockly acá.
var CambioDeJSDeBlocky = Bloque.extend({
registrar_en_blockly() {
// La vista ya está registrada originalmente por el lenguaje Javascript.
// Sólo registro la generación diferente de código
this.registrarGeneracionJS();
},
});
Por ejemplo, el bloque cuyo id es procedures_callnoreturn
, que es la llamada a un procedimiento, está definido visualmente por defecto en Blockly.Blocks, y el comportamiento está definido en nuestro bloques.js.
En conclusión, si se quiere definir un bloque para el que se mantiene la vista original por defecto de Blockly, hay que heredar de CambioDeJSDeBlocky
.
Nota: Para crear nuestro propio lenguaje Blockly.PilasBloquesProgramBuilder, una de las posibilidades sería cambiar este método
registrarGeneracionJS()
para que cargue las cosas en ese lenguaje en vez de en JavaScript. (Además habría que agregar varias otras cosas al lenguaje). Una primera aproximación podría ser:Blockly.PilasBloquesProgramBuilder = Blockly.JavaScript.clone()
y apartir de ahí pisar cosas. Ó hacer que herede 😄
El AccionBuilder
es un Builder que permite crear de manera uniforme y desde sólo ese punto de entrada y sin pensar en el objeto Bloque, los bloques más sencillos y diversos que aparecen en todas las actividades: las típicas "Primitivas" (con el mensaje build(...)
) y los "Sensores" (con el mensaje buildSensor()
)
Recibe un objeto con 5 atributos:
- descripcion es el texto que aparecerá en el bloque
- id es el que usa Blockly para registrar el bloque.
- icono es el ícono que irá junto al texto de la primitiva para reforzar su significado.
-
comportamiento es el comportamiento de ejerciciosPilas al que se asocia este bloque (parte del Code Generator para este bloque, esta info irá en el método
block_javascript()
) - argumentos es el argumento del comportamiento.
Ejemplo:
var AlimentarPez = AccionBuilder.build({
descripcion: 'Alimentar pez',
id: 'AlimentarPez',
icono: 'icono.pez.png',
comportamiento: 'RecogerPorEtiqueta',
argumentos: '{etiqueta: "PezAnimado", idTransicion: "alimentarPez"}',
});
Nota: Un refactor posible podría ser que AccionBuilder pase a llamarse PrimitiveBuilder.
También hay que volarle el código repetido.
- La clase
Bloque
, de la que heredan todos los bloques, tiene cuatro partes importantes que requieren ser definidos en las subclases: - El método
block_init()
, que define cómo se construye visualmente un bloque. Usa la API Javascript de Blockly para construir la parte visual. - El método
block_javascript()
, que define el String que escupirá Blockly al leer el workspace y pasar por ese bloque. Usa una API diferente que sirve para leer el estado actual del bloque ya construido. - El atributo
id
, que es el id del bloque que usará Blockly para referirse internamente a él. - El método
build()
, que escupe el xml para el toolbox y para el workspace, que Blockly usa para saber dónde crear el bloque. - El
AccionBuilder
es un Builder que permite crear de manera uniforme y desde sólo ese punto de entrada, olvidándose de las 4 partes anteriores, los bloques más sencillos y diversos que aparecen en todas las actividades: las típicas "Primitivas" (con el mensajebuild(...)
) y los "Sensores" (con el mensajebuildSensor()
)
Nota: Un refactor interesante sería hacer que el Bloque.id sea un método, para no tener que redefinir el método init() en todas las subclases de Bloque. ¡Usar un Template Method! Mucho más prolijo que el super() en todos lados.
- Es muy buena la guía que hay en la página de Blockly. La sección de Custom Blocks es altamente recomendable, y se lee rápido.