diff --git a/src/engine/blocks.js b/src/engine/blocks.js new file mode 100644 index 000000000..26351c1b1 --- /dev/null +++ b/src/engine/blocks.js @@ -0,0 +1,199 @@ +/** +* @fileoverview + * Store and mutate the VM block representation, + * and handle updates from Scratch Blocks events. + */ + + function Blocks () { + /** + * All blocks in the workspace. + * Keys are block IDs, values are metadata about the block. + * @type {Object.<string, Object>} + */ + this._blocks = {}; + + /** + * All stacks in the workspace. + * A list of block IDs that represent stacks (first block in stack). + * @type {Array.<String>} + */ + this._stacks = []; + } + +/** + * Provide an object with metadata for the requested block ID. + * @param {!string} blockId ID of block we have stored. + * @return {?Object} Metadata about the block, if it exists. + */ + Blocks.prototype.getBlock = function (blockId) { + return this._blocks[blockId]; + }; + +/** + * Get all known top-level blocks that start stacks. + * @return {Array.<string>} List of block IDs. + */ + Blocks.prototype.getStacks = function () { + return this._stacks; + }; + + /** + * 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 + */ + Blocks.prototype.getNextBlock = function (id) { + if (typeof this._blocks[id] === 'undefined') return null; + return this._blocks[id].next; + }; + + /** + * 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 + */ + Blocks.prototype.getSubstack = function (id) { + var block = this._blocks[id]; + if (typeof block === 'undefined') return null; + // Empty C-block? + if (!('SUBSTACK' in block.inputs)) return null; + return block.inputs['SUBSTACK'].block; + }; + + /** + * Get the opcode for a particular block + * @param {?string} id ID of block to query + * @return {?string} the opcode corresponding to that block + */ + Blocks.prototype.getOpcode = function (id) { + if (typeof this._blocks[id] === 'undefined') return null; + return this._blocks[id].opcode; + }; + + // --------------------------------------------------------------------- + + /** + * Block management: create blocks and stacks from a `create` event + * @param {!Object} block Blockly create event to be processed + */ + Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) { + // Create new block + this._blocks[block.id] = block; + + // Push block id to stacks array. + // Blocks are added as a top-level stack if they are marked as a topBlock + // (if they were top-level XML in the event) and if they are not + // flyout blocks. + if (!opt_isFlyoutBlock && block.topBlock) { + this._addStack(block.id); + } + }; + + /** + * Block management: change block field values + * @param {!Object} args Blockly change event to be processed + */ + Blocks.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; + }; + + /** + * Block management: move blocks from parent to parent + * @param {!Object} e Blockly move event to be processed + */ + Blocks.prototype.moveBlock = function (e) { + // Remove from any old parent. + if (e.oldParent !== undefined) { + var oldParent = this._blocks[e.oldParent]; + if (e.oldInput !== undefined && + oldParent.inputs[e.oldInput].block === e.id) { + // This block was connected to the old parent's input. + oldParent.inputs[e.oldInput].block = null; + } else if (oldParent.next === e.id) { + // This block was connected to the old parent's next connection. + oldParent.next = null; + } + } + + // Has the block become a top-level block? + if (e.newParent === undefined) { + this._addStack(e.id); + } else { + // Remove stack, if one exists. + this._deleteStack(e.id); + // Otherwise, try to connect it in its new place. + if (e.newInput !== undefined) { + // Moved to the new parent's input. + this._blocks[e.newParent].inputs[e.newInput] = { + name: e.newInput, + block: e.id + }; + } else { + // Moved to the new parent's next connection. + this._blocks[e.newParent].next = e.id; + } + } + }; + + /** + * Block management: delete blocks and their associated stacks + * @param {!Object} e Blockly delete event to be processed + */ + Blocks.prototype.deleteBlock = function (e) { + // @todo In runtime, stop threads running on this stack + + // Get block + var block = this._blocks[e.id]; + + // Delete children + if (block.next !== null) { + this.deleteBlock({id: block.next}); + } + + // Delete inputs (including substacks) + for (var input in block.inputs) { + // If it's null, the block in this input moved away. + if (block.inputs[input].block !== null) { + this.deleteBlock({id: block.inputs[input].block}); + } + } + + // Delete stack + this._deleteStack(e.id); + + // Delete block + delete this._blocks[e.id]; + }; + + // --------------------------------------------------------------------- + + /** + * Helper to add a stack to `this._stacks` + * @param {?string} id ID of block that starts the stack + */ + Blocks.prototype._addStack = function (id) { + var i = this._stacks.indexOf(id); + if (i > -1) return; // Already in stacks. + this._stacks.push(id); + // Update `topLevel` property on the top block. + this._blocks[id].topLevel = true; + }; + + /** + * Helper to remove a stack from `this._stacks` + * @param {?string} id ID of block that starts the stack + */ + Blocks.prototype._deleteStack = function (id) { + var i = this._stacks.indexOf(id); + if (i > -1) this._stacks.splice(i, 1); + // Update `topLevel` property on the top block. + if (this._blocks[id]) this._blocks[id].topLevel = false; + }; + + module.exports = Blocks; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 17013576c..9531eadc7 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -10,25 +10,18 @@ var defaultBlockPackages = { /** * Manages blocks, stacks, and the sequencer. + * @param blocks Blocks instance for this runtime. */ -function Runtime () { +function Runtime (blocks) { // Bind event emitter EventEmitter.call(this); // State for the runtime - /** - * All blocks in the workspace. - * Keys are block IDs, values are metadata about the block. - * @type {Object.<string, Object>} - */ - this.blocks = {}; /** - * All stacks in the workspace. - * A list of block IDs that represent stacks (first block in stack). - * @type {Array.<String>} + * Block management and storage */ - this.stacks = []; + this.blocks = blocks; /** * A list of threads that are currently running in the VM. @@ -83,106 +76,6 @@ util.inherits(Runtime, EventEmitter); */ Runtime.THREAD_STEP_INTERVAL = 1000 / 30; -/** - * Block management: create blocks and stacks from a `create` event - * @param {!Object} block Blockly create event to be processed - */ -Runtime.prototype.createBlock = function (block, opt_isFlyoutBlock) { - // Create new block - this.blocks[block.id] = block; - - // Push block id to stacks array. - // Blocks are added as a top-level stack if they are marked as a topBlock - // (if they were top-level XML in the event) and if they are not - // flyout blocks. - if (!opt_isFlyoutBlock && block.topBlock) { - this._addStack(block.id); - } -}; - -/** - * Block management: change block field values - * @param {!Object} args Blockly change event to be processed - */ -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; -}; - -/** - * Block management: move blocks from parent to parent - * @param {!Object} e Blockly move event to be processed - */ -Runtime.prototype.moveBlock = function (e) { - var _this = this; - - // Remove from any old parent. - if (e.oldParent !== undefined) { - var oldParent = _this.blocks[e.oldParent]; - if (e.oldInput !== undefined && - oldParent.inputs[e.oldInput].block === e.id) { - // This block was connected to the old parent's input. - oldParent.inputs[e.oldInput].block = null; - } else if (oldParent.next === e.id) { - // This block was connected to the old parent's next connection. - oldParent.next = null; - } - } - - // Has the block become a top-level block? - if (e.newParent === undefined) { - _this._addStack(e.id); - } else { - // Remove stack, if one exists. - _this._deleteStack(e.id); - // Otherwise, try to connect it in its new place. - if (e.newInput !== undefined) { - // Moved to the new parent's input. - _this.blocks[e.newParent].inputs[e.newInput] = { - name: e.newInput, - block: e.id - }; - } else { - // Moved to the new parent's next connection. - _this.blocks[e.newParent].next = e.id; - } - } -}; - -/** - * Block management: delete blocks and their associated stacks - * @param {!Object} e Blockly delete event to be processed - */ -Runtime.prototype.deleteBlock = function (e) { - // @todo Stop threads running on this stack - - // Get block - var block = this.blocks[e.id]; - - // Delete children - if (block.next !== null) { - this.deleteBlock({id: block.next}); - } - - // Delete inputs (including substacks) - for (var input in block.inputs) { - // If it's null, the block in this input moved away. - if (block.inputs[input].block !== null) { - this.deleteBlock({id: block.inputs[input].block}); - } - } - - // Delete stack - this._deleteStack(e.id); - - // Delete block - delete this.blocks[e.id]; -}; // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- @@ -268,10 +161,11 @@ Runtime.prototype.greenFlag = function () { 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]); + var stacks = this.blocks.getStacks(); + for (var j = 0; j < stacks.length; j++) { + var topBlock = stacks[j]; + if (this.blocks.getBlock(topBlock).opcode === 'event_whenflagclicked') { + this._pushThread(stacks[j]); } } }; @@ -281,9 +175,11 @@ Runtime.prototype.greenFlag = function () { */ Runtime.prototype.startDistanceSensors = function () { // Add all top stacks with distance sensor - for (var j = 0; j < this.stacks.length; j++) { - var topBlock = this.stacks[j]; - if (this.blocks[topBlock].opcode === 'wedo_whendistanceclose') { + var stacks = this.blocks.getStacks(); + for (var j = 0; j < stacks.length; j++) { + var topBlock = stacks[j]; + if (this.blocks.getBlock(topBlock).opcode === + 'wedo_whendistanceclose') { var alreadyRunning = false; for (var k = 0; k < this.threads.length; k++) { if (this.threads[k].topBlock === topBlock) { @@ -291,7 +187,7 @@ Runtime.prototype.startDistanceSensors = function () { } } if (!alreadyRunning) { - this._pushThread(this.stacks[j]); + this._pushThread(stacks[j]); } } } @@ -345,60 +241,4 @@ Runtime.prototype.start = function () { }.bind(this), Runtime.THREAD_STEP_INTERVAL); }; -// ----------------------------------------------------------------------------- -// ----------------------------------------------------------------------------- - -/** - * Helper to add a stack to `this.stacks` - * @param {?string} id ID of block that starts the stack - */ -Runtime.prototype._addStack = function (id) { - var i = this.stacks.indexOf(id); - if (i > -1) return; // Already in stacks. - this.stacks.push(id); - // Update `topLevel` property on the top block. - this.blocks[id].topLevel = true; -}; - -/** - * Helper to remove a stack from `this.stacks` - * @param {?string} id ID of block that starts the stack - */ -Runtime.prototype._deleteStack = function (id) { - var i = this.stacks.indexOf(id); - if (i > -1) this.stacks.splice(i, 1); - // Update `topLevel` property on the top block. - if (this.blocks[id]) this.blocks[id].topLevel = false; -}; - -/** - * 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 - */ -Runtime.prototype._getNextBlock = function (id) { - if (typeof this.blocks[id] === 'undefined') return null; - return this.blocks[id].next; -}; - -/** - * 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 - */ -Runtime.prototype._getSubstack = function (id) { - if (typeof this.blocks[id] === 'undefined') return null; - return this.blocks[id].fields['SUBSTACK']; -}; - -/** - * 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; -}; - module.exports = Runtime; diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index f495264c8..95a28f7bf 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -97,13 +97,13 @@ Sequencer.prototype.stepThread = function (thread) { // If the primitive would like to do control flow, // it can overwrite nextBlock. var currentBlock = thread.nextBlock; - if (!currentBlock || !this.runtime.blocks[currentBlock]) { + if (!currentBlock || !this.runtime.blocks.getBlock(currentBlock)) { thread.status = Thread.STATUS_DONE; return; } - thread.nextBlock = this.runtime._getNextBlock(currentBlock); + thread.nextBlock = this.runtime.blocks.getNextBlock(currentBlock); - var opcode = this.runtime._getOpcode(currentBlock); + var opcode = this.runtime.blocks.getOpcode(currentBlock); // Push the current block to the stack thread.stack.push(currentBlock); @@ -130,7 +130,7 @@ Sequencer.prototype.stepThread = function (thread) { var threadDoneCallback = function () { thread.status = Thread.STATUS_DONE; // Refresh nextBlock in case it has changed during a yield. - thread.nextBlock = instance.runtime._getNextBlock(currentBlock); + thread.nextBlock = instance.runtime.blocks.getNextBlock(currentBlock); // Pop the stack and stack frame thread.stack.pop(); thread.stackFrames.pop(); @@ -141,9 +141,10 @@ Sequencer.prototype.stepThread = function (thread) { * @todo very hacked... */ var startHats = function(callback) { - for (var i = 0; i < instance.runtime.stacks.length; i++) { - var stack = instance.runtime.stacks[i]; - var stackBlock = instance.runtime.blocks[stack]; + var stacks = instance.runtime.blocks.getStacks(); + for (var i = 0; i < stacks.length; i++) { + var stack = stacks[i]; + var stackBlock = instance.runtime.blocks.getBlock(stack); var result = callback(stackBlock); if (result) { // Check if the stack is already running @@ -174,7 +175,7 @@ Sequencer.prototype.stepThread = function (thread) { */ var threadStartSubstack = function () { // Set nextBlock to the start of the substack - var substack = instance.runtime._getSubstack(currentBlock); + var substack = instance.runtime.blocks.getSubstack(currentBlock); if (substack && substack.value) { thread.nextBlock = substack.value; } else { @@ -185,7 +186,7 @@ Sequencer.prototype.stepThread = function (thread) { // @todo extreme hack to get the single argument value for prototype var argValues = []; - var blockInputs = this.runtime.blocks[currentBlock].fields; + var blockInputs = this.runtime.blocks.getBlock(currentBlock).fields; for (var bi in blockInputs) { var outer = blockInputs[bi]; for (var b in outer.blocks) { diff --git a/src/index.js b/src/index.js index f8e7e1ca4..ce147657c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ var EventEmitter = require('events'); var util = require('util'); +var Blocks = require('./engine/Blocks'); var Runtime = require('./engine/runtime'); var adapter = require('./engine/adapter'); @@ -15,7 +16,8 @@ function VirtualMachine () { // Bind event emitter and runtime to VM instance // @todo Post message (Web Worker) polyfill EventEmitter.call(instance); - instance.runtime = new Runtime(); + instance.blocks = new Blocks(); + instance.runtime = new Runtime(instance.blocks); /** * Event listener for blocks. Handles validation and serves as a generic @@ -34,11 +36,11 @@ function VirtualMachine () { var newBlocks = adapter(e); // A create event can create many blocks. Add them all. for (var i = 0; i < newBlocks.length; i++) { - instance.runtime.createBlock(newBlocks[i], false); + instance.blocks.createBlock(newBlocks[i], false); } break; case 'change': - instance.runtime.changeBlock({ + instance.blocks.changeBlock({ id: e.blockId, element: e.element, name: e.name, @@ -46,7 +48,7 @@ function VirtualMachine () { }); break; case 'move': - instance.runtime.moveBlock({ + instance.blocks.moveBlock({ id: e.blockId, oldParent: e.oldParentId, oldInput: e.oldInputName, @@ -55,7 +57,7 @@ function VirtualMachine () { }); break; case 'delete': - instance.runtime.deleteBlock({ + instance.blocks.deleteBlock({ id: e.blockId }); break;