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
+
+
-
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)
+ );
}
/**