mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-06-08 11:34:40 -04:00
Refactor block management into a separate module.
This commit is contained in:
parent
25c3a38c16
commit
9be33089a8
4 changed files with 231 additions and 189 deletions
199
src/engine/blocks.js
Normal file
199
src/engine/blocks.js
Normal file
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
12
src/index.js
12
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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue