2016-04-18 18:03:18 -04:00
|
|
|
var EventEmitter = require('events');
|
2016-04-26 14:20:44 -04:00
|
|
|
var Sequencer = require('./sequencer');
|
2016-04-26 16:50:49 -04:00
|
|
|
var Thread = require('./thread');
|
2016-04-18 18:03:18 -04:00
|
|
|
var util = require('util');
|
|
|
|
|
2016-05-02 12:20:27 -04:00
|
|
|
var defaultBlockPackages = {
|
|
|
|
'scratch3': require('../blocks/scratch3'),
|
|
|
|
'wedo2': require('../blocks/wedo2')
|
|
|
|
};
|
|
|
|
|
2016-04-18 17:20:30 -04:00
|
|
|
/**
|
2016-04-26 15:00:45 -04:00
|
|
|
* Manages blocks, stacks, and the sequencer.
|
2016-04-18 17:20:30 -04:00
|
|
|
*/
|
|
|
|
function Runtime () {
|
2016-04-18 18:03:18 -04:00
|
|
|
// Bind event emitter
|
2016-04-26 09:49:52 -04:00
|
|
|
EventEmitter.call(this);
|
2016-04-18 17:20:30 -04:00
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
// State for the runtime
|
2016-04-26 15:51:14 -04:00
|
|
|
/**
|
|
|
|
* All blocks in the workspace.
|
|
|
|
* Keys are block IDs, values are metadata about the block.
|
|
|
|
* @type {Object.<string, Object>}
|
|
|
|
*/
|
2016-04-18 17:20:30 -04:00
|
|
|
this.blocks = {};
|
2016-04-26 15:51:14 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* All stacks in the workspace.
|
|
|
|
* A list of block IDs that represent stacks (first block in stack).
|
|
|
|
* @type {Array.<String>}
|
|
|
|
*/
|
2016-04-18 17:20:30 -04:00
|
|
|
this.stacks = [];
|
2016-04-26 14:20:44 -04:00
|
|
|
|
2016-04-26 15:51:14 -04:00
|
|
|
/**
|
|
|
|
* A list of threads that are currently running in the VM.
|
|
|
|
* Threads are added when execution starts and pruned when execution ends.
|
|
|
|
* @type {Array.<Thread>}
|
|
|
|
*/
|
|
|
|
this.threads = [];
|
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
/** @type {!Sequencer} */
|
2016-04-26 16:50:49 -04:00
|
|
|
this.sequencer = new Sequencer(this);
|
2016-05-02 12:20:27 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Map to look up a block primitive's implementation function by its opcode.
|
|
|
|
* This is a two-step lookup: package name first, then primitive name.
|
|
|
|
* @type {Object.<string, Function>}
|
|
|
|
*/
|
|
|
|
this._primitives = {};
|
|
|
|
this._registerBlockPackages();
|
2016-04-18 17:20:30 -04:00
|
|
|
}
|
|
|
|
|
2016-05-02 13:09:38 -04:00
|
|
|
/**
|
|
|
|
* Event name for glowing a stack
|
|
|
|
* @const {string}
|
|
|
|
*/
|
|
|
|
Runtime.STACK_GLOW_ON = 'STACK_GLOW_ON';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Event name for unglowing a stack
|
|
|
|
* @const {string}
|
|
|
|
*/
|
|
|
|
Runtime.STACK_GLOW_OFF = 'STACK_GLOW_OFF';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Event name for glowing a block
|
|
|
|
* @const {string}
|
|
|
|
*/
|
|
|
|
Runtime.BLOCK_GLOW_ON = 'BLOCK_GLOW_ON';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Event name for unglowing a block
|
|
|
|
* @const {string}
|
|
|
|
*/
|
|
|
|
Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
|
|
|
|
|
2016-04-18 18:03:18 -04:00
|
|
|
/**
|
|
|
|
* Inherit from EventEmitter
|
|
|
|
*/
|
|
|
|
util.inherits(Runtime, EventEmitter);
|
|
|
|
|
2016-04-26 16:50:49 -04:00
|
|
|
/**
|
|
|
|
* How rapidly we try to step threads, in ms.
|
|
|
|
*/
|
|
|
|
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
|
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
/**
|
|
|
|
* Block management: create blocks and stacks from a `create` event
|
|
|
|
* @param {!Object} block Blockly create event to be processed
|
|
|
|
*/
|
2016-04-29 16:49:08 -04:00
|
|
|
Runtime.prototype.createBlock = function (block, opt_isFlyoutBlock) {
|
2016-04-18 17:20:30 -04:00
|
|
|
// Create new block
|
2016-04-26 09:49:52 -04:00
|
|
|
this.blocks[block.id] = block;
|
|
|
|
|
|
|
|
// Walk each field and add any shadow blocks
|
|
|
|
// @todo Expand this to cover vertical / nested blocks
|
|
|
|
for (var i in block.fields) {
|
|
|
|
var shadows = block.fields[i].blocks;
|
|
|
|
for (var y in shadows) {
|
|
|
|
var shadow = shadows[y];
|
|
|
|
this.blocks[shadow.id] = shadow;
|
2016-04-26 09:54:14 -04:00
|
|
|
}
|
2016-04-26 09:49:52 -04:00
|
|
|
}
|
2016-04-18 17:20:30 -04:00
|
|
|
|
|
|
|
// Push block id to stacks array. New blocks are always a stack even if only
|
|
|
|
// momentary. If the new block is added to an existing stack this stack will
|
|
|
|
// be removed by the `moveBlock` method below.
|
2016-04-29 16:49:08 -04:00
|
|
|
if (!opt_isFlyoutBlock) {
|
|
|
|
this.stacks.push(block.id);
|
|
|
|
}
|
2016-04-26 09:49:52 -04:00
|
|
|
};
|
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
/**
|
|
|
|
* Block management: change block field values
|
|
|
|
* @param {!Object} args Blockly change event to be processed
|
|
|
|
*/
|
2016-04-26 09:49:52 -04:00
|
|
|
Runtime.prototype.changeBlock = function (args) {
|
|
|
|
// Validate
|
|
|
|
if (args.element !== 'field') return;
|
|
|
|
if (typeof this.blocks[args.id] === 'undefined') return;
|
|
|
|
if (typeof this.blocks[args.id].fields[args.name] === 'undefined') return;
|
|
|
|
|
|
|
|
// Update block value
|
|
|
|
this.blocks[args.id].fields[args.name].value = args.value;
|
2016-04-18 17:20:30 -04:00
|
|
|
};
|
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
/**
|
|
|
|
* Block management: move blocks from parent to parent
|
|
|
|
* @param {!Object} e Blockly move event to be processed
|
|
|
|
*/
|
2016-04-18 17:20:30 -04:00
|
|
|
Runtime.prototype.moveBlock = function (e) {
|
|
|
|
var _this = this;
|
|
|
|
|
2016-05-02 22:31:00 -04:00
|
|
|
// Block was removed from parent
|
|
|
|
if (e.newParent === undefined && e.oldParent !== undefined) {
|
|
|
|
// Add stack
|
|
|
|
_this.stacks.push(e.id);
|
|
|
|
|
|
|
|
// Update old parent
|
|
|
|
if (e.oldField === undefined) {
|
|
|
|
_this.blocks[e.oldParent].next = null;
|
|
|
|
} else {
|
|
|
|
delete _this.blocks[e.oldParent].fields[e.oldField];
|
|
|
|
}
|
|
|
|
} else if (e.newParent !== undefined) {
|
|
|
|
// Block was moved to a new parent
|
|
|
|
// Either happens because it was previously parentless
|
|
|
|
// (e.oldParent === undefined)
|
|
|
|
// or because a block was moved in front of it.
|
|
|
|
|
2016-04-18 17:20:30 -04:00
|
|
|
// Remove stack
|
|
|
|
_this._deleteStack(e.id);
|
|
|
|
|
|
|
|
// Update new parent
|
2016-04-26 09:49:52 -04:00
|
|
|
if (e.newField === undefined) {
|
2016-04-18 17:20:30 -04:00
|
|
|
_this.blocks[e.newParent].next = e.id;
|
|
|
|
} else {
|
2016-04-26 09:49:52 -04:00
|
|
|
_this.blocks[e.newParent].fields[e.newField] = {
|
|
|
|
name: e.newField,
|
|
|
|
value: e.id,
|
|
|
|
blocks: {}
|
|
|
|
};
|
2016-04-18 17:20:30 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
/**
|
|
|
|
* Block management: delete blocks and their associated stacks
|
|
|
|
* @param {!Object} e Blockly delete event to be processed
|
|
|
|
*/
|
2016-04-18 17:20:30 -04:00
|
|
|
Runtime.prototype.deleteBlock = function (e) {
|
|
|
|
// @todo Stop threads running on this stack
|
|
|
|
|
2016-04-26 09:49:52 -04:00
|
|
|
// Get block
|
2016-04-18 17:20:30 -04:00
|
|
|
var block = this.blocks[e.id];
|
2016-04-26 09:49:52 -04:00
|
|
|
|
|
|
|
// Delete children
|
2016-04-18 17:20:30 -04:00
|
|
|
if (block.next !== null) {
|
|
|
|
this.deleteBlock({id: block.next});
|
|
|
|
}
|
|
|
|
|
2016-04-26 09:49:52 -04:00
|
|
|
// Delete substacks and fields
|
|
|
|
for (var field in block.fields) {
|
|
|
|
if (field === 'SUBSTACK') {
|
|
|
|
this.deleteBlock({id: block.fields[field].value});
|
|
|
|
} else {
|
|
|
|
for (var shadow in block.fields[field].blocks) {
|
|
|
|
this.deleteBlock({id: shadow});
|
|
|
|
}
|
|
|
|
}
|
2016-04-18 17:20:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Delete stack
|
|
|
|
this._deleteStack(e.id);
|
|
|
|
|
|
|
|
// Delete block
|
|
|
|
delete this.blocks[e.id];
|
|
|
|
};
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
2016-04-26 16:50:49 -04:00
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
2016-05-02 12:20:27 -04:00
|
|
|
/**
|
|
|
|
* Register default block packages with this runtime.
|
|
|
|
* @todo Prefix opcodes with package name.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Runtime.prototype._registerBlockPackages = function () {
|
|
|
|
for (var packageName in defaultBlockPackages) {
|
|
|
|
if (defaultBlockPackages.hasOwnProperty(packageName)) {
|
2016-05-02 14:54:32 -04:00
|
|
|
// @todo pass a different runtime depending on package privilege?
|
2016-05-02 14:07:40 -04:00
|
|
|
var packageObject = new (defaultBlockPackages[packageName])(this);
|
2016-05-02 12:20:27 -04:00
|
|
|
var packageContents = packageObject.getPrimitives();
|
|
|
|
for (var op in packageContents) {
|
|
|
|
if (packageContents.hasOwnProperty(op)) {
|
2016-05-02 14:54:32 -04:00
|
|
|
this._primitives[op] =
|
|
|
|
packageContents[op].bind(packageObject);
|
2016-05-02 12:20:27 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve the function associated with the given opcode.
|
|
|
|
* @param {!string} opcode The opcode to look up.
|
|
|
|
* @return {Function} The function which implements the opcode.
|
|
|
|
*/
|
|
|
|
Runtime.prototype.getOpcodeFunction = function (opcode) {
|
|
|
|
return this._primitives[opcode];
|
|
|
|
};
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
2016-04-26 16:50:49 -04:00
|
|
|
/**
|
|
|
|
* Create a thread and push it to the list of threads.
|
|
|
|
* @param {!string} id ID of block that starts the stack
|
|
|
|
*/
|
|
|
|
Runtime.prototype._pushThread = function (id) {
|
2016-05-02 13:09:38 -04:00
|
|
|
this.emit(Runtime.STACK_GLOW_ON, id);
|
2016-04-26 16:50:49 -04:00
|
|
|
var thread = new Thread(id);
|
|
|
|
this.threads.push(thread);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a thread from the list of threads.
|
2016-04-29 17:36:09 -04:00
|
|
|
* @param {?Thread} thread Thread object to remove from actives
|
2016-04-26 16:50:49 -04:00
|
|
|
*/
|
2016-04-29 17:36:09 -04:00
|
|
|
Runtime.prototype._removeThread = function (thread) {
|
|
|
|
var i = this.threads.indexOf(thread);
|
2016-05-02 13:09:38 -04:00
|
|
|
if (i > -1) {
|
|
|
|
this.emit(Runtime.STACK_GLOW_OFF, thread.topBlock);
|
|
|
|
this.threads.splice(i, 1);
|
|
|
|
}
|
2016-04-26 16:50:49 -04:00
|
|
|
};
|
|
|
|
|
2016-04-29 17:31:04 -04:00
|
|
|
/**
|
|
|
|
* Toggle a stack
|
|
|
|
* @param {!string} stackId ID of block that starts the stack
|
|
|
|
*/
|
|
|
|
Runtime.prototype.toggleStack = function (stackId) {
|
|
|
|
// Remove any existing thread
|
|
|
|
for (var i = 0; i < this.threads.length; i++) {
|
|
|
|
if (this.threads[i].topBlock == stackId) {
|
|
|
|
this._removeThread(this.threads[i]);
|
2016-04-29 17:39:18 -04:00
|
|
|
return;
|
2016-04-29 17:31:04 -04:00
|
|
|
}
|
|
|
|
}
|
2016-04-29 17:39:18 -04:00
|
|
|
// Otherwise add it
|
2016-04-29 17:31:04 -04:00
|
|
|
this._pushThread(stackId);
|
|
|
|
};
|
|
|
|
|
2016-04-29 17:58:31 -04:00
|
|
|
/**
|
|
|
|
* Green flag, which stops currently running threads
|
|
|
|
* and adds all top-level stacks that start with the green flag
|
|
|
|
*/
|
|
|
|
Runtime.prototype.greenFlag = function () {
|
|
|
|
// Remove all existing threads
|
|
|
|
for (var i = 0; i < this.threads.length; i++) {
|
|
|
|
this._removeThread(this.threads[i]);
|
|
|
|
}
|
|
|
|
// Add all top stacks with green flag
|
|
|
|
for (var j = 0; j < this.stacks.length; j++) {
|
|
|
|
var topBlock = this.stacks[j];
|
|
|
|
if (this.blocks[topBlock].opcode === 'event_whenflagclicked') {
|
|
|
|
this._pushThread(this.stacks[j]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop "everything"
|
|
|
|
*/
|
|
|
|
Runtime.prototype.stopAll = function () {
|
2016-05-03 14:11:37 -04:00
|
|
|
var threadsCopy = this.threads.slice();
|
|
|
|
while (threadsCopy.length > 0) {
|
|
|
|
this._removeThread(threadsCopy.pop());
|
2016-04-29 17:58:31 -04:00
|
|
|
}
|
|
|
|
// @todo call stop function in all extensions/packages/WeDo stub
|
2016-05-03 17:18:28 -04:00
|
|
|
if (window.native) {
|
|
|
|
window.native.motorStop();
|
|
|
|
}
|
2016-04-29 17:58:31 -04:00
|
|
|
};
|
|
|
|
|
2016-04-26 16:50:49 -04:00
|
|
|
/**
|
|
|
|
* Repeatedly run `sequencer.stepThreads` and filter out
|
|
|
|
* inactive threads after each iteration.
|
|
|
|
*/
|
|
|
|
Runtime.prototype._step = function () {
|
|
|
|
var inactiveThreads = this.sequencer.stepThreads(this.threads);
|
|
|
|
for (var i = 0; i < inactiveThreads.length; i++) {
|
|
|
|
this._removeThread(inactiveThreads[i]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-05-02 18:09:02 -04:00
|
|
|
/**
|
|
|
|
* Emit feedback for block glowing (used in the sequencer).
|
|
|
|
* @param {?string} blockId ID for the block to update glow
|
|
|
|
* @param {boolean} isGlowing True to turn on glow; false to turn off.
|
|
|
|
*/
|
|
|
|
Runtime.prototype.glowBlock = function (blockId, isGlowing) {
|
|
|
|
if (isGlowing) {
|
|
|
|
this.emit(Runtime.BLOCK_GLOW_ON, blockId);
|
|
|
|
} else {
|
|
|
|
this.emit(Runtime.BLOCK_GLOW_OFF, blockId);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-04-26 16:50:49 -04:00
|
|
|
/**
|
|
|
|
* Set up timers to repeatedly step in a browser
|
|
|
|
*/
|
|
|
|
Runtime.prototype.start = function () {
|
|
|
|
if (!window.setInterval) return;
|
|
|
|
window.setInterval(function() {
|
|
|
|
this._step();
|
|
|
|
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
|
|
|
|
};
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
2016-04-18 17:20:30 -04:00
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
/**
|
|
|
|
* Helper to remove a stack from `this.stacks`
|
|
|
|
* @param {?string} id ID of block that starts the stack
|
|
|
|
*/
|
2016-04-18 17:20:30 -04:00
|
|
|
Runtime.prototype._deleteStack = function (id) {
|
|
|
|
var i = this.stacks.indexOf(id);
|
|
|
|
if (i > -1) this.stacks.splice(i, 1);
|
|
|
|
};
|
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
/**
|
|
|
|
* Helper to get the next block for a particular block
|
|
|
|
* @param {?string} id ID of block to get the next block for
|
|
|
|
* @return {?string} ID of next block in the sequence
|
|
|
|
*/
|
2016-04-18 17:20:30 -04:00
|
|
|
Runtime.prototype._getNextBlock = function (id) {
|
|
|
|
if (typeof this.blocks[id] === 'undefined') return null;
|
|
|
|
return this.blocks[id].next;
|
|
|
|
};
|
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
/**
|
|
|
|
* Helper to get the substack for a particular C-shaped block
|
|
|
|
* @param {?string} id ID for block to get the substack for
|
|
|
|
* @return {?string} ID of block in the substack
|
|
|
|
*/
|
2016-04-18 17:20:30 -04:00
|
|
|
Runtime.prototype._getSubstack = function (id) {
|
|
|
|
if (typeof this.blocks[id] === 'undefined') return null;
|
2016-04-26 09:49:52 -04:00
|
|
|
return this.blocks[id].fields['SUBSTACK'];
|
2016-04-18 17:20:30 -04:00
|
|
|
};
|
|
|
|
|
2016-05-02 12:20:27 -04:00
|
|
|
/**
|
|
|
|
* Helper to get the opcode for a particular block
|
|
|
|
* @param {?string} id ID of block to query
|
|
|
|
* @return {?string} the opcode corresponding to that block
|
|
|
|
*/
|
|
|
|
Runtime.prototype._getOpcode = function (id) {
|
|
|
|
if (typeof this.blocks[id] === 'undefined') return null;
|
|
|
|
return this.blocks[id].opcode;
|
|
|
|
};
|
|
|
|
|
2016-04-18 17:20:30 -04:00
|
|
|
module.exports = Runtime;
|