diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..e84613dd6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 4 +trim_trailing_whitespace = true + +[*.{js,html}] +indent_style = space diff --git a/.eslintrc b/.eslintrc index 7624840ff..dbd86477a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,7 @@ "max-len": [2, 80, 4], "semi": [2, "always"], "strict": [2, "never"], - "no-console": [2, {"allow": ["log", "warn", "error"]}], + "no-console": [2, {"allow": ["log", "warn", "error", "groupCollapsed", "groupEnd"]}], "valid-jsdoc": ["error", {"requireReturn": false}] }, "env": { diff --git a/playground/index.html b/playground/index.html index 87cef30b3..3d10a22d0 100644 --- a/playground/index.html +++ b/playground/index.html @@ -12,8 +12,16 @@

Scratch VM Playground

+

+ VM Threads
+ VM Block Representation +

+
+ Thread explorer +

+        
-

VM Block Representation

+ Block explorer

         
diff --git a/playground/playground.css b/playground/playground.css index b20b2bd91..451a7cad5 100644 --- a/playground/playground.css +++ b/playground/playground.css @@ -1,6 +1,9 @@ body { background: rgb(36,36,36); } +a { + color: rgb(217,217,217); +} #blocks { position: absolute; left: 40%; @@ -17,7 +20,7 @@ body { bottom: 0; width: 35%; } -#blockexplorer { +#blockexplorer, #threadexplorer { position: absolute; width: 100%; height: 75%; @@ -28,3 +31,7 @@ body { font-family: monospace; font-size: 10pt; } + +#tab-blockexplorer { + display: none; +} diff --git a/playground/playground.js b/playground/playground.js index 1cc5a4b2e..5669212d7 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -7,6 +7,11 @@ window.onload = function() { var workspace = window.Blockly.inject('blocks', { toolbox: toolbox, media: '../node_modules/scratch-blocks/media/', + zoom: { + controls: true, + wheel: true, + startScale: 0.75 + }, colours: { workspace: '#334771', flyout: '#283856', @@ -20,24 +25,45 @@ window.onload = function() { }); window.workspace = workspace; - // @todo: Also bind to flyout events. // Block events. workspace.addChangeListener(vm.blockListener); + var flyoutWorkspace = workspace.toolbox_.flyout_.workspace_; + flyoutWorkspace.addChangeListener(vm.flyoutBlockListener); - var explorer = document.getElementById('blockexplorer'); + var blockexplorer = document.getElementById('blockexplorer'); workspace.addChangeListener(function() { // On a change, update the block explorer. - explorer.innerHTML = JSON.stringify(vm.runtime.blocks, null, 2); - window.hljs.highlightBlock(explorer); + blockexplorer.innerHTML = JSON.stringify(vm.runtime.blocks, null, 2); + window.hljs.highlightBlock(blockexplorer); }); - // Feedback for stacks running. + var threadexplorer = document.getElementById('threadexplorer'); + var cachedThreadJSON = ''; + var updateThreadExplorer = function () { + var newJSON = JSON.stringify(vm.runtime.threads, null, 2); + if (newJSON != cachedThreadJSON) { + cachedThreadJSON = newJSON; + threadexplorer.innerHTML = cachedThreadJSON; + window.hljs.highlightBlock(threadexplorer); + } + window.requestAnimationFrame(updateThreadExplorer); + }; + updateThreadExplorer(); + + // Feedback for stacks and blocks running. vm.runtime.on('STACK_GLOW_ON', function(blockId) { workspace.glowStack(blockId, true); }); vm.runtime.on('STACK_GLOW_OFF', function(blockId) { workspace.glowStack(blockId, false); }); + vm.runtime.on('BLOCK_GLOW_ON', function(blockId) { + workspace.glowBlock(blockId, true); + }); + vm.runtime.on('BLOCK_GLOW_OFF', function(blockId) { + workspace.glowBlock(blockId, false); + }); + // Run threads vm.runtime.start(); @@ -49,4 +75,19 @@ window.onload = function() { document.getElementById('stopall').addEventListener('click', function() { vm.runtime.stopAll(); }); + + var tabBlockExplorer = document.getElementById('tab-blockexplorer'); + var tabThreadExplorer = document.getElementById('tab-threadexplorer'); + + // Handlers to show different explorers. + document.getElementById('threadexplorer-link').addEventListener('click', + function () { + tabBlockExplorer.style.display = 'none'; + tabThreadExplorer.style.display = 'block'; + }); + document.getElementById('blockexplorer-link').addEventListener('click', + function () { + tabBlockExplorer.style.display = 'block'; + tabThreadExplorer.style.display = 'none'; + }); }; diff --git a/src/blocks/scratch3.js b/src/blocks/scratch3.js index 6bf327642..a2f132532 100644 --- a/src/blocks/scratch3.js +++ b/src/blocks/scratch3.js @@ -23,7 +23,6 @@ Scratch3Blocks.prototype.getPrimitives = function() { }; Scratch3Blocks.prototype.repeat = function(argValues, util) { - console.log('Running: control_repeat'); // Initialize loop if (util.stackFrame.loopCounter === undefined) { util.stackFrame.loopCounter = parseInt(argValues[0]); // @todo arg @@ -37,12 +36,10 @@ Scratch3Blocks.prototype.repeat = function(argValues, util) { }; Scratch3Blocks.prototype.forever = function(argValues, util) { - console.log('Running: control_forever'); util.startSubstack(); }; Scratch3Blocks.prototype.wait = function(argValues, util) { - console.log('Running: control_wait'); util.yield(); util.timeout(function() { util.done(); @@ -50,23 +47,19 @@ Scratch3Blocks.prototype.wait = function(argValues, util) { }; Scratch3Blocks.prototype.stop = function() { - console.log('Running: control_stop'); // @todo - don't use this.runtime this.runtime.stopAll(); }; Scratch3Blocks.prototype.whenFlagClicked = function() { - console.log('Running: event_whenflagclicked'); // No-op }; Scratch3Blocks.prototype.whenBroadcastReceived = function() { - console.log('Running: event_whenbroadcastreceived'); // No-op }; Scratch3Blocks.prototype.broadcast = function(argValues, util) { - console.log('Running: event_broadcast'); util.startHats(function(hat) { if (hat.opcode === 'event_whenbroadcastreceived') { var shadows = hat.fields.CHOICE.blocks; diff --git a/src/blocks/wedo2.js b/src/blocks/wedo2.js index 52f2c372d..d4a2a9308 100644 --- a/src/blocks/wedo2.js +++ b/src/blocks/wedo2.js @@ -144,11 +144,9 @@ WeDo2Blocks.prototype.setColor = function(argValues, util) { }; WeDo2Blocks.prototype.whenDistanceClose = function() { - console.log('Running: wedo_whendistanceclose'); }; WeDo2Blocks.prototype.whenTilt = function() { - console.log('Running: wedo_whentilt'); }; module.exports = WeDo2Blocks; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index b1da172a9..e9ac68427 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -1,3 +1,5 @@ +var adapter = require('./adapter'); + /** * @fileoverview * Store and mutate the VM block representation, @@ -80,6 +82,70 @@ Blocks.prototype.getOpcode = function (id) { // --------------------------------------------------------------------- +/** + * Create event listener for blocks. Handles validation and serves as a generic + * adapter between the blocks and the runtime interface. + * @param {boolean} isFlyout If true, create a listener for flyout events. + * @param {?Runtime} opt_runtime Optional runtime to forward click events to. + * @return {Function} A generated listener to attach to Blockly instance. + */ + +Blocks.prototype.generateBlockListener = function (isFlyout, opt_runtime) { + /** + * The actual generated block listener. + * @param {Object} Blockly "block" event + */ + var instance = this; + return function (e) { + // Validate event + if (typeof e !== 'object') return; + if (typeof e.blockId !== 'string') return; + + // UI event: clicked stacks toggle in the runtime. + if (e.element === 'stackclick') { + if (opt_runtime) { + opt_runtime.toggleStack(e.blockId); + } + return; + } + + // Block create/update/destroy + switch (e.type) { + case 'create': + var newBlocks = adapter(e); + // A create event can create many blocks. Add them all. + for (var i = 0; i < newBlocks.length; i++) { + instance.createBlock(newBlocks[i], isFlyout); + } + break; + case 'change': + instance.changeBlock({ + id: e.blockId, + element: e.element, + name: e.name, + value: e.newValue + }); + break; + case 'move': + instance.moveBlock({ + id: e.blockId, + oldParent: e.oldParentId, + oldInput: e.oldInputName, + newParent: e.newParentId, + newInput: e.newInputName + }); + break; + case 'delete': + instance.deleteBlock({ + id: e.blockId + }); + break; + } + }; +}; + +// --------------------------------------------------------------------- + /** * Block management: create blocks and stacks from a `create` event * @param {!Object} block Blockly create event to be processed diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 5e72eb7d2..930fa0ea5 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -199,7 +199,13 @@ Runtime.prototype.startDistanceSensors = function () { Runtime.prototype.stopAll = function () { var threadsCopy = this.threads.slice(); while (threadsCopy.length > 0) { - this._removeThread(threadsCopy.pop()); + var poppedThread = threadsCopy.pop(); + // Unglow any blocks on this thread's stack. + for (var i = 0; i < poppedThread.stack.length; i++) { + this.glowBlock(poppedThread.stack[i], false); + } + // Actually remove the thread. + this._removeThread(poppedThread); } // @todo call stop function in all extensions/packages/WeDo stub if (window.native) { diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 1d9f3a214..c31081798 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -24,6 +24,12 @@ function Sequencer (runtime) { */ Sequencer.WORK_TIME = 10; +/** + * If set, block calls, args, and return values will be logged to the console. + * @const {boolean} + */ +Sequencer.DEBUG_BLOCK_CALLS = true; + /** * Step through all threads in `this.threads`, running them in order. * @param {Array.} threads List of which threads to step. @@ -135,6 +141,8 @@ Sequencer.prototype.stepThread = function (thread) { // Pop the stack and stack frame thread.stack.pop(); thread.stackFrames.pop(); + // Stop showing run feedback in the editor. + instance.runtime.glowBlock(currentBlock, false); }; /** @@ -203,6 +211,9 @@ Sequencer.prototype.stepThread = function (thread) { } } + // Start showing run feedback in the editor. + this.runtime.glowBlock(currentBlock, true); + if (!opcode) { console.warn('Could not get opcode for block: ' + currentBlock); } @@ -212,9 +223,15 @@ Sequencer.prototype.stepThread = function (thread) { console.warn('Could not get implementation for opcode: ' + opcode); } else { + if (Sequencer.DEBUG_BLOCK_CALLS) { + console.groupCollapsed('Executing: ' + opcode); + console.log('with arguments: ', argValues); + console.log('and stack frame: ', currentStackFrame); + } + var blockFunctionReturnValue = null; try { // @todo deal with the return value - blockFunction(argValues, { + blockFunctionReturnValue = blockFunction(argValues, { yield: threadYieldCallback, done: threadDoneCallback, timeout: YieldTimers.timeout, @@ -237,6 +254,11 @@ Sequencer.prototype.stepThread = function (thread) { // Thread executed without yielding - move to done threadDoneCallback(); } + if (Sequencer.DEBUG_BLOCK_CALLS) { + console.log('ending stack frame: ', currentStackFrame); + console.log('returned: ', blockFunctionReturnValue); + console.groupEnd(); + } } } } diff --git a/src/index.js b/src/index.js index 3bb6bb52f..e8558a19e 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,6 @@ var util = require('util'); var Blocks = require('./engine/blocks'); var Runtime = require('./engine/runtime'); -var adapter = require('./engine/adapter'); /** * Handles connections between blocks, stage, and extensions. @@ -20,83 +19,15 @@ function VirtualMachine () { instance.runtime = new Runtime(instance.blocks); /** - * Event listener for blocks. Handles validation and serves as a generic - * adapter between the blocks and the runtime interface. - * - * @param {Object} e Blockly "block" event + * Event listeners for scratch-blocks. */ - instance.blockListener = function (e) { - // Validate event - if (typeof e !== 'object') return; - if (typeof e.blockId !== 'string') return; + instance.blockListener = ( + instance.blocks.generateBlockListener(false, instance.runtime) + ); - // UI event: clicked stacks toggle in the runtime. - if (e.element === 'stackclick') { - instance.runtime.toggleStack(e.blockId); - return; - } - - // Block create/update/destroy - switch (e.type) { - case 'create': - var newBlocks = adapter(e); - // A create event can create many blocks. Add them all. - for (var i = 0; i < newBlocks.length; i++) { - instance.blocks.createBlock(newBlocks[i], false); - } - break; - case 'change': - instance.blocks.changeBlock({ - id: e.blockId, - element: e.element, - name: e.name, - value: e.newValue - }); - break; - case 'move': - instance.blocks.moveBlock({ - id: e.blockId, - oldParent: e.oldParentId, - oldInput: e.oldInputName, - newParent: e.newParentId, - newInput: e.newInputName - }); - break; - case 'delete': - instance.blocks.deleteBlock({ - id: e.blockId - }); - break; - } - }; - - instance.flyoutBlockListener = function (e) { - switch (e.type) { - case 'create': - var newBlocks = adapter(e); - // A create event can create many blocks. Add them all. - for (var i = 0; i < newBlocks.length; i++) { - instance.blocks.createBlock(newBlocks[i], true); - } - break; - case 'change': - instance.blocks.changeBlock({ - id: e.blockId, - element: e.element, - name: e.name, - value: e.newValue - }); - break; - case 'delete': - instance.blocks.deleteBlock({ - id: e.blockId - }); - break; - case 'stackclick': - instance.runtime.toggleStack(e.blockId); - break; - } - }; + instance.flyoutBlockListener = ( + instance.blocks.generateBlockListener(true, instance.runtime) + ); } /**