Merge branch 'develop' of github.com:morantsur/scratch-vm into feature/serialization

# Conflicts:
#	src/playground/index.html
#	src/serialization/sb2.js
#	src/sprites/rendered-target.js
#	src/virtual-machine.js
#	test/unit/serialization_sb2.js
This commit is contained in:
Ray Schamp 2017-04-26 11:44:53 -04:00
commit a4cdb027ab
8 changed files with 317 additions and 24 deletions

View file

@ -53,9 +53,9 @@
<button id="projectLoadButton">Load</button>
<button id="createEmptyProject">New Project</button><br />
<p>
<input type="button" value="Export to XML" onclick="toXml()">
<input type="button" value="Export to JSON" onclick="toJson()">
&nbsp;
<input type="button" value="Import from XML" onclick="fromXml()" id="import">
<input type="button" value="Import from JSON" onclick="fromJson()" id="import">
<br /><br />
<textarea id="importExport"></textarea>
</p>
@ -71,18 +71,17 @@
<!-- Playground -->
<script src="./playground.js"></script>
<script>
function toXml() {
function toJson() {
var output = document.getElementById('importExport');
var xml = Blockly.Xml.workspaceToDom(Scratch.workspace);
output.value = Blockly.Xml.domToPrettyText(xml);
var json = window.vm.toPrettyJSON(Scratch.workspace);
output.value = json;
output.focus();
output.select();
}
function fromXml() {
function fromJson() {
var input = document.getElementById('importExport');
var xml = Blockly.Xml.textToDom(input.value);
Blockly.Xml.domToWorkspace(Scratch.workspace, xml);
window.vm.fromJSON(input.value);
}
</script>
</body>

View file

@ -11,7 +11,7 @@ const Sprite = require('../sprites/sprite');
const Color = require('../util/color');
const log = require('../util/log');
const uid = require('../util/uid');
const specMap = require('./sb2specmap');
const specMap = require('./sb2_specmap');
const Variable = require('../engine/variable');
const List = require('../engine/list');
@ -283,7 +283,7 @@ const parseScratchObject = function (object, runtime, topLevel) {
*/
const sb2import = function (json, runtime, optForceSprite) {
return parseScratchObject(
JSON.parse(json),
json,
runtime,
!optForceSprite
);
@ -470,4 +470,6 @@ const parseBlock = function (sb2block) {
return activeBlock;
};
module.exports = sb2import;
module.exports = {
deserialize: sb2import
};

166
src/serialization/sb3.js Normal file
View file

@ -0,0 +1,166 @@
/**
* @fileoverview
* Partial implementation of a SB3 serializer and deserializer. Parses provided
* JSON and then generates all needed scratch-vm runtime structures.
*/
var package = require('../../package.json');
var Blocks = require('../engine/blocks');
var RenderedTarget = require('../sprites/rendered-target');
var Sprite = require('../sprites/sprite');
var Variable = require('../engine/variable');
var List = require('../engine/list');
/**
* Serializes the specified VM runtime.
* @param {!Runtime} runtime VM runtime instance to be serialized.
* @return {string} Serialized runtime instance.
*/
var serialize = function (runtime) {
// Fetch targets
var obj = Object.create(null);
obj.targets = runtime.targets;
// Assemble metadata
var meta = Object.create(null);
meta.semver = '3.0.0';
meta.vm = package.version;
// Attach full user agent string to metadata if available
meta.agent = null;
if (typeof navigator !== 'undefined') meta.agent = navigator.userAgent;
// Assemble payload and return
obj.meta = meta;
return obj;
};
/**
* Parse a single "Scratch object" and create all its in-memory VM objects.
* @param {!Object} object From-JSON "Scratch object:" sprite, stage, watcher.
* @param {!Runtime} runtime Runtime object to load all structures into.
* @return {?Target} Target created (stage or sprite).
*/
var parseScratchObject = function (object, runtime) {
if (!object.hasOwnProperty('name')) {
// Watcher/monitor - skip this object until those are implemented in VM.
// @todo
return;
}
// Blocks container for this object.
var blocks = new Blocks();
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
var sprite = new Sprite(blocks, runtime);
// Sprite/stage name from JSON.
if (object.hasOwnProperty('name')) {
sprite.name = object.name;
}
if (object.hasOwnProperty('blocks')) {
for (blockId in object.blocks) {
blocks.createBlock(object.blocks[blockId]);
}
console.log(blocks);
}
// Costumes from JSON.
if (object.hasOwnProperty('costumes') || object.hasOwnProperty('costume')) {
for (var i = 0; i < object.costumeCount; i++) {
var costume = object.costumes[i];
// @todo: Make sure all the relevant metadata is being pulled out.
sprite.costumes.push({
skin: costume.skin,
name: costume.name,
bitmapResolution: costume.bitmapResolution,
rotationCenterX: costume.rotationCenterX,
rotationCenterY: costume.rotationCenterY
});
}
}
// Sounds from JSON
if (object.hasOwnProperty('sounds')) {
for (var s = 0; s < object.sounds.length; s++) {
var sound = object.sounds[s];
sprite.sounds.push({
format: sound.format,
fileUrl: sound.fileUrl,
rate: sound.rate,
sampleCount: sound.sampleCount,
soundID: sound.soundID,
name: sound.name,
md5: sound.md5
});
}
}
// Create the first clone, and load its run-state from JSON.
var target = sprite.createClone();
// Add it to the runtime's list of targets.
runtime.targets.push(target);
// Load target properties from JSON.
if (object.hasOwnProperty('variables')) {
for (var j = 0; j < object.variables.length; j++) {
var variable = object.variables[j];
target.variables[variable.name] = new Variable(
variable.name,
variable.value,
variable.isPersistent
);
}
}
if (object.hasOwnProperty('lists')) {
for (var k = 0; k < object.lists.length; k++) {
var list = object.lists[k];
// @todo: monitor properties.
target.lists[list.listName] = new List(
list.listName,
list.contents
);
}
}
if (object.hasOwnProperty('x')) {
target.x = object.x;
}
if (object.hasOwnProperty('y')) {
target.y = object.y;
}
if (object.hasOwnProperty('direction')) {
target.direction = object.direction;
}
if (object.hasOwnProperty('size')) {
target.size = object.size;
}
if (object.hasOwnProperty('visible')) {
target.visible = object.visible;
}
if (object.hasOwnProperty('currentCostume')) {
target.currentCostume = object.currentCostume;
}
if (object.hasOwnProperty('rotationStyle')) {
target.rotationStyle = object.rotationStyle;
}
if (object.hasOwnProperty('isStage')) {
target.isStage = object.isStage;
}
target.updateAllDrawableProperties();
console.log("returning target:");
console.log(target);
return target;
};
/**
* Deserializes the specified representation of a VM runtime and loads it into
* the provided runtime instance.
* @param {string} json Stringified JSON representation of a VM runtime.
* @param {Runtime} runtime Runtime instance
*/
var deserialize = function (json, runtime) {
for (var i = 0; i < json.targets.length; i++) {
parseScratchObject(json.targets[i], runtime);
}
};
module.exports = {
serialize: serialize,
deserialize: deserialize
};

View file

@ -765,7 +765,7 @@ class RenderedTarget extends Target {
/**
* Serialize sprite info, used when emitting events about the sprite
* @returns {object} sprite data as a simple object
* @returns {object} Sprite data as a simple object
*/
toJSON () {
return {
@ -778,11 +778,14 @@ class RenderedTarget extends Target {
direction: this.direction,
draggable: this.draggable,
costume: this.getCurrentCostume(),
costumes: this.getCostumes(),
sounds: this.getSounds(),
costumeCount: this.getCostumes().length,
visible: this.visible,
rotationStyle: this.rotationStyle
rotationStyle: this.rotationStyle,
blocks: this.blocks._blocks,
variables: this.variables,
lists: this.lists,
costumes: this.getCostumes(),
sounds: this.getSounds()
};
}

View file

@ -2,7 +2,8 @@ const EventEmitter = require('events');
const log = require('./util/log');
const Runtime = require('./engine/runtime');
const sb2import = require('./import/sb2import');
const sb2 = require('./serialization/sb2');
const sb3 = require('./serialization/sb3');
const StringUtil = require('./util/string-util');
const loadCostume = require('./import/load-costume.js');
@ -144,7 +145,7 @@ class VirtualMachine extends EventEmitter {
*/
loadProject (json) {
// @todo: Handle other formats, e.g., Scratch 1.4, Scratch 3.0.
return sb2import(json, this.runtime).then(targets => {
return this.fromJSON(json, this.runtime).then(targets => {
this.clear();
for (let n = 0; n < targets.length; n++) {
if (targets[n] !== null) {
@ -179,20 +180,72 @@ class VirtualMachine extends EventEmitter {
});
}
/**
* return a project in a Scratch 3.0 JSON representation.
*/
saveProjectSb3 () {
// @todo: Handle other formats, e.g., Scratch 1.4, Scratch 2.0.
return this.toJSON();
}
/**
* Export project as a Scratch 3.0 JSON representation.
* @return {string} Serialized state of the runtime.
*/
toJSON () {
return JSON.stringify(sb3.serialize(this.runtime));
}
/**
* Load a project from a Scratch JSON representation.
* @param {string} json JSON string representing a project.
*/
fromJSON (json) {
// Clear the current runtime
this.clear();
// Validate & parse
if (typeof json !== 'string') return;
json = JSON.parse(json);
if (typeof json !== 'object') return;
// Establish version, deserialize, and load into runtime
// @todo Support Scratch 1.4
// @todo This is an extremely naïve / dangerous way of determining version.
// See `scratch-parser` for a more sophisticated validation
// methodology that should be adapted for use here
if ((typeof json.meta !== 'undefined') && (typeof json.meta.semver !== 'undefined') ) {
sb3.deserialize(json, this.runtime);
} else {
sb2.deserialize(json, this.runtime);
}
// Select the first target for editing, e.g., the first sprite.
this.editingTarget = this.runtime.targets[1];
// Update the VM user's knowledge of targets and blocks on the workspace.
this.emitTargetsUpdate();
this.emitWorkspaceUpdate();
this.runtime.setEditingTarget(this.editingTarget);
}
/**
* Add a single sprite from the "Sprite2" (i.e., SB2 sprite) format.
* @param {string} json JSON string representing the sprite.
*/
addSprite2 (json) {
// Select new sprite.
sb2import(json, this.runtime, true).then(targets => {
this.runtime.targets.push(targets[0]);
this.editingTarget = targets[0];
this.editingTarget = sb2.deserialize(json, this.runtime, true);
/* @todo Promisify this
.then(targets => {
this.runtime.targets.push(targets[0]);
this.editingTarget = targets[0];
})
*/
// Update the VM user's knowledge of targets and blocks on the workspace.
this.emitTargetsUpdate();
this.emitWorkspaceUpdate();
this.runtime.setEditingTarget(this.editingTarget);
});
this.emitTargetsUpdate();
this.emitWorkspaceUpdate();
this.runtime.setEditingTarget(this.editingTarget);
}
/**

View file

@ -0,0 +1,51 @@
var path = require('path');
var test = require('tap').test;
var extract = require('../fixtures/extract');
var renderedTarget = require('../../src/sprites/rendered-target');
var runtime = require('../../src/engine/runtime');
var sb2 = require('../../src/serialization/sb2');
test('spec', function (t) {
t.type(sb2, 'object');
t.type(sb2.deserialize, 'function');
t.end();
});
test('default', function (t) {
// Get SB2 JSON (string)
var uri = path.resolve(__dirname, '../fixtures/default.sb2');
var file = extract(uri);
// Create runtime instance & load SB2 into it
var rt = new runtime();
sb2.deserialize(file, rt);
// Test
t.type(file, 'string');
t.type(rt, 'object');
t.type(rt.targets, 'object');
t.ok(rt.targets[0] instanceof renderedTarget);
t.type(rt.targets[0].id, 'string');
t.type(rt.targets[0].blocks, 'object');
t.type(rt.targets[0].variables, 'object');
t.type(rt.targets[0].lists, 'object');
t.equal(rt.targets[0].isOriginal, true);
t.equal(rt.targets[0].currentCostume, 0);
t.equal(rt.targets[0].isOriginal, true);
t.equal(rt.targets[0].isStage, true);
t.ok(rt.targets[1] instanceof renderedTarget);
t.type(rt.targets[1].id, 'string');
t.type(rt.targets[1].blocks, 'object');
t.type(rt.targets[1].variables, 'object');
t.type(rt.targets[1].lists, 'object');
t.equal(rt.targets[1].isOriginal, true);
t.equal(rt.targets[1].currentCostume, 0);
t.equal(rt.targets[1].isOriginal, true);
t.equal(rt.targets[1].isStage, false);
t.end();
});

View file

@ -0,0 +1,19 @@
var test = require('tap').test;
var VirtualMachine = require('../../src/index');
var sb3 = require('../../src/serialization/sb3');
test('serialize', function (t) {
var vm = new VirtualMachine();
vm.fromJSON(JSON.stringify(require('../fixtures/demo.json')));
var result = sb3.serialize(vm.runtime);
console.dir(JSON.stringify(result));
// @todo Analyize
t.end();
});
test('deserialize', function (t) {
var vm = new VirtualMachine();
var result = sb3.deserialize('', vm.runtime);
// @todo Analyize
t.end();
});