mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-06-11 04:51:15 -04:00
Merge branch 'develop' into greenkeeper/scratch-sb1-converter-0.2.7
This commit is contained in:
commit
ebfba83989
13 changed files with 947 additions and 219 deletions
package-lock.jsonpackage.json
src
engine
extensions/scratch3_gdx_for
playground
util
test
webpack.config.js
24
package-lock.json
generated
24
package-lock.json
generated
|
@ -1678,7 +1678,7 @@
|
|||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
|
||||
"integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
|
||||
"requires": {
|
||||
"lodash": "4.17.4"
|
||||
"lodash": "^4.14.0"
|
||||
}
|
||||
},
|
||||
"async-each": {
|
||||
|
@ -6271,8 +6271,8 @@
|
|||
"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.3"
|
||||
"crc32": ">= 0.2.2",
|
||||
"deflate-js": ">= 0.2.2"
|
||||
}
|
||||
},
|
||||
"handle-thing": {
|
||||
|
@ -12166,9 +12166,9 @@
|
|||
}
|
||||
},
|
||||
"scratch-parser": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/scratch-parser/-/scratch-parser-4.3.4.tgz",
|
||||
"integrity": "sha512-2MLf7rcAsJhU645jHkex+BZQwDXkv6FIkcDJyGMoR0IkzH8ocHg8GBn3e3pp9IXpcaXsgdXA8/mtjBKAmuuO5Q==",
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/scratch-parser/-/scratch-parser-4.3.5.tgz",
|
||||
"integrity": "sha512-jOHrR9evVnRxnIc7W+1m7S2E5yDyUCbh8xvPueT10mo7AfLprE9lRKAtc6yF3Gxj0Rm/jhyiGBn8crAPc/F4Vg==",
|
||||
"requires": {
|
||||
"ajv": "6.3.0",
|
||||
"async": "2.6.0",
|
||||
|
@ -12181,9 +12181,9 @@
|
|||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.3.0.tgz",
|
||||
"integrity": "sha1-FlCkERTvAFdMrBC4Ay2PTBSBLac=",
|
||||
"requires": {
|
||||
"fast-deep-equal": "1.0.0",
|
||||
"fast-json-stable-stringify": "2.0.0",
|
||||
"json-schema-traverse": "0.3.1"
|
||||
"fast-deep-equal": "^1.0.0",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12961,9 +12961,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"static-eval": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.0.tgz",
|
||||
"integrity": "sha512-6flshd3F1Gwm+Ksxq463LtFd1liC77N/PX1FVVc3OzL3hAmo2fwHFbuArkcfi7s9rTNsLEhcRmXGFZhlgy40uw==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz",
|
||||
"integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"escodegen": "^1.8.1"
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"jszip": "^3.1.5",
|
||||
"minilog": "3.1.0",
|
||||
"nets": "3.2.0",
|
||||
"scratch-parser": "4.3.4",
|
||||
"scratch-parser": "4.3.5",
|
||||
"scratch-sb1-converter": "0.2.7",
|
||||
"scratch-translate-extension-languages": "0.0.20181205140428",
|
||||
"socket.io-client": "2.0.4",
|
||||
|
@ -79,6 +79,7 @@
|
|||
"stats.js": "^0.17.0",
|
||||
"tap": "^12.0.1",
|
||||
"tiny-worker": "^2.1.1",
|
||||
"uglifyjs-webpack-plugin": "1.2.7",
|
||||
"webpack": "^4.16.5",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-dev-server": "^3.1.5"
|
||||
|
|
|
@ -1556,6 +1556,8 @@ class Runtime extends EventEmitter {
|
|||
}
|
||||
const instance = this;
|
||||
const newThreads = [];
|
||||
// Look up metadata for the relevant hat.
|
||||
const hatMeta = instance._hats[requestedHatOpcode];
|
||||
|
||||
for (const opts in optMatchFields) {
|
||||
if (!optMatchFields.hasOwnProperty(opts)) continue;
|
||||
|
@ -1602,8 +1604,6 @@ class Runtime extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
// Look up metadata for the relevant hat.
|
||||
const hatMeta = instance._hats[requestedHatOpcode];
|
||||
if (hatMeta.restartExistingThreads) {
|
||||
// If `restartExistingThreads` is true, we should stop
|
||||
// any existing threads starting with the top block.
|
||||
|
|
|
@ -12,7 +12,14 @@ const ScratchLinkDeviceAdapter = require('./scratch-link-device-adapter');
|
|||
* @type {string}
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOCAuNSkiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+PHBhdGggZD0iTTEyIDM5LjVBMi41IDIuNSAwIDAgMSA5LjUgMzdjMC0uMy4yLS41LjUtLjVzLjUuMi41LjVhMS41IDEuNSAwIDEgMCAzIDB2LS4yYzAtLjQtLjItLjgtLjUtMWwtLjgtLjljLS41LS40LS43LTEtLjctMS43VjMxYzAtLjMuMi0uNS41LS41cy41LjIuNS41djIuMmMwIC40LjEuOC40IDFsLjguOWMuNS40LjggMSAuOCAxLjd2LjJjMCAxLjQtMS4xIDIuNS0yLjUgMi41eiIgZmlsbD0iI0U2RTdFOCIvPjxwYXRoIGQ9Ik0yMy43LjNBMSAxIDAgMCAwIDIzIDBIMWExIDEgMCAwIDAtLjcuM0ExIDEgMCAwIDAgMCAxdjI2YzAgLjMuMS41LjMuNy4yLjIuNC4zLjcuM2gyMmMuMyAwIC41LS4xLjctLjMuMi0uMi4zLS40LjMtLjdWMWExIDEgMCAwIDAtLjMtLjd6TTEyIDRjMiAwIDMuMyAyIDIuNiAzLjhMMTMuMyAxMWExLjQgMS40IDAgMCAxLTIuNyAwTDkuNSA3LjdsLS4yLTFDOS4yIDUuNCAxMC40IDQgMTIgNHoiIHN0cm9rZT0iIzdDODdBNSIgZmlsbD0iIzg1OTJBRiIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMiAydjI0aDIwVjJIMnptMTAgMmMyIDAgMy4zIDIgMi42IDMuOEwxMy4zIDExYTEuNCAxLjQgMCAwIDEtMi43IDBMOS41IDcuN2wtLjItMUM5LjIgNS40IDEwLjQgNCAxMiA0eiIgc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjNUNCMUQ2IiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIHN0cm9rZT0iIzdDODdBNSIgZmlsbD0iIzg1OTJBRiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNMjIgMjZIMnYtNmwyMC00eiIvPjxwYXRoIGQ9Ik0uMyAyNy43TDIgMjZNLjMuM0wyIDJNMjIgMkwyMy43LjNNMjMuNyAyNy43TDIyIDI2IiBzdHJva2U9IiM3Qzg3QTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxjaXJjbGUgZmlsbD0iI0ZGQkYwMCIgY3g9IjEyIiBjeT0iMTQuOCIgcj0iMS4yIi8+PHBhdGggc3Ryb2tlPSIjN0M4N0E1IiBmaWxsPSIjRTZFN0U4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Ik0xMCAyOGg0djRoLTR6Ii8+PHBhdGggZD0iTTE1LjUgMjJoLTdhLjUuNSAwIDAgMS0uNS0uNWMwLS4zLjItLjUuNS0uNWg3Yy4zIDAgLjUuMi41LjVzLS4yLjUtLjUuNXpNMTcuNSAyNGgtMTFhLjUuNSAwIDAgMS0uNS0uNWMwLS4zLjItLjUuNS0uNWgxMWMuMyAwIC41LjIuNS41cy0uMi41LS41LjV6IiBmaWxsPSIjRkZCRjAwIi8+PC9nPjwvc3ZnPg==';
|
||||
const blockIconURI = 'data:image/png;base64,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';
|
||||
|
||||
/**
|
||||
* Icon png to be displayed in the blocks category menu, encoded as a data URI.
|
||||
* @type {string}
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
const menuIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAA9dJREFUWAnNmE2IFEcUgF/9dE/v7LoaM9kkK4JBRA0EFBIPRm85hBAvEXHXwyo5eFE87GFcReMkObgJiQnkkJzEg9n8HIJixKNe1IMKihgiCbviwV11V3d0d3pmuqsqr5ppcEnb3TNVggVFVVe9eu+r97qqq4tASqp8/fsboQgmU0TMugi571K29bPy9ovPU8Sf16HbpQj3EkYFBcJcr5Am2nZfs94AIWVfqMQeHNwhICUBZ4ypUIA/X2sbIm2AW8AJK0lkEP6TJpfqwXgg4QxmF/fB7Gtvxk1G5ZKHU1CqTgPJoSUXYJYeohSUJu+qrqdVUGh2/pVX4VFffx77WaqBZkrkEFj271+qWH0sXcU3FBzyQe/Mg7B//LbKMTRTxNiDbsMHHjTJlyM7HEJIBHXs2KXFj+oTNSdoQOCYLS5jD9IwBMm5H8NplwwPb/QV4yEIcycaAza9IuA76B38fuz1OF5RXUkmHCdu6rg0BpSMgV/sAe7DdzGFrvvdi0D3mSZjQA0wt7REQsY+iWF0XbfFzyal8SLRxuteD+Du4h4Z/flbqaBHibAQtZmQtcZaAZSMwtTylaR/4vaw1ju5YhWG10pwwAqghmp2FeHO2+t11WqyM80W0m7vAOhsM1kD7CGz8L57Jsq6bitZC/GcWgLf1H6KuHT92cTDAFy/BgXMXm0OCpgV50Bo9kK3BqiBboabQMMU/WoL5im4jToeq/AIgXsiRx5KKCjcwPEsiAv/BQMu9EwyDHXd/3kqCOSzDk6t5/YglQKKeJwq+PNRmJI8kwSTaj1HZy5AhSHqnXkIvU9mMUwEw4Q5wTM57LUtkg8QPw/cdcBJ+PhvKJ0Gj80nGq6JXrg6/XFiX97GXIBpyqTieKpKViOl+WEhWXMaUavvvdIZ8Giy5+Lh3bwKm/t+Be3JazMfxc1tldY26rastiHcsQevTG9pw0znovkAcRWHzSDKnZtaOJLSfMFLB5RqtRBS4LbCurqLCy0YPkU3C0IIPEimMqR2ei7ZX2+KQdRi/WahNT/GmfOD4Vyzhx/66pcjp85dUvcmp6J8+txldXh07PPskdkS+V6EbD0vTOKlB0x9B/O6BS8ULly9PgE6x4kDPR/XX5pyYKj8xcCucsUmkNUQE0JvKKm2VioVK5HRE7UKOHbi6B94RzP+93jtpC0vWgXUF0hr3ipuw8uadwd3jXxoA9IK4Pah8t6BneV9GgjD28Svw1mlxFobgFbeFTz13cKbth93fDryp2CEq0a4hTA+aAPQ/ESJFDdvXLzzzrqNjlTqOP6uDeFf0uhvJ0ZP2QD8D6ZzU6u8YIbBAAAAAElFTkSuQmCC';
|
||||
|
||||
/**
|
||||
* Enum for Vernier godirect protocol.
|
||||
|
@ -53,7 +60,7 @@ const GDXFOR_SENSOR = {
|
|||
/**
|
||||
* The update rate, in milliseconds, for sensor data input from the peripheral.
|
||||
*/
|
||||
const GDXFOR_UPDATE_RATE = 100;
|
||||
const GDXFOR_UPDATE_RATE = 80;
|
||||
|
||||
/**
|
||||
* Threshold for pushing and pulling force, for the whenForcePushedOrPulled hat block.
|
||||
|
@ -61,12 +68,6 @@ const GDXFOR_UPDATE_RATE = 100;
|
|||
*/
|
||||
const FORCE_THRESHOLD = 5;
|
||||
|
||||
/**
|
||||
* Threshold for acceleration magnitude, for the "moved" gesture.
|
||||
* @type {number}
|
||||
*/
|
||||
const MOVED_THRESHOLD = 3;
|
||||
|
||||
/**
|
||||
* Threshold for acceleration magnitude, for the "shaken" gesture.
|
||||
* @type {number}
|
||||
|
@ -91,6 +92,12 @@ const FREEFALL_THRESHOLD = 0.5;
|
|||
*/
|
||||
const FREEFALL_ROTATION_FACTOR = 0.3;
|
||||
|
||||
/**
|
||||
* Threshold in degrees for reporting that the sensor is tilted.
|
||||
* @type {number}
|
||||
*/
|
||||
const TILT_THRESHOLD = 15;
|
||||
|
||||
/**
|
||||
* Acceleration due to gravity, in m/s^2.
|
||||
* @type {number}
|
||||
|
@ -409,7 +416,6 @@ const PushPullValues = {
|
|||
* @enum {string}
|
||||
*/
|
||||
const GestureValues = {
|
||||
MOVED: 'moved',
|
||||
SHAKEN: 'shaken',
|
||||
STARTED_FALLING: 'started falling'
|
||||
};
|
||||
|
@ -423,7 +429,8 @@ const TiltAxisValues = {
|
|||
FRONT: 'front',
|
||||
BACK: 'back',
|
||||
LEFT: 'left',
|
||||
RIGHT: 'right'
|
||||
RIGHT: 'right',
|
||||
ANY: 'any'
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -520,6 +527,20 @@ class Scratch3GdxForBlocks {
|
|||
];
|
||||
}
|
||||
|
||||
get TILT_MENU_ANY () {
|
||||
return [
|
||||
...this.TILT_MENU,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.tiltDirectionMenu.any',
|
||||
default: 'any',
|
||||
description: 'label for any direction element in tilt direction picker for gdxfor extension'
|
||||
}),
|
||||
value: TiltAxisValues.ANY
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
get FACE_MENU () {
|
||||
return [
|
||||
{
|
||||
|
@ -564,14 +585,6 @@ class Scratch3GdxForBlocks {
|
|||
|
||||
get GESTURE_MENU () {
|
||||
return [
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.moved',
|
||||
default: 'moved',
|
||||
description: 'the sensor was moved'
|
||||
}),
|
||||
value: GestureValues.MOVED
|
||||
},
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.shaken',
|
||||
|
@ -614,8 +627,25 @@ class Scratch3GdxForBlocks {
|
|||
id: Scratch3GdxForBlocks.EXTENSION_ID,
|
||||
name: Scratch3GdxForBlocks.EXTENSION_NAME,
|
||||
blockIconURI: blockIconURI,
|
||||
menuIconURI: menuIconURI,
|
||||
showStatusButton: true,
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'whenGesture',
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.whenGesture',
|
||||
default: 'when [GESTURE]',
|
||||
description: 'when the sensor detects a gesture'
|
||||
}),
|
||||
blockType: BlockType.HAT,
|
||||
arguments: {
|
||||
GESTURE: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'gestureOptions',
|
||||
defaultValue: GestureValues.SHAKEN
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'whenForcePushedOrPulled',
|
||||
text: formatMessage({
|
||||
|
@ -643,18 +673,34 @@ class Scratch3GdxForBlocks {
|
|||
},
|
||||
'---',
|
||||
{
|
||||
opcode: 'whenGesture',
|
||||
opcode: 'whenTilted',
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.whenGesture',
|
||||
default: 'when [GESTURE]',
|
||||
description: 'when the sensor detects a gesture'
|
||||
id: 'gdxfor.whenTilted',
|
||||
default: 'when tilted [TILT]',
|
||||
description: 'when the sensor detects tilt'
|
||||
}),
|
||||
blockType: BlockType.HAT,
|
||||
arguments: {
|
||||
GESTURE: {
|
||||
TILT: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'gestureOptions',
|
||||
defaultValue: GestureValues.MOVED
|
||||
menu: 'tiltAnyOptions',
|
||||
defaultValue: TiltAxisValues.ANY
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'isTilted',
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.isTilted',
|
||||
default: 'tilted [TILT]?',
|
||||
description: 'is the device tilted?'
|
||||
}),
|
||||
blockType: BlockType.BOOLEAN,
|
||||
arguments: {
|
||||
TILT: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'tiltAnyOptions',
|
||||
defaultValue: TiltAxisValues.ANY
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -662,7 +708,7 @@ class Scratch3GdxForBlocks {
|
|||
opcode: 'getTilt',
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.getTilt',
|
||||
default: 'tilt [TILT]',
|
||||
default: 'tilt angle [TILT]',
|
||||
description: 'gets tilt'
|
||||
}),
|
||||
blockType: BlockType.REPORTER,
|
||||
|
@ -674,38 +720,6 @@ class Scratch3GdxForBlocks {
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'getSpinSpeed',
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.getSpin',
|
||||
default: 'spin [DIRECTION]',
|
||||
description: 'gets spin speed'
|
||||
}),
|
||||
blockType: BlockType.REPORTER,
|
||||
arguments: {
|
||||
DIRECTION: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'axisOptions',
|
||||
defaultValue: AxisValues.Z
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'getAcceleration',
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.getAcceleration',
|
||||
default: 'acceleration [DIRECTION]',
|
||||
description: 'gets acceleration'
|
||||
}),
|
||||
blockType: BlockType.REPORTER,
|
||||
arguments: {
|
||||
DIRECTION: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'axisOptions',
|
||||
defaultValue: AxisValues.X
|
||||
}
|
||||
}
|
||||
},
|
||||
'---',
|
||||
{
|
||||
opcode: 'isFacing',
|
||||
|
@ -731,7 +745,38 @@ class Scratch3GdxForBlocks {
|
|||
description: 'is the device in free fall?'
|
||||
}),
|
||||
blockType: BlockType.BOOLEAN
|
||||
|
||||
},
|
||||
{
|
||||
opcode: 'getSpinSpeed',
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.getSpin',
|
||||
default: 'spin speed [DIRECTION]',
|
||||
description: 'gets spin speed'
|
||||
}),
|
||||
blockType: BlockType.REPORTER,
|
||||
arguments: {
|
||||
DIRECTION: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'axisOptions',
|
||||
defaultValue: AxisValues.Z
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'getAcceleration',
|
||||
text: formatMessage({
|
||||
id: 'gdxfor.getAcceleration',
|
||||
default: 'acceleration [DIRECTION]',
|
||||
description: 'gets acceleration'
|
||||
}),
|
||||
blockType: BlockType.REPORTER,
|
||||
arguments: {
|
||||
DIRECTION: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'axisOptions',
|
||||
defaultValue: AxisValues.X
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
menus: {
|
||||
|
@ -739,6 +784,7 @@ class Scratch3GdxForBlocks {
|
|||
gestureOptions: this.GESTURE_MENU,
|
||||
axisOptions: this.AXIS_MENU,
|
||||
tiltOptions: this.TILT_MENU,
|
||||
tiltAnyOptions: this.TILT_MENU_ANY,
|
||||
faceOptions: this.FACE_MENU
|
||||
}
|
||||
};
|
||||
|
@ -762,8 +808,6 @@ class Scratch3GdxForBlocks {
|
|||
|
||||
whenGesture (args) {
|
||||
switch (args.GESTURE) {
|
||||
case GestureValues.MOVED:
|
||||
return this.gestureMagnitude() > MOVED_THRESHOLD;
|
||||
case GestureValues.SHAKEN:
|
||||
return this.gestureMagnitude() > SHAKEN_THRESHOLD;
|
||||
case GestureValues.STARTED_FALLING:
|
||||
|
@ -774,24 +818,48 @@ class Scratch3GdxForBlocks {
|
|||
}
|
||||
}
|
||||
|
||||
whenTilted (args) {
|
||||
return this._isTilted(args.TILT);
|
||||
}
|
||||
|
||||
isTilted (args) {
|
||||
return this._isTilted(args.TILT);
|
||||
}
|
||||
|
||||
getTilt (args) {
|
||||
return this._getTiltAngle(args.TILT);
|
||||
}
|
||||
|
||||
_isTilted (direction) {
|
||||
switch (direction) {
|
||||
case TiltAxisValues.ANY:
|
||||
return this._getTiltAngle(TiltAxisValues.FRONT) > TILT_THRESHOLD ||
|
||||
this._getTiltAngle(TiltAxisValues.BACK) > TILT_THRESHOLD ||
|
||||
this._getTiltAngle(TiltAxisValues.LEFT) > TILT_THRESHOLD ||
|
||||
this._getTiltAngle(TiltAxisValues.RIGHT) > TILT_THRESHOLD;
|
||||
default:
|
||||
return this._getTiltAngle(direction) > TILT_THRESHOLD;
|
||||
}
|
||||
}
|
||||
|
||||
_getTiltAngle (direction) {
|
||||
// Tilt values are calculated using acceleration due to gravity,
|
||||
// so we need to return 0 when the peripheral is not connected.
|
||||
if (!this._peripheral.isConnected()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (args.TILT) {
|
||||
switch (direction) {
|
||||
case TiltAxisValues.FRONT:
|
||||
return Math.round(this._peripheral.getTiltFrontBack(false));
|
||||
case TiltAxisValues.BACK:
|
||||
return Math.round(this._peripheral.getTiltFrontBack(true));
|
||||
case TiltAxisValues.BACK:
|
||||
return Math.round(this._peripheral.getTiltFrontBack(false));
|
||||
case TiltAxisValues.LEFT:
|
||||
return Math.round(this._peripheral.getTiltLeftRight(false));
|
||||
case TiltAxisValues.RIGHT:
|
||||
return Math.round(this._peripheral.getTiltLeftRight(true));
|
||||
case TiltAxisValues.RIGHT:
|
||||
return Math.round(this._peripheral.getTiltLeftRight(false));
|
||||
default:
|
||||
log.warn(`Unknown direction in getTilt: ${args.TILT}`);
|
||||
log.warn(`Unknown direction in getTilt: ${direction}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,56 @@
|
|||
// Track loading time with timestamps and if possible the performance api.
|
||||
if (window.performance) {
|
||||
// Mark with the performance API when benchmark.js and its dependecies start
|
||||
// evaluation. This can tell us once measured how long the code spends time
|
||||
// turning into execution code for the first time. Skipping evaluation of
|
||||
// some of the code can help us make it faster.
|
||||
performance.mark('Scratch.EvalStart');
|
||||
}
|
||||
|
||||
class LoadingMiddleware {
|
||||
constructor () {
|
||||
this.middleware = [];
|
||||
this.host = null;
|
||||
this.original = null;
|
||||
}
|
||||
|
||||
install (host, original) {
|
||||
this.host = host;
|
||||
this.original = original;
|
||||
const {middleware} = this;
|
||||
return function (...args) {
|
||||
let i = 0;
|
||||
const next = function (_args) {
|
||||
if (i >= middleware.length) {
|
||||
return original.call(host, ..._args);
|
||||
}
|
||||
return middleware[i++](_args, next);
|
||||
};
|
||||
return next(args);
|
||||
};
|
||||
}
|
||||
|
||||
push (middleware) {
|
||||
this.middleware.push(middleware);
|
||||
}
|
||||
}
|
||||
|
||||
const importLoadCostume = require('../import/load-costume');
|
||||
const costumeMiddleware = new LoadingMiddleware();
|
||||
importLoadCostume.loadCostume = costumeMiddleware.install(importLoadCostume, importLoadCostume.loadCostume);
|
||||
|
||||
const importLoadSound = require('../import/load-sound');
|
||||
const soundMiddleware = new LoadingMiddleware();
|
||||
importLoadSound.loadSound = soundMiddleware.install(importLoadSound, importLoadSound.loadSound);
|
||||
|
||||
const ScratchStorage = require('scratch-storage');
|
||||
const VirtualMachine = require('..');
|
||||
const Runtime = require('../engine/runtime');
|
||||
|
||||
const ScratchRender = require('scratch-render');
|
||||
const AudioEngine = require('scratch-audio');
|
||||
const ScratchSVGRenderer = require('scratch-svg-renderer');
|
||||
|
||||
const Scratch = window.Scratch = window.Scratch || {};
|
||||
|
||||
const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu/';
|
||||
|
@ -60,24 +113,114 @@ const getAssetUrl = function (asset) {
|
|||
|
||||
class LoadingProgress {
|
||||
constructor (callback) {
|
||||
this.total = 0;
|
||||
this.complete = 0;
|
||||
this.dataLoaded = 0;
|
||||
this.contentTotal = 0;
|
||||
this.contentComplete = 0;
|
||||
this.hydrateTotal = 0;
|
||||
this.hydrateComplete = 0;
|
||||
this.memoryCurrent = 0;
|
||||
this.memoryPeak = 0;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
on (storage) {
|
||||
sampleMemory () {
|
||||
if (window.performance && window.performance.memory) {
|
||||
this.memoryCurrent = window.performance.memory.usedJSHeapSize;
|
||||
this.memoryPeak = Math.max(this.memoryCurrent, this.memoryPeak);
|
||||
}
|
||||
}
|
||||
|
||||
attachHydrateMiddleware (middleware) {
|
||||
const _this = this;
|
||||
middleware.push((args, next) => {
|
||||
_this.hydrateTotal += 1;
|
||||
_this.sampleMemory();
|
||||
_this.callback(_this);
|
||||
return Promise.resolve(next(args))
|
||||
.then(value => {
|
||||
_this.hydrateComplete += 1;
|
||||
_this.sampleMemory();
|
||||
_this.callback(_this);
|
||||
return value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
on (storage, vm) {
|
||||
const _this = this;
|
||||
|
||||
this.attachHydrateMiddleware(costumeMiddleware);
|
||||
this.attachHydrateMiddleware(soundMiddleware);
|
||||
|
||||
const _load = storage.webHelper.load;
|
||||
storage.webHelper.load = function (...args) {
|
||||
if (_this.dataLoaded === 0 && window.performance) {
|
||||
// Mark in browser inspectors how long it takes to load the
|
||||
// projects initial data file.
|
||||
performance.mark('Scratch.LoadDataStart');
|
||||
}
|
||||
|
||||
const result = _load.call(this, ...args);
|
||||
_this.total += 1;
|
||||
|
||||
if (_this.dataLoaded) {
|
||||
if (_this.contentTotal === 0 && window.performance) {
|
||||
performance.mark('Scratch.DownloadStart');
|
||||
}
|
||||
|
||||
_this.contentTotal += 1;
|
||||
}
|
||||
_this.sampleMemory();
|
||||
_this.callback(_this);
|
||||
|
||||
result.then(() => {
|
||||
_this.complete += 1;
|
||||
if (_this.dataLoaded === 0) {
|
||||
if (window.performance) {
|
||||
// How long did loading the data file take?
|
||||
performance.mark('Scratch.LoadDataEnd');
|
||||
performance.measure('Scratch.LoadData', 'Scratch.LoadDataStart', 'Scratch.LoadDataEnd');
|
||||
}
|
||||
|
||||
_this.dataLoaded = 1;
|
||||
|
||||
window.ScratchVMLoadDataEnd = Date.now();
|
||||
} else {
|
||||
_this.contentComplete += 1;
|
||||
}
|
||||
|
||||
if (_this.contentComplete && _this.contentComplete === _this.contentTotal) {
|
||||
if (window.performance) {
|
||||
// How long did it take to download the html, js, and
|
||||
// all the project assets?
|
||||
performance.mark('Scratch.DownloadEnd');
|
||||
performance.measure('Scratch.Download', 'Scratch.DownloadStart', 'Scratch.DownloadEnd');
|
||||
}
|
||||
|
||||
window.ScratchVMDownloadEnd = Date.now();
|
||||
}
|
||||
|
||||
_this.sampleMemory();
|
||||
_this.callback(_this);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
vm.runtime.on(Runtime.PROJECT_LOADED, () => {
|
||||
// Currently LoadingProgress tracks when the data has been loaded
|
||||
// and not when the data has been decoded. It may be difficult to
|
||||
// track that but it isn't hard to track when its all been decoded.
|
||||
if (window.performance) {
|
||||
// How long did it take to load and hydrate the html, js, and
|
||||
// all the project assets?
|
||||
performance.mark('Scratch.LoadEnd');
|
||||
performance.measure('Scratch.Load', 'Scratch.LoadStart', 'Scratch.LoadEnd');
|
||||
}
|
||||
|
||||
window.ScratchVMLoadEnd = Date.now();
|
||||
|
||||
// With this event lets update LoadingProgress a final time so its
|
||||
// displayed loading time is accurate.
|
||||
_this.sampleMemory();
|
||||
_this.callback(_this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -439,23 +582,46 @@ class ProfilerRun {
|
|||
const runBenchmark = function () {
|
||||
// Lots of global variables to make debugging easier
|
||||
// Instantiate the VM.
|
||||
const vm = new window.VirtualMachine();
|
||||
const vm = new VirtualMachine();
|
||||
Scratch.vm = vm;
|
||||
|
||||
vm.setTurboMode(true);
|
||||
|
||||
const storage = new ScratchStorage(); /* global ScratchStorage */
|
||||
const storage = new ScratchStorage();
|
||||
const AssetType = storage.AssetType;
|
||||
storage.addWebSource([AssetType.Project], getProjectUrl);
|
||||
storage.addWebSource([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl);
|
||||
vm.attachStorage(storage);
|
||||
|
||||
new LoadingProgress(progress => {
|
||||
document.getElementsByClassName('loading-total')[0]
|
||||
.innerText = progress.total;
|
||||
document.getElementsByClassName('loading-complete')[0]
|
||||
.innerText = progress.complete;
|
||||
}).on(storage);
|
||||
const setElement = (name, value) => {
|
||||
document.getElementsByClassName(name)[0].innerText = value;
|
||||
};
|
||||
const sinceLoadStart = key => (
|
||||
`(${(window[key] || Date.now()) - window.ScratchVMLoadStart}ms)`
|
||||
);
|
||||
|
||||
setElement('loading-total', 1);
|
||||
setElement('loading-complete', progress.dataLoaded);
|
||||
setElement('loading-time', sinceLoadStart('ScratchVMLoadDataEnd'));
|
||||
|
||||
setElement('loading-content-total', progress.contentTotal);
|
||||
setElement('loading-content-complete', progress.contentComplete);
|
||||
setElement('loading-content-time', sinceLoadStart('ScratchVMDownloadEnd'));
|
||||
|
||||
setElement('loading-hydrate-total', progress.hydrateTotal);
|
||||
setElement('loading-hydrate-complete', progress.hydrateComplete);
|
||||
setElement('loading-hydrate-time', sinceLoadStart('ScratchVMLoadEnd'));
|
||||
|
||||
if (progress.memoryPeak) {
|
||||
setElement('loading-memory-current',
|
||||
`${(progress.memoryCurrent / 1000000).toFixed(0)}MB`
|
||||
);
|
||||
setElement('loading-memory-peak',
|
||||
`${(progress.memoryPeak / 1000000).toFixed(0)}MB`
|
||||
);
|
||||
}
|
||||
}).on(storage, vm);
|
||||
|
||||
let warmUpTime = 4000;
|
||||
let maxRecordedTime = 6000;
|
||||
|
@ -476,12 +642,11 @@ const runBenchmark = function () {
|
|||
|
||||
// Instantiate the renderer and connect it to the VM.
|
||||
const canvas = document.getElementById('scratch-stage');
|
||||
const renderer = new window.ScratchRender(canvas);
|
||||
const renderer = new ScratchRender(canvas);
|
||||
Scratch.renderer = renderer;
|
||||
vm.attachRenderer(renderer);
|
||||
const audioEngine = new window.AudioEngine();
|
||||
const audioEngine = new AudioEngine();
|
||||
vm.attachAudioEngine(audioEngine);
|
||||
/* global ScratchSVGRenderer */
|
||||
vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer());
|
||||
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
|
||||
|
||||
|
@ -555,12 +720,12 @@ const runBenchmark = function () {
|
|||
* @param {object} json data from a previous benchmark run.
|
||||
*/
|
||||
const renderBenchmarkData = function (json) {
|
||||
const vm = new window.VirtualMachine();
|
||||
const vm = new VirtualMachine();
|
||||
new ProfilerRun({vm}).render(json);
|
||||
setShareLink(json);
|
||||
};
|
||||
|
||||
window.onload = function () {
|
||||
const onload = function () {
|
||||
if (location.hash.substring(1).startsWith('view')) {
|
||||
document.body.className = 'render';
|
||||
const data = location.hash.substring(6);
|
||||
|
@ -575,3 +740,12 @@ window.onload = function () {
|
|||
window.onhashchange = function () {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
if (window.performance) {
|
||||
performance.mark('Scratch.EvalEnd');
|
||||
performance.measure('Scratch.Eval', 'Scratch.EvalStart', 'Scratch.EvalEnd');
|
||||
}
|
||||
|
||||
window.ScratchVMEvalEnd = Date.now();
|
||||
|
||||
onload();
|
||||
|
|
|
@ -5,6 +5,18 @@
|
|||
<meta charset="utf-8">
|
||||
<title>Scratch VM Benchmark</title>
|
||||
<link rel="stylesheet" href="./benchmark.css" type="text/css" media="screen">
|
||||
<script>
|
||||
// Track loading time with timestamps and if possible the performance
|
||||
// api.
|
||||
|
||||
// Start tracking loading of Scratch before the body dom is evaluated.
|
||||
window.ScratchVMLoadStart = Date.now();
|
||||
if (window.performance) {
|
||||
// Mark for browser performance inspectors and if we want to use
|
||||
// other performance apis.
|
||||
performance.mark('Scratch.LoadStart');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Scratch VM Benchmark</h2>
|
||||
|
@ -28,31 +40,45 @@
|
|||
|
||||
<canvas id="scratch-stage"></canvas><br />
|
||||
|
||||
<div class="loading">
|
||||
<label>Loading:</label>
|
||||
<span class="loading-complete">0</span> / <span class="loading-total">0</span>
|
||||
</div>
|
||||
<div class="profile-count-group">
|
||||
<div class="profile-count">
|
||||
<label>Percent of time worked:</label>
|
||||
<span class="profile-count-value profile-count-amount-recorded">...</span>
|
||||
<div class="layer">
|
||||
<div class="loading">
|
||||
<label>Loading Data:</label>
|
||||
<span class="loading-complete">0</span> / <span class="loading-total">0</span> <span class="loading-time">(--ms)</span>
|
||||
</div>
|
||||
<div class="profile-count">
|
||||
<label>Steps looped:</label>
|
||||
<span class="profile-count-value profile-count-steps-looped">...</span>
|
||||
<div class="loading">
|
||||
<label>Loading Content:</label>
|
||||
<span class="loading-content-complete">0</span> / <span class="loading-content-total">0</span> <span class="loading-content-time">(--ms)</span>
|
||||
</div>
|
||||
<div class="profile-count">
|
||||
<label>Blocks executed:</label>
|
||||
<span class="profile-count-value profile-count-blocks-executed">...</span>
|
||||
<div class="loading">
|
||||
<label>Hydrating:</label>
|
||||
<span class="loading-hydrate-complete">0</span> / <span class="loading-hydrate-total">0</span> <span class="loading-hydrate-time">(--ms)</span>
|
||||
</div>
|
||||
<div class="loading">
|
||||
<label>Memory:</label>
|
||||
<span class="loading-memory-current">--</span> / <span class="loading-memory-peak">--</span>
|
||||
</div>
|
||||
<div class="profile-count-group">
|
||||
<div class="profile-count">
|
||||
<label>Percent of time worked:</label>
|
||||
<span class="profile-count-value profile-count-amount-recorded">...</span>
|
||||
</div>
|
||||
<div class="profile-count">
|
||||
<label>Steps looped:</label>
|
||||
<span class="profile-count-value profile-count-steps-looped">...</span>
|
||||
</div>
|
||||
<div class="profile-count">
|
||||
<label>Blocks executed:</label>
|
||||
<span class="profile-count-value profile-count-blocks-executed">...</span>
|
||||
</div>
|
||||
<a class="share"><div class="profile-count">
|
||||
<label>Share this report</label>
|
||||
</div></a>
|
||||
<a class="share" target="_parent">
|
||||
<div class="profile-count">
|
||||
<label>Run the full suite</label>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a class="share"><div class="profile-count">
|
||||
<label>Share this report</label>
|
||||
</div></a>
|
||||
<a class="share" target="_parent">
|
||||
<div class="profile-count">
|
||||
<label>Run the full suite</label>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="profile-tables">
|
||||
|
@ -79,16 +105,6 @@
|
|||
|
||||
<div id="blocks"></div>
|
||||
|
||||
<!-- FPS counter, Blocks, Renderer -->
|
||||
<script src="./vendor.js"></script>
|
||||
<!-- Storage module -->
|
||||
<script src="./scratch-storage.js"></script>
|
||||
<!-- Stage rendering -->
|
||||
<script src="./scratch-render.js"></script>
|
||||
<!-- SVG rendering -->
|
||||
<script src="./scratch-svg-renderer.js"></script>
|
||||
<!-- VM -->
|
||||
<script src="./scratch-vm.js"></script>
|
||||
<!-- Playground -->
|
||||
<script src="./benchmark.js"></script>
|
||||
</body>
|
||||
|
|
|
@ -12,18 +12,33 @@ class TaskQueue {
|
|||
*
|
||||
* @param {number} maxTokens - the maximum number of tokens in the bucket (burst size).
|
||||
* @param {number} refillRate - the number of tokens to be added per second (sustain rate).
|
||||
* @param {number} [startingTokens=maxTokens] - the number of tokens the bucket starts with.
|
||||
* @param {object} options - optional settings for the new task queue instance.
|
||||
* @property {number} startingTokens - the number of tokens the bucket starts with (default: `maxTokens`).
|
||||
* @property {number} maxTotalCost - reject a task if total queue cost would pass this limit (default: no limit).
|
||||
* @memberof TaskQueue
|
||||
*/
|
||||
constructor (maxTokens, refillRate, startingTokens = maxTokens) {
|
||||
constructor (maxTokens, refillRate, options = {}) {
|
||||
this._maxTokens = maxTokens;
|
||||
this._refillRate = refillRate;
|
||||
this._pendingTaskRecords = [];
|
||||
this._tokenCount = startingTokens;
|
||||
this._tokenCount = options.hasOwnProperty('startingTokens') ? options.startingTokens : maxTokens;
|
||||
this._maxTotalCost = options.hasOwnProperty('maxTotalCost') ? options.maxTotalCost : Infinity;
|
||||
this._timer = new Timer();
|
||||
this._timer.start();
|
||||
this._timeout = null;
|
||||
this._lastUpdateTime = this._timer.timeElapsed();
|
||||
|
||||
this._runTasks = this._runTasks.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of queued tasks which have not yet started.
|
||||
*
|
||||
* @readonly
|
||||
* @memberof TaskQueue
|
||||
*/
|
||||
get length () {
|
||||
return this._pendingTaskRecords.length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,38 +50,57 @@ class TaskQueue {
|
|||
* @memberof TaskQueue
|
||||
*/
|
||||
do (task, cost = 1) {
|
||||
const newRecord = {};
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
newRecord.wrappedTask = () => {
|
||||
const canRun = this._refillAndSpend(cost);
|
||||
if (canRun) {
|
||||
// Remove this task from the queue and run it
|
||||
this._pendingTaskRecords.shift();
|
||||
try {
|
||||
resolve(task());
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
if (this._maxTotalCost < Infinity) {
|
||||
const currentTotalCost = this._pendingTaskRecords.reduce((t, r) => t + r.cost, 0);
|
||||
if (currentTotalCost + cost > this._maxTotalCost) {
|
||||
return Promise.reject('Maximum total cost exceeded');
|
||||
}
|
||||
}
|
||||
const newRecord = {
|
||||
cost
|
||||
};
|
||||
newRecord.promise = new Promise((resolve, reject) => {
|
||||
newRecord.cancel = () => {
|
||||
reject(new Error('Task canceled'));
|
||||
};
|
||||
|
||||
// Tell the next wrapper to start trying to run its task
|
||||
if (this._pendingTaskRecords.length > 0) {
|
||||
const nextRecord = this._pendingTaskRecords[0];
|
||||
nextRecord.wrappedTask();
|
||||
}
|
||||
} else {
|
||||
// This task can't run yet. Estimate when it will be able to, then try again.
|
||||
newRecord.reject = reject;
|
||||
this._waitUntilAffordable(cost).then(() => newRecord.wrappedTask());
|
||||
// The caller, `_runTasks()`, is responsible for cost-checking and spending tokens.
|
||||
newRecord.wrappedTask = () => {
|
||||
try {
|
||||
resolve(task());
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
});
|
||||
this._pendingTaskRecords.push(newRecord);
|
||||
|
||||
// If the queue has been idle we need to prime the pump
|
||||
if (this._pendingTaskRecords.length === 1) {
|
||||
newRecord.wrappedTask();
|
||||
this._runTasks();
|
||||
}
|
||||
|
||||
return promise;
|
||||
return newRecord.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel one pending task, rejecting its promise.
|
||||
*
|
||||
* @param {Promise} taskPromise - the promise returned by `do()`.
|
||||
* @returns {boolean} - true if the task was found, or false otherwise.
|
||||
* @memberof TaskQueue
|
||||
*/
|
||||
cancel (taskPromise) {
|
||||
const taskIndex = this._pendingTaskRecords.findIndex(r => r.promise === taskPromise);
|
||||
if (taskIndex !== -1) {
|
||||
const [taskRecord] = this._pendingTaskRecords.splice(taskIndex, 1);
|
||||
taskRecord.cancel();
|
||||
if (taskIndex === 0 && this._pendingTaskRecords.length > 0) {
|
||||
this._runTasks();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,15 +110,16 @@ class TaskQueue {
|
|||
*/
|
||||
cancelAll () {
|
||||
if (this._timeout !== null) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timer.clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
this._pendingTaskRecords.forEach(r => r.reject());
|
||||
const oldTasks = this._pendingTaskRecords;
|
||||
this._pendingTaskRecords = [];
|
||||
oldTasks.forEach(r => r.cancel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for calling @ _refill() then _spend(cost).
|
||||
* Shorthand for calling _refill() then _spend(cost).
|
||||
*
|
||||
* @see {@link TaskQueue#_refill}
|
||||
* @see {@link TaskQueue#_spend}
|
||||
|
@ -129,33 +164,37 @@ class TaskQueue {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a Promise which will resolve when the bucket will be able to "afford" the given cost.
|
||||
* Note that this won't refill the bucket, so make sure to refill after the promise resolves.
|
||||
* Loop until the task queue is empty, running each task and spending tokens to do so.
|
||||
* Any time the bucket can't afford the next task, delay asynchronously until it can.
|
||||
*
|
||||
* @param {number} cost - wait until the token count is at least this much.
|
||||
* @returns {Promise} - to be resolved once the bucket is due for a token count greater than or equal to the cost.
|
||||
* @memberof TaskQueue
|
||||
*/
|
||||
_waitUntilAffordable (cost) {
|
||||
if (cost <= this._tokenCount) {
|
||||
return Promise.resolve();
|
||||
_runTasks () {
|
||||
if (this._timeout) {
|
||||
this._timer.clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
if (!(cost <= this._maxTokens)) {
|
||||
return Promise.reject(new Error(`Task cost ${cost} is greater than bucket limit ${this._maxTokens}`));
|
||||
for (;;) {
|
||||
const nextRecord = this._pendingTaskRecords.shift();
|
||||
if (!nextRecord) {
|
||||
// We ran out of work. Go idle until someone adds another task to the queue.
|
||||
return;
|
||||
}
|
||||
if (nextRecord.cost > this._maxTokens) {
|
||||
throw new Error(`Task cost ${nextRecord.cost} is greater than bucket limit ${this._maxTokens}`);
|
||||
}
|
||||
// Refill before each task in case the time it took for the last task to run was enough to afford the next.
|
||||
if (this._refillAndSpend(nextRecord.cost)) {
|
||||
nextRecord.wrappedTask();
|
||||
} else {
|
||||
// We can't currently afford this task. Put it back and wait until we can and try again.
|
||||
this._pendingTaskRecords.unshift(nextRecord);
|
||||
const tokensNeeded = Math.max(nextRecord.cost - this._tokenCount, 0);
|
||||
const estimatedWait = Math.ceil(1000 * tokensNeeded / this._refillRate);
|
||||
this._timeout = this._timer.setTimeout(this._runTasks, estimatedWait);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const tokensNeeded = Math.max(cost - this._tokenCount, 0);
|
||||
const estimatedWait = Math.ceil(1000 * tokensNeeded / this._refillRate);
|
||||
|
||||
let timeout = null;
|
||||
const onTimeout = () => {
|
||||
if (this._timeout === timeout) {
|
||||
this._timeout = null;
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
this._timeout = timeout = setTimeout(onTimeout, estimatedWait);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -90,6 +90,25 @@ class Timer {
|
|||
timeElapsed () {
|
||||
return this.nowObj.now() - this.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a handler function after a specified amount of time has elapsed.
|
||||
* @param {function} handler - function to call after the timeout
|
||||
* @param {number} timeout - number of milliseconds to delay before calling the handler
|
||||
* @returns {number} - the ID of the new timeout
|
||||
*/
|
||||
setTimeout (handler, timeout) {
|
||||
return global.setTimeout(handler, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a timeout from the pending timeout pool.
|
||||
* @param {number} timeoutId - the ID returned by `setTimeout()`
|
||||
* @memberof Timer
|
||||
*/
|
||||
clearTimeout (timeoutId) {
|
||||
global.clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Timer;
|
||||
|
|
167
test/fixtures/mock-timer.js
vendored
Normal file
167
test/fixtures/mock-timer.js
vendored
Normal file
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* Mimic the Timer class with external control of the "time" value, allowing tests to run more quickly and
|
||||
* reliably. Multiple instances of this class operate independently: they may report different time values, and
|
||||
* advancing one timer will not trigger timeouts set on another.
|
||||
*/
|
||||
class MockTimer {
|
||||
/**
|
||||
* Creates an instance of MockTimer.
|
||||
* @param {*} [nowObj=null] - alert the caller that this parameter, supported by Timer, is not supported here.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
constructor (nowObj = null) {
|
||||
if (nowObj) {
|
||||
throw new Error('nowObj is not implemented in MockTimer');
|
||||
}
|
||||
|
||||
/**
|
||||
* The fake "current time" value, in epoch milliseconds.
|
||||
* @type {number}
|
||||
*/
|
||||
this._mockTime = 0;
|
||||
|
||||
/**
|
||||
* Used to store the start time of a timer action.
|
||||
* Updated when calling `timer.start`.
|
||||
* @type {number}
|
||||
*/
|
||||
this.startTime = 0;
|
||||
|
||||
/**
|
||||
* The ID to use the next time `setTimeout` is called.
|
||||
* @type {number}
|
||||
*/
|
||||
this._nextTimeoutId = 1;
|
||||
|
||||
/**
|
||||
* Map of timeout ID to pending timeout callback info.
|
||||
* @type {Map.<Object>}
|
||||
* @property {number} time - the time at/after which this handler should run
|
||||
* @property {Function} handler - the handler to call when the time comes
|
||||
*/
|
||||
this._timeouts = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance this MockTimer's idea of "current time", running timeout handlers if appropriate.
|
||||
*
|
||||
* @param {number} milliseconds - the amount of time to add to the current mock time value, in milliseconds.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
advanceMockTime (milliseconds) {
|
||||
if (milliseconds < 0) {
|
||||
throw new Error('Time may not move backward');
|
||||
}
|
||||
this._mockTime += milliseconds;
|
||||
this._runTimeouts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance this MockTimer's idea of "current time", running timeout handlers if appropriate.
|
||||
*
|
||||
* @param {number} milliseconds - the amount of time to add to the current mock time value, in milliseconds.
|
||||
* @returns {Promise} - promise which resolves after timeout handlers have had an opportunity to run.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
advanceMockTimeAsync (milliseconds) {
|
||||
return new Promise(resolve => {
|
||||
this.advanceMockTime(milliseconds);
|
||||
global.setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} - current mock time elapsed since 1 January 1970 00:00:00 UTC.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
time () {
|
||||
return this._mockTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a time accurate relative to other times produced by this function.
|
||||
* @returns {number} ms-scale accurate time relative to other relative times.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
relativeTime () {
|
||||
return this._mockTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a timer for measuring elapsed time.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
start () {
|
||||
this.startTime = this._mockTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} - the time elapsed since `start()` was called.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
timeElapsed () {
|
||||
return this._mockTime - this.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a handler function after a specified amount of time has elapsed.
|
||||
* Guaranteed to happen in between "ticks" of JavaScript.
|
||||
* @param {function} handler - function to call after the timeout
|
||||
* @param {number} timeout - number of milliseconds to delay before calling the handler
|
||||
* @returns {number} - the ID of the new timeout.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
setTimeout (handler, timeout) {
|
||||
const timeoutId = this._nextTimeoutId++;
|
||||
this._timeouts.set(timeoutId, {
|
||||
time: this._mockTime + timeout,
|
||||
handler
|
||||
});
|
||||
this._runTimeouts();
|
||||
return timeoutId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a particular timeout from the pending timeout pool.
|
||||
* @param {number} timeoutId - the value returned from `setTimeout()`
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
clearTimeout (timeoutId) {
|
||||
this._timeouts.delete(timeoutId);
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: this method has no equivalent in `Timer`. Do not use this method outside of tests!
|
||||
* @returns {boolean} - true if there are any pending timeouts, false otherwise.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
hasTimeouts () {
|
||||
return this._timeouts.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run any timeout handlers whose timeouts have expired.
|
||||
* @memberof MockTimer
|
||||
*/
|
||||
_runTimeouts () {
|
||||
const ready = [];
|
||||
|
||||
this._timeouts.forEach((timeoutRecord, timeoutId) => {
|
||||
const isReady = timeoutRecord.time <= this._mockTime;
|
||||
if (isReady) {
|
||||
ready.push(timeoutRecord);
|
||||
this._timeouts.delete(timeoutId);
|
||||
}
|
||||
});
|
||||
|
||||
// sort so that earlier timeouts run before later timeouts
|
||||
ready.sort((a, b) => a.time < b.time);
|
||||
|
||||
// next tick, call everything that's ready
|
||||
global.setTimeout(() => {
|
||||
ready.forEach(o => o.handler());
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MockTimer;
|
91
test/unit/mock-timer.js
Normal file
91
test/unit/mock-timer.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
const test = require('tap').test;
|
||||
const MockTimer = require('../fixtures/mock-timer');
|
||||
|
||||
test('spec', t => {
|
||||
const timer = new MockTimer();
|
||||
|
||||
t.type(MockTimer, 'function');
|
||||
t.type(timer, 'object');
|
||||
|
||||
// Most members of MockTimer mimic members of Timer.
|
||||
t.type(timer.startTime, 'number');
|
||||
t.type(timer.time, 'function');
|
||||
t.type(timer.start, 'function');
|
||||
t.type(timer.timeElapsed, 'function');
|
||||
t.type(timer.setTimeout, 'function');
|
||||
t.type(timer.clearTimeout, 'function');
|
||||
|
||||
// A few members of MockTimer have no Timer equivalent and should only be used in tests.
|
||||
t.type(timer.advanceMockTime, 'function');
|
||||
t.type(timer.advanceMockTimeAsync, 'function');
|
||||
t.type(timer.hasTimeouts, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('time', t => {
|
||||
const timer = new MockTimer();
|
||||
const delta = 1;
|
||||
|
||||
const time1 = timer.time();
|
||||
const time2 = timer.time();
|
||||
timer.advanceMockTime(delta);
|
||||
const time3 = timer.time();
|
||||
|
||||
t.equal(time1, time2);
|
||||
t.equal(time2 + delta, time3);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('start / timeElapsed', t => new Promise(resolve => {
|
||||
const timer = new MockTimer();
|
||||
const halfDelay = 1;
|
||||
const fullDelay = halfDelay + halfDelay;
|
||||
|
||||
timer.start();
|
||||
|
||||
let timeoutCalled = 0;
|
||||
|
||||
// Wait and measure timer
|
||||
timer.setTimeout(() => {
|
||||
t.equal(timeoutCalled, 0);
|
||||
++timeoutCalled;
|
||||
|
||||
const timeElapsed = timer.timeElapsed();
|
||||
t.equal(timeElapsed, fullDelay);
|
||||
t.end();
|
||||
|
||||
resolve();
|
||||
}, fullDelay);
|
||||
|
||||
// this should not trigger the callback
|
||||
timer.advanceMockTime(halfDelay);
|
||||
|
||||
// give the mock timer a chance to run tasks
|
||||
global.setTimeout(() => {
|
||||
// we've only mock-waited for half the delay so it should not have run yet
|
||||
t.equal(timeoutCalled, 0);
|
||||
|
||||
// this should trigger the callback
|
||||
timer.advanceMockTime(halfDelay);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
test('clearTimeout / hasTimeouts', t => new Promise((resolve, reject) => {
|
||||
const timer = new MockTimer();
|
||||
|
||||
const timeoutId = timer.setTimeout(() => {
|
||||
reject(new Error('Canceled task ran'));
|
||||
}, 1);
|
||||
|
||||
timer.setTimeout(() => {
|
||||
resolve('Non-canceled task ran');
|
||||
t.end();
|
||||
}, 2);
|
||||
|
||||
timer.clearTimeout(timeoutId);
|
||||
|
||||
while (timer.hasTimeouts()) {
|
||||
timer.advanceMockTime(1);
|
||||
}
|
||||
}));
|
|
@ -1,21 +1,92 @@
|
|||
const test = require('tap').skip;
|
||||
const test = require('tap').test;
|
||||
|
||||
const TaskQueue = require('../../src/util/task-queue');
|
||||
|
||||
const MockTimer = require('../fixtures/mock-timer');
|
||||
const testCompare = require('../fixtures/test-compare');
|
||||
|
||||
test('constructor', t => {
|
||||
// Max tokens = 1000, refill 1000 tokens per second (1 per millisecond), and start with 0 tokens
|
||||
const bukkit = new TaskQueue(1000, 1000, 0);
|
||||
// Max tokens = 1000
|
||||
// Refill 1000 tokens per second (1 per millisecond)
|
||||
// Token bucket starts empty
|
||||
// Max total cost of queued tasks = 10000 tokens = 10 seconds
|
||||
const makeTestQueue = () => {
|
||||
const bukkit = new TaskQueue(1000, 1000, {
|
||||
startingTokens: 0,
|
||||
maxTotalCost: 10000
|
||||
});
|
||||
|
||||
// Simulate time passing with a stubbed timer
|
||||
const simulatedTimeStart = Date.now();
|
||||
bukkit._timer = {timeElapsed: () => Date.now() - simulatedTimeStart};
|
||||
const mockTimer = new MockTimer();
|
||||
bukkit._timer = mockTimer;
|
||||
mockTimer.start();
|
||||
|
||||
return bukkit;
|
||||
};
|
||||
|
||||
test('spec', t => {
|
||||
t.type(TaskQueue, 'function');
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
t.type(bukkit, 'object');
|
||||
|
||||
t.type(bukkit.length, 'number');
|
||||
t.type(bukkit.do, 'function');
|
||||
t.type(bukkit.cancel, 'function');
|
||||
t.type(bukkit.cancelAll, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('constructor', t => {
|
||||
t.ok(new TaskQueue(1, 1));
|
||||
t.ok(new TaskQueue(1, 1, {}));
|
||||
t.ok(new TaskQueue(1, 1, {startingTokens: 0}));
|
||||
t.ok(new TaskQueue(1, 1, {maxTotalCost: 999}));
|
||||
t.ok(new TaskQueue(1, 1, {startingTokens: 0, maxTotalCost: 999}));
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('run tasks', async t => {
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
const taskResults = [];
|
||||
|
||||
const promises = [
|
||||
bukkit.do(() => {
|
||||
taskResults.push('a');
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '>=', 50, 'Costly task must wait');
|
||||
}, 50),
|
||||
bukkit.do(() => {
|
||||
taskResults.push('b');
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '>=', 60, 'Tasks must run in serial');
|
||||
}, 10),
|
||||
bukkit.do(() => {
|
||||
taskResults.push('c');
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '<=', 70, 'Cheap task should run soon');
|
||||
}, 1)
|
||||
];
|
||||
|
||||
// advance 10 simulated milliseconds per JS tick
|
||||
while (bukkit.length > 0) {
|
||||
await bukkit._timer.advanceMockTimeAsync(10);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
t.deepEqual(taskResults, ['a', 'b', 'c'], 'All tasks must run in correct order');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('cancel', async t => {
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
const taskResults = [];
|
||||
const promises = [];
|
||||
const goodCancelMessage = 'Task was canceled correctly';
|
||||
bukkit.do(() => taskResults.push('nope'), 999).then(
|
||||
const afterCancelMessage = 'Task was run correctly';
|
||||
const cancelTaskPromise = bukkit.do(
|
||||
() => {
|
||||
taskResults.push('nope');
|
||||
}, 999);
|
||||
const cancelCheckPromise = cancelTaskPromise.then(
|
||||
() => {
|
||||
t.fail('Task should have been canceled');
|
||||
},
|
||||
|
@ -23,19 +94,99 @@ test('constructor', t => {
|
|||
taskResults.push(goodCancelMessage);
|
||||
}
|
||||
);
|
||||
bukkit.cancelAll();
|
||||
promises.push(
|
||||
bukkit.do(() => taskResults.push('a'), 50).then(() =>
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '>=', 50, 'Costly task must wait')
|
||||
),
|
||||
bukkit.do(() => taskResults.push('b'), 10).then(() =>
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '>=', 60, 'Tasks must run in serial')
|
||||
),
|
||||
bukkit.do(() => taskResults.push('c'), 1).then(() =>
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '<', 80, 'Cheap task should run soon')
|
||||
)
|
||||
);
|
||||
return Promise.all(promises).then(() => {
|
||||
t.deepEqual(taskResults, [goodCancelMessage, 'a', 'b', 'c'], 'All tasks must run in correct order');
|
||||
const keepTaskPromise = bukkit.do(
|
||||
() => {
|
||||
taskResults.push(afterCancelMessage);
|
||||
testCompare(t, bukkit._timer.timeElapsed(), '<', 10, 'Canceled task must not delay other tasks');
|
||||
}, 5);
|
||||
|
||||
// give the bucket a chance to make a mistake
|
||||
await bukkit._timer.advanceMockTimeAsync(1);
|
||||
|
||||
t.equal(bukkit.length, 2);
|
||||
const taskWasCanceled = bukkit.cancel(cancelTaskPromise);
|
||||
t.ok(taskWasCanceled);
|
||||
t.equal(bukkit.length, 1);
|
||||
|
||||
while (bukkit.length > 0) {
|
||||
await bukkit._timer.advanceMockTimeAsync(1);
|
||||
}
|
||||
|
||||
return Promise.all([cancelCheckPromise, keepTaskPromise]).then(() => {
|
||||
t.deepEqual(taskResults, [goodCancelMessage, afterCancelMessage]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('cancelAll', async t => {
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
const taskResults = [];
|
||||
const goodCancelMessage1 = 'Task1 was canceled correctly';
|
||||
const goodCancelMessage2 = 'Task2 was canceled correctly';
|
||||
|
||||
const promises = [
|
||||
bukkit.do(() => taskResults.push('nope'), 999).then(
|
||||
() => {
|
||||
t.fail('Task1 should have been canceled');
|
||||
},
|
||||
() => {
|
||||
taskResults.push(goodCancelMessage1);
|
||||
}
|
||||
),
|
||||
bukkit.do(() => taskResults.push('nah'), 999).then(
|
||||
() => {
|
||||
t.fail('Task2 should have been canceled');
|
||||
},
|
||||
() => {
|
||||
taskResults.push(goodCancelMessage2);
|
||||
}
|
||||
)
|
||||
];
|
||||
|
||||
// advance time, but not enough that any task should run
|
||||
await bukkit._timer.advanceMockTimeAsync(100);
|
||||
|
||||
bukkit.cancelAll();
|
||||
|
||||
// advance enough that both tasks would run if they hadn't been canceled
|
||||
await bukkit._timer.advanceMockTimeAsync(10000);
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
t.deepEqual(taskResults, [goodCancelMessage1, goodCancelMessage2], 'Tasks should cancel in order');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('max total cost', async t => {
|
||||
const bukkit = makeTestQueue();
|
||||
|
||||
let numTasks = 0;
|
||||
|
||||
const task = () => ++numTasks;
|
||||
|
||||
// Fill the queue
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
bukkit.do(task, 1000);
|
||||
}
|
||||
|
||||
// This one should be rejected because the queue is full
|
||||
bukkit
|
||||
.do(task, 1000)
|
||||
.then(
|
||||
() => {
|
||||
t.fail('Full queue did not reject task');
|
||||
},
|
||||
() => {
|
||||
t.pass();
|
||||
}
|
||||
);
|
||||
|
||||
while (bukkit.length > 0) {
|
||||
await bukkit._timer.advanceMockTimeAsync(1000);
|
||||
}
|
||||
|
||||
// this should be 10 if the last task is rejected or 11 if it runs
|
||||
t.equal(numTasks, 10);
|
||||
t.end();
|
||||
});
|
||||
|
|
|
@ -11,6 +11,8 @@ test('spec', t => {
|
|||
t.type(timer.time, 'function');
|
||||
t.type(timer.start, 'function');
|
||||
t.type(timer.timeElapsed, 'function');
|
||||
t.type(timer.setTimeout, 'function');
|
||||
t.type(timer.clearTimeout, 'function');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
@ -32,7 +34,7 @@ test('start / timeElapsed', t => {
|
|||
timer.start();
|
||||
|
||||
// Wait and measure timer
|
||||
setTimeout(() => {
|
||||
timer.setTimeout(() => {
|
||||
const timeElapsed = timer.timeElapsed();
|
||||
t.ok(timeElapsed >= 0);
|
||||
t.ok(timeElapsed >= (delay - threshold) &&
|
||||
|
@ -40,3 +42,15 @@ test('start / timeElapsed', t => {
|
|||
t.end();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
test('setTimeout / clearTimeout', t => new Promise((resolve, reject) => {
|
||||
const timer = new Timer();
|
||||
const cancelId = timer.setTimeout(() => {
|
||||
reject(new Error('Canceled task ran'));
|
||||
}, 1);
|
||||
timer.setTimeout(() => {
|
||||
resolve('Non-canceled task ran');
|
||||
t.end();
|
||||
}, 2);
|
||||
timer.clearTimeout(cancelId);
|
||||
}));
|
||||
|
|
|
@ -88,19 +88,7 @@ module.exports = [
|
|||
defaultsDeep({}, base, {
|
||||
target: 'web',
|
||||
entry: {
|
||||
'scratch-vm': './src/index.js',
|
||||
'vendor': [
|
||||
// FPS counter
|
||||
'stats.js/build/stats.min.js',
|
||||
// Scratch Blocks
|
||||
'scratch-blocks/dist/vertical.js',
|
||||
// Audio
|
||||
'scratch-audio',
|
||||
// Storage
|
||||
'scratch-storage',
|
||||
// Renderer
|
||||
'scratch-render'
|
||||
],
|
||||
'benchmark': './src/playground/benchmark',
|
||||
'video-sensing-extension-debug': './src/extensions/scratch3_video_sensing/debug'
|
||||
},
|
||||
output: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue