Merge branch 'develop' into greenkeeper/script-loader-0.7.2
This commit is contained in:
72 files changed
+3808
-698
No files matched your search
+3
-2
@@ -5,13 +5,14 @@ node_js:
|
||||
env:
|
||||
- NPM_SCRIPT="tap:unit -- --jobs=4"
|
||||
- NPM_SCRIPT="tap:integration -- --jobs=4"
|
||||
- NODE_ENV=production
|
||||
sudo: false
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
install:
|
||||
- npm install
|
||||
- npm update
|
||||
- npm --production=false install
|
||||
- npm --production=false update
|
||||
script: npm run $NPM_SCRIPT
|
||||
jobs:
|
||||
include:
|
||||
|
||||
+16
-15
@@ -11,18 +11,18 @@
|
||||
},
|
||||
"main": "./dist/node/scratch-vm.js",
|
||||
"scripts": {
|
||||
"build": "./node_modules/.bin/webpack --progress --colors --bail",
|
||||
"coverage": "./node_modules/.bin/tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov",
|
||||
"deploy": "touch playground/.nojekyll && ./node_modules/.bin/gh-pages -t -d playground -m \"Build for $(git log --pretty=format:%H -n1)\"",
|
||||
"lint": "./node_modules/.bin/eslint .",
|
||||
"build": "webpack --progress --colors --bail",
|
||||
"coverage": "tap ./test/{unit,integration}/*.js --coverage --coverage-report=lcov",
|
||||
"deploy": "touch playground/.nojekyll && gh-pages -t -d playground -m \"Build for $(git log --pretty=format:%H -n1)\"",
|
||||
"lint": "eslint .",
|
||||
"prepublish": "in-publish && npm run build || not-in-publish",
|
||||
"start": "./node_modules/.bin/webpack-dev-server",
|
||||
"tap": "./node_modules/.bin/tap ./test/{unit,integration}/*.js",
|
||||
"tap:unit": "./node_modules/.bin/tap ./test/unit/*.js",
|
||||
"tap:integration": "./node_modules/.bin/tap ./test/integration/*.js",
|
||||
"start": "webpack-dev-server",
|
||||
"tap": "tap ./test/{unit,integration}/*.js",
|
||||
"tap:unit": "tap ./test/unit/*.js",
|
||||
"tap:integration": "tap ./test/integration/*.js",
|
||||
"test": "npm run lint && npm run tap",
|
||||
"watch": "./node_modules/.bin/webpack --progress --colors --watch",
|
||||
"version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
||||
"watch": "webpack --progress --colors --watch",
|
||||
"version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"adm-zip": "0.4.7",
|
||||
@@ -31,10 +31,11 @@
|
||||
"babel-loader": "^7.0.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"copy-webpack-plugin": "4.0.1",
|
||||
"escape-html": "1.0.3",
|
||||
"eslint": "^4.5.0",
|
||||
"eslint-config-scratch": "^4.0.0",
|
||||
"eslint-config-scratch": "^5.0.0",
|
||||
"expose-loader": "0.7.3",
|
||||
"gh-pages": "^0.12.0",
|
||||
"gh-pages": "^1.1.0",
|
||||
"got": "5.7.1",
|
||||
"highlightjs": "^9.8.0",
|
||||
"htmlparser2": "3.9.2",
|
||||
@@ -43,13 +44,13 @@
|
||||
"json": "^9.0.4",
|
||||
"lodash.defaultsdeep": "4.6.0",
|
||||
"minilog": "3.1.0",
|
||||
"promise": "7.1.1",
|
||||
"promise": "8.0.1",
|
||||
"scratch-audio": "latest",
|
||||
"scratch-blocks": "latest",
|
||||
"scratch-render": "latest",
|
||||
"scratch-storage": "^0.2.0",
|
||||
"scratch-storage": "^0.3.0",
|
||||
"script-loader": "0.7.2",
|
||||
"socket.io-client": "1.7.3",
|
||||
"socket.io-client": "2.0.4",
|
||||
"stats.js": "^0.17.0",
|
||||
"tap": "^10.2.0",
|
||||
"tiny-worker": "^2.1.1",
|
||||
|
||||
+18
-18
@@ -52,8 +52,8 @@ class Scratch3DataBlocks {
|
||||
// If it is, report contents joined together with no separator.
|
||||
// If it's not, report contents joined together with a space.
|
||||
let allSingleLetters = true;
|
||||
for (let i = 0; i < list.contents.length; i++) {
|
||||
const listItem = list.contents[i];
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
const listItem = list.value[i];
|
||||
if (!((typeof listItem === 'string') &&
|
||||
(listItem.length === 1))) {
|
||||
allSingleLetters = false;
|
||||
@@ -61,73 +61,73 @@ class Scratch3DataBlocks {
|
||||
}
|
||||
}
|
||||
if (allSingleLetters) {
|
||||
return list.contents.join('');
|
||||
return list.value.join('');
|
||||
}
|
||||
return list.contents.join(' ');
|
||||
return list.value.join(' ');
|
||||
|
||||
}
|
||||
|
||||
addToList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(args.LIST);
|
||||
list.contents.push(args.ITEM);
|
||||
list.value.push(args.ITEM);
|
||||
}
|
||||
|
||||
deleteOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(args.LIST);
|
||||
const index = Cast.toListIndex(args.INDEX, list.contents.length);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
} else if (index === Cast.LIST_ALL) {
|
||||
list.contents = [];
|
||||
list.value = [];
|
||||
return;
|
||||
}
|
||||
list.contents.splice(index - 1, 1);
|
||||
list.value.splice(index - 1, 1);
|
||||
}
|
||||
|
||||
insertAtList (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(args.LIST);
|
||||
const index = Cast.toListIndex(args.INDEX, list.contents.length + 1);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length + 1);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
}
|
||||
list.contents.splice(index - 1, 0, item);
|
||||
list.value.splice(index - 1, 0, item);
|
||||
}
|
||||
|
||||
replaceItemOfList (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(args.LIST);
|
||||
const index = Cast.toListIndex(args.INDEX, list.contents.length);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return;
|
||||
}
|
||||
list.contents.splice(index - 1, 1, item);
|
||||
list.value.splice(index - 1, 1, item);
|
||||
}
|
||||
|
||||
getItemOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(args.LIST);
|
||||
const index = Cast.toListIndex(args.INDEX, list.contents.length);
|
||||
const index = Cast.toListIndex(args.INDEX, list.value.length);
|
||||
if (index === Cast.LIST_INVALID) {
|
||||
return '';
|
||||
}
|
||||
return list.contents[index - 1];
|
||||
return list.value[index - 1];
|
||||
}
|
||||
|
||||
lengthOfList (args, util) {
|
||||
const list = util.target.lookupOrCreateList(args.LIST);
|
||||
return list.contents.length;
|
||||
return list.value.length;
|
||||
}
|
||||
|
||||
listContainsItem (args, util) {
|
||||
const item = args.ITEM;
|
||||
const list = util.target.lookupOrCreateList(args.LIST);
|
||||
if (list.contents.indexOf(item) >= 0) {
|
||||
if (list.value.indexOf(item) >= 0) {
|
||||
return true;
|
||||
}
|
||||
// Try using Scratch comparison operator on each item.
|
||||
// (Scratch considers the string '123' equal to the number 123).
|
||||
for (let i = 0; i < list.contents.length; i++) {
|
||||
if (Cast.compare(list.contents[i], item) === 0) {
|
||||
for (let i = 0; i < list.value.length; i++) {
|
||||
if (Cast.compare(list.value[i], item) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
const Cast = require('../util/cast');
|
||||
const Clone = require('../util/clone');
|
||||
const RenderedTarget = require('../sprites/rendered-target');
|
||||
|
||||
/**
|
||||
* @typedef {object} BubbleState - the bubble state associated with a particular target.
|
||||
* @property {Boolean} onSpriteRight - tracks whether the bubble is right or left of the sprite.
|
||||
* @property {?int} drawableId - the ID of the associated bubble Drawable, null if none.
|
||||
* @property {Boolean} drawableVisible - false if drawable has been hidden by blank text.
|
||||
* See _renderBubble for explanation of this optimization.
|
||||
* @property {string} text - the text of the bubble.
|
||||
* @property {string} type - the type of the bubble, "say" or "think"
|
||||
*/
|
||||
|
||||
class Scratch3LooksBlocks {
|
||||
constructor (runtime) {
|
||||
@@ -7,6 +19,196 @@ class Scratch3LooksBlocks {
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
this._onTargetMoved = this._onTargetMoved.bind(this);
|
||||
this._onResetBubbles = this._onResetBubbles.bind(this);
|
||||
this._onTargetWillExit = this._onTargetWillExit.bind(this);
|
||||
this._updateBubble = this._updateBubble.bind(this);
|
||||
|
||||
// Reset all bubbles on start/stop
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._onResetBubbles);
|
||||
this.runtime.on('targetWasRemoved', this._onTargetWillExit);
|
||||
|
||||
// Enable other blocks to use bubbles like ask/answer
|
||||
this.runtime.on('SAY', this._updateBubble);
|
||||
}
|
||||
|
||||
/**
|
||||
* The default bubble state, to be used when a target has no existing bubble state.
|
||||
* @type {BubbleState}
|
||||
*/
|
||||
static get DEFAULT_BUBBLE_STATE () {
|
||||
return {
|
||||
drawableId: null,
|
||||
drawableVisible: true,
|
||||
onSpriteRight: true,
|
||||
skinId: null,
|
||||
text: '',
|
||||
type: 'say'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The key to load & store a target's bubble-related state.
|
||||
* @type {string}
|
||||
*/
|
||||
static get STATE_KEY () {
|
||||
return 'Scratch.looks';
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* @private
|
||||
*/
|
||||
_getBubbleState (target) {
|
||||
let bubbleState = target.getCustomState(Scratch3LooksBlocks.STATE_KEY);
|
||||
if (!bubbleState) {
|
||||
bubbleState = Clone.simple(Scratch3LooksBlocks.DEFAULT_BUBBLE_STATE);
|
||||
target.setCustomState(Scratch3LooksBlocks.STATE_KEY, bubbleState);
|
||||
}
|
||||
return bubbleState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a target which has moved.
|
||||
* @param {RenderedTarget} target - the target which has moved.
|
||||
* @private
|
||||
*/
|
||||
_onTargetMoved (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
if (bubbleState.drawableId) {
|
||||
this._positionBubble(target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a target which is exiting.
|
||||
* @param {RenderedTarget} target - the target.
|
||||
* @private
|
||||
*/
|
||||
_onTargetWillExit (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
if (bubbleState.drawableId && bubbleState.skinId) {
|
||||
this.runtime.renderer.destroyDrawable(bubbleState.drawableId);
|
||||
this.runtime.renderer.destroySkin(bubbleState.skinId);
|
||||
bubbleState.drawableId = null;
|
||||
bubbleState.skinId = null;
|
||||
bubbleState.drawableVisible = true; // Reset back to default value
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
target.removeListener(RenderedTarget.EVENT_TARGET_MOVED, this._onTargetMoved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle project start/stop by clearing all visible bubbles.
|
||||
* @private
|
||||
*/
|
||||
_onResetBubbles () {
|
||||
for (let n = 0; n < this.runtime.targets.length; n++) {
|
||||
this._onTargetWillExit(this.runtime.targets[n]);
|
||||
}
|
||||
clearTimeout(this._bubbleTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the bubble of a target. If it doesn't fit on the specified side, flip and rerender.
|
||||
* @param {!Target} target Target whose bubble needs positioning.
|
||||
* @private
|
||||
*/
|
||||
_positionBubble (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
const [bubbleWidth, bubbleHeight] = this.runtime.renderer.getSkinSize(bubbleState.drawableId);
|
||||
const targetBounds = target.getBounds();
|
||||
const stageBounds = this.runtime.getTargetForStage().getBounds();
|
||||
if (bubbleState.onSpriteRight && bubbleWidth + targetBounds.right > stageBounds.right &&
|
||||
(targetBounds.left - bubbleWidth > stageBounds.left)) { // Only flip if it would fit
|
||||
bubbleState.onSpriteRight = false;
|
||||
this._renderBubble(target);
|
||||
} else if (!bubbleState.onSpriteRight && targetBounds.left - bubbleWidth < stageBounds.left &&
|
||||
(bubbleWidth + targetBounds.right < stageBounds.right)) { // Only flip if it would fit
|
||||
bubbleState.onSpriteRight = true;
|
||||
this._renderBubble(target);
|
||||
} else {
|
||||
this.runtime.renderer.updateDrawableProperties(bubbleState.drawableId, {
|
||||
position: [
|
||||
bubbleState.onSpriteRight ? (
|
||||
Math.min(stageBounds.right - bubbleWidth, targetBounds.right)
|
||||
) : (
|
||||
Math.max(stageBounds.left, targetBounds.left - bubbleWidth)
|
||||
),
|
||||
Math.min(stageBounds.top, targetBounds.top + bubbleHeight)
|
||||
]
|
||||
});
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a visible bubble for a target. If a bubble exists for the target,
|
||||
* just set it to visible and update the type/text. Otherwise create a new
|
||||
* bubble and update the relevant custom state.
|
||||
* @param {!Target} target Target who needs a bubble.
|
||||
* @return {undefined} Early return if text is empty string.
|
||||
* @private
|
||||
*/
|
||||
_renderBubble (target) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
const {drawableVisible, type, text, onSpriteRight} = bubbleState;
|
||||
|
||||
// Remove the bubble if target is not visible, or text is being set to blank
|
||||
// without being initialized. See comment below about blank text optimization.
|
||||
if (!target.visible || (text === '' && !bubbleState.skinId)) {
|
||||
return this._onTargetWillExit(target);
|
||||
}
|
||||
|
||||
if (bubbleState.skinId) {
|
||||
// Optimization: if text is set to blank, hide the drawable instead of
|
||||
// getting rid of it. This prevents flickering in "typewriter" projects
|
||||
if ((text === '' && drawableVisible) || (text !== '' && !drawableVisible)) {
|
||||
bubbleState.drawableVisible = text !== '';
|
||||
this.runtime.renderer.updateDrawableProperties(bubbleState.drawableId, {
|
||||
visible: bubbleState.drawableVisible
|
||||
});
|
||||
}
|
||||
if (bubbleState.drawableVisible) {
|
||||
this.runtime.renderer.updateTextSkin(bubbleState.skinId, type, text, onSpriteRight, [0, 0]);
|
||||
}
|
||||
} else {
|
||||
target.addListener(RenderedTarget.EVENT_TARGET_MOVED, this._onTargetMoved);
|
||||
|
||||
// TODO is there a way to figure out before rendering whether to default left or right?
|
||||
const targetBounds = target.getBounds();
|
||||
const stageBounds = this.runtime.getTargetForStage().getBounds();
|
||||
if (targetBounds.right + 170 > stageBounds.right) {
|
||||
bubbleState.onSpriteRight = false;
|
||||
}
|
||||
|
||||
bubbleState.drawableId = this.runtime.renderer.createDrawable();
|
||||
bubbleState.skinId = this.runtime.renderer.createTextSkin(type, text, bubbleState.onSpriteRight, [0, 0]);
|
||||
|
||||
this.runtime.renderer.setDrawableOrder(bubbleState.drawableId, Infinity);
|
||||
this.runtime.renderer.updateDrawableProperties(bubbleState.drawableId, {
|
||||
skinId: bubbleState.skinId
|
||||
});
|
||||
}
|
||||
|
||||
this._positionBubble(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point for say/think blocks. Clears existing bubble if the text is empty.
|
||||
* Set the bubble custom state and then call _renderBubble.
|
||||
* @param {!Target} target Target that say/think blocks are being called on.
|
||||
* @param {!string} type Either "say" or "think"
|
||||
* @param {!string} text The text for the bubble, empty string clears the bubble.
|
||||
* @private
|
||||
*/
|
||||
_updateBubble (target, type, text) {
|
||||
const bubbleState = this._getBubbleState(target);
|
||||
bubbleState.type = type;
|
||||
bubbleState.text = text;
|
||||
this._renderBubble(target);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,7 +220,7 @@ class Scratch3LooksBlocks {
|
||||
looks_say: this.say,
|
||||
looks_sayforsecs: this.sayforsecs,
|
||||
looks_think: this.think,
|
||||
looks_thinkforsecs: this.sayforsecs,
|
||||
looks_thinkforsecs: this.thinkforsecs,
|
||||
looks_show: this.show,
|
||||
looks_hide: this.hide,
|
||||
looks_switchcostumeto: this.switchCostume,
|
||||
@@ -41,30 +243,35 @@ class Scratch3LooksBlocks {
|
||||
}
|
||||
|
||||
say (args, util) {
|
||||
util.target.setSay('say', args.MESSAGE);
|
||||
// @TODO in 2.0 calling say/think resets the right/left bias of the bubble
|
||||
this._updateBubble(util.target, 'say', String(args.MESSAGE));
|
||||
}
|
||||
|
||||
sayforsecs (args, util) {
|
||||
util.target.setSay('say', args.MESSAGE);
|
||||
this.say(args, util);
|
||||
const _target = util.target;
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
this._bubbleTimeout = setTimeout(() => {
|
||||
this._bubbleTimeout = null;
|
||||
// Clear say bubble and proceed.
|
||||
util.target.setSay();
|
||||
this._updateBubble(_target, 'say', '');
|
||||
resolve();
|
||||
}, 1000 * args.SECS);
|
||||
});
|
||||
}
|
||||
|
||||
think (args, util) {
|
||||
util.target.setSay('think', args.MESSAGE);
|
||||
this._updateBubble(util.target, 'think', String(args.MESSAGE));
|
||||
}
|
||||
|
||||
thinkforsecs (args, util) {
|
||||
util.target.setSay('think', args.MESSAGE);
|
||||
this.think(args, util);
|
||||
const _target = util.target;
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
this._bubbleTimeout = setTimeout(() => {
|
||||
this._bubbleTimeout = null;
|
||||
// Clear say bubble and proceed.
|
||||
util.target.setSay();
|
||||
this._updateBubble(_target, 'think', '');
|
||||
resolve();
|
||||
}, 1000 * args.SECS);
|
||||
});
|
||||
@@ -72,10 +279,12 @@ class Scratch3LooksBlocks {
|
||||
|
||||
show (args, util) {
|
||||
util.target.setVisible(true);
|
||||
this._renderBubble(util.target);
|
||||
}
|
||||
|
||||
hide (args, util) {
|
||||
util.target.setVisible(false);
|
||||
this._renderBubble(util.target);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+307
-98
@@ -1,14 +1,35 @@
|
||||
const ArgumentType = require('../extension-support/argument-type');
|
||||
const BlockType = require('../extension-support/block-type');
|
||||
const Cast = require('../util/cast');
|
||||
const Clone = require('../util/clone');
|
||||
const Color = require('../util/color');
|
||||
const MathUtil = require('../util/math-util');
|
||||
const RenderedTarget = require('../sprites/rendered-target');
|
||||
const log = require('../util/log');
|
||||
|
||||
/**
|
||||
* Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
|
||||
* @type {string}
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
const iconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+cGVuLWljb248L3RpdGxlPjxnIHN0cm9rZT0iIzU3NUU3NSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik04Ljc1MyAzNC42MDJsLTQuMjUgMS43OCAxLjc4My00LjIzN2MxLjIxOC0yLjg5MiAyLjkwNy01LjQyMyA1LjAzLTcuNTM4TDMxLjA2NiA0LjkzYy44NDYtLjg0MiAyLjY1LS40MSA0LjAzMi45NjcgMS4zOCAxLjM3NSAxLjgxNiAzLjE3My45NyA0LjAxNUwxNi4zMTggMjkuNTljLTIuMTIzIDIuMTE2LTQuNjY0IDMuOC03LjU2NSA1LjAxMiIgZmlsbD0iI0ZGRiIvPjxwYXRoIGQ9Ik0yOS40MSA2LjExcy00LjQ1LTIuMzc4LTguMjAyIDUuNzcyYy0xLjczNCAzLjc2Ni00LjM1IDEuNTQ2LTQuMzUgMS41NDYiLz48cGF0aCBkPSJNMzYuNDIgOC44MjVjMCAuNDYzLS4xNC44NzMtLjQzMiAxLjE2NGwtOS4zMzUgOS4zYy4yODItLjI5LjQxLS42NjguNDEtMS4xMiAwLS44NzQtLjUwNy0xLjk2My0xLjQwNi0yLjg2OC0xLjM2Mi0xLjM1OC0zLjE0Ny0xLjgtNC4wMDItLjk5TDMwLjk5IDUuMDFjLjg0NC0uODQgMi42NS0uNDEgNC4wMzUuOTYuODk4LjkwNCAxLjM5NiAxLjk4MiAxLjM5NiAyLjg1NU0xMC41MTUgMzMuNzc0Yy0uNTczLjMwMi0xLjE1Ny41Ny0xLjc2NC44M0w0LjUgMzYuMzgybDEuNzg2LTQuMjM1Yy4yNTgtLjYwNC41My0xLjE4Ni44MzMtMS43NTcuNjkuMTgzIDEuNDQ4LjYyNSAyLjEwOCAxLjI4Mi42Ni42NTggMS4xMDIgMS40MTIgMS4yODcgMi4xMDIiIGZpbGw9IiM0Qzk3RkYiLz48cGF0aCBkPSJNMzYuNDk4IDguNzQ4YzAgLjQ2NC0uMTQuODc0LS40MzMgMS4xNjVsLTE5Ljc0MiAxOS42OGMtMi4xMyAyLjExLTQuNjczIDMuNzkzLTcuNTcyIDUuMDFMNC41IDM2LjM4bC45NzQtMi4zMTYgMS45MjUtLjgwOGMyLjg5OC0xLjIxOCA1LjQ0LTIuOSA3LjU3LTUuMDFsMTkuNzQzLTE5LjY4Yy4yOTItLjI5Mi40MzItLjcwMi40MzItMS4xNjUgMC0uNjQ2LS4yNy0xLjQtLjc4LTIuMTIyLjI1LjE3Mi41LjM3Ny43MzcuNjE0Ljg5OC45MDUgMS4zOTYgMS45ODMgMS4zOTYgMi44NTYiIGZpbGw9IiM1NzVFNzUiIG9wYWNpdHk9Ii4xNSIvPjxwYXRoIGQ9Ik0xOC40NSAxMi44M2MwIC41LS40MDQuOTA1LS45MDQuOTA1cy0uOTA1LS40MDUtLjkwNS0uOTA0YzAtLjUuNDA3LS45MDMuOTA2LS45MDMuNSAwIC45MDQuNDA0LjkwNC45MDR6IiBmaWxsPSIjNTc1RTc1Ii8+PC9nPjwvc3ZnPg==';
|
||||
|
||||
/**
|
||||
* Enum for pen color parameters.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
const ColorParam = {
|
||||
COLOR: 'color',
|
||||
SATURATION: 'saturation',
|
||||
BRIGHTNESS: 'brightness',
|
||||
TRANSPARENCY: 'transparency'
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} PenState - the pen state associated with a particular target.
|
||||
* @property {Boolean} penDown - tracks whether the pen should draw for this target.
|
||||
* @property {number} hue - the current hue of the pen.
|
||||
* @property {number} shade - the current shade of the pen.
|
||||
* @property {number} color - the current color (hue) of the pen.
|
||||
* @property {PenAttributes} penAttributes - cached pen attributes for the renderer. This is the authoritative value for
|
||||
* diameter but not for pen color.
|
||||
*/
|
||||
@@ -53,9 +74,11 @@ class Scratch3PenBlocks {
|
||||
static get DEFAULT_PEN_STATE () {
|
||||
return {
|
||||
penDown: false,
|
||||
hue: 120,
|
||||
shade: 50,
|
||||
color: 66.66,
|
||||
saturation: 100,
|
||||
brightness: 100,
|
||||
transparency: 0,
|
||||
_shade: 50, // Used only for legacy `change shade by` blocks
|
||||
penAttributes: {
|
||||
color4f: [0, 0, 1, 1],
|
||||
diameter: 1
|
||||
@@ -169,44 +192,22 @@ class Scratch3PenBlocks {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cached color from the hue, shade and transparency values in the provided
|
||||
* PenState object.
|
||||
* @param {PenState} penState - the pen state to update.
|
||||
* @private
|
||||
*/
|
||||
_updatePenColor (penState) {
|
||||
let rgb = Color.hsvToRgb({h: penState.hue * 180 / 100, s: 1, v: 1});
|
||||
const shade = (penState.shade > 100) ? 200 - penState.shade : penState.shade;
|
||||
if (shade < 50) {
|
||||
rgb = Color.mixRgb(Color.RGB_BLACK, rgb, (10 + shade) / 60);
|
||||
} else {
|
||||
rgb = Color.mixRgb(rgb, Color.RGB_WHITE, (shade - 50) / 60);
|
||||
}
|
||||
penState.penAttributes.color4f[0] = rgb.r / 255.0;
|
||||
penState.penAttributes.color4f[1] = rgb.g / 255.0;
|
||||
penState.penAttributes.color4f[2] = rgb.b / 255.0;
|
||||
penState.penAttributes.color4f[3] = this._transparencyToAlpha(penState.transparency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a pen hue or shade values to the range (0,200).
|
||||
* @param {number} value - the pen hue or shade value to the proper range.
|
||||
* Wrap a color input into the range (0,100).
|
||||
* @param {number} value - the value to be wrapped.
|
||||
* @returns {number} the wrapped value.
|
||||
* @private
|
||||
*/
|
||||
_wrapHueOrShade (value) {
|
||||
value = value % 200;
|
||||
if (value < 0) value += 200;
|
||||
return value;
|
||||
_wrapColor (value) {
|
||||
return MathUtil.wrapClamp(value, 0, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a pen transparency value to the range (0,100).
|
||||
* @param {number} value - the pen transparency value to be clamped.
|
||||
* Clamp a pen color parameter to the range (0,100).
|
||||
* @param {number} value - the value to be clamped.
|
||||
* @returns {number} the clamped value.
|
||||
* @private
|
||||
*/
|
||||
_clampTransparency (value) {
|
||||
_clampColorParam (value) {
|
||||
return MathUtil.clamp(value, 0, 100);
|
||||
}
|
||||
|
||||
@@ -235,24 +236,151 @@ class Scratch3PenBlocks {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
* @returns {object} metadata for this extension and its blocks.
|
||||
*/
|
||||
getPrimitives () {
|
||||
getInfo () {
|
||||
return {
|
||||
pen_clear: this.clear,
|
||||
pen_stamp: this.stamp,
|
||||
pen_pendown: this.penDown,
|
||||
pen_penup: this.penUp,
|
||||
pen_setpencolortocolor: this.setPenColorToColor,
|
||||
pen_changepencolorby: this.changePenHueBy,
|
||||
pen_setpencolortonum: this.setPenHueToNumber,
|
||||
pen_changepenshadeby: this.changePenShadeBy,
|
||||
pen_setpenshadeto: this.setPenShadeToNumber,
|
||||
pen_changepensizeby: this.changePenSizeBy,
|
||||
pen_setpensizeto: this.setPenSizeTo,
|
||||
pen_changepentransparencyby: this.changePenTransparencyBy,
|
||||
pen_setpentransparencyto: this.setPenTransparencyTo
|
||||
id: 'pen',
|
||||
name: 'Pen',
|
||||
iconURI: iconURI,
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'clear',
|
||||
blockType: BlockType.COMMAND
|
||||
},
|
||||
{
|
||||
opcode: 'stamp',
|
||||
blockType: BlockType.COMMAND
|
||||
},
|
||||
{
|
||||
opcode: 'penDown',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'pen down'
|
||||
},
|
||||
{
|
||||
opcode: 'penUp',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'pen up'
|
||||
},
|
||||
{
|
||||
opcode: 'setPenColorToColor',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen color to [COLOR]',
|
||||
arguments: {
|
||||
COLOR: {
|
||||
type: ArgumentType.COLOR
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'changePenColorParamBy',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'change pen [COLOR_PARAM] by [VALUE]',
|
||||
arguments: {
|
||||
COLOR_PARAM: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'colorParam',
|
||||
defaultValue: ColorParam.COLOR
|
||||
},
|
||||
VALUE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'setPenColorParamTo',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen [COLOR_PARAM] to [VALUE]',
|
||||
arguments: {
|
||||
COLOR_PARAM: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'colorParam',
|
||||
defaultValue: ColorParam.COLOR
|
||||
},
|
||||
VALUE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'changePenSizeBy',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'change pen size by [SIZE]',
|
||||
arguments: {
|
||||
SIZE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'setPenSizeTo',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen size to [SIZE]',
|
||||
arguments: {
|
||||
SIZE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
/* Legacy blocks, should not be shown in flyout */
|
||||
{
|
||||
opcode: 'setPenShadeToNumber',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen shade to [SHADE]',
|
||||
arguments: {
|
||||
SHADE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
},
|
||||
hideFromPalette: true
|
||||
},
|
||||
{
|
||||
opcode: 'changePenShadeBy',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'change pen shade by [SHADE]',
|
||||
arguments: {
|
||||
SHADE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
},
|
||||
hideFromPalette: true
|
||||
},
|
||||
{
|
||||
opcode: 'setPenHueToNumber',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set pen hue to [HUE]',
|
||||
arguments: {
|
||||
HUE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
},
|
||||
hideFromPalette: true
|
||||
},
|
||||
{
|
||||
opcode: 'changePenHueBy',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'change pen hue by [HUE]',
|
||||
arguments: {
|
||||
HUE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
},
|
||||
hideFromPalette: true
|
||||
}
|
||||
],
|
||||
menus: {
|
||||
colorParam:
|
||||
[ColorParam.COLOR, ColorParam.SATURATION,
|
||||
ColorParam.BRIGHTNESS, ColorParam.TRANSPARENCY]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -319,6 +447,7 @@ class Scratch3PenBlocks {
|
||||
|
||||
/**
|
||||
* The pen "set pen color to {color}" block sets the pen to a particular RGB color.
|
||||
* The transparency is reset to 0.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {int} COLOR - the color to set, expressed as a 24-bit RGB value (0xRRGGBB).
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
@@ -327,66 +456,91 @@ class Scratch3PenBlocks {
|
||||
const penState = this._getPenState(util.target);
|
||||
const rgb = Cast.toRgbColorObject(args.COLOR);
|
||||
const hsv = Color.rgbToHsv(rgb);
|
||||
penState.color = (hsv.h / 360) * 100;
|
||||
penState.saturation = hsv.s * 100;
|
||||
penState.brightness = hsv.v * 100;
|
||||
if (rgb.hasOwnProperty('a')) {
|
||||
penState.transparency = 100 * (1 - (rgb.a / 255.0));
|
||||
} else {
|
||||
penState.transparency = 0;
|
||||
}
|
||||
|
||||
penState.hue = 200 * hsv.h / 360;
|
||||
penState.shade = 50 * hsv.v;
|
||||
// Set the legacy "shade" value the same way scratch 2 did.
|
||||
penState._shade = penState.brightness / 2;
|
||||
|
||||
this._updatePenColor(penState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cached color from the color, saturation, brightness and transparency values
|
||||
* in the provided PenState object.
|
||||
* @param {PenState} penState - the pen state to update.
|
||||
* @private
|
||||
*/
|
||||
_updatePenColor (penState) {
|
||||
const rgb = Color.hsvToRgb({
|
||||
h: penState.color * 360 / 100,
|
||||
s: penState.saturation / 100,
|
||||
v: penState.brightness / 100
|
||||
});
|
||||
penState.penAttributes.color4f[0] = rgb.r / 255.0;
|
||||
penState.penAttributes.color4f[1] = rgb.g / 255.0;
|
||||
penState.penAttributes.color4f[2] = rgb.b / 255.0;
|
||||
if (rgb.hasOwnProperty('a')) { // Will there always be an 'a'?
|
||||
penState.penAttributes.color4f[3] = rgb.a / 255.0;
|
||||
} else {
|
||||
penState.penAttributes.color4f[3] = 1;
|
||||
penState.penAttributes.color4f[3] = this._transparencyToAlpha(penState.transparency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or change a single color parameter on the pen state, and update the pen color.
|
||||
* @param {ColorParam} param - the name of the color parameter to set or change.
|
||||
* @param {number} value - the value to set or change the param by.
|
||||
* @param {PenState} penState - the pen state to update.
|
||||
* @param {boolean} change - if true change param by value, if false set param to value.
|
||||
* @private
|
||||
*/
|
||||
_setOrChangeColorParam (param, value, penState, change) {
|
||||
switch (param) {
|
||||
case ColorParam.COLOR:
|
||||
penState.color = this._wrapColor(value + (change ? penState.color : 0));
|
||||
break;
|
||||
case ColorParam.SATURATION:
|
||||
penState.saturation = this._clampColorParam(value + (change ? penState.saturation : 0));
|
||||
break;
|
||||
case ColorParam.BRIGHTNESS:
|
||||
penState.brightness = this._clampColorParam(value + (change ? penState.brightness : 0));
|
||||
break;
|
||||
case ColorParam.TRANSPARENCY:
|
||||
penState.transparency = this._clampColorParam(value + (change ? penState.transparency : 0));
|
||||
break;
|
||||
default:
|
||||
log.warn(`Tried to set or change unknown color parameter: ${param}`);
|
||||
}
|
||||
penState.transparency = this._alphaToTransparency(penState.penAttributes.color4f[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The pen "change pen color by {number}" block rotates the hue of the pen by the given amount.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} COLOR - the amount of desired hue rotation.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
*/
|
||||
changePenHueBy (args, util) {
|
||||
const penState = this._getPenState(util.target);
|
||||
penState.hue = this._wrapHueOrShade(penState.hue + Cast.toNumber(args.COLOR));
|
||||
this._updatePenColor(penState);
|
||||
}
|
||||
|
||||
/**
|
||||
* The pen "set pen color to {number}" block sets the hue of the pen.
|
||||
* The "change pen {ColorParam} by {number}" block changes one of the pen's color parameters
|
||||
* by a given amound.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} COLOR - the desired hue.
|
||||
* @property {ColorParam} COLOR_PARAM - the name of the selected color parameter.
|
||||
* @property {number} VALUE - the amount to change the selected parameter by.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
*/
|
||||
setPenHueToNumber (args, util) {
|
||||
changePenColorParamBy (args, util) {
|
||||
const penState = this._getPenState(util.target);
|
||||
penState.hue = this._wrapHueOrShade(Cast.toNumber(args.COLOR));
|
||||
this._updatePenColor(penState);
|
||||
this._setOrChangeColorParam(args.COLOR_PARAM, Cast.toNumber(args.VALUE), penState, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* The pen "change pen shade by {number}" block changes the "shade" of the pen, related to the HSV value.
|
||||
* The "set pen {ColorParam} to {number}" block sets one of the pen's color parameters
|
||||
* to a given amound.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} SHADE - the amount of desired shade change.
|
||||
* @property {ColorParam} COLOR_PARAM - the name of the selected color parameter.
|
||||
* @property {number} VALUE - the amount to set the selected parameter to.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
*/
|
||||
changePenShadeBy (args, util) {
|
||||
setPenColorParamTo (args, util) {
|
||||
const penState = this._getPenState(util.target);
|
||||
penState.shade = this._wrapHueOrShade(penState.shade + Cast.toNumber(args.SHADE));
|
||||
this._updatePenColor(penState);
|
||||
}
|
||||
|
||||
/**
|
||||
* The pen "set pen shade to {number}" block sets the "shade" of the pen, related to the HSV value.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} SHADE - the amount of desired shade change.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
*/
|
||||
setPenShadeToNumber (args, util) {
|
||||
const penState = this._getPenState(util.target);
|
||||
penState.shade = this._wrapHueOrShade(Cast.toNumber(args.SHADE));
|
||||
this._updatePenColor(penState);
|
||||
this._setOrChangeColorParam(args.COLOR_PARAM, Cast.toNumber(args.VALUE), penState, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,29 +565,84 @@ class Scratch3PenBlocks {
|
||||
penAttributes.diameter = this._clampPenSize(Cast.toNumber(args.SIZE));
|
||||
}
|
||||
|
||||
/* LEGACY OPCODES */
|
||||
/**
|
||||
* The pen "change pen transparency by {number}" block changes the RGBA "transparency" of the pen.
|
||||
* Scratch 2 "hue" param is equivelant to twice the new "color" param.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} TRANSPARENCY - the amount of desired transparency change.
|
||||
* @property {number} HUE - the amount to set the hue to.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
*/
|
||||
changePenTransparencyBy (args, util) {
|
||||
setPenHueToNumber (args, util) {
|
||||
const penState = this._getPenState(util.target);
|
||||
penState.transparency = this._clampTransparency(penState.transparency + Cast.toNumber(args.TRANSPARENCY));
|
||||
const hueValue = Cast.toNumber(args.HUE);
|
||||
const colorValue = hueValue / 2;
|
||||
this._setOrChangeColorParam(ColorParam.COLOR, colorValue, penState, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scratch 2 "hue" param is equivelant to twice the new "color" param.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} HUE - the amount of desired hue change.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
*/
|
||||
changePenHueBy (args, util) {
|
||||
const penState = this._getPenState(util.target);
|
||||
const hueChange = Cast.toNumber(args.HUE);
|
||||
const colorChange = hueChange / 2;
|
||||
this._setOrChangeColorParam(ColorParam.COLOR, colorChange, penState, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use legacy "set shade" code to calculate RGB value for shade,
|
||||
* then convert back to HSV and store those components.
|
||||
* It is important to also track the given shade in penState._shade
|
||||
* because it cannot be accurately backed out of the new HSV later.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} SHADE - the amount to set the shade to.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
*/
|
||||
setPenShadeToNumber (args, util) {
|
||||
const penState = this._getPenState(util.target);
|
||||
let newShade = Cast.toNumber(args.SHADE);
|
||||
|
||||
// Wrap clamp the new shade value the way scratch 2 did.
|
||||
newShade = newShade % 200;
|
||||
if (newShade < 0) newShade += 200;
|
||||
|
||||
// Create the new color in RGB using the scratch 2 "shade" model
|
||||
let rgb = Color.hsvToRgb({h: penState.color * 360 / 100, s: 1, v: 1});
|
||||
const shade = (newShade > 100) ? 200 - newShade : newShade;
|
||||
if (shade < 50) {
|
||||
rgb = Color.mixRgb(Color.RGB_BLACK, rgb, (10 + shade) / 60);
|
||||
} else {
|
||||
rgb = Color.mixRgb(rgb, Color.RGB_WHITE, (shade - 50) / 60);
|
||||
}
|
||||
|
||||
// Update the pen state according to new color
|
||||
const hsv = Color.rgbToHsv(rgb);
|
||||
penState.color = 100 * hsv.h / 360;
|
||||
penState.saturation = 100 * hsv.s;
|
||||
penState.brightness = 100 * hsv.v;
|
||||
|
||||
// And store the shade that was used to compute this new color for later use.
|
||||
penState._shade = newShade;
|
||||
|
||||
this._updatePenColor(penState);
|
||||
}
|
||||
|
||||
/**
|
||||
* The pen "set pen transparency to {number}" block sets the RGBA "transparency" of the pen.
|
||||
* Because "shade" cannot be backed out of hsv consistently, use the previously
|
||||
* stored penState._shade to make the shade change.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} TRANSPARENCY - the amount of desired transparency change.
|
||||
* @property {number} SHADE - the amount of desired shade change.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
*/
|
||||
setPenTransparencyTo (args, util) {
|
||||
changePenShadeBy (args, util) {
|
||||
const penState = this._getPenState(util.target);
|
||||
penState.transparency = this._clampTransparency(Cast.toNumber(args.TRANSPARENCY));
|
||||
this._updatePenColor(penState);
|
||||
const shadeChange = Cast.toNumber(args.SHADE);
|
||||
this.setPenShadeToNumber({SHADE: penState._shade + shadeChange}, util);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Scratch3PenBlocks;
|
||||
@@ -13,32 +13,42 @@ class Scratch3ProcedureBlocks {
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
procedures_defnoreturn: this.defNoReturn,
|
||||
procedures_callnoreturn: this.callNoReturn,
|
||||
procedures_param: this.param
|
||||
procedures_definition: this.definition,
|
||||
procedures_call: this.call,
|
||||
argument_reporter_string_number: this.param,
|
||||
argument_reporter_boolean: this.param
|
||||
};
|
||||
}
|
||||
|
||||
defNoReturn () {
|
||||
definition () {
|
||||
// No-op: execute the blocks.
|
||||
}
|
||||
|
||||
callNoReturn (args, util) {
|
||||
call (args, util) {
|
||||
if (!util.stackFrame.executed) {
|
||||
const procedureCode = args.mutation.proccode;
|
||||
const paramNames = util.getProcedureParamNames(procedureCode);
|
||||
|
||||
// If null, procedure could not be found, which can happen if custom
|
||||
// block is dragged between sprites without the definition.
|
||||
// Match Scratch 2.0 behavior and noop.
|
||||
if (paramNames === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < paramNames.length; i++) {
|
||||
if (args.hasOwnProperty(`input${i}`)) {
|
||||
util.pushParam(paramNames[i], args[`input${i}`]);
|
||||
}
|
||||
}
|
||||
|
||||
util.stackFrame.executed = true;
|
||||
util.startProcedure(procedureCode);
|
||||
}
|
||||
}
|
||||
|
||||
param (args, util) {
|
||||
const value = util.getParam(args.mutation.paramname);
|
||||
const value = util.getParam(args.VALUE);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,21 @@ class Scratch3SensingBlocks {
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
/**
|
||||
* The "answer" block value.
|
||||
* @type {string}
|
||||
*/
|
||||
this._answer = '';
|
||||
|
||||
/**
|
||||
* The list of queued questions and respective `resolve` callbacks.
|
||||
* @type {!Array}
|
||||
*/
|
||||
this._questionList = [];
|
||||
|
||||
this.runtime.on('ANSWER', this._onAnswer.bind(this));
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._clearAllQuestions.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,10 +43,64 @@ class Scratch3SensingBlocks {
|
||||
sensing_keypressed: this.getKeyPressed,
|
||||
sensing_current: this.current,
|
||||
sensing_dayssince2000: this.daysSince2000,
|
||||
sensing_loudness: this.getLoudness
|
||||
sensing_loudness: this.getLoudness,
|
||||
sensing_askandwait: this.askAndWait,
|
||||
sensing_answer: this.getAnswer
|
||||
};
|
||||
}
|
||||
|
||||
_onAnswer (answer) {
|
||||
this._answer = answer;
|
||||
const questionObj = this._questionList.shift();
|
||||
if (questionObj) {
|
||||
const [_question, resolve, target, wasVisible] = questionObj;
|
||||
// If the target was visible when asked, hide the say bubble.
|
||||
if (wasVisible) {
|
||||
this.runtime.emit('SAY', target, 'say', '');
|
||||
}
|
||||
resolve();
|
||||
this._askNextQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
_enqueueAsk (question, resolve, target, wasVisible) {
|
||||
this._questionList.push([question, resolve, target, wasVisible]);
|
||||
}
|
||||
|
||||
_askNextQuestion () {
|
||||
if (this._questionList.length > 0) {
|
||||
const [question, _resolve, target, wasVisible] = this._questionList[0];
|
||||
// If the target is visible, emit a blank question and use the
|
||||
// say event to trigger a bubble.
|
||||
if (wasVisible) {
|
||||
this.runtime.emit('SAY', target, 'say', question);
|
||||
this.runtime.emit('QUESTION', '');
|
||||
} else {
|
||||
this.runtime.emit('QUESTION', question);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clearAllQuestions () {
|
||||
this._questionList = [];
|
||||
this.runtime.emit('QUESTION', null);
|
||||
}
|
||||
|
||||
askAndWait (args, util) {
|
||||
const _target = util.target;
|
||||
return new Promise(resolve => {
|
||||
const isQuestionAsked = this._questionList.length > 0;
|
||||
this._enqueueAsk(args.QUESTION, resolve, _target, _target.visible);
|
||||
if (!isQuestionAsked) {
|
||||
this._askNextQuestion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAnswer () {
|
||||
return this._answer;
|
||||
}
|
||||
|
||||
touchingObject (args, util) {
|
||||
const requestedObject = args.TOUCHINGOBJECTMENU;
|
||||
if (requestedObject === '_mouse_') {
|
||||
@@ -167,8 +236,10 @@ class Scratch3SensingBlocks {
|
||||
|
||||
// Variables
|
||||
const varName = args.PROPERTY;
|
||||
if (attrTarget.variables.hasOwnProperty(varName)) {
|
||||
return attrTarget.variables[varName].value;
|
||||
for (const id in attrTarget.variables) {
|
||||
if (attrTarget.variables[id].name === varName) {
|
||||
return attrTarget.variables[id].value;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, 0
|
||||
|
||||
@@ -91,10 +91,6 @@ class Scratch3SoundBlocks {
|
||||
sound_play: this.playSound,
|
||||
sound_playuntildone: this.playSoundAndWait,
|
||||
sound_stopallsounds: this.stopAllSounds,
|
||||
sound_playnoteforbeats: this.playNoteForBeats,
|
||||
sound_playdrumforbeats: this.playDrumForBeats,
|
||||
sound_restforbeats: this.restForBeats,
|
||||
sound_setinstrumentto: this.setInstrument,
|
||||
sound_seteffectto: this.setEffect,
|
||||
sound_changeeffectby: this.changeEffect,
|
||||
sound_cleareffects: this.clearEffects,
|
||||
@@ -103,10 +99,7 @@ class Scratch3SoundBlocks {
|
||||
sound_effects_menu: this.effectsMenu,
|
||||
sound_setvolumeto: this.setVolume,
|
||||
sound_changevolumeby: this.changeVolume,
|
||||
sound_volume: this.getVolume,
|
||||
sound_settempotobpm: this.setTempo,
|
||||
sound_changetempoby: this.changeTempo,
|
||||
sound_tempo: this.getTempo
|
||||
sound_volume: this.getVolume
|
||||
};
|
||||
}
|
||||
|
||||
@@ -167,50 +160,6 @@ class Scratch3SoundBlocks {
|
||||
util.target.audioPlayer.stopAllSounds();
|
||||
}
|
||||
|
||||
playNoteForBeats (args, util) {
|
||||
let note = Cast.toNumber(args.NOTE);
|
||||
note = MathUtil.clamp(note, Scratch3SoundBlocks.MIDI_NOTE_RANGE.min, Scratch3SoundBlocks.MIDI_NOTE_RANGE.max);
|
||||
let beats = Cast.toNumber(args.BEATS);
|
||||
beats = this._clampBeats(beats);
|
||||
const soundState = this._getSoundState(util.target);
|
||||
const inst = soundState.currentInstrument;
|
||||
const vol = soundState.volume;
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||
return this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, vol);
|
||||
}
|
||||
|
||||
playDrumForBeats (args, util) {
|
||||
let drum = Cast.toNumber(args.DRUM);
|
||||
drum -= 1; // drums are one-indexed
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||
drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1);
|
||||
let beats = Cast.toNumber(args.BEATS);
|
||||
beats = this._clampBeats(beats);
|
||||
if (util.target.audioPlayer === null) return;
|
||||
return util.target.audioPlayer.playDrumForBeats(drum, beats);
|
||||
}
|
||||
|
||||
restForBeats (args) {
|
||||
let beats = Cast.toNumber(args.BEATS);
|
||||
beats = this._clampBeats(beats);
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||
return this.runtime.audioEngine.waitForBeats(beats);
|
||||
}
|
||||
|
||||
_clampBeats (beats) {
|
||||
return MathUtil.clamp(beats, Scratch3SoundBlocks.BEAT_RANGE.min, Scratch3SoundBlocks.BEAT_RANGE.max);
|
||||
}
|
||||
|
||||
setInstrument (args, util) {
|
||||
const soundState = this._getSoundState(util.target);
|
||||
let instNum = Cast.toNumber(args.INSTRUMENT);
|
||||
instNum -= 1; // instruments are one-indexed
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||
instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments - 1);
|
||||
soundState.currentInstrument = instNum;
|
||||
return this.runtime.audioEngine.instrumentPlayer.loadInstrument(soundState.currentInstrument);
|
||||
}
|
||||
|
||||
setEffect (args, util) {
|
||||
this._updateEffect(args, util, false);
|
||||
}
|
||||
@@ -273,29 +222,6 @@ class Scratch3SoundBlocks {
|
||||
return soundState.volume;
|
||||
}
|
||||
|
||||
setTempo (args) {
|
||||
const tempo = Cast.toNumber(args.TEMPO);
|
||||
this._updateTempo(tempo);
|
||||
}
|
||||
|
||||
changeTempo (args) {
|
||||
const change = Cast.toNumber(args.TEMPO);
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||
const tempo = change + this.runtime.audioEngine.currentTempo;
|
||||
this._updateTempo(tempo);
|
||||
}
|
||||
|
||||
_updateTempo (tempo) {
|
||||
tempo = MathUtil.clamp(tempo, Scratch3SoundBlocks.TEMPO_RANGE.min, Scratch3SoundBlocks.TEMPO_RANGE.max);
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||
this.runtime.audioEngine.setTempo(tempo);
|
||||
}
|
||||
|
||||
getTempo () {
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||
return this.runtime.audioEngine.currentTempo;
|
||||
}
|
||||
|
||||
soundsMenu (args) {
|
||||
return args.SOUND_MENU;
|
||||
}
|
||||
|
||||
+195
-27
@@ -1,6 +1,15 @@
|
||||
const ArgumentType = require('../extension-support/argument-type');
|
||||
const BlockType = require('../extension-support/block-type');
|
||||
const color = require('../util/color');
|
||||
const log = require('../util/log');
|
||||
|
||||
/**
|
||||
* Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
|
||||
* @type {string}
|
||||
*/
|
||||
// eslint-disable-next-line max-len
|
||||
const iconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+d2VkbzItYmxvY2staWNvbjwvdGl0bGU+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMzUuMzEzIDEwLjQ2N0gzMi4wOVY4Ljg2NWMwLS4yMjMuMTgtLjQwNC40MDUtLjQwNGgyLjQxMmMuMjI0IDAgLjQwNi4xODIuNDA2LjQwNXYxLjYwMnpNMzAuNDc3IDEwLjQ2N2gtMy4yMjRWOC44NjVjMC0uMjIzLjE4My0uNDA0LjQwNy0uNDA0aDIuNDFjLjIyNiAwIC40MDcuMTgyLjQwNy40MDV2MS42MDJ6TTI1LjY0IDEwLjQ2N0gyMi40MlY4Ljg2NWMwLS4yMjMuMTgyLS40MDQuNDA2LS40MDRoMi40MWMuMjI2IDAgLjQwNy4xODIuNDA3LjQwNXYxLjYwMnpNMjAuODA2IDEwLjQ2N2gtMy4yMjRWOC44NjVjMC0uMjIzLjE4Mi0uNDA0LjQwNi0uNDA0SDIwLjRjLjIyNCAwIC40MDYuMTgyLjQwNi40MDV2MS42MDJ6TTE1Ljk3IDEwLjQ2N2gtMy4yMjRWOC44NjVjMC0uMjIzLjE4Mi0uNDA0LjQwNy0uNDA0aDIuNDFjLjIyNiAwIC40MDcuMTgyLjQwNy40MDV2MS42MDJ6TTExLjEzNSAxMC40NjdINy45MVY4Ljg2NWMwLS4yMjMuMTgzLS40MDQuNDA3LS40MDRoMi40MTJjLjIyMyAwIC40MDUuMTgyLjQwNS40MDV2MS42MDJ6IiBzdHJva2U9IiM2Rjc4OTMiIGZpbGw9IiNGRkYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0zNy43MyAxMC40NjdINi4zYy0yLjY3IDAtNC44MzYgMi4xNTMtNC44MzYgNC44MDh2My4yMDVoMzcuMDczdi03LjIxYzAtLjQ0NC0uMzYyLS44MDMtLjgwNy0uODAzeiIgc3Ryb2tlPSIjNkY3ODkzIiBmaWxsPSIjRkZGIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMzguMTM0IDMwLjk4SDEuODY3Yy0uMjI0IDAtLjQwMy0uMTgtLjQwMy0uNFYxNi4yMzZoMzIuNzFjLjczIDAgMS40My4yODcgMS45NDUuOC41MTUuNTE0IDEuMjE1LjgwMiAxLjk0NC44MDJoLjQ3M3YxMi43NGMwIC4yMi0uMTguNC0uNDAzLjR6IiBzdHJva2U9IiM2Rjc4OTMiIGZpbGw9IiNFNkU3RTgiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIHN0cm9rZT0iIzZGNzg5MyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNMzQuODMgMTYuMjM3bC40ODMtMi41NjVoMy4yMjMiLz48cGF0aCBkPSJNMzguNTM2IDExLjI2OFYzMC41OGMwIC4yMi0uMTguNC0uNDAzLjRIMS44NjZjLS4yMiAwLS40MDMtLjE4LS40MDMtLjR2LTEuMjAzaDM0LjI4MmMuNjUgMCAxLjE4LS41MjQgMS4xOC0xLjE3M1YxMC40NjdoLjgwNWMuNDQ2IDAgLjgwNi4zNi44MDYuOHoiIHN0cm9rZT0iIzZGNzg5MyIgZmlsbD0iIzZGNzg5MyIgb3BhY2l0eT0iLjE1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTEuNTM4IDE2LjI4aDIwLjE0OGMuMjIyIDAgLjQwMy4xOC40MDMuNHY2LjUyN2MwIC4yMjItLjE4Mi40LS40MDQuNEgxMS41MzhjLS4yMjMgMC0uNDA0LS4xNzgtLjQwNC0uNFYxNi42OGMwLS4yMi4xOC0uNC40MDQtLjQiIGZpbGw9IiNFNkU3RTgiLz48cGF0aCBkPSJNMTEuNTM4IDE2LjI4aDIwLjE0OGMuMjIyIDAgLjQwMy4xOC40MDMuNHY2LjUyN2MwIC4yMjItLjE4Mi40LS40MDQuNEgxMS41MzhjLS4yMjMgMC0uNDA0LS4xNzgtLjQwNC0uNFYxNi42OGMwLS4yMi4xOC0uNC40MDQtLjR6IiBzdHJva2U9IiM2Rjc4OTMiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0zMi4wOSAxNi4yOHY2LjkyN2MwIC4yMjItLjE4LjQtLjQwNC40aC0yMC4xNWMtLjIyIDAtLjQtLjE4LS40LS40di0xLjJoMTguMTZjLjY1MyAwIDEuMTgtLjUyNiAxLjE4LTEuMTc0VjE2LjI4aDEuNjEzeiIgc3Ryb2tlPSIjNkY3ODkzIiBmaWxsPSIjNkU3NzkyIiBvcGFjaXR5PSIuMTUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0zMC40NzcgMTYuMjhoLTMuMjI0di0xLjYwNGMwLS4yMjMuMTgzLS40MDQuNDA3LS40MDRoMi40MWMuMjI2IDAgLjQwNy4xOC40MDcuNDA0djEuNjAzek0xNS45NyAxNi4yOGgtMy4yMjR2LTEuNjA0YzAtLjIyMy4xODItLjQwNC40MDctLjQwNGgyLjQxYy4yMjYgMCAuNDA3LjE4LjQwNy40MDR2MS42MDN6TTI1LjY0IDE2LjI4SDIyLjQydi0xLjYwNGMwLS4yMjMuMTgyLS40MDQuNDA2LS40MDRoMi40MWMuMjI2IDAgLjQwNy4xOC40MDcuNDA0djEuNjAzek0yMC44MDYgMTYuMjhoLTMuMjI0di0xLjYwNGMwLS4yMjMuMTgyLS40MDQuNDA2LS40MDRIMjAuNGMuMjI0IDAgLjQwNi4xOC40MDYuNDA0djEuNjAzeiIgc3Ryb2tlPSIjNkY3ODkzIiBmaWxsPSIjRTZFN0U4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNMTguNTU3IDE5LjkxYzAgMS4wMjUtLjgzNyAxLjg1Ny0xLjg3IDEuODU3LTEuMDMgMC0xLjg2Ny0uODMyLTEuODY3LTEuODU4IDAtMS4wMjcuODM3LTEuODU4IDEuODY4LTEuODU4IDEuMDMyIDAgMS44Ny44MyAxLjg3IDEuODU3ek0yMy40OCAxOS45MWMwIDEuMDI1LS44MzYgMS44NTctMS44NjggMS44NTdzLTEuODctLjgzMi0xLjg3LTEuODU4YzAtMS4wMjcuODM4LTEuODU4IDEuODctMS44NThzMS44NjguODMgMS44NjggMS44NTd6TTI4LjQwNCAxOS45MWMwIDEuMDI1LS44MzcgMS44NTctMS44NjggMS44NTctMS4wMzIgMC0xLjg3LS44MzItMS44Ny0xLjg1OCAwLTEuMDI3LjgzOC0xLjg1OCAxLjg3LTEuODU4IDEuMDMgMCAxLjg2OC44MyAxLjg2OCAxLjg1N3oiIHN0cm9rZT0iIzZGNzg5MyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTE4LjU1NyAxOS45MjJjMCAxLjAyNi0uODM3IDEuODU4LTEuODcgMS44NTgtMS4wMyAwLTEuODY3LS44MzItMS44NjctMS44NTggMC0xLjAyNS44MzctMS44NTcgMS44NjgtMS44NTcgMS4wMzIgMCAxLjg3LjgzMiAxLjg3IDEuODU3TTIzLjQ4IDE5LjkyMmMwIDEuMDI2LS44MzYgMS44NTgtMS44NjggMS44NThzLTEuODctLjgzMi0xLjg3LTEuODU4YzAtMS4wMjUuODM4LTEuODU3IDEuODctMS44NTdzMS44NjguODMyIDEuODY4IDEuODU3TTI4LjQwNCAxOS45MjJjMCAxLjAyNi0uODM3IDEuODU4LTEuODY4IDEuODU4LTEuMDMyIDAtMS44Ny0uODMyLTEuODctMS44NTggMC0xLjAyNS44MzgtMS44NTcgMS44Ny0xLjg1NyAxLjAzIDAgMS44NjguODMyIDEuODY4IDEuODU3IiBmaWxsPSIjNkY3ODkzIiBvcGFjaXR5PSIuNSIvPjwvZz48L3N2Zz4=';
|
||||
|
||||
/**
|
||||
* Manage power, direction, and timers for one WeDo 2.0 motor.
|
||||
*/
|
||||
@@ -243,7 +252,7 @@ class WeDo2 {
|
||||
* @return {number} - the latest value received from the distance sensor.
|
||||
*/
|
||||
get distance () {
|
||||
return this._sensors.distance;
|
||||
return this._sensors.distance * 10;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,9 +380,9 @@ const TiltDirection = {
|
||||
class Scratch3WeDo2Blocks {
|
||||
|
||||
/**
|
||||
* @return {string} - the name of this extension.
|
||||
* @return {string} - the ID of this extension.
|
||||
*/
|
||||
static get EXTENSION_NAME () {
|
||||
static get EXTENSION_ID () {
|
||||
return 'wedo2';
|
||||
}
|
||||
|
||||
@@ -395,7 +404,185 @@ class Scratch3WeDo2Blocks {
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
this.runtime.HACK_WeDo2Blocks = this;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object} metadata for this extension and its blocks.
|
||||
*/
|
||||
getInfo () {
|
||||
return {
|
||||
id: Scratch3WeDo2Blocks.EXTENSION_ID,
|
||||
name: 'WeDo 2.0',
|
||||
iconURI: iconURI,
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'motorOnFor',
|
||||
text: 'turn [MOTOR_ID] on for [DURATION] seconds',
|
||||
blockType: BlockType.COMMAND,
|
||||
arguments: {
|
||||
MOTOR_ID: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'motorID',
|
||||
defaultValue: MotorID.DEFAULT
|
||||
},
|
||||
DURATION: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'motorOn',
|
||||
text: 'turn [MOTOR_ID] on',
|
||||
blockType: BlockType.COMMAND,
|
||||
arguments: {
|
||||
MOTOR_ID: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'motorID',
|
||||
defaultValue: MotorID.DEFAULT
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'motorOff',
|
||||
text: 'turn [MOTOR_ID] off',
|
||||
blockType: BlockType.COMMAND,
|
||||
arguments: {
|
||||
MOTOR_ID: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'motorID',
|
||||
defaultValue: MotorID.DEFAULT
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'startMotorPower',
|
||||
text: 'set [MOTOR_ID] power to [POWER]',
|
||||
blockType: BlockType.COMMAND,
|
||||
arguments: {
|
||||
MOTOR_ID: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'motorID',
|
||||
defaultValue: MotorID.DEFAULT
|
||||
},
|
||||
POWER: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 100
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'setMotorDirection',
|
||||
text: 'set [MOTOR_ID] direction to [DIRECTION]',
|
||||
blockType: BlockType.COMMAND,
|
||||
arguments: {
|
||||
MOTOR_ID: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'motorID',
|
||||
defaultValue: MotorID.DEFAULT
|
||||
},
|
||||
DIRECTION: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'motorDirection',
|
||||
defaultValue: MotorDirection.FORWARD
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'setLightHue',
|
||||
text: 'set light color to [HUE]',
|
||||
blockType: BlockType.COMMAND,
|
||||
arguments: {
|
||||
HUE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'playNoteFor',
|
||||
text: 'play note [NOTE] for [DURATION] seconds',
|
||||
blockType: BlockType.COMMAND,
|
||||
arguments: {
|
||||
NOTE: {
|
||||
type: ArgumentType.NUMBER, // TODO: ArgumentType.MIDI_NOTE?
|
||||
defaultValue: 60
|
||||
},
|
||||
DURATION: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 0.5
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'whenDistance',
|
||||
text: 'when distance [OP] [REFERENCE]',
|
||||
blockType: BlockType.HAT,
|
||||
arguments: {
|
||||
OP: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'lessMore',
|
||||
defaultValue: '<'
|
||||
},
|
||||
REFERENCE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'whenTilted',
|
||||
text: 'when tilted [DIRECTION]',
|
||||
func: 'isTilted',
|
||||
blockType: BlockType.HAT,
|
||||
arguments: {
|
||||
DIRECTION: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'tiltDirectionAny',
|
||||
defaultValue: TiltDirection.ANY
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'getDistance',
|
||||
text: 'distance',
|
||||
blockType: BlockType.REPORTER
|
||||
},
|
||||
{
|
||||
opcode: 'isTilted',
|
||||
text: 'tilted [DIRECTION]?',
|
||||
blockType: BlockType.BOOLEAN,
|
||||
arguments: {
|
||||
DIRECTION: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'tiltDirectionAny',
|
||||
defaultValue: TiltDirection.ANY
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'getTiltAngle',
|
||||
text: 'tilt angle [DIRECTION]',
|
||||
blockType: BlockType.REPORTER,
|
||||
arguments: {
|
||||
DIRECTION: {
|
||||
type: ArgumentType.STRING,
|
||||
menu: 'tiltDirection',
|
||||
defaultValue: TiltDirection.UP
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
menus: {
|
||||
motorID: [MotorID.DEFAULT, MotorID.A, MotorID.B, MotorID.ALL],
|
||||
motorDirection: [MotorDirection.FORWARD, MotorDirection.BACKWARD, MotorDirection.REVERSE],
|
||||
tiltDirection: [TiltDirection.UP, TiltDirection.DOWN, TiltDirection.LEFT, TiltDirection.RIGHT],
|
||||
tiltDirectionAny:
|
||||
[TiltDirection.UP, TiltDirection.DOWN, TiltDirection.LEFT, TiltDirection.RIGHT, TiltDirection.ANY],
|
||||
lessMore: ['<', '>']
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,7 +594,7 @@ class Scratch3WeDo2Blocks {
|
||||
}
|
||||
const deviceManager = this.runtime.ioDevices.deviceManager;
|
||||
const finder = this._finder =
|
||||
deviceManager.searchAndConnect(Scratch3WeDo2Blocks.EXTENSION_NAME, WeDo2.DEVICE_TYPE);
|
||||
deviceManager.searchAndConnect(Scratch3WeDo2Blocks.EXTENSION_ID, WeDo2.DEVICE_TYPE);
|
||||
this._finder.promise.then(
|
||||
socket => {
|
||||
if (this._finder === finder) {
|
||||
@@ -427,27 +614,6 @@ class Scratch3WeDo2Blocks {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the block primitives implemented by this package.
|
||||
* @return {object.<string, Function>} Mapping of opcode to Function.
|
||||
*/
|
||||
getPrimitives () {
|
||||
return {
|
||||
wedo2_motorOnFor: this.motorOnFor,
|
||||
wedo2_motorOn: this.motorOn,
|
||||
wedo2_motorOff: this.motorOff,
|
||||
wedo2_startMotorPower: this.startMotorPower,
|
||||
wedo2_setMotorDirection: this.setMotorDirection,
|
||||
wedo2_setLightHue: this.setLightHue,
|
||||
wedo2_playNoteFor: this.playNoteFor,
|
||||
wedo2_whenDistance: this.whenDistance,
|
||||
wedo2_whenTilted: this.whenTilted,
|
||||
wedo2_getDistance: this.getDistance,
|
||||
wedo2_isTilted: this.isTilted,
|
||||
wedo2_getTiltAngle: this.getTiltAngle
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn specified motor(s) on for a specified duration.
|
||||
* @param {object} args - the block's arguments.
|
||||
@@ -574,8 +740,10 @@ class Scratch3WeDo2Blocks {
|
||||
whenDistance (args) {
|
||||
switch (args.OP) {
|
||||
case '<':
|
||||
case '<':
|
||||
return this._device.distance < args.REFERENCE;
|
||||
case '>':
|
||||
case '>':
|
||||
return this._device.distance > args.REFERENCE;
|
||||
default:
|
||||
log.warn(`Unknown comparison operator in whenDistance: ${args.OP}`);
|
||||
@@ -597,7 +765,7 @@ class Scratch3WeDo2Blocks {
|
||||
* @return {number} - the distance sensor's value, scaled to the [0,100] range.
|
||||
*/
|
||||
getDistance () {
|
||||
return this._device.distance * 10;
|
||||
return this._device.distance;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,6 +91,16 @@ class SharedDispatch {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a particular service lives on another worker.
|
||||
* @param {string} service - the service to check.
|
||||
* @returns {boolean} - true if the service is remote (calls must cross a Worker boundary), false otherwise.
|
||||
* @private
|
||||
*/
|
||||
_isRemoteService (service) {
|
||||
return this._getServiceProvider(service).isRemote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link call}, but force the call to be posted through a particular communication channel.
|
||||
* @param {object} provider - send the call through this object's `postMessage` function.
|
||||
|
||||
@@ -75,6 +75,10 @@ const domToBlock = function (blockDOM, blocks, isTopBlock, parent) {
|
||||
id: fieldId,
|
||||
value: fieldData
|
||||
};
|
||||
const fieldVarType = xmlChild.attribs.variabletype;
|
||||
if (typeof fieldVarType === 'string') {
|
||||
block.fields[fieldName].variableType = fieldVarType;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'value':
|
||||
@@ -156,7 +160,7 @@ const adapter = function (e) {
|
||||
if (typeof e !== 'object') return;
|
||||
if (typeof e.xml !== 'object') return;
|
||||
|
||||
return domToBlocks(html.parseDOM(e.xml.outerHTML));
|
||||
return domToBlocks(html.parseDOM(e.xml.outerHTML, {decodeEntities: true}));
|
||||
};
|
||||
|
||||
module.exports = adapter;
|
||||
@@ -0,0 +1,153 @@
|
||||
const Thread = require('./thread');
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
* Interface provided to block primitive functions for interacting with the
|
||||
* runtime, thread, target, and convenient methods.
|
||||
*/
|
||||
|
||||
class BlockUtility {
|
||||
constructor (sequencer = null, thread = null) {
|
||||
/**
|
||||
* A sequencer block primitives use to branch or start procedures with
|
||||
* @type {?Sequencer}
|
||||
*/
|
||||
this.sequencer = sequencer;
|
||||
|
||||
/**
|
||||
* The block primitives thread with the block's target, stackFrame and
|
||||
* modifiable status.
|
||||
* @type {?Thread}
|
||||
*/
|
||||
this.thread = thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* The target the primitive is working on.
|
||||
* @type {Target}
|
||||
*/
|
||||
get target () {
|
||||
return this.thread.target;
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime the block primitive is running in.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
get runtime () {
|
||||
return this.sequencer.runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* The stack frame used by loop and other blocks to track internal state.
|
||||
* @type {object}
|
||||
*/
|
||||
get stackFrame () {
|
||||
return this.thread.peekStackFrame().executionContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the thread to yield.
|
||||
*/
|
||||
yield () {
|
||||
this.thread.status = Thread.STATUS_YIELD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a branch in the current block.
|
||||
* @param {number} branchNum Which branch to step to (i.e., 1, 2).
|
||||
* @param {boolean} isLoop Whether this block is a loop.
|
||||
*/
|
||||
startBranch (branchNum, isLoop) {
|
||||
this.sequencer.stepToBranch(this.thread, branchNum, isLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all threads.
|
||||
*/
|
||||
stopAll () {
|
||||
this.sequencer.runtime.stopAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop threads other on this target other than the thread holding the
|
||||
* executed block.
|
||||
*/
|
||||
stopOtherTargetThreads () {
|
||||
this.sequencer.runtime.stopForTarget(this.thread.target, this.thread);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop this thread.
|
||||
*/
|
||||
stopThisScript () {
|
||||
this.thread.stopThisScript();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a specified procedure on this thread.
|
||||
* @param {string} procedureCode Procedure code for procedure to start.
|
||||
*/
|
||||
startProcedure (procedureCode) {
|
||||
this.sequencer.stepToProcedure(this.thread, procedureCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get names for parameters for the given procedure.
|
||||
* @param {string} procedureCode Procedure code for procedure to query.
|
||||
* @return {Array.<string>} List of param names for a procedure.
|
||||
*/
|
||||
getProcedureParamNames (procedureCode) {
|
||||
return this.thread.target.blocks.getProcedureParamNames(procedureCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a procedure parameter value by its name.
|
||||
* @param {string} paramName The procedure's parameter name.
|
||||
* @param {*} paramValue The procedure's parameter value.
|
||||
*/
|
||||
pushParam (paramName, paramValue) {
|
||||
this.thread.pushParam(paramName, paramValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the stored parameter value for a given parameter name.
|
||||
* @param {string} paramName The procedure's parameter name.
|
||||
* @return {*} The parameter's current stored value.
|
||||
*/
|
||||
getParam (paramName) {
|
||||
return this.thread.getParam(paramName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start all relevant hats.
|
||||
* @param {!string} requestedHat Opcode of hats to start.
|
||||
* @param {object=} optMatchFields Optionally, fields to match on the hat.
|
||||
* @param {Target=} optTarget Optionally, a target to restrict to.
|
||||
* @return {Array.<Thread>} List of threads started by this function.
|
||||
*/
|
||||
startHats (requestedHat, optMatchFields, optTarget) {
|
||||
return (
|
||||
this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query a named IO device.
|
||||
* @param {string} device The name of like the device, like keyboard.
|
||||
* @param {string} func The name of the device's function to query.
|
||||
* @param {Array.<*>} args Arguments to pass to the device's function.
|
||||
* @return {*} The expected output for the device's function.
|
||||
*/
|
||||
ioQuery (device, func, args) {
|
||||
// Find the I/O device and execute the query/function call.
|
||||
if (
|
||||
this.sequencer.runtime.ioDevices[device] &&
|
||||
this.sequencer.runtime.ioDevices[device][func]) {
|
||||
const devObject = this.sequencer.runtime.ioDevices[device];
|
||||
return devObject[func].apply(devObject, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlockUtility;
|
||||
+119
-27
@@ -25,6 +25,30 @@ class Blocks {
|
||||
* @type {Array.<String>}
|
||||
*/
|
||||
this._scripts = [];
|
||||
|
||||
/**
|
||||
* Runtime Cache
|
||||
* @type {{inputs: {}, procedureParamNames: {}, procedureDefinitions: {}}}
|
||||
* @private
|
||||
*/
|
||||
this._cache = {
|
||||
/**
|
||||
* Cache block inputs by block id
|
||||
* @type {object.<string, !Array.<object>>}
|
||||
*/
|
||||
inputs: {},
|
||||
/**
|
||||
* Cache procedure Param Names by block id
|
||||
* @type {object.<string, ?Array.<string>>}
|
||||
*/
|
||||
procedureParamNames: {},
|
||||
/**
|
||||
* Cache procedure definitions by block id
|
||||
* @type {object.<string, ?string>}
|
||||
*/
|
||||
procedureDefinitions: {}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,11 +129,16 @@ class Blocks {
|
||||
/**
|
||||
* Get all non-branch inputs for a block.
|
||||
* @param {?object} block the block to query.
|
||||
* @return {!object} All non-branch inputs and their associated blocks.
|
||||
* @return {?Array.<object>} All non-branch inputs and their associated blocks.
|
||||
*/
|
||||
getInputs (block) {
|
||||
if (typeof block === 'undefined') return null;
|
||||
const inputs = {};
|
||||
let inputs = this._cache.inputs[block.id];
|
||||
if (typeof inputs !== 'undefined') {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
inputs = {};
|
||||
for (const input in block.inputs) {
|
||||
// Ignore blocks prefixed with branch prefix.
|
||||
if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !==
|
||||
@@ -117,6 +146,8 @@ class Blocks {
|
||||
inputs[input] = block.inputs[input];
|
||||
}
|
||||
}
|
||||
|
||||
this._cache.inputs[block.id] = inputs;
|
||||
return inputs;
|
||||
}
|
||||
|
||||
@@ -149,33 +180,50 @@ class Blocks {
|
||||
* @return {?string} ID of procedure definition.
|
||||
*/
|
||||
getProcedureDefinition (name) {
|
||||
const blockID = this._cache.procedureDefinitions[name];
|
||||
if (typeof blockID !== 'undefined') {
|
||||
return blockID;
|
||||
}
|
||||
|
||||
for (const id in this._blocks) {
|
||||
if (!this._blocks.hasOwnProperty(id)) continue;
|
||||
const block = this._blocks[id];
|
||||
if ((block.opcode === 'procedures_defnoreturn' ||
|
||||
block.opcode === 'procedures_defreturn') &&
|
||||
block.mutation.proccode === name) {
|
||||
return id;
|
||||
if (block.opcode === 'procedures_definition') {
|
||||
const internal = this._getCustomBlockInternal(block);
|
||||
if (internal && internal.mutation.proccode === name) {
|
||||
this._cache.procedureDefinitions[name] = id; // The outer define block id
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._cache.procedureDefinitions[name] = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the procedure definition for a given name.
|
||||
* Get names of parameters for the given procedure.
|
||||
* @param {?string} name Name of procedure to query.
|
||||
* @return {?string} ID of procedure definition.
|
||||
* @return {?Array.<string>} List of param names for a procedure.
|
||||
*/
|
||||
getProcedureParamNames (name) {
|
||||
const cachedNames = this._cache.procedureParamNames[name];
|
||||
if (typeof cachedNames !== 'undefined') {
|
||||
return cachedNames;
|
||||
}
|
||||
|
||||
for (const id in this._blocks) {
|
||||
if (!this._blocks.hasOwnProperty(id)) continue;
|
||||
const block = this._blocks[id];
|
||||
if ((block.opcode === 'procedures_defnoreturn' ||
|
||||
block.opcode === 'procedures_defreturn') &&
|
||||
if (block.opcode === 'procedures_prototype' &&
|
||||
block.mutation.proccode === name) {
|
||||
return JSON.parse(block.mutation.argumentnames);
|
||||
const paramNames = JSON.parse(block.mutation.argumentnames);
|
||||
this._cache.procedureParamNames[name] = paramNames;
|
||||
return paramNames;
|
||||
}
|
||||
}
|
||||
|
||||
this._cache.procedureParamNames[name] = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -249,12 +297,16 @@ class Blocks {
|
||||
if (optRuntime && this._blocks[e.blockId].topLevel) {
|
||||
optRuntime.quietGlow(e.blockId);
|
||||
}
|
||||
this.deleteBlock({
|
||||
id: e.blockId
|
||||
});
|
||||
this.deleteBlock(e.blockId);
|
||||
break;
|
||||
case 'var_create':
|
||||
stage.createVariable(e.varId, e.varName);
|
||||
// New variables being created by the user are all global.
|
||||
// Check if this variable exists on the current target or stage.
|
||||
// If not, create it on the stage.
|
||||
// TODO create global and local variables when UI provides a way.
|
||||
if (!optRuntime.getEditingTarget().lookupVariableById(e.varId)) {
|
||||
stage.createVariable(e.varId, e.varName, e.varType);
|
||||
}
|
||||
break;
|
||||
case 'var_rename':
|
||||
stage.renameVariable(e.varId, e.newName);
|
||||
@@ -267,6 +319,15 @@ class Blocks {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reset all runtime caches.
|
||||
*/
|
||||
resetCache () {
|
||||
this._cache.inputs = {};
|
||||
this._cache.procedureParamNames = {};
|
||||
this._cache.procedureDefinitions = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Block management: create blocks and scripts from a `create` event
|
||||
* @param {!object} block Blockly create event to be processed
|
||||
@@ -285,6 +346,8 @@ class Blocks {
|
||||
if (block.topLevel) {
|
||||
this._addScript(block.id);
|
||||
}
|
||||
|
||||
this.resetCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,13 +360,12 @@ class Blocks {
|
||||
if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return;
|
||||
const block = this._blocks[args.id];
|
||||
if (typeof block === 'undefined') return;
|
||||
|
||||
const wasMonitored = block.isMonitored;
|
||||
switch (args.element) {
|
||||
case 'field':
|
||||
// Update block value
|
||||
if (!block.fields[args.name]) return;
|
||||
if (args.name === 'VARIABLE') {
|
||||
if (args.name === 'VARIABLE' || args.name === 'LIST') {
|
||||
// Get variable name using the id in args.value.
|
||||
const variable = optRuntime.getEditingTarget().lookupVariableById(args.value);
|
||||
if (variable) {
|
||||
@@ -333,6 +395,8 @@ class Blocks {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.resetCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,6 +453,7 @@ class Blocks {
|
||||
}
|
||||
this._blocks[e.id].parent = e.newParent;
|
||||
}
|
||||
this.resetCache();
|
||||
}
|
||||
|
||||
|
||||
@@ -406,38 +471,45 @@ class Blocks {
|
||||
}
|
||||
|
||||
/**
|
||||
* Block management: delete blocks and their associated scripts.
|
||||
* @param {!object} e Blockly delete event to be processed.
|
||||
* Block management: delete blocks and their associated scripts. Does nothing if a block
|
||||
* with the given ID does not exist.
|
||||
* @param {!string} blockId Id of block to delete
|
||||
*/
|
||||
deleteBlock (e) {
|
||||
deleteBlock (blockId) {
|
||||
// @todo In runtime, stop threads running on this script.
|
||||
|
||||
// Get block
|
||||
const block = this._blocks[e.id];
|
||||
const block = this._blocks[blockId];
|
||||
if (!block) {
|
||||
// No block with the given ID exists
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete children
|
||||
if (block.next !== null) {
|
||||
this.deleteBlock({id: block.next});
|
||||
this.deleteBlock(block.next);
|
||||
}
|
||||
|
||||
// Delete inputs (including branches)
|
||||
for (const input in block.inputs) {
|
||||
// If it's null, the block in this input moved away.
|
||||
if (block.inputs[input].block !== null) {
|
||||
this.deleteBlock({id: block.inputs[input].block});
|
||||
this.deleteBlock(block.inputs[input].block);
|
||||
}
|
||||
// Delete obscured shadow blocks.
|
||||
if (block.inputs[input].shadow !== null &&
|
||||
block.inputs[input].shadow !== block.inputs[input].block) {
|
||||
this.deleteBlock({id: block.inputs[input].shadow});
|
||||
this.deleteBlock(block.inputs[input].shadow);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any script starting with this block.
|
||||
this._deleteScript(e.id);
|
||||
this._deleteScript(blockId);
|
||||
|
||||
// Delete block itself.
|
||||
delete this._blocks[e.id];
|
||||
delete this._blocks[blockId];
|
||||
|
||||
this.resetCache();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -492,11 +564,20 @@ class Blocks {
|
||||
for (const field in block.fields) {
|
||||
if (!block.fields.hasOwnProperty(field)) continue;
|
||||
const blockField = block.fields[field];
|
||||
xmlString += `<field name="${blockField.name}"`;
|
||||
const fieldId = blockField.id;
|
||||
if (fieldId) {
|
||||
xmlString += ` id="${fieldId}"`;
|
||||
}
|
||||
const varType = blockField.variableType;
|
||||
if (typeof varType === 'string') {
|
||||
xmlString += ` variabletype="${varType}"`;
|
||||
}
|
||||
let value = blockField.value;
|
||||
if (typeof value === 'string') {
|
||||
value = xmlEscape(blockField.value);
|
||||
}
|
||||
xmlString += `<field name="${blockField.name}">${value}</field>`;
|
||||
xmlString += `>${value}</field>`;
|
||||
}
|
||||
// Add blocks connected to the next connection.
|
||||
if (block.next) {
|
||||
@@ -547,6 +628,17 @@ class Blocks {
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the corresponding internal procedure definition block
|
||||
* @param {!object} defineBlock Outer define block.
|
||||
* @return {!object} internal definition block which has the mutation.
|
||||
*/
|
||||
_getCustomBlockInternal (defineBlock) {
|
||||
if (defineBlock.inputs && defineBlock.inputs.custom_block) {
|
||||
return this._blocks[defineBlock.inputs.custom_block.block];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to add a stack to `this._scripts`.
|
||||
* @param {?string} topBlockId ID of block that starts the script.
|
||||
|
||||
+105
-96
@@ -1,14 +1,94 @@
|
||||
const BlockUtility = require('./block-utility');
|
||||
const log = require('../util/log');
|
||||
const Thread = require('./thread');
|
||||
const {Map} = require('immutable');
|
||||
|
||||
/**
|
||||
* Single BlockUtility instance reused by execute for every pritimive ran.
|
||||
* @const
|
||||
*/
|
||||
const blockUtility = new BlockUtility();
|
||||
|
||||
/**
|
||||
* Profiler frame name for block functions.
|
||||
* @const {string}
|
||||
*/
|
||||
const blockFunctionProfilerFrame = 'blockFunction';
|
||||
|
||||
/**
|
||||
* Profiler frame ID for 'blockFunction'.
|
||||
* @type {number}
|
||||
*/
|
||||
let blockFunctionProfilerId = -1;
|
||||
|
||||
/**
|
||||
* Utility function to determine if a value is a Promise.
|
||||
* @param {*} value Value to check for a Promise.
|
||||
* @return {boolean} True if the value appears to be a Promise.
|
||||
*/
|
||||
const isPromise = function (value) {
|
||||
return value && value.then && typeof value.then === 'function';
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.then === 'function'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle any reported value from the primitive, either directly returned
|
||||
* or after a promise resolves.
|
||||
* @param {*} resolvedValue Value eventually returned from the primitive.
|
||||
* @param {!Sequencer} sequencer Sequencer stepping the thread for the ran
|
||||
* primitive.
|
||||
* @param {!Thread} thread Thread containing the primitive.
|
||||
* @param {!string} currentBlockId Id of the block in its thread for value from
|
||||
* the primitive.
|
||||
* @param {!string} opcode opcode used to identify a block function primitive.
|
||||
* @param {!boolean} isHat Is the current block a hat?
|
||||
*/
|
||||
// @todo move this to callback attached to the thread when we have performance
|
||||
// metrics (dd)
|
||||
const handleReport = function (
|
||||
resolvedValue, sequencer, thread, currentBlockId, opcode, isHat) {
|
||||
thread.pushReportedValue(resolvedValue);
|
||||
if (isHat) {
|
||||
// Hat predicate was evaluated.
|
||||
if (sequencer.runtime.getIsEdgeActivatedHat(opcode)) {
|
||||
// If this is an edge-activated hat, only proceed if the value is
|
||||
// true and used to be false, or the stack was activated explicitly
|
||||
// via stack click
|
||||
if (!thread.stackClick) {
|
||||
const oldEdgeValue = sequencer.runtime.updateEdgeActivatedValue(
|
||||
currentBlockId,
|
||||
resolvedValue
|
||||
);
|
||||
const edgeWasActivated = !oldEdgeValue && resolvedValue;
|
||||
if (!edgeWasActivated) {
|
||||
sequencer.retireThread(thread);
|
||||
}
|
||||
}
|
||||
} else if (!resolvedValue) {
|
||||
// Not an edge-activated hat: retire the thread
|
||||
// if predicate was false.
|
||||
sequencer.retireThread(thread);
|
||||
}
|
||||
} else {
|
||||
// In a non-hat, report the value visually if necessary if
|
||||
// at the top of the thread stack.
|
||||
if (typeof resolvedValue !== 'undefined' && thread.atStackTop()) {
|
||||
if (thread.stackClick) {
|
||||
sequencer.runtime.visualReport(currentBlockId, resolvedValue);
|
||||
}
|
||||
if (thread.updateMonitor) {
|
||||
sequencer.runtime.requestUpdateMonitor(Map({
|
||||
id: currentBlockId,
|
||||
value: String(resolvedValue)
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Finished any yields.
|
||||
thread.status = Thread.STATUS_RUNNING;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -61,55 +141,6 @@ const execute = function (sequencer, thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle any reported value from the primitive, either directly returned
|
||||
* or after a promise resolves.
|
||||
* @param {*} resolvedValue Value eventually returned from the primitive.
|
||||
*/
|
||||
// @todo move this to callback attached to the thread when we have performance
|
||||
// metrics (dd)
|
||||
const handleReport = function (resolvedValue) {
|
||||
thread.pushReportedValue(resolvedValue);
|
||||
if (isHat) {
|
||||
// Hat predicate was evaluated.
|
||||
if (runtime.getIsEdgeActivatedHat(opcode)) {
|
||||
// If this is an edge-activated hat, only proceed if
|
||||
// the value is true and used to be false, or the stack was activated
|
||||
// explicitly via stack click
|
||||
if (!thread.stackClick) {
|
||||
const oldEdgeValue = runtime.updateEdgeActivatedValue(
|
||||
currentBlockId,
|
||||
resolvedValue
|
||||
);
|
||||
const edgeWasActivated = !oldEdgeValue && resolvedValue;
|
||||
if (!edgeWasActivated) {
|
||||
sequencer.retireThread(thread);
|
||||
}
|
||||
}
|
||||
} else if (!resolvedValue) {
|
||||
// Not an edge-activated hat: retire the thread
|
||||
// if predicate was false.
|
||||
sequencer.retireThread(thread);
|
||||
}
|
||||
} else {
|
||||
// In a non-hat, report the value visually if necessary if
|
||||
// at the top of the thread stack.
|
||||
if (typeof resolvedValue !== 'undefined' && thread.atStackTop()) {
|
||||
if (thread.stackClick) {
|
||||
runtime.visualReport(currentBlockId, resolvedValue);
|
||||
}
|
||||
if (thread.updateMonitor) {
|
||||
runtime.requestUpdateMonitor(Map({
|
||||
id: currentBlockId,
|
||||
value: String(resolvedValue)
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Finished any yields.
|
||||
thread.status = Thread.STATUS_RUNNING;
|
||||
}
|
||||
};
|
||||
|
||||
// Hats and single-field shadows are implemented slightly differently
|
||||
// from regular blocks.
|
||||
// For hats: if they have an associated block function,
|
||||
@@ -124,7 +155,7 @@ const execute = function (sequencer, thread) {
|
||||
const keys = Object.keys(fields);
|
||||
if (keys.length === 1 && Object.keys(inputs).length === 0) {
|
||||
// One field and no inputs - treat as arg.
|
||||
handleReport(fields[keys[0]].value);
|
||||
handleReport(fields[keys[0]].value, sequencer, thread, currentBlockId, opcode, isHat);
|
||||
} else {
|
||||
log.warn(`Could not get implementation for opcode: ${opcode}`);
|
||||
}
|
||||
@@ -138,7 +169,7 @@ const execute = function (sequencer, thread) {
|
||||
// Add all fields on this block to the argValues.
|
||||
for (const fieldName in fields) {
|
||||
if (!fields.hasOwnProperty(fieldName)) continue;
|
||||
if (fieldName === 'VARIABLE') {
|
||||
if (fieldName === 'VARIABLE' || fieldName === 'LIST') {
|
||||
argValues[fieldName] = fields[fieldName].id;
|
||||
} else {
|
||||
argValues[fieldName] = fields[fieldName].value;
|
||||
@@ -148,6 +179,8 @@ const execute = function (sequencer, thread) {
|
||||
// Recursively evaluate input blocks.
|
||||
for (const inputName in inputs) {
|
||||
if (!inputs.hasOwnProperty(inputName)) continue;
|
||||
// Do not evaluate the internal custom command block within definition
|
||||
if (inputName === 'custom_block') continue;
|
||||
const input = inputs[inputName];
|
||||
const inputBlockId = input.block;
|
||||
// Is there no value for this input waiting in the stack frame?
|
||||
@@ -184,49 +217,25 @@ const execute = function (sequencer, thread) {
|
||||
currentStackFrame.reported = {};
|
||||
|
||||
let primitiveReportedValue = null;
|
||||
primitiveReportedValue = blockFunction(argValues, {
|
||||
stackFrame: currentStackFrame.executionContext,
|
||||
target: target,
|
||||
yield: function () {
|
||||
thread.status = Thread.STATUS_YIELD;
|
||||
},
|
||||
startBranch: function (branchNum, isLoop) {
|
||||
sequencer.stepToBranch(thread, branchNum, isLoop);
|
||||
},
|
||||
stopAll: function () {
|
||||
runtime.stopAll();
|
||||
},
|
||||
stopOtherTargetThreads: function () {
|
||||
runtime.stopForTarget(target, thread);
|
||||
},
|
||||
stopThisScript: function () {
|
||||
thread.stopThisScript();
|
||||
},
|
||||
startProcedure: function (procedureCode) {
|
||||
sequencer.stepToProcedure(thread, procedureCode);
|
||||
},
|
||||
getProcedureParamNames: function (procedureCode) {
|
||||
return blockContainer.getProcedureParamNames(procedureCode);
|
||||
},
|
||||
pushParam: function (paramName, paramValue) {
|
||||
thread.pushParam(paramName, paramValue);
|
||||
},
|
||||
getParam: function (paramName) {
|
||||
return thread.getParam(paramName);
|
||||
},
|
||||
startHats: function (requestedHat, optMatchFields, optTarget) {
|
||||
return (
|
||||
runtime.startHats(requestedHat, optMatchFields, optTarget)
|
||||
);
|
||||
},
|
||||
ioQuery: function (device, func, args) {
|
||||
// Find the I/O device and execute the query/function call.
|
||||
if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) {
|
||||
const devObject = runtime.ioDevices[device];
|
||||
return devObject[func].apply(devObject, args);
|
||||
}
|
||||
blockUtility.sequencer = sequencer;
|
||||
blockUtility.thread = thread;
|
||||
if (runtime.profiler !== null) {
|
||||
if (blockFunctionProfilerId === -1) {
|
||||
blockFunctionProfilerId = runtime.profiler.idByName(blockFunctionProfilerFrame);
|
||||
}
|
||||
});
|
||||
// The method commented below has its code inlined underneath to reduce
|
||||
// the bias recorded for the profiler's calls in this time sensitive
|
||||
// execute function.
|
||||
//
|
||||
// runtime.profiler.start(blockFunctionProfilerId, opcode);
|
||||
runtime.profiler.records.push(
|
||||
runtime.profiler.START, blockFunctionProfilerId, opcode, performance.now());
|
||||
}
|
||||
primitiveReportedValue = blockFunction(argValues, blockUtility);
|
||||
if (runtime.profiler !== null) {
|
||||
// runtime.profiler.stop(blockFunctionProfilerId);
|
||||
runtime.profiler.records.push(runtime.profiler.STOP, performance.now());
|
||||
}
|
||||
|
||||
if (typeof primitiveReportedValue === 'undefined') {
|
||||
// No value reported - potentially a command block.
|
||||
@@ -242,7 +251,7 @@ const execute = function (sequencer, thread) {
|
||||
}
|
||||
// Promise handlers
|
||||
primitiveReportedValue.then(resolvedValue => {
|
||||
handleReport(resolvedValue);
|
||||
handleReport(resolvedValue, sequencer, thread, currentBlockId, opcode, isHat);
|
||||
if (typeof resolvedValue === 'undefined') {
|
||||
let stackFrame;
|
||||
let nextBlockId;
|
||||
@@ -275,7 +284,7 @@ const execute = function (sequencer, thread) {
|
||||
thread.popStack();
|
||||
});
|
||||
} else if (thread.status === Thread.STATUS_RUNNING) {
|
||||
handleReport(primitiveReportedValue);
|
||||
handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* Object representing a Scratch list.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {!string} name Name of the list.
|
||||
* @param {Array} contents Contents of the list, as an array.
|
||||
* @constructor
|
||||
*/
|
||||
class List {
|
||||
constructor (name, contents) {
|
||||
this.name = name;
|
||||
this.contents = contents;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = List;
|
||||
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* A way to profile Scratch internal performance. Like what blocks run during a
|
||||
* step? How much time do they take? How much time is spent inbetween blocks?
|
||||
*
|
||||
* Profiler aims for to spend as little time inside its functions while
|
||||
* recording. For this it has a simple internal record structure that records a
|
||||
* series of values for each START and STOP event in a single array. This lets
|
||||
* all the values be pushed in one call for the array. This simplicity allows
|
||||
* the contents of the start() and stop() calls to be inlined in areas that are
|
||||
* called frequently enough to want even greater performance from Profiler so
|
||||
* what is recorded better reflects on the profiled code and not Profiler
|
||||
* itself.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The next id returned for a new profile'd function.
|
||||
* @type {number}
|
||||
*/
|
||||
let nextId = 0;
|
||||
|
||||
/**
|
||||
* The mapping of names to ids.
|
||||
* @const {Object.<string, number>}
|
||||
*/
|
||||
const profilerNames = {};
|
||||
|
||||
/**
|
||||
* The START event identifier in Profiler records.
|
||||
* @const {number}
|
||||
*/
|
||||
const START = 0;
|
||||
|
||||
/**
|
||||
* The STOP event identifier in Profiler records.
|
||||
* @const {number}
|
||||
*/
|
||||
const STOP = 1;
|
||||
|
||||
/**
|
||||
* The number of cells used in the records array by a START event.
|
||||
* @const {number}
|
||||
*/
|
||||
const START_SIZE = 4;
|
||||
|
||||
/**
|
||||
* The number of cells used in the records array by a STOP event.
|
||||
* @const {number}
|
||||
*/
|
||||
const STOP_SIZE = 2;
|
||||
|
||||
/**
|
||||
* Stored reference to Performance instance provided by the Browser.
|
||||
* @const {Performance}
|
||||
*/
|
||||
const performance = typeof window === 'object' && window.performance;
|
||||
|
||||
|
||||
/**
|
||||
* Callback handle called by Profiler for each frame it decodes from its
|
||||
* records.
|
||||
* @callback FrameCallback
|
||||
* @param {ProfilerFrame} frame
|
||||
*/
|
||||
|
||||
/**
|
||||
* A set of information about a frame of execution that was recorded.
|
||||
*/
|
||||
class ProfilerFrame {
|
||||
/**
|
||||
* @param {number} depth Depth of the frame in the recorded stack.
|
||||
*/
|
||||
constructor (depth) {
|
||||
/**
|
||||
* The numeric id of a record symbol like Runtime._step or
|
||||
* blockFunction.
|
||||
* @type {number}
|
||||
*/
|
||||
this.id = -1;
|
||||
|
||||
/**
|
||||
* The amount of time spent inside the recorded frame and any deeper
|
||||
* frames.
|
||||
* @type {number}
|
||||
*/
|
||||
this.totalTime = 0;
|
||||
|
||||
/**
|
||||
* The amount of time spent only inside this record frame. Not
|
||||
* including time in any deeper frames.
|
||||
* @type {number}
|
||||
*/
|
||||
this.selfTime = 0;
|
||||
|
||||
/**
|
||||
* An arbitrary argument for the recorded frame. For example a block
|
||||
* function might record its opcode as an argument.
|
||||
* @type {*}
|
||||
*/
|
||||
this.arg = null;
|
||||
|
||||
/**
|
||||
* The depth of the recorded frame. This can help compare recursive
|
||||
* funtions that are recorded. Each level of recursion with have a
|
||||
* different depth value.
|
||||
* @type {number}
|
||||
*/
|
||||
this.depth = depth;
|
||||
}
|
||||
}
|
||||
|
||||
class Profiler {
|
||||
/**
|
||||
* @param {FrameCallback} onFrame a handle called for each recorded frame.
|
||||
* The passed frame value may not be stored as it'll be updated with later
|
||||
* frame information. Any information that is further stored by the handler
|
||||
* should make copies or reduce the information.
|
||||
*/
|
||||
constructor (onFrame = function () {}) {
|
||||
/**
|
||||
* A series of START and STOP values followed by arguments. After
|
||||
* recording is complete the full set of records is reported back by
|
||||
* stepping through the series to connect the relative START and STOP
|
||||
* information.
|
||||
* @type {Array.<*>}
|
||||
*/
|
||||
this.records = [];
|
||||
|
||||
/**
|
||||
* A cache of ProfilerFrames to reuse when reporting the recorded
|
||||
* frames in records.
|
||||
* @type {Array.<ProfilerFrame>}
|
||||
*/
|
||||
this._stack = [new ProfilerFrame(0)];
|
||||
|
||||
/**
|
||||
* A callback handle called with each decoded frame when reporting back
|
||||
* all the recorded times.
|
||||
* @type {FrameCallback}
|
||||
*/
|
||||
this.onFrame = onFrame;
|
||||
|
||||
/**
|
||||
* A reference to the START record id constant.
|
||||
* @const {number}
|
||||
*/
|
||||
this.START = START;
|
||||
|
||||
/**
|
||||
* A reference to the STOP record id constant.
|
||||
* @const {number}
|
||||
*/
|
||||
this.STOP = STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start recording a frame of time for an id and optional argument.
|
||||
* @param {number} id The id returned by idByName for a name symbol like
|
||||
* Runtime._step.
|
||||
* @param {?*} arg An arbitrary argument value to store with the frame.
|
||||
*/
|
||||
start (id, arg) {
|
||||
this.records.push(START, id, arg, performance.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current frame.
|
||||
*/
|
||||
stop () {
|
||||
this.records.push(STOP, performance.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode records and report all frames to `this.onFrame`.
|
||||
*/
|
||||
reportFrames () {
|
||||
const stack = this._stack;
|
||||
let depth = 1;
|
||||
|
||||
// Step through the records and initialize Frame instances from the
|
||||
// START and STOP events. START and STOP events are separated by events
|
||||
// for deeper frames run by higher frames. Frames are stored on a stack
|
||||
// and reinitialized for each START event. When a stop event is reach
|
||||
// the Frame for the current depth has its final values stored and its
|
||||
// passed to the current onFrame callback. This way Frames are "pushed"
|
||||
// for each START event and "popped" for each STOP and handed to an
|
||||
// outside handle to any desired reduction of the collected data.
|
||||
for (let i = 0; i < this.records.length;) {
|
||||
if (this.records[i] === START) {
|
||||
if (depth >= stack.length) {
|
||||
stack.push(new ProfilerFrame(depth));
|
||||
}
|
||||
|
||||
// Store id, arg, totalTime, and initialize selfTime.
|
||||
const frame = stack[depth++];
|
||||
frame.id = this.records[i + 1];
|
||||
frame.arg = this.records[i + 2];
|
||||
// totalTime is first set as the time recorded by this START
|
||||
// event. Once the STOP event is reached the stored start time
|
||||
// is subtracted from the recorded stop time. The resulting
|
||||
// difference is the actual totalTime, and replaces the start
|
||||
// time in frame.totalTime.
|
||||
//
|
||||
// totalTime is used this way as a convenient member to store a
|
||||
// value between the two events without needing additional
|
||||
// members on the Frame or in a shadow map.
|
||||
frame.totalTime = this.records[i + 3];
|
||||
// selfTime is decremented until we reach the STOP event for
|
||||
// this frame. totalTime will be added to it then to get the
|
||||
// time difference.
|
||||
frame.selfTime = 0;
|
||||
|
||||
i += START_SIZE;
|
||||
} else if (this.records[i] === STOP) {
|
||||
const now = this.records[i + 1];
|
||||
|
||||
const frame = stack[--depth];
|
||||
// totalTime is the difference between the start event time
|
||||
// stored in totalTime and the stop event time pulled from this
|
||||
// record.
|
||||
frame.totalTime = now - frame.totalTime;
|
||||
// selfTime is the difference of this frame's totalTime and the
|
||||
// sum of totalTime of deeper frames.
|
||||
frame.selfTime += frame.totalTime;
|
||||
|
||||
// Remove this frames totalTime from the parent's selfTime.
|
||||
stack[depth - 1].selfTime -= frame.totalTime;
|
||||
|
||||
this.onFrame(frame);
|
||||
|
||||
i += STOP_SIZE;
|
||||
} else {
|
||||
this.records.length = 0;
|
||||
throw new Error('Unable to decode Profiler records.');
|
||||
}
|
||||
}
|
||||
|
||||
this.records.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup or create an id for a frame name.
|
||||
* @param {string} name The name to return an id for.
|
||||
* @return {number} The id for the passed name.
|
||||
*/
|
||||
idByName (name) {
|
||||
return Profiler.idByName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse lookup the name from a given frame id.
|
||||
* @param {number} id The id to search for.
|
||||
* @return {string} The name for the given id.
|
||||
*/
|
||||
nameById (id) {
|
||||
return Profiler.nameById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup or create an id for a frame name.
|
||||
* @static
|
||||
* @param {string} name The name to return an id for.
|
||||
* @return {number} The id for the passed name.
|
||||
*/
|
||||
static idByName (name) {
|
||||
if (typeof profilerNames[name] !== 'number') {
|
||||
profilerNames[name] = nextId++;
|
||||
}
|
||||
return profilerNames[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse lookup the name from a given frame id.
|
||||
* @static
|
||||
* @param {number} id The id to search for.
|
||||
* @return {string} The name for the given id.
|
||||
*/
|
||||
static nameById (id) {
|
||||
for (const name in profilerNames) {
|
||||
if (profilerNames[name] === id) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profiler is only available on platforms with the Performance API.
|
||||
* @return {boolean} Can the Profiler run in this browser?
|
||||
*/
|
||||
static available () {
|
||||
return (
|
||||
typeof window === 'object' &&
|
||||
typeof window.performance !== 'undefined');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A reference to the START record id constant.
|
||||
* @const {number}
|
||||
*/
|
||||
Profiler.START = START;
|
||||
|
||||
/**
|
||||
* A reference to the STOP record id constant.
|
||||
* @const {number}
|
||||
*/
|
||||
Profiler.STOP = STOP;
|
||||
|
||||
module.exports = Profiler;
|
||||
+245
-32
@@ -1,11 +1,13 @@
|
||||
const EventEmitter = require('events');
|
||||
const {OrderedMap} = require('immutable');
|
||||
const escapeHtml = require('escape-html');
|
||||
|
||||
const ArgumentType = require('../extension-support/argument-type');
|
||||
const Blocks = require('./blocks');
|
||||
const BlockType = require('../extension-support/block-type');
|
||||
const Sequencer = require('./sequencer');
|
||||
const Thread = require('./thread');
|
||||
const Profiler = require('./profiler');
|
||||
|
||||
// Virtual I/O devices.
|
||||
const Clock = require('../io/clock');
|
||||
@@ -19,12 +21,10 @@ const defaultBlockPackages = {
|
||||
scratch3_looks: require('../blocks/scratch3_looks'),
|
||||
scratch3_motion: require('../blocks/scratch3_motion'),
|
||||
scratch3_operators: require('../blocks/scratch3_operators'),
|
||||
scratch3_pen: require('../blocks/scratch3_pen'),
|
||||
scratch3_sound: require('../blocks/scratch3_sound'),
|
||||
scratch3_sensing: require('../blocks/scratch3_sensing'),
|
||||
scratch3_data: require('../blocks/scratch3_data'),
|
||||
scratch3_procedures: require('../blocks/scratch3_procedures'),
|
||||
scratch3_wedo2: require('../blocks/scratch3_wedo2')
|
||||
scratch3_procedures: require('../blocks/scratch3_procedures')
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,6 +33,13 @@ const defaultBlockPackages = {
|
||||
*/
|
||||
const ArgumentTypeMap = (() => {
|
||||
const map = {};
|
||||
map[ArgumentType.ANGLE] = {
|
||||
shadowType: 'math_angle',
|
||||
fieldType: 'NUM'
|
||||
};
|
||||
map[ArgumentType.COLOR] = {
|
||||
shadowType: 'colour_picker'
|
||||
};
|
||||
map[ArgumentType.NUMBER] = {
|
||||
shadowType: 'math_number',
|
||||
fieldType: 'NUM'
|
||||
@@ -41,9 +48,8 @@ const ArgumentTypeMap = (() => {
|
||||
shadowType: 'text',
|
||||
fieldType: 'TEXT'
|
||||
};
|
||||
// @TODO: talk to Rachel & co. to figure out what goes here. Make it OK to not have a field. Add `check` support.
|
||||
map[ArgumentType.BOOLEAN] = {
|
||||
shadowType: ''
|
||||
check: 'Boolean'
|
||||
};
|
||||
return map;
|
||||
})();
|
||||
@@ -74,6 +80,24 @@ const ScratchBlocksConstants = {
|
||||
OUTPUT_SHAPE_SQUARE: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* Numeric ID for Runtime._step in Profiler instances.
|
||||
* @type {number}
|
||||
*/
|
||||
let stepProfilerId = -1;
|
||||
|
||||
/**
|
||||
* Numeric ID for Sequencer.stepThreads in Profiler instances.
|
||||
* @type {number}
|
||||
*/
|
||||
let stepThreadsProfilerId = -1;
|
||||
|
||||
/**
|
||||
* Numeric ID for RenderWebGL.draw in Profiler instances.
|
||||
* @type {number}
|
||||
*/
|
||||
let rendererDrawProfilerId = -1;
|
||||
|
||||
/**
|
||||
* Manages targets, scripts, and the sequencer.
|
||||
* @constructor
|
||||
@@ -229,6 +253,13 @@ class Runtime extends EventEmitter {
|
||||
keyboard: new Keyboard(this),
|
||||
mouse: new Mouse(this)
|
||||
};
|
||||
|
||||
/**
|
||||
* A runtime profiler that records timed events for later playback to
|
||||
* diagnose Scratch performance.
|
||||
* @type {Profiler}
|
||||
*/
|
||||
this.profiler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,7 +311,8 @@ class Runtime extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Event name for glowing the green flag
|
||||
* Event name when threads start running.
|
||||
* Used by the UI to indicate running status.
|
||||
* @const {string}
|
||||
*/
|
||||
static get PROJECT_RUN_START () {
|
||||
@@ -288,13 +320,23 @@ class Runtime extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Event name for unglowing the green flag
|
||||
* Event name when threads stop running
|
||||
* Used by the UI to indicate not-running status.
|
||||
* @const {string}
|
||||
*/
|
||||
static get PROJECT_RUN_STOP () {
|
||||
return 'PROJECT_RUN_STOP';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event name for project being stopped or restarted by the user.
|
||||
* Used by blocks that need to reset state.
|
||||
* @const {string}
|
||||
*/
|
||||
static get PROJECT_STOP_ALL () {
|
||||
return 'PROJECT_STOP_ALL';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event name for visual value report.
|
||||
* @const {string}
|
||||
@@ -385,6 +427,17 @@ class Runtime extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an extension-specific menu ID.
|
||||
* @param {string} menuName - the name of the menu.
|
||||
* @param {string} extensionId - the ID of the extension hosting the menu.
|
||||
* @returns {string} - the constructed ID.
|
||||
* @private
|
||||
*/
|
||||
_makeExtensionMenuId (menuName, extensionId) {
|
||||
return `${extensionId}.menu.${escapeHtml(menuName)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the primitives provided by an extension.
|
||||
* @param {ExtensionInfo} extensionInfo - information about the extension (id, blocks, etc.)
|
||||
@@ -394,22 +447,81 @@ class Runtime extends EventEmitter {
|
||||
const categoryInfo = {
|
||||
id: extensionInfo.id,
|
||||
name: extensionInfo.name,
|
||||
iconURI: extensionInfo.iconURI,
|
||||
color1: '#FF6680',
|
||||
color2: '#FF4D6A',
|
||||
color3: '#FF3355',
|
||||
blocks: []
|
||||
blocks: [],
|
||||
menus: []
|
||||
};
|
||||
|
||||
this._blockInfo.push(categoryInfo);
|
||||
|
||||
for (const menuName in extensionInfo.menus) {
|
||||
if (extensionInfo.menus.hasOwnProperty(menuName)) {
|
||||
const menuItems = extensionInfo.menus[menuName];
|
||||
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuItems, categoryInfo);
|
||||
categoryInfo.menus.push(convertedMenu);
|
||||
}
|
||||
}
|
||||
for (const blockInfo of extensionInfo.blocks) {
|
||||
const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo);
|
||||
const opcode = convertedBlock.json.type;
|
||||
categoryInfo.blocks.push(convertedBlock);
|
||||
this._primitives[opcode] = convertedBlock.info.func;
|
||||
if (blockInfo.blockType === BlockType.HAT) {
|
||||
this._hats[opcode] = {edgeActivated: true}; /** @TODO let extension specify this */
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(Runtime.EXTENSION_ADDED, categoryInfo.blocks);
|
||||
this.emit(Runtime.EXTENSION_ADDED, categoryInfo.blocks.concat(categoryInfo.menus));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the scratch-blocks JSON for a menu. Note that scratch-blocks treats menus as a special kind of block.
|
||||
* @param {string} menuName - the name of the menu
|
||||
* @param {array} menuItems - the list of items for this menu
|
||||
* @param {CategoryInfo} categoryInfo - the category for this block
|
||||
* @returns {object} - a JSON-esque object ready for scratch-blocks' consumption
|
||||
* @private
|
||||
*/
|
||||
_buildMenuForScratchBlocks (menuName, menuItems, categoryInfo) {
|
||||
const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id);
|
||||
|
||||
/** @TODO: support dynamic menus when 'menuItems' is a method name string (see extension spec) */
|
||||
if (typeof menuItems === 'string') {
|
||||
throw new Error(`Dynamic extension menus are not yet supported. Menu name: ${menuName}`);
|
||||
}
|
||||
const options = menuItems.map(item => {
|
||||
switch (typeof item) {
|
||||
case 'string':
|
||||
return [item, item];
|
||||
case 'object':
|
||||
return [item.text, item.value];
|
||||
default:
|
||||
throw new Error(`Can't interpret menu item: ${item}`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
json: {
|
||||
message0: '%1',
|
||||
type: menuId,
|
||||
inputsInline: true,
|
||||
output: 'String',
|
||||
colour: categoryInfo.color1,
|
||||
colourSecondary: categoryInfo.color2,
|
||||
colourTertiary: categoryInfo.color3,
|
||||
outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND,
|
||||
args0: [
|
||||
{
|
||||
type: 'field_dropdown',
|
||||
name: menuName,
|
||||
options: options
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,31 +551,67 @@ class Runtime extends EventEmitter {
|
||||
// but each `[ARG]` will need to be replaced with the number in this map instead of `args0.length`.
|
||||
const argsMap = {};
|
||||
|
||||
blockJSON.message0 = blockInfo.text.replace(/\[(.+?)]/g, (match, placeholder) => {
|
||||
blockJSON.message0 = '';
|
||||
|
||||
// If an icon for the extension exists, prepend it to each block
|
||||
if (categoryInfo.iconURI) {
|
||||
blockJSON.message0 = '%1';
|
||||
const iconJSON = {
|
||||
type: 'field_image',
|
||||
src: categoryInfo.iconURI,
|
||||
width: 40,
|
||||
height: 40
|
||||
};
|
||||
blockJSON.args0.push(iconJSON);
|
||||
}
|
||||
|
||||
blockJSON.message0 += blockInfo.text.replace(/\[(.+?)]/g, (match, placeholder) => {
|
||||
// Sanitize the placeholder to ensure valid XML
|
||||
placeholder = placeholder.replace(/[<"&]/, '_');
|
||||
|
||||
blockJSON.args0.push({
|
||||
const argJSON = {
|
||||
type: 'input_value',
|
||||
name: placeholder
|
||||
});
|
||||
|
||||
// scratch-blocks uses 1-based argument indexing
|
||||
const argNum = blockJSON.args0.length;
|
||||
argsMap[placeholder] = argNum;
|
||||
};
|
||||
|
||||
const argInfo = blockInfo.arguments[placeholder] || {};
|
||||
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
||||
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ? '' : argInfo.defaultValue.toString());
|
||||
inputList.push(
|
||||
`<value name="${placeholder}">` +
|
||||
`<shadow type="${argTypeInfo.shadowType}">` +
|
||||
`<field name="${argTypeInfo.fieldType}">${defaultValue}</field>` +
|
||||
'</shadow>' +
|
||||
'</value>'
|
||||
);
|
||||
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ?
|
||||
'' :
|
||||
escapeHtml(argInfo.defaultValue.toString()));
|
||||
|
||||
if (argTypeInfo.check) {
|
||||
argJSON.check = argTypeInfo.check;
|
||||
}
|
||||
|
||||
const shadowType = (argInfo.menu ?
|
||||
this._makeExtensionMenuId(argInfo.menu, categoryInfo.id) :
|
||||
argTypeInfo.shadowType);
|
||||
const fieldType = argInfo.menu || argTypeInfo.fieldType;
|
||||
|
||||
// <value> is the ScratchBlocks name for a block input.
|
||||
inputList.push(`<value name="${placeholder}">`);
|
||||
|
||||
// The <shadow> is a placeholder for a reporter and is visible when there's no reporter in this input.
|
||||
// Boolean inputs don't need to specify a shadow in the XML.
|
||||
if (shadowType) {
|
||||
inputList.push(`<shadow type="${shadowType}">`);
|
||||
|
||||
// <field> is a text field that the user can type into. Some shadows, like the color picker, don't allow
|
||||
// text input and therefore don't need a field element.
|
||||
if (fieldType) {
|
||||
inputList.push(`<field name="${fieldType}">${defaultValue}</field>`);
|
||||
}
|
||||
|
||||
inputList.push('</shadow>');
|
||||
}
|
||||
|
||||
inputList.push('</value>');
|
||||
|
||||
// scratch-blocks uses 1-based argument indexing
|
||||
blockJSON.args0.push(argJSON);
|
||||
const argNum = blockJSON.args0.length;
|
||||
argsMap[placeholder] = argNum;
|
||||
return `%${argNum}`;
|
||||
});
|
||||
|
||||
@@ -522,10 +670,9 @@ class Runtime extends EventEmitter {
|
||||
const xmlParts = [];
|
||||
for (const categoryInfo of this._blockInfo) {
|
||||
const {name, color1, color2} = categoryInfo;
|
||||
const paletteBlocks = categoryInfo.blocks.filter(block => !block.info.hideFromPalette);
|
||||
xmlParts.push(`<category name="${name}" colour="${color1}" secondaryColour="${color2}">`);
|
||||
// @todo only add this label for user-loaded extensions?
|
||||
xmlParts.push(`<label text="${name}" web-class="extensionLabel"/>`);
|
||||
xmlParts.push.apply(xmlParts, categoryInfo.blocks.map(blockInfo => blockInfo.xml));
|
||||
xmlParts.push.apply(xmlParts, paletteBlocks.map(block => block.xml));
|
||||
xmlParts.push('</category>');
|
||||
}
|
||||
return xmlParts.join('\n');
|
||||
@@ -657,6 +804,7 @@ class Runtime extends EventEmitter {
|
||||
* This is used by `startHats` to and is necessary to ensure 2.0-like execution order.
|
||||
* Test project: https://scratch.mit.edu/projects/130183108/
|
||||
* @param {!Thread} thread Thread object to restart.
|
||||
* @return {Thread} The restarted thread.
|
||||
*/
|
||||
_restartThread (thread) {
|
||||
const newThread = new Thread(thread.topBlock);
|
||||
@@ -667,9 +815,10 @@ class Runtime extends EventEmitter {
|
||||
const i = this.threads.indexOf(thread);
|
||||
if (i > -1) {
|
||||
this.threads[i] = newThread;
|
||||
} else {
|
||||
this.threads.push(thread);
|
||||
return newThread;
|
||||
}
|
||||
this.threads.push(thread);
|
||||
return thread;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,7 +827,11 @@ class Runtime extends EventEmitter {
|
||||
* @return {boolean} True if the thread is active/running.
|
||||
*/
|
||||
isActiveThread (thread) {
|
||||
return this.threads.indexOf(thread) > -1;
|
||||
return (
|
||||
(
|
||||
thread.stack.length > 0 &&
|
||||
thread.status !== Thread.STATUS_DONE) &&
|
||||
this.threads.indexOf(thread) > -1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -795,13 +948,14 @@ class Runtime extends EventEmitter {
|
||||
|
||||
// If no fields are present, check inputs (horizontal blocks)
|
||||
if (Object.keys(hatFields).length === 0) {
|
||||
hatFields = {}; // don't overwrite the block's actual fields list
|
||||
const hatInputs = blocks.getInputs(block);
|
||||
for (const input in hatInputs) {
|
||||
if (!hatInputs.hasOwnProperty(input)) continue;
|
||||
const id = hatInputs[input].block;
|
||||
const inpBlock = blocks.getBlock(id);
|
||||
const fields = blocks.getFields(inpBlock);
|
||||
hatFields = Object.assign(fields, hatFields);
|
||||
Object.assign(hatFields, fields);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,7 +978,7 @@ class Runtime extends EventEmitter {
|
||||
if (instance.threads[i].topBlock === topBlockId &&
|
||||
!instance.threads[i].stackClick && // stack click threads and hat threads can coexist
|
||||
instance.threads[i].target === target) {
|
||||
instance._restartThread(instance.threads[i]);
|
||||
newThreads.push(instance._restartThread(instance.threads[i]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -905,6 +1059,9 @@ class Runtime extends EventEmitter {
|
||||
* Stop "everything."
|
||||
*/
|
||||
stopAll () {
|
||||
// Emit stop event to allow blocks to clean up any state.
|
||||
this.emit(Runtime.PROJECT_STOP_ALL);
|
||||
|
||||
// Dispose all clones.
|
||||
const newTargets = [];
|
||||
for (let i = 0; i < this.targets.length; i++) {
|
||||
@@ -930,6 +1087,12 @@ class Runtime extends EventEmitter {
|
||||
* inactive threads after each iteration.
|
||||
*/
|
||||
_step () {
|
||||
if (this.profiler !== null) {
|
||||
if (stepProfilerId === -1) {
|
||||
stepProfilerId = this.profiler.idByName('Runtime._step');
|
||||
}
|
||||
this.profiler.start(stepProfilerId);
|
||||
}
|
||||
// Find all edge-activated hats, and add them to threads to be evaluated.
|
||||
for (const hatType in this._hats) {
|
||||
if (!this._hats.hasOwnProperty(hatType)) continue;
|
||||
@@ -940,7 +1103,16 @@ class Runtime extends EventEmitter {
|
||||
}
|
||||
this.redrawRequested = false;
|
||||
this._pushMonitors();
|
||||
if (this.profiler !== null) {
|
||||
if (stepThreadsProfilerId === -1) {
|
||||
stepThreadsProfilerId = this.profiler.idByName('Sequencer.stepThreads');
|
||||
}
|
||||
this.profiler.start(stepThreadsProfilerId);
|
||||
}
|
||||
const doneThreads = this.sequencer.stepThreads();
|
||||
if (this.profiler !== null) {
|
||||
this.profiler.stop();
|
||||
}
|
||||
this._updateGlows(doneThreads);
|
||||
// Add done threads so that even if a thread finishes within 1 frame, the green
|
||||
// flag will still indicate that a script ran.
|
||||
@@ -949,7 +1121,16 @@ class Runtime extends EventEmitter {
|
||||
this._getMonitorThreadCount([...this.threads, ...doneThreads]));
|
||||
if (this.renderer) {
|
||||
// @todo: Only render when this.redrawRequested or clones rendered.
|
||||
if (this.profiler !== null) {
|
||||
if (rendererDrawProfilerId === -1) {
|
||||
rendererDrawProfilerId = this.profiler.idByName('RenderWebGL.draw');
|
||||
}
|
||||
this.profiler.start(rendererDrawProfilerId);
|
||||
}
|
||||
this.renderer.draw();
|
||||
if (this.profiler !== null) {
|
||||
this.profiler.stop();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._refreshTargets) {
|
||||
@@ -961,6 +1142,11 @@ class Runtime extends EventEmitter {
|
||||
this.emit(Runtime.MONITORS_UPDATE, this._monitorState);
|
||||
this._prevMonitorState = this._monitorState;
|
||||
}
|
||||
|
||||
if (this.profiler !== null) {
|
||||
this.profiler.stop();
|
||||
this.profiler.reportFrames();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1227,6 +1413,15 @@ class Runtime extends EventEmitter {
|
||||
this.emit('targetWasCreated', newTarget, sourceTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report that a clone target is being removed.
|
||||
* @param {Target} target - the target being removed
|
||||
* @fires Runtime#targetWasRemoved
|
||||
*/
|
||||
fireTargetWasRemoved (target) {
|
||||
this.emit('targetWasRemoved', target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a target representing the Scratch stage, if one exists.
|
||||
* @return {?Target} The target, if found.
|
||||
@@ -1279,6 +1474,24 @@ class Runtime extends EventEmitter {
|
||||
this._step();
|
||||
}, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn on profiling.
|
||||
* @param {Profiler/FrameCallback} onFrame A callback handle passed a
|
||||
* profiling frame when the profiler reports its collected data.
|
||||
*/
|
||||
enableProfiling (onFrame) {
|
||||
if (Profiler.available()) {
|
||||
this.profiler = new Profiler(onFrame);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn off profiling.
|
||||
*/
|
||||
disableProfiling () {
|
||||
this.profiler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+101
-10
@@ -2,6 +2,42 @@ const Timer = require('../util/timer');
|
||||
const Thread = require('./thread');
|
||||
const execute = require('./execute.js');
|
||||
|
||||
/**
|
||||
* Profiler frame name for stepping a single thread.
|
||||
* @const {string}
|
||||
*/
|
||||
const stepThreadProfilerFrame = 'Sequencer.stepThread';
|
||||
|
||||
/**
|
||||
* Profiler frame name for the inner loop of stepThreads.
|
||||
* @const {string}
|
||||
*/
|
||||
const stepThreadsInnerProfilerFrame = 'Sequencer.stepThreads#inner';
|
||||
|
||||
/**
|
||||
* Profiler frame name for execute.
|
||||
* @const {string}
|
||||
*/
|
||||
const executeProfilerFrame = 'execute';
|
||||
|
||||
/**
|
||||
* Profiler frame ID for stepThreadProfilerFrame.
|
||||
* @type {number}
|
||||
*/
|
||||
let stepThreadProfilerId = -1;
|
||||
|
||||
/**
|
||||
* Profiler frame ID for stepThreadsInnerProfilerFrame.
|
||||
* @type {number}
|
||||
*/
|
||||
let stepThreadsInnerProfilerId = -1;
|
||||
|
||||
/**
|
||||
* Profiler frame ID for executeProfilerFrame.
|
||||
* @type {number}
|
||||
*/
|
||||
let executeProfilerId = -1;
|
||||
|
||||
class Sequencer {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
@@ -38,7 +74,7 @@ class Sequencer {
|
||||
let numActiveThreads = Infinity;
|
||||
// Whether `stepThreads` has run through a full single tick.
|
||||
let ranFirstTick = false;
|
||||
const doneThreads = [];
|
||||
const doneThreads = this.runtime.threads.map(() => null);
|
||||
// Conditions for continuing to stepping threads:
|
||||
// 1. We must have threads in the list, and some must be active.
|
||||
// 2. Time elapsed must be less than WORK_TIME.
|
||||
@@ -47,6 +83,13 @@ class Sequencer {
|
||||
numActiveThreads > 0 &&
|
||||
this.timer.timeElapsed() < WORK_TIME &&
|
||||
(this.runtime.turboMode || !this.runtime.redrawRequested)) {
|
||||
if (this.runtime.profiler !== null) {
|
||||
if (stepThreadsInnerProfilerId === -1) {
|
||||
stepThreadsInnerProfilerId = this.runtime.profiler.idByName(stepThreadsInnerProfilerFrame);
|
||||
}
|
||||
this.runtime.profiler.start(stepThreadsInnerProfilerId);
|
||||
}
|
||||
|
||||
numActiveThreads = 0;
|
||||
// Attempt to run each thread one time.
|
||||
for (let i = 0; i < this.runtime.threads.length; i++) {
|
||||
@@ -54,11 +97,13 @@ class Sequencer {
|
||||
if (activeThread.stack.length === 0 ||
|
||||
activeThread.status === Thread.STATUS_DONE) {
|
||||
// Finished with this thread.
|
||||
if (doneThreads.indexOf(activeThread) < 0) {
|
||||
doneThreads.push(activeThread);
|
||||
}
|
||||
doneThreads[i] = activeThread;
|
||||
continue;
|
||||
}
|
||||
// A thread was removed, added or this thread was restarted.
|
||||
if (doneThreads[i] !== null) {
|
||||
doneThreads[i] = null;
|
||||
}
|
||||
if (activeThread.status === Thread.STATUS_YIELD_TICK &&
|
||||
!ranFirstTick) {
|
||||
// Clear single-tick yield from the last call of `stepThreads`.
|
||||
@@ -67,7 +112,16 @@ class Sequencer {
|
||||
if (activeThread.status === Thread.STATUS_RUNNING ||
|
||||
activeThread.status === Thread.STATUS_YIELD) {
|
||||
// Normal-mode thread: step.
|
||||
if (this.runtime.profiler !== null) {
|
||||
if (stepThreadProfilerId === -1) {
|
||||
stepThreadProfilerId = this.runtime.profiler.idByName(stepThreadProfilerFrame);
|
||||
}
|
||||
this.runtime.profiler.start(stepThreadProfilerId);
|
||||
}
|
||||
this.stepThread(activeThread);
|
||||
if (this.runtime.profiler !== null) {
|
||||
this.runtime.profiler.stop();
|
||||
}
|
||||
activeThread.warpTimer = null;
|
||||
if (activeThread.isKilled) {
|
||||
i--; // if the thread is removed from the list (killed), do not increase index
|
||||
@@ -80,14 +134,33 @@ class Sequencer {
|
||||
// We successfully ticked once. Prevents running STATUS_YIELD_TICK
|
||||
// threads on the next tick.
|
||||
ranFirstTick = true;
|
||||
|
||||
if (this.runtime.profiler !== null) {
|
||||
this.runtime.profiler.stop();
|
||||
}
|
||||
}
|
||||
// Filter inactive threads from `this.runtime.threads`.
|
||||
this.runtime.threads = this.runtime.threads.filter(thread => {
|
||||
if (doneThreads.indexOf(thread) > -1) {
|
||||
return false;
|
||||
numActiveThreads = 0;
|
||||
for (let i = 0; i < this.runtime.threads.length; i++) {
|
||||
const thread = this.runtime.threads[i];
|
||||
if (doneThreads[i] === null) {
|
||||
this.runtime.threads[numActiveThreads] = thread;
|
||||
numActiveThreads++;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
this.runtime.threads.length = numActiveThreads;
|
||||
|
||||
// Filter undefined and null values from `doneThreads`.
|
||||
let numDoneThreads = 0;
|
||||
for (let i = 0; i < doneThreads.length; i++) {
|
||||
const maybeThread = doneThreads[i];
|
||||
if (maybeThread !== null) {
|
||||
doneThreads[numDoneThreads] = maybeThread;
|
||||
numDoneThreads++;
|
||||
}
|
||||
}
|
||||
doneThreads.length = numDoneThreads;
|
||||
|
||||
return doneThreads;
|
||||
}
|
||||
|
||||
@@ -112,7 +185,23 @@ class Sequencer {
|
||||
// Execute the current block.
|
||||
// Save the current block ID to notice if we did control flow.
|
||||
currentBlockId = thread.peekStack();
|
||||
if (this.runtime.profiler !== null) {
|
||||
if (executeProfilerId === -1) {
|
||||
executeProfilerId = this.runtime.profiler.idByName(executeProfilerFrame);
|
||||
}
|
||||
// The method commented below has its code inlined underneath to
|
||||
// reduce the bias recorded for the profiler's calls in this
|
||||
// time sensitive stepThread method.
|
||||
//
|
||||
// this.runtime.profiler.start(executeProfilerId, null);
|
||||
this.runtime.profiler.records.push(
|
||||
this.runtime.profiler.START, executeProfilerId, null, performance.now());
|
||||
}
|
||||
execute(this, thread);
|
||||
if (this.runtime.profiler !== null) {
|
||||
// this.runtime.profiler.stop();
|
||||
this.runtime.profiler.records.push(this.runtime.profiler.STOP, performance.now());
|
||||
}
|
||||
thread.blockGlowInFrame = currentBlockId;
|
||||
// If the thread has yielded or is waiting, yield to other threads.
|
||||
if (thread.status === Thread.STATUS_YIELD) {
|
||||
@@ -225,7 +314,9 @@ class Sequencer {
|
||||
// Look for warp-mode flag on definition, and set the thread
|
||||
// to warp-mode if needed.
|
||||
const definitionBlock = thread.target.blocks.getBlock(definition);
|
||||
const doWarp = definitionBlock.mutation.warp;
|
||||
const innerBlock = thread.target.blocks.getBlock(
|
||||
definitionBlock.inputs.custom_block.block);
|
||||
const doWarp = innerBlock.mutation.warp;
|
||||
if (doWarp) {
|
||||
thread.peekStackFrame().warpMode = true;
|
||||
} else if (isRecursive) {
|
||||
|
||||
+12
-20
@@ -2,7 +2,6 @@ const EventEmitter = require('events');
|
||||
|
||||
const Blocks = require('./blocks');
|
||||
const Variable = require('../engine/variable');
|
||||
const List = require('../engine/list');
|
||||
const uid = require('../util/uid');
|
||||
const {Map} = require('immutable');
|
||||
|
||||
@@ -88,7 +87,7 @@ class Target extends EventEmitter {
|
||||
const variable = this.lookupVariableById(id);
|
||||
if (variable) return variable;
|
||||
// No variable with this name exists - create it locally.
|
||||
const newVariable = new Variable(id, name, 0, false);
|
||||
const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false);
|
||||
this.variables[id] = newVariable;
|
||||
return newVariable;
|
||||
}
|
||||
@@ -117,24 +116,16 @@ class Target extends EventEmitter {
|
||||
/**
|
||||
* Look up a list object for this target, and create it if one doesn't exist.
|
||||
* Search begins for local lists; then look for globals.
|
||||
* @param {!string} id Id of the list.
|
||||
* @param {!string} name Name of the list.
|
||||
* @return {!List} List object.
|
||||
*/
|
||||
lookupOrCreateList (name) {
|
||||
// If we have a local copy, return it.
|
||||
if (this.lists.hasOwnProperty(name)) {
|
||||
return this.lists[name];
|
||||
}
|
||||
// If the stage has a global copy, return it.
|
||||
if (this.runtime && !this.isStage) {
|
||||
const stage = this.runtime.getTargetForStage();
|
||||
if (stage.lists.hasOwnProperty(name)) {
|
||||
return stage.lists[name];
|
||||
}
|
||||
}
|
||||
// No list with this name exists - create it locally.
|
||||
const newList = new List(name, []);
|
||||
this.lists[name] = newList;
|
||||
lookupOrCreateList (id, name) {
|
||||
const list = this.lookupVariableById(id);
|
||||
if (list) return list;
|
||||
// No variable with this name exists - create it locally.
|
||||
const newList = new Variable(id, name, Variable.LIST_TYPE, false);
|
||||
this.variables[id] = newList;
|
||||
return newList;
|
||||
}
|
||||
|
||||
@@ -143,11 +134,11 @@ class Target extends EventEmitter {
|
||||
* dictionary of variables.
|
||||
* @param {string} id Id of variable
|
||||
* @param {string} name Name of variable.
|
||||
* @param {string} type Type of variable, '' or 'list'
|
||||
*/
|
||||
createVariable (id, name) {
|
||||
createVariable (id, name, type) {
|
||||
if (!this.variables.hasOwnProperty(id)) {
|
||||
const newVariable = new Variable(id, name, 0,
|
||||
false);
|
||||
const newVariable = new Variable(id, name, type, false);
|
||||
this.variables[id] = newVariable;
|
||||
}
|
||||
}
|
||||
@@ -189,6 +180,7 @@ class Target extends EventEmitter {
|
||||
if (this.variables.hasOwnProperty(id)) {
|
||||
delete this.variables[id];
|
||||
if (this.runtime) {
|
||||
this.runtime.monitorBlocks.deleteBlock(id);
|
||||
this.runtime.requestRemoveMonitor(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ class Thread {
|
||||
let blockID = this.peekStack();
|
||||
while (blockID !== null) {
|
||||
const block = this.target.blocks.getBlock(blockID);
|
||||
if (typeof block !== 'undefined' && block.opcode === 'procedures_callnoreturn') {
|
||||
if (typeof block !== 'undefined' && block.opcode === 'procedures_call') {
|
||||
break;
|
||||
}
|
||||
this.popStack();
|
||||
@@ -271,7 +271,7 @@ class Thread {
|
||||
const sp = this.stack.length - 1;
|
||||
for (let i = sp - 1; i >= 0; i--) {
|
||||
const block = this.target.blocks.getBlock(this.stack[i]);
|
||||
if (block.opcode === 'procedures_callnoreturn' &&
|
||||
if (block.opcode === 'procedures_call' &&
|
||||
block.mutation.proccode === procedureCode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+32
-4
@@ -9,19 +9,47 @@ class Variable {
|
||||
/**
|
||||
* @param {string} id Id of the variable.
|
||||
* @param {string} name Name of the variable.
|
||||
* @param {(string|number)} value Value of the variable.
|
||||
* @param {string} type Type of the variable, one of '' or 'list'
|
||||
* @param {boolean} isCloud Whether the variable is stored in the cloud.
|
||||
* @constructor
|
||||
*/
|
||||
constructor (id, name, value, isCloud) {
|
||||
constructor (id, name, type, isCloud) {
|
||||
this.id = id || uid();
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
this.isCloud = isCloud;
|
||||
switch (this.type) {
|
||||
case Variable.SCALAR_TYPE:
|
||||
this.value = 0;
|
||||
break;
|
||||
case Variable.LIST_TYPE:
|
||||
this.value = [];
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid variable type: ${this.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
toXML () {
|
||||
return `<variable type="" id="${this.id}">${this.name}</variable>`;
|
||||
return `<variable type="${this.type}" id="${this.id}">${this.name}</variable>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representation for scalar variables.
|
||||
* This is currently represented as ''
|
||||
* for compatibility with blockly.
|
||||
* @const {string}
|
||||
*/
|
||||
static get SCALAR_TYPE () {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representation for list variables.
|
||||
* @const {string}
|
||||
*/
|
||||
static get LIST_TYPE () {
|
||||
return 'list';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const ArgumentType = {
|
||||
ANGLE: 'angle',
|
||||
BOOLEAN: 'Boolean',
|
||||
COLOR: 'color',
|
||||
NUMBER: 'number',
|
||||
STRING: 'string',
|
||||
BOOLEAN: 'Boolean'
|
||||
STRING: 'string'
|
||||
};
|
||||
|
||||
module.exports = ArgumentType;
|
||||
@@ -1,9 +1,9 @@
|
||||
const BlockType = {
|
||||
COMMAND: 'command',
|
||||
REPORTER: 'reporter',
|
||||
BOOLEAN: 'Boolean',
|
||||
COMMAND: 'command',
|
||||
CONDITIONAL: 'conditional',
|
||||
HAT: 'hat',
|
||||
CONDITIONAL: 'conditional'
|
||||
REPORTER: 'reporter'
|
||||
};
|
||||
|
||||
module.exports = BlockType;
|
||||
@@ -3,6 +3,18 @@ const log = require('../util/log');
|
||||
|
||||
const BlockType = require('./block-type');
|
||||
|
||||
// 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: change extension spec so that library info, including extension ID, can be collected through static methods
|
||||
const Scratch3PenBlocks = require('../blocks/scratch3_pen');
|
||||
const Scratch3WeDo2Blocks = require('../blocks/scratch3_wedo2');
|
||||
const Scratch3MusicBlocks = require('../extensions/scratch3_music');
|
||||
const builtinExtensions = {
|
||||
pen: Scratch3PenBlocks,
|
||||
wedo2: Scratch3WeDo2Blocks,
|
||||
music: Scratch3MusicBlocks
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} ArgumentInfo - Information about an extension block argument
|
||||
* @property {ArgumentType} type - the type of value this argument can take
|
||||
@@ -20,6 +32,7 @@ const BlockType = require('./block-type');
|
||||
* @property {object.<string,ArgumentInfo>|undefined} arguments - information about this block's arguments, if any
|
||||
* @property {string|Function|undefined} func - the method for this block on the extension service (default: opcode)
|
||||
* @property {Array.<string>|undefined} filter - the list of targets for which this block should appear (default: all)
|
||||
* @property {Boolean|undefined} hideFromPalette - true if should not be appear in the palette. (default false)
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -39,7 +52,7 @@ const BlockType = require('./block-type');
|
||||
*/
|
||||
|
||||
class ExtensionManager {
|
||||
constructor () {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The ID number to provide to the next extension worker.
|
||||
* @type {int}
|
||||
@@ -60,17 +73,57 @@ class ExtensionManager {
|
||||
*/
|
||||
this.pendingWorkers = [];
|
||||
|
||||
/**
|
||||
* Set of loaded extension URLs/IDs (equivalent for built-in extensions).
|
||||
* @type {Set.<string>}
|
||||
* @private
|
||||
*/
|
||||
this._loadedExtensions = new Set();
|
||||
|
||||
/**
|
||||
* Keep a reference to the runtime so we can construct internal extension objects.
|
||||
* TODO: remove this in favor of extensions accessing the runtime as a service.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
dispatch.setService('extensions', this).catch(e => {
|
||||
log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an extension by URL
|
||||
* @param {string} extensionURL - the URL for the extension to load
|
||||
* Check whether an extension is registered or is in the process of loading. This is intended to control loading or
|
||||
* adding extensions so it may return `true` before the extension is ready to be used. Use the promise returned by
|
||||
* `loadExtensionURL` if you need to wait until the extension is truly ready.
|
||||
* @param {string} extensionID - the ID of the extension.
|
||||
* @returns {boolean} - true if loaded, false otherwise.
|
||||
*/
|
||||
isExtensionLoaded (extensionID) {
|
||||
return this._loadedExtensions.has(extensionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an extension by URL or internal extension ID
|
||||
* @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
|
||||
* @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure
|
||||
*/
|
||||
loadExtensionURL (extensionURL) {
|
||||
if (builtinExtensions.hasOwnProperty(extensionURL)) {
|
||||
/** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
|
||||
if (this.isExtensionLoaded(extensionURL)) {
|
||||
const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`;
|
||||
log.warn(message);
|
||||
return Promise.reject(new Error(message));
|
||||
}
|
||||
|
||||
const extension = builtinExtensions[extensionURL];
|
||||
const extensionInstance = new extension(this.runtime);
|
||||
return this._registerInternalExtension(extensionInstance).then(() => {
|
||||
this._loadedExtensions.add(extensionURL);
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// If we `require` this at the global level it breaks non-webpack targets, including tests
|
||||
const ExtensionWorker = require('worker-loader?name=extension-worker.js!./extension-worker');
|
||||
@@ -87,12 +140,21 @@ class ExtensionManager {
|
||||
return [id, workerInfo.extensionURL];
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect extension metadata from the specified service and begin the extension registration process.
|
||||
* @param {string} serviceName - the name of the service hosting the extension.
|
||||
*/
|
||||
registerExtensionService (serviceName) {
|
||||
dispatch.call(serviceName, 'getInfo').then(info => {
|
||||
this._registerExtensionInfo(serviceName, info);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by an extension worker to indicate that the worker has finished initialization.
|
||||
* @param {int} id - the worker ID.
|
||||
* @param {*?} e - the error encountered during initialization, if any.
|
||||
*/
|
||||
onWorkerInit (id, e) {
|
||||
const workerInfo = this.pendingWorkers[id];
|
||||
delete this.pendingWorkers[id];
|
||||
@@ -103,6 +165,25 @@ class ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an internal (non-Worker) extension object
|
||||
* @param {object} extensionObject - the extension object to register
|
||||
* @returns {Promise} resolved once the extension is fully registered or rejected on failure
|
||||
*/
|
||||
_registerInternalExtension (extensionObject) {
|
||||
const extensionInfo = extensionObject.getInfo();
|
||||
const fakeWorkerId = this.nextExtensionWorker++;
|
||||
const serviceName = `extension.${fakeWorkerId}.${extensionInfo.id}`;
|
||||
return dispatch.setService(serviceName, extensionObject)
|
||||
.then(() => dispatch.call('extensions', 'registerExtensionService', serviceName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize extension info then register its primitives with the VM.
|
||||
* @param {string} serviceName - the name of the service hosting the extension
|
||||
* @param {ExtensionInfo} extensionInfo - the extension's metadata
|
||||
* @private
|
||||
*/
|
||||
_registerExtensionInfo (serviceName, extensionInfo) {
|
||||
extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo);
|
||||
dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => {
|
||||
@@ -139,7 +220,7 @@ class ExtensionManager {
|
||||
result.push(this._prepareBlockInfo(serviceName, blockInfo));
|
||||
} catch (e) {
|
||||
// TODO: more meaningful error reporting
|
||||
log.error(`Skipping malformed block: ${JSON.stringify(e)}`);
|
||||
log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
@@ -162,7 +243,18 @@ class ExtensionManager {
|
||||
}, blockInfo);
|
||||
blockInfo.opcode = this._sanitizeID(blockInfo.opcode);
|
||||
blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
|
||||
blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func);
|
||||
blockInfo.text = blockInfo.text || blockInfo.opcode;
|
||||
|
||||
/**
|
||||
* This is only here because the VM performs poorly when blocks return promises.
|
||||
* @TODO make it possible for the VM to resolve a promise and continue during the same frame.
|
||||
*/
|
||||
if (dispatch._isRemoteService(serviceName)) {
|
||||
blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func);
|
||||
} else {
|
||||
const serviceObject = dispatch.services[serviceName];
|
||||
blockInfo.func = serviceObject[blockInfo.func].bind(serviceObject);
|
||||
}
|
||||
return blockInfo;
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,629 @@
|
||||
const ArgumentType = require('../../extension-support/argument-type');
|
||||
const BlockType = require('../../extension-support/block-type');
|
||||
const Clone = require('../../util/clone');
|
||||
const Cast = require('../../util/cast');
|
||||
const MathUtil = require('../../util/math-util');
|
||||
const Timer = require('../../util/timer');
|
||||
|
||||
/**
|
||||
* Class for the music-related blocks in Scratch 3.0
|
||||
* @param {Runtime} runtime - the runtime instantiating this block package.
|
||||
* @constructor
|
||||
*/
|
||||
class Scratch3MusicBlocks {
|
||||
constructor (runtime) {
|
||||
/**
|
||||
* The runtime instantiating this block package.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
/**
|
||||
* The current tempo in beats per minute. The tempo is a global property of the project,
|
||||
* not a property of each sprite, so it is not stored in the MusicState object.
|
||||
* @type {number}
|
||||
*/
|
||||
this.tempo = 60;
|
||||
|
||||
/**
|
||||
* The number of drum sounds currently being played simultaneously.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this._drumConcurrencyCounter = 0;
|
||||
|
||||
/**
|
||||
* An array of audio buffers, one for each drum sound.
|
||||
* @type {Array}
|
||||
* @private
|
||||
*/
|
||||
this._drumBuffers = [];
|
||||
|
||||
this._loadAllDrumSounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and decode the full set of drum sounds, and store the audio buffers
|
||||
* in the drum buffers array.
|
||||
* @TODO: Also load the instrument sounds here (rename this fn)
|
||||
*/
|
||||
_loadAllDrumSounds () {
|
||||
const loadingPromises = [];
|
||||
this.DRUM_INFO.forEach((drumInfo, index) => {
|
||||
const promise = this._loadSound(drumInfo.fileName, index, this._drumBuffers);
|
||||
loadingPromises.push(promise);
|
||||
});
|
||||
Promise.all(loadingPromises).then(() => {
|
||||
// @TODO: Update the extension status indicator.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and decode a sound, and store the buffer in an array.
|
||||
* @param {string} fileName - the audio file name.
|
||||
* @param {number} index - the index at which to store the audio buffer.
|
||||
* @param {array} bufferArray - the array of buffers in which to store it.
|
||||
* @return {Promise} - a promise which will resolve once the sound has loaded.
|
||||
*/
|
||||
_loadSound (fileName, index, bufferArray) {
|
||||
if (!this.runtime.storage) return;
|
||||
if (!this.runtime.audioEngine) return;
|
||||
return this.runtime.storage.load(this.runtime.storage.AssetType.Sound, fileName, 'mp3')
|
||||
.then(soundAsset =>
|
||||
this.runtime.audioEngine.audioContext.decodeAudioData(soundAsset.data.buffer)
|
||||
)
|
||||
.then(buffer => {
|
||||
bufferArray[index] = buffer;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create data for a menu in scratch-blocks format, consisting of an array of objects with text and
|
||||
* value properties. The text is a translated string, and the value is one-indexed.
|
||||
* @param {object[]} info - An array of info objects each having a name property.
|
||||
* @return {array} - An array of objects with text and value properties.
|
||||
* @private
|
||||
*/
|
||||
_buildMenu (info) {
|
||||
return info.map((entry, index) => {
|
||||
const obj = {};
|
||||
obj.text = entry.name;
|
||||
obj.value = index + 1;
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of translatable drum names and corresponding audio file names.
|
||||
* @type {array}
|
||||
*/
|
||||
get DRUM_INFO () {
|
||||
return [
|
||||
{
|
||||
name: '(1) Snare Drum',
|
||||
fileName: '1-snare'
|
||||
},
|
||||
{
|
||||
name: '(2) Bass Drum',
|
||||
fileName: '2-bass-drum'
|
||||
},
|
||||
{
|
||||
name: '(3) Side Stick',
|
||||
fileName: '3-side-stick'
|
||||
},
|
||||
{
|
||||
name: '(4) Crash Cymbal',
|
||||
fileName: '4-crash-cymbal'
|
||||
},
|
||||
{
|
||||
name: '(5) Open Hi-Hat',
|
||||
fileName: '5-open-hi-hat'
|
||||
},
|
||||
{
|
||||
name: '(6) Closed Hi-Hat',
|
||||
fileName: '6-closed-hi-hat'
|
||||
},
|
||||
{
|
||||
name: '(7) Tambourine',
|
||||
fileName: '7-tambourine'
|
||||
},
|
||||
{
|
||||
name: '(8) Hand Clap',
|
||||
fileName: '8-hand-clap'
|
||||
},
|
||||
{
|
||||
name: '(9) Claves',
|
||||
fileName: '9-claves'
|
||||
},
|
||||
{
|
||||
name: '(10) Wood Block',
|
||||
fileName: '10-wood-block'
|
||||
},
|
||||
{
|
||||
name: '(11) Cowbell',
|
||||
fileName: '11-cowbell'
|
||||
},
|
||||
{
|
||||
name: '(12) Triangle',
|
||||
fileName: '12-triangle'
|
||||
},
|
||||
{
|
||||
name: '(13) Bongo',
|
||||
fileName: '13-bongo'
|
||||
},
|
||||
{
|
||||
name: '(14) Conga',
|
||||
fileName: '14-conga'
|
||||
},
|
||||
{
|
||||
name: '(15) Cabasa',
|
||||
fileName: '15-cabasa'
|
||||
},
|
||||
{
|
||||
name: '(16) Guiro',
|
||||
fileName: '16-guiro'
|
||||
},
|
||||
{
|
||||
name: '(17) Vibraslap',
|
||||
fileName: '17-vibraslap'
|
||||
},
|
||||
{
|
||||
name: '(18) Cuica',
|
||||
fileName: '18-cuica'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of translatable instrument names and corresponding audio file names.
|
||||
* @type {array}
|
||||
*/
|
||||
get INSTRUMENT_INFO () {
|
||||
return [
|
||||
{
|
||||
name: '(1) Piano',
|
||||
fileName: '1-piano'
|
||||
},
|
||||
{
|
||||
name: '(2) Electric Piano',
|
||||
fileName: '2-electric-piano'
|
||||
},
|
||||
{
|
||||
name: '(3) Organ',
|
||||
fileName: '3-organ'
|
||||
},
|
||||
{
|
||||
name: '(4) Guitar',
|
||||
fileName: '4-guitar'
|
||||
},
|
||||
{
|
||||
name: '(5) Electric Guitar',
|
||||
fileName: '5-electric-guitar'
|
||||
},
|
||||
{
|
||||
name: '(6) Bass',
|
||||
fileName: '6-bass'
|
||||
},
|
||||
{
|
||||
name: '(7) Pizzicato',
|
||||
fileName: '7-pizzicato'
|
||||
},
|
||||
{
|
||||
name: '(8) Cello',
|
||||
fileName: '8-cello'
|
||||
},
|
||||
{
|
||||
name: '(9) Trombone',
|
||||
fileName: '9-trombone'
|
||||
},
|
||||
{
|
||||
name: '(10) Clarinet',
|
||||
fileName: '10-clarinet'
|
||||
},
|
||||
{
|
||||
name: '(11) Saxophone',
|
||||
fileName: '11-saxophone'
|
||||
},
|
||||
{
|
||||
name: '(12) Flute',
|
||||
fileName: '12-flute'
|
||||
},
|
||||
{
|
||||
name: '(13) Wooden Flute',
|
||||
fileName: '13-wooden-flute'
|
||||
},
|
||||
{
|
||||
name: '(14) Bassoon',
|
||||
fileName: '14-bassoon'
|
||||
},
|
||||
{
|
||||
name: '(15) Choir',
|
||||
fileName: '15-choir'
|
||||
},
|
||||
{
|
||||
name: '(16) Vibraphone',
|
||||
fileName: '16-vibraphone'
|
||||
},
|
||||
{
|
||||
name: '(17) Music Box',
|
||||
fileName: '17-music-box'
|
||||
},
|
||||
{
|
||||
name: '(18) Steel Drum',
|
||||
fileName: '18-steel-drum'
|
||||
},
|
||||
{
|
||||
name: '(19) Marimba',
|
||||
fileName: '19-marimba'
|
||||
},
|
||||
{
|
||||
name: '(20) Synth Lead',
|
||||
fileName: '20-synth-lead'
|
||||
},
|
||||
{
|
||||
name: '(21) Synth Pad',
|
||||
fileName: '21-synth-pad'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The key to load & store a target's music-related state.
|
||||
* @type {string}
|
||||
*/
|
||||
static get STATE_KEY () {
|
||||
return 'Scratch.music';
|
||||
}
|
||||
|
||||
/**
|
||||
* The default music-related state, to be used when a target has no existing music state.
|
||||
* @type {MusicState}
|
||||
*/
|
||||
static get DEFAULT_MUSIC_STATE () {
|
||||
return {
|
||||
currentInstrument: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum and maximum MIDI note numbers, for clamping the input to play note.
|
||||
* @type {{min: number, max: number}}
|
||||
*/
|
||||
static get MIDI_NOTE_RANGE () {
|
||||
return {min: 36, max: 96}; // C2 to C7
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum and maximum beat values, for clamping the duration of play note, play drum and rest.
|
||||
* 100 beats at the default tempo of 60bpm is 100 seconds.
|
||||
* @type {{min: number, max: number}}
|
||||
*/
|
||||
static get BEAT_RANGE () {
|
||||
return {min: 0, max: 100};
|
||||
}
|
||||
|
||||
/** The minimum and maximum tempo values, in bpm.
|
||||
* @type {{min: number, max: number}}
|
||||
*/
|
||||
static get TEMPO_RANGE () {
|
||||
return {min: 20, max: 500};
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum number of sounds to allow to play simultaneously.
|
||||
* @type {number}
|
||||
*/
|
||||
static get CONCURRENCY_LIMIT () {
|
||||
return 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Target} target - collect music state for this target.
|
||||
* @returns {MusicState} the mutable music state associated with that target. This will be created if necessary.
|
||||
* @private
|
||||
*/
|
||||
_getMusicState (target) {
|
||||
let musicState = target.getCustomState(Scratch3MusicBlocks.STATE_KEY);
|
||||
if (!musicState) {
|
||||
musicState = Clone.simple(Scratch3MusicBlocks.DEFAULT_MUSIC_STATE);
|
||||
target.setCustomState(Scratch3MusicBlocks.STATE_KEY, musicState);
|
||||
}
|
||||
return musicState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object} metadata for this extension and its blocks.
|
||||
*/
|
||||
getInfo () {
|
||||
return {
|
||||
id: 'music',
|
||||
name: 'Music',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'playDrumForBeats',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'play drum [DRUM] for [BEATS] beats',
|
||||
arguments: {
|
||||
DRUM: {
|
||||
type: ArgumentType.NUMBER,
|
||||
menu: 'drums',
|
||||
defaultValue: 1
|
||||
},
|
||||
BEATS: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 0.25
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'restForBeats',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'rest for [BEATS] beats',
|
||||
arguments: {
|
||||
BEATS: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 0.25
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'playNoteForBeats',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'play note [NOTE] for [BEATS] beats',
|
||||
arguments: {
|
||||
NOTE: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 60
|
||||
},
|
||||
BEATS: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 0.25
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'setInstrument',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set instrument to [INSTRUMENT]',
|
||||
arguments: {
|
||||
INSTRUMENT: {
|
||||
type: ArgumentType.NUMBER,
|
||||
menu: 'instruments',
|
||||
defaultValue: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'setTempo',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'set tempo to [TEMPO]',
|
||||
arguments: {
|
||||
TEMPO: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 60
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'changeTempo',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'change tempo by [TEMPO]',
|
||||
arguments: {
|
||||
TEMPO: {
|
||||
type: ArgumentType.NUMBER,
|
||||
defaultValue: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'getTempo',
|
||||
text: 'tempo',
|
||||
blockType: BlockType.REPORTER
|
||||
}
|
||||
],
|
||||
menus: {
|
||||
drums: this._buildMenu(this.DRUM_INFO),
|
||||
instruments: this._buildMenu(this.INSTRUMENT_INFO)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a drum sound for some number of beats.
|
||||
* @param {object} args - the block arguments.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
* @property {int} DRUM - the number of the drum to play.
|
||||
* @property {number} BEATS - the duration in beats of the drum sound.
|
||||
*/
|
||||
playDrumForBeats (args, util) {
|
||||
if (this._stackTimerNeedsInit(util)) {
|
||||
let drum = Cast.toNumber(args.DRUM);
|
||||
drum = Math.round(drum);
|
||||
drum -= 1; // drums are one-indexed
|
||||
drum = MathUtil.wrapClamp(drum, 0, this.DRUM_INFO.length - 1);
|
||||
let beats = Cast.toNumber(args.BEATS);
|
||||
beats = this._clampBeats(beats);
|
||||
this._playDrumNum(util, drum);
|
||||
this._startStackTimer(util, this._beatsToSec(beats));
|
||||
} else {
|
||||
this._checkStackTimer(util);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a drum sound using its 0-indexed number.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
* @param {number} drumNum - the number of the drum to play.
|
||||
* @private
|
||||
*/
|
||||
_playDrumNum (util, drumNum) {
|
||||
if (util.target.audioPlayer === null) return;
|
||||
// If we're playing too many sounds, do not play the drum sound.
|
||||
if (this._drumConcurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
|
||||
return;
|
||||
}
|
||||
const outputNode = util.target.audioPlayer.getInputNode();
|
||||
const bufferSource = this.runtime.audioEngine.audioContext.createBufferSource();
|
||||
bufferSource.buffer = this._drumBuffers[drumNum];
|
||||
bufferSource.connect(outputNode);
|
||||
bufferSource.start();
|
||||
this._drumConcurrencyCounter++;
|
||||
bufferSource.onended = () => {
|
||||
this._drumConcurrencyCounter--;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rest for some number of beats.
|
||||
* @param {object} args - the block arguments.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
* @property {number} BEATS - the duration in beats of the rest.
|
||||
*/
|
||||
restForBeats (args, util) {
|
||||
if (this._stackTimerNeedsInit(util)) {
|
||||
let beats = Cast.toNumber(args.BEATS);
|
||||
beats = this._clampBeats(beats);
|
||||
this._startStackTimer(util, this._beatsToSec(beats));
|
||||
} else {
|
||||
this._checkStackTimer(util);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a note using the current musical instrument for some number of beats.
|
||||
* @param {object} args - the block arguments.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
* @property {number} NOTE - the pitch of the note to play, interpreted as a MIDI note number.
|
||||
* @property {number} BEATS - the duration in beats of the note.
|
||||
*/
|
||||
playNoteForBeats (args, util) {
|
||||
if (this._stackTimerNeedsInit(util)) {
|
||||
let note = Cast.toNumber(args.NOTE);
|
||||
note = MathUtil.clamp(note,
|
||||
Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max);
|
||||
let beats = Cast.toNumber(args.BEATS);
|
||||
beats = this._clampBeats(beats);
|
||||
const musicState = this._getMusicState(util.target);
|
||||
const inst = musicState.currentInstrument;
|
||||
if (typeof this.runtime.audioEngine !== 'undefined') {
|
||||
this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, 100);
|
||||
}
|
||||
this._startStackTimer(util, this._beatsToSec(beats));
|
||||
} else {
|
||||
this._checkStackTimer(util);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a duration in beats to the allowed min and max duration.
|
||||
* @param {number} beats - a duration in beats.
|
||||
* @return {number} - the clamped duration.
|
||||
* @private
|
||||
*/
|
||||
_clampBeats (beats) {
|
||||
return MathUtil.clamp(beats, Scratch3MusicBlocks.BEAT_RANGE.min, Scratch3MusicBlocks.BEAT_RANGE.max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a number of beats to a number of seconds, using the current tempo.
|
||||
* @param {number} beats - number of beats to convert to secs.
|
||||
* @return {number} seconds - number of seconds `beats` will last.
|
||||
* @private
|
||||
*/
|
||||
_beatsToSec (beats) {
|
||||
return (60 / this.tempo) * beats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the stack timer needs initialization.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
* @return {boolean} - true if the stack timer needs to be initialized.
|
||||
* @private
|
||||
*/
|
||||
_stackTimerNeedsInit (util) {
|
||||
return !util.stackFrame.timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the stack timer and the yield the thread if necessary.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
* @param {number} duration - a duration in seconds to set the timer for.
|
||||
* @private
|
||||
*/
|
||||
_startStackTimer (util, duration) {
|
||||
util.stackFrame.timer = new Timer();
|
||||
util.stackFrame.timer.start();
|
||||
util.stackFrame.duration = duration;
|
||||
util.yield();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the stack timer, and if its time is not up yet, yield the thread.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
* @private
|
||||
*/
|
||||
_checkStackTimer (util) {
|
||||
const timeElapsed = util.stackFrame.timer.timeElapsed();
|
||||
if (timeElapsed < util.stackFrame.duration * 1000) {
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an instrument for playing notes.
|
||||
* @param {object} args - the block arguments.
|
||||
* @param {object} util - utility object provided by the runtime.
|
||||
* @property {int} INSTRUMENT - the number of the instrument to select.
|
||||
* @return {Promise} - a promise which will resolve once the instrument has loaded.
|
||||
*/
|
||||
setInstrument (args, util) {
|
||||
const musicState = this._getMusicState(util.target);
|
||||
let instNum = Cast.toNumber(args.INSTRUMENT);
|
||||
instNum -= 1; // instruments are one-indexed
|
||||
if (typeof this.runtime.audioEngine === 'undefined') return;
|
||||
instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments - 1);
|
||||
musicState.currentInstrument = instNum;
|
||||
return this.runtime.audioEngine.instrumentPlayer.loadInstrument(musicState.currentInstrument);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current tempo to a new value.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} TEMPO - the tempo, in beats per minute.
|
||||
*/
|
||||
setTempo (args) {
|
||||
const tempo = Cast.toNumber(args.TEMPO);
|
||||
this._updateTempo(tempo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current tempo by some amount.
|
||||
* @param {object} args - the block arguments.
|
||||
* @property {number} TEMPO - the amount to change the tempo, in beats per minute.
|
||||
*/
|
||||
changeTempo (args) {
|
||||
const change = Cast.toNumber(args.TEMPO);
|
||||
const tempo = change + this.tempo;
|
||||
this._updateTempo(tempo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current tempo, clamping it to the min and max allowable range.
|
||||
* @param {number} tempo - the tempo to set, in beats per minute.
|
||||
* @private
|
||||
*/
|
||||
_updateTempo (tempo) {
|
||||
tempo = MathUtil.clamp(tempo, Scratch3MusicBlocks.TEMPO_RANGE.min, Scratch3MusicBlocks.TEMPO_RANGE.max);
|
||||
this.tempo = tempo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tempo.
|
||||
* @return {number} - the current tempo, in beats per minute.
|
||||
*/
|
||||
getTempo () {
|
||||
return this.tempo;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scratch3MusicBlocks;
|
||||
@@ -66,7 +66,9 @@
|
||||
|
||||
<!-- FPS counter, Syntax highlighter, Blocks, Renderer -->
|
||||
<script src="./vendor.js"></script>
|
||||
<!-- VM Worker -->
|
||||
<!-- Storage module -->
|
||||
<script src="./scratch-storage.js"></script>
|
||||
<!-- VM -->
|
||||
<script src="./scratch-vm.js"></script>
|
||||
<!-- Playground -->
|
||||
<script src="./playground.js"></script>
|
||||
|
||||
@@ -40,13 +40,79 @@ const getAssetUrl = function (asset) {
|
||||
return assetUrlParts.join('');
|
||||
};
|
||||
|
||||
const addProfilerPanels = function (
|
||||
stats, vm, panelName = 'BLK%', panelFg = '#fff', panelBg = '#111') {
|
||||
|
||||
vm.runtime.enableProfiling();
|
||||
if (vm.runtime.profiler === null) {
|
||||
// Profiler isn't available on the local system.
|
||||
return;
|
||||
}
|
||||
|
||||
const blockPercentPanel = stats.addPanel(new window.Stats.Panel(panelName, panelFg, panelBg));
|
||||
|
||||
// Store the profiler reference for later.
|
||||
const profiler = vm.runtime.profiler;
|
||||
// Set profiler to null to disable profiling until later.
|
||||
vm.runtime.profiler = null;
|
||||
|
||||
const stepThreadsProfilerId = profiler.idByName('Sequencer.stepThreads');
|
||||
const blockFunctionProfilerId = profiler.idByName('blockFunction');
|
||||
|
||||
let blockFunctionTime = 0;
|
||||
const stepValues = [];
|
||||
let lastUpdate = Date.now();
|
||||
|
||||
// Collect time used by `blockFunction` calls in `execute` and add a column
|
||||
// to the stats graph of the average for the last second of recordings.
|
||||
profiler.onFrame = function ({id, totalTime}) {
|
||||
if (id === stepThreadsProfilerId && totalTime > 0) {
|
||||
// This frame wraps Sequencer.stepThreads.
|
||||
|
||||
// Push the most recently summed blockFunctionTime.
|
||||
stepValues.push(blockFunctionTime / totalTime * 100);
|
||||
// Every second, average the pushed values and render that as a new
|
||||
// column in the stats graph.
|
||||
if (Date.now() - lastUpdate > 1000) {
|
||||
lastUpdate = Date.now();
|
||||
const average = stepValues.reduce(
|
||||
(a, b) => a + b,
|
||||
0) / stepValues.length;
|
||||
blockPercentPanel.update(average, 100);
|
||||
stepValues.length = 0;
|
||||
}
|
||||
blockFunctionTime = 0;
|
||||
} else if (id === blockFunctionProfilerId) {
|
||||
// This frame wraps around each blockFunction call.
|
||||
blockFunctionTime += totalTime;
|
||||
}
|
||||
};
|
||||
|
||||
// Set the stats panel to not display by default.
|
||||
blockPercentPanel.dom.style.display = 'none';
|
||||
|
||||
// When the parent of the stats graphs is clicked, check if the
|
||||
// blockPercentPanel is visible. If it is visible, enable profiling by
|
||||
// setting the runtime's profiler to the stored Profiler instance. If it is
|
||||
// not visible, disable profiling by setting the profiler to null.
|
||||
stats.dom.addEventListener('click', () => {
|
||||
if (blockPercentPanel.dom.style.display === 'block') {
|
||||
vm.runtime.profiler = profiler;
|
||||
} else {
|
||||
vm.runtime.profiler = null;
|
||||
}
|
||||
});
|
||||
|
||||
return blockPercentPanel;
|
||||
};
|
||||
|
||||
window.onload = function () {
|
||||
// Lots of global variables to make debugging easier
|
||||
// Instantiate the VM.
|
||||
const vm = new window.VirtualMachine();
|
||||
Scratch.vm = vm;
|
||||
|
||||
const storage = new Scratch.Storage();
|
||||
const storage = new ScratchStorage(); /* global ScratchStorage */
|
||||
const AssetType = storage.AssetType;
|
||||
storage.addWebSource([AssetType.Project], getProjectUrl);
|
||||
storage.addWebSource([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl);
|
||||
@@ -99,6 +165,7 @@ window.onload = function () {
|
||||
const stats = new window.Stats();
|
||||
document.getElementById('tab-renderexplorer').appendChild(stats.dom);
|
||||
stats.dom.style.position = 'relative';
|
||||
addProfilerPanels(stats, vm, 'BLK%', '#fff', '#111');
|
||||
stats.begin();
|
||||
|
||||
// Playground data tabs.
|
||||
|
||||
+99
-43
@@ -13,7 +13,6 @@ const log = require('../util/log');
|
||||
const uid = require('../util/uid');
|
||||
const specMap = require('./sb2_specmap');
|
||||
const Variable = require('../engine/variable');
|
||||
const List = require('../engine/list');
|
||||
|
||||
const {loadCostume} = require('../import/load-costume.js');
|
||||
const {loadSound} = require('../import/load-sound.js');
|
||||
@@ -52,6 +51,18 @@ const parseProcedureArgMap = function (procCode) {
|
||||
return argMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a list of "argument IDs" for procdefs and caller mutations.
|
||||
* IDs just end up being `input0`, `input1`, ... which is good enough.
|
||||
* @param {string} procCode Scratch 2.0 procedure string.
|
||||
* @return {Array.<string>} Array of argument id strings.
|
||||
*/
|
||||
const parseProcedureArgIds = function (procCode) {
|
||||
return parseProcedureArgMap(procCode)
|
||||
.map(arg => arg.inputName)
|
||||
.filter(name => name); // Filter out unnamed inputs which are labels
|
||||
};
|
||||
|
||||
/**
|
||||
* Flatten a block tree into a block list.
|
||||
* Children are temporarily stored on the `block.children` property.
|
||||
@@ -78,15 +89,16 @@ const flatten = function (blocks) {
|
||||
* or a list of blocks in an argument (e.g., move [pick random...]).
|
||||
* @param {Array.<object>} blockList SB2 JSON-format block list.
|
||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
* @return {Array.<object>} Scratch VM-format block list.
|
||||
*/
|
||||
const parseBlockList = function (blockList, getVariableId) {
|
||||
const parseBlockList = function (blockList, getVariableId, extensions) {
|
||||
const resultingList = [];
|
||||
let previousBlock = null; // For setting next.
|
||||
for (let i = 0; i < blockList.length; i++) {
|
||||
const block = blockList[i];
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const parsedBlock = parseBlock(block, getVariableId);
|
||||
const parsedBlock = parseBlock(block, getVariableId, extensions);
|
||||
if (typeof parsedBlock === 'undefined') continue;
|
||||
if (previousBlock) {
|
||||
parsedBlock.parent = previousBlock.id;
|
||||
@@ -104,14 +116,15 @@ const parseBlockList = function (blockList, getVariableId) {
|
||||
* @param {!object} scripts Scripts object from SB2 JSON.
|
||||
* @param {!Blocks} blocks Blocks object to load parsed blocks into.
|
||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
*/
|
||||
const parseScripts = function (scripts, blocks, getVariableId) {
|
||||
const parseScripts = function (scripts, blocks, getVariableId, extensions) {
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
const script = scripts[i];
|
||||
const scriptX = script[0];
|
||||
const scriptY = script[1];
|
||||
const blockList = script[2];
|
||||
const parsedBlockList = parseBlockList(blockList, getVariableId);
|
||||
const parsedBlockList = parseBlockList(blockList, getVariableId, extensions);
|
||||
if (parsedBlockList[0]) {
|
||||
// Adjust script coordinates to account for
|
||||
// larger block size in scratch-blocks.
|
||||
@@ -155,12 +168,14 @@ const generateVariableIdGetter = (function () {
|
||||
|
||||
/**
|
||||
* 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 {boolean} topLevel Whether this is the top-level object (stage).
|
||||
* @return {?Promise} Promise that resolves to the loaded targets when ready.
|
||||
* TODO: parse the "info" section, especially "savedExtensions"
|
||||
* @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 {boolean} topLevel - Whether this is the top-level object (stage).
|
||||
* @return {!Promise.<Array.<Target>>} Promise for the loaded targets when ready, or null for unsupported objects.
|
||||
*/
|
||||
const parseScratchObject = function (object, runtime, topLevel) {
|
||||
const parseScratchObject = function (object, runtime, extensions, topLevel) {
|
||||
if (!object.hasOwnProperty('objName')) {
|
||||
// Watcher/monitor - skip this object until those are implemented in VM.
|
||||
// @todo
|
||||
@@ -219,26 +234,31 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
||||
const newVariable = new Variable(
|
||||
getVariableId(variable.name),
|
||||
variable.name,
|
||||
variable.value,
|
||||
Variable.SCALAR_TYPE,
|
||||
variable.isPersistent
|
||||
);
|
||||
newVariable.value = variable.value;
|
||||
target.variables[newVariable.id] = newVariable;
|
||||
}
|
||||
}
|
||||
|
||||
// If included, parse any and all scripts/blocks on the object.
|
||||
if (object.hasOwnProperty('scripts')) {
|
||||
parseScripts(object.scripts, blocks, getVariableId);
|
||||
parseScripts(object.scripts, blocks, getVariableId, extensions);
|
||||
}
|
||||
|
||||
if (object.hasOwnProperty('lists')) {
|
||||
for (let k = 0; k < object.lists.length; k++) {
|
||||
const list = object.lists[k];
|
||||
// @todo: monitor properties.
|
||||
target.lists[list.listName] = new List(
|
||||
const newVariable = new Variable(
|
||||
getVariableId(list.listName),
|
||||
list.listName,
|
||||
list.contents
|
||||
Variable.LIST_TYPE,
|
||||
false
|
||||
);
|
||||
newVariable.value = list.contents;
|
||||
target.variables[newVariable.id] = newVariable;
|
||||
}
|
||||
}
|
||||
if (object.hasOwnProperty('scratchX')) {
|
||||
@@ -287,7 +307,7 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
||||
const childrenPromises = [];
|
||||
if (object.children) {
|
||||
for (let m = 0; m < object.children.length; m++) {
|
||||
childrenPromises.push(parseScratchObject(object.children[m], runtime, false));
|
||||
childrenPromises.push(parseScratchObject(object.children[m], runtime, extensions, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,31 +332,42 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
||||
* @param {!object} json SB2-format JSON to load.
|
||||
* @param {!Runtime} runtime Runtime object to load all structures into.
|
||||
* @param {boolean=} optForceSprite If set, treat as sprite (Sprite2).
|
||||
* @return {?Promise} Promise that resolves to the loaded targets when ready.
|
||||
* @return {Promise.<ImportedProject>} Promise that resolves to the loaded targets when ready.
|
||||
*/
|
||||
const sb2import = function (json, runtime, optForceSprite) {
|
||||
return parseScratchObject(
|
||||
json,
|
||||
runtime,
|
||||
!optForceSprite
|
||||
);
|
||||
const extensions = {
|
||||
extensionIDs: new Set(),
|
||||
extensionURLs: new Map()
|
||||
};
|
||||
return parseScratchObject(json, runtime, extensions, !optForceSprite)
|
||||
.then(targets => ({
|
||||
targets,
|
||||
extensions
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a single SB2 JSON-formatted block and its children.
|
||||
* @param {!object} sb2block SB2 JSON-formatted block.
|
||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||
* @return {object} Scratch VM format block.
|
||||
* @param {Function} getVariableId function to retrieve a variable's ID based on name
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
* @return {object} Scratch VM format block, or null if unsupported object.
|
||||
*/
|
||||
const parseBlock = function (sb2block, getVariableId) {
|
||||
const parseBlock = function (sb2block, getVariableId, extensions) {
|
||||
// First item in block object is the old opcode (e.g., 'forward:').
|
||||
const oldOpcode = sb2block[0];
|
||||
// Convert the block using the specMap. See sb2specmap.js.
|
||||
if (!oldOpcode || !specMap[oldOpcode]) {
|
||||
log.warn('Couldn\'t find SB2 block: ', oldOpcode);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const blockMetadata = specMap[oldOpcode];
|
||||
// If the block is from an extension, record it.
|
||||
const dotIndex = blockMetadata.opcode.indexOf('.');
|
||||
if (dotIndex >= 0) {
|
||||
const extension = blockMetadata.opcode.substring(0, dotIndex);
|
||||
extensions.extensionIDs.add(extension);
|
||||
}
|
||||
// Block skeleton.
|
||||
const activeBlock = {
|
||||
id: uid(), // Generate a new block unique ID.
|
||||
@@ -373,10 +404,10 @@ const parseBlock = function (sb2block, getVariableId) {
|
||||
let innerBlocks;
|
||||
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
||||
// Block list occupies the input.
|
||||
innerBlocks = parseBlockList(providedArg, getVariableId);
|
||||
innerBlocks = parseBlockList(providedArg, getVariableId, extensions);
|
||||
} else {
|
||||
// Single block occupies the input.
|
||||
innerBlocks = [parseBlock(providedArg, getVariableId)];
|
||||
innerBlocks = [parseBlock(providedArg, getVariableId, extensions)];
|
||||
}
|
||||
let previousBlock = null;
|
||||
for (let j = 0; j < innerBlocks.length; j++) {
|
||||
@@ -459,10 +490,14 @@ const parseBlock = function (sb2block, getVariableId) {
|
||||
value: providedArg
|
||||
};
|
||||
|
||||
if (expectedArg.fieldName === 'VARIABLE') {
|
||||
if (expectedArg.fieldName === 'VARIABLE' || expectedArg.fieldName === 'LIST') {
|
||||
// Add `id` property to variable fields
|
||||
activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg);
|
||||
}
|
||||
const varType = expectedArg.variableType;
|
||||
if (typeof varType === 'string') {
|
||||
activeBlock.fields[expectedArg.fieldName].variableType = varType;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Special cases to generate mutations.
|
||||
@@ -481,30 +516,51 @@ const parseBlock = function (sb2block, getVariableId) {
|
||||
// Mutation for procedure definition:
|
||||
// store all 2.0 proc data.
|
||||
const procData = sb2block.slice(1);
|
||||
activeBlock.mutation = {
|
||||
tagName: 'mutation',
|
||||
proccode: procData[0], // e.g., "abc %n %b %s"
|
||||
argumentnames: JSON.stringify(procData[1]), // e.g. ['arg1', 'arg2']
|
||||
argumentdefaults: JSON.stringify(procData[2]), // e.g., [1, 'abc']
|
||||
warp: procData[3], // Warp mode, e.g., true/false.
|
||||
children: []
|
||||
// Create a new block and input metadata.
|
||||
const inputUid = uid();
|
||||
const inputName = 'custom_block';
|
||||
activeBlock.inputs[inputName] = {
|
||||
name: inputName,
|
||||
block: inputUid,
|
||||
shadow: inputUid
|
||||
};
|
||||
activeBlock.children = [{
|
||||
id: inputUid,
|
||||
opcode: 'procedures_prototype',
|
||||
inputs: {},
|
||||
fields: {},
|
||||
next: null,
|
||||
shadow: true,
|
||||
children: [],
|
||||
mutation: {
|
||||
tagName: 'mutation',
|
||||
proccode: procData[0], // e.g., "abc %n %b %s"
|
||||
argumentnames: JSON.stringify(procData[1]), // e.g. ['arg1', 'arg2']
|
||||
argumentids: JSON.stringify(parseProcedureArgIds(procData[0])),
|
||||
argumentdefaults: JSON.stringify(procData[2]), // e.g., [1, 'abc']
|
||||
warp: procData[3], // Warp mode, e.g., true/false.
|
||||
children: []
|
||||
}
|
||||
}];
|
||||
} else if (oldOpcode === 'call') {
|
||||
// Mutation for procedure call:
|
||||
// string for proc code (e.g., "abc %n %b %s").
|
||||
activeBlock.mutation = {
|
||||
tagName: 'mutation',
|
||||
children: [],
|
||||
proccode: sb2block[1]
|
||||
proccode: sb2block[1],
|
||||
argumentids: JSON.stringify(parseProcedureArgIds(sb2block[1]))
|
||||
};
|
||||
} else if (oldOpcode === 'getParam') {
|
||||
// Mutation for procedure parameter.
|
||||
activeBlock.mutation = {
|
||||
tagName: 'mutation',
|
||||
children: [],
|
||||
paramname: sb2block[1], // Name of parameter.
|
||||
shape: sb2block[2] // Shape - in 2.0, 'r' or 'b'.
|
||||
};
|
||||
// Assign correct opcode based on the block shape.
|
||||
switch (sb2block[2]) {
|
||||
case 'r':
|
||||
activeBlock.opcode = 'argument_reporter_string_number';
|
||||
break;
|
||||
case 'b':
|
||||
activeBlock.opcode = 'argument_reporter_boolean';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return activeBlock;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,26 @@
|
||||
* properties. By hand, I matched the opcode name to the 3.0 opcode.
|
||||
* Finally, I filled in the expected arguments as below.
|
||||
*/
|
||||
|
||||
const Variable = require('../engine/variable');
|
||||
|
||||
/**
|
||||
* @typedef {object} SB2SpecMap_blockInfo
|
||||
* @property {string} opcode - the Scratch 3.0 block opcode. Use 'extensionID.opcode' for extension opcodes.
|
||||
* @property {Array.<SB2SpecMap_argInfo>} argMap - metadata for this block's arguments.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SB2SpecMap_argInfo
|
||||
* @property {string} type - the type of this arg (such as 'input' or 'field')
|
||||
* @property {string} inputOp - the scratch-blocks shadow type for this arg
|
||||
* @property {string} inputName - the name this argument will take when provided to the block implementation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mapping of Scratch 2.0 opcode to Scratch 3.0 block metadata.
|
||||
* @type {object.<SB2SpecMap_blockInfo>}
|
||||
*/
|
||||
const specMap = {
|
||||
'forward:': {
|
||||
opcode: 'motion_movesteps',
|
||||
@@ -400,7 +420,7 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'playDrum': {
|
||||
opcode: 'sound_playdrumforbeats',
|
||||
opcode: 'music.playDrumForBeats',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -415,7 +435,7 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'rest:elapsed:from:': {
|
||||
opcode: 'sound_restforbeats',
|
||||
opcode: 'music.restForBeats',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -425,7 +445,7 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'noteOn:duration:elapsed:from:': {
|
||||
opcode: 'sound_playnoteforbeats',
|
||||
opcode: 'music.playNoteForBeats',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -440,7 +460,7 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'instrument:': {
|
||||
opcode: 'sound_setinstrumentto',
|
||||
opcode: 'music.setInstrument',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -475,7 +495,7 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'changeTempoBy:': {
|
||||
opcode: 'sound_changetempoby',
|
||||
opcode: 'music.changeTempo',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -485,7 +505,7 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'setTempoTo:': {
|
||||
opcode: 'sound_settempotobpm',
|
||||
opcode: 'music.setTempo',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -495,32 +515,32 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'tempo': {
|
||||
opcode: 'sound_tempo',
|
||||
opcode: 'music.getTempo',
|
||||
argMap: [
|
||||
]
|
||||
},
|
||||
'clearPenTrails': {
|
||||
opcode: 'pen_clear',
|
||||
opcode: 'pen.clear',
|
||||
argMap: [
|
||||
]
|
||||
},
|
||||
'stampCostume': {
|
||||
opcode: 'pen_stamp',
|
||||
opcode: 'pen.stamp',
|
||||
argMap: [
|
||||
]
|
||||
},
|
||||
'putPenDown': {
|
||||
opcode: 'pen_pendown',
|
||||
opcode: 'pen.penDown',
|
||||
argMap: [
|
||||
]
|
||||
},
|
||||
'putPenUp': {
|
||||
opcode: 'pen_penup',
|
||||
opcode: 'pen.penUp',
|
||||
argMap: [
|
||||
]
|
||||
},
|
||||
'penColor:': {
|
||||
opcode: 'pen_setpencolortocolor',
|
||||
opcode: 'pen.setPenColorToColor',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -530,27 +550,27 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'changePenHueBy:': {
|
||||
opcode: 'pen_changepencolorby',
|
||||
opcode: 'pen.changePenHueBy',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'COLOR'
|
||||
inputName: 'HUE'
|
||||
}
|
||||
]
|
||||
},
|
||||
'setPenHueTo:': {
|
||||
opcode: 'pen_setpencolortonum',
|
||||
opcode: 'pen.setPenHueToNumber',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'COLOR'
|
||||
inputName: 'HUE'
|
||||
}
|
||||
]
|
||||
},
|
||||
'changePenShadeBy:': {
|
||||
opcode: 'pen_changepenshadeby',
|
||||
opcode: 'pen.changePenShadeBy',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -560,7 +580,7 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'setPenShadeTo:': {
|
||||
opcode: 'pen_setpenshadeto',
|
||||
opcode: 'pen.setPenShadeToNumber',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -570,7 +590,7 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'changePenSizeBy:': {
|
||||
opcode: 'pen_changepensizeby',
|
||||
opcode: 'pen.changePenSizeBy',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -580,7 +600,7 @@ const specMap = {
|
||||
]
|
||||
},
|
||||
'penSize:': {
|
||||
opcode: 'pen_setpensizeto',
|
||||
opcode: 'pen.setPenSizeTo',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -1183,7 +1203,8 @@ const specMap = {
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'VARIABLE'
|
||||
fieldName: 'VARIABLE',
|
||||
variableType: Variable.SCALAR_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1192,7 +1213,8 @@ const specMap = {
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'VARIABLE'
|
||||
fieldName: 'VARIABLE',
|
||||
variableType: Variable.SCALAR_TYPE
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
@@ -1206,7 +1228,8 @@ const specMap = {
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'VARIABLE'
|
||||
fieldName: 'VARIABLE',
|
||||
variableType: Variable.SCALAR_TYPE
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
@@ -1220,7 +1243,8 @@ const specMap = {
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'VARIABLE'
|
||||
fieldName: 'VARIABLE',
|
||||
variableType: Variable.SCALAR_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1229,16 +1253,18 @@ const specMap = {
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'VARIABLE'
|
||||
fieldName: 'VARIABLE',
|
||||
variableType: Variable.SCALAR_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
'contentsOfList:': {
|
||||
opcode: 'data_list',
|
||||
opcode: 'data_listcontents',
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1252,7 +1278,8 @@ const specMap = {
|
||||
},
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1266,7 +1293,8 @@ const specMap = {
|
||||
},
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1285,7 +1313,8 @@ const specMap = {
|
||||
},
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1299,7 +1328,8 @@ const specMap = {
|
||||
},
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
@@ -1318,7 +1348,8 @@ const specMap = {
|
||||
},
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1327,7 +1358,8 @@ const specMap = {
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1336,7 +1368,8 @@ const specMap = {
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
@@ -1350,7 +1383,8 @@ const specMap = {
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1359,21 +1393,204 @@ const specMap = {
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'LIST'
|
||||
fieldName: 'LIST',
|
||||
variableType: Variable.LIST_TYPE
|
||||
}
|
||||
]
|
||||
},
|
||||
'procDef': {
|
||||
opcode: 'procedures_defnoreturn',
|
||||
opcode: 'procedures_definition',
|
||||
argMap: []
|
||||
},
|
||||
'getParam': {
|
||||
opcode: 'procedures_param',
|
||||
argMap: []
|
||||
// Doesn't map to single opcode. Import step assigns final correct opcode.
|
||||
opcode: 'argument_reporter_string_number',
|
||||
argMap: [
|
||||
{
|
||||
type: 'field',
|
||||
fieldName: 'VALUE'
|
||||
}
|
||||
]
|
||||
},
|
||||
'call': {
|
||||
opcode: 'procedures_callnoreturn',
|
||||
opcode: 'procedures_call',
|
||||
argMap: []
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add to the specMap entries for an opcode from a Scratch 2.0 extension. Two entries will be made with the same
|
||||
* metadata; this is done to support projects saved by both older and newer versions of the Scratch 2.0 editor.
|
||||
* @param {string} sb2Extension - the Scratch 2.0 name of the extension
|
||||
* @param {string} sb2Opcode - the Scratch 2.0 opcode
|
||||
* @param {SB2SpecMap_blockInfo} blockInfo - the Scratch 3.0 block info
|
||||
*/
|
||||
const addExtensionOp = function (sb2Extension, sb2Opcode, blockInfo) {
|
||||
/**
|
||||
* This string separates the name of an extension and the name of an opcode in more recent Scratch 2.0 projects.
|
||||
* Earlier projects used '.' as a separator, up until we added the 'LEGO WeDo 2.0' extension...
|
||||
* @type {string}
|
||||
*/
|
||||
const sep = '\u001F'; // Unicode Unit Separator
|
||||
|
||||
// make one entry for projects saved by recent versions of the Scratch 2.0 editor
|
||||
specMap[`${sb2Extension}${sep}${sb2Opcode}`] = blockInfo;
|
||||
|
||||
// make a second for projects saved by older versions of the Scratch 2.0 editor
|
||||
specMap[`${sb2Extension}.${sb2Opcode}`] = blockInfo;
|
||||
};
|
||||
|
||||
const weDo2 = 'LEGO WeDo 2.0';
|
||||
|
||||
addExtensionOp(weDo2, 'motorOnFor', {
|
||||
opcode: 'wedo2.motorOnFor',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.motorID',
|
||||
inputName: 'MOTOR_ID'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'DURATION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'motorOn', {
|
||||
opcode: 'wedo2.motorOn',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.motorID',
|
||||
inputName: 'MOTOR_ID'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'motorOff', {
|
||||
opcode: 'wedo2.motorOff',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.motorID',
|
||||
inputName: 'MOTOR_ID'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'startMotorPower', {
|
||||
opcode: 'wedo2.startMotorPower',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.motorID',
|
||||
inputName: 'MOTOR_ID'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'POWER'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'setMotorDirection', {
|
||||
opcode: 'wedo2.setMotorDirection',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.motorID',
|
||||
inputName: 'MOTOR_ID'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.motorDirection',
|
||||
inputName: 'DIRECTION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'setLED', {
|
||||
opcode: 'wedo2.setLightHue',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'HUE'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'playNote', {
|
||||
opcode: 'wedo2.playNoteFor',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'NOTE'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'DURATION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'whenDistance', {
|
||||
opcode: 'wedo2.whenDistance',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.lessMore',
|
||||
inputName: 'OP'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'REFERENCE'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'whenTilted', {
|
||||
opcode: 'wedo2.whenTilted',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.tiltDirectionAny',
|
||||
inputName: 'DIRECTION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'getDistance', {
|
||||
opcode: 'wedo2.getDistance',
|
||||
argMap: []
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'isTilted', {
|
||||
opcode: 'wedo2.isTilted',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.tiltDirectionAny',
|
||||
inputName: 'DIRECTION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'getTilt', {
|
||||
opcode: 'wedo2.getTiltAngle',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'wedo2.menu.tiltDirection',
|
||||
inputName: 'DIRECTION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = specMap;
|
||||
+41
-22
@@ -8,11 +8,22 @@ const vmPackage = require('../../package.json');
|
||||
const Blocks = require('../engine/blocks');
|
||||
const Sprite = require('../sprites/sprite');
|
||||
const Variable = require('../engine/variable');
|
||||
const List = require('../engine/list');
|
||||
|
||||
const {loadCostume} = require('../import/load-costume.js');
|
||||
const {loadSound} = require('../import/load-sound.js');
|
||||
|
||||
/**
|
||||
* @typedef {object} ImportedProject
|
||||
* @property {Array.<Target>} targets - the imported Scratch 3.0 target objects.
|
||||
* @property {ImportedExtensionsInfo} extensionsInfo - the ID of each extension actually used by this project.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ImportedExtensionsInfo
|
||||
* @property {Set.<string>} extensionIDs - the ID of each extension actually in use by blocks in this project.
|
||||
* @property {Map.<string, string>} extensionURLs - map of ID => URL from project metadata. May not match extensionIDs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serializes the specified VM runtime.
|
||||
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
||||
@@ -41,13 +52,14 @@ const serialize = function (runtime) {
|
||||
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
||||
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
||||
* @param {!Runtime} runtime Runtime object to load all structures into.
|
||||
* @return {?Target} Target created (stage or sprite).
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
* @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
|
||||
*/
|
||||
const parseScratchObject = function (object, runtime) {
|
||||
const parseScratchObject = function (object, runtime, extensions) {
|
||||
if (!object.hasOwnProperty('name')) {
|
||||
// Watcher/monitor - skip this object until those are implemented in VM.
|
||||
// @todo
|
||||
return;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
// Blocks container for this object.
|
||||
const blocks = new Blocks();
|
||||
@@ -61,7 +73,14 @@ const parseScratchObject = function (object, runtime) {
|
||||
}
|
||||
if (object.hasOwnProperty('blocks')) {
|
||||
for (const blockId in object.blocks) {
|
||||
blocks.createBlock(object.blocks[blockId]);
|
||||
const blockJSON = object.blocks[blockId];
|
||||
blocks.createBlock(blockJSON);
|
||||
|
||||
const dotIndex = blockJSON.opcode.indexOf('.');
|
||||
if (dotIndex >= 0) {
|
||||
const extensionId = blockJSON.opcode.substring(0, dotIndex);
|
||||
extensions.extensionIDs.add(extensionId);
|
||||
}
|
||||
}
|
||||
// console.log(blocks);
|
||||
}
|
||||
@@ -105,22 +124,13 @@ const parseScratchObject = function (object, runtime) {
|
||||
const newVariable = new Variable(
|
||||
variable.id,
|
||||
variable.name,
|
||||
variable.value,
|
||||
variable.type,
|
||||
variable.isPersistent
|
||||
);
|
||||
newVariable.value = variable.value;
|
||||
target.variables[newVariable.id] = newVariable;
|
||||
}
|
||||
}
|
||||
if (object.hasOwnProperty('lists')) {
|
||||
for (let k = 0; k < object.lists.length; k++) {
|
||||
const list = object.lists[k];
|
||||
// @todo: monitor properties.
|
||||
target.lists[list.listName] = new List(
|
||||
list.listName,
|
||||
list.contents
|
||||
);
|
||||
}
|
||||
}
|
||||
if (object.hasOwnProperty('x')) {
|
||||
target.x = object.x;
|
||||
}
|
||||
@@ -155,14 +165,23 @@ const parseScratchObject = function (object, runtime) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Deserializes the specified representation of a VM runtime and loads it into
|
||||
* the provided runtime instance.
|
||||
* @param {object} json JSON representation of a VM runtime.
|
||||
* @param {Runtime} runtime Runtime instance
|
||||
* @returns {Promise} Promise that resolves to the list of targets after the project is deserialized
|
||||
* Deserialize the specified representation of a VM runtime and loads it into the provided runtime instance.
|
||||
* TODO: parse extension info (also, design extension info storage...)
|
||||
* @param {object} json - JSON representation of a VM runtime.
|
||||
* @param {Runtime} runtime - Runtime instance
|
||||
* @returns {Promise.<ImportedProject>} Promise that resolves to the list of targets after the project is deserialized
|
||||
*/
|
||||
const deserialize = function (json, runtime) {
|
||||
return Promise.all((json.targets || []).map(target => parseScratchObject(target, runtime)));
|
||||
const extensions = {
|
||||
extensionIDs: new Set(),
|
||||
extensionURLs: new Map()
|
||||
};
|
||||
return Promise.all(
|
||||
(json.targets || []).map(target => parseScratchObject(target, runtime, extensions))
|
||||
).then(targets => ({
|
||||
targets,
|
||||
extensions
|
||||
}));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -71,6 +71,7 @@ class Sprite {
|
||||
* @param {!RenderedTarget} clone - the clone to be removed.
|
||||
*/
|
||||
removeClone (clone) {
|
||||
this.runtime.fireTargetWasRemoved(clone);
|
||||
const cloneIndex = this.clones.indexOf(clone);
|
||||
if (cloneIndex >= 0) {
|
||||
this.clones.splice(cloneIndex, 1);
|
||||
|
||||
+54
-29
@@ -33,7 +33,7 @@ class VirtualMachine extends EventEmitter {
|
||||
/**
|
||||
* The "currently editing"/selected target ID for the VM.
|
||||
* Block events from any Blockly workspace are routed to this target.
|
||||
* @type {!string}
|
||||
* @type {Target}
|
||||
*/
|
||||
this.editingTarget = null;
|
||||
// Runtime emits are passed along as VM emits.
|
||||
@@ -68,7 +68,7 @@ class VirtualMachine extends EventEmitter {
|
||||
this.emit(Runtime.EXTENSION_ADDED, blocksInfo);
|
||||
});
|
||||
|
||||
this.extensionManager = new ExtensionManager();
|
||||
this.extensionManager = new ExtensionManager(this.runtime);
|
||||
|
||||
this.blockListener = this.blockListener.bind(this);
|
||||
this.flyoutBlockListener = this.flyoutBlockListener.bind(this);
|
||||
@@ -228,19 +228,42 @@ class VirtualMachine extends EventEmitter {
|
||||
deserializer = sb2;
|
||||
}
|
||||
|
||||
return deserializer.deserialize(json, this.runtime).then(targets => {
|
||||
this.clear();
|
||||
for (let n = 0; n < targets.length; n++) {
|
||||
if (targets[n] !== null) {
|
||||
this.runtime.targets.push(targets[n]);
|
||||
targets[n].updateAllDrawableProperties();
|
||||
}
|
||||
return deserializer.deserialize(json, this.runtime)
|
||||
.then(({targets, extensions}) =>
|
||||
this.installTargets(targets, extensions, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Install `deserialize` results: zero or more targets after the extensions (if any) used by those targets.
|
||||
* @param {Array.<Target>} targets - the targets to be installed
|
||||
* @param {ImportedExtensionsInfo} extensions - metadata about extensions used by these targets
|
||||
* @param {boolean} wholeProject - set to true if installing a whole project, as opposed to a single sprite.
|
||||
* @returns {Promise} resolved once targets have been installed
|
||||
*/
|
||||
installTargets (targets, extensions, wholeProject) {
|
||||
const extensionPromises = [];
|
||||
extensions.extensionIDs.forEach(extensionID => {
|
||||
if (!this.extensionManager.isExtensionLoaded(extensionID)) {
|
||||
const extensionURL = extensions.extensionURLs.get(extensionID) || extensionID;
|
||||
extensionPromises.push(this.extensionManager.loadExtensionURL(extensionURL));
|
||||
}
|
||||
});
|
||||
|
||||
targets = targets.filter(target => !!target);
|
||||
|
||||
return Promise.all(extensionPromises).then(() => {
|
||||
if (wholeProject) {
|
||||
this.clear();
|
||||
}
|
||||
targets.forEach(target => {
|
||||
this.runtime.targets.push(target);
|
||||
(/** @type RenderedTarget */ target).updateAllDrawableProperties();
|
||||
});
|
||||
// Select the first target for editing, e.g., the first sprite.
|
||||
if (this.runtime.targets.length > 1) {
|
||||
this.editingTarget = this.runtime.targets[1];
|
||||
if (wholeProject && (targets.length > 1)) {
|
||||
this.editingTarget = targets[1];
|
||||
} else {
|
||||
this.editingTarget = this.runtime.targets[0];
|
||||
this.editingTarget = targets[0];
|
||||
}
|
||||
|
||||
// Update the VM user's knowledge of targets and blocks on the workspace.
|
||||
@@ -267,17 +290,9 @@ class VirtualMachine extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select new sprite.
|
||||
return sb2.deserialize(json, this.runtime, true).then(targets => {
|
||||
this.runtime.targets.push(targets[0]);
|
||||
this.editingTarget = targets[0];
|
||||
this.editingTarget.updateAllDrawableProperties();
|
||||
|
||||
// Update the VM user's knowledge of targets and blocks on the workspace.
|
||||
this.emitTargetsUpdate();
|
||||
this.emitWorkspaceUpdate();
|
||||
this.runtime.setEditingTarget(this.editingTarget);
|
||||
});
|
||||
return sb2.deserialize(json, this.runtime, true)
|
||||
.then(({targets, extensions}) =>
|
||||
this.installTargets(targets, extensions, false));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,9 +415,13 @@ class VirtualMachine extends EventEmitter {
|
||||
costume.rotationCenterY = rotationCenterY;
|
||||
this.runtime.renderer.updateSVGSkin(costume.skinId, svg, [rotationCenterX, rotationCenterY]);
|
||||
}
|
||||
// @todo: Also update storage in addition to renderer. Without storage, if you switch
|
||||
// costumes and switch back, you will lose your changes in the paint editor.
|
||||
// @todo: emitTargetsUpdate if we need to update the storage ID on the updated costume.
|
||||
const storage = this.runtime.storage;
|
||||
costume.assetId = storage.builtinHelper.cache(
|
||||
storage.AssetType.ImageVector,
|
||||
storage.DataFormat.SVG,
|
||||
(new TextEncoder()).encode(svg)
|
||||
);
|
||||
this.emitTargetsUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -455,9 +474,9 @@ class VirtualMachine extends EventEmitter {
|
||||
*/
|
||||
deleteSprite (targetId) {
|
||||
const target = this.runtime.getTargetById(targetId);
|
||||
const targetIndexBeforeDelete = this.runtime.targets.map(t => t.id).indexOf(target.id);
|
||||
|
||||
if (target) {
|
||||
const targetIndexBeforeDelete = this.runtime.targets.map(t => t.id).indexOf(target.id);
|
||||
if (!target.isSprite()) {
|
||||
throw new Error('Cannot delete non-sprite targets.');
|
||||
}
|
||||
@@ -473,7 +492,11 @@ class VirtualMachine extends EventEmitter {
|
||||
// Ensure editing target is switched if we are deleting it.
|
||||
if (clone === currentEditingTarget) {
|
||||
const nextTargetIndex = Math.min(this.runtime.targets.length - 1, targetIndexBeforeDelete);
|
||||
this.setEditingTarget(this.runtime.targets[nextTargetIndex].id);
|
||||
if (this.runtime.targets.length > 0){
|
||||
this.setEditingTarget(this.runtime.targets[nextTargetIndex].id);
|
||||
} else {
|
||||
this.editingTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sprite object should be deleted by GC.
|
||||
@@ -486,6 +509,8 @@ class VirtualMachine extends EventEmitter {
|
||||
/**
|
||||
* Duplicate a sprite.
|
||||
* @param {string} targetId ID of a target whose sprite to duplicate.
|
||||
* @returns {Promise} Promise that resolves when duplicated target has
|
||||
* been added to the runtime.
|
||||
*/
|
||||
duplicateSprite (targetId) {
|
||||
const target = this.runtime.getTargetById(targetId);
|
||||
@@ -496,7 +521,7 @@ class VirtualMachine extends EventEmitter {
|
||||
} else if (!target.sprite) {
|
||||
throw new Error('No sprite associated with this target.');
|
||||
}
|
||||
target.duplicate().then(newTarget => {
|
||||
return target.duplicate().then(newTarget => {
|
||||
this.runtime.targets.push(newTarget);
|
||||
this.setEditingTarget(newTarget.id);
|
||||
});
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"targets":[{"id":"poXv!.~0@*~j*F(Om/X5","name":"Stage","isStage":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"currentCostume":10,"costume":{"name":"backdrop2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":7,"assetId":"7da4181ee167de7b3f5d1a91880277ff","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},"costumeCount":11,"visible":true,"rotationStyle":"all around","blocks":{"O7D.%E~TH^ULpAuHM8)@":{"id":"O7D.%E~TH^ULpAuHM8)@","opcode":"event_whenflagclicked","inputs":{},"fields":{},"next":"78+=E[P7b8!mBXMwQF`@","shadow":false,"x":138,"y":356.40000000000003,"topLevel":true,"parent":null},"78+=E[P7b8!mBXMwQF`@":{"id":"78+=E[P7b8!mBXMwQF`@","opcode":"control_forever","inputs":{"SUBSTACK":{"name":"SUBSTACK","block":"nlnk{2XqtVI*Qg,admbW","shadow":null}},"fields":{},"next":null,"shadow":false,"parent":"O7D.%E~TH^ULpAuHM8)@"},"nlnk{2XqtVI*Qg,admbW":{"id":"nlnk{2XqtVI*Qg,admbW","opcode":"looks_changeeffectby","inputs":{"EFFECT":{"name":"EFFECT","block":"*vh;qV87Q}5IP@sW=)wD","shadow":"*vh;qV87Q}5IP@sW=)wD"},"CHANGE":{"name":"CHANGE","block":"woYo[[v=PD(`R;qW{PZ%","shadow":"woYo[[v=PD(`R;qW{PZ%"}},"fields":{},"next":null,"shadow":false,"parent":"78+=E[P7b8!mBXMwQF`@"},"*vh;qV87Q}5IP@sW=)wD":{"id":"*vh;qV87Q}5IP@sW=)wD","opcode":"looks_effectmenu","inputs":{},"fields":{"EFFECT":{"name":"EFFECT","value":"color"}},"next":null,"topLevel":false,"parent":"nlnk{2XqtVI*Qg,admbW","shadow":true},"woYo[[v=PD(`R;qW{PZ%":{"id":"woYo[[v=PD(`R;qW{PZ%","opcode":"math_number","inputs":{},"fields":{"NUM":{"name":"NUM","value":1}},"next":null,"topLevel":false,"parent":"nlnk{2XqtVI*Qg,admbW","shadow":true}},"variables":{"vy":{"name":"vy","value":-3.5165000000000006,"isCloud":false},"g":{"name":"g","value":"-.5","isCloud":false},"vx":{"name":"vx","value":1.4580000000000002,"isCloud":false},"i":{"name":"i","value":3,"isCloud":false},"d1":{"name":"d1","value":0,"isCloud":false},"x":{"name":"x","value":233.9173278872492,"isCloud":false},"acceleration":{"name":"acceleration","value":"-3","isCloud":false}},"lists":{},"costumes":[{"name":"blue sky2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":0,"assetId":"7623e679b2baa2e7d48808614820844f","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},{"name":"woods","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":3,"assetId":"1e0f7a4c932423b13250b5cb44928109","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},{"name":"party","bitmapResolution":1,"rotationCenterX":251,"rotationCenterY":189,"skinId":2,"assetId":"108160d0e44d1c340182e31c9dc0758a","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},{"name":"boardwalk","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":6,"assetId":"de0e54cd11551566f044e7e6bc588b2c","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"blue sky3","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":179,"skinId":1,"assetId":"2024d59c1980e667e8f656134796e2c1","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}},{"name":"underwater1","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":5,"assetId":"f339c6f31b11ea71d0fb8d607cec392e","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"underwater2","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":11,"assetId":"1517c21786d2d0edc2f3037408d850bd","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"stars","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":4,"assetId":"e87fed9c2a968dbeae8c94617e600e8c","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"parking-ramp","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":9,"assetId":"a7832479977c166ca0057f2a99a73305","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"backdrop1","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"skinId":10,"assetId":"f67dc7de38bac6fbb0ab68e46352521d","assetType":{"contentType":"image/png","name":"ImageBitmap","runtimeFormat":"png"}},{"name":"backdrop2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"skinId":7,"assetId":"7da4181ee167de7b3f5d1a91880277ff","assetType":{"contentType":"image/svg+xml","name":"ImageVector","runtimeFormat":"svg"}}],"sounds":[{"name":"pop","format":"","rate":11025,"sampleCount":258,"soundID":-1,"md5":"83a9787d4cb6f3b7632b4ddfebf74367.wav","data":null,"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","assetType":{"contentType":"audio/x-wav","name":"Sound","runtimeFormat":"wav"}}]},{"id":"9!d8G^[6i]k*:l[W%4;l","name":"Earth","isStage":false,"x":-10,"y":10,"size":100,"direction":90,"draggable":false,"currentCostLine truncated
|
||||
{"targets":[{"id":"m#69^p_G_]:NI,ZR#?iy","name":"Stage","isStage":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"currentCostume":10,"costume":{"skinId":10,"name":"backdrop2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"dataFormat":"svg","assetId":"7da4181ee167de7b3f5d1a91880277ff"},"costumeCount":11,"visible":true,"rotationStyle":"all around","blocks":{"O7D.%E~TH^ULpAuHM8)@":{"id":"O7D.%E~TH^ULpAuHM8)@","opcode":"event_whenflagclicked","inputs":{},"fields":{},"next":"78+=E[P7b8!mBXMwQF`@","shadow":false,"x":138,"y":356.40000000000003,"topLevel":true,"parent":null},"78+=E[P7b8!mBXMwQF`@":{"id":"78+=E[P7b8!mBXMwQF`@","opcode":"control_forever","inputs":{"SUBSTACK":{"name":"SUBSTACK","block":"nlnk{2XqtVI*Qg,admbW","shadow":null}},"fields":{},"next":null,"shadow":false,"parent":"O7D.%E~TH^ULpAuHM8)@"},"nlnk{2XqtVI*Qg,admbW":{"id":"nlnk{2XqtVI*Qg,admbW","opcode":"looks_changeeffectby","inputs":{"EFFECT":{"name":"EFFECT","block":"*vh;qV87Q}5IP@sW=)wD","shadow":"*vh;qV87Q}5IP@sW=)wD"},"CHANGE":{"name":"CHANGE","block":"woYo[[v=PD(`R;qW{PZ%","shadow":"woYo[[v=PD(`R;qW{PZ%"}},"fields":{},"next":null,"shadow":false,"parent":"78+=E[P7b8!mBXMwQF`@"},"*vh;qV87Q}5IP@sW=)wD":{"id":"*vh;qV87Q}5IP@sW=)wD","opcode":"looks_effectmenu","inputs":{},"fields":{"EFFECT":{"name":"EFFECT","value":"color"}},"next":null,"topLevel":false,"parent":"nlnk{2XqtVI*Qg,admbW","shadow":true},"woYo[[v=PD(`R;qW{PZ%":{"id":"woYo[[v=PD(`R;qW{PZ%","opcode":"math_number","inputs":{},"fields":{"NUM":{"name":"NUM","value":1}},"next":null,"topLevel":false,"parent":"nlnk{2XqtVI*Qg,admbW","shadow":true}},"variables":{"6C~ysz%{q=IY/@VyR,}u":{"id":"6C~ysz%{q=IY/@VyR,}u","name":"vy","value":-3.5165000000000006,"type":""},"@M%UU5XBzcjofm,PEY-3":{"id":"@M%UU5XBzcjofm,PEY-3","name":"g","value":"-.5","type":""},"rNxv=Ia#{]Jvhmo{G6cf":{"id":"rNxv=Ia#{]Jvhmo{G6cf","name":"vx","value":1.4580000000000002,"type":""},"fc2rL3s1GcD4UyOOCaN(":{"id":"fc2rL3s1GcD4UyOOCaN(","name":"i","value":3, "type":""},"0A~.tga@}|%UW31FAhz5":{"id":"0A~.tga@}|%UW31FAhz5","name":"d1","value":0, "type":""},"^T|b;%@AzqwgHp},y98=":{"id":"^T|b;%@AzqwgHp},y98=","name":"x","value":233.9173278872492,"type":""},"6m`MGC(+0!,Mwj8xE2OB":{"id":"6m`MGC(+0!,Mwj8xE2OB","name":"acceleration","value":"-3","type":""}},"lists":{},"costumes":[{"skinId":9,"name":"blue sky2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"dataFormat":"svg","assetId":"7623e679b2baa2e7d48808614820844f"},{"skinId":13,"name":"woods","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"dataFormat":"svg","assetId":"1e0f7a4c932423b13250b5cb44928109"},{"skinId":5,"name":"party","bitmapResolution":1,"rotationCenterX":251,"rotationCenterY":189,"dataFormat":"svg","assetId":"108160d0e44d1c340182e31c9dc0758a"},{"skinId":3,"name":"boardwalk","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"de0e54cd11551566f044e7e6bc588b2c"},{"skinId":6,"name":"blue sky3","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":179,"dataFormat":"svg","assetId":"2024d59c1980e667e8f656134796e2c1"},{"skinId":14,"name":"underwater1","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"f339c6f31b11ea71d0fb8d607cec392e"},{"skinId":7,"name":"underwater2","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"1517c21786d2d0edc2f3037408d850bd"},{"skinId":8,"name":"stars","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"e87fed9c2a968dbeae8c94617e600e8c"},{"skinId":11,"name":"parking-ramp","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"a7832479977c166ca0057f2a99a73305"},{"skinId":12,"name":"backdrop1","bitmapResolution":2,"rotationCenterX":480,"rotationCenterY":360,"dataFormat":"png","assetId":"f67dc7de38bac6fbb0ab68e46352521d"},{"skinId":10,"name":"backdrop2","bitmapResolution":1,"rotationCenterX":240,"rotationCenterY":180,"dataFormat":"svg","assetId":"7da4181ee167de7b3f5d1a91880277ff"}],"sounds":[{"format":"","rate":11025,"sampleCount":258,"soundID":-1,"name":"pop","md5":"83a9787d4cb6f3b7632b4ddfebf74367.wav","data":null,"dataFormat":"wav","assetId":"83a9787d4cb6f3b7632b4ddfebf74367","soundId":"yGa~fKV|[-9r)^[i+xMU"}]},{"id":"./Q-Mm#v_Ifl9@[}Ye^8","name":"Earth","isStage":false,"x":-10,"y":10,"size":100,"direction":90,"draggable":false,"currentCostume":0,"costume":{"skinId":4,"name":"earth","bitmapResolution":1,"rotationCenterX":72,"rotationCenterY":72,"dataFormat":"svg","assetId":"814197522984a302972998b1a7f92d91"},"costumeCount":1,"visible":true,"rotationStyle":"all around","blocks":{},"variables":{},"lists":{},"costumes":[{"skinId":4,"name":"earth","bitmapResolution":1,"rotationCenterX":72,"rotationCenterY":72,"dataFormat":"svg","assetId":"814197522984a302972998b1a7f92d91"}],"sounds":[{"format":"","rate":11025,"sampleCount":258,"soundID":-1,"name":"pop","md5":"83a9787d4cb6f3Line truncated
|
||||
Vendored
+6
@@ -60,6 +60,12 @@
|
||||
"outerHTML": "<block type='operator_equals' id='l^H_{8[DDyDW?m)HIt@b' x='100' y='362'><value name='OPERAND1'><shadow type='text' id='Ud@4y]bc./]uv~te?brb'><field name='TEXT'></field></shadow></value><value name='OPERAND2'><shadow type='text' id='p8[y..,[K;~G,k7]N;08'><field name='TEXT'></field></shadow></value></block>"
|
||||
}
|
||||
},
|
||||
"createvariablewithentity": {
|
||||
"name": "block",
|
||||
"xml": {
|
||||
"outerHTML": "<block type='data_variable' id='/b?}ZGt/N5FmS2yw5GHZ' x='0' y='0'><field name='VARIABLE' id='k-q!YMpHim*lrSX)v(8t' variabletype=''>this & that</field></block>"
|
||||
}
|
||||
},
|
||||
"createobscuredshadow": {
|
||||
"name": "block",
|
||||
"xml": {
|
||||
|
||||
@@ -16,7 +16,8 @@ test('complex', t => {
|
||||
const threads = JSON.parse(e.threads);
|
||||
t.ok(threads.length === 0);
|
||||
|
||||
const results = vm.runtime.targets[0].lists.results.contents;
|
||||
const resultKey = Object.keys(vm.runtime.targets[0].variables)[0];
|
||||
const results = vm.runtime.targets[0].variables[resultKey].value;
|
||||
t.deepEqual(results, ['3', '2', '1', 'stage']);
|
||||
|
||||
t.end();
|
||||
|
||||
@@ -21,7 +21,7 @@ test('default', t => {
|
||||
// Create runtime instance & load SB2 into it
|
||||
const rt = new runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
sb2.deserialize(json, rt).then(targets => {
|
||||
sb2.deserialize(json, rt).then(({targets}) => {
|
||||
// Test
|
||||
t.type(file, 'string');
|
||||
t.type(json, 'object');
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const test = require('tap').test;
|
||||
const Worker = require('tiny-worker');
|
||||
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
class TestInternalExtension {
|
||||
constructor () {
|
||||
this.status = {};
|
||||
this.status.constructorCalled = true;
|
||||
}
|
||||
|
||||
getInfo () {
|
||||
this.status.getInfoCalled = true;
|
||||
return {
|
||||
id: 'test-internal-extension',
|
||||
name: 'Test Internal Extension',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'go'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
go () {
|
||||
this.status.goCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
test('internal extension', t => {
|
||||
const vm = new VirtualMachine();
|
||||
|
||||
const extension = new TestInternalExtension();
|
||||
t.ok(extension.status.constructorCalled);
|
||||
|
||||
t.notOk(extension.status.getInfoCalled);
|
||||
return vm.extensionManager._registerInternalExtension(extension).then(() => {
|
||||
t.ok(extension.status.getInfoCalled);
|
||||
|
||||
const func = vm.runtime.getOpcodeFunction('test-internal-extension.go');
|
||||
t.type(func, 'function');
|
||||
|
||||
t.notOk(extension.status.goCalled);
|
||||
func();
|
||||
t.ok(extension.status.goCalled);
|
||||
});
|
||||
});
|
||||
+14
-8
@@ -1,8 +1,10 @@
|
||||
const Worker = require('tiny-worker');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
|
||||
const Scratch3PenBlocks = require('../../src/blocks/scratch3_pen');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const extract = require('../fixtures/extract');
|
||||
@@ -10,6 +12,9 @@ const extract = require('../fixtures/extract');
|
||||
const uri = path.resolve(__dirname, '../fixtures/pen.sb2');
|
||||
const project = extract(uri);
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
test('pen', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
@@ -42,14 +47,15 @@ test('pen', t => {
|
||||
vm.clear();
|
||||
vm.setCompatibilityMode(false);
|
||||
vm.setTurboMode(false);
|
||||
vm.loadProject(project).then(() => {
|
||||
vm.greenFlag();
|
||||
vm.loadProject(project)
|
||||
.then(() => {
|
||||
vm.greenFlag();
|
||||
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
// After two seconds, get playground data and stop
|
||||
setTimeout(() => {
|
||||
vm.getPlaygroundData();
|
||||
vm.stopAll();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -103,7 +103,7 @@ test('sb3-roundtrip', t => {
|
||||
return sb3.deserialize(serializedState, runtime2);
|
||||
});
|
||||
|
||||
return serializeAndDeserialize.then(targets => {
|
||||
return serializeAndDeserialize.then(({targets}) => {
|
||||
runtime2.targets = targets;
|
||||
testRuntimeState('copy', runtime2);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
const Worker = require('tiny-worker');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const extract = require('../fixtures/extract');
|
||||
const VirtualMachine = require('../../src/index');
|
||||
const dispatch = require('../../src/dispatch/central-dispatch');
|
||||
|
||||
const uri = path.resolve(__dirname, '../fixtures/sound.sb2');
|
||||
const project = extract(uri);
|
||||
|
||||
// By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead.
|
||||
dispatch.workerClass = Worker;
|
||||
|
||||
test('sound', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachStorage(makeTestStorage());
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
const test = require('tap').test;
|
||||
|
||||
const Blocks = require('../../src/engine/blocks');
|
||||
const BlockUtility = require('../../src/engine/block-utility');
|
||||
const Event = require('../../src/blocks/scratch3_event');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const Target = require('../../src/engine/target');
|
||||
const Thread = require('../../src/engine/thread');
|
||||
|
||||
test('#760 - broadcastAndWait', t => {
|
||||
const broadcastAndWaitBlock = {
|
||||
id: 'broadcastAndWaitBlock',
|
||||
fields: {
|
||||
BROADCAST_OPTION: {
|
||||
id: 'BROADCAST_OPTION',
|
||||
value: 'message'
|
||||
}
|
||||
},
|
||||
inputs: Object,
|
||||
block: 'fakeBlock',
|
||||
opcode: 'event_broadcastandwait',
|
||||
next: null,
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
const receiveMessageBlock = {
|
||||
id: 'receiveMessageBlock',
|
||||
fields: {
|
||||
BROADCAST_OPTION: {
|
||||
id: 'BROADCAST_OPTION',
|
||||
value: 'message'
|
||||
}
|
||||
},
|
||||
inputs: Object,
|
||||
block: 'fakeBlock',
|
||||
opcode: 'event_whenbroadcastreceived',
|
||||
next: null,
|
||||
parent: null,
|
||||
shadow: false,
|
||||
topLevel: true,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
|
||||
const rt = new Runtime();
|
||||
const e = new Event(rt);
|
||||
const b = new Blocks();
|
||||
b.createBlock(broadcastAndWaitBlock);
|
||||
b.createBlock(receiveMessageBlock);
|
||||
const tgt = new Target(rt, b);
|
||||
rt.targets.push(tgt);
|
||||
|
||||
let th = rt._pushThread('broadcastAndWaitBlock', t);
|
||||
const util = new BlockUtility();
|
||||
util.sequencer = rt.sequencer;
|
||||
util.thread = th;
|
||||
|
||||
// creates threads
|
||||
e.broadcastAndWait({BROADCAST_OPTION: 'message'}, util);
|
||||
t.strictEqual(rt.threads.length, 2);
|
||||
t.strictEqual(rt.threads[1].topBlock, 'receiveMessageBlock');
|
||||
// yields when some thread is active
|
||||
t.strictEqual(th.status, Thread.STATUS_YIELD);
|
||||
th.status = Thread.STATUS_RUNNING;
|
||||
e.broadcastAndWait({BROADCAST_OPTION: 'message'}, util);
|
||||
t.strictEqual(th.status, Thread.STATUS_YIELD);
|
||||
// does not yield once all threads are done
|
||||
th.status = Thread.STATUS_RUNNING;
|
||||
rt.threads[1].status = Thread.STATUS_DONE;
|
||||
e.broadcastAndWait({BROADCAST_OPTION: 'message'}, util);
|
||||
t.strictEqual(th.status, Thread.STATUS_RUNNING);
|
||||
|
||||
// restarts done threads that are in runtime threads
|
||||
th = rt._pushThread('broadcastAndWaitBlock', tgt);
|
||||
util.thread = th;
|
||||
e.broadcastAndWait({BROADCAST_OPTION: 'message'}, util);
|
||||
t.strictEqual(rt.threads.length, 3);
|
||||
t.strictEqual(rt.threads[1].status, Thread.STATUS_RUNNING);
|
||||
t.strictEqual(th.status, Thread.STATUS_YIELD);
|
||||
// yields when some restarted thread is active
|
||||
th.status = Thread.STATUS_RUNNING;
|
||||
e.broadcastAndWait({BROADCAST_OPTION: 'message'}, util);
|
||||
t.strictEqual(th.status, Thread.STATUS_YIELD);
|
||||
// does not yield once all threads are done
|
||||
th.status = Thread.STATUS_RUNNING;
|
||||
rt.threads[1].status = Thread.STATUS_DONE;
|
||||
e.broadcastAndWait({BROADCAST_OPTION: 'message'}, util);
|
||||
t.strictEqual(th.status, Thread.STATUS_RUNNING);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
const test = require('tap').test;
|
||||
const Procedures = require('../../src/blocks/scratch3_procedures');
|
||||
|
||||
const blocks = new Procedures(null);
|
||||
|
||||
test('getPrimitives', t => {
|
||||
t.type(blocks.getPrimitives(), 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Originally inspired by https://github.com/LLK/scratch-gui/issues/809
|
||||
test('calling a custom block with no definition does not throw', t => {
|
||||
const args = {
|
||||
mutation: {
|
||||
proccode: 'undefined proc'
|
||||
}
|
||||
};
|
||||
const util = {
|
||||
getProcedureParamNames: () => null,
|
||||
stackFrame: {
|
||||
executed: false
|
||||
}
|
||||
};
|
||||
t.doesNotThrow(() => {
|
||||
blocks.call(args, util);
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
const test = require('tap').test;
|
||||
const Sensing = require('../../src/blocks/scratch3_sensing');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('getPrimitives', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
t.type(s.getPrimitives(), 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('ask and answer with a hidden target', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
const util = {target: {visible: false}};
|
||||
|
||||
const expectedQuestion = 'a question';
|
||||
const expectedAnswer = 'the answer';
|
||||
|
||||
// Test is written out of order because of promises, follow the (#) comments.
|
||||
rt.addListener('QUESTION', question => {
|
||||
// (2) Assert the question is correct, then emit the answer
|
||||
t.strictEqual(question, expectedQuestion);
|
||||
rt.emit('ANSWER', expectedAnswer);
|
||||
});
|
||||
|
||||
// (1) Emit the question.
|
||||
const promise = s.askAndWait({QUESTION: expectedQuestion}, util);
|
||||
|
||||
// (3) Ask block resolves after the answer is emitted.
|
||||
promise.then(() => {
|
||||
t.strictEqual(s.getAnswer(), expectedAnswer);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('ask and answer with a visible target', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
const util = {target: {visible: true}};
|
||||
|
||||
const expectedQuestion = 'a question';
|
||||
const expectedAnswer = 'the answer';
|
||||
|
||||
rt.removeAllListeners('SAY'); // Prevent say blocks from executing
|
||||
|
||||
rt.addListener('SAY', (target, type, question) => {
|
||||
// Should emit SAY with the question
|
||||
t.strictEqual(question, expectedQuestion);
|
||||
});
|
||||
|
||||
rt.addListener('QUESTION', question => {
|
||||
// Question should be blank for a visible target
|
||||
t.strictEqual(question, '');
|
||||
|
||||
// Remove the say listener and add a new one to assert bubble is cleared
|
||||
// by setting say to empty string after answer is received.
|
||||
rt.removeAllListeners('SAY');
|
||||
rt.addListener('SAY', (target, type, text) => {
|
||||
t.strictEqual(text, '');
|
||||
t.end();
|
||||
});
|
||||
rt.emit('ANSWER', expectedAnswer);
|
||||
});
|
||||
|
||||
s.askAndWait({QUESTION: expectedQuestion}, util);
|
||||
});
|
||||
@@ -1,18 +1,8 @@
|
||||
const test = require('tap').test;
|
||||
const Sound = require('../../src/blocks/scratch3_sound');
|
||||
let playedSound;
|
||||
let playedDrum;
|
||||
let playedInstrument;
|
||||
const runtime = {
|
||||
audioEngine: {
|
||||
numDrums: 3,
|
||||
numInstruments: 3,
|
||||
instrumentPlayer: {
|
||||
loadInstrument: instrument => (playedInstrument = instrument)
|
||||
}
|
||||
}
|
||||
};
|
||||
const blocks = new Sound(runtime);
|
||||
|
||||
const blocks = new Sound();
|
||||
const util = {
|
||||
target: {
|
||||
sprite: {
|
||||
@@ -24,8 +14,7 @@ const util = {
|
||||
]
|
||||
},
|
||||
audioPlayer: {
|
||||
playSound: soundId => (playedSound = soundId),
|
||||
playDrumForBeats: drum => (playedDrum = drum)
|
||||
playSound: soundId => (playedSound = soundId)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -82,30 +71,3 @@ test('playSound prioritizes sound name if given a string', t => {
|
||||
t.strictEqual(playedSound, 'fourth soundId');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('playDrum uses 1-indexing and wrap clamps', t => {
|
||||
let args = {DRUM: 1};
|
||||
blocks.playDrumForBeats(args, util);
|
||||
t.strictEqual(playedDrum, 0);
|
||||
|
||||
args = {DRUM: runtime.audioEngine.numDrums + 1};
|
||||
blocks.playDrumForBeats(args, util);
|
||||
t.strictEqual(playedDrum, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setInstrument uses 1-indexing and wrap clamps', t => {
|
||||
// Stub getSoundState
|
||||
blocks._getSoundState = () => ({});
|
||||
|
||||
let args = {INSTRUMENT: 1};
|
||||
blocks.setInstrument(args, util);
|
||||
t.strictEqual(playedInstrument, 0);
|
||||
|
||||
args = {INSTRUMENT: runtime.audioEngine.numInstruments + 1};
|
||||
blocks.setInstrument(args, util);
|
||||
t.strictEqual(playedInstrument, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -155,6 +155,24 @@ test('create with obscured shadow', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create variable with entity in name', t => {
|
||||
const result = adapter(events.createvariablewithentity);
|
||||
|
||||
t.ok(Array.isArray(result));
|
||||
t.equal(result.length, 1);
|
||||
|
||||
t.type(result[0].id, 'string');
|
||||
t.type(result[0].opcode, 'string');
|
||||
t.type(result[0].fields, 'object');
|
||||
t.type(result[0].fields.VARIABLE, 'object');
|
||||
t.type(result[0].fields.VARIABLE.value, 'string');
|
||||
t.equal(result[0].fields.VARIABLE.value, 'this & that');
|
||||
t.type(result[0].inputs, 'object');
|
||||
t.type(result[0].topLevel, 'boolean');
|
||||
t.equal(result[0].topLevel, true);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('create with invalid block xml', t => {
|
||||
// Entirely invalid block XML
|
||||
const result = adapter(events.createinvalid);
|
||||
|
||||
@@ -421,9 +421,7 @@ test('delete', t => {
|
||||
inputs: {},
|
||||
topLevel: true
|
||||
});
|
||||
b.deleteBlock({
|
||||
id: 'foo'
|
||||
});
|
||||
b.deleteBlock('foo');
|
||||
|
||||
t.type(b._blocks.foo, 'undefined');
|
||||
t.equal(b._scripts.indexOf('foo'), -1);
|
||||
@@ -458,9 +456,7 @@ test('delete chain', t => {
|
||||
inputs: {},
|
||||
topLevel: false
|
||||
});
|
||||
b.deleteBlock({
|
||||
id: 'foo'
|
||||
});
|
||||
b.deleteBlock('foo');
|
||||
t.type(b._blocks.foo, 'undefined');
|
||||
t.type(b._blocks.foo2, 'undefined');
|
||||
t.type(b._blocks.foo3, 'undefined');
|
||||
@@ -531,9 +527,7 @@ test('delete inputs', t => {
|
||||
inputs: {},
|
||||
topLevel: false
|
||||
});
|
||||
b.deleteBlock({
|
||||
id: 'foo'
|
||||
});
|
||||
b.deleteBlock('foo');
|
||||
t.type(b._blocks.foo, 'undefined');
|
||||
t.type(b._blocks.foo2, 'undefined');
|
||||
t.type(b._blocks.foo3, 'undefined');
|
||||
|
||||
@@ -36,7 +36,7 @@ const generateBlock = function (id) {
|
||||
block: 'fakeBlock',
|
||||
name: 'fakeName',
|
||||
next: null,
|
||||
opcode: 'procedures_defnoreturn',
|
||||
opcode: 'procedures_definition',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
@@ -55,7 +55,7 @@ const generateBlockInput = function (id, next, inp) {
|
||||
block: 'fakeBlock',
|
||||
name: 'fakeName',
|
||||
next: next,
|
||||
opcode: 'procedures_defnoreturn',
|
||||
opcode: 'procedures_definition',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
@@ -153,9 +153,19 @@ test('stepToProcedure', t => {
|
||||
t.strictEquals(th.peekStack(), expectedBlock);
|
||||
s.stepToProcedure(th, 'faceCode');
|
||||
t.strictEquals(th.peekStack(), expectedBlock);
|
||||
s.stepToProcedure(th, 'faceCode');
|
||||
th.target.blocks.getBlock(th.stack[th.stack.length - 4]).mutation.proccode = 'othercode';
|
||||
|
||||
th.target.blocks.createBlock({
|
||||
id: 'internalId',
|
||||
opcode: 'procedures_prototype',
|
||||
mutation: {
|
||||
proccode: 'othercode'
|
||||
}
|
||||
});
|
||||
expectedBlock = th.stack[th.stack.length - 4];
|
||||
th.target.blocks.getBlock(expectedBlock).inputs.custom_block = {
|
||||
type: 'custom_block',
|
||||
block: 'internalId'
|
||||
};
|
||||
s.stepToProcedure(th, 'othercode');
|
||||
t.strictEquals(th.peekStack(), expectedBlock);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const test = require('tap').test;
|
||||
const Target = require('../../src/engine/target');
|
||||
const Variable = require('../../src/engine/variable');
|
||||
|
||||
test('spec', t => {
|
||||
const target = new Target();
|
||||
@@ -23,13 +24,14 @@ test('spec', t => {
|
||||
// Create Variable tests.
|
||||
test('createVariable', t => {
|
||||
const target = new Target();
|
||||
target.createVariable('foo', 'bar');
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo');
|
||||
t.equal(variable.name, 'bar');
|
||||
t.equal(variable.type, Variable.SCALAR_TYPE);
|
||||
t.equal(variable.value, 0);
|
||||
t.equal(variable.isCloud, false);
|
||||
|
||||
@@ -39,8 +41,8 @@ test('createVariable', t => {
|
||||
// Create Same Variable twice.
|
||||
test('createVariable2', t => {
|
||||
const target = new Target();
|
||||
target.createVariable('foo', 'bar');
|
||||
target.createVariable('foo', 'bar');
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
@@ -48,10 +50,38 @@ test('createVariable2', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Create a list
|
||||
test('createListVariable creates a list', t => {
|
||||
const target = new Target();
|
||||
target.createVariable('foo', 'bar', Variable.LIST_TYPE);
|
||||
|
||||
const variables = target.variables;
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
const variable = variables[Object.keys(variables)[0]];
|
||||
t.equal(variable.id, 'foo');
|
||||
t.equal(variable.name, 'bar');
|
||||
t.equal(variable.type, Variable.LIST_TYPE);
|
||||
t.assert(variable.value instanceof Array, true);
|
||||
t.equal(variable.value.length, 0);
|
||||
t.equal(variable.isCloud, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('createVariable throws when given invalid type', t => {
|
||||
const target = new Target();
|
||||
t.throws(
|
||||
(() => target.createVariable('foo', 'bar', 'baz')),
|
||||
new Error('Invalid variable type: baz')
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Rename Variable tests.
|
||||
test('renameVariable', t => {
|
||||
const target = new Target();
|
||||
target.createVariable('foo', 'bar');
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
target.renameVariable('foo', 'bar2');
|
||||
|
||||
const variables = target.variables;
|
||||
@@ -80,7 +110,7 @@ test('renameVariable2', t => {
|
||||
// Expect no change.
|
||||
test('renameVariable3', t => {
|
||||
const target = new Target();
|
||||
target.createVariable('foo1', 'foo');
|
||||
target.createVariable('foo1', 'foo', Variable.SCALAR_TYPE);
|
||||
target.renameVariable('foo', 'bar2');
|
||||
|
||||
const variables = target.variables;
|
||||
@@ -95,7 +125,7 @@ test('renameVariable3', t => {
|
||||
// Delete Variable tests.
|
||||
test('deleteVariable', t => {
|
||||
const target = new Target();
|
||||
target.createVariable('foo', 'bar');
|
||||
target.createVariable('foo', 'bar', Variable.SCALAR_TYPE);
|
||||
target.deleteVariable('foo');
|
||||
|
||||
const variables = target.variables;
|
||||
@@ -114,3 +144,32 @@ test('deleteVariable2', t => {
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('lookupOrCreateList creates a list if var with given id does not exist', t => {
|
||||
const target = new Target();
|
||||
const variables = target.variables;
|
||||
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
const listVar = target.lookupOrCreateList('foo', 'bar');
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
t.equal(listVar.id, 'foo');
|
||||
t.equal(listVar.name, 'bar');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('lookupOrCreateList returns list if one with given id exists', t => {
|
||||
const target = new Target();
|
||||
const variables = target.variables;
|
||||
|
||||
t.equal(Object.keys(variables).length, 0);
|
||||
target.createVariable('foo', 'bar', Variable.LIST_TYPE);
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
|
||||
const listVar = target.lookupOrCreateList('foo', 'bar');
|
||||
t.equal(Object.keys(variables).length, 1);
|
||||
t.equal(listVar.id, 'foo');
|
||||
t.equal(listVar.name, 'bar');
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -139,7 +139,7 @@ test('goToNextBlock', t => {
|
||||
block: 'fakeBlock',
|
||||
name: 'STEPS',
|
||||
next: null,
|
||||
opcode: 'procedures_callnoreturn',
|
||||
opcode: 'procedures_call',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
@@ -194,7 +194,7 @@ test('stopThisScript', t => {
|
||||
block: 'fakeBlock',
|
||||
name: 'STEPS',
|
||||
next: null,
|
||||
opcode: 'procedures_callnoreturn',
|
||||
opcode: 'procedures_call',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
@@ -246,7 +246,7 @@ test('isRecursiveCall', t => {
|
||||
block: 'fakeBlock',
|
||||
name: 'STEPS',
|
||||
next: null,
|
||||
opcode: 'procedures_callnoreturn',
|
||||
opcode: 'procedures_call',
|
||||
mutation: {proccode: 'fakeCode'},
|
||||
parent: null,
|
||||
shadow: false,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
const test = require('tap').test;
|
||||
const Music = require('../../src/extensions/scratch3_music/index.js');
|
||||
let playedDrum;
|
||||
let playedInstrument;
|
||||
const runtime = {
|
||||
audioEngine: {
|
||||
numInstruments: 3,
|
||||
instrumentPlayer: {
|
||||
loadInstrument: instrument => (playedInstrument = instrument)
|
||||
}
|
||||
}
|
||||
};
|
||||
const blocks = new Music(runtime);
|
||||
blocks._playDrumNum = (util, drum) => (playedDrum = drum);
|
||||
|
||||
const util = {
|
||||
stackFrame: Object.create(null),
|
||||
target: {
|
||||
audioPlayer: null
|
||||
},
|
||||
yield: () => null
|
||||
};
|
||||
|
||||
test('playDrum uses 1-indexing and wrap clamps', t => {
|
||||
let args = {DRUM: 1};
|
||||
blocks.playDrumForBeats(args, util);
|
||||
t.strictEqual(playedDrum, 0);
|
||||
|
||||
args = {DRUM: blocks.DRUM_INFO.length + 1};
|
||||
blocks.playDrumForBeats(args, util);
|
||||
t.strictEqual(playedDrum, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('setInstrument uses 1-indexing and wrap clamps', t => {
|
||||
// Stub getMusicState
|
||||
blocks._getMusicState = () => ({});
|
||||
|
||||
let args = {INSTRUMENT: 1};
|
||||
blocks.setInstrument(args, util);
|
||||
t.strictEqual(playedInstrument, 0);
|
||||
|
||||
args = {INSTRUMENT: runtime.audioEngine.numInstruments + 1};
|
||||
blocks.setInstrument(args, util);
|
||||
t.strictEqual(playedInstrument, 0);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -20,7 +20,7 @@ test('default', t => {
|
||||
|
||||
// Create runtime instance & load SB2 into it
|
||||
const rt = new Runtime();
|
||||
sb2.deserialize(json, rt).then(targets => {
|
||||
sb2.deserialize(json, rt).then(({targets}) => {
|
||||
// Test
|
||||
t.type(file, 'string');
|
||||
t.type(json, 'object');
|
||||
@@ -60,7 +60,7 @@ test('data scoping', t => {
|
||||
|
||||
// Create runtime instance & load SB2 into it
|
||||
const rt = new Runtime();
|
||||
sb2.deserialize(json, rt).then(targets => {
|
||||
sb2.deserialize(json, rt).then(({targets}) => {
|
||||
const globalVariableIds = Object.keys(targets[0].variables);
|
||||
const localVariableIds = Object.keys(targets[1].variables);
|
||||
t.equal(targets[0].variables[globalVariableIds[0]].name, 'foo');
|
||||
|
||||
@@ -14,8 +14,8 @@ test('serialize', t => {
|
||||
|
||||
test('deserialize', t => {
|
||||
const vm = new VirtualMachine();
|
||||
sb3.deserialize('', vm.runtime).then(targets => {
|
||||
// @todo Analyize
|
||||
sb3.deserialize('', vm.runtime).then(({targets}) => {
|
||||
// @todo Analyze
|
||||
t.type(targets, 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const test = require('tap').test;
|
||||
const VirtualMachine = require('../../src/virtual-machine.js');
|
||||
const Sprite = require('../../src/sprites/sprite.js');
|
||||
|
||||
test('renameSprite throws when there is no sprite with that id', t => {
|
||||
const vm = new VirtualMachine();
|
||||
@@ -110,6 +111,173 @@ test('renameSprite does not increment when renaming to the same name', t => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteSprite throws when used on a non-sprite target', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.runtime.targets = [{
|
||||
id: 'id',
|
||||
isSprite: () => false
|
||||
}];
|
||||
t.throws(
|
||||
(() => vm.deleteSprite('id')),
|
||||
new Error('Cannot delete non-sprite targets.')
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteSprite throws when there is no sprite for the given target', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.runtime.targets = [{
|
||||
id: 'id',
|
||||
isSprite: () => true,
|
||||
sprite: null
|
||||
}];
|
||||
t.throws(
|
||||
(() => vm.deleteSprite('id')),
|
||||
new Error('No sprite associated with this target.')
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteSprite throws when there is no target with given id', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.runtime.targets = [{
|
||||
id: 'id',
|
||||
isSprite: () => true,
|
||||
sprite: {
|
||||
name: 'this name'
|
||||
}
|
||||
}];
|
||||
t.throws(
|
||||
(() => vm.deleteSprite('id1')),
|
||||
new Error('No target with the provided id.')
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteSprite deletes a sprite when given id is associated with a known sprite', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const spr = new Sprite(null, vm.runtime);
|
||||
const currTarget = spr.createClone();
|
||||
|
||||
vm.runtime.targets = [currTarget];
|
||||
|
||||
t.equal(currTarget.sprite.clones.length, 1);
|
||||
vm.deleteSprite(currTarget.id);
|
||||
t.equal(currTarget.sprite.clones.length, 0);
|
||||
t.end();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
test('deleteSprite sets editing target as null when given sprite is current editing target, and the only target in the runtime', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const spr = new Sprite(null, vm.runtime);
|
||||
const currTarget = spr.createClone();
|
||||
|
||||
vm.editingTarget = currTarget;
|
||||
vm.runtime.targets = [currTarget];
|
||||
|
||||
vm.deleteSprite(currTarget.id);
|
||||
|
||||
t.equal(vm.runtime.targets.length, 0);
|
||||
t.equal(vm.editingTarget, null);
|
||||
t.end();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
test('deleteSprite updates editingTarget when sprite being deleted is current editing target, and there is another target in the runtime', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const spr1 = new Sprite(null, vm.runtime);
|
||||
const spr2 = new Sprite(null, vm.runtime);
|
||||
const currTarget = spr1.createClone();
|
||||
const otherTarget = spr2.createClone();
|
||||
|
||||
vm.emitWorkspaceUpdate = () => null;
|
||||
|
||||
vm.runtime.targets = [currTarget, otherTarget];
|
||||
vm.editingTarget = currTarget;
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
vm.deleteSprite(currTarget.id);
|
||||
t.equal(vm.runtime.targets.length, 1);
|
||||
t.equal(vm.editingTarget.id, otherTarget.id);
|
||||
|
||||
// now let's try them in the other order in the runtime.targets list
|
||||
|
||||
// can't reuse deleted targets
|
||||
const currTarget2 = spr1.createClone();
|
||||
const otherTarget2 = spr2.createClone();
|
||||
|
||||
vm.runtime.targets = [otherTarget2, currTarget2];
|
||||
vm.editingTarget = currTarget2;
|
||||
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
vm.deleteSprite(currTarget2.id);
|
||||
t.equal(vm.editingTarget.id, otherTarget2.id);
|
||||
t.equal(vm.runtime.targets.length, 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateSprite throws when there is no target with given id', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.runtime.targets = [{
|
||||
id: 'id',
|
||||
isSprite: () => true,
|
||||
sprite: {
|
||||
name: 'this name'
|
||||
}
|
||||
}];
|
||||
t.throws(
|
||||
(() => vm.duplicateSprite('id1')),
|
||||
new Error('No target with the provided id')
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateSprite throws when used on a non-sprite target', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.runtime.targets = [{
|
||||
id: 'id',
|
||||
isSprite: () => false
|
||||
}];
|
||||
t.throws(
|
||||
(() => vm.duplicateSprite('id')),
|
||||
new Error('Cannot duplicate non-sprite targets.')
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateSprite throws when there is no sprite for the given target', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.runtime.targets = [{
|
||||
id: 'id',
|
||||
isSprite: () => true,
|
||||
sprite: null
|
||||
}];
|
||||
t.throws(
|
||||
(() => vm.duplicateSprite('id')),
|
||||
new Error('No sprite associated with this target.')
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('duplicateSprite duplicates a sprite when given id is associated with known sprite', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const spr = new Sprite(null, vm.runtime);
|
||||
const currTarget = spr.createClone();
|
||||
vm.editingTarget = currTarget;
|
||||
|
||||
vm.emitWorkspaceUpdate = () => null;
|
||||
|
||||
vm.runtime.targets = [currTarget];
|
||||
t.equal(vm.runtime.targets.length, 1);
|
||||
vm.duplicateSprite(currTarget.id).then(() => {
|
||||
t.equal(vm.runtime.targets.length, 2);
|
||||
t.end();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('emitWorkspaceUpdate', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.runtime.targets = [
|
||||
|
||||
+12
-10
@@ -20,12 +20,12 @@ const base = {
|
||||
}
|
||||
}]
|
||||
},
|
||||
plugins: [
|
||||
plugins: process.env.NODE_ENV === 'production' ? [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
include: /\.min\.js$/,
|
||||
minimize: true
|
||||
})
|
||||
]
|
||||
] : []
|
||||
};
|
||||
|
||||
module.exports = [
|
||||
@@ -60,7 +60,13 @@ module.exports = [
|
||||
libraryTarget: 'commonjs2',
|
||||
path: path.resolve(__dirname, 'dist/node'),
|
||||
filename: '[name].js'
|
||||
}
|
||||
},
|
||||
plugins: base.plugins.concat([
|
||||
new CopyWebpackPlugin([{
|
||||
from: './src/extensions/scratch3_music/assets',
|
||||
to: 'assets/scratch3_music'
|
||||
}])
|
||||
])
|
||||
}),
|
||||
// Playground
|
||||
defaultsDeep({}, base, {
|
||||
@@ -77,9 +83,7 @@ module.exports = [
|
||||
// Audio
|
||||
'scratch-audio',
|
||||
// Renderer
|
||||
'scratch-render',
|
||||
// Storage
|
||||
'scratch-storage'
|
||||
'scratch-render'
|
||||
]
|
||||
},
|
||||
output: {
|
||||
@@ -111,10 +115,6 @@ module.exports = [
|
||||
{
|
||||
test: require.resolve('scratch-render'),
|
||||
loader: 'expose-loader?RenderWebGL'
|
||||
},
|
||||
{
|
||||
test: require.resolve('scratch-storage'),
|
||||
loader: 'expose-loader?Scratch.Storage'
|
||||
}
|
||||
])
|
||||
},
|
||||
@@ -124,6 +124,8 @@ module.exports = [
|
||||
to: 'media'
|
||||
}, {
|
||||
from: 'node_modules/highlightjs/styles/zenburn.css'
|
||||
}, {
|
||||
from: 'node_modules/scratch-storage/dist/web'
|
||||
}, {
|
||||
from: 'src/playground'
|
||||
}])
|
||||
|
||||
Reference in new issue
Block a user