mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-05-17 17:01:02 -04:00
commit
852690d7cb
27 changed files with 15414 additions and 26 deletions
18
.eslintrc
Normal file
18
.eslintrc
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"rules": {
|
||||
"curly": [2, "multi-line"],
|
||||
"eol-last": [2],
|
||||
"indent": [2, 4],
|
||||
"quotes": [2, "single"],
|
||||
"linebreak-style": [2, "unix"],
|
||||
"max-len": [2, 80, 4],
|
||||
"semi": [2, "always"],
|
||||
"strict": [2, "never"],
|
||||
"no-console": [2, {"allow": ["log", "warn", "error"]}]
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true
|
||||
},
|
||||
"extends": "eslint:recommended"
|
||||
}
|
5
.npmrc
5
.npmrc
|
@ -1,5 +0,0 @@
|
|||
engine-strict=true
|
||||
save-exact=true
|
||||
save-prefix=~
|
||||
init-license=BSD-3-Clause
|
||||
init-author-name=Massachusetts Institute of Technology
|
8
.travis.yml
Normal file
8
.travis.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "4"
|
||||
- "stable"
|
||||
sudo: false
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
18
Makefile
18
Makefile
|
@ -1,12 +1,21 @@
|
|||
ESLINT=./node_modules/.bin/eslint
|
||||
NODE=node
|
||||
TAP=./node_modules/.bin/tap
|
||||
WEBPACK=./node_modules/.bin/webpack --progress --colors
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
build:
|
||||
$(WEBPACK)
|
||||
|
||||
watch:
|
||||
$(WEBPACK) --watch
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
lint:
|
||||
$(ESLINT) ./*.js
|
||||
$(ESLINT) ./lib/*.js
|
||||
$(ESLINT) ./src/*.js
|
||||
$(ESLINT) ./src/**/*.js
|
||||
$(ESLINT) ./test/**/*.js
|
||||
|
||||
test:
|
||||
|
@ -16,9 +25,6 @@ test:
|
|||
coverage:
|
||||
$(TAP) ./test/{unit,integration}/*.js --coverage --coverage-report=lcov
|
||||
|
||||
benchmark:
|
||||
$(NODE) ./test/benchmark/performance.js
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
.PHONY: lint test coverage benchmark
|
||||
.PHONY: build lint test coverage benchmark
|
||||
|
|
78
README.md
78
README.md
|
@ -1,13 +1,84 @@
|
|||
## scratch-vm
|
||||
#### Scratch VM is a library for representing, running, and maintaining the state of computer programs written using [Scratch Blocks](https://github.com/LLK/scratch-blocks).
|
||||
|
||||
[](https://travis-ci.com/LLK/scratch-vm)
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
npm install scratch-vm
|
||||
```
|
||||
|
||||
## Integration
|
||||
## Setup
|
||||
```js
|
||||
var VirtualMachine = require('scratch-vm');
|
||||
var vm = new VirtualMachine();
|
||||
|
||||
// Block events
|
||||
workspace.addChangeListener(function(e) {
|
||||
// Handle "tapping" a block
|
||||
if (e instanceof Blockly.Events.Ui && e.element === 'click') {
|
||||
var stackBlock = workspace.getBlockById(e.blockId).getRootBlock().id;
|
||||
vm.runtime.toggleStack(stackBlock);
|
||||
// Otherwise, pass along to the block listener
|
||||
} else {
|
||||
vm.blockListener(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Run threads
|
||||
vm.runtime.start();
|
||||
```
|
||||
|
||||
## Standalone Build
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
```html
|
||||
<script src="/path/to/vm.js"></script>
|
||||
<script>
|
||||
var vm = new window.VirtualMachine();
|
||||
// do things
|
||||
</script>
|
||||
```
|
||||
|
||||
## Abstract Syntax Tree
|
||||
|
||||
#### Overview
|
||||
The Virtual Machine constructs and maintains the state of an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST) by listening to events emitted by the [scratch-blocks](https://github.com/LLK/scratch-blocks) workspace via the `blockListener`. At any time, the current state of the AST can be viewed by inspecting the `vm.runtime.blocks` object.
|
||||
|
||||
#### Anatomy of a Block
|
||||
```json
|
||||
{
|
||||
"id": "^1r~63Gdl7;Dh?I*OP3_",
|
||||
"opcode": "wedo_motorclockwise",
|
||||
"next": null,
|
||||
"fields": {
|
||||
"DURATION": {
|
||||
"name": "DURATION",
|
||||
"value": null,
|
||||
"blocks": {
|
||||
"1?P=eV(OiDY3vMk!24Ip": {
|
||||
"id": "1?P=eV(OiDY3vMk!24Ip",
|
||||
"opcode": "math_number",
|
||||
"next": null,
|
||||
"fields": {
|
||||
"NUM": {
|
||||
"name": "NUM",
|
||||
"value": "10",
|
||||
"blocks": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SUBSTACK": {
|
||||
"name": "SUBSTACK",
|
||||
"value": "@1ln(HsUO4!]*2*%BrE|",
|
||||
"blocks": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
@ -19,6 +90,5 @@ make test
|
|||
make coverage
|
||||
```
|
||||
|
||||
```bash
|
||||
make benchmark
|
||||
```
|
||||
## Donation
|
||||
We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, community, and resource development efforts. Donations of any size are appreciated. Thank you!
|
||||
|
|
0
index.js
0
index.js
15
package.json
15
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "scratch-vm",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"version": "0.1.0",
|
||||
"description": "Virtual Machine for Scratch 3.0",
|
||||
"author": "Massachusetts Institute of Technology",
|
||||
"license": "BSD-3-Clause",
|
||||
"homepage": "https://github.com/LLK/scratch-vm#readme",
|
||||
|
@ -9,13 +9,18 @@
|
|||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/LLK/scratch-vm.git"
|
||||
},
|
||||
"main": "index.js",
|
||||
"main": "./src/index.js",
|
||||
"scripts": {
|
||||
"test": "make test"
|
||||
},
|
||||
"dependencies": {
|
||||
"htmlparser2": "3.9.0",
|
||||
"memoizee": "0.3.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"benchmark": "2.1.0",
|
||||
"eslint": "2.7.0",
|
||||
"tap": "5.7.1"
|
||||
"json-loader": "0.5.4",
|
||||
"tap": "5.7.1",
|
||||
"webpack": "1.13.0"
|
||||
}
|
||||
}
|
||||
|
|
82
src/blocks/scratch3.js
Normal file
82
src/blocks/scratch3.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
function Scratch3Blocks(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.
|
||||
*/
|
||||
Scratch3Blocks.prototype.getPrimitives = function() {
|
||||
return {
|
||||
'control_repeat': this.repeat,
|
||||
'control_forever': this.forever,
|
||||
'control_wait': this.wait,
|
||||
'control_stop': this.stop,
|
||||
'event_whenflagclicked': this.whenFlagClicked,
|
||||
'event_whenbroadcastreceived': this.whenBroadcastReceived,
|
||||
'event_broadcast': this.broadcast
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
// Decrease counter
|
||||
util.stackFrame.loopCounter--;
|
||||
// If we still have some left, start the substack
|
||||
if (util.stackFrame.loopCounter >= 0) {
|
||||
util.startSubstack();
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
}, 1000 * parseFloat(argValues[0]));
|
||||
};
|
||||
|
||||
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;
|
||||
for (var sb in shadows) {
|
||||
var shadowblock = shadows[sb];
|
||||
return shadowblock.fields.CHOICE.value === argValues[0];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Scratch3Blocks;
|
154
src/blocks/wedo2.js
Normal file
154
src/blocks/wedo2.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
|
||||
var YieldTimers = require('../util/yieldtimers.js');
|
||||
|
||||
function WeDo2Blocks(runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
/**
|
||||
* Current motor speed, as a percentage (100 = full speed).
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this._motorSpeed = 100;
|
||||
|
||||
/**
|
||||
* The timeout ID for a pending motor action.
|
||||
* @type {?int}
|
||||
* @private
|
||||
*/
|
||||
this._motorTimeout = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {Object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
WeDo2Blocks.prototype.getPrimitives = function() {
|
||||
return {
|
||||
'wedo_motorclockwise': this.motorClockwise,
|
||||
'wedo_motorcounterclockwise': this.motorCounterClockwise,
|
||||
'wedo_motorspeed': this.motorSpeed,
|
||||
'wedo_setcolor': this.setColor,
|
||||
'wedo_whendistanceclose': this.whenDistanceClose,
|
||||
'wedo_whentilt': this.whenTilt
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Clamp a value between a minimum and maximum value.
|
||||
* @todo move this to a common utility class.
|
||||
* @param val The value to clamp.
|
||||
* @param min The minimum return value.
|
||||
* @param max The maximum return value.
|
||||
* @returns {number} The clamped value.
|
||||
* @private
|
||||
*/
|
||||
WeDo2Blocks.prototype._clamp = function(val, min, max) {
|
||||
return Math.max(min, Math.min(val, max));
|
||||
};
|
||||
|
||||
/**
|
||||
* Common implementation for motor blocks.
|
||||
* @param direction The direction to turn ('left' or 'right').
|
||||
* @param durationSeconds The number of seconds to run.
|
||||
* @param util The util instance to use for yielding and finishing.
|
||||
* @private
|
||||
*/
|
||||
WeDo2Blocks.prototype._motorOnFor = function(direction, durationSeconds, util) {
|
||||
if (this._motorTimeout > 0) {
|
||||
// @todo maybe this should go through util
|
||||
YieldTimers.resolve(this._motorTimeout);
|
||||
this._motorTimeout = null;
|
||||
}
|
||||
if (window.native) {
|
||||
window.native.motorRun(direction, this._motorSpeed);
|
||||
}
|
||||
|
||||
var instance = this;
|
||||
var myTimeout = this._motorTimeout = util.timeout(function() {
|
||||
if (instance._motorTimeout == myTimeout) {
|
||||
instance._motorTimeout = null;
|
||||
}
|
||||
if (window.native) {
|
||||
window.native.motorStop();
|
||||
}
|
||||
util.done();
|
||||
}, 1000 * durationSeconds);
|
||||
|
||||
util.yield();
|
||||
};
|
||||
|
||||
WeDo2Blocks.prototype.motorClockwise = function(argValues, util) {
|
||||
this._motorOnFor('right', parseFloat(argValues[0]), util);
|
||||
};
|
||||
|
||||
WeDo2Blocks.prototype.motorCounterClockwise = function(argValues, util) {
|
||||
this._motorOnFor('left', parseFloat(argValues[0]), util);
|
||||
};
|
||||
|
||||
WeDo2Blocks.prototype.motorSpeed = function(argValues) {
|
||||
var speed = argValues[0];
|
||||
switch (speed) {
|
||||
case 'slow':
|
||||
this._motorSpeed = 20;
|
||||
break;
|
||||
case 'medium':
|
||||
this._motorSpeed = 50;
|
||||
break;
|
||||
case 'fast':
|
||||
this._motorSpeed = 100;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a color name to a WeDo color index.
|
||||
* Supports 'mystery' for a random hue.
|
||||
* @param colorName The color to retrieve.
|
||||
* @returns {number} The WeDo color index.
|
||||
* @private
|
||||
*/
|
||||
WeDo2Blocks.prototype._getColor = function(colorName) {
|
||||
var colors = {
|
||||
'yellow': 7,
|
||||
'orange': 8,
|
||||
'coral': 9,
|
||||
'magenta': 1,
|
||||
'purple': 2,
|
||||
'blue': 3,
|
||||
'green': 6,
|
||||
'white': 10
|
||||
};
|
||||
|
||||
if (colorName == 'mystery') {
|
||||
return Math.floor((Math.random() * 10) + 1);
|
||||
}
|
||||
|
||||
return colors[colorName];
|
||||
};
|
||||
|
||||
WeDo2Blocks.prototype.setColor = function(argValues, util) {
|
||||
if (window.native) {
|
||||
var colorIndex = this._getColor(argValues[0]);
|
||||
window.native.setLedColor(colorIndex);
|
||||
}
|
||||
// Pause for quarter second
|
||||
util.yield();
|
||||
util.timeout(function() {
|
||||
util.done();
|
||||
}, 250);
|
||||
};
|
||||
|
||||
WeDo2Blocks.prototype.whenDistanceClose = function() {
|
||||
console.log('Running: wedo_whendistanceclose');
|
||||
};
|
||||
|
||||
WeDo2Blocks.prototype.whenTilt = function() {
|
||||
console.log('Running: wedo_whentilt');
|
||||
};
|
||||
|
||||
module.exports = WeDo2Blocks;
|
87
src/engine/adapter.js
Normal file
87
src/engine/adapter.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
var html = require('htmlparser2');
|
||||
var memoize = require('memoizee');
|
||||
var parseDOM = memoize(html.parseDOM, {
|
||||
length: 1,
|
||||
resolvers: [String],
|
||||
max: 200
|
||||
});
|
||||
|
||||
/**
|
||||
* Adapter between block creation events and block representation which can be
|
||||
* used by the Scratch runtime.
|
||||
*
|
||||
* @param {Object} `Blockly.events.create`
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
module.exports = function (e) {
|
||||
// Validate input
|
||||
if (typeof e !== 'object') return;
|
||||
if (typeof e.blockId !== 'string') return;
|
||||
if (typeof e.xml !== 'object') return;
|
||||
|
||||
// Storage object
|
||||
var obj = {
|
||||
id: e.blockId,
|
||||
opcode: null,
|
||||
next: null,
|
||||
fields: {}
|
||||
};
|
||||
|
||||
// Set opcode
|
||||
if (typeof e.xml.attributes === 'object') {
|
||||
obj.opcode = e.xml.attributes.type.value;
|
||||
}
|
||||
|
||||
// Extract fields from event's `innerHTML`
|
||||
if (typeof e.xml.innerHTML !== 'string') return obj;
|
||||
if (e.xml.innerHTML === '') return obj;
|
||||
obj.fields = extract(parseDOM(e.xml.innerHTML));
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts fields from a block's innerHTML.
|
||||
* @todo Extend this to support vertical grammar / nested blocks.
|
||||
*
|
||||
* @param {Object} DOM representation of block's innerHTML
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function extract (dom) {
|
||||
// Storage object
|
||||
var fields = {};
|
||||
|
||||
// Field
|
||||
var field = dom[0];
|
||||
var fieldName = field.attribs.name;
|
||||
fields[fieldName] = {
|
||||
name: fieldName,
|
||||
value: null,
|
||||
blocks: {}
|
||||
};
|
||||
|
||||
// Shadow block
|
||||
var shadow = field.children[0];
|
||||
var shadowId = shadow.attribs.id;
|
||||
var shadowOpcode = shadow.attribs.type;
|
||||
fields[fieldName].blocks[shadowId] = {
|
||||
id: shadowId,
|
||||
opcode: shadowOpcode,
|
||||
next: null,
|
||||
fields: {}
|
||||
};
|
||||
|
||||
// Primitive
|
||||
var primitive = shadow.children[0];
|
||||
var primitiveName = primitive.attribs.name;
|
||||
var primitiveValue = primitive.children[0].data;
|
||||
fields[fieldName].blocks[shadowId].fields[primitiveName] = {
|
||||
name: primitiveName,
|
||||
value: primitiveValue,
|
||||
blocks: null
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
402
src/engine/runtime.js
Normal file
402
src/engine/runtime.js
Normal file
|
@ -0,0 +1,402 @@
|
|||
var EventEmitter = require('events');
|
||||
var Sequencer = require('./sequencer');
|
||||
var Thread = require('./thread');
|
||||
var util = require('util');
|
||||
|
||||
var defaultBlockPackages = {
|
||||
'scratch3': require('../blocks/scratch3'),
|
||||
'wedo2': require('../blocks/wedo2')
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages blocks, stacks, and the sequencer.
|
||||
*/
|
||||
function Runtime () {
|
||||
// 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>}
|
||||
*/
|
||||
this.stacks = [];
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
/** @type {!Sequencer} */
|
||||
this.sequencer = new Sequencer(this);
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Inherit from EventEmitter
|
||||
*/
|
||||
util.inherits(Runtime, EventEmitter);
|
||||
|
||||
/**
|
||||
* How rapidly we try to step threads, in ms.
|
||||
*/
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (!opt_isFlyoutBlock) {
|
||||
this.stacks.push(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;
|
||||
|
||||
// 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.
|
||||
|
||||
// Remove stack
|
||||
_this._deleteStack(e.id);
|
||||
|
||||
// Update new parent
|
||||
if (e.newField === undefined) {
|
||||
_this.blocks[e.newParent].next = e.id;
|
||||
} else {
|
||||
_this.blocks[e.newParent].fields[e.newField] = {
|
||||
name: e.newField,
|
||||
value: e.id,
|
||||
blocks: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete stack
|
||||
this._deleteStack(e.id);
|
||||
|
||||
// Delete block
|
||||
delete this.blocks[e.id];
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
// @todo pass a different runtime depending on package privilege?
|
||||
var packageObject = new (defaultBlockPackages[packageName])(this);
|
||||
var packageContents = packageObject.getPrimitives();
|
||||
for (var op in packageContents) {
|
||||
if (packageContents.hasOwnProperty(op)) {
|
||||
this._primitives[op] =
|
||||
packageContents[op].bind(packageObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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];
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
this.emit(Runtime.STACK_GLOW_ON, id);
|
||||
var thread = new Thread(id);
|
||||
this.threads.push(thread);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a thread from the list of threads.
|
||||
* @param {?Thread} thread Thread object to remove from actives
|
||||
*/
|
||||
Runtime.prototype._removeThread = function (thread) {
|
||||
var i = this.threads.indexOf(thread);
|
||||
if (i > -1) {
|
||||
this.emit(Runtime.STACK_GLOW_OFF, thread.topBlock);
|
||||
this.threads.splice(i, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Otherwise add it
|
||||
this._pushThread(stackId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Distance sensor hack
|
||||
*/
|
||||
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 alreadyRunning = false;
|
||||
for (var k = 0; k < this.threads.length; k++) {
|
||||
if (this.threads[k].topBlock === topBlock) {
|
||||
alreadyRunning = true;
|
||||
}
|
||||
}
|
||||
if (!alreadyRunning) {
|
||||
this._pushThread(this.stacks[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop "everything"
|
||||
*/
|
||||
Runtime.prototype.stopAll = function () {
|
||||
var threadsCopy = this.threads.slice();
|
||||
while (threadsCopy.length > 0) {
|
||||
this._removeThread(threadsCopy.pop());
|
||||
}
|
||||
// @todo call stop function in all extensions/packages/WeDo stub
|
||||
if (window.native) {
|
||||
window.native.motorStop();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
241
src/engine/sequencer.js
Normal file
241
src/engine/sequencer.js
Normal file
|
@ -0,0 +1,241 @@
|
|||
var Timer = require('../util/timer');
|
||||
var Thread = require('./thread');
|
||||
var YieldTimers = require('../util/yieldtimers.js');
|
||||
|
||||
function Sequencer (runtime) {
|
||||
/**
|
||||
* A utility timer for timing thread sequencing.
|
||||
* @type {!Timer}
|
||||
*/
|
||||
this.timer = new Timer();
|
||||
|
||||
/**
|
||||
* Reference to the runtime owning this sequencer.
|
||||
* @type {!Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sequencer does as much work as it can within WORK_TIME milliseconds,
|
||||
* then yields. This is essentially a rate-limiter for blocks.
|
||||
* In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
|
||||
* @const {!number}
|
||||
*/
|
||||
Sequencer.WORK_TIME = 10;
|
||||
|
||||
/**
|
||||
* Step through all threads in `this.threads`, running them in order.
|
||||
* @return {Array.<Thread>} All threads which have finished in this iteration.
|
||||
*/
|
||||
Sequencer.prototype.stepThreads = function (threads) {
|
||||
// Start counting toward WORK_TIME
|
||||
this.timer.start();
|
||||
// List of threads which have been killed by this step.
|
||||
var inactiveThreads = [];
|
||||
// If all of the threads are yielding, we should yield.
|
||||
var numYieldingThreads = 0;
|
||||
// While there are still threads to run and we are within WORK_TIME,
|
||||
// continue executing threads.
|
||||
while (threads.length > 0 &&
|
||||
threads.length > numYieldingThreads &&
|
||||
this.timer.timeElapsed() < Sequencer.WORK_TIME) {
|
||||
// New threads at the end of the iteration.
|
||||
var newThreads = [];
|
||||
// Attempt to run each thread one time
|
||||
for (var i = 0; i < threads.length; i++) {
|
||||
var activeThread = threads[i];
|
||||
if (activeThread.status === Thread.STATUS_RUNNING) {
|
||||
// Normal-mode thread: step.
|
||||
this.stepThread(activeThread);
|
||||
} else if (activeThread.status === Thread.STATUS_YIELD) {
|
||||
// Yield-mode thread: check if the time has passed.
|
||||
YieldTimers.resolve(activeThread.yieldTimerId);
|
||||
numYieldingThreads++;
|
||||
} else if (activeThread.status === Thread.STATUS_DONE) {
|
||||
// Moved to a done state - finish up
|
||||
activeThread.status = Thread.STATUS_RUNNING;
|
||||
// @todo Deal with the return value
|
||||
}
|
||||
// First attempt to pop from the stack
|
||||
if (activeThread.stack.length > 0 &&
|
||||
activeThread.nextBlock === null &&
|
||||
activeThread.status === Thread.STATUS_DONE) {
|
||||
activeThread.nextBlock = activeThread.stack.pop();
|
||||
// Don't pop stack frame - we need the data.
|
||||
// A new one won't be created when we execute.
|
||||
if (activeThread.nextBlock !== null) {
|
||||
activeThread.status === Thread.STATUS_RUNNING;
|
||||
}
|
||||
}
|
||||
if (activeThread.nextBlock === null &&
|
||||
activeThread.status === Thread.STATUS_DONE) {
|
||||
// Finished with this thread - tell runtime to clean it up.
|
||||
inactiveThreads.push(activeThread);
|
||||
} else {
|
||||
// Keep this thead in the loop.
|
||||
newThreads.push(activeThread);
|
||||
}
|
||||
}
|
||||
// Effectively filters out threads that have stopped.
|
||||
threads = newThreads;
|
||||
}
|
||||
return inactiveThreads;
|
||||
};
|
||||
|
||||
/**
|
||||
* Step the requested thread
|
||||
* @param {!Thread} thread Thread object to step
|
||||
*/
|
||||
Sequencer.prototype.stepThread = function (thread) {
|
||||
// Save the yield timer ID, in case a primitive makes a new one
|
||||
// @todo hack - perhaps patch this to allow more than one timer per
|
||||
// primitive, for example...
|
||||
var oldYieldTimerId = YieldTimers.timerId;
|
||||
|
||||
// Save the current block and set the nextBlock.
|
||||
// If the primitive would like to do control flow,
|
||||
// it can overwrite nextBlock.
|
||||
var currentBlock = thread.nextBlock;
|
||||
if (!currentBlock || !this.runtime.blocks[currentBlock]) {
|
||||
thread.status = Thread.STATUS_DONE;
|
||||
return;
|
||||
}
|
||||
thread.nextBlock = this.runtime._getNextBlock(currentBlock);
|
||||
|
||||
var opcode = this.runtime._getOpcode(currentBlock);
|
||||
|
||||
// Push the current block to the stack
|
||||
thread.stack.push(currentBlock);
|
||||
// Push an empty stack frame, if we need one.
|
||||
// Might not, if we just popped the stack.
|
||||
if (thread.stack.length > thread.stackFrames.length) {
|
||||
thread.stackFrames.push({});
|
||||
}
|
||||
var currentStackFrame = thread.stackFrames[thread.stackFrames.length - 1];
|
||||
|
||||
/**
|
||||
* A callback for the primitive to indicate its thread should yield.
|
||||
* @type {Function}
|
||||
*/
|
||||
var threadYieldCallback = function () {
|
||||
thread.status = Thread.STATUS_YIELD;
|
||||
};
|
||||
|
||||
/**
|
||||
* A callback for the primitive to indicate its thread is finished
|
||||
* @type {Function}
|
||||
*/
|
||||
var instance = this;
|
||||
var threadDoneCallback = function () {
|
||||
thread.status = Thread.STATUS_DONE;
|
||||
// Refresh nextBlock in case it has changed during a yield.
|
||||
thread.nextBlock = instance.runtime._getNextBlock(currentBlock);
|
||||
// Pop the stack and stack frame
|
||||
thread.stack.pop();
|
||||
thread.stackFrames.pop();
|
||||
};
|
||||
|
||||
/**
|
||||
* A callback for the primitive to start hats.
|
||||
* @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 result = callback(stackBlock);
|
||||
if (result) {
|
||||
// Check if the stack is already running
|
||||
var stackRunning = false;
|
||||
|
||||
for (var j = 0; j < instance.runtime.threads.length; j++) {
|
||||
if (instance.runtime.threads[j].topBlock == stack) {
|
||||
stackRunning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stackRunning) {
|
||||
instance.runtime._pushThread(stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Record whether we have switched stack,
|
||||
* to avoid proceeding the thread automatically.
|
||||
* @type {boolean}
|
||||
*/
|
||||
var switchedStack = false;
|
||||
/**
|
||||
* A callback for a primitive to start a substack.
|
||||
* @type {Function}
|
||||
*/
|
||||
var threadStartSubstack = function () {
|
||||
// Set nextBlock to the start of the substack
|
||||
var substack = instance.runtime._getSubstack(currentBlock);
|
||||
if (substack && substack.value) {
|
||||
thread.nextBlock = substack.value;
|
||||
} else {
|
||||
thread.nextBlock = null;
|
||||
}
|
||||
switchedStack = true;
|
||||
};
|
||||
|
||||
// @todo extreme hack to get the single argument value for prototype
|
||||
var argValues = [];
|
||||
var blockInputs = this.runtime.blocks[currentBlock].fields;
|
||||
for (var bi in blockInputs) {
|
||||
var outer = blockInputs[bi];
|
||||
for (var b in outer.blocks) {
|
||||
var block = outer.blocks[b];
|
||||
var fields = block.fields;
|
||||
for (var f in fields) {
|
||||
var field = fields[f];
|
||||
argValues.push(field.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!opcode) {
|
||||
console.warn('Could not get opcode for block: ' + currentBlock);
|
||||
}
|
||||
else {
|
||||
var blockFunction = this.runtime.getOpcodeFunction(opcode);
|
||||
if (!blockFunction) {
|
||||
console.warn('Could not get implementation for opcode: ' + opcode);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
// @todo deal with the return value
|
||||
blockFunction(argValues, {
|
||||
yield: threadYieldCallback,
|
||||
done: threadDoneCallback,
|
||||
timeout: YieldTimers.timeout,
|
||||
stackFrame: currentStackFrame,
|
||||
startSubstack: threadStartSubstack,
|
||||
startHats: startHats
|
||||
});
|
||||
}
|
||||
catch(e) {
|
||||
console.error(
|
||||
'Exception calling block function for opcode: ' +
|
||||
opcode + '\n' + e);
|
||||
} finally {
|
||||
// Update if the thread has set a yield timer ID
|
||||
// @todo hack
|
||||
if (YieldTimers.timerId > oldYieldTimerId) {
|
||||
thread.yieldTimerId = YieldTimers.timerId;
|
||||
}
|
||||
if (thread.status === Thread.STATUS_RUNNING && !switchedStack) {
|
||||
// Thread executed without yielding - move to done
|
||||
threadDoneCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = Sequencer;
|
65
src/engine/thread.js
Normal file
65
src/engine/thread.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* A thread is a running stack context and all the metadata needed.
|
||||
* @param {?string} firstBlock First block to execute in the thread.
|
||||
* @constructor
|
||||
*/
|
||||
function Thread (firstBlock) {
|
||||
/**
|
||||
* ID of top block of the thread
|
||||
* @type {!string}
|
||||
*/
|
||||
this.topBlock = firstBlock;
|
||||
/**
|
||||
* ID of next block that the thread will execute, or null if none.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.nextBlock = firstBlock;
|
||||
/**
|
||||
* Stack for the thread. When the sequencer enters a control structure,
|
||||
* the block is pushed onto the stack so we know where to exit.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
this.stack = [];
|
||||
|
||||
/**
|
||||
* Stack frames for the thread. Store metadata for the executing blocks.
|
||||
* @type {Array.<Object>}
|
||||
*/
|
||||
this.stackFrames = [];
|
||||
|
||||
/**
|
||||
* Status of the thread, one of three states (below)
|
||||
* @type {number}
|
||||
*/
|
||||
this.status = 0; /* Thread.STATUS_RUNNING */
|
||||
|
||||
/**
|
||||
* Yield timer ID (for checking when the thread should unyield).
|
||||
* @type {number}
|
||||
*/
|
||||
this.yieldTimerId = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread status for initialized or running thread.
|
||||
* Threads are in this state when the primitive is called for the first time.
|
||||
* @const
|
||||
*/
|
||||
Thread.STATUS_RUNNING = 0;
|
||||
|
||||
/**
|
||||
* Thread status for a yielded thread.
|
||||
* Threads are in this state when a primitive has yielded.
|
||||
* @const
|
||||
*/
|
||||
Thread.STATUS_YIELD = 1;
|
||||
|
||||
/**
|
||||
* Thread status for a finished/done thread.
|
||||
* Thread is moved to this state when the interpreter
|
||||
* can proceed with execution.
|
||||
* @const
|
||||
*/
|
||||
Thread.STATUS_DONE = 2;
|
||||
|
||||
module.exports = Thread;
|
92
src/index.js
Normal file
92
src/index.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
var EventEmitter = require('events');
|
||||
var util = require('util');
|
||||
|
||||
var Runtime = require('./engine/runtime');
|
||||
var adapter = require('./engine/adapter');
|
||||
|
||||
/**
|
||||
* Handles connections between blocks, stage, and extensions.
|
||||
*
|
||||
* @author Andrew Sliwinski <ascii@media.mit.edu>
|
||||
*/
|
||||
function VirtualMachine () {
|
||||
var instance = this;
|
||||
|
||||
// Bind event emitter and runtime to VM instance
|
||||
// @todo Post message (Web Worker) polyfill
|
||||
EventEmitter.call(instance);
|
||||
instance.runtime = new Runtime();
|
||||
|
||||
/**
|
||||
* Event listener for blocks. Handles validation and serves as a generic
|
||||
* adapter between the blocks and the runtime interface.
|
||||
*
|
||||
* @param {Object} Blockly "block" event
|
||||
*/
|
||||
instance.blockListener = function (e) {
|
||||
// Validate event
|
||||
if (typeof e !== 'object') return;
|
||||
if (typeof e.blockId !== 'string') return;
|
||||
|
||||
// Blocks
|
||||
switch (e.type) {
|
||||
case 'create':
|
||||
instance.runtime.createBlock(adapter(e), false);
|
||||
break;
|
||||
case 'change':
|
||||
instance.runtime.changeBlock({
|
||||
id: e.blockId,
|
||||
element: e.element,
|
||||
name: e.name,
|
||||
value: e.newValue
|
||||
});
|
||||
break;
|
||||
case 'move':
|
||||
instance.runtime.moveBlock({
|
||||
id: e.blockId,
|
||||
oldParent: e.oldParentId,
|
||||
oldField: e.oldInputName,
|
||||
newParent: e.newParentId,
|
||||
newField: e.newInputName
|
||||
});
|
||||
break;
|
||||
case 'delete':
|
||||
instance.runtime.deleteBlock({
|
||||
id: e.blockId
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
instance.flyoutBlockListener = function (e) {
|
||||
switch (e.type) {
|
||||
case 'create':
|
||||
instance.runtime.createBlock(adapter(e), true);
|
||||
break;
|
||||
case 'change':
|
||||
instance.runtime.changeBlock({
|
||||
id: e.blockId,
|
||||
element: e.element,
|
||||
name: e.name,
|
||||
value: e.newValue
|
||||
});
|
||||
break;
|
||||
case 'delete':
|
||||
instance.runtime.deleteBlock({
|
||||
id: e.blockId
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit from EventEmitter
|
||||
*/
|
||||
util.inherits(VirtualMachine, EventEmitter);
|
||||
|
||||
/**
|
||||
* Export and bind to `window`
|
||||
*/
|
||||
module.exports = VirtualMachine;
|
||||
if (typeof window !== 'undefined') window.VirtualMachine = module.exports;
|
20
src/util/timer.js
Normal file
20
src/util/timer.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Constructor
|
||||
*/
|
||||
function Timer () {
|
||||
this.startTime = 0;
|
||||
}
|
||||
|
||||
Timer.prototype.time = function () {
|
||||
return Date.now();
|
||||
};
|
||||
|
||||
Timer.prototype.start = function () {
|
||||
this.startTime = this.time();
|
||||
};
|
||||
|
||||
Timer.prototype.timeElapsed = function () {
|
||||
return this.time() - this.startTime;
|
||||
};
|
||||
|
||||
module.exports = Timer;
|
90
src/util/yieldtimers.js
Normal file
90
src/util/yieldtimers.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* @fileoverview Timers that are synchronized with the Scratch sequencer.
|
||||
*/
|
||||
var Timer = require('./timer');
|
||||
|
||||
function YieldTimers () {}
|
||||
|
||||
/**
|
||||
* Shared collection of timers.
|
||||
* Each timer is a [Function, number] with the callback
|
||||
* and absolute time for it to run.
|
||||
* @type {Object.<number,Array>}
|
||||
*/
|
||||
YieldTimers.timers = {};
|
||||
|
||||
/**
|
||||
* Monotonically increasing timer ID.
|
||||
* @type {number}
|
||||
*/
|
||||
YieldTimers.timerId = 0;
|
||||
|
||||
/**
|
||||
* Utility for measuring time.
|
||||
* @type {!Timer}
|
||||
*/
|
||||
YieldTimers.globalTimer = new Timer();
|
||||
|
||||
/**
|
||||
* The timeout function is passed to primitives and is intended
|
||||
* as a convenient replacement for window.setTimeout.
|
||||
* The sequencer will attempt to resolve the timer every time
|
||||
* the yielded thread would have been stepped.
|
||||
* @param {!Function} callback To be called when the timer is done.
|
||||
* @param {number} timeDelta Time to wait, in ms.
|
||||
* @return {number} Timer ID to be used with other methods.
|
||||
*/
|
||||
YieldTimers.timeout = function (callback, timeDelta) {
|
||||
var id = ++YieldTimers.timerId;
|
||||
YieldTimers.timers[id] = [
|
||||
callback,
|
||||
YieldTimers.globalTimer.time() + timeDelta
|
||||
];
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to resolve a timeout.
|
||||
* If the time has passed, call the callback.
|
||||
* Otherwise, do nothing.
|
||||
* @param {number} id Timer ID to resolve.
|
||||
* @return {boolean} True if the timer has resolved.
|
||||
*/
|
||||
YieldTimers.resolve = function (id) {
|
||||
var timer = YieldTimers.timers[id];
|
||||
if (!timer) {
|
||||
// No such timer.
|
||||
return false;
|
||||
}
|
||||
var callback = timer[0];
|
||||
var time = timer[1];
|
||||
if (YieldTimers.globalTimer.time() < time) {
|
||||
// Not done yet.
|
||||
return false;
|
||||
}
|
||||
// Execute the callback and remove the timer.
|
||||
callback();
|
||||
delete YieldTimers.timers[id];
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reject a timer so the callback never executes.
|
||||
* @param {number} id Timer ID to reject.
|
||||
*/
|
||||
YieldTimers.reject = function (id) {
|
||||
if (YieldTimers.timers[id]) {
|
||||
delete YieldTimers.timers[id];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reject all timers currently stored.
|
||||
* Especially useful for a Scratch "stop."
|
||||
*/
|
||||
YieldTimers.rejectAll = function () {
|
||||
YieldTimers.timers = {};
|
||||
YieldTimers.timerId = 0;
|
||||
};
|
||||
|
||||
module.exports = YieldTimers;
|
20
test/fixtures/events.json
vendored
Normal file
20
test/fixtures/events.json
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"create": {
|
||||
"blockId": "z!+#Nqr,_(V=xz0y7a@d",
|
||||
"workspaceId": "7Luws3lyb*Z98~Kk+IG|",
|
||||
"group": ";OswyM#@%`%,xOrhOXC=",
|
||||
"recordUndo": true,
|
||||
"xml": {
|
||||
"attributes": {
|
||||
"type": {
|
||||
"value": "wedo_motorclockwise"
|
||||
}
|
||||
},
|
||||
"innerHTML": "<value name=\"DURATION\"><shadow type=\"math_number\" id=\"!6Ahqg4f}Ljl}X5Hws?Z\"><field name=\"NUM\">10</field></shadow></value>"
|
||||
},
|
||||
"ids": [
|
||||
"z!+#Nqr,_(V=xz0y7a@d",
|
||||
"!6Ahqg4f}Ljl}X5Hws?Z"
|
||||
]
|
||||
}
|
||||
}
|
27
test/integration/index.js
Normal file
27
test/integration/index.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
var test = require('tap').test;
|
||||
var VirtualMachine = require('../../src/index');
|
||||
|
||||
test('spec', function (t) {
|
||||
var vm = new VirtualMachine();
|
||||
|
||||
t.type(VirtualMachine, 'function');
|
||||
t.type(vm, 'object');
|
||||
t.type(vm.blockListener, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create', function (t) {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('move', function (t) {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('change', function (t) {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('delete', function (t) {
|
||||
t.end();
|
||||
});
|
20
test/unit/adapter.js
Normal file
20
test/unit/adapter.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
var test = require('tap').test;
|
||||
var adapter = require('../../src/engine/adapter');
|
||||
var events = require('../fixtures/events.json');
|
||||
|
||||
test('spec', function (t) {
|
||||
t.type(adapter, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create event', function (t) {
|
||||
var result = adapter(events.create);
|
||||
|
||||
t.type(result, 'object');
|
||||
t.type(result.id, 'string');
|
||||
t.type(result.opcode, 'string');
|
||||
t.type(result.fields, 'object');
|
||||
t.type(result.fields['DURATION'], 'object');
|
||||
|
||||
t.end();
|
||||
});
|
89
test/unit/runtime.js
Normal file
89
test/unit/runtime.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
var test = require('tap').test;
|
||||
var Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('spec', function (t) {
|
||||
var r = new Runtime();
|
||||
|
||||
t.type(Runtime, 'function');
|
||||
t.type(r, 'object');
|
||||
t.ok(r instanceof Runtime);
|
||||
|
||||
t.type(r.blocks, 'object');
|
||||
t.type(r.stacks, 'object');
|
||||
t.ok(Array.isArray(r.stacks));
|
||||
|
||||
t.type(r.createBlock, 'function');
|
||||
t.type(r.moveBlock, 'function');
|
||||
t.type(r.changeBlock, 'function');
|
||||
t.type(r.deleteBlock, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create', function (t) {
|
||||
var r = new Runtime();
|
||||
r.createBlock({
|
||||
id: 'foo',
|
||||
opcode: 'TEST_BLOCK',
|
||||
next: null,
|
||||
fields: {}
|
||||
});
|
||||
|
||||
t.type(r.blocks['foo'], 'object');
|
||||
t.equal(r.blocks['foo'].opcode, 'TEST_BLOCK');
|
||||
t.notEqual(r.stacks.indexOf('foo'), -1);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('move', function (t) {
|
||||
var r = new Runtime();
|
||||
r.createBlock({
|
||||
id: 'foo',
|
||||
opcode: 'TEST_BLOCK',
|
||||
next: null,
|
||||
fields: {}
|
||||
});
|
||||
r.createBlock({
|
||||
id: 'bar',
|
||||
opcode: 'TEST_BLOCK',
|
||||
next: null,
|
||||
fields: {}
|
||||
});
|
||||
|
||||
// Attach 'bar' to the end of 'foo'
|
||||
r.moveBlock({
|
||||
id: 'bar',
|
||||
newParent: 'foo'
|
||||
});
|
||||
t.equal(r.stacks.length, 1);
|
||||
t.equal(Object.keys(r.blocks).length, 2);
|
||||
t.equal(r.blocks['foo'].next, 'bar');
|
||||
|
||||
// Detach 'bar' from 'foo'
|
||||
r.moveBlock({
|
||||
id: 'bar',
|
||||
oldParent: 'foo'
|
||||
});
|
||||
t.equal(r.stacks.length, 2);
|
||||
t.equal(Object.keys(r.blocks).length, 2);
|
||||
t.equal(r.blocks['foo'].next, null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('delete', function (t) {
|
||||
var r = new Runtime();
|
||||
r.createBlock({
|
||||
id: 'foo',
|
||||
opcode: 'TEST_BLOCK',
|
||||
next: null,
|
||||
fields: {}
|
||||
});
|
||||
r.deleteBlock({
|
||||
id: 'foo'
|
||||
});
|
||||
|
||||
t.type(r.blocks['foo'], 'undefined');
|
||||
t.equal(r.stacks.indexOf('foo'), -1);
|
||||
t.end();
|
||||
});
|
8
test/unit/sequencer.js
Normal file
8
test/unit/sequencer.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
var test = require('tap').test;
|
||||
var Sequencer = require('../../src/engine/sequencer');
|
||||
|
||||
test('spec', function (t) {
|
||||
t.type(Sequencer, 'function');
|
||||
// @todo
|
||||
t.end();
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
var test = require('tap').test;
|
||||
var vm = require('../../index');
|
||||
|
||||
test('spec', function (t) {
|
||||
t.end();
|
||||
});
|
8
test/unit/thread.js
Normal file
8
test/unit/thread.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
var test = require('tap').test;
|
||||
var Thread = require('../../src/engine/thread');
|
||||
|
||||
test('spec', function (t) {
|
||||
t.type(Thread, 'function');
|
||||
// @todo
|
||||
t.end();
|
||||
});
|
42
test/unit/timer.js
Normal file
42
test/unit/timer.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
var test = require('tap').test;
|
||||
var Timer = require('../../src/util/timer');
|
||||
|
||||
test('spec', function (t) {
|
||||
var timer = new Timer();
|
||||
|
||||
t.type(Timer, 'function');
|
||||
t.type(timer, 'object');
|
||||
|
||||
t.type(timer.startTime, 'number');
|
||||
t.type(timer.time, 'function');
|
||||
t.type(timer.start, 'function');
|
||||
t.type(timer.timeElapsed, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('time', function (t) {
|
||||
var timer = new Timer();
|
||||
var time = timer.time();
|
||||
|
||||
t.ok(Date.now() >= time);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('start / timeElapsed', function (t) {
|
||||
var timer = new Timer();
|
||||
var delay = 100;
|
||||
var threshold = 1000 / 60; // 60 hz
|
||||
|
||||
// Start timer
|
||||
timer.start();
|
||||
|
||||
// Wait and measure timer
|
||||
setTimeout(function () {
|
||||
var timeElapsed = timer.timeElapsed();
|
||||
t.ok(timeElapsed >= 0);
|
||||
t.ok(timeElapsed >= (delay - threshold) &&
|
||||
timeElapsed <= (delay + threshold));
|
||||
t.end();
|
||||
}, delay);
|
||||
});
|
11
vm.min.js
vendored
Normal file
11
vm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
29
webpack.config.js
Normal file
29
webpack.config.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
var webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
'vm': './src/index.js',
|
||||
'vm.min': './src/index.js'
|
||||
},
|
||||
output: {
|
||||
path: __dirname,
|
||||
filename: '[name].js'
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
include: /\.min\.js$/,
|
||||
minimize: true,
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
})
|
||||
]
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue