mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 06:23:37 -05:00
Mutations in block representation; an unfeatured procedure call (#212)
* Add scratch3_procedures and no-op for defnoreturn * Add mutation adapter to parse mutations in CREATE/CHANGE events * Add mutation-to-XML * Update spec map for Blockly procedure names * Placeholder for procedure special cases * Basic stepping to procedures * Remove extra case * Validation for changeBlock
This commit is contained in:
parent
dd624aea06
commit
0a66c62f6a
9 changed files with 171 additions and 8 deletions
32
src/blocks/scratch3_procedures.js
Normal file
32
src/blocks/scratch3_procedures.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
function Scratch3ProcedureBlocks(runtime) {
|
||||||
|
/**
|
||||||
|
* The runtime instantiating this block package.
|
||||||
|
* @type {Runtime}
|
||||||
|
*/
|
||||||
|
this.runtime = runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the block primitives implemented by this package.
|
||||||
|
* @return {Object.<string, Function>} Mapping of opcode to Function.
|
||||||
|
*/
|
||||||
|
Scratch3ProcedureBlocks.prototype.getPrimitives = function() {
|
||||||
|
return {
|
||||||
|
'procedures_defnoreturn': this.defNoReturn,
|
||||||
|
'procedures_callnoreturn': this.callNoReturn
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Scratch3ProcedureBlocks.prototype.defNoReturn = function () {
|
||||||
|
// No-op: execute the blocks.
|
||||||
|
};
|
||||||
|
|
||||||
|
Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) {
|
||||||
|
if (!util.stackFrame.executed) {
|
||||||
|
var procedureName = args.mutation.name;
|
||||||
|
util.stackFrame.executed = true;
|
||||||
|
util.startProcedure(procedureName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Scratch3ProcedureBlocks;
|
|
@ -1,3 +1,4 @@
|
||||||
|
var mutationAdapter = require('./mutation-adapter');
|
||||||
var html = require('htmlparser2');
|
var html = require('htmlparser2');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,6 +139,9 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
|
||||||
// Link next block to this block.
|
// Link next block to this block.
|
||||||
block.next = childBlockNode.attribs.id;
|
block.next = childBlockNode.attribs.id;
|
||||||
break;
|
break;
|
||||||
|
case 'mutation':
|
||||||
|
block.mutation = mutationAdapter(xmlChild);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
var adapter = require('./adapter');
|
var adapter = require('./adapter');
|
||||||
|
var mutationAdapter = require('./mutation-adapter');
|
||||||
var xmlEscape = require('../util/xml-escape');
|
var xmlEscape = require('../util/xml-escape');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -116,6 +117,16 @@ Blocks.prototype.getInputs = function (id) {
|
||||||
return inputs;
|
return inputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mutation data for a block.
|
||||||
|
* @param {?string} id ID of block to query.
|
||||||
|
* @return {!Object} Mutation for the block.
|
||||||
|
*/
|
||||||
|
Blocks.prototype.getMutation = function (id) {
|
||||||
|
if (typeof this._blocks[id] === 'undefined') return null;
|
||||||
|
return this._blocks[id].mutation;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the top-level script for a given block.
|
* Get the top-level script for a given block.
|
||||||
* @param {?string} id ID of block to query.
|
* @param {?string} id ID of block to query.
|
||||||
|
@ -130,6 +141,23 @@ Blocks.prototype.getTopLevelScript = function (id) {
|
||||||
return block.id;
|
return block.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the procedure definition for a given name.
|
||||||
|
* @param {?string} name Name of procedure to query.
|
||||||
|
* @return {?string} ID of procedure definition.
|
||||||
|
*/
|
||||||
|
Blocks.prototype.getProcedureDefinition = function (name) {
|
||||||
|
for (var id in this._blocks) {
|
||||||
|
var block = this._blocks[id];
|
||||||
|
if ((block.opcode == 'procedures_defnoreturn' ||
|
||||||
|
block.opcode == 'procedures_defreturn') &&
|
||||||
|
block.fields['NAME'].value == name) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,12 +254,16 @@ Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) {
|
||||||
*/
|
*/
|
||||||
Blocks.prototype.changeBlock = function (args) {
|
Blocks.prototype.changeBlock = function (args) {
|
||||||
// Validate
|
// Validate
|
||||||
if (args.element !== 'field') return;
|
if (args.element !== 'field' && args.element !== 'mutation') return;
|
||||||
if (typeof this._blocks[args.id] === 'undefined') return;
|
if (typeof this._blocks[args.id] === 'undefined') return;
|
||||||
if (typeof this._blocks[args.id].fields[args.name] === 'undefined') return;
|
|
||||||
|
|
||||||
// Update block value
|
if (args.element == 'field') {
|
||||||
this._blocks[args.id].fields[args.name].value = args.value;
|
// Update block value
|
||||||
|
if (!this._blocks[args.id].fields[args.name]) return;
|
||||||
|
this._blocks[args.id].fields[args.name].value = args.value;
|
||||||
|
} else if (args.element == 'mutation') {
|
||||||
|
this._blocks[args.id].mutation = mutationAdapter(args.value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -355,6 +387,10 @@ Blocks.prototype.blockToXML = function (blockId) {
|
||||||
' type="' + block.opcode + '"' +
|
' type="' + block.opcode + '"' +
|
||||||
xy +
|
xy +
|
||||||
'>';
|
'>';
|
||||||
|
// Add any mutation. Must come before inputs.
|
||||||
|
if (block.mutation) {
|
||||||
|
xmlString += this.mutationToXML(block.mutation);
|
||||||
|
}
|
||||||
// Add any inputs on this block.
|
// Add any inputs on this block.
|
||||||
for (var input in block.inputs) {
|
for (var input in block.inputs) {
|
||||||
var blockInput = block.inputs[input];
|
var blockInput = block.inputs[input];
|
||||||
|
@ -389,6 +425,25 @@ Blocks.prototype.blockToXML = function (blockId) {
|
||||||
return xmlString;
|
return xmlString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively encode a mutation object to XML.
|
||||||
|
* @param {!Object} mutation Object representing a mutation.
|
||||||
|
* @return {string} XML string representing a mutation.
|
||||||
|
*/
|
||||||
|
Blocks.prototype.mutationToXML = function (mutation) {
|
||||||
|
var mutationString = '<' + mutation.tagName;
|
||||||
|
for (var prop in mutation) {
|
||||||
|
if (prop == 'children' || prop == 'tagName') continue;
|
||||||
|
mutationString += ' ' + prop + '="' + mutation[prop] + '"';
|
||||||
|
}
|
||||||
|
mutationString += '>';
|
||||||
|
for (var i = 0; i < mutation.children.length; i++) {
|
||||||
|
mutationString += this.mutationToXML(mutation.children[i]);
|
||||||
|
}
|
||||||
|
mutationString += '</' + mutation.tagName + '>';
|
||||||
|
return mutationString;
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -132,6 +132,12 @@ var execute = function (sequencer, thread) {
|
||||||
argValues[inputName] = currentStackFrame.reported[inputName];
|
argValues[inputName] = currentStackFrame.reported[inputName];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add any mutation to args (e.g., for procedures).
|
||||||
|
var mutation = target.blocks.getMutation(currentBlockId);
|
||||||
|
if (mutation) {
|
||||||
|
argValues.mutation = mutation;
|
||||||
|
}
|
||||||
|
|
||||||
// If we've gotten this far, all of the input blocks are evaluated,
|
// If we've gotten this far, all of the input blocks are evaluated,
|
||||||
// and `argValues` is fully populated. So, execute the block primitive.
|
// and `argValues` is fully populated. So, execute the block primitive.
|
||||||
// First, clear `currentStackFrame.reported`, so any subsequent execution
|
// First, clear `currentStackFrame.reported`, so any subsequent execution
|
||||||
|
@ -155,6 +161,9 @@ var execute = function (sequencer, thread) {
|
||||||
startBranch: function (branchNum) {
|
startBranch: function (branchNum) {
|
||||||
sequencer.stepToBranch(thread, branchNum);
|
sequencer.stepToBranch(thread, branchNum);
|
||||||
},
|
},
|
||||||
|
startProcedure: function (procedureName) {
|
||||||
|
sequencer.stepToProcedure(thread, procedureName);
|
||||||
|
},
|
||||||
startHats: function(requestedHat, opt_matchFields, opt_target) {
|
startHats: function(requestedHat, opt_matchFields, opt_target) {
|
||||||
return (
|
return (
|
||||||
runtime.startHats(requestedHat, opt_matchFields, opt_target)
|
runtime.startHats(requestedHat, opt_matchFields, opt_target)
|
||||||
|
|
39
src/engine/mutation-adapter.js
Normal file
39
src/engine/mutation-adapter.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
var html = require('htmlparser2');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter between mutator XML or DOM and block representation which can be
|
||||||
|
* used by the Scratch runtime.
|
||||||
|
* @param {(Object|string)} mutation Mutation XML string or DOM.
|
||||||
|
* @return {Object} Object representing the mutation.
|
||||||
|
*/
|
||||||
|
module.exports = function (mutation) {
|
||||||
|
var mutationParsed;
|
||||||
|
// Check if the mutation is already parsed; if not, parse it.
|
||||||
|
if (typeof mutation === 'object') {
|
||||||
|
mutationParsed = mutation;
|
||||||
|
} else {
|
||||||
|
mutationParsed = html.parseDOM(mutation)[0];
|
||||||
|
}
|
||||||
|
return mutatorTagToObject(mutationParsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a part of a mutation DOM to a mutation VM object, recursively.
|
||||||
|
* @param {Object} dom DOM object for mutation tag.
|
||||||
|
* @return {Object} Object representing useful parts of this mutation.
|
||||||
|
*/
|
||||||
|
function mutatorTagToObject (dom) {
|
||||||
|
var obj = Object.create(null);
|
||||||
|
obj.tagName = dom.name;
|
||||||
|
obj.children = [];
|
||||||
|
for (var prop in dom.attribs) {
|
||||||
|
if (prop == 'xmlns') continue;
|
||||||
|
obj[prop] = dom.attribs[prop];
|
||||||
|
}
|
||||||
|
for (var i = 0; i < dom.children.length; i++) {
|
||||||
|
obj.children.push(
|
||||||
|
mutatorTagToObject(dom.children[i])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
|
@ -15,7 +15,8 @@ var defaultBlockPackages = {
|
||||||
'scratch3_motion': require('../blocks/scratch3_motion'),
|
'scratch3_motion': require('../blocks/scratch3_motion'),
|
||||||
'scratch3_operators': require('../blocks/scratch3_operators'),
|
'scratch3_operators': require('../blocks/scratch3_operators'),
|
||||||
'scratch3_sensing': require('../blocks/scratch3_sensing'),
|
'scratch3_sensing': require('../blocks/scratch3_sensing'),
|
||||||
'scratch3_data': require('../blocks/scratch3_data')
|
'scratch3_data': require('../blocks/scratch3_data'),
|
||||||
|
'scratch3_procedures': require('../blocks/scratch3_procedures')
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -124,6 +124,16 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step a procedure.
|
||||||
|
* @param {!Thread} thread Thread object to step to procedure.
|
||||||
|
* @param {!string} procedureName Name of procedure defined in this target.
|
||||||
|
*/
|
||||||
|
Sequencer.prototype.stepToProcedure = function (thread, procedureName) {
|
||||||
|
var definition = thread.target.blocks.getProcedureDefinition(procedureName);
|
||||||
|
thread.pushStack(definition);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step a thread into an input reporter, and manage its status appropriately.
|
* Step a thread into an input reporter, and manage its status appropriately.
|
||||||
* @param {!Thread} thread Thread object to step to reporter.
|
* @param {!Thread} thread Thread object to step to reporter.
|
||||||
|
|
|
@ -327,6 +327,14 @@ function parseBlock (sb2block) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Special cases to generate mutations.
|
||||||
|
if (oldOpcode == 'call') {
|
||||||
|
activeBlock.mutation = {
|
||||||
|
tagName: 'mutation',
|
||||||
|
children: [],
|
||||||
|
name: sb2block[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
return activeBlock;
|
return activeBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1374,15 +1374,20 @@ var specMap = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
'procDef':{
|
'procDef':{
|
||||||
'opcode':'proc_def',
|
'opcode':'procedures_defnoreturn',
|
||||||
'argMap':[]
|
'argMap':[
|
||||||
|
{
|
||||||
|
'type':'field',
|
||||||
|
'fieldName':'NAME'
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
'getParam':{
|
'getParam':{
|
||||||
'opcode':'proc_param',
|
'opcode':'proc_param',
|
||||||
'argMap':[]
|
'argMap':[]
|
||||||
},
|
},
|
||||||
'call':{
|
'call':{
|
||||||
'opcode':'proc_call',
|
'opcode':'procedures_callnoreturn',
|
||||||
'argMap':[]
|
'argMap':[]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue