2016-04-18 17:20:30 -04:00
|
|
|
var EventEmitter = require('events');
|
|
|
|
var util = require('util');
|
|
|
|
|
|
|
|
var Runtime = require('./engine/runtime');
|
2016-08-31 13:56:05 -04:00
|
|
|
var sb2import = require('./import/sb2import');
|
2016-09-12 11:05:16 -04:00
|
|
|
var Sprite = require('./sprites/sprite');
|
|
|
|
var Blocks = require('./engine/blocks');
|
2016-04-18 17:20:30 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles connections between blocks, stage, and extensions.
|
|
|
|
*
|
|
|
|
* @author Andrew Sliwinski <ascii@media.mit.edu>
|
|
|
|
*/
|
2016-09-20 15:07:05 -04:00
|
|
|
function VirtualMachine () {
|
2016-04-18 17:20:30 -04:00
|
|
|
var instance = this;
|
|
|
|
// Bind event emitter and runtime to VM instance
|
|
|
|
EventEmitter.call(instance);
|
|
|
|
/**
|
2016-08-31 12:08:54 -04:00
|
|
|
* VM runtime, to store blocks, I/O devices, sprites/targets, etc.
|
|
|
|
* @type {!Runtime}
|
2016-04-18 17:20:30 -04:00
|
|
|
*/
|
2016-09-20 15:07:05 -04:00
|
|
|
instance.runtime = new Runtime();
|
2016-08-31 12:08:54 -04:00
|
|
|
/**
|
|
|
|
* The "currently editing"/selected target ID for the VM.
|
|
|
|
* Block events from any Blockly workspace are routed to this target.
|
|
|
|
* @type {!string}
|
|
|
|
*/
|
|
|
|
instance.editingTarget = null;
|
2016-06-21 15:30:27 -04:00
|
|
|
// Runtime emits are passed along as VM emits.
|
2016-09-08 09:40:53 -04:00
|
|
|
instance.runtime.on(Runtime.SCRIPT_GLOW_ON, function (id) {
|
|
|
|
instance.emit(Runtime.SCRIPT_GLOW_ON, {id: id});
|
2016-06-21 15:30:27 -04:00
|
|
|
});
|
2016-09-08 09:40:53 -04:00
|
|
|
instance.runtime.on(Runtime.SCRIPT_GLOW_OFF, function (id) {
|
|
|
|
instance.emit(Runtime.SCRIPT_GLOW_OFF, {id: id});
|
2016-06-21 15:30:27 -04:00
|
|
|
});
|
|
|
|
instance.runtime.on(Runtime.BLOCK_GLOW_ON, function (id) {
|
|
|
|
instance.emit(Runtime.BLOCK_GLOW_ON, {id: id});
|
|
|
|
});
|
|
|
|
instance.runtime.on(Runtime.BLOCK_GLOW_OFF, function (id) {
|
|
|
|
instance.emit(Runtime.BLOCK_GLOW_OFF, {id: id});
|
|
|
|
});
|
2016-07-07 19:42:38 -04:00
|
|
|
instance.runtime.on(Runtime.VISUAL_REPORT, function (id, value) {
|
|
|
|
instance.emit(Runtime.VISUAL_REPORT, {id: id, value: value});
|
|
|
|
});
|
2016-09-13 17:49:45 -04:00
|
|
|
|
|
|
|
this.blockListener = this.blockListener.bind(this);
|
2016-04-18 17:20:30 -04:00
|
|
|
}
|
|
|
|
|
2016-06-21 15:30:27 -04:00
|
|
|
/**
|
|
|
|
* Inherit from EventEmitter
|
|
|
|
*/
|
|
|
|
util.inherits(VirtualMachine, EventEmitter);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start running the VM - do this before anything else.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.start = function () {
|
|
|
|
this.runtime.start();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* "Green flag" handler - start all threads starting with a green flag.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.greenFlag = function () {
|
|
|
|
this.runtime.greenFlag();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop all threads and running activities.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.stopAll = function () {
|
|
|
|
this.runtime.stopAll();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get data for playground. Data comes back in an emitted event.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.getPlaygroundData = function () {
|
2016-09-21 16:31:23 -04:00
|
|
|
var instance = this;
|
|
|
|
// Only send back thread data for the current editingTarget.
|
|
|
|
var threadData = this.runtime.threads.filter(function(thread) {
|
|
|
|
return thread.target == instance.editingTarget;
|
|
|
|
});
|
|
|
|
// Remove the target key, since it's a circular reference.
|
|
|
|
var filteredThreadData = JSON.stringify(threadData, function(key, value) {
|
|
|
|
if (key == 'target') return undefined;
|
|
|
|
return value;
|
|
|
|
}, 2);
|
2016-06-21 15:30:27 -04:00
|
|
|
this.emit('playgroundData', {
|
2016-08-31 12:08:54 -04:00
|
|
|
blocks: this.editingTarget.blocks,
|
2016-09-21 16:31:23 -04:00
|
|
|
threads: filteredThreadData
|
2016-06-21 15:30:27 -04:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-07-01 11:52:43 -04:00
|
|
|
/**
|
|
|
|
* Handle an animation frame.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.animationFrame = function () {
|
|
|
|
this.runtime.animationFrame();
|
|
|
|
};
|
|
|
|
|
2016-08-15 21:37:36 -04:00
|
|
|
/**
|
|
|
|
* Post I/O data to the virtual devices.
|
|
|
|
* @param {?string} device Name of virtual I/O device.
|
|
|
|
* @param {Object} data Any data object to post to the I/O device.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.postIOData = function (device, data) {
|
|
|
|
if (this.runtime.ioDevices[device]) {
|
|
|
|
this.runtime.ioDevices[device].postData(data);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-08-31 12:28:09 -04:00
|
|
|
/**
|
|
|
|
* Load a project from a Scratch 2.0 JSON representation.
|
2016-09-12 11:05:16 -04:00
|
|
|
* @param {?string} json JSON string representing the project.
|
2016-08-31 12:28:09 -04:00
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.loadProject = function (json) {
|
2016-08-31 13:56:05 -04:00
|
|
|
// @todo: Handle other formats, e.g., Scratch 1.4, Scratch 3.0.
|
|
|
|
sb2import(json, this.runtime);
|
2016-08-31 12:28:09 -04:00
|
|
|
// Select the first target for editing, e.g., the stage.
|
|
|
|
this.editingTarget = this.runtime.targets[0];
|
|
|
|
// Update the VM user's knowledge of targets and blocks on the workspace.
|
|
|
|
this.emitTargetsUpdate();
|
|
|
|
this.emitWorkspaceUpdate();
|
2016-09-08 09:40:53 -04:00
|
|
|
this.runtime.setEditingTarget(this.editingTarget);
|
2016-08-31 12:28:09 -04:00
|
|
|
};
|
|
|
|
|
2016-09-12 12:03:24 -04:00
|
|
|
/**
|
|
|
|
* Temporary way to make an empty project, in case the desired project
|
|
|
|
* cannot be loaded from the online server.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.createEmptyProject = function () {
|
|
|
|
// Stage.
|
|
|
|
var blocks2 = new Blocks();
|
2016-10-12 13:56:31 -04:00
|
|
|
var stage = new Sprite(blocks2, this.runtime);
|
2016-09-12 12:03:24 -04:00
|
|
|
stage.name = 'Stage';
|
|
|
|
stage.costumes.push({
|
|
|
|
skin: '/assets/stage.png',
|
|
|
|
name: 'backdrop1',
|
2016-10-04 20:38:11 -04:00
|
|
|
bitmapResolution: 2,
|
|
|
|
rotationCenterX: 480,
|
|
|
|
rotationCenterY: 360
|
2016-09-12 12:03:24 -04:00
|
|
|
});
|
|
|
|
var target2 = stage.createClone();
|
|
|
|
this.runtime.targets.push(target2);
|
|
|
|
target2.x = 0;
|
|
|
|
target2.y = 0;
|
|
|
|
target2.direction = 90;
|
|
|
|
target2.size = 200;
|
|
|
|
target2.visible = true;
|
|
|
|
target2.isStage = true;
|
|
|
|
// Sprite1 (cat).
|
|
|
|
var blocks1 = new Blocks();
|
2016-10-12 13:56:31 -04:00
|
|
|
var sprite = new Sprite(blocks1, this.runtime);
|
2016-09-12 12:03:24 -04:00
|
|
|
sprite.name = 'Sprite1';
|
|
|
|
sprite.costumes.push({
|
|
|
|
skin: '/assets/scratch_cat.svg',
|
|
|
|
name: 'costume1',
|
|
|
|
bitmapResolution: 1,
|
|
|
|
rotationCenterX: 47,
|
|
|
|
rotationCenterY: 55
|
|
|
|
});
|
|
|
|
var target1 = sprite.createClone();
|
|
|
|
this.runtime.targets.push(target1);
|
|
|
|
target1.x = 0;
|
|
|
|
target1.y = 0;
|
|
|
|
target1.direction = 90;
|
|
|
|
target1.size = 100;
|
|
|
|
target1.visible = true;
|
|
|
|
this.editingTarget = this.runtime.targets[0];
|
|
|
|
this.emitTargetsUpdate();
|
|
|
|
this.emitWorkspaceUpdate();
|
|
|
|
};
|
|
|
|
|
2016-09-20 15:07:05 -04:00
|
|
|
/**
|
|
|
|
* Set the renderer for the VM/runtime
|
|
|
|
* @param {!RenderWebGL} renderer The renderer to attach
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.attachRenderer = function (renderer) {
|
|
|
|
this.runtime.attachRenderer(renderer);
|
|
|
|
};
|
|
|
|
|
2016-09-13 17:49:45 -04:00
|
|
|
/**
|
|
|
|
* Handle a Blockly event for the current editing target.
|
|
|
|
* @param {!Blockly.Event} e Any Blockly event.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.blockListener = function (e) {
|
|
|
|
if (this.editingTarget) {
|
|
|
|
this.editingTarget.blocks.blocklyListen(
|
|
|
|
e,
|
|
|
|
false,
|
|
|
|
this.runtime
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-08-31 12:28:09 -04:00
|
|
|
/**
|
|
|
|
* Set an editing target. An editor UI can use this function to switch
|
|
|
|
* between editing different targets, sprites, etc.
|
|
|
|
* After switching the editing target, the VM may emit updates
|
|
|
|
* to the list of targets and any attached workspace blocks
|
|
|
|
* (see `emitTargetsUpdate` and `emitWorkspaceUpdate`).
|
|
|
|
* @param {string} targetId Id of target to set as editing.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.setEditingTarget = function (targetId) {
|
2016-09-02 09:47:27 -04:00
|
|
|
// Has the target id changed? If not, exit.
|
|
|
|
if (targetId == this.editingTarget.id) {
|
2016-08-31 12:28:09 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
var target = this.runtime.getTargetById(targetId);
|
|
|
|
if (target) {
|
|
|
|
this.editingTarget = target;
|
|
|
|
// Emit appropriate UI updates.
|
|
|
|
this.emitTargetsUpdate();
|
|
|
|
this.emitWorkspaceUpdate();
|
2016-09-08 09:40:53 -04:00
|
|
|
this.runtime.setEditingTarget(target);
|
2016-08-31 12:28:09 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Emit metadata about available targets.
|
|
|
|
* An editor UI could use this to display a list of targets and show
|
|
|
|
* the currently editing one.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.emitTargetsUpdate = function () {
|
|
|
|
this.emit('targetsUpdate', {
|
|
|
|
// [[target id, human readable target name], ...].
|
2016-09-15 19:37:12 -04:00
|
|
|
targetList: this.runtime.targets.filter(function (target) {
|
|
|
|
// Don't report clones.
|
|
|
|
return !target.hasOwnProperty('isOriginal') || target.isOriginal;
|
|
|
|
}).map(function(target) {
|
2016-08-31 12:28:09 -04:00
|
|
|
return [target.id, target.getName()];
|
|
|
|
}),
|
|
|
|
// Currently editing target id.
|
|
|
|
editingTarget: this.editingTarget.id
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Emit an Blockly/scratch-blocks compatible XML representation
|
|
|
|
* of the current editing target's blocks.
|
|
|
|
*/
|
|
|
|
VirtualMachine.prototype.emitWorkspaceUpdate = function () {
|
|
|
|
this.emit('workspaceUpdate', {
|
|
|
|
'xml': this.editingTarget.blocks.toXML()
|
|
|
|
});
|
|
|
|
};
|
2016-09-20 10:29:47 -04:00
|
|
|
|
2016-04-18 17:20:30 -04:00
|
|
|
module.exports = VirtualMachine;
|