mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 23:12:24 -05:00
Merge changes from LLK/develop
This commit is contained in:
commit
256758122d
18 changed files with 693 additions and 302 deletions
81
package-lock.json
generated
81
package-lock.json
generated
|
@ -1089,9 +1089,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vernier/godirect": {
|
"@vernier/godirect": {
|
||||||
"version": "1.4.1",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vernier/godirect/-/godirect-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vernier/godirect/-/godirect-1.5.0.tgz",
|
||||||
"integrity": "sha512-5RaBF0UOLJR85UWlPTUv4c7aE3OW3wtaWd6MFEEhVrHTfUuEJPq7444U8tC4hKZnrNyppV/bCN9JtSmN0OlHkg=="
|
"integrity": "sha512-vMS0fQubI3WSSLg1Ry3aey/qWCl9XoCsFzgwOWYkeJs45YxjPel+42pLh5pO7uP6oF47sjZUKx8kGOoTiiiirA=="
|
||||||
},
|
},
|
||||||
"@webassemblyjs/ast": {
|
"@webassemblyjs/ast": {
|
||||||
"version": "1.5.13",
|
"version": "1.5.13",
|
||||||
|
@ -1673,14 +1673,6 @@
|
||||||
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
|
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"async": {
|
|
||||||
"version": "2.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
|
|
||||||
"integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
|
|
||||||
"requires": {
|
|
||||||
"lodash": "^4.14.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"async-each": {
|
"async-each": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
|
||||||
|
@ -3228,7 +3220,8 @@
|
||||||
"crc32": {
|
"crc32": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/crc32/-/crc32-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/crc32/-/crc32-0.2.2.tgz",
|
||||||
"integrity": "sha1-etIg1v/c0Rn5/BJ6d3LKzqOQpLo="
|
"integrity": "sha1-etIg1v/c0Rn5/BJ6d3LKzqOQpLo=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"create-ecdh": {
|
"create-ecdh": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
|
@ -3440,11 +3433,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deflate-js": {
|
|
||||||
"version": "0.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/deflate-js/-/deflate-js-0.2.3.tgz",
|
|
||||||
"integrity": "sha1-+Fq7WOvFFRowYUdHPVfD5PfkQms="
|
|
||||||
},
|
|
||||||
"del": {
|
"del": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
|
||||||
|
@ -6271,15 +6259,6 @@
|
||||||
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
|
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"gzip-js": {
|
|
||||||
"version": "0.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/gzip-js/-/gzip-js-0.3.2.tgz",
|
|
||||||
"integrity": "sha1-IxF+/usozzhSSN7/Df+tiUg22Ws=",
|
|
||||||
"requires": {
|
|
||||||
"crc32": ">= 0.2.2",
|
|
||||||
"deflate-js": ">= 0.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handle-thing": {
|
"handle-thing": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz",
|
||||||
|
@ -7460,7 +7439,8 @@
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.4",
|
"version": "4.17.4",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
|
||||||
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
|
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"lodash._baseassign": {
|
"lodash._baseassign": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
|
@ -12149,9 +12129,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scratch-blocks": {
|
"scratch-blocks": {
|
||||||
"version": "0.1.0-prerelease.1551865183",
|
"version": "0.1.0-prerelease.1552662801",
|
||||||
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1551865183.tgz",
|
"resolved": "https://registry.npmjs.org/scratch-blocks/-/scratch-blocks-0.1.0-prerelease.1552662801.tgz",
|
||||||
"integrity": "sha512-jb4J40y60HyRCryvFSwW9zXsQwaM2DzP5i9KQkJem9RP8gXOZrr56rXHF9VOQ9jQ06fulG65NMTkCL+BU5ulnw==",
|
"integrity": "sha512-OPTvPvzsV0ryhOGNfV9F0dopsuohIyu0SglzQw7jmEf6eeQ3yW0hOGqUeU2HR3c9F/uS3Uy7PispXhYCAZo/tg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"exports-loader": "0.6.3",
|
"exports-loader": "0.6.3",
|
||||||
|
@ -12171,14 +12151,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scratch-parser": {
|
"scratch-parser": {
|
||||||
"version": "4.3.5",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/scratch-parser/-/scratch-parser-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/scratch-parser/-/scratch-parser-5.0.0.tgz",
|
||||||
"integrity": "sha512-jOHrR9evVnRxnIc7W+1m7S2E5yDyUCbh8xvPueT10mo7AfLprE9lRKAtc6yF3Gxj0Rm/jhyiGBn8crAPc/F4Vg==",
|
"integrity": "sha512-7kjxoxivLgYYvmAJVLOOWnca4CigwuCpgjy9+6UuxOMgSZKO1xqIjxIADupabmh1ZLZZDVe45DBM/CQTdtVDkw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ajv": "6.3.0",
|
"ajv": "6.3.0",
|
||||||
"async": "2.6.0",
|
"jszip": "3.1.5",
|
||||||
"gzip-js": "0.3.2",
|
"pify": "4.0.1"
|
||||||
"jszip": "3.1.5"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": {
|
"ajv": {
|
||||||
|
@ -12190,6 +12169,11 @@
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
"json-schema-traverse": "^0.3.0"
|
"json-schema-traverse": "^0.3.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"pify": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -12248,13 +12232,6 @@
|
||||||
"js-md5": "0.7.3",
|
"js-md5": "0.7.3",
|
||||||
"minilog": "3.1.0",
|
"minilog": "3.1.0",
|
||||||
"text-encoding": "^0.7.0"
|
"text-encoding": "^0.7.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"text-encoding": {
|
|
||||||
"version": "0.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
|
|
||||||
"integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scratch-storage": {
|
"scratch-storage": {
|
||||||
|
@ -12276,19 +12253,13 @@
|
||||||
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
|
||||||
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==",
|
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
|
||||||
"text-encoding": {
|
|
||||||
"version": "0.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
|
|
||||||
"integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==",
|
|
||||||
"dev": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scratch-svg-renderer": {
|
"scratch-svg-renderer": {
|
||||||
"version": "0.2.0-prerelease.20190125192231",
|
"version": "0.2.0-prerelease.20190304180800",
|
||||||
"resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20190125192231.tgz",
|
"resolved": "https://registry.npmjs.org/scratch-svg-renderer/-/scratch-svg-renderer-0.2.0-prerelease.20190304180800.tgz",
|
||||||
"integrity": "sha512-8cOLJsN2zDT2FLcB3wLxew3tzO6fkI25uiaW0c6juQl5zJseANIvP4tc31gaeUG4xSQa1zfk/PpXKPQDGa66Tw==",
|
"integrity": "sha512-fFePDGb65g1YaN/fkBl6hfgvjxr3hE9TMWuAYU1MeK5vVeZA+HPslUyMIvsEvLV5nCqs+0Jv7ievnP39ihhHLQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"base64-js": "1.2.1",
|
"base64-js": "1.2.1",
|
||||||
|
@ -13518,9 +13489,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"text-encoding": {
|
"text-encoding": {
|
||||||
"version": "0.6.4",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
|
||||||
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk="
|
"integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA=="
|
||||||
},
|
},
|
||||||
"text-table": {
|
"text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
"version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
"version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vernier/godirect": "1.4.1",
|
"@vernier/godirect": "1.5.0",
|
||||||
"arraybuffer-loader": "^1.0.6",
|
"arraybuffer-loader": "^1.0.6",
|
||||||
"atob": "2.1.2",
|
"atob": "2.1.2",
|
||||||
"btoa": "1.2.1",
|
"btoa": "1.2.1",
|
||||||
|
@ -43,11 +43,11 @@
|
||||||
"jszip": "^3.1.5",
|
"jszip": "^3.1.5",
|
||||||
"minilog": "3.1.0",
|
"minilog": "3.1.0",
|
||||||
"nets": "3.2.0",
|
"nets": "3.2.0",
|
||||||
"scratch-parser": "4.3.5",
|
"scratch-parser": "5.0.0",
|
||||||
"scratch-sb1-converter": "0.2.7",
|
"scratch-sb1-converter": "0.2.7",
|
||||||
"scratch-translate-extension-languages": "0.0.20181205140428",
|
"scratch-translate-extension-languages": "0.0.20181205140428",
|
||||||
"socket.io-client": "2.0.4",
|
"socket.io-client": "2.0.4",
|
||||||
"text-encoding": "0.6.4",
|
"text-encoding": "0.7.0",
|
||||||
"worker-loader": "^1.1.1"
|
"worker-loader": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -76,6 +76,14 @@ class Scratch3LooksBlocks {
|
||||||
return {min: 0, max: 100};
|
return {min: 0, max: 100};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit for brightness effect
|
||||||
|
* @const {object}
|
||||||
|
*/
|
||||||
|
static get EFFECT_BRIGHTNESS_LIMIT (){
|
||||||
|
return {min: -100, max: 100};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Target} target - collect bubble state for this target. Probably, but not necessarily, a RenderedTarget.
|
* @param {Target} target - collect bubble state for this target. Probably, but not necessarily, a RenderedTarget.
|
||||||
* @returns {BubbleState} the mutable bubble state associated with that target. This will be created if necessary.
|
* @returns {BubbleState} the mutable bubble state associated with that target. This will be created if necessary.
|
||||||
|
@ -484,27 +492,36 @@ class Scratch3LooksBlocks {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clampEffect (effect, value) {
|
||||||
|
let clampedValue = value;
|
||||||
|
switch (effect) {
|
||||||
|
case 'ghost':
|
||||||
|
clampedValue = MathUtil.clamp(value,
|
||||||
|
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.min,
|
||||||
|
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.max);
|
||||||
|
break;
|
||||||
|
case 'brightness':
|
||||||
|
clampedValue = MathUtil.clamp(value,
|
||||||
|
Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.min,
|
||||||
|
Scratch3LooksBlocks.EFFECT_BRIGHTNESS_LIMIT.max);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return clampedValue;
|
||||||
|
}
|
||||||
|
|
||||||
changeEffect (args, util) {
|
changeEffect (args, util) {
|
||||||
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
||||||
const change = Cast.toNumber(args.CHANGE);
|
const change = Cast.toNumber(args.CHANGE);
|
||||||
if (!util.target.effects.hasOwnProperty(effect)) return;
|
if (!util.target.effects.hasOwnProperty(effect)) return;
|
||||||
let newValue = change + util.target.effects[effect];
|
let newValue = change + util.target.effects[effect];
|
||||||
if (effect === 'ghost') {
|
newValue = this.clampEffect(effect, newValue);
|
||||||
newValue = MathUtil.clamp(newValue,
|
|
||||||
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.min,
|
|
||||||
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.max);
|
|
||||||
}
|
|
||||||
util.target.setEffect(effect, newValue);
|
util.target.setEffect(effect, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
setEffect (args, util) {
|
setEffect (args, util) {
|
||||||
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
const effect = Cast.toString(args.EFFECT).toLowerCase();
|
||||||
let value = Cast.toNumber(args.VALUE);
|
let value = Cast.toNumber(args.VALUE);
|
||||||
if (effect === 'ghost') {
|
value = this.clampEffect(effect, value);
|
||||||
value = MathUtil.clamp(value,
|
|
||||||
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.min,
|
|
||||||
Scratch3LooksBlocks.EFFECT_GHOST_LIMIT.max);
|
|
||||||
}
|
|
||||||
util.target.setEffect(effect, value);
|
util.target.setEffect(effect, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,7 @@ class Scratch3MotionBlocks {
|
||||||
targetX = Math.round(stageWidth * (Math.random() - 0.5));
|
targetX = Math.round(stageWidth * (Math.random() - 0.5));
|
||||||
targetY = Math.round(stageHeight * (Math.random() - 0.5));
|
targetY = Math.round(stageHeight * (Math.random() - 0.5));
|
||||||
} else {
|
} else {
|
||||||
|
targetName = Cast.toString(targetName);
|
||||||
const goToTarget = this.runtime.getSpriteTargetByName(targetName);
|
const goToTarget = this.runtime.getSpriteTargetByName(targetName);
|
||||||
if (!goToTarget) return;
|
if (!goToTarget) return;
|
||||||
targetX = goToTarget.x;
|
targetX = goToTarget.x;
|
||||||
|
@ -127,6 +128,7 @@ class Scratch3MotionBlocks {
|
||||||
util.target.setDirection(Math.round(Math.random() * 360) - 180);
|
util.target.setDirection(Math.round(Math.random() * 360) - 180);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
args.TOWARDS = Cast.toString(args.TOWARDS);
|
||||||
const pointTarget = this.runtime.getSpriteTargetByName(args.TOWARDS);
|
const pointTarget = this.runtime.getSpriteTargetByName(args.TOWARDS);
|
||||||
if (!pointTarget) return;
|
if (!pointTarget) return;
|
||||||
targetX = pointTarget.x;
|
targetX = pointTarget.x;
|
||||||
|
|
|
@ -191,6 +191,7 @@ class Scratch3SensingBlocks {
|
||||||
targetX = util.ioQuery('mouse', 'getScratchX');
|
targetX = util.ioQuery('mouse', 'getScratchX');
|
||||||
targetY = util.ioQuery('mouse', 'getScratchY');
|
targetY = util.ioQuery('mouse', 'getScratchY');
|
||||||
} else {
|
} else {
|
||||||
|
args.DISTANCETOMENU = Cast.toString(args.DISTANCETOMENU);
|
||||||
const distTarget = this.runtime.getSpriteTargetByName(
|
const distTarget = this.runtime.getSpriteTargetByName(
|
||||||
args.DISTANCETOMENU
|
args.DISTANCETOMENU
|
||||||
);
|
);
|
||||||
|
@ -282,6 +283,7 @@ class Scratch3SensingBlocks {
|
||||||
if (args.OBJECT === '_stage_') {
|
if (args.OBJECT === '_stage_') {
|
||||||
attrTarget = this.runtime.getTargetForStage();
|
attrTarget = this.runtime.getTargetForStage();
|
||||||
} else {
|
} else {
|
||||||
|
args.OBJECT = Cast.toString(args.OBJECT);
|
||||||
attrTarget = this.runtime.getSpriteTargetByName(args.OBJECT);
|
attrTarget = this.runtime.getSpriteTargetByName(args.OBJECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -735,6 +735,12 @@ class Blocks {
|
||||||
if (this._blocks[e.newParent].inputs.hasOwnProperty(e.newInput)) {
|
if (this._blocks[e.newParent].inputs.hasOwnProperty(e.newInput)) {
|
||||||
oldShadow = this._blocks[e.newParent].inputs[e.newInput].shadow;
|
oldShadow = this._blocks[e.newParent].inputs[e.newInput].shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the block being attached is itself a shadow, make sure to set
|
||||||
|
// both block and shadow to that blocks ID. This happens when adding
|
||||||
|
// inputs to a custom procedure.
|
||||||
|
if (this._blocks[e.id].shadow) oldShadow = e.id;
|
||||||
|
|
||||||
this._blocks[e.newParent].inputs[e.newInput] = {
|
this._blocks[e.newParent].inputs[e.newInput] = {
|
||||||
name: e.newInput,
|
name: e.newInput,
|
||||||
block: e.id,
|
block: e.id,
|
||||||
|
|
|
@ -7,34 +7,22 @@ const BlockType = require('./block-type');
|
||||||
// These extensions are currently built into the VM repository but should not be loaded at startup.
|
// These extensions are currently built into the VM repository but should not be loaded at startup.
|
||||||
// TODO: move these out into a separate repository?
|
// TODO: move these out into a separate repository?
|
||||||
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods
|
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods
|
||||||
const Scratch3PenBlocks = require('../extensions/scratch3_pen');
|
|
||||||
const Scratch3WeDo2Blocks = require('../extensions/scratch3_wedo2');
|
|
||||||
const Scratch3MusicBlocks = require('../extensions/scratch3_music');
|
|
||||||
const Scratch3MicroBitBlocks = require('../extensions/scratch3_microbit');
|
|
||||||
const Scratch3Text2SpeechBlocks = require('../extensions/scratch3_text2speech');
|
|
||||||
const Scratch3TranslateBlocks = require('../extensions/scratch3_translate');
|
|
||||||
const Scratch3VideoSensingBlocks = require('../extensions/scratch3_video_sensing');
|
|
||||||
const Scratch3Speech2TextBlocks = require('../extensions/scratch3_speech2text');
|
|
||||||
const Scratch3Ev3Blocks = require('../extensions/scratch3_ev3');
|
|
||||||
const Scratch3MakeyMakeyBlocks = require('../extensions/scratch3_makeymakey');
|
|
||||||
const Scratch3BoostBlocks = require('../extensions/scratch3_boost');
|
|
||||||
// todo: only load this extension once we have a compatible way to load its
|
|
||||||
// Vernier module dependency.
|
|
||||||
// const Scratch3GdxForBlocks = require('../extensions/scratch3_gdx_for');
|
|
||||||
|
|
||||||
const builtinExtensions = {
|
const builtinExtensions = {
|
||||||
pen: Scratch3PenBlocks,
|
pen: () => require('../extensions/scratch3_pen'),
|
||||||
wedo2: Scratch3WeDo2Blocks,
|
wedo2: () => require('../extensions/scratch3_wedo2'),
|
||||||
music: Scratch3MusicBlocks,
|
music: () => require('../extensions/scratch3_music'),
|
||||||
microbit: Scratch3MicroBitBlocks,
|
microbit: () => require('../extensions/scratch3_microbit'),
|
||||||
text2speech: Scratch3Text2SpeechBlocks,
|
text2speech: () => require('../extensions/scratch3_text2speech'),
|
||||||
translate: Scratch3TranslateBlocks,
|
translate: () => require('../extensions/scratch3_translate'),
|
||||||
videoSensing: Scratch3VideoSensingBlocks,
|
videoSensing: () => require('../extensions/scratch3_video_sensing'),
|
||||||
speech2text: Scratch3Speech2TextBlocks,
|
speech2text: () => require('../extensions/scratch3_speech2text'),
|
||||||
ev3: Scratch3Ev3Blocks,
|
ev3: () => require('../extensions/scratch3_ev3'),
|
||||||
makeymakey: Scratch3MakeyMakeyBlocks,
|
makeymakey: () => require('../extensions/scratch3_makeymakey'),
|
||||||
boost: Scratch3BoostBlocks
|
boost: () => require('../extensions/scratch3_boost')
|
||||||
// gdxfor: Scratch3GdxForBlocks
|
// todo: only load this extension once we have a compatible way to load its
|
||||||
|
// Vernier module dependency.
|
||||||
|
// gdxfor: () => require('../extensions/scratch3_gdx_for')
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -135,7 +123,7 @@ class ExtensionManager {
|
||||||
return Promise.reject(new Error(message));
|
return Promise.reject(new Error(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
const extension = builtinExtensions[extensionURL];
|
const extension = builtinExtensions[extensionURL]();
|
||||||
const extensionInstance = new extension(this.runtime);
|
const extensionInstance = new extension(this.runtime);
|
||||||
return this._registerInternalExtension(extensionInstance).then(serviceName => {
|
return this._registerInternalExtension(extensionInstance).then(serviceName => {
|
||||||
this._loadedExtensions.set(extensionURL, serviceName);
|
this._loadedExtensions.set(extensionURL, serviceName);
|
||||||
|
|
|
@ -65,6 +65,41 @@ const GIANT_ID = 'GIANT';
|
||||||
*/
|
*/
|
||||||
const KITTEN_ID = 'KITTEN';
|
const KITTEN_ID = 'KITTEN';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playback rate for the tenor voice, for cases where we have only a female gender voice.
|
||||||
|
*/
|
||||||
|
const FEMALE_TENOR_RATE = 0.89; // -2 semitones
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playback rate for the giant voice, for cases where we have only a female gender voice.
|
||||||
|
*/
|
||||||
|
const FEMALE_GIANT_RATE = 0.79; // -4 semitones
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language ids. The value for each language id is a valid Scratch locale.
|
||||||
|
*/
|
||||||
|
const CHINESE_ID = 'zh-cn';
|
||||||
|
const DANISH_ID = 'da';
|
||||||
|
const DUTCH_ID = 'nl';
|
||||||
|
const ENGLISH_ID = 'en';
|
||||||
|
const FRENCH_ID = 'fr';
|
||||||
|
const GERMAN_ID = 'de';
|
||||||
|
const ICELANDIC_ID = 'is';
|
||||||
|
const ITALIAN_ID = 'it';
|
||||||
|
const JAPANESE_ID = 'ja';
|
||||||
|
const KOREAN_ID = 'ko';
|
||||||
|
const NORWEGIAN_ID = 'nb';
|
||||||
|
const POLISH_ID = 'pl';
|
||||||
|
const PORTUGUESE_BR_ID = 'pt-br';
|
||||||
|
const PORTUGUESE_ID = 'pt';
|
||||||
|
const ROMANIAN_ID = 'ro';
|
||||||
|
const RUSSIAN_ID = 'ru';
|
||||||
|
const SPANISH_ID = 'es';
|
||||||
|
const SPANISH_419_ID = 'es-419';
|
||||||
|
const SWEDISH_ID = 'sv';
|
||||||
|
const TURKISH_ID = 'tr';
|
||||||
|
const WELSH_ID = 'cy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for the text2speech blocks.
|
* Class for the text2speech blocks.
|
||||||
* @constructor
|
* @constructor
|
||||||
|
@ -92,6 +127,12 @@ class Scratch3Text2SpeechBlocks {
|
||||||
if (this.runtime) {
|
if (this.runtime) {
|
||||||
runtime.on('targetWasCreated', this._onTargetCreated);
|
runtime.on('targetWasCreated', this._onTargetCreated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of all Scratch locales that are supported by the extension.
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
this._supportedLocales = this._getSupportedLocales();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,55 +189,140 @@ class Scratch3Text2SpeechBlocks {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object with language names mapped to their language codes.
|
* An object with information for each language.
|
||||||
|
*
|
||||||
|
* A note on the different sets of locales referred to in this extension:
|
||||||
|
*
|
||||||
|
* SCRATCH LOCALE
|
||||||
|
* Set by the editor, and used to store the language state in the project.
|
||||||
|
* Listed in l10n: https://github.com/LLK/scratch-l10n/blob/master/src/supported-locales.js
|
||||||
|
* SUPPORTED LOCALE
|
||||||
|
* A Scratch locale that has a corresponding extension locale.
|
||||||
|
* EXTENSION LOCALE
|
||||||
|
* A locale corresponding to one of the available spoken languages
|
||||||
|
* in the extension. There can be multiple supported locales for a single
|
||||||
|
* extension locale. For example, for both written versions of chinese,
|
||||||
|
* zh-cn and zh-tw, we use a single spoken language (Mandarin). So there
|
||||||
|
* are two supported locales, with a single extension locale.
|
||||||
|
* SPEECH SYNTH LOCALE
|
||||||
|
* A different locale code system, used by our speech synthesis service.
|
||||||
|
* Each extension locale has a speech synth locale.
|
||||||
*/
|
*/
|
||||||
get LANGUAGE_INFO () {
|
get LANGUAGE_INFO () {
|
||||||
return {
|
return {
|
||||||
'Danish': 'da',
|
[CHINESE_ID]: {
|
||||||
'Dutch': 'nl',
|
name: 'Chinese (Mandarin)',
|
||||||
'English': 'en',
|
locales: ['zh-cn', 'zh-tw'],
|
||||||
'French': 'fr',
|
speechSynthLocale: 'cmn-CN',
|
||||||
'German': 'de',
|
singleGender: true
|
||||||
'Icelandic': 'is',
|
},
|
||||||
'Italian': 'it',
|
[DANISH_ID]: {
|
||||||
'Japanese': 'ja',
|
name: 'Danish',
|
||||||
'Polish': 'pl',
|
locales: ['da'],
|
||||||
'Portuguese (Brazilian)': 'pt-br',
|
speechSynthLocale: 'da-DK'
|
||||||
'Portuguese (European)': 'pt',
|
},
|
||||||
'Russian': 'ru',
|
[DUTCH_ID]: {
|
||||||
'Spanish (European)': 'es',
|
name: 'Dutch',
|
||||||
'Spanish (Latin American)': 'es-419'
|
locales: ['nl'],
|
||||||
};
|
speechSynthLocale: 'nl-NL'
|
||||||
|
},
|
||||||
|
[ENGLISH_ID]: {
|
||||||
|
name: 'English',
|
||||||
|
locales: ['en'],
|
||||||
|
speechSynthLocale: 'en-US'
|
||||||
|
},
|
||||||
|
[FRENCH_ID]: {
|
||||||
|
name: 'French',
|
||||||
|
locales: ['fr'],
|
||||||
|
speechSynthLocale: 'fr-FR'
|
||||||
|
},
|
||||||
|
[GERMAN_ID]: {
|
||||||
|
name: 'German',
|
||||||
|
locales: ['de'],
|
||||||
|
speechSynthLocale: 'de-DE'
|
||||||
|
},
|
||||||
|
[ICELANDIC_ID]: {
|
||||||
|
name: 'Icelandic',
|
||||||
|
locales: ['is'],
|
||||||
|
speechSynthLocale: 'is-IS'
|
||||||
|
},
|
||||||
|
[ITALIAN_ID]: {
|
||||||
|
name: 'Italian',
|
||||||
|
locales: ['it'],
|
||||||
|
speechSynthLocale: 'it-IT'
|
||||||
|
},
|
||||||
|
[JAPANESE_ID]: {
|
||||||
|
name: 'Japanese',
|
||||||
|
locales: ['ja', 'ja-Hira'],
|
||||||
|
speechSynthLocale: 'ja-JP'
|
||||||
|
},
|
||||||
|
[KOREAN_ID]: {
|
||||||
|
name: 'Korean',
|
||||||
|
locales: ['ko'],
|
||||||
|
speechSynthLocale: 'ko-KR',
|
||||||
|
singleGender: true
|
||||||
|
},
|
||||||
|
[NORWEGIAN_ID]: {
|
||||||
|
name: 'Norwegian',
|
||||||
|
locales: ['nb', 'nn'],
|
||||||
|
speechSynthLocale: 'nb-NO',
|
||||||
|
singleGender: true
|
||||||
|
},
|
||||||
|
[POLISH_ID]: {
|
||||||
|
name: 'Polish',
|
||||||
|
locales: ['pl'],
|
||||||
|
speechSynthLocale: 'pl-PL'
|
||||||
|
},
|
||||||
|
[PORTUGUESE_BR_ID]: {
|
||||||
|
name: 'Portuguese (Brazilian)',
|
||||||
|
locales: ['pt-br'],
|
||||||
|
speechSynthLocale: 'pt-BR'
|
||||||
|
},
|
||||||
|
[PORTUGUESE_ID]: {
|
||||||
|
name: 'Portuguese (European)',
|
||||||
|
locales: ['pt'],
|
||||||
|
speechSynthLocale: 'pt-PT'
|
||||||
|
},
|
||||||
|
[ROMANIAN_ID]: {
|
||||||
|
name: 'Romanian',
|
||||||
|
locales: ['ro'],
|
||||||
|
speechSynthLocale: 'ro-RO',
|
||||||
|
singleGender: true
|
||||||
|
},
|
||||||
|
[RUSSIAN_ID]: {
|
||||||
|
name: 'Russian',
|
||||||
|
locales: ['ru'],
|
||||||
|
speechSynthLocale: 'ru-RU'
|
||||||
|
},
|
||||||
|
[SPANISH_ID]: {
|
||||||
|
name: 'Spanish (European)',
|
||||||
|
locales: ['es'],
|
||||||
|
speechSynthLocale: 'es-ES'
|
||||||
|
},
|
||||||
|
[SPANISH_419_ID]: {
|
||||||
|
name: 'Spanish (Latin American)',
|
||||||
|
locales: ['es-419'],
|
||||||
|
speechSynthLocale: 'es-US'
|
||||||
|
},
|
||||||
|
[SWEDISH_ID]: {
|
||||||
|
name: 'Swedish',
|
||||||
|
locales: ['sv'],
|
||||||
|
speechSynthLocale: 'sv-SE',
|
||||||
|
singleGender: true
|
||||||
|
},
|
||||||
|
[TURKISH_ID]: {
|
||||||
|
name: 'Turkish',
|
||||||
|
locales: ['tr'],
|
||||||
|
speechSynthLocale: 'tr-TR',
|
||||||
|
singleGender: true
|
||||||
|
},
|
||||||
|
[WELSH_ID]: {
|
||||||
|
name: 'Welsh',
|
||||||
|
locales: ['cy'],
|
||||||
|
speechSynthLocale: 'cy-GB',
|
||||||
|
singleGender: true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a temporary adapter to convert Scratch locale codes to Amazon polly's locale codes.
|
|
||||||
* @todo remove this once the speech synthesis server can perform this conversion
|
|
||||||
* @param {string} locale the Scratch locale to convert.
|
|
||||||
* @return {string} the Amazon polly locale.
|
|
||||||
*/
|
|
||||||
localeToPolly (locale) {
|
|
||||||
const pollyLocales = {
|
|
||||||
'da': 'da-DK', // Danish
|
|
||||||
'nl': 'nl-NL', // Dutch
|
|
||||||
'en': 'en-US', // English
|
|
||||||
'fr': 'fr-FR', // French
|
|
||||||
'de': 'de-DE', // German
|
|
||||||
'is': 'is-IS', // Icelandic
|
|
||||||
'it': 'it-IT', // Italian
|
|
||||||
'ja': 'ja-JP', // Japanese
|
|
||||||
'pl': 'pl-PL', // Polish
|
|
||||||
'pt-br': 'pt-BR', // Portuguese (Brazilian)
|
|
||||||
'pt': 'pt-PT', // Portuguese (European)
|
|
||||||
'ru': 'ru-RU', // Russian
|
|
||||||
'es': 'es-ES', // Spanish (European)
|
|
||||||
'es-419': 'es-US' // Spanish (Latin American)
|
|
||||||
};
|
};
|
||||||
let converted = 'en-US';
|
|
||||||
if (pollyLocales[locale]) {
|
|
||||||
converted = pollyLocales[locale];
|
|
||||||
}
|
|
||||||
return converted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -222,7 +348,7 @@ class Scratch3Text2SpeechBlocks {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get DEFAULT_LANGUAGE () {
|
get DEFAULT_LANGUAGE () {
|
||||||
return 'en';
|
return ENGLISH_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -272,7 +398,11 @@ class Scratch3Text2SpeechBlocks {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'text2speech',
|
id: 'text2speech',
|
||||||
name: 'Text to Speech',
|
name: formatMessage({
|
||||||
|
id: 'text2speech.categoryName',
|
||||||
|
default: 'Text to Speech',
|
||||||
|
description: 'Name of the Text to Speech extension.'
|
||||||
|
}),
|
||||||
blockIconURI: blockIconURI,
|
blockIconURI: blockIconURI,
|
||||||
menuIconURI: menuIconURI,
|
menuIconURI: menuIconURI,
|
||||||
blocks: [
|
blocks: [
|
||||||
|
@ -334,7 +464,7 @@ class Scratch3Text2SpeechBlocks {
|
||||||
/**
|
/**
|
||||||
* Get the language code currently set in the editor, or fall back to the
|
* Get the language code currently set in the editor, or fall back to the
|
||||||
* browser locale.
|
* browser locale.
|
||||||
* @return {string} the language code.
|
* @return {string} a Scratch locale code.
|
||||||
*/
|
*/
|
||||||
getEditorLanguage () {
|
getEditorLanguage () {
|
||||||
return formatMessage.setup().locale ||
|
return formatMessage.setup().locale ||
|
||||||
|
@ -342,8 +472,8 @@ class Scratch3Text2SpeechBlocks {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the language for speech synthesis.
|
* Get the language code currently set for the extension.
|
||||||
* @returns {string} the language code.
|
* @returns {string} a Scratch locale code.
|
||||||
*/
|
*/
|
||||||
getCurrentLanguage () {
|
getCurrentLanguage () {
|
||||||
const stage = this.runtime.getTargetForStage();
|
const stage = this.runtime.getTargetForStage();
|
||||||
|
@ -356,17 +486,18 @@ class Scratch3Text2SpeechBlocks {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the language for speech synthesis.
|
* Set the language code for the extension.
|
||||||
* It is stored in the stage so it can be saved and loaded with the project.
|
* It is stored in the stage so it can be saved and loaded with the project.
|
||||||
* @param {string} languageCode a locale code to set.
|
* @param {string} locale a locale code.
|
||||||
*/
|
*/
|
||||||
setCurrentLanguage (languageCode) {
|
setCurrentLanguage (locale) {
|
||||||
const stage = this.runtime.getTargetForStage();
|
const stage = this.runtime.getTargetForStage();
|
||||||
if (!stage) return;
|
if (!stage) return;
|
||||||
// Only set the language if it is in the list.
|
|
||||||
if (this.isSupportedLanguage(languageCode)) {
|
if (this.isSupportedLanguage(locale)) {
|
||||||
stage.textToSpeechLanguage = languageCode;
|
stage.textToSpeechLanguage = this._getExtensionLocaleForSupportedLocale(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the language is null, set it to the default language.
|
// If the language is null, set it to the default language.
|
||||||
// This can occur e.g. if the extension was loaded with the editor
|
// This can occur e.g. if the extension was loaded with the editor
|
||||||
// set to a language that is not in the list.
|
// set to a language that is not in the list.
|
||||||
|
@ -376,13 +507,49 @@ class Scratch3Text2SpeechBlocks {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a language code is in the list of supported languages for the
|
* Get the extension locale for a supported locale, or null.
|
||||||
|
* @param {string} locale a locale code.
|
||||||
|
* @returns {?string} a locale supported by the extension.
|
||||||
|
*/
|
||||||
|
_getExtensionLocaleForSupportedLocale (locale) {
|
||||||
|
for (const lang in this.LANGUAGE_INFO) {
|
||||||
|
if (this.LANGUAGE_INFO[lang].locales.includes(locale)) {
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.error(`cannot find extension locale for locale ${locale}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the locale code used by the speech synthesis server corresponding to
|
||||||
|
* the current language code set for the extension.
|
||||||
|
* @returns {string} a speech synthesis locale.
|
||||||
|
*/
|
||||||
|
_getSpeechSynthLocale () {
|
||||||
|
let speechSynthLocale = this.LANGUAGE_INFO[this.DEFAULT_LANGUAGE].speechSynthLocale;
|
||||||
|
if (this.LANGUAGE_INFO[this.getCurrentLanguage()]) {
|
||||||
|
speechSynthLocale = this.LANGUAGE_INFO[this.getCurrentLanguage()].speechSynthLocale;
|
||||||
|
}
|
||||||
|
return speechSynthLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of the locales supported by this extension.
|
||||||
|
* @returns {Array} An array of locale strings.
|
||||||
|
*/
|
||||||
|
_getSupportedLocales () {
|
||||||
|
return Object.keys(this.LANGUAGE_INFO).reduce((acc, lang) =>
|
||||||
|
acc.concat(this.LANGUAGE_INFO[lang].locales), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a Scratch language code is in the list of supported languages for the
|
||||||
* speech synthesis service.
|
* speech synthesis service.
|
||||||
* @param {string} languageCode the language code to check.
|
* @param {string} languageCode the language code to check.
|
||||||
* @returns {boolean} true if the language code is supported.
|
* @returns {boolean} true if the language code is supported.
|
||||||
*/
|
*/
|
||||||
isSupportedLanguage (languageCode) {
|
isSupportedLanguage (languageCode) {
|
||||||
return Object.values(this.LANGUAGE_INFO).includes(languageCode);
|
return this._supportedLocales.includes(languageCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -401,9 +568,9 @@ class Scratch3Text2SpeechBlocks {
|
||||||
* @return {array} the text and value for each menu item.
|
* @return {array} the text and value for each menu item.
|
||||||
*/
|
*/
|
||||||
getLanguageMenu () {
|
getLanguageMenu () {
|
||||||
return Object.keys(this.LANGUAGE_INFO).map(languageName => ({
|
return Object.keys(this.LANGUAGE_INFO).map(key => ({
|
||||||
text: languageName,
|
text: this.LANGUAGE_INFO[key].name,
|
||||||
value: this.LANGUAGE_INFO[languageName]
|
value: key
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,16 +624,29 @@ class Scratch3Text2SpeechBlocks {
|
||||||
speakAndWait (args, util) {
|
speakAndWait (args, util) {
|
||||||
// Cast input to string
|
// Cast input to string
|
||||||
let words = Cast.toString(args.WORDS);
|
let words = Cast.toString(args.WORDS);
|
||||||
let locale = this.localeToPolly(this.getCurrentLanguage());
|
let locale = this._getSpeechSynthLocale();
|
||||||
|
|
||||||
const state = this._getState(util.target);
|
const state = this._getState(util.target);
|
||||||
|
|
||||||
const gender = this.VOICE_INFO[state.voiceId].gender;
|
let gender = this.VOICE_INFO[state.voiceId].gender;
|
||||||
const playbackRate = this.VOICE_INFO[state.voiceId].playbackRate;
|
let playbackRate = this.VOICE_INFO[state.voiceId].playbackRate;
|
||||||
|
|
||||||
|
// Special case for voices where the synthesis service only provides a
|
||||||
|
// single gender voice. In that case, always request the female voice,
|
||||||
|
// and set special playback rates for the tenor and giant voices.
|
||||||
|
if (this.LANGUAGE_INFO[this.getCurrentLanguage()].singleGender) {
|
||||||
|
gender = 'female';
|
||||||
|
if (state.voiceId === TENOR_ID) {
|
||||||
|
playbackRate = FEMALE_TENOR_RATE;
|
||||||
|
}
|
||||||
|
if (state.voiceId === GIANT_ID) {
|
||||||
|
playbackRate = FEMALE_GIANT_RATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (state.voiceId === KITTEN_ID) {
|
if (state.voiceId === KITTEN_ID) {
|
||||||
words = words.replace(/\S+/g, 'meow');
|
words = words.replace(/\S+/g, 'meow');
|
||||||
locale = 'en-US';
|
locale = this.LANGUAGE_INFO[this.DEFAULT_LANGUAGE].speechSynthLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build up URL
|
// Build up URL
|
||||||
|
|
|
@ -32,6 +32,53 @@ const loadVector_ = function (costume, runtime, rotationCenter, optVersion) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canvasPool = (function () {
|
||||||
|
/**
|
||||||
|
* A pool of canvas objects that can be reused to reduce memory
|
||||||
|
* allocations. And time spent in those allocations and the later garbage
|
||||||
|
* collection.
|
||||||
|
*/
|
||||||
|
class CanvasPool {
|
||||||
|
constructor () {
|
||||||
|
this.pool = [];
|
||||||
|
this.clearSoon = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After a short wait period clear the pool to let the VM collect
|
||||||
|
* garbage.
|
||||||
|
*/
|
||||||
|
clear () {
|
||||||
|
if (!this.clearSoon) {
|
||||||
|
this.clearSoon = new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
.then(() => {
|
||||||
|
this.pool.length = 0;
|
||||||
|
this.clearSoon = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a canvas. Create the canvas if the pool is empty.
|
||||||
|
* @returns {HTMLCanvasElement} A canvas element.
|
||||||
|
*/
|
||||||
|
create () {
|
||||||
|
return this.pool.pop() || document.createElement('canvas');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release the canvas to be reused.
|
||||||
|
* @param {HTMLCanvasElement} canvas A canvas element.
|
||||||
|
*/
|
||||||
|
release (canvas) {
|
||||||
|
this.clear();
|
||||||
|
this.pool.push(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CanvasPool();
|
||||||
|
}());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a promise to fetch a bitmap from storage and return it as a canvas
|
* Return a promise to fetch a bitmap from storage and return it as a canvas
|
||||||
* If the costume has bitmapResolution 1, it will be converted to bitmapResolution 2 here (the standard for Scratch 3)
|
* If the costume has bitmapResolution 1, it will be converted to bitmapResolution 2 here (the standard for Scratch 3)
|
||||||
|
@ -54,64 +101,53 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
||||||
return Promise.reject('No V2 Bitmap adapter present.');
|
return Promise.reject('No V2 Bitmap adapter present.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Promise.all([costume.asset, costume.textLayerAsset].map(asset => {
|
||||||
|
if (!asset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof createImageBitmap !== 'undefined') {
|
||||||
|
return createImageBitmap(
|
||||||
|
new Blob([asset.data], {type: asset.assetType.contentType})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const baseImageElement = new Image();
|
const image = new Image();
|
||||||
let textImageElement;
|
image.onload = function () {
|
||||||
|
resolve(image);
|
||||||
// We need to wait for 2 images total to load. loadedOne will be true when one
|
image.onload = null;
|
||||||
// is done, and we are just waiting for one more.
|
image.onerror = null;
|
||||||
let loadedOne = false;
|
};
|
||||||
|
image.onerror = function () {
|
||||||
const onError = function () {
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
removeEventListeners();
|
|
||||||
reject('Costume load failed. Asset could not be read.');
|
reject('Costume load failed. Asset could not be read.');
|
||||||
|
image.onload = null;
|
||||||
|
image.onerror = null;
|
||||||
};
|
};
|
||||||
const onLoad = function () {
|
image.src = asset.encodeDataURI();
|
||||||
if (loadedOne) {
|
});
|
||||||
// eslint-disable-next-line no-use-before-define
|
}))
|
||||||
removeEventListeners();
|
.then(([baseImageElement, textImageElement]) => {
|
||||||
resolve([baseImageElement, textImageElement]);
|
const mergeCanvas = canvasPool.create();
|
||||||
} else {
|
|
||||||
loadedOne = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeEventListeners = function () {
|
|
||||||
baseImageElement.removeEventListener('error', onError);
|
|
||||||
baseImageElement.removeEventListener('load', onLoad);
|
|
||||||
if (textImageElement) {
|
|
||||||
textImageElement.removeEventListener('error', onError);
|
|
||||||
textImageElement.removeEventListener('load', onLoad);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
baseImageElement.addEventListener('load', onLoad);
|
|
||||||
baseImageElement.addEventListener('error', onError);
|
|
||||||
if (costume.textLayerAsset) {
|
|
||||||
textImageElement = new Image();
|
|
||||||
textImageElement.addEventListener('load', onLoad);
|
|
||||||
textImageElement.addEventListener('error', onError);
|
|
||||||
textImageElement.src = costume.textLayerAsset.encodeDataURI();
|
|
||||||
} else {
|
|
||||||
loadedOne = true;
|
|
||||||
}
|
|
||||||
baseImageElement.src = costume.asset.encodeDataURI();
|
|
||||||
}).then(imageElements => {
|
|
||||||
const [baseImageElement, textImageElement] = imageElements;
|
|
||||||
|
|
||||||
let canvas = document.createElement('canvas');
|
|
||||||
const scale = costume.bitmapResolution === 1 ? 2 : 1;
|
const scale = costume.bitmapResolution === 1 ? 2 : 1;
|
||||||
canvas.width = baseImageElement.width;
|
mergeCanvas.width = baseImageElement.width;
|
||||||
canvas.height = baseImageElement.height;
|
mergeCanvas.height = baseImageElement.height;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = mergeCanvas.getContext('2d');
|
||||||
ctx.drawImage(baseImageElement, 0, 0);
|
ctx.drawImage(baseImageElement, 0, 0);
|
||||||
if (textImageElement) {
|
if (textImageElement) {
|
||||||
ctx.drawImage(textImageElement, 0, 0);
|
ctx.drawImage(textImageElement, 0, 0);
|
||||||
}
|
}
|
||||||
|
// Track the canvas we merged the bitmaps onto separately from the
|
||||||
|
// canvas that we receive from resize if scale is not 1. We know
|
||||||
|
// resize treats mergeCanvas as read only data. We don't know when
|
||||||
|
// resize may use or modify the canvas. So we'll only release the
|
||||||
|
// mergeCanvas back into the canvas pool. Reusing the canvas from
|
||||||
|
// resize may cause errors.
|
||||||
|
let canvas = mergeCanvas;
|
||||||
if (scale !== 1) {
|
if (scale !== 1) {
|
||||||
canvas = runtime.v2BitmapAdapter.resize(canvas, canvas.width * scale, canvas.height * scale);
|
canvas = runtime.v2BitmapAdapter.resize(mergeCanvas, canvas.width * scale, canvas.height * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
// By scaling, we've converted it to bitmap resolution 2
|
// By scaling, we've converted it to bitmap resolution 2
|
||||||
|
@ -128,8 +164,9 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
||||||
delete costume.textLayerAsset;
|
delete costume.textLayerAsset;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canvas: canvas,
|
canvas,
|
||||||
rotationCenter: rotationCenter,
|
mergeCanvas,
|
||||||
|
rotationCenter,
|
||||||
// True if the asset matches the base layer; false if it required adjustment
|
// True if the asset matches the base layer; false if it required adjustment
|
||||||
assetMatchesBase: scale === 1 && !textImageElement
|
assetMatchesBase: scale === 1 && !textImageElement
|
||||||
};
|
};
|
||||||
|
@ -141,12 +178,17 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadBitmap_ = function (costume, runtime, rotationCenter) {
|
const loadBitmap_ = function (costume, runtime, _rotationCenter) {
|
||||||
return fetchBitmapCanvas_(costume, runtime, rotationCenter).then(fetched => new Promise(resolve => {
|
return fetchBitmapCanvas_(costume, runtime, _rotationCenter)
|
||||||
rotationCenter = fetched.rotationCenter;
|
.then(fetched => {
|
||||||
|
|
||||||
const updateCostumeAsset = function (dataURI) {
|
const updateCostumeAsset = function (dataURI) {
|
||||||
if (!runtime.v2BitmapAdapter) {
|
if (!runtime.v2BitmapAdapter) {
|
||||||
|
// TODO: This might be a bad practice since the returned
|
||||||
|
// promise isn't acted on. If this is something we should be
|
||||||
|
// creating a rejected promise for we should also catch it
|
||||||
|
// somewhere and act on that error (like logging).
|
||||||
|
//
|
||||||
|
// Return a rejection to stop executing updateCostumeAsset.
|
||||||
return Promise.reject('No V2 Bitmap adapter present.');
|
return Promise.reject('No V2 Bitmap adapter present.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,11 +208,13 @@ const loadBitmap_ = function (costume, runtime, rotationCenter) {
|
||||||
if (!fetched.assetMatchesBase) {
|
if (!fetched.assetMatchesBase) {
|
||||||
updateCostumeAsset(fetched.canvas.toDataURL());
|
updateCostumeAsset(fetched.canvas.toDataURL());
|
||||||
}
|
}
|
||||||
resolve(fetched.canvas);
|
|
||||||
}))
|
return fetched;
|
||||||
.then(canvas => {
|
})
|
||||||
|
.then(({canvas, mergeCanvas, rotationCenter}) => {
|
||||||
// createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
|
// createBitmapSkin does the right thing if costume.bitmapResolution or rotationCenter are undefined...
|
||||||
costume.skinId = runtime.renderer.createBitmapSkin(canvas, costume.bitmapResolution, rotationCenter);
|
costume.skinId = runtime.renderer.createBitmapSkin(canvas, costume.bitmapResolution, rotationCenter);
|
||||||
|
canvasPool.release(mergeCanvas);
|
||||||
const renderSize = runtime.renderer.getSkinSize(costume.skinId);
|
const renderSize = runtime.renderer.getSkinSize(costume.skinId);
|
||||||
costume.size = [renderSize[0] * 2, renderSize[1] * 2]; // Actual size, since all bitmaps are resolution 2
|
costume.size = [renderSize[0] * 2, renderSize[1] * 2]; // Actual size, since all bitmaps are resolution 2
|
||||||
|
|
||||||
|
|
|
@ -73,14 +73,19 @@ class BLE extends JSONRPCWebSocket {
|
||||||
* Close the websocket.
|
* Close the websocket.
|
||||||
*/
|
*/
|
||||||
disconnect () {
|
disconnect () {
|
||||||
if (!this._connected) return;
|
if (this._ws.readyState === this._ws.OPEN) {
|
||||||
|
|
||||||
this._ws.close();
|
this._ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._connected) {
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._discoverTimeoutID) {
|
if (this._discoverTimeoutID) {
|
||||||
window.clearTimeout(this._discoverTimeoutID);
|
window.clearTimeout(this._discoverTimeoutID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets connection status icon to orange
|
||||||
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED);
|
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,14 +75,19 @@ class BT extends JSONRPCWebSocket {
|
||||||
* Close the websocket.
|
* Close the websocket.
|
||||||
*/
|
*/
|
||||||
disconnect () {
|
disconnect () {
|
||||||
if (!this._connected) return;
|
if (this._ws.readyState === this._ws.OPEN) {
|
||||||
|
|
||||||
this._ws.close();
|
this._ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._connected) {
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._discoverTimeoutID) {
|
if (this._discoverTimeoutID) {
|
||||||
window.clearTimeout(this._discoverTimeoutID);
|
window.clearTimeout(this._discoverTimeoutID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets connection status icon to orange
|
||||||
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED);
|
this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -823,46 +823,32 @@ const deserializeBlocks = function (blocks) {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
* Parse the assets of a single "Scratch object" and load them. This
|
||||||
|
* preprocesses objects to support loading the data for those assets over a
|
||||||
|
* network while the objects are further processed into Blocks, Sprites, and a
|
||||||
|
* list of needed Extensions.
|
||||||
* @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.
|
||||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
|
||||||
* @param {JSZip} zip Sb3 file describing this project (to load assets from)
|
* @param {JSZip} zip Sb3 file describing this project (to load assets from)
|
||||||
* @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
|
* @return {?{costumePromises:Array.<Promise>,soundPromises:Array.<Promise>,soundBank:SoundBank}}
|
||||||
|
* Object of arrays of promises for asset objects used in Sprites. As well as a
|
||||||
|
* SoundBank for the sound assets. null for unsupported objects.
|
||||||
*/
|
*/
|
||||||
const parseScratchObject = function (object, runtime, extensions, zip) {
|
const parseScratchAssets = function (object, runtime, zip) {
|
||||||
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 Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
// Blocks container for this object.
|
|
||||||
const blocks = new Blocks(runtime);
|
|
||||||
|
|
||||||
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
|
const assets = {
|
||||||
const sprite = new Sprite(blocks, runtime);
|
costumePromises: null,
|
||||||
|
soundPromises: null,
|
||||||
|
soundBank: runtime.audioEngine && runtime.audioEngine.createBank()
|
||||||
|
};
|
||||||
|
|
||||||
// Sprite/stage name from JSON.
|
|
||||||
if (object.hasOwnProperty('name')) {
|
|
||||||
sprite.name = object.name;
|
|
||||||
}
|
|
||||||
if (object.hasOwnProperty('blocks')) {
|
|
||||||
deserializeBlocks(object.blocks);
|
|
||||||
// Take a second pass to create objects and add extensions
|
|
||||||
for (const blockId in object.blocks) {
|
|
||||||
if (!object.blocks.hasOwnProperty(blockId)) continue;
|
|
||||||
const blockJSON = object.blocks[blockId];
|
|
||||||
blocks.createBlock(blockJSON);
|
|
||||||
|
|
||||||
// If the block is from an extension, record it.
|
|
||||||
const extensionID = getExtensionIdForOpcode(blockJSON.opcode);
|
|
||||||
if (extensionID) {
|
|
||||||
extensions.extensionIDs.add(extensionID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Costumes from JSON.
|
// Costumes from JSON.
|
||||||
const costumePromises = (object.costumes || []).map(costumeSource => {
|
assets.costumePromises = (object.costumes || []).map(costumeSource => {
|
||||||
// @todo: Make sure all the relevant metadata is being pulled out.
|
// @todo: Make sure all the relevant metadata is being pulled out.
|
||||||
const costume = {
|
const costume = {
|
||||||
// costumeSource only has an asset if an image is being uploaded as
|
// costumeSource only has an asset if an image is being uploaded as
|
||||||
|
@ -894,7 +880,7 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
// process has been completed
|
// process has been completed
|
||||||
});
|
});
|
||||||
// Sounds from JSON
|
// Sounds from JSON
|
||||||
const soundPromises = (object.sounds || []).map(soundSource => {
|
assets.soundPromises = (object.sounds || []).map(soundSource => {
|
||||||
const sound = {
|
const sound = {
|
||||||
assetId: soundSource.assetId,
|
assetId: soundSource.assetId,
|
||||||
format: soundSource.format,
|
format: soundSource.format,
|
||||||
|
@ -914,10 +900,59 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
// any translation that needs to happen will happen in the process
|
// any translation that needs to happen will happen in the process
|
||||||
// of building up the costume object into an sb3 format
|
// of building up the costume object into an sb3 format
|
||||||
return deserializeSound(sound, runtime, zip)
|
return deserializeSound(sound, runtime, zip)
|
||||||
.then(() => loadSound(sound, runtime, sprite.soundBank));
|
.then(() => loadSound(sound, runtime, assets.soundBank));
|
||||||
// Only attempt to load the sound after the deserialization
|
// Only attempt to load the sound after the deserialization
|
||||||
// process has been completed.
|
// process has been completed.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return assets;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||||
|
* @param {JSZip} zip Sb3 file describing this project (to load assets from)
|
||||||
|
* @param {object} assets - Promises for assets of this scratch object grouped
|
||||||
|
* into costumes and sounds
|
||||||
|
* @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
|
||||||
|
*/
|
||||||
|
const parseScratchObject = function (object, runtime, extensions, zip, assets) {
|
||||||
|
if (!object.hasOwnProperty('name')) {
|
||||||
|
// Watcher/monitor - skip this object until those are implemented in VM.
|
||||||
|
// @todo
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
// Blocks container for this object.
|
||||||
|
const blocks = new Blocks(runtime);
|
||||||
|
|
||||||
|
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
|
||||||
|
const sprite = new Sprite(blocks, runtime);
|
||||||
|
|
||||||
|
// Sprite/stage name from JSON.
|
||||||
|
if (object.hasOwnProperty('name')) {
|
||||||
|
sprite.name = object.name;
|
||||||
|
}
|
||||||
|
if (object.hasOwnProperty('blocks')) {
|
||||||
|
deserializeBlocks(object.blocks);
|
||||||
|
// Take a second pass to create objects and add extensions
|
||||||
|
for (const blockId in object.blocks) {
|
||||||
|
if (!object.blocks.hasOwnProperty(blockId)) continue;
|
||||||
|
const blockJSON = object.blocks[blockId];
|
||||||
|
blocks.createBlock(blockJSON);
|
||||||
|
|
||||||
|
// If the block is from an extension, record it.
|
||||||
|
const extensionID = getExtensionIdForOpcode(blockJSON.opcode);
|
||||||
|
if (extensionID) {
|
||||||
|
extensions.extensionIDs.add(extensionID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Costumes from JSON.
|
||||||
|
const {costumePromises} = assets;
|
||||||
|
// Sounds from JSON
|
||||||
|
const {soundBank, soundPromises} = assets;
|
||||||
// Create the first clone, and load its run-state from JSON.
|
// Create the first clone, and load its run-state from JSON.
|
||||||
const target = sprite.createClone(object.isStage ? StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER);
|
const target = sprite.createClone(object.isStage ? StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER);
|
||||||
// Load target properties from JSON.
|
// Load target properties from JSON.
|
||||||
|
@ -1039,6 +1074,8 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
});
|
});
|
||||||
Promise.all(soundPromises).then(sounds => {
|
Promise.all(soundPromises).then(sounds => {
|
||||||
sprite.sounds = sounds;
|
sprite.sounds = sounds;
|
||||||
|
// Make sure if soundBank is undefined, sprite.soundBank is then null.
|
||||||
|
sprite.soundBank = soundBank || null;
|
||||||
});
|
});
|
||||||
return Promise.all(costumePromises.concat(soundPromises)).then(() => target);
|
return Promise.all(costumePromises.concat(soundPromises)).then(() => target);
|
||||||
};
|
};
|
||||||
|
@ -1190,10 +1227,16 @@ const deserialize = function (json, runtime, zip, isSingleSprite) {
|
||||||
|
|
||||||
const monitorObjects = json.monitors || [];
|
const monitorObjects = json.monitors || [];
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.resolve(
|
||||||
targetObjects.map(target =>
|
targetObjects.map(target =>
|
||||||
parseScratchObject(target, runtime, extensions, zip))
|
parseScratchAssets(target, runtime, zip))
|
||||||
)
|
)
|
||||||
|
// Force this promise to wait for the next loop in the js tick. Let
|
||||||
|
// storage have some time to send off asset requests.
|
||||||
|
.then(assets => Promise.resolve(assets))
|
||||||
|
.then(assets => Promise.all(targetObjects
|
||||||
|
.map((target, index) =>
|
||||||
|
parseScratchObject(target, runtime, extensions, zip, assets[index]))))
|
||||||
.then(targets => targets // Re-sort targets back into original sprite-pane ordering
|
.then(targets => targets // Re-sort targets back into original sprite-pane ordering
|
||||||
.map((t, i) => {
|
.map((t, i) => {
|
||||||
// Add layer order property to deserialized targets.
|
// Add layer order property to deserialized targets.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
const MathUtil = require('../util/math-util');
|
const MathUtil = require('../util/math-util');
|
||||||
const StringUtil = require('../util/string-util');
|
const StringUtil = require('../util/string-util');
|
||||||
|
const Cast = require('../util/cast');
|
||||||
const Clone = require('../util/clone');
|
const Clone = require('../util/clone');
|
||||||
const Target = require('../engine/target');
|
const Target = require('../engine/target');
|
||||||
const StageLayering = require('../engine/stage-layering');
|
const StageLayering = require('../engine/stage-layering');
|
||||||
|
@ -840,6 +841,7 @@ class RenderedTarget extends Target {
|
||||||
* @return {boolean} True iff touching a clone of the sprite.
|
* @return {boolean} True iff touching a clone of the sprite.
|
||||||
*/
|
*/
|
||||||
isTouchingSprite (spriteName) {
|
isTouchingSprite (spriteName) {
|
||||||
|
spriteName = Cast.toString(spriteName);
|
||||||
const firstClone = this.runtime.getSpriteTargetByName(spriteName);
|
const firstClone = this.runtime.getSpriteTargetByName(spriteName);
|
||||||
if (!firstClone || !this.renderer) {
|
if (!firstClone || !this.renderer) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
const TextEncoder = require('text-encoding').TextEncoder;
|
let _TextEncoder;
|
||||||
|
if (typeof TextEncoder === 'undefined') {
|
||||||
|
_TextEncoder = require('text-encoding').TextEncoder;
|
||||||
|
} else {
|
||||||
|
/* global TextEncoder */
|
||||||
|
_TextEncoder = TextEncoder;
|
||||||
|
}
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const JSZip = require('jszip');
|
const JSZip = require('jszip');
|
||||||
|
|
||||||
|
@ -8,12 +14,8 @@ const ExtensionManager = require('./extension-support/extension-manager');
|
||||||
const log = require('./util/log');
|
const log = require('./util/log');
|
||||||
const MathUtil = require('./util/math-util');
|
const MathUtil = require('./util/math-util');
|
||||||
const Runtime = require('./engine/runtime');
|
const Runtime = require('./engine/runtime');
|
||||||
const {SB1File, ValidationError} = require('scratch-sb1-converter');
|
|
||||||
const sb2 = require('./serialization/sb2');
|
|
||||||
const sb3 = require('./serialization/sb3');
|
|
||||||
const StringUtil = require('./util/string-util');
|
const StringUtil = require('./util/string-util');
|
||||||
const formatMessage = require('format-message');
|
const formatMessage = require('format-message');
|
||||||
const validate = require('scratch-parser');
|
|
||||||
|
|
||||||
const Variable = require('./engine/variable');
|
const Variable = require('./engine/variable');
|
||||||
const newBlockIds = require('./util/new-block-ids');
|
const newBlockIds = require('./util/new-block-ids');
|
||||||
|
@ -297,6 +299,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationPromise = new Promise((resolve, reject) => {
|
const validationPromise = new Promise((resolve, reject) => {
|
||||||
|
const validate = require('scratch-parser');
|
||||||
// The second argument of false below indicates to the validator that the
|
// The second argument of false below indicates to the validator that the
|
||||||
// input should be parsed/validated as an entire project (and not a single sprite)
|
// input should be parsed/validated as an entire project (and not a single sprite)
|
||||||
validate(input, false, (error, res) => {
|
validate(input, false, (error, res) => {
|
||||||
|
@ -305,6 +308,8 @@ class VirtualMachine extends EventEmitter {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
const {SB1File, ValidationError} = require('scratch-sb1-converter');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sb1 = new SB1File(input);
|
const sb1 = new SB1File(input);
|
||||||
const json = sb1.json;
|
const json = sb1.json;
|
||||||
|
@ -410,6 +415,8 @@ class VirtualMachine extends EventEmitter {
|
||||||
* specified by optZipType or blob by default.
|
* specified by optZipType or blob by default.
|
||||||
*/
|
*/
|
||||||
exportSprite (targetId, optZipType) {
|
exportSprite (targetId, optZipType) {
|
||||||
|
const sb3 = require('./serialization/sb3');
|
||||||
|
|
||||||
const soundDescs = serializeSounds(this.runtime, targetId);
|
const soundDescs = serializeSounds(this.runtime, targetId);
|
||||||
const costumeDescs = serializeCostumes(this.runtime, targetId);
|
const costumeDescs = serializeCostumes(this.runtime, targetId);
|
||||||
const spriteJson = StringUtil.stringify(sb3.serialize(this.runtime, targetId));
|
const spriteJson = StringUtil.stringify(sb3.serialize(this.runtime, targetId));
|
||||||
|
@ -432,6 +439,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
* @return {string} Serialized state of the runtime.
|
* @return {string} Serialized state of the runtime.
|
||||||
*/
|
*/
|
||||||
toJSON () {
|
toJSON () {
|
||||||
|
const sb3 = require('./serialization/sb3');
|
||||||
return StringUtil.stringify(sb3.serialize(this.runtime));
|
return StringUtil.stringify(sb3.serialize(this.runtime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,9 +469,11 @@ class VirtualMachine extends EventEmitter {
|
||||||
const deserializePromise = function () {
|
const deserializePromise = function () {
|
||||||
const projectVersion = projectJSON.projectVersion;
|
const projectVersion = projectJSON.projectVersion;
|
||||||
if (projectVersion === 2) {
|
if (projectVersion === 2) {
|
||||||
|
const sb2 = require('./serialization/sb2');
|
||||||
return sb2.deserialize(projectJSON, runtime, false, zip);
|
return sb2.deserialize(projectJSON, runtime, false, zip);
|
||||||
}
|
}
|
||||||
if (projectVersion === 3) {
|
if (projectVersion === 3) {
|
||||||
|
const sb3 = require('./serialization/sb3');
|
||||||
return sb3.deserialize(projectJSON, runtime, zip);
|
return sb3.deserialize(projectJSON, runtime, zip);
|
||||||
}
|
}
|
||||||
return Promise.reject('Unable to verify Scratch Project version.');
|
return Promise.reject('Unable to verify Scratch Project version.');
|
||||||
|
@ -553,6 +563,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationPromise = new Promise((resolve, reject) => {
|
const validationPromise = new Promise((resolve, reject) => {
|
||||||
|
const validate = require('scratch-parser');
|
||||||
// The second argument of true below indicates to the parser/validator
|
// The second argument of true below indicates to the parser/validator
|
||||||
// that the given input should be treated as a single sprite and not
|
// that the given input should be treated as a single sprite and not
|
||||||
// an entire project
|
// an entire project
|
||||||
|
@ -592,6 +603,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
_addSprite2 (sprite, zip) {
|
_addSprite2 (sprite, zip) {
|
||||||
// Validate & parse
|
// Validate & parse
|
||||||
|
|
||||||
|
const sb2 = require('./serialization/sb2');
|
||||||
return sb2.deserialize(sprite, this.runtime, true, zip)
|
return sb2.deserialize(sprite, this.runtime, true, zip)
|
||||||
.then(({targets, extensions}) =>
|
.then(({targets, extensions}) =>
|
||||||
this.installTargets(targets, extensions, false));
|
this.installTargets(targets, extensions, false));
|
||||||
|
@ -605,7 +617,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
_addSprite3 (sprite, zip) {
|
_addSprite3 (sprite, zip) {
|
||||||
// Validate & parse
|
// Validate & parse
|
||||||
|
const sb3 = require('./serialization/sb3');
|
||||||
return sb3
|
return sb3
|
||||||
.deserialize(sprite, this.runtime, zip, true)
|
.deserialize(sprite, this.runtime, zip, true)
|
||||||
.then(({targets, extensions}) => this.installTargets(targets, extensions, false));
|
.then(({targets, extensions}) => this.installTargets(targets, extensions, false));
|
||||||
|
@ -912,7 +924,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
costume.asset = storage.createAsset(
|
costume.asset = storage.createAsset(
|
||||||
storage.AssetType.ImageVector,
|
storage.AssetType.ImageVector,
|
||||||
costume.dataFormat,
|
costume.dataFormat,
|
||||||
(new TextEncoder()).encode(svg),
|
(new _TextEncoder()).encode(svg),
|
||||||
null,
|
null,
|
||||||
true // generate md5
|
true // generate md5
|
||||||
);
|
);
|
||||||
|
@ -1187,6 +1199,8 @@ class VirtualMachine extends EventEmitter {
|
||||||
* @return {!Promise} Promise that resolves when the extensions and blocks have been added.
|
* @return {!Promise} Promise that resolves when the extensions and blocks have been added.
|
||||||
*/
|
*/
|
||||||
shareBlocksToTarget (blocks, targetId, optFromTargetId) {
|
shareBlocksToTarget (blocks, targetId, optFromTargetId) {
|
||||||
|
const sb3 = require('./serialization/sb3');
|
||||||
|
|
||||||
const copiedBlocks = JSON.parse(JSON.stringify(blocks));
|
const copiedBlocks = JSON.parse(JSON.stringify(blocks));
|
||||||
newBlockIds(copiedBlocks);
|
newBlockIds(copiedBlocks);
|
||||||
const target = this.runtime.getTargetById(targetId);
|
const target = this.runtime.getTargetById(targetId);
|
||||||
|
|
BIN
test/fixtures/execute/sprite-number-name.sb2
vendored
Normal file
BIN
test/fixtures/execute/sprite-number-name.sb2
vendored
Normal file
Binary file not shown.
|
@ -211,3 +211,48 @@ test('numbers should be rounded to two decimals in say', t => {
|
||||||
|
|
||||||
looks.say(args, util);
|
looks.say(args, util);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('clamp graphic effects', t => {
|
||||||
|
const rt = new Runtime();
|
||||||
|
const looks = new Looks(rt);
|
||||||
|
const expectedValues = {
|
||||||
|
brightness: {high: 100, low: -100},
|
||||||
|
ghost: {high: 100, low: 0},
|
||||||
|
color: {high: 500, low: -500},
|
||||||
|
fisheye: {high: 500, low: -500},
|
||||||
|
whirl: {high: 500, low: -500},
|
||||||
|
pixelate: {high: 500, low: -500},
|
||||||
|
mosaic: {high: 500, low: -500}
|
||||||
|
};
|
||||||
|
const args = [
|
||||||
|
{EFFECT: 'brightness', VALUE: 500, CLAMP: 'high'},
|
||||||
|
{EFFECT: 'brightness', VALUE: -500, CLAMP: 'low'},
|
||||||
|
{EFFECT: 'ghost', VALUE: 500, CLAMP: 'high'},
|
||||||
|
{EFFECT: 'ghost', VALUE: -500, CLAMP: 'low'},
|
||||||
|
{EFFECT: 'color', VALUE: 500, CLAMP: 'high'},
|
||||||
|
{EFFECT: 'color', VALUE: -500, CLAMP: 'low'},
|
||||||
|
{EFFECT: 'fisheye', VALUE: 500, CLAMP: 'high'},
|
||||||
|
{EFFECT: 'fisheye', VALUE: -500, CLAMP: 'low'},
|
||||||
|
{EFFECT: 'whirl', VALUE: 500, CLAMP: 'high'},
|
||||||
|
{EFFECT: 'whirl', VALUE: -500, CLAMP: 'low'},
|
||||||
|
{EFFECT: 'pixelate', VALUE: 500, CLAMP: 'high'},
|
||||||
|
{EFFECT: 'pixelate', VALUE: -500, CLAMP: 'low'},
|
||||||
|
{EFFECT: 'mosaic', VALUE: 500, CLAMP: 'high'},
|
||||||
|
{EFFECT: 'mosaic', VALUE: -500, CLAMP: 'low'}
|
||||||
|
];
|
||||||
|
|
||||||
|
util.target.setEffect = function (effectName, actualValue) {
|
||||||
|
const clamp = actualValue > 0 ? 'high' : 'low';
|
||||||
|
rt.emit(effectName + clamp, effectName, actualValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const arg of args) {
|
||||||
|
rt.addListener(arg.EFFECT + arg.CLAMP, (effectName, actualValue) => {
|
||||||
|
const expected = expectedValues[arg.EFFECT][arg.CLAMP];
|
||||||
|
t.strictEqual(actualValue, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
looks.setEffect(arg, util);
|
||||||
|
}
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
|
@ -358,6 +358,43 @@ test('move no obscure shadow', t => {
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('move - attaching new shadow', t => {
|
||||||
|
const b = new Blocks(new Runtime());
|
||||||
|
// Block/shadow are null to mimic state right after a procedure_call block
|
||||||
|
// is mutated by adding an input. The "move" will attach the new shadow.
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {
|
||||||
|
fooInput: {
|
||||||
|
name: 'fooInput',
|
||||||
|
block: null,
|
||||||
|
shadow: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'bar',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
shadow: true,
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.moveBlock({
|
||||||
|
id: 'bar',
|
||||||
|
newInput: 'fooInput',
|
||||||
|
newParent: 'foo'
|
||||||
|
});
|
||||||
|
t.equal(b._blocks.foo.inputs.fooInput.block, 'bar');
|
||||||
|
t.equal(b._blocks.foo.inputs.fooInput.shadow, 'bar');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
test('change', t => {
|
test('change', t => {
|
||||||
const b = new Blocks(new Runtime());
|
const b = new Blocks(new Runtime());
|
||||||
b.createBlock({
|
b.createBlock({
|
||||||
|
|
30
test/unit/extension_text_to_speech.js
Normal file
30
test/unit/extension_text_to_speech.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
const test = require('tap').test;
|
||||||
|
const TextToSpeech = require('../../src/extensions/scratch3_text2speech/index.js');
|
||||||
|
|
||||||
|
const fakeStage = {
|
||||||
|
textToSpeechLanguage: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeRuntime = {
|
||||||
|
getTargetForStage: () => fakeStage,
|
||||||
|
on: () => {} // Stub out listener methods used in constructor.
|
||||||
|
};
|
||||||
|
|
||||||
|
const ext = new TextToSpeech(fakeRuntime);
|
||||||
|
|
||||||
|
test('if no language is saved in the project, use default', t => {
|
||||||
|
t.strictEqual(ext.getCurrentLanguage(), 'en');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('if an unsupported language is dropped onto the set language block, use default', t => {
|
||||||
|
ext.setLanguage({LANGUAGE: 'nope'});
|
||||||
|
t.strictEqual(ext.getCurrentLanguage(), 'en');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get the extension locale for a supported locale that differs', t => {
|
||||||
|
ext.setLanguage({LANGUAGE: 'ja-Hira'});
|
||||||
|
t.strictEqual(ext.getCurrentLanguage(), 'ja');
|
||||||
|
t.end();
|
||||||
|
});
|
Loading…
Reference in a new issue