4

What I am doing

I am in the middle of building a turtle graphics app using Blockly. The user can build a code from blocks, then the Blockly engine generates JS code, which draws to a canvas.

What my problem is

The Blockly engine generates the JS code, but returns it as a string, which I have to eval() to draw to the canvas.

I can change the code of the blocks to generate different output, but it's important to keep it as simple as possible, because the users can read the actual code behind the block input. So I would like not to mess it up.

What I would like to do

I have full control over the atomic operations (go, turn, etc.), so I would like to insert a small piece of code to the beginning of the functions, which delays the execution of the rest of the bodies of the functions. Something like:

function go(dir, dist) {
  // wait here a little

  // do the drawing
}

I think it should be something synchronous, which keeps the delay in the flow of the execution. I've tried to use setTimeout (async, fail), a promise (fail), timestamp checks in a loop (fail).

Is it even possible in JS?

Nekomajin42
  • 638
  • 1
  • 6
  • 20
  • 1
    Why would you like such a delay? To show some animation for example? – Tamas Hegedus Aug 21 '16 at 22:29
  • What does wait mean? If you mean block the js execute engine, a loop is the only way. – zhang Aug 21 '16 at 22:52
  • You can't delay synchronous execution. You will have to generate asynchronous code, but as I see, there is no such code generator yet for blocky. It would make little sense, the asynchronous variant is much harder to read. But there is the js interpreter, with which you can esentialy run the code line-by-line asynchronously (and safely). – Tamas Hegedus Aug 21 '16 at 23:09
  • 1
    if you're using an environment that supports `async/await` you can write code that *appears* to be synchronous, however in the end it **must** be asynchronous. – zzzzBov Aug 21 '16 at 23:12
  • @TamasHegedus It is an educational application. It's good to see step-by-step how the shape is created. – Nekomajin42 Aug 21 '16 at 23:38

3 Answers3

3

You must not make the code wait synchronously. The only thing you will get is a frozen browser window.

What you need is to use the js interpreter instead of eval. This way you can pause the execution, play animations, highlight currently executing blocks, etc... The tutorial has many examples that will help you get started. Here is a working code, based on the JS interpreter example:

var workspace = Blockly.inject("editor-div", {
  toolbox: document.getElementById('toolbox')
});

Blockly.JavaScript.STATEMENT_PREFIX = 'highlightBlock(%1);\n';
Blockly.JavaScript.addReservedWords('highlightBlock');

Blockly.JavaScript['text_print'] = function(block) {
  var argument0 = Blockly.JavaScript.valueToCode(
    block, 'TEXT',
    Blockly.JavaScript.ORDER_FUNCTION_CALL
  ) || '\'\'';
  return "print(" + argument0 + ');\n';
};

function run() {
  var code = Blockly.JavaScript.workspaceToCode(workspace);
  var running = false;

  workspace.traceOn(true);
  workspace.highlightBlock(null);

  var lastBlockToHighlight = null;
  var myInterpreter = new Interpreter(code, (interpreter, scope) => {
    interpreter.setProperty(
      scope, 'highlightBlock',
      interpreter.createNativeFunction(id => {
        id = id ? id.toString() : '';
        running = false;
        workspace.highlightBlock(lastBlockToHighlight);
        lastBlockToHighlight = id;
      })
    );
    interpreter.setProperty(
      scope, 'print',
      interpreter.createNativeFunction(val => {
        val = val ? val.toString() : '';
        console.log(val);
      })
    );
  });

  var intervalId = setInterval(() => {
    running = true;
    while (running) {
      if (!myInterpreter.step()) {
        workspace.highlightBlock(lastBlockToHighlight);
        clearInterval(intervalId);
        return;
      }
    }
  }, 500);
}
#editor-div {
  width: 500px;
  height: 150px;
}
<script src="https://rawgit.com/google/blockly/master/blockly_compressed.js"></script>
<script src="https://rawgit.com/google/blockly/master/blocks_compressed.js"></script>
<script src="https://rawgit.com/google/blockly/master/javascript_compressed.js"></script>
<script src="https://rawgit.com/google/blockly/master/msg/js/en.js"></script>
<script src="https://rawgit.com/NeilFraser/JS-Interpreter/master/acorn_interpreter.js"></script>

<xml id="toolbox" style="display: none">
  <block type="text"></block>
  <block type="text_print"></block>
  <block type="controls_repeat_ext"></block>
 <block type="math_number"></block>
</xml>

<div>
  <button id="run-code" onclick="run()">run</button>
</div>
<div id="editor-div"></div>

EDIT

Added variable running to control the interpreter. Now it steps over until the running variable is set to false, so the running = false statement inside the highlightBlock function essentially works as a breakpoint.

EDIT

Introduced lastBlockToHighlight variable to delay the highlighting, so the latest run statement is highlighted, not the next one. Unfortunately the JavaScript code generator doesn't have a STATEMENT_SUFFIX config similar to STATEMENT_PREFIX.

Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
  • I will give it a look. Thanks! – Nekomajin42 Aug 21 '16 at 23:35
  • Can I pass an object to the interpreter with all of it's members? All of my atomic functions are members of a `turtle` object. It would be easier to pass everything at once, instead of two dozens of functions one-by-one, if it is possible. – Nekomajin42 Aug 22 '16 at 01:24
  • OK, I made it work. However, code execution is really slow. It seems like the delay between two steps is about 30 times slower than it should. If I set a 1000ms delay for the `setInterval`, the delay between two steps is about 30s. Any idea? – Nekomajin42 Aug 24 '16 at 15:54
  • The interpreter does many steps while evaluating expressions. In one of the examples they used a global variable to break after specific expressions, lets see if I can reproduce the same behaviour – Tamas Hegedus Aug 24 '16 at 15:59
  • 1
    @Nekomajin42: Added some improvements – Tamas Hegedus Aug 24 '16 at 16:05
  • OK, finally it works. I have one last problem. The block highlight and code execution are out of sync. Code #1 is executed when block #2 is highlighted, and so on. It seems to be a problem with your snippet too. – Nekomajin42 Aug 24 '16 at 18:00
  • @Nekomajin42 I did it on purpose. For me, highlighting the next statement to be executed seemed reasonable. I am going to edit the snippet. – Tamas Hegedus Aug 24 '16 at 18:56
  • 1
    @Nekomajin42 There you go – Tamas Hegedus Aug 24 '16 at 19:07
2

Recently I published a library that allows you to interact asynchronously with blockly, I designed this library for games like that. In fact in the documentation you can find a game demo that is a remake of the maze game. The library is called blockly-gamepad , I hope it's what you were looking for.


Here is a gif of the demo.

Demo


How it works

This is a different and simplified approach compared to the normal use of blockly.

At first you have to define the blocks (see how to define them in the documentation).
You don't have to define any code generator, all that concerns the generation of code is carried out by the library.

enter image description here


Each block generate a request.

// the request
{ method: 'TURN', args: ['RIGHT'] }


When a block is executed the corresponding request is passed to your game.

class Game{
    manageRequests(request){
        // requests are passed here
        if(request.method == 'TURN')
            // animate your sprite
            turn(request.args)
    }
}


You can use promises to manage asynchronous animations, as in your case.

class Game{
    async manageRequests(request){
        if(request.method == 'TURN')
            await turn(request.args)
    }
}


The link between the blocks and your game is managed by the gamepad.

let gamepad = new Blockly.Gamepad(),
    game = new Game()

// requests will be passed here
gamepad.setGame(game, game.manageRequest)


The gamepad provides some methods to manage the blocks execution and consequently the requests generation.

// load the code from the blocks in the workspace
gamepad.load()
// reset the code loaded previously
gamepad.reset()

// the blocks are executed one after the other
gamepad.play() 
// play in reverse
gamepad.play(true)
// the blocks execution is paused
gamepad.pause()
// toggle play
gamepad.togglePlay()

// load the next request 
gamepad.forward()
// load the prior request
gamepad.backward()

// use a block as a breakpoint and play until it is reached
gamepad.debug(id)

You can read the full documentation here.


EDIT: I updated the name of the library, now it is called blockly-gamepad.

Paolo Longo
  • 76
  • 1
  • 4
  • Hi Paolo. Please can you explain how your library fixes the problem that the OP is asking about. – Mike Poole Jul 12 '19 at 17:21
  • You can find an explanation of how it works [here](https://paol-imi.github.io/gamepad.js/#/theidea). @MikePoole – Paolo Longo Jul 13 '19 at 18:12
  • Hi Paolo. The point of my comment is that you should explain how this works in SO rather than linking out to another site where the link may die. – Mike Poole Jul 15 '19 at 08:58
  • Hi Mike, the link redirects to the github documentation that should not die. Now I modify the answer to make everything clearer, thanks for the advice. – Paolo Longo Jul 15 '19 at 11:19
1

If i understood you!

You can build a new class to handle the executing of go(dir, dist) functions, and override the go function to create new go in the executor.

function GoExecutor(){

    var executeArray = [];     // Store go methods that waiting for execute
    var isRunning = false;     // Handle looper function

    // start runner function
    var run = function(){
        if(isRunning)
            return;
        isRunning = true;
        runner();
    }

    // looper for executeArray
    var runner = function(){
        if(executeArray.length == 0){
            isRunning = false;
            return;
        }

        // pop the first inserted params 
        var currentExec = executeArray.shift(0);

        // wait delay miliseconds
        setTimeout(function(){
            // execute the original go function
            originalGoFunction(currentExec.dir, currentExec.dist);

            // after finish drawing loop on the next execute method
            runner();
        }, currentExec.delay);

    }
    this.push = function(dir, dist){
        executeArray.push([dir,dist]);
        run();
    }
}

// GoExecutor instance
var goExec = new GoExecutor();

// Override go function
var originalGoFunction = go;
var go = function (dir, dist, delay){
    goExec.push({"dir":dir, "dist":dist, "delay":delay});
}

Edit 1:

Now you have to call callWithDelay with your function and params, the executor will handle this call by applying the params to the specified function.

function GoExecutor(){

    var executeArray = [];     // Store go methods that waiting for execute
    var isRunning = false;     // Handle looper function

    // start runner function
    var run = function(){
        if(isRunning)
            return;
        isRunning = true;
        runner();
    }

    // looper for executeArray
    var runner = function(){
        if(executeArray.length == 0){
            isRunning = false;
            return;
        }

        // pop the first inserted params 
        var currentExec = executeArray.shift(0);

        // wait delay miliseconds
        setTimeout(function(){
            // execute the original go function
            currentExec.funcNam.apply(currentExec.funcNam, currentExec.arrayParams);

            // after finish drawing loop on the next execute method
            runner();
        }, currentExec.delay);

    }
    this.push = function(dir, dist){
        executeArray.push([dir,dist]);
        run();
    }
}

// GoExecutor instance
var goExec = new GoExecutor();

var callWithDelay = function (func, arrayParams, delay){
    goExec.push({"func": func, "arrayParams":arrayParams, "delay":delay});
}
David Antoon
  • 815
  • 6
  • 18