Scratch3.0二次开发

Posted by Kaka Blog on November 12, 2019

项目结构

scratch3.0把相关环境分成了三个部分,vm、blocks和gui; vm管理项目配置和逻辑、gui管理页面渲染、blocks是管理积木模块内容。

项目启动

scratch-vm:

npm link
npm run watch

scratch-blocks:

npm link
npm run prepublish

新开一个shell,进入scratch-gui目录:

npm link scratch-vm scratch-blocks
npm start

自定义扩展积木属性

逻辑梳理

scratch-vm自定义block,然后生成scratch-blocks所需的json内容;scratch-blocks拿到json后,会解析成一个对象,然后触发积木创建事件,将该对象进行处理生成xml格式;scratch-vm监听积木事件,会提取xml的信息转换成对象。

详细步骤

scratch-vm:

1、修改extensions文件夹里定义积木,这些自定义积木由extension-manager.js管理,需要加入自定义积木的文件;

{
    opcode: 'fourdigitdisplay_displayString',
    blockType: BlockType.COMMAND,
    text: formatMessage({
        id: '4displayText',
        default: '四位数码管 显示文本 “[VALUE]”',
        description: 'text.'
    }),
    arguments: {
        VALUE: {
            type: ArgumentType.STRING,
            defaultValue: formatMessage({
                id: 'bangbao.4displayText',
                default: '0000'
            })
        }
    },
    gen: {
        arduino: ''
    }
}

2、修改runtime.js的_convertBlockForScratchBlocks方法,先获取积木blockInfo的代码属性,增加到blockJSON里面。

_convertBlockForScratchBlocks (blockInfo, categoryInfo) {
    const extendedOpcode = `${categoryInfo.id}_${blockInfo.opcode}`;
    let code = '';
    if (blockInfo.arguments.CODE) {
        code = blockInfo.arguments.CODE[0];
    }
    let func = null;
    if (blockInfo.gen) {
        func = blockInfo.gen.arduino;
    }
    const blockJSON = {
        type: extendedOpcode,
        code: code,
        gen: func,
        inputsInline: true,
        category: categoryInfo.name,
        colour: categoryInfo.color1,
        colourSecondary: categoryInfo.color2,
        colourTertiary: categoryInfo.color3,
        extensions: ['scratch_extension']
    };

这里增加了codegen属性。

cratch-blocks:

1、 修改block.js数据模型,增加属性;

this.genCode = null;
···
Blockly.Block.prototype.setGenCode = function(genCode) {
  this.genCode = genCode;
}
···
Blockly.Block.prototype.jsonInit = function(json) {
  ···
  if (json['gen']) {
    this.setGenCode(json['gen']);
  }
};

2、根据block对象生成xml格式,修改xml.js文件,添加xml标签。

Blockly.Xml.blockToDom = function(block, opt_noId) {
  var element = goog.dom.createDom(block.isShadow() ? 'shadow' : 'block');
  element.setAttribute('type', block.type);
  if (!opt_noId) {
    element.setAttribute('id', block.id);
  }
  if (block.genCode) {
    element.setAttribute('code', block.genCode);
  }
  ···

scratch-vm:

1、 修改adapter.js,给block添加新属性,并用scratch-blocks里xml的标签属性赋值。

const domToBlock = function (blockDOM, blocks, isTopBlock, parent) {
    if (!blockDOM.attribs.id) {
        blockDOM.attribs.id = uid();
    }

    // Block skeleton.
    const block = {
        id: blockDOM.attribs.id, // Block ID
        opcode: blockDOM.attribs.type, // For execution, "event_whengreenflag".
        code: blockDOM.attribs.code,
        inputs: {}, // Inputs to this block and the blocks they point to.
        fields: {}, // Fields on this block and their values.
        next: null, // Next block in the stack, if one exists.
        topLevel: isTopBlock, // If this block starts a stack.
        parent: parent, // Parent block ID, if available.
        shadow: blockDOM.name === 'shadow', // If this represents a shadow/slot.
        x: blockDOM.attribs.x, // X position of script, if top-level.
        y: blockDOM.attribs.y // Y position of script, if top-level.
    };

代码生成框架

scratch-vm

1、在自定义扩展积木时定义代码生成函数:

img img

函数解释:第一个参数是Generator对象,里面定义了代码生成方法;第二个参数是block对象

2、将积木的代码生成方法放到积木里,修改runtime.js文件

_convertBlockForScratchBlocks (blockInfo, categoryInfo) {
    const extendedOpcode = `${categoryInfo.id}_${blockInfo.opcode}`;
    let code = '';
    if (blockInfo.arguments.CODE) {
        code = blockInfo.arguments.CODE[0];
    }
    let func = null;
    if (blockInfo.gen) {
        func = blockInfo.gen.arduino;
    }
    const blockJSON = {
        type: extendedOpcode,
        code: code,
        gen: func,
        inputsInline: true,
        category: categoryInfo.name,
        colour: categoryInfo.color1,
        colourSecondary: categoryInfo.color2,
        colourTertiary: categoryInfo.color3,
        extensions: ['scratch_extension']
    };

scratch-blocks

1、给积木原型加上代码生成方法属性,修改block.js文件的Blockly.Block.prototype.jsonInit方法,为了下一个步骤能取到代码生成函数变量。

this.genCode = null;
···
Blockly.Block.prototype.setGenCode = function(genCode) {
  this.genCode = genCode;
}
···
Blockly.Block.prototype.jsonInit = function(json) {
  ···
  if (json['gen']) {
    this.setGenCode(json['gen']);
  }
};

2、增加generator.js文件,该文件负责积木转换成代码。参考:https://github.com/BlocklyDuino/BlocklyDuino/blob/gh-pages/blockly/core/generator.js

  • 修改blockToCode方法:

img

func.call第一个参数表示调用的对象,第二个参数表示genCode方法的第一个参数,第三个参数表示genCode方法的第二个参数。

  • 修改init方法:http://www.superblockly.com/banbao/js/arduino.js

img

  • 修改finish方法:可以参考http://www.superblockly.com/banbao/js/arduino.js

img

scratch-gui

1、 调用workspaceToCode生成代码:

img

代码生成框架优化

上面的代码生成框架能够满足生成代码的功能,但是需要定义多一个属性,并将属性传递到scratch-blocks,对框架的改动较大。故可以对代码进行优化。

scratch-blocks

1、获取积木使用var blocks = workspace.getTopBlocks(true);

2、调用代码生成函数,关键文件是generator.js,这里使用this[block.type],this指的是generator

img

3、定义代码生成方法 增加BBRobot.js文件,该文件负责传感器代码的生成,目录结构如下: img

传感器代码生成如下: img

aduino文件夹可使用奥松编程的: 地址:http://www.superblockly.com/banbao/generators/arduino/ 这些是代码辅助作用,比如有些text类型的积木,直接返回文本就可以。 img

4、修改build.py文件,把新增的文件加入打包路径。 img

scratch-vm

主要修改/extensions里面文件夹的index.js文件,注意以下几个地方的规范: img

注意:getFieldValue的name必须和menu的值一致。

scratch-gui

调用代码生成:this.ScratchBlocks.Arduino.workspaceToCode(this.workspace);

最终效果

四位数码管代码生成: img

温度传感器代码生成: img

积木生成代码: img img

积木定义规范

原生积木

增加代码生成方法

以下步骤只需要在scratch-blocks项目操作。

  1. 方法名和积木type一致;
  2. valueToCode、getFieldValue、statementToCode第二个参数和积木定义的args的name一致。
  3. 返回值如果是代码段则直接返回拼接后的字符串;否则返回数组,第一个元素是拼接后的字符串,第二个参数参考generator.js以ORDER_前缀的属性

扩展积木

在scratch-vm项目增加积木定义

1、进入src/extensions对应积木文件夹,修改index.js文件,增加以下代码:

{
opcode: ‘积木ID',
blockType: 积木类型,参考block-type.js文件,
text: formatMessage({
      id: '文本ID',
      default: '显示的文本,[]表示变量',
      description: '文本描述'
}),
arguments: {
      变量名称: {
             type: 变量类型,参考argument-type.js文件,
             menu: 字符串,可选,定义下拉菜单,名称对应menus列表
             defaultValue: formatMessage({
                   id: '变量ID’,
                   default: 默认值
             })
       }
}
}

2、按照定义的opcode定义方法,起运行积木功能的作用。

opcode名称 (args, util) {

}

3、如果有定义menu,则需要定义下拉菜单方法,返回一个数组,元素是有text和value组成的元素。

_initTemperatureSensorTypeParam () {
        return [
            {
                text: formatMessage({
                    id: 'temperatureSensor.celsius',
                    default: temperatureSensorType.celsius.name
                }),
                value: temperatureSensorType.celsius.value
            },
            {
                text: formatMessage({
                    id: 'temperatureSensor.fahrenheit',
                    default: temperatureSensorType.fahrenheit.name
                }),
                value: temperatureSensorType.fahrenheit.value
            }
        ]
    }

在scratch-blocks项目增加代码生成方法

  1. 增加一个方法,方法名由插件ID_积木OPCODE组成。
  2. valueToCode、getFieldValue、statementToCode第二个参数和积木定义的arguments的变量名称一致,如果是下拉菜单,则和menu名称一致。
  3. definitions_定义include头文件、变量定义和方法定义。
  4. setups_定义setup函数里的语句。
  5. 返回值如果是完整代码段则直接返回,如果是可以作为输入参数则返回数组,第一个元素是拼接的字符串,第二个参数参考generator.js以ORDER_前缀的属性。

FAQ

1、如何监听积木区变化,一旦积木区发生变化,产生对应的代码?

解决:舞台每次变化都会调用onWorkspaceMetricsChange方法,在该方法里调用this.sb2cpp()即可。

2、自定义扩展积木定义时参考奥松编程的代码去定义,这样后面可以重用奥松编程的代码:http://www.superblockly.com/banbao/js/arduino.js

解决:积木生成的时候会生成type属性,该属性与category的id和积木的opcode组成,例如categoryId为BB,积木的opcode为ds18b20_sersor,则type为BB_ds18b20_sersor。在scratch_blocks里定义方法时必须保持一样.

3、Generator的valueToCode和getFieldValue没有效果?

解决:分析代码知道block.getFieldValue是调用getField,而getField是通过查找当前block的inputList数组,但是对于下拉菜单的数据是放在了childBlocks_数组,所以修改getField方法: img