mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 15:02:52 -05:00
Load extensions when loading a project or sprite
- Accumulate extension info while deserializing JSON - Install extensions (if any) before installing target(s) - Merged 'install' steps of the "add sprite" and "load project" paths
This commit is contained in:
parent
c5d3e2dbb4
commit
0af3de9bf0
4 changed files with 306 additions and 55 deletions
|
@ -78,15 +78,16 @@ const flatten = function (blocks) {
|
||||||
* or a list of blocks in an argument (e.g., move [pick random...]).
|
* or a list of blocks in an argument (e.g., move [pick random...]).
|
||||||
* @param {Array.<object>} blockList SB2 JSON-format block list.
|
* @param {Array.<object>} blockList SB2 JSON-format block list.
|
||||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||||
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||||
* @return {Array.<object>} Scratch VM-format block list.
|
* @return {Array.<object>} Scratch VM-format block list.
|
||||||
*/
|
*/
|
||||||
const parseBlockList = function (blockList, getVariableId) {
|
const parseBlockList = function (blockList, getVariableId, extensions) {
|
||||||
const resultingList = [];
|
const resultingList = [];
|
||||||
let previousBlock = null; // For setting next.
|
let previousBlock = null; // For setting next.
|
||||||
for (let i = 0; i < blockList.length; i++) {
|
for (let i = 0; i < blockList.length; i++) {
|
||||||
const block = blockList[i];
|
const block = blockList[i];
|
||||||
// eslint-disable-next-line no-use-before-define
|
// eslint-disable-next-line no-use-before-define
|
||||||
const parsedBlock = parseBlock(block, getVariableId);
|
const parsedBlock = parseBlock(block, getVariableId, extensions);
|
||||||
if (typeof parsedBlock === 'undefined') continue;
|
if (typeof parsedBlock === 'undefined') continue;
|
||||||
if (previousBlock) {
|
if (previousBlock) {
|
||||||
parsedBlock.parent = previousBlock.id;
|
parsedBlock.parent = previousBlock.id;
|
||||||
|
@ -104,14 +105,15 @@ const parseBlockList = function (blockList, getVariableId) {
|
||||||
* @param {!object} scripts Scripts object from SB2 JSON.
|
* @param {!object} scripts Scripts object from SB2 JSON.
|
||||||
* @param {!Blocks} blocks Blocks object to load parsed blocks into.
|
* @param {!Blocks} blocks Blocks object to load parsed blocks into.
|
||||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||||
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||||
*/
|
*/
|
||||||
const parseScripts = function (scripts, blocks, getVariableId) {
|
const parseScripts = function (scripts, blocks, getVariableId, extensions) {
|
||||||
for (let i = 0; i < scripts.length; i++) {
|
for (let i = 0; i < scripts.length; i++) {
|
||||||
const script = scripts[i];
|
const script = scripts[i];
|
||||||
const scriptX = script[0];
|
const scriptX = script[0];
|
||||||
const scriptY = script[1];
|
const scriptY = script[1];
|
||||||
const blockList = script[2];
|
const blockList = script[2];
|
||||||
const parsedBlockList = parseBlockList(blockList, getVariableId);
|
const parsedBlockList = parseBlockList(blockList, getVariableId, extensions);
|
||||||
if (parsedBlockList[0]) {
|
if (parsedBlockList[0]) {
|
||||||
// Adjust script coordinates to account for
|
// Adjust script coordinates to account for
|
||||||
// larger block size in scratch-blocks.
|
// larger block size in scratch-blocks.
|
||||||
|
@ -155,12 +157,14 @@ const generateVariableIdGetter = (function () {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
||||||
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
* TODO: parse the "info" section, especially "savedExtensions"
|
||||||
* @param {!Runtime} runtime Runtime object to load all structures into.
|
* @param {!object} object - From-JSON "Scratch object:" sprite, stage, watcher.
|
||||||
* @param {boolean} topLevel Whether this is the top-level object (stage).
|
* @param {!Runtime} runtime - Runtime object to load all structures into.
|
||||||
* @return {?Promise} Promise that resolves to the loaded targets when ready.
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||||
|
* @param {boolean} topLevel - Whether this is the top-level object (stage).
|
||||||
|
* @return {!Promise.<Array.<Target>>} Promise for the loaded targets when ready, or null for unsupported objects.
|
||||||
*/
|
*/
|
||||||
const parseScratchObject = function (object, runtime, topLevel) {
|
const parseScratchObject = function (object, runtime, extensions, topLevel) {
|
||||||
if (!object.hasOwnProperty('objName')) {
|
if (!object.hasOwnProperty('objName')) {
|
||||||
// Watcher/monitor - skip this object until those are implemented in VM.
|
// Watcher/monitor - skip this object until those are implemented in VM.
|
||||||
// @todo
|
// @todo
|
||||||
|
@ -228,7 +232,7 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
||||||
|
|
||||||
// If included, parse any and all scripts/blocks on the object.
|
// If included, parse any and all scripts/blocks on the object.
|
||||||
if (object.hasOwnProperty('scripts')) {
|
if (object.hasOwnProperty('scripts')) {
|
||||||
parseScripts(object.scripts, blocks, getVariableId);
|
parseScripts(object.scripts, blocks, getVariableId, extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (object.hasOwnProperty('lists')) {
|
if (object.hasOwnProperty('lists')) {
|
||||||
|
@ -287,7 +291,7 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
||||||
const childrenPromises = [];
|
const childrenPromises = [];
|
||||||
if (object.children) {
|
if (object.children) {
|
||||||
for (let m = 0; m < object.children.length; m++) {
|
for (let m = 0; m < object.children.length; m++) {
|
||||||
childrenPromises.push(parseScratchObject(object.children[m], runtime, false));
|
childrenPromises.push(parseScratchObject(object.children[m], runtime, extensions, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,31 +316,42 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
||||||
* @param {!object} json SB2-format JSON to load.
|
* @param {!object} json SB2-format JSON to load.
|
||||||
* @param {!Runtime} runtime Runtime object to load all structures into.
|
* @param {!Runtime} runtime Runtime object to load all structures into.
|
||||||
* @param {boolean=} optForceSprite If set, treat as sprite (Sprite2).
|
* @param {boolean=} optForceSprite If set, treat as sprite (Sprite2).
|
||||||
* @return {?Promise} Promise that resolves to the loaded targets when ready.
|
* @return {Promise.<ImportedProject>} Promise that resolves to the loaded targets when ready.
|
||||||
*/
|
*/
|
||||||
const sb2import = function (json, runtime, optForceSprite) {
|
const sb2import = function (json, runtime, optForceSprite) {
|
||||||
return parseScratchObject(
|
const extensions = {
|
||||||
json,
|
extensionIDs: new Set(),
|
||||||
runtime,
|
extensionURLs: new Map()
|
||||||
!optForceSprite
|
};
|
||||||
);
|
return parseScratchObject(json, runtime, extensions, !optForceSprite)
|
||||||
|
.then(targets => ({
|
||||||
|
targets,
|
||||||
|
extensions
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a single SB2 JSON-formatted block and its children.
|
* Parse a single SB2 JSON-formatted block and its children.
|
||||||
* @param {!object} sb2block SB2 JSON-formatted block.
|
* @param {!object} sb2block SB2 JSON-formatted block.
|
||||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
* @param {Function} getVariableId function to retrieve a variable's ID based on name
|
||||||
* @return {object} Scratch VM format block.
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||||
|
* @return {object} Scratch VM format block, or null if unsupported object.
|
||||||
*/
|
*/
|
||||||
const parseBlock = function (sb2block, getVariableId) {
|
const parseBlock = function (sb2block, getVariableId, extensions) {
|
||||||
// First item in block object is the old opcode (e.g., 'forward:').
|
// First item in block object is the old opcode (e.g., 'forward:').
|
||||||
const oldOpcode = sb2block[0];
|
const oldOpcode = sb2block[0];
|
||||||
// Convert the block using the specMap. See sb2specmap.js.
|
// Convert the block using the specMap. See sb2specmap.js.
|
||||||
if (!oldOpcode || !specMap[oldOpcode]) {
|
if (!oldOpcode || !specMap[oldOpcode]) {
|
||||||
log.warn('Couldn\'t find SB2 block: ', oldOpcode);
|
log.warn('Couldn\'t find SB2 block: ', oldOpcode);
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
const blockMetadata = specMap[oldOpcode];
|
const blockMetadata = specMap[oldOpcode];
|
||||||
|
// If the block is from an extension, record it.
|
||||||
|
const dotIndex = blockMetadata.opcode.indexOf('.');
|
||||||
|
if (dotIndex >= 0) {
|
||||||
|
const extension = blockMetadata.opcode.substring(0, dotIndex);
|
||||||
|
extensions.extensionIDs.add(extension);
|
||||||
|
}
|
||||||
// Block skeleton.
|
// Block skeleton.
|
||||||
const activeBlock = {
|
const activeBlock = {
|
||||||
id: uid(), // Generate a new block unique ID.
|
id: uid(), // Generate a new block unique ID.
|
||||||
|
@ -373,10 +388,10 @@ const parseBlock = function (sb2block, getVariableId) {
|
||||||
let innerBlocks;
|
let innerBlocks;
|
||||||
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
||||||
// Block list occupies the input.
|
// Block list occupies the input.
|
||||||
innerBlocks = parseBlockList(providedArg, getVariableId);
|
innerBlocks = parseBlockList(providedArg, getVariableId, extensions);
|
||||||
} else {
|
} else {
|
||||||
// Single block occupies the input.
|
// Single block occupies the input.
|
||||||
innerBlocks = [parseBlock(providedArg, getVariableId)];
|
innerBlocks = [parseBlock(providedArg, getVariableId, extensions)];
|
||||||
}
|
}
|
||||||
let previousBlock = null;
|
let previousBlock = null;
|
||||||
for (let j = 0; j < innerBlocks.length; j++) {
|
for (let j = 0; j < innerBlocks.length; j++) {
|
||||||
|
|
|
@ -21,6 +21,24 @@
|
||||||
* properties. By hand, I matched the opcode name to the 3.0 opcode.
|
* properties. By hand, I matched the opcode name to the 3.0 opcode.
|
||||||
* Finally, I filled in the expected arguments as below.
|
* Finally, I filled in the expected arguments as below.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} SB2SpecMap_blockInfo
|
||||||
|
* @property {string} opcode - the Scratch 3.0 block opcode. Use 'extensionID.opcode' for extension opcodes.
|
||||||
|
* @property {Array.<SB2SpecMap_argInfo>} argMap - metadata for this block's arguments.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} SB2SpecMap_argInfo
|
||||||
|
* @property {string} type - the type of this arg (such as 'input' or 'field')
|
||||||
|
* @property {string} inputOp - the scratch-blocks shadow type for this arg
|
||||||
|
* @property {string} inputName - the name this argument will take when provided to the block implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of Scratch 2.0 opcode to Scratch 3.0 block metadata.
|
||||||
|
* @type {object.<SB2SpecMap_blockInfo>}
|
||||||
|
*/
|
||||||
const specMap = {
|
const specMap = {
|
||||||
'forward:': {
|
'forward:': {
|
||||||
opcode: 'motion_movesteps',
|
opcode: 'motion_movesteps',
|
||||||
|
@ -1376,4 +1394,179 @@ const specMap = {
|
||||||
argMap: []
|
argMap: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add to the specMap entries for an opcode from a Scratch 2.0 extension. Two entries will be made with the same
|
||||||
|
* metadata; this is done to support projects saved by both older and newer versions of the Scratch 2.0 editor.
|
||||||
|
* @param {string} sb2Extension - the Scratch 2.0 name of the extension
|
||||||
|
* @param {string} sb2Opcode - the Scratch 2.0 opcode
|
||||||
|
* @param {SB2SpecMap_blockInfo} blockInfo - the Scratch 3.0 block info
|
||||||
|
*/
|
||||||
|
const addExtensionOp = function (sb2Extension, sb2Opcode, blockInfo) {
|
||||||
|
/**
|
||||||
|
* This string separates the name of an extension and the name of an opcode in more recent Scratch 2.0 projects.
|
||||||
|
* Earlier projects used '.' as a separator, up until we added the 'LEGO WeDo 2.0' extension...
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const sep = '\u001F'; // Unicode Unit Separator
|
||||||
|
|
||||||
|
// make one entry for projects saved by recent versions of the Scratch 2.0 editor
|
||||||
|
specMap[`${sb2Extension}${sep}${sb2Opcode}`] = blockInfo;
|
||||||
|
|
||||||
|
// make a second for projects saved by older versions of the Scratch 2.0 editor
|
||||||
|
specMap[`${sb2Extension}.${sb2Opcode}`] = blockInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
const weDo2 = 'LEGO WeDo 2.0';
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'motorOnFor', {
|
||||||
|
opcode: 'wedo2.motorOnFor',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'MOTOR_ID'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'math_number',
|
||||||
|
inputName: 'DURATION'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'motorOn', {
|
||||||
|
opcode: 'wedo2.motorOn',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'MOTOR_ID'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'motorOff', {
|
||||||
|
opcode: 'wedo2.motorOff',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'MOTOR_ID'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'startMotorPower', {
|
||||||
|
opcode: 'wedo2.startMotorPower',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'MOTOR_ID'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'math_number',
|
||||||
|
inputName: 'POWER'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'setMotorDirection', {
|
||||||
|
opcode: 'wedo2.setMotorDirection',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'MOTOR_ID'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'DIRECTION'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'setLED', {
|
||||||
|
opcode: 'wedo2.setLightHue',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'math_number',
|
||||||
|
inputName: 'HUE'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'playNote', {
|
||||||
|
opcode: 'wedo2.playNoteFor',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'math_number',
|
||||||
|
inputName: 'NOTE'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'math_number',
|
||||||
|
inputName: 'DURATION'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'whenDistance', {
|
||||||
|
opcode: 'wedo2.whenDistance',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'OP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'math_number',
|
||||||
|
inputName: 'REFERENCE'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'whenTilted', {
|
||||||
|
opcode: 'wedo2.whenTilted',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'DIRECTION'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'getDistance', {
|
||||||
|
opcode: 'wedo2.motorOn'
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'isTilted', {
|
||||||
|
opcode: 'wedo2.motorOn',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'DIRECTION'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
addExtensionOp(weDo2, 'getTilt', {
|
||||||
|
opcode: 'getTiltAngle',
|
||||||
|
argMap: [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
inputOp: 'text',
|
||||||
|
inputName: 'DIRECTION'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = specMap;
|
module.exports = specMap;
|
||||||
|
|
|
@ -13,6 +13,18 @@ const List = require('../engine/list');
|
||||||
const {loadCostume} = require('../import/load-costume.js');
|
const {loadCostume} = require('../import/load-costume.js');
|
||||||
const {loadSound} = require('../import/load-sound.js');
|
const {loadSound} = require('../import/load-sound.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ImportedProject
|
||||||
|
* @property {Array.<Target>} targets - the imported Scratch 3.0 target objects.
|
||||||
|
* @property {ImportedExtensionsInfo} extensionsInfo - the ID of each extension actually used by this project.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ImportedExtensionsInfo
|
||||||
|
* @property {Set.<string>} extensionIDs - the ID of each extension actually in use by blocks in this project.
|
||||||
|
* @property {Map.<string, string>} extensionURLs - map of ID => URL from project metadata. May not match extensionIDs.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the specified VM runtime.
|
* Serializes the specified VM runtime.
|
||||||
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
||||||
|
@ -41,13 +53,14 @@ const serialize = function (runtime) {
|
||||||
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
||||||
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
||||||
* @param {!Runtime} runtime Runtime object to load all structures into.
|
* @param {!Runtime} runtime Runtime object to load all structures into.
|
||||||
* @return {?Target} Target created (stage or sprite).
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||||
|
* @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
|
||||||
*/
|
*/
|
||||||
const parseScratchObject = function (object, runtime) {
|
const parseScratchObject = function (object, runtime, extensions) {
|
||||||
if (!object.hasOwnProperty('name')) {
|
if (!object.hasOwnProperty('name')) {
|
||||||
// Watcher/monitor - skip this object until those are implemented in VM.
|
// Watcher/monitor - skip this object until those are implemented in VM.
|
||||||
// @todo
|
// @todo
|
||||||
return;
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
// Blocks container for this object.
|
// Blocks container for this object.
|
||||||
const blocks = new Blocks();
|
const blocks = new Blocks();
|
||||||
|
@ -61,7 +74,14 @@ const parseScratchObject = function (object, runtime) {
|
||||||
}
|
}
|
||||||
if (object.hasOwnProperty('blocks')) {
|
if (object.hasOwnProperty('blocks')) {
|
||||||
for (const blockId in object.blocks) {
|
for (const blockId in object.blocks) {
|
||||||
blocks.createBlock(object.blocks[blockId]);
|
const blockJSON = object.blockType[blockId];
|
||||||
|
blocks.createBlock(blockJSON);
|
||||||
|
|
||||||
|
const dotIndex = blockJSON.opcode.indexOf('.');
|
||||||
|
if (dotIndex >= 0) {
|
||||||
|
const extensionId = blockJSON.opcode.substring(0, dotIndex);
|
||||||
|
extensions.extensionIDs.add(extensionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// console.log(blocks);
|
// console.log(blocks);
|
||||||
}
|
}
|
||||||
|
@ -155,14 +175,23 @@ const parseScratchObject = function (object, runtime) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deserializes the specified representation of a VM runtime and loads it into
|
* Deserialize the specified representation of a VM runtime and loads it into the provided runtime instance.
|
||||||
* the provided runtime instance.
|
* TODO: parse extension info (also, design extension info storage...)
|
||||||
* @param {object} json JSON representation of a VM runtime.
|
* @param {object} json - JSON representation of a VM runtime.
|
||||||
* @param {Runtime} runtime Runtime instance
|
* @param {Runtime} runtime - Runtime instance
|
||||||
* @returns {Promise} Promise that resolves to the list of targets after the project is deserialized
|
* @returns {Promise.<ImportedProject>} Promise that resolves to the list of targets after the project is deserialized
|
||||||
*/
|
*/
|
||||||
const deserialize = function (json, runtime) {
|
const deserialize = function (json, runtime) {
|
||||||
return Promise.all((json.targets || []).map(target => parseScratchObject(target, runtime)));
|
const extensions = {
|
||||||
|
extensionIDs: new Set(),
|
||||||
|
extensionURLs: new Map()
|
||||||
|
};
|
||||||
|
return Promise.all(
|
||||||
|
(json.targets || []).map(target => parseScratchObject(target, runtime, extensions))
|
||||||
|
).then(targets => ({
|
||||||
|
targets,
|
||||||
|
extensions
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -33,7 +33,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* The "currently editing"/selected target ID for the VM.
|
* The "currently editing"/selected target ID for the VM.
|
||||||
* Block events from any Blockly workspace are routed to this target.
|
* Block events from any Blockly workspace are routed to this target.
|
||||||
* @type {!string}
|
* @type {Target}
|
||||||
*/
|
*/
|
||||||
this.editingTarget = null;
|
this.editingTarget = null;
|
||||||
// Runtime emits are passed along as VM emits.
|
// Runtime emits are passed along as VM emits.
|
||||||
|
@ -228,19 +228,41 @@ class VirtualMachine extends EventEmitter {
|
||||||
deserializer = sb2;
|
deserializer = sb2;
|
||||||
}
|
}
|
||||||
|
|
||||||
return deserializer.deserialize(json, this.runtime).then(targets => {
|
return deserializer.deserialize(json, this.runtime)
|
||||||
this.clear();
|
.then(({targets, extensions}) =>
|
||||||
for (let n = 0; n < targets.length; n++) {
|
this.installTargets(targets, extensions, true));
|
||||||
if (targets[n] !== null) {
|
}
|
||||||
this.runtime.targets.push(targets[n]);
|
|
||||||
targets[n].updateAllDrawableProperties();
|
/**
|
||||||
}
|
* Install `deserialize` results: zero or more targets after the extensions (if any) used by those targets.
|
||||||
|
* @param {Array.<Target>} targets - the targets to be installed
|
||||||
|
* @param {ImportedExtensionsInfo} extensions - metadata about extensions used by these targets
|
||||||
|
* @param {boolean} wholeProject - set to true if installing a whole project, as opposed to a single sprite.
|
||||||
|
*/
|
||||||
|
installTargets (targets, extensions, wholeProject) {
|
||||||
|
const extensionPromises = [];
|
||||||
|
extensions.extensionIDs.forEach(extensionID => {
|
||||||
|
if (!this.extensionManager.isExtensionLoaded(extensionID)) {
|
||||||
|
const extensionURL = extensions.extensionURLs.get(extensionID) || extensionID;
|
||||||
|
extensionPromises.push(this.extensionManager.loadExtensionURL(extensionURL));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
targets = targets.filter(target => !!target);
|
||||||
|
|
||||||
|
Promise.all(extensionPromises).then(() => {
|
||||||
|
if (wholeProject) {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
targets.forEach(target => {
|
||||||
|
this.runtime.targets.push(target);
|
||||||
|
(/** @type RenderedTarget */ target).updateAllDrawableProperties();
|
||||||
|
});
|
||||||
// Select the first target for editing, e.g., the first sprite.
|
// Select the first target for editing, e.g., the first sprite.
|
||||||
if (this.runtime.targets.length > 1) {
|
if (wholeProject && (targets.length > 1)) {
|
||||||
this.editingTarget = this.runtime.targets[1];
|
this.editingTarget = targets[1];
|
||||||
} else {
|
} else {
|
||||||
this.editingTarget = this.runtime.targets[0];
|
this.editingTarget = targets[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the VM user's knowledge of targets and blocks on the workspace.
|
// Update the VM user's knowledge of targets and blocks on the workspace.
|
||||||
|
@ -267,17 +289,9 @@ class VirtualMachine extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select new sprite.
|
return sb2.deserialize(json, this.runtime, true)
|
||||||
return sb2.deserialize(json, this.runtime, true).then(targets => {
|
.then(({targets, extensions}) =>
|
||||||
this.runtime.targets.push(targets[0]);
|
this.installTargets(targets, extensions, false));
|
||||||
this.editingTarget = targets[0];
|
|
||||||
this.editingTarget.updateAllDrawableProperties();
|
|
||||||
|
|
||||||
// Update the VM user's knowledge of targets and blocks on the workspace.
|
|
||||||
this.emitTargetsUpdate();
|
|
||||||
this.emitWorkspaceUpdate();
|
|
||||||
this.runtime.setEditingTarget(this.editingTarget);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue