执行Blockly生成代码
路由
下一篇
历史回顾
Babylon.js部分
- Vue实现图形化积木式编程(一) ---- Babylon.js基础场景搭建
- Vue实现图形化积木式编程(二) ---- Babylon.js加载模型到场景中
- Vue实现图形化积木式编程(三) ---- Babylon.js点击拖拽移动模型
- Vue实现图形化积木式编程(四) ---- Babylon.js实现碰撞效果
- Vue实现图形化积木式编程(五) ---- Babylon.js自定义启动界面
- Vue实现图形化积木式编程(六) ---- Babylon.js相机控制与相机动画
- Vue实现图形化积木式编程(七) ---- babylonjs-gui 按钮实现
- Vue实现图形化积木式编程(八) ---- 将3d界面放入可拖动窗口中
Blockly部分
- Vue实现图形化积木式编程(九) ---- Blockly代码块编辑区域基本场景搭建
- Vue实现图形化积木式编程(十) ---- Blockly自定义块
- Vue实现图形化积木式编程(十一) ---- Blockly插件使用
前言
TIPS:该案例设计主要参考iRobot Coding,只用做学习用途,侵删。
最终实现效果
本文内容
- 在 Blockly代码块编辑区域基本场景搭建 这篇文章里笔者提到过在之后的文章中会对使用
eval方法执行代码
进行优化,对于通过Blockly代码块生成的代码字符串,本文介绍如何将代码字符串转化为执行语句来调用控制模型运动的方法。
本文主要内容是执行Blockly生成的代码字符串,会忽略通过babylonjs来控制模型对象运动的细节。实际上控制模型运动并不难,主要解决的是顺序执行当前生成的代码指令时,如move、arc等指令,需要设定在一定时间内模型的x、y、rotation等参数如何变化。
实现思路
如果想要将字符串转为可执行代码,可以使用eval()
或new Function([arg1,arg2,...], 代码字符串)
- 现在存在一个robot的实例对象,包含一些控制运动的方法
// robot.js
class Robot {
constructor() {
this.isRun = false
}
init() {
console.log("robot模块化程序初始化")
this.isRun = true
}
stop() {
console.log("robot模块运行结束")
this.isRun = false
}
checkStatus() {
if (!this.isRun) {
throw '程序需要初始化模块'
}
}
async move(distance) {
this.checkStatus()
// 模拟小车运动
return new Promise(resolve => {
let moveDis = 0
let interval = setInterval(() => {
if (moveDis < distance) {
console.log(`move ${moveDis++}`)
} else {
clearInterval(interval)
interval = undefined
resolve()
}
}, 100)
})
}
async arc(direction, degree, distance) {
this.checkStatus()
// 模拟小车运动
return new Promise(resolve => {
let moveDis = 0
let interval = setInterval(() => {
if (moveDis < distance) {
console.log(`direction: ${direction}, move:${moveDis++}, degree: ${degree}`)
} else {
clearInterval(interval)
interval = undefined
resolve()
}
}, 100)
})
}
}
export default Robot
import Robot from 'robot.js'
const robot = new Robot()
- 然后使用 Blockly自定义块 自定义了move、arc等指令的代码块,并通过这些代码块拼接并生成了如下代码字符串:
// 现在需要执行这一代码字符串
let code = `
robot.init();
robot.move(50);
robot.arc(0, 90,50);
robot.stop();
`
- 使用
eval
- 使用eval执行,需要将robot对象挂载到window对象上
window.robot = robot
eval(code)
- 使用
new Function ([arg1, arg2, ...], '代码字符串')
- 使用new Function,最后一个参数是执行代码字符串,前面的参数都是提供给代码字符串的变量
/** 这里相当于定义了一个函数
function(robot) {
//执行的code内容
}
*/
let fn = new Function('robot', code)
// 执行函数, 将robot作为变量传入
fn(robot)
问题分析
问题
- 执行函数后会发现,如果
move
和arc
都是一个异步函数,程序执行顺序有问题,执行顺序为init->stop->move和arc交替执行
原因
- 上面生成的代码字符串是顺序执行的,然后move和arc是异步任务,会放进入宏任务列表,宏任务会在主线程的同步任务执行完之后再执行。
不优雅解决
- 在有异步操作的代码块生成的代码中加入await关键字,异步代码需要在有async标记的函数中执行,所以需要在执行的代码块中包裹一个立即执行的异步匿名函数
Blockly.JavaScript['while_program_start'] = function () {
var while_content = Blockly.JavaScript.statementToCode(block, 'while_content');
const code = `
(async ()=>{
robot.init();\n${while_content}robot.stop();
})()
`
return code;
};
Blockly.JavaScript['move'] = function (block) {
var text_move_distance = block.getFieldValue('move_distance');
var code = `await robot.move(${text_move_distance});\n`;
return code;
};
Blockly.JavaScript['arc'] = function (block) {
var dropdown_dirction = block.getFieldValue('dirction');
var angle_degree = block.getFieldValue('degree');
var radius = block.getFieldValue('radius');
var code = `await robot.arc(${dropdown_dirction}, ${angle_degree}, ${radius});\n`;
return code;
};
// 生成的代码字符串变成如下形式
(async ()=>{
robot.init();
await robot.move(50);
await robot.arc(0, 90, 50);
robot.stop();
})()
优雅解决
- 优雅的方案是使用 JS-Interpreter - JavaScript语法解析器来将字符串代码可执行的代码。在解析器在创建时可自定义属性、自定函数和其对应执行的同步或异步方法,同时,可以通过step方法来步骤执行代码,这就可以配合Blockly的highlightBlock(blockId)方法来实现步骤执行代码块高亮的效果。
- 接入js-interpreter,步骤运行block块将会再下一篇文章中详细描述哦
完整代码
- 测试用例
<template>
<div id="blockly">
<!-- 工作区 -->
<div id="blocklyDiv" ref="blocklyDiv" style="height: 500px; width: 800px;"></div>
<button style="position: fixed;left: 50px;top: 10px;" @click="block2code">生成代码</button>
<!-- 代码显示区 -->
<div style="background-color: lightgrey;width: 800px;text-align: left">
<pre v-html="code?code:'请点击生成代码按钮'"></pre>
</div>
<button style="position: fixed;left: 150px;top: 10px;" @click="runCode">eval执行代码</button>
<button style="position: fixed;left: 300px;top: 10px;" @click="runCode2">new Function执行代码</button>
</div>
</template>
<script>
import Blockly from 'blockly'
import BlocklyJS from 'blockly/javascript';
import './customBlock'
import Robot from './robot'
export default {
name: "blocklyClass3",
data() {
return {
code:'',
options: {
horizontalLayout: true,//工具箱水平
toolboxPosition: "end",//工具箱在底部
toolbox: {
"kind": "flyoutToolbox",
"contents": [
{
"kind": "block",
"type": "while_program_start",
},
{
"kind": "block",
"type": "move",
},
{
"kind": "block",
"type": "arc"
},
{
"kind": "block",
"type": "controls_repeat_ext"
},
{
"kind": "block",
"type": "controls_whileUntil"
},
{
"kind": "block",
"type": "controls_for"
},
{
"kind": "block",
"type": "controls_if"
},
{
"kind": "block",
"type": "logic_compare"
},
{
"kind": "block",
"type": "logic_operation"
},
{
"kind": "block",
"type": "logic_negate"
},
{
"kind": "block",
"type": "logic_boolean"
},
{
"kind": "sep",
"gap": "32"
},
{
"kind": "block",
"blockxml": "<block type='math_number'><field name='NUM'>10</field></block>"
},
{
"kind": "block",
"type": "math_arithmetic"
},
{
"kind": "block",
"type": "math_single"
},
{
"kind": "block",
"type": "text"
},
{
"kind": "block",
"type": "text_length"
},
{
"kind": "block",
"type": "text_print"
},
{
"kind": "block",
"type": "variables_get"
},
{
"kind": "block",
"type": "variables_set"
},
]
}
}
}
},
mounted() {
Blockly.inject(this.$refs.blocklyDiv, this.options);
this.robot = new Robot()
},
methods:{
/**
* block代码块转为代码
*/
block2code(){
this.code = BlocklyJS.workspaceToCode(this.$refs.blocklyDiv.workspace)
},
/**
* 执行生成代码
*/
runCode(){
if(!this.code){alert('请先点击生成代码');return}
window.robot = this.robot
eval(this.code)
},
runCode2(){
if(!this.code){alert('请先点击生成代码');return}
let fn = new Function('robot', this.code)
fn(this.robot)
}
},
}
</script>
<style scoped>
#blockly {
position: absolute;
left: 50px;
top: 50px;
bottom: 0;
width: calc(100vw - 50px);
height: calc(100vh - 50px);
display: flex;
flex-direction: column;
}
</style>
- Blockly自定义组件
import * as Blockly from 'blockly/core'
import * as hans from 'blockly/msg/zh-hans'
Blockly.setLocale(hans);//汉化
/**
* 自定义组件注册
*/
Blockly.defineBlocksWithJsonArray(
[
//事件
{
"type": "while_program_start",
"message0": "当程序运行 %1 %2",
"args0": [
{
"type": "input_dummy"
},
{
"type": "input_statement",
"name": "while_content"
}
],
"previousStatement": null,
"nextStatement": null,
"colour": "#609FD6",
"strokeColour": "#4088C8",
"tooltip": "123",
"helpUrl": "1"
},
//指令
{
"type": "move",
"message0": "移动 %1 CM",
"args0": [
{
"type": "field_input",
"name": "move_distance",
"text": "50"
}
],
"previousStatement": null,
"nextStatement": null,
"colour": "#F7D233",
"strokeColour": "#CCAD2B",
"tooltip": "",
"helpUrl": ""
},
{
"type": "arc",
"message0": "弧形 %1 %2 ,半径 %3 CM",
"args0": [
{
"type": "field_dropdown",
"name": "dirction",
"options": [
[
"向左",
"0"
],
[
"向右",
"1"
]
]
},
{
"type": "field_angle",
"name": "degree",
"angle": 90
},
{
"type": "field_number",
"name": "radius",
"value": 50,
"min": 1,
"max": 100
}
],
"previousStatement": null,
"nextStatement": null,
"colour": "#F7D233",
"strokeColour": "#CCAD2B",
"tooltip": "",
"helpUrl": ""
}
],
"previousStatement": null,
"nextStatement": null,
"colour": "#81C679",
"tooltip": "",
"helpUrl": ""
},
]
);
/**
* 自定义组件生成代码
* @param block
* @returns {string}
*/
Blockly.JavaScript['while_program_start'] = function (block) {
var while_content = Blockly.JavaScript.statementToCode(block, 'while_content');
const code = `
(async ()=>{
robot.init();\n${while_content}robot.stop();
})()
`
return code;
};
Blockly.JavaScript['move'] = function (block) {
var text_move_distance = block.getFieldValue('move_distance');
var code = `await robot.move(${text_move_distance});\n`;
return code;
};
Blockly.JavaScript['arc'] = function (block) {
var dropdown_dirction = block.getFieldValue('dirction');
var angle_degree = block.getFieldValue('degree');
var radius = block.getFieldValue('radius');
var code = `await robot.arc(${dropdown_dirction}, ${angle_degree}, ${radius});\n`;
return code;
};
- Robot控制类
// eslint-disable-next-line no-unused-vars
class Robot {
constructor() {
this.isRun = false
}
init() {
console.log("robot模块化程序初始化")
this.isRun = true
}
stop() {
console.log("robot模块运行结束")
this.isRun = false
}
checkStatus() {
if (!this.isRun) {
throw '程序需要初始化模块'
}
}
async move(distance) {
this.checkStatus()
// 模拟小车运动
return new Promise(resolve => {
let moveDis = 0
let interval = setInterval(() => {
if (moveDis < distance) {
console.log(`move ${moveDis++}`)
} else {
clearInterval(interval)
interval = undefined
resolve()
}
}, 100)
})
}
async arc(direction, degree, distance) {
this.checkStatus()
// 模拟小车运动
return new Promise(resolve => {
let moveDis = 0
let interval = setInterval(() => {
if (moveDis < distance) {
console.log(`direction: ${direction}, move:${moveDis++}, degree: ${degree}`)
} else {
clearInterval(interval)
interval = undefined
resolve()
}
}, 100)
})
}
}
export default Robot
后续计划
- 接入js-interpreter,步骤运行block块