diff --git a/src/playground/index.html b/src/playground/index.html index 5f293e9b4..2d2f1698c 100644 --- a/src/playground/index.html +++ b/src/playground/index.html @@ -53,9 +53,9 @@

- +   - +

@@ -71,18 +71,17 @@ diff --git a/src/import/sb2import.js b/src/serialization/sb2.js similarity index 99% rename from src/import/sb2import.js rename to src/serialization/sb2.js index 4abd6230b..53a7cfc4d 100644 --- a/src/import/sb2import.js +++ b/src/serialization/sb2.js @@ -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 +}; diff --git a/src/import/sb2specmap.js b/src/serialization/sb2_specmap.js similarity index 100% rename from src/import/sb2specmap.js rename to src/serialization/sb2_specmap.js diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js new file mode 100644 index 000000000..7543b4cd1 --- /dev/null +++ b/src/serialization/sb3.js @@ -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 +}; diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index e6323ec73..9880d4750 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -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() }; } diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 8e4e9bcfe..a69539a06 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -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); } /** diff --git a/test/unit/serialization_sb2.js b/test/unit/serialization_sb2.js new file mode 100644 index 000000000..530311d33 --- /dev/null +++ b/test/unit/serialization_sb2.js @@ -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(); +}); diff --git a/test/unit/serialization_sb3.js b/test/unit/serialization_sb3.js new file mode 100644 index 000000000..0ec56284d --- /dev/null +++ b/test/unit/serialization_sb3.js @@ -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(); +});